Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/project-native-research.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
paths:
- '.github/workflows/project-native-research.yml'
- 'scripts/project_native_research.py'
- 'scripts/project_native_cursor_acquisition.py'
- 'scripts/project_native_service.py'
- 'scripts/project_native_collectors/**'
- 'scripts/test_project_native_*.py'
Expand Down
256 changes: 256 additions & 0 deletions docs/plans/project-install-e2e/remaining-native-qualification.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package clientdetect

import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/all"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
)

// These are inert host probes through the actual registries and detector, not
// execution of an editor, Agent, package payload or native-registry command.
func TestCursorProjectRegistrySelectionPreservesUserAuthority(t *testing.T) {
for _, row := range []struct {
name string
editor, agent bool
config, invalid bool
}{
{name: "agent", agent: true},
{name: "editor", editor: true},
{name: "both", editor: true, agent: true},
{name: "config", config: true},
{name: "absent"},
{name: "invalid-profile", editor: true, agent: true, invalid: true},
} {
t.Run(row.name, func(t *testing.T) {
home := cursorSelectionHome(t, row.config, row.invalid)
binaries := map[string]string{}
if row.editor {
binaries["cursor"] = filepath.Join(home, "editor")
}
if row.agent {
binaries["cursor-agent"] = filepath.Join(home, "agent")
}
user := cursorSelectionClient(t, cursorSelectionDetector(home, all.Default(), binaries), false)
project := cursorSelectionClient(t, cursorSelectionDetector(home, all.Project(), binaries), false)
assertCursorSelection(t, user, row.editor && !row.invalid, row.invalid, home, binaries["cursor"])
assertCursorSelection(t, project, (row.editor || row.agent) && !row.invalid, row.invalid, home, binaries["cursor"])
if !reflect.DeepEqual(user.Surfaces, project.Surfaces) {
t.Fatal("project selection changed raw editor/Agent/profile evidence")
}
})
}
}

func TestCursorProjectAgentSelectionNeverBorrowsExecutableOrVersion(t *testing.T) {
home := cursorSelectionHome(t, false, false)
detector := cursorSelectionDetector(home, all.Project(), map[string]string{"cursor-agent": filepath.Join(home, "agent")})
detector.ProbeVersion = func(context.Context, string) (string, error) {
t.Fatal("Agent-only project selection invoked an editor version probe")
return "", nil
}
client := cursorSelectionClient(t, detector, true)
assertCursorSelection(t, client, true, false, home, "")
inspector, ok := clients.As[clients.RegistryInspector](all.Project(), domain.ClientCursor)
if !ok || inspector.UsesNativeRegistryExecutable() {
t.Fatal("project components gained a native executable registry probe")
}
adapter, ok := clients.As[clients.NativeRegistryLayout](all.Project(), domain.ClientCursor)
if !ok {
t.Fatal("project Cursor registry layout disappeared")
}
project := filepath.Join(home, "project")
if err := os.Mkdir(project, 0o700); err != nil {
t.Fatal(err)
}
root, executable := adapter.NativeRegistry(clients.PlanInput{Client: client, TargetContext: domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: project, UserConfigRoot: client.ConfigRoot}})
if root != filepath.Join(project, ".cursor") || executable != "" {
t.Fatalf("project native registry borrowed Agent identity: root=%q executable=%q", root, executable)
}
}

