Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions cli/cmd/agentplugins/cursor_project_fixture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package main

import (
"context"
"path/filepath"
"strings"
"testing"

"github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli"
"github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/sourceacquisition"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/ports"
)

type cursorProjectDetector struct{ root string }

func (detector cursorProjectDetector) Detect(context.Context) ([]domain.DetectedClient, error) {
return []domain.DetectedClient{{ClientID: domain.ClientCursor, ConfigRoot: detector.root, Status: domain.DetectionDetected}}, nil
}

// Reuse the core journey's production CLI/state helpers and real TLS Git/ZIP
// transports. Cursor supplies its own detected profile and native layout.
type cursorProjectJourney struct{ projectJourney }

func newCursorProjectJourney(t *testing.T, transport *projectJourneyTransport) cursorProjectJourney {
t.Helper()
f := cursorProjectJourney{projectJourney{home: t.TempDir(), a: t.TempDir(), b: t.TempDir(), transport: transport}}
f.cache = filepath.Join(f.home, "uap")
for _, root := range []string{"", f.a, f.b} {
native := f.cursorRoot(root)
projectJourneyWrite(t, filepath.Join(native, "mcp.json"), `{"mcpServers":{"foreign":{"url":"https://foreign.test/mcp","disabled":true}},"foreignPreference":"keep"}`)
projectJourneyWrite(t, filepath.Join(native, "skills", "foreign", "SKILL.md"), "foreign skill\n")
projectJourneyWrite(t, filepath.Join(native, "hooks.json"), "foreign hooks\n")
}
registry, err := specregistry.New()
if err != nil {
t.Fatal(err)
}
app, err := newAgentpluginsCLIApp(f.home, f.cache, nil, nil, nil, registry)
if err != nil {
t.Fatal(err)
}
f.app = configureCursorProjectJourney(app, f.home, transport)
return f
}

// Preserve the production scoped composer, installer, ownership kernel, store,
// registry and solver. Only host discovery and security assessment are inert.
func configureCursorProjectJourney(app agentpluginscli.App, home string, transport *projectJourneyTransport) agentpluginscli.App {
app.Detector = cursorProjectDetector{root: filepath.Join(home, ".cursor")}
app.SecurityIndex, app.DirectoryClient, app.DiscoveryClient = nil, nil, nil
app.SecurityEvaluator = projectJourneySecurity{}
app.SecurityEvaluatorForDryRun = func(string) ports.PackageSecurityEvaluator { return projectJourneySecurity{} }
app.SourceAcquirer = sourceacquisition.Acquirer{TempRoot: home, HTTPClient: transport.server.Client()}
app.SourceAcquirerForDryRun = func(root string) agentpluginscli.SourceAcquirer {
return sourceacquisition.Acquirer{TempRoot: root, HTTPClient: transport.server.Client()}
}
app.DirectoryClientForDryRun, app.CommitDirectoryPreview = nil, nil
compose := app.ForScope
app.ForScope = func(scope domain.InstallScope, root string) (agentpluginscli.App, error) {
selected, err := compose(scope, root)
if err != nil {
return selected, err
}
return configureCursorProjectJourney(selected, home, transport), nil
}
return app
}

func (f cursorProjectJourney) cursorRoot(project string) string {
if project == "" {
return filepath.Join(f.home, ".cursor")
}
return filepath.Join(project, ".cursor")
}

func (f cursorProjectJourney) assertCursorOwned(t *testing.T, project, skill, endpoint string) (domain.Installation, domain.ClientBinding) {
t.Helper()
installation, binding := f.owned(t, project)
if binding.Scope != string(domain.ScopeProject) || binding.NativeProfileRoot != f.cursorRoot(project) || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled {
t.Fatalf("Cursor project lifecycle lost its exact scope or conservative observation: %+v", binding)
}
if err := pathpolicy.RequireContainedChild(filepath.Join(project, ".uap"), binding.TargetLocator); err != nil {
t.Fatal(err)
}
for _, root := range []string{binding.TargetLocator, f.cursorRoot(project)} {
if projectJourneyRead(t, filepath.Join(root, "skills", "docs", "SKILL.md")) != skill {
t.Fatal("Cursor managed/native skill bytes differ from the selected pin")
}
}
settings := projectJourneyRead(t, filepath.Join(f.cursorRoot(project), "mcp.json"))
for _, text := range []string{endpoint, "foreign.test", "foreignPreference", `"disabled":true`} {
if !strings.Contains(strings.ReplaceAll(settings, " ", ""), text) {
t.Fatalf("Cursor settings lost selected or foreign member %q: %s", text, settings)
}
}
return installation, binding
}

