Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
31efca6
docs: record project installation scope and deferred providers
777genius Oct 23, 2026
d37dd54
docs: record reviewed project core and all client implementation plans
777genius Oct 23, 2026
b4a4f60
feat(core): add explicit project target and lifecycle policy contracts
777genius Oct 23, 2026
5021573
feat(core): isolate project environment receipt ownership
777genius Oct 23, 2026
6f58195
feat(project): carry scoped lifecycle authority and guard mutable nam…
777genius Oct 23, 2026
8fb1d83
feat(project): compose scoped CLI and isolate saved lifecycle authority
777genius Oct 23, 2026
84b3192
fix(project): verify scoped CLI compilation and lint boundaries
777genius Oct 23, 2026
c198e4b
test(project): retain stable unsupported boundary and core review evi…
777genius Oct 23, 2026
7a35fe5
feat(gemini): route native component lifecycle into selected project
777genius Oct 23, 2026
7307e2d
Merge reviewed project core into Gemini stack
777genius Oct 23, 2026
59a5bf0
feat(gemini): qualify project components without overstating native r…
777genius Oct 23, 2026
7485863
fix(project): preserve authoring flags and satisfy full CI gates
777genius Oct 23, 2026
3a9c524
Record Gemini review evidence and merge current project core
777genius Oct 23, 2026
3c26d6b
test(gemini): record qualified project scope in compatibility catalog
777genius Oct 23, 2026
5716453
fix(project): preserve root registry and shared preview authority
777genius Oct 23, 2026
5860c85
docs(project): close independent core implementation review
777genius Oct 23, 2026
7be6b66
merge: align Gemini project support with reviewed core and compatibil…
777genius Oct 23, 2026
f15712d
test(project): compose Git and archive sources with isolated saved pr…
777genius Oct 23, 2026
a32a1e6
docs(project): record independent composed source and project lifecyc…
777genius Oct 23, 2026
0a0e24f
fix(project): capture eager Windows directory identity and retain exa…
777genius Oct 23, 2026
938bcb8
fix(project): integrate independently reviewed Windows identity corre…
777genius Oct 23, 2026
f893c66
merge: align reviewed project core, source transports and neutral com…
777genius Oct 23, 2026
7dff14f
feat(project): reject unsupported ChatGPT scope before acquisition an…
777genius Oct 23, 2026
d32fad9
feat(cursor): install project skills and MCP with scoped lifecycle E2E
777genius Oct 23, 2026
bc3d4e6
ci: enable reviewed core and native checks for project stacks
777genius Oct 23, 2026
915f4e9
merge: preserve source and project admission gates in ChatGPT stack
777genius Oct 23, 2026
0ea2e26
ci: run reviewed project and native gates for cursor stack
777genius Oct 23, 2026
aa51071
test(cursor): include composed project lifecycle in all platform gates
777genius Oct 23, 2026
006de1f
feat(copilot): add scoped skills and MCP installation with public E2E
777genius Oct 23, 2026
88a239a
fix(copilot): require exact scoped package and direct delivery authority
777genius Oct 23, 2026
8c4f85a
Merge reviewed Gemini project installation into project feature
777genius Oct 24, 2026
94a3e30
Integrate reviewed common project source lifecycle and feature CI rou…
777genius Oct 24, 2026
58d4bca
Record source feature ancestry for the isolated project installation …
777genius Oct 24, 2026
1d1e309
Merge reviewed ChatGPT project refusal boundary into project feature
777genius Oct 24, 2026
b573765
Merge reviewed Cursor project component lifecycle into project feature
777genius Oct 24, 2026
a347c32
merge(project): preserve Cursor and Copilot project declarations
777genius Oct 24, 2026
815e923
docs(copilot): preserve two independent feature integration approvals
777genius Oct 24, 2026
a0d1b5f
Merge reviewed Copilot project component lifecycle into project feature
777genius Oct 24, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/agentplugins-native-clients.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Native Client Runtime

on:
pull_request:
branches: [main]
branches: [main, 'feat/project-*', feat/source-transports-e2e]
paths:
- 'repotests/agentplugins_*native*'
- 'repotests/agentplugins_*runtime*'
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/core-fast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Core fast
# fast enough to keep a part from waiting 10-27 minutes.
on:
pull_request:
branches: [main, 'refactor/**']
branches: [main, 'refactor/**', 'feat/project-*', feat/source-transports-e2e]
paths:
- 'install/integrationctl/agentplugins/**'
# The only ports.PathPolicy implementation lives here.
Expand All @@ -32,6 +32,32 @@ concurrency:
cancel-in-progress: true