func TestCursorProjectRegistryKeepsPreparedAndAttestationBoundary(t *testing.T) {
home := cursorSelectionHome(t, false, false)
registry := all.Project()
client := cursorSelectionClient(t, cursorSelectionDetector(home, registry, map[string]string{"cursor-agent": filepath.Join(home, "agent")}), false)
project := filepath.Join(home, "project")
target := domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: project, UserConfigRoot: client.ConfigRoot}
plan := domain.DeliveryPlan{Scope: domain.ScopeProject, TargetContext: target, Status: domain.PlanReady,
NativeRegistryRoot: filepath.Join(project, ".cursor"),
Components: []domain.ComponentDecision{{Kind: domain.ComponentSkill, Name: "inert", Support: domain.SupportNative}}}
refiner, ok := clients.As[clients.PlanRefiner](registry, domain.ClientCursor)
if !ok {
t.Fatal("project Cursor planning capability disappeared")
}
if err := refiner.RefinePlan(context.Background(), clients.PlanInput{Client: client, TargetContext: target}, &plan); err != nil || plan.Activation != domain.ActivationPrepared {
t.Fatalf("Agent presence upgraded project configuration beyond Prepared: %+v, %v", plan, err)
}
preflight, ok := clients.As[clients.ActivationPreflighter](registry, domain.ClientCursor)
if !ok {
t.Fatal("project Cursor activation preflight disappeared")
}
err := preflight.PreflightActivation(clients.Env{}, domain.ActivationRequest{Client: client, Plan: plan, ActivationComplete: true})
if err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") {
t.Fatalf("Agent presence bypassed native discovery attestation refusal: %v", err)
}
}

func cursorSelectionHome(t *testing.T, config, invalid bool) string {
t.Helper()
home, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
root := filepath.Join(home, ".cursor")
if invalid {
if err := os.WriteFile(root, []byte("inert non-directory profile"), 0o600); err != nil {
t.Fatal(err)
}
} else if config {
if err := os.Mkdir(root, 0o700); err != nil {
t.Fatal(err)
}
}
return home
}

func cursorSelectionDetector(home string, registry *clients.Registry, binaries map[string]string) Detector {
return Detector{
HomeDir: home, WorkingDir: home, GOOS: "linux", Registry: registry,
Lstat: os.Lstat, ReadDir: os.ReadDir, EvalSymlinks: filepath.EvalSymlinks,
LookPath: func(name string) (string, error) {
if path := binaries[name]; path != "" {
return path, nil
}
return "", fmt.Errorf("inert absent binary %s", name)
},
}
}

func cursorSelectionClient(t *testing.T, detector Detector, probe bool) domain.DetectedClient {
t.Helper()
var detected []domain.DetectedClient
var err error
if probe {
detected, err = detector.DetectTargetsWithVersionProbe(context.Background(), []domain.ClientID{domain.ClientCursor})
} else {
detected, err = detector.Detect(context.Background())
}
if err != nil {
t.Fatal(err)
}
for _, client := range detected {
if client.ClientID == domain.ClientCursor {
return client
}
}
t.Fatal("real registry lost Cursor")
return domain.DetectedClient{}
}