// Select inert remote configuration so the composed proof never executes an
// MCP command, copies a test binary as a launcher, or starts a native client.
func cursorProjectCommitSource(t *testing.T, transport *projectJourneyTransport, skill, endpoint string) {
t.Helper()
projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), skill)
projectJourneyWrite(t, filepath.Join(transport.repo, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"`+endpoint+`"}}}`)
projectJourneyGit(t, transport.repo, "add", ".")
projectJourneyGit(t, transport.repo, "commit", "-m", "select inert Cursor project components")
projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main")
transport.mu.Lock()
defer transport.mu.Unlock()
transport.archive = projectJourneyArchive(t, transport.repo)
}
102 changes: 102 additions & 0 deletions cli/cmd/agentplugins/cursor_project_lifecycle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)

// This is the production CLI/dependency/installer/native-filesystem composition
// with real loopback TLS Git and ZIP acquisition. Native Cursor discovery,
// enablement, MCP initialization, model use and runtime are not certified.
func TestProjectCursorComposedSourceLifecycle(t *testing.T) {
for _, kind := range []string{"git", "zip"} {
t.Run(kind, func(t *testing.T) {
transport := newProjectJourneyTransport(t, kind)
cursorProjectCommitSource(t, transport, projectJourneySkill, "https://managed.test/v1")
f := newCursorProjectJourney(t, transport)
userBefore := securityFixtureTree(t, f.cursorRoot(""))
for _, root := range []string{f.a, f.b} {
f.run(t, root, "add", transport.endpoint, "--alias", "docs", "--target", "cursor")
f.assertCursorOwned(t, root, projectJourneySkill, "https://managed.test/v1")
}
bNative := securityFixtureTree(t, f.cursorRoot(f.b))
bState := projectJourneyRead(t, filepath.Join(f.namespace(f.b), "state-v2.json"))
cursorProjectReconcileAndRepair(t, f)
cursorProjectUpdateAndTransfer(t, f)
cursorProjectRemove(t, f)
if !reflect.DeepEqual(userBefore, securityFixtureTree(t, f.cursorRoot(""))) || !reflect.DeepEqual(bNative, securityFixtureTree(t, f.cursorRoot(f.b))) || bState != projectJourneyRead(t, filepath.Join(f.namespace(f.b), "state-v2.json")) {
t.Fatal("Cursor project-A lifecycle changed user or project-B native configuration/state")
}
f.assertCursorOwned(t, f.b, projectJourneySkill, "https://managed.test/v1")
})
}
}

func cursorProjectReconcileAndRepair(t *testing.T, f cursorProjectJourney) {
t.Helper()
statePath := filepath.Join(f.namespace(f.a), "state-v2.json")
before := projectJourneyRead(t, statePath)
f.run(t, f.a, "deps", "sync", "--locked", "--yes")
if before != projectJourneyRead(t, statePath) {
t.Fatal("Cursor locked no-op changed exact ownership receipts")
}
installation, _ := f.owned(t, f.a)
native := securityFixtureTree(t, f.cursorRoot(f.a))
for _, args := range [][]string{{"list"}, {"info", installation.InstallationID}, {"doctor", installation.InstallationID}, {"outdated", installation.InstallationID}, {"deps", "status"}, {"deps", "tree"}} {
f.run(t, f.a, args...)
}
if before != projectJourneyRead(t, statePath) || !reflect.DeepEqual(native, securityFixtureTree(t, f.cursorRoot(f.a))) {
t.Fatal("Cursor project read commands changed native files or ownership receipts")
}
if err := os.RemoveAll(filepath.Join(f.cursorRoot(f.a), "skills", "docs")); err != nil {
t.Fatal(err)
}
f.run(t, f.a, "repair", installation.InstallationID, "--target", "cursor")
f.assertCursorOwned(t, f.a, projectJourneySkill, "https://managed.test/v1")
f.inspectNoRecovery(t, f.a)
}

func cursorProjectUpdateAndTransfer(t *testing.T, f cursorProjectJourney) {
t.Helper()
previous, binding := f.owned(t, f.a)
cursorProjectCommitSource(t, f.transport, projectJourneyAdvancedSkill, "https://managed.test/v2")
f.run(t, f.a, "update", "docs", "--target", "cursor")
updated, selected := f.assertCursorOwned(t, f.a, projectJourneyAdvancedSkill, "https://managed.test/v2")
if updated.Source.SourceBindingID != previous.Source.SourceBindingID || updated.Source.TreeDigest == previous.Source.TreeDigest || selected.ClientBindingID != binding.ClientBindingID || selected.TargetLocator != binding.TargetLocator {
t.Fatal("Cursor update did not retain source/binding authority while advancing the pin")
}
pair := f.assertPortablePair(t, f.a)
exchange := t.TempDir()
f.run(t, f.a, "deps", "export", exchange, "--yes")
if pair != f.assertPortablePair(t, exchange) {
t.Fatal("Cursor export changed exact portable pins")
}
cold := newCursorProjectJourney(t, f.transport)
projectJourneyAbsent(t, cold.cache, filepath.Join(cold.a, ".uap"))
cold.run(t, cold.a, "deps", "import", exchange, "--install", "--yes")
restored, _ := cold.assertCursorOwned(t, cold.a, projectJourneyAdvancedSkill, "https://managed.test/v2")
if restored.Source.SourceBindingID != updated.Source.SourceBindingID || restored.Source.TreeDigest != updated.Source.TreeDigest || pair != cold.assertPortablePair(t, cold.a) {
t.Fatal("Cursor cold import changed source identity or portable pins")
}
cold.inspectNoRecovery(t, cold.a)
}

func cursorProjectRemove(t *testing.T, f cursorProjectJourney) {
t.Helper()
_, binding := f.owned(t, f.a)
f.run(t, f.a, "remove", "docs", "--target", "cursor")
projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(f.cursorRoot(f.a), "skills", "docs"))
settings := projectJourneyRead(t, filepath.Join(f.cursorRoot(f.a), "mcp.json"))
if strings.Contains(settings, "managed.test") || !strings.Contains(settings, "foreign.test") || !strings.Contains(settings, "foreignPreference") {
t.Fatalf("Cursor removal changed foreign MCP settings or retained its entry: %s", settings)
}
if projectJourneyRead(t, filepath.Join(f.cursorRoot(f.a), "skills", "foreign", "SKILL.md")) != "foreign skill\n" || projectJourneyRead(t, filepath.Join(f.cursorRoot(f.a), "hooks.json")) != "foreign hooks\n" {
t.Fatal("Cursor removal changed foreign skill or hooks")
}
if len(f.state(t, f.a).EnvironmentReceipts) != 0 {
t.Fatal("Cursor removal retained an environment ownership receipt")
}
}
11 changes: 11 additions & 0 deletions docs/plans/project-install-e2e/client-plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,17 @@ editor or official `cursor-agent`/`agent` version must load project MCP/skill
in A and not B; CLI and UI evidence are separate. No genuine Stop or plugin
discovery claim follows from this component work.

### Implemented component contract checkpoint

The Cursor child stack implements native project component ownership at
`.cursor/skills` and `.cursor/mcp.json`, using `.uap` managed artifacts and the
existing kernel/managed stdio launcher. User package preparation remains manual.
Production loader/planner/renderer/stager/provider contracts pass for scopes,
updates, missing-same-pin repair, exact removal and failure preservation. Project
readiness remains prepared+installed; native session discovery is unobserved
and attestation cannot upgrade it. See `cursor-implementation-evidence.md` for
current official docs, pinned read-only CLI help and explicit native proof limits.

## 4. Gemini CLI — native project skills and settings

**Evidence/choice.** Project settings are `.gemini/settings.json`; project skills
Expand Down
114 changes: 114 additions & 0 deletions docs/plans/project-install-e2e/cursor-code-review-r1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Cursor project adapter independent implementation review R1

Reviewer: codex_claude, 2026-10-23.
Reviewed adapter commit `524b808b401d29ebe5f932dc7ce0f5258746f31d`
in `work/uap-project-cursor`. The shared ProjectComponents helper already has
separate independent implementation reviews; this review concentrates on the
final Cursor adapter, production composition, projection and lifecycle.

## Decision: GO for the configured project component implementation

No correctness blocker found in the reviewed adapter diff. This decision does
not qualify Cursor native discovery, an MCP handshake or tool result, fresh
native-session behavior, plugin installation, genuine Stop, or release readiness.
The implementation and evidence correctly keep those observations pending.

## Reviewed boundaries

- Project scope selects exact `<project>/.cursor` native authority while the
detected user ConfigRoot remains distinct. Planning, activation, inspection,
binding validation and removal revalidate the selected project/native root;
mismatching scope, original user root and copied project context refuse.
Prepared package state remains under the selected project's `.uap` namespace.
- Project policy is declared through core's scoped native-config lifecycle
traits. Existing user preparation and its CLI-registry traits remain intact;
project mutations do not invoke a user CLI or redirect HOME/ConfigRoot.
- Selected skills, including existing rendered command skills, receive exact
directory ownership. MCP projection goes through the existing exact-entry
kernel. The adapter strips only the shared managed-package receipt before
handing objects to the native component helper; unknown ownership kinds are
retained for closed refusal. Removal never treats a shared config as owned.
- Stdio projection uses the trusted managed launcher with absolute package,
persistent-data and cwd bindings. Active reprojection reads already-bound
staged bytes without executing or mutating them. Remote Cursor native config
uses the documented URL/headers shape. The official MCP documentation permits
the URL-only remote shape for HTTP/SSE; no native execution claim follows.
- Automatic file success is downgraded from shared activation success to
`prepared` plus `installation_verified`, with discovery, aliases, enablement,
approvals, policy and MCP initialization explicitly unobserved. Generic
`ActivationComplete` is rejected by preflight before provider shortcut
admission and also by direct adapter reuse, including prepared selections.
- Provider contracts cover unchanged verification, update, missing-same-pin
repair, withdrawn MCP, repeated remove, persistent data, malformed JSON,
foreign disabled/member/skill collisions, writer failure rollback, contained
alias refusal, and user/project-A/project-B byte-and-mode isolation. These are
loader/planner/stager/provider fixture contracts; they are not a claim that
native Cursor was run or that a hosted all-command journey has completed.

## Independent verification

Using the shared Go 1.26 toolchain and bounded existing caches:

- Complete `clients/cursor` package: PASS, 0.141s.
- `providers -run 'TestProjectCursor|TestCursor' -count=1`: PASS, 0.168s;
this selected all newly added composed Cursor project contracts. The matching
pattern selected no client-package tests, so the complete client package was
run separately as recorded above.

Inspected the production activation preflight/attestation order, shared MCP
projection and nativeconfig neutral projection, scoped domain declaration,
existing user projector and all four new provider fixtures. Consulted current
official `https://cursor.com/docs/mcp`, which documents project/global config
locations, URL/headers remote entries and native enablement/approval boundaries.
No Cursor process, model task, package payload, trust action, login, configured
MCP command or launcher was executed by this review.