jobs:
project-contracts:
name: project-contracts (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
env:
GOMAXPROCS: '3'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Embedded schema digests bind exact Git bytes on every platform.
env:
GIT_CONFIG_COUNT: '1'
GIT_CONFIG_KEY_0: core.autocrlf
GIT_CONFIG_VALUE_0: 'false'
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.10"
cache: false
- name: Verify scoped ownership, path guards and disposable project lifecycle
run: go test -p=2 -count=1 -timeout=8m ./install/integrationctl/agentplugins/domain ./install/integrationctl/agentplugins/adapters/statev2 ./install/integrationctl/agentplugins/adapters/nativeconfig ./install/integrationctl/agentplugins/clients/... ./install/integrationctl/agentplugins/providers ./install/integrationctl/agentplugins/planner ./install/integrationctl/agentplugins/installer ./install/integrationctl/agentplugins/environment ./install/integrationctl/agentplugins/usecase ./cli/internal/agentpluginscli ./cli/cmd/agentplugins -run 'Test(TargetContext|Project|PrepareRejectsCrossEnvironmentContext)'

opencode-transition-windows:
name: opencode-transition-windows
runs-on: windows-2025
Expand Down
52 changes: 43 additions & 9 deletions cli/cmd/agentplugins/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,17 @@ func newProductionFeedClients(dataRoot string) (*directoryv1.Client, *discoveryv
}

func newAgentpluginsCLIApp(home, dataRoot string, directoryClient *directoryv1.Client, discoveryClient *discoveryv1.Client, securityClient *securityv1.Client, registry *specregistry.Registry) (agentpluginscli.App, error) {
return newScopedAgentpluginsCLIApp(home, dataRoot, "", directoryClient, discoveryClient, securityClient, registry)
}

func newScopedAgentpluginsCLIApp(home, cacheRoot, projectRoot string, directoryClient *directoryv1.Client, discoveryClient *discoveryv1.Client, securityClient *securityv1.Client, registry *specregistry.Registry) (agentpluginscli.App, error) {
dataRoot := cacheRoot
if projectRoot != "" {
dataRoot = filepath.Join(projectRoot, ".uap")
}
packageLoader := loader.Loader{Registry: registry}
runner := processadapter.OS{}
v2Store := statev2.Store{Path: filepath.Join(dataRoot, "state-v2.json")}
v2Store := statev2.Store{Path: filepath.Join(dataRoot, "state-v2.json"), ProjectRoot: projectRoot}
directoryManager := dirswap.Manager{JournalDir: filepath.Join(dataRoot, "operations-v2")}
mutationLock := processlock.Lock{Path: filepath.Join(dataRoot, "mutation.lock")}
clientRegistry := clientregistry.Default()
Expand All @@ -113,10 +121,10 @@ func newAgentpluginsCLIApp(home, dataRoot string, directoryClient *directoryv1.C
return agentpluginscli.App{}, err
}
stager := newManagedStager(clientRegistry, paths, helperExecutable)
planner := clientplanner.Planner{ManagedRoot: filepath.Join(dataRoot, "managed"), Paths: paths, Registry: clientRegistry}
planner := clientplanner.Planner{ProjectRoot: projectRoot, ManagedRoot: filepath.Join(dataRoot, "managed"), Paths: paths, Registry: clientRegistry}
lifecycle := newAgentpluginsLifecycle(dataRoot, v2Store, paths, clientRegistry, stager, runner, planner, directoryManager, mutationLock, nativeKernel)
facade, err := installer.New(installer.Config{
StateRoot: dataRoot, StateFile: v2Store.Path, LockFile: filepath.Join(dataRoot, "mutation.lock"),
ProjectRoot: projectRoot, StateRoot: dataRoot, StateFile: v2Store.Path, LockFile: filepath.Join(dataRoot, "mutation.lock"),
OperationsDir: filepath.Join(dataRoot, "operations-v2"), PluginDataBase: filepath.Join(dataRoot, "plugin-data"),
ManagedRoot: filepath.Join(dataRoot, "managed"), TempRoot: filepath.Join(dataRoot, "installer-tmp"),
HelperExecutable: helperExecutable, HelperVersion: version, Registry: clientRegistry, Runner: runner,
Expand All @@ -125,8 +133,22 @@ func newAgentpluginsCLIApp(home, dataRoot string, directoryClient *directoryv1.C
if err != nil {
return agentpluginscli.App{}, err
}
app := assembleAgentpluginsApp(home, dataRoot, v2Store, mutationLock, lifecycle, directoryClient, discoveryClient, securityClient, packageLoader, clientRegistry, planner)
lifecycle.ProjectRoot = projectRoot
lifecycle.CheckNamespace = facade.CheckProjectNamespace
if projectRoot != "" {
lifecycle.Kernel.Namespace = dataRoot
lifecycle.Kernel.PhysicalAuthority = planner
lifecycle.Kernel.Directory.Namespace = dataRoot
}
app := assembleAgentpluginsApp(home, dataRoot, cacheRoot, v2Store, mutationLock, lifecycle, directoryClient, discoveryClient, securityClient, packageLoader, clientRegistry, planner)
app.Installer = facade
setAgentpluginsProjectContext(&app, projectRoot)
app.ForScope = func(scope domain.InstallScope, root string) (agentpluginscli.App, error) {
if scope != domain.ScopeProject {
root = ""
}
return newScopedAgentpluginsCLIApp(home, cacheRoot, root, directoryClient, discoveryClient, securityClient, registry)
}
return app, nil
}

Expand All @@ -151,7 +173,7 @@ func newAgentpluginsLifecycle(dataRoot string, v2Store statev2.Store, paths path
}
}

func assembleAgentpluginsApp(home, dataRoot string, v2Store statev2.Store, mutationLock processlock.Lock, lifecycle usecase.Service, directoryClient *directoryv1.Client, discoveryClient *discoveryv1.Client, securityClient *securityv1.Client, packageLoader loader.Loader, clientRegistry *clients.Registry, planner clientplanner.Planner) agentpluginscli.App {
func assembleAgentpluginsApp(home, dataRoot, cacheRoot string, v2Store statev2.Store, mutationLock processlock.Lock, lifecycle usecase.Service, directoryClient *directoryv1.Client, discoveryClient *discoveryv1.Client, securityClient *securityv1.Client, packageLoader loader.Loader, clientRegistry *clients.Registry, planner clientplanner.Planner) agentpluginscli.App {
detector := clientdetect.NewOS(home)
detector.Registry = clientRegistry
legacyStatePath := filepath.Join(home, ".plugin-kit-ai", "state.json")
Expand All @@ -168,7 +190,7 @@ func assembleAgentpluginsApp(home, dataRoot string, v2Store statev2.Store, mutat
UserHome: home,
ManagedRoot: filepath.Join(dataRoot, "managed"),
DependenciesRoot: dataRoot,
EnvironmentStoreRoot: filepath.Join(dataRoot, "store"),
EnvironmentStoreRoot: filepath.Join(cacheRoot, "store"),
EnvironmentLockPath: filepath.Join(dataRoot, "environment.lock"),
StateStore: v2Store,
StateMigrator: &migrator,
Expand All @@ -186,13 +208,13 @@ func assembleAgentpluginsApp(home, dataRoot string, v2Store statev2.Store, mutat
return commitDependencyDirectoryPreview(directoryClient, preview)
},
DiscoveryClient: discoveryClient,
SourceAcquirer: lazySourceAcquirer{dataRoot: dataRoot, acquirer: sourceacquisition.Acquirer{TempRoot: dataRoot}},
SourceAcquirer: lazySourceAcquirer{dataRoot: cacheRoot, acquirer: sourceacquisition.Acquirer{TempRoot: cacheRoot}},
PackageLoader: packageLoader,
NativePackageLoader: loader.OpenAILoader{Loader: packageLoader},
SecurityIndex: securityClient,
SecurityEvaluator: securityscan.Evaluator{
Scanner: securityscan.ReleaseScanner{Root: filepath.Join(dataRoot, "security", "lintai"), HTTPClient: lintaiReleaseHTTPClient()},
Cache: securityscan.FileCache{Root: filepath.Join(dataRoot, "security", "assessments")}, Requirement: securityscan.DefaultRequirement(),
Scanner: securityscan.ReleaseScanner{Root: filepath.Join(cacheRoot, "security", "lintai"), HTTPClient: lintaiReleaseHTTPClient()},
Cache: securityscan.FileCache{Root: filepath.Join(cacheRoot, "security", "assessments")}, Requirement: securityscan.DefaultRequirement(),
},
SourceAcquirerForDryRun: func(root string) agentpluginscli.SourceAcquirer { return sourceacquisition.Acquirer{TempRoot: root} },
SecurityEvaluatorForDryRun: func(root string) ports.PackageSecurityEvaluator {
Expand Down Expand Up @@ -240,3 +262,15 @@ func commitDependencyDirectoryPreview(original *directoryv1.Client, preview agen
_, err = original.Cache.Reconcile(bundle, original.Trust)
return err
}

func setAgentpluginsProjectContext(app *agentpluginscli.App, projectRoot string) {
app.Scope, app.ProjectRoot = domain.ScopeUser, projectRoot
if projectRoot == "" {
return
}
app.Scope = domain.ScopeProject
app.DependenciesRoot = projectRoot
app.StateMigrator = nil
app.LegacyLifecycle = nil
app.LegacyStateLock = nil
}
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)
}
Loading
Loading