func assertCursorSelection(t *testing.T, client domain.DetectedClient, detected, invalid bool, home, editor string) {
t.Helper()
if (client.Status == domain.DetectionDetected) != detected || (client.DetectionError != nil) != invalid {
t.Fatalf("Cursor selection or profile-error precedence changed: %+v", client)
}
if invalid {
if client.ConfigRoot != "" || client.ExecutablePath != "" || client.Version != "" || len(client.Surfaces) != 0 {
t.Fatalf("invalid Cursor profile retained authority: %+v", client)
}
return
}
if client.ConfigRoot != filepath.Join(home, ".cursor") || client.ExecutablePath != editor || client.Version != "" {
t.Fatalf("Cursor project selection replaced editor/profile identity: %+v", client)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ var projectRegistry = sync.OnceValue(func() *clients.Registry { return buildRegi

func buildRegistry(project bool) *clients.Registry {
var workspace clients.Adapter = vscode.New()
var cursorComponents clients.Adapter = cursor.New()
if project {
workspace = vscode.NewProject()
cursorComponents = cursor.NewProject()
}
registry, err := clients.NewRegistry(
pi.New(),
codex.New(),
chatgpt.New(),
cursor.New(),
cursorComponents,
copilot.New(),
workspace,
kiro.New(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,8 @@ These source/attributed startup observations prove no genuine Stop, native MCP
request, restart discovery, Windows native I/O, or release availability.
S4 needs the supplied official artifact to be available for an isolated TEST
native discovery run. Independent code review precedes public delivery.

Project MCP stdio entries use the private launcher's explicit encoded argv form
to preserve bundled commands and opaque arguments through Cursor normalization.
The same strict decoder validates installed entries and execution; selected
package/data/cwd authority and legacy raw entries retain their existing checks.
14 changes: 11 additions & 3 deletions install/integrationctl/agentplugins/clients/cursor/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import (
)

// Adapter implements the client contract for Cursor.
type Adapter struct{}
type Adapter struct{ projectComponents bool }

// New builds the adapter. The composition root registers it in clients/all.
func New() *Adapter { return &Adapter{} }

// NewProject selects project components using either the editor or Agent
// surface. It does not qualify native discovery or replace editor identity.
func NewProject() *Adapter { return &Adapter{projectComponents: true} }

var (
_ clients.Adapter = (*Adapter)(nil)
_ clients.HostDetector = (*Adapter)(nil)
Expand All @@ -25,7 +29,7 @@ func (*Adapter) ID() domain.ClientID { return domain.ClientCursor }
// DetectSurfaces reports editor and agent binary presence separately. The
// retained package preparation contract is for the editor; neither presence
// nor a leftover directory qualifies native agent plugins or Stop events.
func (*Adapter) DetectSurfaces(host clients.Host) clients.Detection {
func (adapter *Adapter) DetectSurfaces(host clients.Host) clients.Detection {
configRoot, err := detectedProfileRoot(host)
if err != nil {
return clients.Detection{Err: err}
Expand All @@ -43,10 +47,14 @@ func (*Adapter) DetectSurfaces(host clients.Host) clients.Detection {
} else if host.GOOS() == "linux" {
surfaces = append(surfaces, host.LinuxDesktopSurface("cursor_desktop", "cursor.desktop"))
}
selection := []string{"cursor_editor", "cursor_desktop"}
if adapter.projectComponents {
selection = append(selection, "cursor_agent")
}
return clients.Detection{
ConfigRoot: configRoot,
ExecutablePath: editor,
Surfaces: surfaces,
SelectionSurfaceIDs: []string{"cursor_editor", "cursor_desktop"},
SelectionSurfaceIDs: selection,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func bindCursorStdio(entry map[string]any, server domain.MCPServer, activePath,
command = "./" + strings.TrimPrefix(command, "${package.root}/")
}
entry["command"] = filepath.Join(activePath, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName)
entry["args"] = managedstdio.Arguments(activePath, dataPath, absoluteCWD, cwd.Anchor, command, args)
entry["args"] = managedstdio.EncodedArguments(activePath, dataPath, absoluteCWD, cwd.Anchor, command, args)
delete(entry, "cwd")
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cursor

import (
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/pathcontract"
)

func TestProjectStdioPreservesOpaqueVectorInEncodedProjection(t *testing.T) {
active, data := filepath.Join(testBase(t), "package"), filepath.Join(testBase(t), "data")
args := []any{"", ".", "./relative", "~/relative", "${UNKNOWN}", "雪", "\r\n"}
entry := map[string]any{"cwd": active, "args": args}
source := domain.MCPServer{Type: "stdio", Decoded: map[string]any{"command": "./bin/probe", "cwd": "${PLUGIN_ROOT}"}}
if err := bindCursorStdio(entry, source, active, data); err != nil {
t.Fatal(err)
}
wire, ok := entry["args"].([]string)
if !ok || len(wire) != 14 || wire[5] != "--argv-base64-v1" || wire[6] != "Li9iaW4vcHJvYmU=" {
t.Fatal("Cursor did not project the explicit encoded transport")
}
decoded, err := managedstdio.DecodeCommandArguments(wire[5], wire[6:])
want := []string{"./bin/probe", "", ".", "./relative", "~/relative", "${UNKNOWN}", "雪", "\r\n"}
if err != nil || !reflect.DeepEqual(decoded, want) {
t.Fatal("Cursor projected child vector changed", err)
}
if _, exists := entry["cwd"]; exists || entry["command"] != filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) {
t.Fatal("Cursor bypassed managed cwd/launcher authority")
}
}

func TestProjectStdioVerificationAcceptsOwnedLegacyAndEncodedForms(t *testing.T) {
root := testBase(t)
active, data := filepath.Join(root, "active"), filepath.Join(root, "data")
for _, path := range []string{active, data} {
if err := os.Mkdir(path, 0700); err != nil {
t.Fatal(err)
}
}
envelope := domain.PackageEnvelope{MCP: domain.MCPComponent{Servers: map[string]domain.MCPServer{"probe": {Type: "stdio", Decoded: map[string]any{"command": "./bin/probe", "cwd": "${PLUGIN_ROOT}"}}}}}
plan := domain.DeliveryPlan{ActivePath: active, Components: []domain.ComponentDecision{{Kind: domain.ComponentMCPServer, Name: "probe", Support: domain.SupportNative}}}
legacy := managedstdio.Arguments(active, data, active, pathcontract.Plugin, "./bin/probe", []string{"./relative", "\n"})
encoded := managedstdio.EncodedArguments(active, data, active, pathcontract.Plugin, "./bin/probe", []string{"./relative", "\n"})
for _, fault := range []string{"legacy", "encoded", "mode", "plugin", "data", "cwd", "anchor", "separator", "missing-command", "malformed-command", "malformed-tail", "wire-newline", "decoded-nul"} {
t.Run(fault, func(t *testing.T) {
args := append([]string(nil), encoded...)
switch fault {
case "legacy":
args = append([]string(nil), legacy...)
case "mode":
args[0] = "--unknown"
case "plugin":
args[1] = filepath.Join(root, "foreign")
case "data":
args[2] = filepath.Join(root, "foreign")
case "cwd":
args[3] = data
case "anchor":
args[4] = "data"
case "separator":
args[5] = "--unknown"
case "missing-command":
args = args[:6]
case "malformed-command":
args[6] = "?"
case "malformed-tail":
args[len(args)-1] = "?"
case "wire-newline":
args[6] += "\n"
case "decoded-nul":
args[len(args)-1] = "AA=="
}
entry := map[string]any{"command": filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName), "args": args, "env": map[string]string{"PLUGIN_ROOT": active, "PLUGIN_DATA": data}}
body, err := json.Marshal(map[string]any{"mcpServers": map[string]any{"probe": entry}})
if err != nil || os.WriteFile(filepath.Join(active, "mcp.json"), body, 0600) != nil {
t.Fatal("inert Cursor projection fixture", err)
}
err = validateCursorProjectStdio(active, envelope, plan, data)
if fault == "legacy" || fault == "encoded" {
if err != nil {
t.Fatal("owned supported wire was orphaned", err)
}
} else if !errors.Is(err, domain.ErrProjectionRefreshRequired) {
t.Fatal("malformed wire or wrong selected locator was verified", err)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ func validateCursorProjectStdio(root string, envelope domain.PackageEnvelope, pl
if err != nil {
return err
}
prefix := managedstdio.Arguments(plan.ActivePath, dataPath, resolvedCWD, cwd.Anchor, "", nil)[:6]
if len(server.Args) < 7 || !slices.Equal(server.Args[:6], prefix) {
prefix := managedstdio.Arguments(plan.ActivePath, dataPath, resolvedCWD, cwd.Anchor, "", nil)[:5]
if len(server.Args) < 7 || !slices.Equal(server.Args[:5], prefix) {
return fmt.Errorf("%w: Cursor launcher package/data/cwd arguments differ from selected locators", domain.ErrProjectionRefreshRequired)
}
if _, err := managedstdio.DecodeCommandArguments(server.Args[5], server.Args[6:]); err != nil {
return fmt.Errorf("%w: Cursor launcher child argument transport differs", domain.ErrProjectionRefreshRequired)
}
}
return nil
}
Loading
Loading