## Remaining evidence boundary

The authored fixture proof and pinned official help do not establish runtime
discovery or initialization. A genuine pinned Cursor session in the approved
disposable-host workflow remains the separate gate for native observations.
Public reporting must keep this adapter's configured/prepared status and those
pending native observations distinct. A separate independent R2 review is still
required for the final adapter checkpoint.

## Final R1 selected-data correction follow-up: GO at f55dc462

Independently reviewed the narrow corrective commit `f55dc462` after the root
identified the analogous stale selected-data authority gap. Project active
reprojection now validates each selected stdio entry against the exact managed
launcher path, selected active package, selected data receipt and resolved cwd
anchor. Both native PLUGIN_ROOT/PLUGIN_DATA values and the launcher's six-field
package/data/cwd protocol prefix must agree; missing, changed or stale values
return ErrProjectionRefreshRequired. This is read-only validation of already
digest-verified package bytes. Existing user projection and remote-only native
entries retain their prior contract.

The existing composed stdio fixture now refuses an empty locator and a changed
selected receipt, while comparing the complete immutable active-package
inventory after each refusal. It still accepts the exact selected authority.
Independent `providers -run '^TestProjectCursor' -count=1` **PASS 0.065s**
(Go1.26.10/GOMAXPROCS3/-p2). The same pattern selected no client tests, which is
recorded rather than presented as new full-client execution; the earlier full
client run remains recorded above. No blocker remains in this correction.
Neither review execution launched Cursor, its launcher or an MCP process, and
the genuine hosted native boundary is unchanged. The independent R2 author
has separately recorded GO for the same corrective checkpoint.

## Scoped hosted test-selection closure: GO at 32c33c6f

Independently reviewed `32c33c6fa96d790396a6fc1be3f1a36537ef4c9c`
after the reviewed project-stack CI baseline merge `935e3e08`. The complete
source file is byte-identical after replacing only the function symbol
TestCursorProjectComposedSourceLifecycle with
TestProjectCursorComposedSourceLifecycle; no fixture or assertion changed.
The adjacent evidence accurately updates its test name and records pending
hosted/native results.

Extracted the actual project-contracts workflow regex
`Test(TargetContext|Project|PrepareRejectsCrossEnvironmentContext)` and checked
that it selects the corrected name while excluding the previous spelling.
That job includes the public CLI package on Linux/macOS/Windows. No broad
compile or rerun is needed for this literal symbol-only correction, and none
was represented as fresh execution. The bounded selection correction is GO;
real cross-platform/native outcomes remain their separate hosted gates.
Loading
Loading