diff --git a/.github/workflows/agentplugins-native-clients.yml b/.github/workflows/agentplugins-native-clients.yml index 1e2da7ad1..0e28ba42e 100644 --- a/.github/workflows/agentplugins-native-clients.yml +++ b/.github/workflows/agentplugins-native-clients.yml @@ -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*' @@ -69,10 +69,15 @@ jobs: AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' shell: bash run: | + project_args=() + if [[ '${{ matrix.client }}' == 'codex' || '${{ matrix.client }}' == 'claude' || '${{ matrix.client }}' == 'opencode' ]]; then + project_args+=(--project-components) + fi python scripts/run-native-client-matrix.py \ --client '${{ matrix.client }}' \ --target '${{ matrix.platform.target }}' \ - --output '${{ runner.temp }}/native-client-proof' + --output '${{ runner.temp }}/native-client-proof' \ + "${project_args[@]}" - name: Upload native evidence including failed stages if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 diff --git a/.github/workflows/cline-native-research.yml b/.github/workflows/cline-native-research.yml new file mode 100644 index 000000000..ec7d58458 --- /dev/null +++ b/.github/workflows/cline-native-research.yml @@ -0,0 +1,46 @@ +name: Cline Pinned Read-Only Config Research + +on: + pull_request: + paths: + - '.github/workflows/cline-native-research.yml' + - 'scripts/research-cline-native.py' + - 'scripts/test_cline_native_research.py' + - 'scripts/native_client_research.py' + - 'scripts/research-grok-native.py' + - 'scripts/run-native-client-matrix.py' + - 'scripts/native_client_draft.py' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: cline-native-research-${{ github.ref }} + cancel-in-progress: true + +jobs: + research: + name: Cline 3.0.73 read-only config observations + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify inert research safety contracts + run: python3 -m unittest discover -s scripts -p 'test_*native_research.py' + - name: Capture genuine pinned configuration observations + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-cline-native.py --output '${{ runner.temp }}/cline-native-research' + - name: Upload research including failed collection + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: cline-3.0.73-precode-config-linux-amd64 + path: ${{ runner.temp }}/cline-native-research/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/cline-project-native.yml b/.github/workflows/cline-project-native.yml new file mode 100644 index 000000000..fe0aae5d7 --- /dev/null +++ b/.github/workflows/cline-project-native.yml @@ -0,0 +1,71 @@ +name: Cline Current Source Read-Only Project Gate +on: + pull_request: + paths: + - '.github/workflows/cline-project-native.yml' + - 'cli/**' + - 'install/integrationctl/agentplugins/**' + - 'scripts/research-cline-project.py' + - 'scripts/cline_project_envelope.py' + - 'scripts/test_cline_project_*.py' + - 'scripts/research-cline-native.py' + - 'scripts/native_client_research.py' + - 'scripts/research-grok-native.py' + - 'scripts/run-native-client-matrix.py' + - 'scripts/native_client_project_artifacts.py' + - 'scripts/test_native_client_project_artifacts.py' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: cline-project-native-${{ github.ref }} + cancel-in-progress: true +jobs: + research: + name: Cline 3.0.73 actual UAP enabled config metadata + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.26.10' + cache: false + - name: Verify inert acquisition, publication and fixture contracts + run: | + python3 -m unittest discover -s scripts -p 'test_cline_project_*.py' + python3 -m unittest discover -s scripts -p 'test_cline_native_research.py' + python3 -m unittest discover -s scripts -p 'test_native_client_project_artifacts.py' + (cd cli && go test -p=1 ./cmd/agentplugins -run '^TestClineGate') + - name: Observe pinned version and read-only config from actual UAP files + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-cline-project.py --output '${{ runner.temp }}/cline-project-native' + - name: Admit only exact bounded upload ZIP + id: upload_admission + if: ${{ always() }} + env: + RESEARCH_OUTPUT: ${{ runner.temp }}/cline-project-native/upload/research.zip + run: | + /usr/bin/python3 -I - <<'PYTHON' + import os, stat + from pathlib import Path + path = Path(os.environ['RESEARCH_OUTPUT']) + info = path.lstat() + if path.resolve() != path or not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_uid != os.getuid() or info.st_size > 32 * 1024 * 1024: + raise SystemExit('refusing nonregular or unbounded upload ZIP') + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + output.write('valid=true\n') + PYTHON + - name: Upload admitted evidence including failed stages + if: ${{ always() && steps.upload_admission.outputs.valid == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: cline-3.0.73-current-source-enabled-project-config-linux-amd64 + path: ${{ runner.temp }}/cline-project-native/upload/research.zip + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/core-fast.yml b/.github/workflows/core-fast.yml index e734a1b7a..cfe2db34d 100644 --- a/.github/workflows/core-fast.yml +++ b/.github/workflows/core-fast.yml @@ -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. @@ -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 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 954cb7520..a350f970b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -38,11 +38,10 @@ jobs: mkdir -p coverage mapfile -t root_packages < <(go list ./... | grep -v '/repotests$') go test -count=1 -timeout=20m -covermode=atomic -coverprofile=coverage/root.out "${root_packages[@]}" - # These tests recursively execute the instrumented test binary with an - # intentionally minimal environment and assert byte-exact stderr. They - # remain covered by Required; Go's GOCOVERDIR warning makes them - # incompatible with coverage instrumentation itself. - go test -count=1 -timeout=20m -skip '^TestReleaseCompletionProcessStderr$' -covermode=atomic -coverprofile=coverage/cli.out ./cli/... + go test -count=1 -timeout=20m -covermode=atomic -coverprofile=coverage/cli.out ./cli/... + # The version-probe test re-executes its instrumented test binary with + # an intentionally minimal environment. Go's missing-GOCOVERDIR warning + # changes the probe output; Required still runs it without coverage. go test -count=1 -timeout=20m -skip '^TestExecutableVersionProbeIsSanitizedAndIsolated$' -covermode=atomic -coverprofile=coverage/integrationctl.out ./install/integrationctl/... # Nested module: do not use a parent-relative ./agentplugins/... glob. # That pattern needs go.work; GOWORK=off fails loud instead of covering diff --git a/.github/workflows/grok-native-research.yml b/.github/workflows/grok-native-research.yml new file mode 100644 index 000000000..691a3ca30 --- /dev/null +++ b/.github/workflows/grok-native-research.yml @@ -0,0 +1,47 @@ +name: Grok Pinned Project Source Research + +on: + pull_request: + paths: + - '.github/workflows/grok-native-research.yml' + - 'scripts/research-grok-native.py' + - 'scripts/test_grok_native_research.py' + - 'scripts/native_client_research.py' + - 'scripts/run-native-client-matrix.py' + - 'scripts/native_client_draft.py' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: grok-native-research-${{ github.ref }} + cancel-in-progress: true + +jobs: + research: + name: Grok 1.0.67 project source observations + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify inert Grok and shared Qoder research safety contracts + run: | + python3 -m unittest discover -s scripts -p 'test_grok_native_research.py' + python3 -m unittest discover -s scripts -p 'test_qoder_native_research.py' + - name: Capture pinned genuine source observations in fresh profiles + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-grok-native.py --output '${{ runner.temp }}/grok-native-research' + - name: Upload research including failed collection + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: grok-1.0.67-precode-project-source-linux-amd64 + path: ${{ runner.temp }}/grok-native-research/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/project-git-diagnostic.yml b/.github/workflows/project-git-diagnostic.yml new file mode 100644 index 000000000..e168b10a2 --- /dev/null +++ b/.github/workflows/project-git-diagnostic.yml @@ -0,0 +1,26 @@ +name: Project Git Fixture Diagnostic + +on: + push: + branches: [fix/project-git-diagnostic-20261025] + workflow_dispatch: + +permissions: + contents: read + +jobs: + git-fixtures: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.26.10' + cache: false + - name: Observe bounded local Git cleanup once + env: + AGENTPLUGINS_GIT_CLEANUP_DIAGNOSTIC: '1' + run: go test -count=1 -timeout=7m -v -run '^TestOSRunnerLocalGitFetchCleanupObservation$' ./install/integrationctl/adapters/process diff --git a/.github/workflows/project-windows-atomic-diagnostic.yml b/.github/workflows/project-windows-atomic-diagnostic.yml new file mode 100644 index 000000000..200bbd1ef --- /dev/null +++ b/.github/workflows/project-windows-atomic-diagnostic.yml @@ -0,0 +1,53 @@ +name: Windows atomic publication diagnostic + +on: + push: + branches: [fix/project-windows-atomic-20261025] + workflow_dispatch: + +permissions: + contents: read + +jobs: + qualify: + runs-on: windows-2025 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version: '1.26.10' + cache: false + - name: Record exact source and toolchain + shell: bash + run: | + mkdir -p "$RUNNER_TEMP/windows-atomic-evidence" + git rev-parse HEAD > "$RUNNER_TEMP/windows-atomic-evidence/commit.txt" + git rev-parse 'HEAD^{tree}' > "$RUNNER_TEMP/windows-atomic-evidence/tree.txt" + go version > "$RUNNER_TEMP/windows-atomic-evidence/go-version.txt" + - name: Qualify exact publication and refusal contracts once + shell: bash + run: | + go test -count=1 -timeout=2m -v ./install/integrationctl/adapters/atomicfile ./install/integrationctl/agentplugins/adapters/securityscan -run '^(TestWindowsAtomicPublicationHeldReaders|TestWindowsAtomicPublicationRefusals|TestWindowsAtomicPublicationUnicodeLongPath|TestWindowsAtomicPublicationSecurityCache)$' 2>&1 | tee "$RUNNER_TEMP/windows-atomic-evidence/tests.log" + python - "$RUNNER_TEMP/windows-atomic-evidence/tests.log" <<'PY' + import pathlib, re, sys + body = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") + expected = { + "TestWindowsAtomicPublicationHeldReaders", + "TestWindowsAtomicPublicationRefusals", + "TestWindowsAtomicPublicationUnicodeLongPath", + "TestWindowsAtomicPublicationSecurityCache", + } + passed = re.findall(r"^--- PASS: (\w+) \(", body, re.MULTILINE) + if set(passed) != expected or len(passed) != len(expected) or re.search(r"--- (SKIP|FAIL):", body): + raise SystemExit("all four exact publication contracts must pass without skips") + PY + - name: Preserve raw observation + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: windows-atomic-publication + path: ${{ runner.temp }}/windows-atomic-evidence + if-no-files-found: error diff --git a/.github/workflows/qoder-native-research.yml b/.github/workflows/qoder-native-research.yml new file mode 100644 index 000000000..459258ab5 --- /dev/null +++ b/.github/workflows/qoder-native-research.yml @@ -0,0 +1,49 @@ +name: Qoder Pinned Native Research + +on: + pull_request: + paths: + - '.github/workflows/qoder-native-research.yml' + - 'scripts/research-qoder-native.py' + - 'scripts/native_client_research.py' + - 'scripts/test_qoder_native_research.py' + - 'scripts/native_client_research.py' + - 'scripts/test_native_client_research.py' + - 'scripts/run-native-client-matrix.py' + - 'scripts/native_client_draft.py' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: qoder-native-research-${{ github.ref }} + cancel-in-progress: true + +jobs: + research: + name: Qoder 1.1.78 version help and empty list + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify inert research safety contracts + run: | + python3 -m unittest discover -s scripts -p 'test_native_client_research.py' + python3 -m unittest discover -s scripts -p 'test_qoder_native_research.py' + - name: Capture pinned genuine research in a fresh credential-free profile + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-qoder-native.py --output '${{ runner.temp }}/qoder-native-research' + - name: Upload research including failed probes + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: qoder-1.1.78-precode-research-linux-amd64 + path: ${{ runner.temp }}/qoder-native-research/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/qoder-populated-research.yml b/.github/workflows/qoder-populated-research.yml new file mode 100644 index 000000000..373349cac --- /dev/null +++ b/.github/workflows/qoder-populated-research.yml @@ -0,0 +1,51 @@ +name: Qoder Pinned Populated Scope Research + +on: + pull_request: + paths: + - '.github/workflows/qoder-populated-research.yml' + - 'scripts/research-qoder-populated.py' + - 'scripts/test_qoder_populated_research.py' + - 'scripts/research-qoder-native.py' + - 'scripts/research-grok-native.py' + - 'scripts/native_client_research.py' + - 'scripts/test_native_client_research.py' + - 'scripts/run-native-client-matrix.py' + - 'scripts/native_client_draft.py' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: qoder-populated-research-${{ github.ref }} + cancel-in-progress: true + +jobs: + research: + name: Qoder 1.1.78 populated user and project observations + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify inert research contracts + run: | + python3 -m unittest discover -s scripts -p 'test_native_client_research.py' + python3 -m unittest discover -s scripts -p 'test_qoder_native_research.py' + python3 -m unittest discover -s scripts -p 'test_qoder_populated_research.py' + - name: Observe finite installs and lists in a fresh nonroot network namespace + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-qoder-populated.py --output '${{ runner.temp }}/qoder-populated-research' --upload-output '${{ runner.temp }}/qoder-populated-upload' + - name: Preserve actual observations including failures + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: qoder-1.1.78-populated-scope-research-linux-amd64 + path: ${{ runner.temp }}/qoder-populated-upload/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/vscode-native-research.yml b/.github/workflows/vscode-native-research.yml new file mode 100644 index 000000000..9dc8fe4e1 --- /dev/null +++ b/.github/workflows/vscode-native-research.yml @@ -0,0 +1,65 @@ +name: VS Code Pinned Publisher Research + +on: + pull_request: + paths: + - '.github/workflows/vscode-native-research.yml' + - 'scripts/research-vscode-native.py' + - 'scripts/test_vscode_native_research.py' + - 'scripts/native_client_research.py' + - 'scripts/test_native_client_research.py' + - 'scripts/research-grok-native.py' + - 'scripts/run-native-client-matrix.py' + - 'scripts/native_client_draft.py' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: vscode-native-research-${{ github.ref }} + cancel-in-progress: true + +jobs: + research: + name: VS Code 1.143.1 help and version only + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Verify inert archive and collector safety contracts + run: | + python3 -m unittest discover -s scripts -p 'test_vscode_native_research.py' + python3 -m unittest discover -s scripts -p 'test_native_client_research.py' + - name: Capture exact publisher help and version in isolated disposable processes + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-vscode-native.py --output '${{ runner.temp }}/vscode-native-research' + - name: Admit the exact bounded upload envelope + id: upload_admission + if: ${{ always() }} + env: + RESEARCH_OUTPUT: ${{ runner.temp }}/vscode-native-research/upload/research.zip + run: | + python3 - <<'PYTHON' + import os, stat + from pathlib import Path + path = Path(os.environ["RESEARCH_OUTPUT"]) + info = path.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 32 * 1024 * 1024: + raise SystemExit("refusing unbounded or nonregular upload envelope") + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write("valid=true\n") + PYTHON + - name: Upload bounded research including failed stages + if: ${{ always() && steps.upload_admission.outputs.valid == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.0 + with: + name: vscode-1.143.1-precode-publisher-linux-amd64 + path: ${{ runner.temp }}/vscode-native-research/upload/research.zip + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/vscode-workspace-research.yml b/.github/workflows/vscode-workspace-research.yml new file mode 100644 index 000000000..137cf7ae3 --- /dev/null +++ b/.github/workflows/vscode-workspace-research.yml @@ -0,0 +1,68 @@ +name: VS Code Untrusted Workspace Research +on: + pull_request: + paths: + - '.github/workflows/vscode-workspace-research.yml' + - 'scripts/research-vscode-workspace.py' + - 'scripts/test_vscode_workspace_research.py' + - 'scripts/vscode-workspace-observer/**' + - 'install/integrationctl/research/vscodeworkspace/**' + - 'scripts/research-vscode-native.py' + - 'scripts/native_client_research.py' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: vscode-workspace-research-${{ github.ref }} + cancel-in-progress: true +jobs: + research: + name: VS Code 1.143.1 untrusted metadata only + runs-on: ubuntu-22.04 + timeout-minutes: 12 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + with: + go-version: '1.26.10' + cache: false + - name: Verify inert collector and observer contracts + run: | + python3 -m unittest discover -s scripts -p 'test_vscode_workspace_research.py' + python3 -m unittest discover -s scripts -p 'test_vscode_native_research.py' + python3 -m unittest discover -s scripts -p 'test_native_client_research.py' + node --test scripts/vscode-workspace-observer/shape.test.cjs + go test -p=1 ./install/integrationctl/research/vscodeworkspace + - name: Observe pinned untrusted workbench metadata in strict isolated trees + env: + AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED: '1' + EXPECTED_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/research-vscode-workspace.py --output '${{ runner.temp }}/vscode-workspace-research' + - name: Admit exact bounded upload envelope + id: upload_admission + if: ${{ always() }} + env: + RESEARCH_OUTPUT: ${{ runner.temp }}/vscode-workspace-research/upload/research.zip + run: | + /usr/bin/python3 -I - <<'PYTHON' + import os, stat + from pathlib import Path + path = Path(os.environ["RESEARCH_OUTPUT"]) + info = path.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 32 * 1024 * 1024: + raise SystemExit("refusing unbounded or nonregular upload envelope") + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write("valid=true\n") + PYTHON + - name: Upload bounded observation including failed stages + if: ${{ always() && steps.upload_admission.outputs.valid == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: vscode-1.143.1-untrusted-workspace-linux-amd64 + path: ${{ runner.temp }}/vscode-workspace-research/upload/research.zip + if-no-files-found: error + retention-days: 14 + diff --git a/README.md b/README.md index 11a72e939..b8c0181ee 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ and open the copied folder. Then restore the recorded plugins and dependencies: agentplugins deps import --install ``` -The optional directory argument defaults to the current folder. Export reads -the global user environment; import writes it. Only `uap.toml` and `uap.lock` +The optional directory argument defaults to the current folder. By default, +export reads the global user environment and import writes it. Only `uap.toml` and `uap.lock` are transferred. Restore verifies exact locked packages from still-available sources or the destination cache for supported dependency clients: Claude Code, Codex, OpenCode, Cursor and Gemini CLI. @@ -169,6 +169,33 @@ from a GitHub repository or from an HTTPS publisher that serves a `.well-known` Skills index. Publisher installs take an exact `--skill` name. The local browser picker and saved dependencies are included. +## Install for one project + +**SOURCE ONLY / UNRELEASED:** select project scope explicitly in a compatible +source build: + +```sh +agentplugins add owner/repo --alias review --target cursor \ + --scope project --project-root /path/to/project +agentplugins install --locked --scope project --project-root /path/to/project +agentplugins skills add owner/repository --ui \ + --scope project --project-root /path/to/project +``` + +The portable `uap.toml` and `uap.lock` live in that project. Local receipts, +recovery state and retained data live under `.uap/`; the user's environment +remains separate. The default scope is still user, and project scope without +`--project-root` uses the current directory. Native capabilities and activation +requirements depend on the client. See [Project installation](docs/PROJECT_INSTALLATION.md) +for supported-target reporting, update/repair/removal and portable restore. +The [current project support table](docs/PROJECT_CLIENT_SUPPORT.md) records each +client's component paths and activation limits; user-scope compatibility below +is a separate contract. + +VS Code project scope prepares `.github/skills` and owned `.vscode/mcp.json` +servers for one local folder. Its Prepared result leaves workspace/server trust +and runtime selection to the client. See [VS Code project preparation](docs/PROJECT_INSTALLATION.md#vs-code-project-preparation). + ## Any Agent Plugins 1.0 package Install from the registry, a local folder, or a GitHub repository pinned to an diff --git a/cli/cmd/agentplugins/compose.go b/cli/cmd/agentplugins/compose.go index c5bc4a64e..106aa7bcd 100644 --- a/cli/cmd/agentplugins/compose.go +++ b/cli/cmd/agentplugins/compose.go @@ -101,12 +101,23 @@ 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() + if projectRoot != "" { + clientRegistry = clientregistry.Project() + } paths := pathpolicy.Policy{} nativeKernel := nativeconfig.New() helperExecutable, err := os.Executable() @@ -114,10 +125,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, @@ -126,8 +137,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 } @@ -140,7 +165,7 @@ func newManagedStager(clientRegistry *clients.Registry, paths pathpolicy.Policy, } func newAgentpluginsLifecycle(dataRoot string, v2Store statev2.Store, paths pathpolicy.Policy, clientRegistry *clients.Registry, stager providers.Stager, runner processadapter.OS, planner clientplanner.Planner, directoryManager dirswap.Manager, mutationLock processlock.Lock, nativeKernel nativeconfig.Kernel) usecase.Service { - transitions := opencode.NativeTransitions{PackageVerifier: stager, State: transaction.Kernel{StateStore: v2Store}, Kernel: nativeKernel} + transitions := opencode.NativeTransitions{ProjectRoot: v2Store.ProjectRoot, PackageVerifier: stager, State: transaction.Kernel{StateStore: v2Store}, Kernel: nativeKernel} return usecase.Service{ ClientPreparation: providers.NewOpenCodeClientPreparation(clientRegistry), StateStore: v2Store, Paths: paths, Planner: planner, Targets: planner, Stager: stager, @@ -152,7 +177,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") @@ -169,7 +194,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, @@ -187,13 +212,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 skillsource.Acquirer{Acquirer: sourceacquisition.Acquirer{TempRoot: root}} @@ -243,3 +268,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 +} diff --git a/cli/cmd/agentplugins/cursor_project_fixture_test.go b/cli/cmd/agentplugins/cursor_project_fixture_test.go new file mode 100644 index 000000000..07f0455d6 --- /dev/null +++ b/cli/cmd/agentplugins/cursor_project_fixture_test.go @@ -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: projectJourneyTemp(t), a: projectJourneyTemp(t), b: projectJourneyTemp(t), 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) +} diff --git a/cli/cmd/agentplugins/cursor_project_lifecycle_test.go b/cli/cmd/agentplugins/cursor_project_lifecycle_test.go new file mode 100644 index 000000000..e58a5cb9f --- /dev/null +++ b/cli/cmd/agentplugins/cursor_project_lifecycle_test.go @@ -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 := projectJourneyTemp(t) + 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") + } +} diff --git a/cli/cmd/agentplugins/devin_project_admission_test.go b/cli/cmd/agentplugins/devin_project_admission_test.go new file mode 100644 index 000000000..72ceeaf30 --- /dev/null +++ b/cli/cmd/agentplugins/devin_project_admission_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectDevinNoSaveUsesSelectedHostAndAbsentLegacyProfile(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + devinProjectCommitSource(t, transport, projectJourneySkill, "https://managed.test/v1") + f := newDevinProjectJourney(t, transport) + user, b := securityFixtureTree(t, f.devinRoot("")), securityFixtureTree(t, f.devinRoot(f.b)) + f.run(t, f.a, "add", transport.repo, "--no-save", "--target", "devin") + state := f.state(t, f.a) + if len(state.Installations) != 1 || len(state.Installations[0].Clients) != 1 || len(state.EnvironmentReceipts) != 0 { + t.Fatalf("no-save changed ownership route: %+v", state) + } + installation := state.Installations[0] + var binding domain.ClientBinding + for _, owned := range installation.Clients { + binding = owned + } + if binding.ClientID != "windsurf" || binding.NativeProfileRoot != f.devinRoot(f.a) || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("no-save project host evidence missing: %+v", binding) + } + if !strings.Contains(projectJourneyRead(t, filepath.Join(f.devinRoot(f.a), "mcp_config.json")), "managed.test/v1") { + t.Fatal("no-save current Devin dedicated MCP missing") + } + f.run(t, f.a, "remove", installation.InstallationID, "--target", "windsurf") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(f.devinRoot(f.a), "skills", "docs")) + if !reflect.DeepEqual(user, securityFixtureTree(t, f.devinRoot(""))) || !reflect.DeepEqual(b, securityFixtureTree(t, f.devinRoot(f.b))) { + t.Fatal("no-save changed another native scope") + } +} + +type devinUnqualifiedDetector struct{ client domain.DetectedClient } + +func (detector devinUnqualifiedDetector) Detect(_ context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{detector.client}, nil +} + +func TestProjectDevinCLIRefusesMissingOlderAndWrongHarnessBeforeNativeEffects(t *testing.T) { + transport := newProjectJourneyTransport(t, "zip") + devinProjectCommitSource(t, transport, projectJourneySkill, "https://managed.test/v1") + for _, version := range []string{"", "3000.3.0", "3000.11.205-alpha", "wrong-harness"} { + t.Run("version="+version, func(t *testing.T) { + f := newDevinProjectJourney(t, transport) + client := domain.DetectedClient{ClientID: domain.ClientWindsurf, Status: domain.DetectionDetected, ExecutablePath: filepath.Join(f.home, "devin"), Version: version} + if version == "wrong-harness" { + client.Version = "3000.11.205" + client.ExecutablePath = filepath.Join(f.home, "windsurf") + } + compose := f.app.ForScope + f.app.ForScope = func(scope domain.InstallScope, root string) (agentpluginscli.App, error) { + selected, err := compose(scope, root) + if err != nil { + return selected, err + } + selected.Detector = devinUnqualifiedDetector{client: client} + return selected, nil + } + before, user, b := securityFixtureTree(t, f.devinRoot(f.a)), securityFixtureTree(t, f.devinRoot("")), securityFixtureTree(t, f.devinRoot(f.b)) + _, err := f.execute(f.a, "add", transport.repo, "--no-save", "--target", "windsurf") + if err == nil || !strings.Contains(err.Error(), "explicitly selected primary") { + t.Fatalf("unqualified CLI host accepted: %v", err) + } + if !reflect.DeepEqual(before, securityFixtureTree(t, f.devinRoot(f.a))) || !reflect.DeepEqual(user, securityFixtureTree(t, f.devinRoot(""))) || !reflect.DeepEqual(b, securityFixtureTree(t, f.devinRoot(f.b))) { + t.Fatal("unqualified CLI host changed native scope") + } + }) + } +} diff --git a/cli/cmd/agentplugins/devin_project_fixture_test.go b/cli/cmd/agentplugins/devin_project_fixture_test.go new file mode 100644 index 000000000..83f2304d0 --- /dev/null +++ b/cli/cmd/agentplugins/devin_project_fixture_test.go @@ -0,0 +1,123 @@ +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 devinProjectDetector struct{ root string } + +func (detector devinProjectDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientWindsurf, ConfigRoot: "", ExecutablePath: filepath.Join(detector.root, "devin"), Version: "3000.11.205", Status: domain.DetectionDetected}}, nil +} + +// Reuse the core journey's production CLI/state helpers and real TLS Git/ZIP +// transports. Devin supplies its own detected profile and native layout. +type devinProjectJourney struct{ projectJourney } + +func newDevinProjectJourney(t *testing.T, transport *projectJourneyTransport) devinProjectJourney { + t.Helper() + f := devinProjectJourney{projectJourney{home: devinJourneyTemp(t), a: devinJourneyTemp(t), b: devinJourneyTemp(t), transport: transport}} + f.cache = filepath.Join(f.home, "uap") + for _, root := range []string{"", f.a, f.b} { + native := f.devinRoot(root) + projectJourneyWrite(t, filepath.Join(native, "mcp_config.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 = configureDevinProjectJourney(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 configureDevinProjectJourney(app agentpluginscli.App, home string, transport *projectJourneyTransport) agentpluginscli.App { + app.Detector = devinProjectDetector{root: filepath.Join(home, ".devin")} + 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 configureDevinProjectJourney(selected, home, transport), nil + } + return app +} + +func (f devinProjectJourney) devinRoot(project string) string { + if project == "" { + return filepath.Join(f.home, ".devin") + } + return filepath.Join(project, ".devin") +} + +func (f devinProjectJourney) assertDevinOwned(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.devinRoot(project) || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("Devin 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.devinRoot(project)} { + if projectJourneyRead(t, filepath.Join(root, "skills", "docs", "SKILL.md")) != skill { + t.Fatal("Devin managed/native skill bytes differ from the selected pin") + } + } + settings := projectJourneyRead(t, filepath.Join(f.devinRoot(project), "mcp_config.json")) + for _, text := range []string{endpoint, "foreign.test", "foreignPreference", `"disabled":true`} { + if !strings.Contains(strings.ReplaceAll(settings, " ", ""), text) { + t.Fatalf("Devin 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 devinProjectCommitSource(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 Devin project components") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + transport.mu.Lock() + defer transport.mu.Unlock() + transport.archive = projectJourneyArchive(t, transport.repo) +} + +func devinJourneyTemp(t *testing.T) string { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + return root +} diff --git a/cli/cmd/agentplugins/devin_project_lifecycle_test.go b/cli/cmd/agentplugins/devin_project_lifecycle_test.go new file mode 100644 index 000000000..9511a4b3a --- /dev/null +++ b/cli/cmd/agentplugins/devin_project_lifecycle_test.go @@ -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 Devin discovery, +// enablement, MCP initialization, model use and runtime are not certified. +func TestProjectDevinComposedSourceLifecycle(t *testing.T) { + for _, kind := range []string{"git", "zip"} { + t.Run(kind, func(t *testing.T) { + transport := newProjectJourneyTransport(t, kind) + devinProjectCommitSource(t, transport, projectJourneySkill, "https://managed.test/v1") + f := newDevinProjectJourney(t, transport) + userBefore := securityFixtureTree(t, f.devinRoot("")) + for _, root := range []string{f.a, f.b} { + f.run(t, root, "add", transport.endpoint, "--alias", "docs", "--target", "windsurf") + f.assertDevinOwned(t, root, projectJourneySkill, "https://managed.test/v1") + } + bNative := securityFixtureTree(t, f.devinRoot(f.b)) + bState := projectJourneyRead(t, filepath.Join(f.namespace(f.b), "state-v2.json")) + devinProjectReconcileAndRepair(t, f) + devinProjectUpdateAndTransfer(t, f) + devinProjectRemove(t, f) + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, f.devinRoot(""))) || !reflect.DeepEqual(bNative, securityFixtureTree(t, f.devinRoot(f.b))) || bState != projectJourneyRead(t, filepath.Join(f.namespace(f.b), "state-v2.json")) { + t.Fatal("Devin project-A lifecycle changed user or project-B native configuration/state") + } + f.assertDevinOwned(t, f.b, projectJourneySkill, "https://managed.test/v1") + }) + } +} + +func devinProjectReconcileAndRepair(t *testing.T, f devinProjectJourney) { + 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("Devin locked no-op changed exact ownership receipts") + } + installation, _ := f.owned(t, f.a) + native := securityFixtureTree(t, f.devinRoot(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.devinRoot(f.a))) { + t.Fatal("Devin project read commands changed native files or ownership receipts") + } + if err := os.RemoveAll(filepath.Join(f.devinRoot(f.a), "skills", "docs")); err != nil { + t.Fatal(err) + } + f.run(t, f.a, "repair", installation.InstallationID, "--target", "windsurf") + f.assertDevinOwned(t, f.a, projectJourneySkill, "https://managed.test/v1") + f.inspectNoRecovery(t, f.a) +} + +func devinProjectUpdateAndTransfer(t *testing.T, f devinProjectJourney) { + t.Helper() + previous, binding := f.owned(t, f.a) + devinProjectCommitSource(t, f.transport, projectJourneyAdvancedSkill, "https://managed.test/v2") + f.run(t, f.a, "update", "docs", "--target", "windsurf") + updated, selected := f.assertDevinOwned(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("Devin 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("Devin export changed exact portable pins") + } + cold := newDevinProjectJourney(t, f.transport) + projectJourneyAbsent(t, cold.cache, filepath.Join(cold.a, ".uap")) + cold.run(t, cold.a, "deps", "import", exchange, "--install", "--yes") + restored, _ := cold.assertDevinOwned(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("Devin cold import changed source identity or portable pins") + } + cold.inspectNoRecovery(t, cold.a) +} + +func devinProjectRemove(t *testing.T, f devinProjectJourney) { + t.Helper() + _, binding := f.owned(t, f.a) + f.run(t, f.a, "remove", "docs", "--target", "windsurf") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(f.devinRoot(f.a), "skills", "docs")) + settings := projectJourneyRead(t, filepath.Join(f.devinRoot(f.a), "mcp_config.json")) + if strings.Contains(settings, "managed.test") || !strings.Contains(settings, "foreign.test") || !strings.Contains(settings, "foreignPreference") { + t.Fatalf("Devin removal changed foreign MCP settings or retained its entry: %s", settings) + } + if projectJourneyRead(t, filepath.Join(f.devinRoot(f.a), "skills", "foreign", "SKILL.md")) != "foreign skill\n" || projectJourneyRead(t, filepath.Join(f.devinRoot(f.a), "hooks.json")) != "foreign hooks\n" { + t.Fatal("Devin removal changed foreign skill or hooks") + } + if len(f.state(t, f.a).EnvironmentReceipts) != 0 { + t.Fatal("Devin removal retained an environment ownership receipt") + } +} diff --git a/cli/cmd/agentplugins/kiro_project_fixture_test.go b/cli/cmd/agentplugins/kiro_project_fixture_test.go new file mode 100644 index 000000000..b31571d8b --- /dev/null +++ b/cli/cmd/agentplugins/kiro_project_fixture_test.go @@ -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 kiroProjectDetector struct{ root string } + +func (detector kiroProjectDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientKiro, ConfigRoot: detector.root, Status: domain.DetectionDetected}}, nil +} + +// Reuse the core journey's production CLI/state helpers and real TLS Git/ZIP +// transports. Kiro supplies its own detected profile and native layout. +type kiroProjectJourney struct{ projectJourney } + +func newKiroProjectJourney(t *testing.T, transport *projectJourneyTransport) kiroProjectJourney { + t.Helper() + f := kiroProjectJourney{projectJourney{home: projectJourneyTemp(t), a: projectJourneyTemp(t), b: projectJourneyTemp(t), transport: transport}} + f.cache = filepath.Join(f.home, "uap") + for _, root := range []string{"", f.a, f.b} { + native := f.kiroRoot(root) + projectJourneyWrite(t, filepath.Join(native, "settings", "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 = configureKiroProjectJourney(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 configureKiroProjectJourney(app agentpluginscli.App, home string, transport *projectJourneyTransport) agentpluginscli.App { + app.Detector = kiroProjectDetector{root: filepath.Join(home, ".kiro")} + 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 configureKiroProjectJourney(selected, home, transport), nil + } + return app +} + +func (f kiroProjectJourney) kiroRoot(project string) string { + if project == "" { + return filepath.Join(f.home, ".kiro") + } + return filepath.Join(project, ".kiro") +} + +func (f kiroProjectJourney) assertKiroOwned(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.kiroRoot(project) || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("Kiro 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.kiroRoot(project)} { + if projectJourneyRead(t, filepath.Join(root, "skills", "docs", "SKILL.md")) != skill { + t.Fatal("Kiro managed/native skill bytes differ from the selected pin") + } + } + settings := projectJourneyRead(t, filepath.Join(f.kiroRoot(project), "settings", "mcp.json")) + for _, text := range []string{endpoint, "foreign.test", "foreignPreference", `"disabled":true`} { + if !strings.Contains(strings.ReplaceAll(settings, " ", ""), text) { + t.Fatalf("Kiro 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 kiroProjectCommitSource(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 Kiro project components") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + transport.mu.Lock() + defer transport.mu.Unlock() + transport.archive = projectJourneyArchive(t, transport.repo) +} diff --git a/cli/cmd/agentplugins/kiro_project_lifecycle_test.go b/cli/cmd/agentplugins/kiro_project_lifecycle_test.go new file mode 100644 index 000000000..18a7ab15b --- /dev/null +++ b/cli/cmd/agentplugins/kiro_project_lifecycle_test.go @@ -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 Kiro discovery, +// enablement, MCP initialization, model use and runtime are not certified. +func TestProjectKiroComposedSourceLifecycle(t *testing.T) { + for _, kind := range []string{"git", "zip"} { + t.Run(kind, func(t *testing.T) { + transport := newProjectJourneyTransport(t, kind) + kiroProjectCommitSource(t, transport, projectJourneySkill, "https://managed.test/v1") + f := newKiroProjectJourney(t, transport) + userBefore := securityFixtureTree(t, f.kiroRoot("")) + for _, root := range []string{f.a, f.b} { + f.run(t, root, "add", transport.endpoint, "--alias", "docs", "--target", "kiro") + f.assertKiroOwned(t, root, projectJourneySkill, "https://managed.test/v1") + } + bNative := securityFixtureTree(t, f.kiroRoot(f.b)) + bState := projectJourneyRead(t, filepath.Join(f.namespace(f.b), "state-v2.json")) + kiroProjectReconcileAndRepair(t, f) + kiroProjectUpdateAndTransfer(t, f) + kiroProjectRemove(t, f) + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, f.kiroRoot(""))) || !reflect.DeepEqual(bNative, securityFixtureTree(t, f.kiroRoot(f.b))) || bState != projectJourneyRead(t, filepath.Join(f.namespace(f.b), "state-v2.json")) { + t.Fatal("Kiro project-A lifecycle changed user or project-B native configuration/state") + } + f.assertKiroOwned(t, f.b, projectJourneySkill, "https://managed.test/v1") + }) + } +} + +func kiroProjectReconcileAndRepair(t *testing.T, f kiroProjectJourney) { + 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("Kiro locked no-op changed exact ownership receipts") + } + installation, _ := f.owned(t, f.a) + native := securityFixtureTree(t, f.kiroRoot(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.kiroRoot(f.a))) { + t.Fatal("Kiro project read commands changed native files or ownership receipts") + } + if err := os.RemoveAll(filepath.Join(f.kiroRoot(f.a), "skills", "docs")); err != nil { + t.Fatal(err) + } + f.run(t, f.a, "repair", installation.InstallationID, "--target", "kiro") + f.assertKiroOwned(t, f.a, projectJourneySkill, "https://managed.test/v1") + f.inspectNoRecovery(t, f.a) +} + +func kiroProjectUpdateAndTransfer(t *testing.T, f kiroProjectJourney) { + t.Helper() + previous, binding := f.owned(t, f.a) + kiroProjectCommitSource(t, f.transport, projectJourneyAdvancedSkill, "https://managed.test/v2") + f.run(t, f.a, "update", "docs", "--target", "kiro") + updated, selected := f.assertKiroOwned(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("Kiro update did not retain source/binding authority while advancing the pin") + } + pair := f.assertPortablePair(t, f.a) + exchange := projectJourneyTemp(t) + f.run(t, f.a, "deps", "export", exchange, "--yes") + if pair != f.assertPortablePair(t, exchange) { + t.Fatal("Kiro export changed exact portable pins") + } + cold := newKiroProjectJourney(t, f.transport) + projectJourneyAbsent(t, cold.cache, filepath.Join(cold.a, ".uap")) + cold.run(t, cold.a, "deps", "import", exchange, "--install", "--yes") + restored, _ := cold.assertKiroOwned(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("Kiro cold import changed source identity or portable pins") + } + cold.inspectNoRecovery(t, cold.a) +} + +func kiroProjectRemove(t *testing.T, f kiroProjectJourney) { + t.Helper() + _, binding := f.owned(t, f.a) + f.run(t, f.a, "remove", "docs", "--target", "kiro") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(f.kiroRoot(f.a), "skills", "docs")) + settings := projectJourneyRead(t, filepath.Join(f.kiroRoot(f.a), "settings", "mcp.json")) + if strings.Contains(settings, "managed.test") || !strings.Contains(settings, "foreign.test") || !strings.Contains(settings, "foreignPreference") { + t.Fatalf("Kiro removal changed foreign MCP settings or retained its entry: %s", settings) + } + if projectJourneyRead(t, filepath.Join(f.kiroRoot(f.a), "skills", "foreign", "SKILL.md")) != "foreign skill\n" || projectJourneyRead(t, filepath.Join(f.kiroRoot(f.a), "hooks.json")) != "foreign hooks\n" { + t.Fatal("Kiro removal changed foreign skill or hooks") + } + if len(f.state(t, f.a).EnvironmentReceipts) != 0 { + t.Fatal("Kiro removal retained an environment ownership receipt") + } +} diff --git a/cli/cmd/agentplugins/main_test.go b/cli/cmd/agentplugins/main_test.go index 93d7b7f8e..60c1bdab0 100644 --- a/cli/cmd/agentplugins/main_test.go +++ b/cli/cmd/agentplugins/main_test.go @@ -111,7 +111,7 @@ func TestInvalidScopeAndTargetDoNotCreateDataRoot(t *testing.T) { t.Setenv("AGENTPLUGINS_HOME", dataRoot) os.Args = args err := run() - if err == nil || (name == "project-scope" && !strings.Contains(err.Error(), "user scope")) { + if err == nil || (name == "project-scope" && !strings.Contains(err.Error(), "requires --target")) { t.Fatalf("invalid invocation error = %v", err) } if _, statErr := os.Lstat(dataRoot); !os.IsNotExist(statErr) { @@ -196,11 +196,11 @@ func TestOfflinePublicInstallerSecurityBoundary(t *testing.T) { } defer stderr.Close() os.Stdout, os.Stderr = stdout, stderr - argv := []string{"add", source, "--target=codex", "--scope=project", "--dry-run", "--format=json"} + argv := []string{"add", source, "--target=chatgpt", "--scope=project", "--dry-run", "--format=json"} os.Args = append([]string{"agentplugins"}, argv...) var out, errout bytes.Buffer err = executeRelease(context.Background(), argv, authoringcli.Streams{Out: &out, Err: &errout}, run) - if err == nil || out.Len() != 0 || errout.String() != "agentplugins: --scope project is not supported by the current client adapters; the public CLI supports user scope only\n" || len(transport.urls) != 0 { + if err == nil || out.Len() != 0 || errout.String() != "agentplugins: chatgpt does not support project scope\n" || len(transport.urls) != 0 { t.Fatalf("preflight boundary: %v %q %q requests=%v", err, out.String(), errout.String(), transport.urls) } if !reflect.DeepEqual(beforeState, securityFixtureTree(t, state)) || !reflect.DeepEqual(beforeSource, securityFixtureTree(t, source)) || !reflect.DeepEqual(beforeHome, securityFixtureTree(t, home)) { diff --git a/cli/cmd/agentplugins/project_chatgpt_fixture_test.go b/cli/cmd/agentplugins/project_chatgpt_fixture_test.go new file mode 100644 index 000000000..114428210 --- /dev/null +++ b/cli/cmd/agentplugins/project_chatgpt_fixture_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "bytes" + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type projectChatGPTBoundaries struct{ acquisition, detection, security int } + +func (spy *projectChatGPTBoundaries) Detect(context.Context) ([]domain.DetectedClient, error) { + spy.detection++ + return nil, errors.New("unexpected detection after unsupported project request") +} + +func (spy *projectChatGPTBoundaries) Evaluate(context.Context, domain.SecurityEvaluationInput) (domain.SecurityAssessment, error) { + spy.security++ + return domain.SecurityAssessment{}, errors.New("unexpected security evaluation after unsupported project request") +} + +func (spy *projectChatGPTBoundaries) rejectAcquisition() (domain.PackageSnapshot, error) { + spy.acquisition++ + return domain.PackageSnapshot{}, errors.New("unexpected acquisition after unsupported project request") +} + +func (spy *projectChatGPTBoundaries) AcquireLocal(context.Context, string) (domain.PackageSnapshot, error) { + return spy.rejectAcquisition() +} + +func (spy *projectChatGPTBoundaries) AcquireGitHub(context.Context, string, string, string) (domain.PackageSnapshot, error) { + return spy.rejectAcquisition() +} + +func (spy *projectChatGPTBoundaries) AcquireGitHubVerified(context.Context, string, string, string, string) (domain.PackageSnapshot, error) { + return spy.rejectAcquisition() +} + +func (spy *projectChatGPTBoundaries) DiscoverGitHubPackages(context.Context, string, string) ([]string, error) { + spy.acquisition++ + return nil, errors.New("unexpected discovery after unsupported project request") +} + +func projectChatGPTApp(app agentpluginscli.App, spy *projectChatGPTBoundaries) agentpluginscli.App { + app.Detector, app.SourceAcquirer, app.SecurityEvaluator = spy, spy, spy + app.DirectoryClient, app.DiscoveryClient, app.SecurityIndex = nil, nil, nil + app.DirectoryClientForDryRun, app.CommitDirectoryPreview = nil, nil + app.SourceAcquirerForDryRun, app.SecurityEvaluatorForDryRun = 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 projectChatGPTApp(selected, spy), nil + } + return app +} + +func projectChatGPTExecute(app agentpluginscli.App, project string, args ...string) (string, error) { + var output bytes.Buffer + app.Input, app.Output, app.ErrorOutput, app.Terminal = strings.NewReader(""), &output, &output, false + command := agentpluginscli.NewRoot(app) + command.SetOut(&output) + command.SetErr(&output) + args = append(args, "--scope", "project", "--format", "json") + if project != "" { + args = append(args, "--project-root", project) + } + command.SetArgs(args) + err := command.Execute() + return output.String(), err +} + +func projectChatGPTWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} diff --git a/cli/cmd/agentplugins/project_chatgpt_test.go b/cli/cmd/agentplugins/project_chatgpt_test.go new file mode 100644 index 000000000..f6b15eee8 --- /dev/null +++ b/cli/cmd/agentplugins/project_chatgpt_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +func TestProjectChatGPTCLIRefusesAllTargetedIntentsBeforeEffects(t *testing.T) { + app, user, a, b := projectCompositionFixture(t) + spy := &projectChatGPTBoundaries{} + app = projectChatGPTApp(app, spy) + source := filepath.Join(t.TempDir(), "package") + projectChatGPTWrite(t, filepath.Join(source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"chatgpt-scope-proof"}`) + for _, root := range []string{app.UserHome, a, b} { + projectChatGPTWrite(t, filepath.Join(root, "foreign.txt"), "preserve bytes and mode\n") + } + before := []map[string]string{securityFixtureTree(t, app.UserHome), securityFixtureTree(t, a), securityFixtureTree(t, b)} + for _, args := range [][]string{ + {"add", "acme/docs", "--target", "chatgpt"}, + {"add", source, "--no-save", "--target", "chatgpt"}, + {"add", "context7", "--target", "chatgpt", "--chatgpt-app-id", "plugin_asdk_app_0123456789abcdef0123456789abcdef"}, + {"add", source, "--no-save", "--target", "chatgpt", "--activation-complete", "--auth-complete"}, + {"add", source, "--no-save", "--target", "gemini,chatgpt"}, + {"install", source, "--no-save", "--target", "chatgpt"}, + {"update", "00000000-0000-4000-8000-000000000001", "--target", "chatgpt"}, + {"repair", "00000000-0000-4000-8000-000000000001", "--target", "chatgpt"}, + {"remove", "00000000-0000-4000-8000-000000000001", "--target", "chatgpt", "--external-uninstalled"}, + } { + output, err := projectChatGPTExecute(app, a, args...) + if err == nil || err.Error() != "chatgpt does not support project scope" || strings.Contains(output, "success") { + t.Fatalf("unsupported project request %v: %v\n%s", args, err, output) + } + } + t.Chdir(a) + if _, err := projectChatGPTExecute(app, "", "add", source, "--target", "chatgpt", "--no-save"); err == nil || err.Error() != "chatgpt does not support project scope" { + t.Fatalf("invocation-directory default bypassed project refusal: %v", err) + } + for index, root := range []string{app.UserHome, a, b} { + if !reflect.DeepEqual(before[index], securityFixtureTree(t, root)) { + t.Fatalf("unsupported project command changed user/A/B filesystem: %s", root) + } + } + projectChatGPTAssertNoEffects(t, spy, user, a) +} + +func TestProjectChatGPTSavedTargetRefusesBeforeLockAndAcquisition(t *testing.T) { + for _, args := range [][]string{{"deps", "lock"}, {"deps", "update", "docs"}, {"deps", "sync", "--yes"}, {"install", "--yes"}} { + t.Run(strings.Join(args, " "), func(t *testing.T) { + app, user, a, _ := projectCompositionFixture(t) + spy := &projectChatGPTBoundaries{} + app = projectChatGPTApp(app, spy) + projectChatGPTWrite(t, filepath.Join(a, "uap.toml"), "schema_version = 1\n[dependencies.docs]\ngit = \"https://github.com/acme/docs.git\"\nref = \"main\"\n[targets]\nchatgpt = [\"docs\"]\n") + before := securityFixtureTree(t, a) + output, err := projectChatGPTExecute(app, a, args...) + if err == nil { + t.Fatalf("saved unsupported project target succeeded: %s", output) + } + if !reflect.DeepEqual(before, securityFixtureTree(t, a)) { + t.Fatal("unsupported saved target created a namespace/lock or changed intent") + } + if err.Error() != "chatgpt does not support project scope" || strings.Contains(output, "success") { + t.Fatalf("saved unsupported project target guidance: %v\n%s", err, output) + } + projectChatGPTAssertNoEffects(t, spy, user, a) + }) + } +} + +func projectChatGPTAssertNoEffects(t *testing.T, spy *projectChatGPTBoundaries, user, project string) { + t.Helper() + if spy.acquisition != 0 || spy.detection != 0 || spy.security != 0 { + t.Fatalf("unsupported project crossed host boundaries: %+v", spy) + } + for _, path := range []string{user, filepath.Join(project, ".uap"), filepath.Join(project, "uap.lock")} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("unsupported project request created %s: %v", path, err) + } + } +} diff --git a/cli/cmd/agentplugins/project_cline_e2e_test.go b/cli/cmd/agentplugins/project_cline_e2e_test.go new file mode 100644 index 000000000..dd623450d --- /dev/null +++ b/cli/cmd/agentplugins/project_cline_e2e_test.go @@ -0,0 +1,133 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type projectClineDetector struct{ root string } + +func (d projectClineDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientCline, ConfigRoot: d.root, Status: domain.DetectionDetected}}, nil +} + +type projectClineNoNative struct{ calls int } + +func (r *projectClineNoNative) Run(_ context.Context, c legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("unexpected Cline native command %v", c.Argv) +} + +func configureProjectCline(t *testing.T, app agentpluginscli.App, user string, runner *projectClineNoNative) agentpluginscli.App { + t.Helper() + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{StateRoot: filepath.Dir(app.ManagedRoot), ManagedRoot: app.ManagedRoot, ProjectRoot: app.ProjectRoot, Registry: app.ClientRegistry, Runner: runner, EnableNativeObserver: true, HelperExecutable: helper}) + if err != nil { + t.Fatal(err) + } + app.Installer = engine + app.Detector = projectClineDetector{user} + app.Lifecycle.Activator = providers.Activator{Registry: app.ClientRegistry, Runner: runner} + app.Lifecycle.NativeObserver = providers.NativeIdentityObserver{Registry: app.ClientRegistry, Runner: runner} + 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 configureProjectCline(t, selected, user, runner), nil + } + return app +} + +// Real saved CLI/TLS Git/immutable pin/state and native file transactions. +// The explicit process boundary refuses every vendor or MCP command. +func TestProjectClineSavedLifecycleSeparatesUserAndProjects(t *testing.T) { + t.Setenv("CLINE_MCP_SETTINGS_PATH", "") + t.Setenv("CLINE_DATA_DIR", "") + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, ".cline") + projectJourneyWrite(t, filepath.Join(user, "keep"), "original user") + runner := &projectClineNoNative{} + journey.app = configureProjectCline(t, journey.app, user, runner) + journey.run(t, "", "add", transport.endpoint, "--alias", "docs", "--target", "cline") + userBefore := securityFixtureTree(t, user) + userStateBefore := projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) + for _, project := range []string{journey.a, journey.b} { + journey.run(t, project, "add", transport.endpoint, "--alias", "docs", "--target", "cline") + projectClineOwned(t, journey, project, projectJourneySkill) + } + bBefore := securityFixtureTree(t, journey.b) + installation, binding := journey.owned(t, journey.a) + beforeState := projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) + nativeBefore := securityFixtureTree(t, filepath.Join(journey.a, ".cline")) + configBefore := projectJourneyRead(t, filepath.Join(journey.a, ".uap", "clients", "cline", "mcp.json")) + if _, err := journey.execute(journey.a, "add", transport.endpoint, "--alias", "docs", "--target", "cline", "--activation-complete"); err == nil { + t.Fatal("saved replay accepted activation attestation") + } + if beforeState != projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) || !reflect.DeepEqual(nativeBefore, securityFixtureTree(t, filepath.Join(journey.a, ".cline"))) || configBefore != projectJourneyRead(t, filepath.Join(journey.a, ".uap", "clients", "cline", "mcp.json")) { + t.Fatal("denied saved attestation changed state/native files") + } + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "managed package drift") + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance Cline fixture") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "cline") + projectClineOwned(t, journey, journey.a, projectJourneySkill) + if err := os.RemoveAll(filepath.Join(journey.a, ".cline", "skills", "docs")); err != nil { + t.Fatal(err) + } + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "cline") + projectClineOwned(t, journey, journey.a, projectJourneySkill) + journey.run(t, journey.a, "update", "docs", "--target", "cline") + updated, current := projectClineOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator { + t.Fatal("saved update lost exact authority") + } + beforeCalls := runner.calls + status := journey.run(t, journey.a, "deps", "status") + if runner.calls != beforeCalls || !strings.Contains(status, "activation_prepared") || strings.Contains(status, "activation_active") { + t.Fatal("saved status called native or promoted activation", status) + } + journey.run(t, journey.a, "remove", "docs", "--target", "cline") + projectJourneyAbsent(t, current.TargetLocator, filepath.Join(journey.a, ".cline", "skills", "docs")) + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) || userStateBefore != projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) { + t.Fatal("A lifecycle changed original user native/state authority or B") + } + if runner.calls != 0 { + t.Fatal("file preparation invoked native Cline or MCP") + } +} + +func projectClineOwned(t *testing.T, j projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, b := j.owned(t, project) + if b.ClientID != "cline" || b.Scope != "project" || b.NativeProfileRoot != project || b.Activation != domain.ActivationPrepared || b.Verification != domain.VerificationInstalled { + t.Fatalf("Cline lost scoped Prepared authority: %+v", b) + } + if projectJourneyRead(t, filepath.Join(project, ".cline", "skills", "docs", "SKILL.md")) != expected { + t.Fatal("actual project skill differs") + } + body := projectJourneyRead(t, filepath.Join(project, ".uap", "clients", "cline", "mcp.json")) + if !strings.Contains(body, `"docs"`) { + t.Fatal("prepared MCP member missing") + } + return installation, b +} diff --git a/cli/cmd/agentplugins/project_cline_native_contract_linux_test.go b/cli/cmd/agentplugins/project_cline_native_contract_linux_test.go new file mode 100644 index 000000000..b1bb95c3a --- /dev/null +++ b/cli/cmd/agentplugins/project_cline_native_contract_linux_test.go @@ -0,0 +1,266 @@ +//go:build linux + +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestClineGateNativeMCPRequiresExactEnabledSourceRows(t *testing.T) { + const path = "/tmp/project/.uap/clients/cline/mcp.json" + row := func() map[string]any { + return map[string]any{"name": "marker", "transportType": "streamableHttp", "disabled": false, "path": path} + } + for _, kind := range []string{"exact", "empty", "disabled", "wrong-source", "extra", "duplicate", "malformed", "trailing", "duplicate-key"} { + t.Run(kind, func(t *testing.T) { + r := row() + rows := []map[string]any{r} + switch kind { + case "empty": + rows = make([]map[string]any, 0, 4) + case "disabled": + r["disabled"] = true + case "wrong-source": + r["path"] = "/tmp/user/mcp.json" + case "extra": + r["unknown"] = true + case "duplicate": + rows = append(rows, row()) + case "malformed": + r["disabled"] = "false" + } + body, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + if kind == "trailing" { + body = append(body, []byte(" {}")...) + } + if kind == "duplicate-key" { + body = []byte("[{\"name\":\"marker\",\"name\":\"marker\"}]") + } + err = clineGateMCP(body, path, []string{"marker"}) + if (err == nil) != (kind == "exact") { + t.Fatalf("kind=%s err=%v", kind, err) + } + }) + } +} +func TestClineGateSkillPathsKeepDuplicatesAndRejectSiblingMarkers(t *testing.T) { + const path = "/tmp/A/.cline/skills/marker/SKILL.md" + row := func() map[string]any { + return map[string]any{"name": "marker", "path": path, "description": "A", "instructions": "Inert A", "frontmatter": map[string]any{"disable-model-invocation": true}} + } + want := []clineGateSkill{{path: path, name: "marker", marker: "A"}} + for _, kind := range []string{"exact", "empty", "wrong-path", "duplicate", "contradictory-duplicate", "sibling", "sibling-foreign"} { + t.Run(kind, func(t *testing.T) { + r := row() + rows := []map[string]any{r} + switch kind { + case "empty": + rows = make([]map[string]any, 0, 4) + case "wrong-path": + r["path"] = "/tmp/user/marker/SKILL.md" + case "duplicate": + rows = append(rows, row()) + case "contradictory-duplicate": + extra := row() + extra["name"] = "other" + rows = append(rows, extra) + case "sibling": + extra := row() + extra["path"] = "/tmp/B/.cline/skills/marker/SKILL.md" + rows = append(rows, extra) + case "sibling-foreign": + extra := row() + extra["name"] = "foreign-b" + extra["path"] = "/tmp/B/.cline/skills/foreign-b/SKILL.md" + rows = append(rows, extra) + } + body, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + err = clineGateSkills(body, want, []string{"/tmp/B/.cline/skills"}) + if (err == nil) != (kind == "exact") { + t.Fatalf("kind=%s err=%v", kind, err) + } + }) + } +} +func TestClineGateSameNameInventoriesRejectEverySiblingRoot(t *testing.T) { + f := &clineGate{home: "/tmp/fixture/home", A: "/tmp/fixture/A", B: "/tmp/fixture/B"} + for _, project := range []string{"", f.A, f.B} { + t.Run(filepath.Base(project), func(t *testing.T) { + path := f.skill(project, "uap-cline-shared") + row := map[string]any{"name": "uap-cline-shared", "path": path, "description": "selected", "instructions": "Inert selected", "frontmatter": map[string]any{"disable-model-invocation": true}} + want := []clineGateSkill{{path: path, name: "uap-cline-shared", marker: "selected"}} + forbidden := f.sameNameForbidden(project) + count := 1 + if project == "" { + count = 2 + } + if len(forbidden) != count { + t.Fatal("same-name sibling table differs", forbidden) + } + body, err := json.Marshal([]map[string]any{row}) + if err != nil || clineGateSkills(body, want, forbidden) != nil { + t.Fatal("selected same-name inventory refused", err) + } + for _, root := range forbidden { + extra := map[string]any{"name": "foreign-sibling", "path": filepath.Join(root, "foreign-sibling", "SKILL.md"), "description": "foreign", "instructions": "Inert foreign", "frontmatter": map[string]any{}} + body, err = json.Marshal([]map[string]any{row, extra}) + if err != nil { + t.Fatal(err) + } + if err := clineGateSkills(body, want, forbidden); err == nil || !strings.Contains(err.Error(), "sibling/removed") { + t.Fatalf("differently named sibling skill admitted:%s %v", root, err) + } + } + }) + } +} +func TestClineGateChildArgumentsAndEnvironmentAreClosed(t *testing.T) { + for _, args := range [][]string{{"--version"}, {"config", "skills", "--json"}, {"config", "mcp", "--json"}} { + if !reflectClineArgs(args) { + t.Fatal("reviewed command refused") + } + } + for _, args := range [][]string{{"config"}, {"config", "tools", "--json"}, {"mcp", "list"}, {"--help"}, {"--version", "--extra"}, {"config", "mcp", "--json", "--extra"}} { + if reflectClineArgs(args) { + t.Fatal("unreviewed command accepted", args) + } + } + f := &clineGate{t: t, root: "/tmp/fixture", home: "/tmp/fixture/home", A: "/tmp/fixture/A", B: "/tmp/fixture/B"} + t.Setenv("CLINE_MCP_SETTINGS_PATH", "/foreign") + t.Setenv("GITHUB_TOKEN", "inert") + base := strings.Join(f.env(""), "\n") + if strings.Contains(base, "GITHUB_TOKEN") || strings.Contains(base, "CLINE_MCP_SETTINGS_PATH") || !strings.Contains(base, "CLINE_NO_AUTO_UPDATE=1") { + t.Fatal("ambient authority leaked") + } + if !strings.Contains(strings.Join(f.env(f.A), "\n"), "CLINE_MCP_SETTINGS_PATH="+f.projectMCP(f.A)) { + t.Fatal("exact override missing") + } + for label := range clineGateExpectedUsed() { + if clineGateClass(label) == "" { + t.Fatal("finite table inconsistent", label) + } + } + if clineGateClass("unreviewed") != "" || len(clineGateExpectedUsed()) != 38 { + t.Fatal("finite table grew unexpectedly") + } +} +func TestClineGateLiveBoundRetainsLimitAndCancelsBeforeOverflow(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + b := &clineGateBuffer{cancel: cancel} + n, err := b.Write([]byte(strings.Repeat("x", clineGateStream+1))) + body, overflow := b.snapshot() + if err == nil || n != clineGateStream || len(body) != clineGateStream || !overflow || ctx.Err() == nil { + t.Fatal("live limit not enforced") + } +} +func TestClineGatePublicProgressIsExactAndNeverAnArbitraryWarning(t *testing.T) { + cases := []struct{ label, expected string }{ + {"add-A", "Resolving and validating one Agent Plugin package for every selected target...\nInstalling uap-cline-a-package for 1 selected clients...\n"}, + {"repeat-add-A", "Resolving and validating one Agent Plugin package for every selected target...\nApplying transactional client package...\n"}, + {"update-A", "Resolving and validating one updated Agent Plugin package for every selected target...\n"}, + {"missing-repair-A", "Resolving and validating each unique exact installed package revision once...\n"}, {"remove-A", ""}} + for _, item := range cases { + if !clineGateAcceptProgress(item.label, []byte(item.expected)) { + t.Fatal("source-established progress refused", item.label) + } + for _, bad := range []string{item.expected + "warning\n", item.expected + item.expected + "extra\n", "prefix" + item.expected, strings.ReplaceAll(item.expected, "1 selected", "2 selected")} { + if bad != item.expected && clineGateAcceptProgress(item.label, []byte(bad)) { + t.Fatal("arbitrary/changed progress accepted", item.label) + } + } + } + if clineGateAcceptProgress("unknown", nil) { + t.Fatal("unknown progress admitted") + } +} +func clineGateSourceCleanup(t *testing.T, f *clineGate) { + t.Helper() + t.Cleanup(func() { + f.cancel() + result := f.cleanup() + if result.err != nil || result.exit != 0 || result.overflow { + t.Errorf("owned source cleanup:%v", result.err) + } + }) +} +func TestClineGateSourceUsesPublicCobraAndUneditedEnabledFiles(t *testing.T) { + f := newClineGate(t) + clineGateSourceCleanup(t, f) + source := f.prepareUnique() + before := f.snapshot(f.ownedRoots()...) + nativeRoots := []string{filepath.Join(f.A, ".cline", "skills"), f.projectMCP(f.A)} + nativeStable := f.snapshot(nativeRoots...) + for _, step := range []struct{ label, verb, selector string }{{"repeat-add-A", "add", source}, {"unchanged-update-A", "update", "uap-cline-a-package"}, {"intact-repair-A", "repair", "uap-cline-a-package"}} { + args := []string{step.verb, step.selector} + if step.verb == "add" { + args = append(args, "--no-save") + } + clineGateMutation(t, f.uap(step.label, f.A, args...), step.verb == "repair") + if step.verb != "repair" { + f.requireSnapshot(before, f.ownedRoots()...) + } else { + f.requireSnapshot(nativeStable, nativeRoots...) + } + } + if f.runner.calls != 0 { + t.Fatal("source stage called native/MCP") + } + for _, project := range []string{f.A, f.B} { + f.assertPrepared(project, "") + } + if !strings.Contains(projectJourneyRead(t, f.projectMCP(f.A)), "http://127.0.0.1:9/A-ONE") { + t.Fatal("emitted enabled marker changed") + } + if _, err := os.Stat(f.skill(f.A, "uap-cline-a")); err != nil { + t.Fatal(err) + } +} +func TestClineGatePublicSourceUpdateMissingRepairAndRemove(t *testing.T) { + f := newClineGate(t) + clineGateSourceCleanup(t, f) + source := f.prepareUnique() + roots := []string{filepath.Join(f.B, ".uap"), filepath.Join(f.B, ".cline"), filepath.Join(f.home, ".cline"), filepath.Join(f.cache, "state-v2.json")} + other := f.snapshot(roots...) + f.source("A", "uap-cline-a-package", "uap-cline-a", "A-TWO") + clineGateMutation(t, f.uap("update-A", f.A, "update", source), true) + if !strings.Contains(projectJourneyRead(t, f.skill(f.A, "uap-cline-a")), "A-TWO") { + t.Fatal("public update missing") + } + if err := os.RemoveAll(filepath.Dir(f.skill(f.A, "uap-cline-a"))); err != nil { + t.Fatal(err) + } + clineGateMutation(t, f.uap("missing-repair-A", f.A, "repair", "uap-cline-a-package"), true) + if !strings.Contains(projectJourneyRead(t, f.skill(f.A, "uap-cline-a")), "A-TWO") { + t.Fatal("public repair did not restore selected local bytes") + } + f.inspect("repaired-A-inspection.json", f.A) + clineGateMutation(t, f.uap("remove-A", f.A, "remove", "uap-cline-a-package"), true) + if _, err := os.Lstat(f.skill(f.A, "uap-cline-a")); !os.IsNotExist(err) { + t.Fatal("owned skill retained after removal") + } + object := clineGateObject(t, []byte(projectJourneyRead(t, f.projectMCP(f.A)))) + servers, ok := object["mcpServers"].(map[string]any) + if !ok { + t.Fatal("remaining MCP object missing") + } + if _, ok := servers["uap-cline-a"]; ok { + t.Fatal("owned MCP member retained") + } + f.foreign(f.A) + f.requireSnapshot(other, roots...) + if f.runner.calls != 0 { + t.Fatal("source-only lifecycle launched native") + } +} diff --git a/cli/cmd/agentplugins/project_cline_native_files_linux_test.go b/cli/cmd/agentplugins/project_cline_native_files_linux_test.go new file mode 100644 index 000000000..4c97070b8 --- /dev/null +++ b/cli/cmd/agentplugins/project_cline_native_files_linux_test.go @@ -0,0 +1,191 @@ +//go:build linux + +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" +) + +func clineGateClass(label string) string { + for _, item := range []struct { + class string + labels []string + }{ + {"git", []string{"git-user", "git-A", "git-B"}}, + {"public_uap", []string{"add-user", "add-A", "add-B", "repeat-add-A", "unchanged-update-A", "intact-repair-A", "update-A", "missing-repair-A", "remove-A", "add-same-user", "add-same-A", "add-same-B"}}, + {"native", []string{"version", "initial-A-skills", "initial-B-skills", "default-A", "default-B", "override-A", "override-B", "cross-A-from-B", "restart-default-A", "stable-A-skills", "stable-A-mcp", "updated-A-skills", "updated-A-mcp", "repaired-A-skills", "repaired-A-mcp", "removed-A-skills", "removed-A-mcp", "same-user-skills", "same-A-skills", "same-B-skills", "same-user-mcp", "same-A-mcp", "same-B-mcp"}}, + {"cleanup", []string{"cleanup"}}, + } { + for _, name := range item.labels { + if label == name { + return item.class + } + } + } + return "" +} +func clineGateExpectedUsed() map[string]bool { + result := map[string]bool{} + for _, label := range []string{"git-user", "git-A", "git-B", "add-user", "add-A", "add-B", "repeat-add-A", "unchanged-update-A", "intact-repair-A", "update-A", "missing-repair-A", "remove-A", "add-same-user", "add-same-A", "add-same-B", "version", "initial-A-skills", "initial-B-skills", "default-A", "default-B", "override-A", "override-B", "cross-A-from-B", "restart-default-A", "stable-A-skills", "stable-A-mcp", "updated-A-skills", "updated-A-mcp", "repaired-A-skills", "repaired-A-mcp", "removed-A-skills", "removed-A-mcp", "same-user-skills", "same-A-skills", "same-B-skills", "same-user-mcp", "same-A-mcp", "same-B-mcp"} { + result[label] = true + } + return result +} +func (f *clineGate) admit(label, class string) { + f.t.Helper() + if f.used[label] || clineGateClass(label) != class || len(f.used) >= 60 { + f.t.Fatal("command outside finite table") + } +} +func (f *clineGate) gitRoots() { + for _, item := range []struct{ label, path string }{{"git-user", f.user}, {"git-A", f.A}, {"git-B", f.B}} { + f.admit(item.label, "git") + args := []string{"-c", "core.hooksPath=/dev/null", "init", "--quiet", "--template=", "--initial-branch=main"} + r := captureClineGate(f.ctx, "/usr/bin/git", item.path, f.env(""), args, 10*time.Second) + f.record(item.label, "git", item.path, append([]string{"/usr/bin/git"}, args...), r) + if r.err != nil || r.exit != 0 || r.overflow || len(r.stderr) != 0 { + f.t.Fatal("fresh inert Git boundary failed") + } + info, err := os.Lstat(filepath.Join(item.path, ".git")) + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + f.t.Fatal("exact Git boundary missing") + } + } +} +func clineGateBinaryPin(path string) (result error) { + info, err := os.Lstat(path) + if err != nil { + return err + } + st, ok := info.Sys().(*syscall.Stat_t) + if !ok || !info.Mode().IsRegular() || st.Nlink != 1 || info.Size() != 132511200 || !filepath.IsAbs(path) { + return fmt.Errorf("native binary type/length/path differs") + } + file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) + if err != nil { + return err + } + defer func() { result = errors.Join(result, file.Close()) }() + opened, err := file.Stat() + if err != nil || !os.SameFile(info, opened) { + return fmt.Errorf("native binary opened identity differs") + } + hash := sha256.New() + n, err := io.Copy(hash, io.LimitReader(file, 132511201)) + if err != nil { + return err + } + after, ae := file.Stat() + current, ce := os.Lstat(path) + if ae != nil || ce != nil || n != 132511200 || !os.SameFile(info, after) || !os.SameFile(info, current) || !after.ModTime().Equal(info.ModTime()) || !current.ModTime().Equal(info.ModTime()) || hex.EncodeToString(hash.Sum(nil)) != clineGatePin { + return fmt.Errorf("native binary pin/held identity changed") + } + return nil +} +func (f *clineGate) begin(stage string) { + f.current = stage + f.stages[stage] = map[string]string{"status": "attempting", "reason": "in progress"} +} +func (f *clineGate) cleanup() clineGateCapture { + info, err := os.Lstat(f.root) + if err != nil { + return clineGateCapture{exit: -1, err: err} + } + st, ok := info.Sys().(*syscall.Stat_t) + if !os.SameFile(info, f.rootIdentity) { + return clineGateCapture{exit: -1, err: fmt.Errorf("cleanup task root replaced")} + } + if !ok || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return clineGateCapture{exit: -1, err: fmt.Errorf("cleanup task identity unavailable")} + } + code := strings.Join([]string{ + "import os,stat,sys", + "root,dev,ino,uid=sys.argv[1:]; parent,name=os.path.split(root)", + "p=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW); fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=p)", + "identity=os.fstat(fd)", + "if (identity.st_dev,identity.st_ino,identity.st_uid)!=(int(dev),int(ino),int(uid)):raise SystemExit('root authority changed')", + "count=[0]", + "def clear(fd,depth):", + " if depth>32:raise RuntimeError('cleanup depth')", + " os.fchmod(fd,os.fstat(fd).st_mode|0o700)", + " with os.scandir(fd) as entries:", + " for entry in entries:", + " count[0]+=1", + " if count[0]>8192:raise RuntimeError('cleanup entry bound')", + " item=os.stat(entry.name,dir_fd=fd,follow_symlinks=False)", + " if item.st_uid!=int(uid):raise RuntimeError('foreign cleanup owner')", + " if stat.S_ISDIR(item.st_mode):", + " child=os.open(entry.name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)", + " try:", + " if not os.path.samestat(item,os.fstat(child)):raise RuntimeError('directory changed')", + " clear(child,depth+1)", + " finally:os.close(child)", + " os.rmdir(entry.name,dir_fd=fd)", + " else:os.unlink(entry.name,dir_fd=fd)", + "clear(fd,0)", + "if not os.path.samestat(identity,os.stat(name,dir_fd=p,follow_symlinks=False)):raise SystemExit('cleanup root replaced')", + "os.close(fd);os.rmdir(name,dir_fd=p);os.close(p)", + }, "\n") + return captureClineGate(context.Background(), "/usr/bin/python3", "/", []string{"PATH=/usr/bin:/bin", "LANG=C.UTF-8"}, []string{"-I", "-c", code, f.root, strconv.FormatUint(st.Dev, 10), strconv.FormatUint(st.Ino, 10), strconv.FormatUint(uint64(st.Uid), 10)}, 4500*time.Millisecond) +} +func (f *clineGate) finish() { + f.cancel() + if f.t.Failed() && f.current != "" { + f.stages[f.current] = map[string]string{"status": "failed", "reason": "see exact bounded command/source evidence"} + } + cleanup := f.cleanup() + f.record("cleanup", "cleanup", "/", nil, cleanup) + complete := !f.t.Failed() && cleanup.err == nil && cleanup.exit == 0 && !cleanup.overflow + f.evidence["collection_complete"], f.evidence["cleanup_complete"] = complete, cleanup.err == nil && cleanup.exit == 0 && !cleanup.overflow + f.evidence["cleanup_error"] = clineGateError(cleanup.err) + f.evidence["commands"], f.evidence["stages"] = f.commands, f.stages + f.evidence["finished_utc"] = time.Now().UTC().Format(time.RFC3339Nano) + hashes := map[string]string{} + for name, body := range f.files { + hashes[name] = clineGateSHA(body) + } + f.evidence["artifact_sha256"] = hashes + body, err := json.MarshalIndent(f.evidence, "", " ") + if err != nil || len(body) > 1<<20 { + f.t.Error("bounded evidence summary failed") + return + } + f.keep("cline-project-evidence.json", append(body, '\n')) + if f.output == "" { + f.output = f.t.TempDir() + } + info, err := os.Lstat(f.output) + resolved, re := filepath.EvalSymlinks(f.output) + if err != nil || re != nil || resolved != f.output || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || f.outputIdentity != nil && !os.SameFile(info, f.outputIdentity) { + f.t.Error("exact evidence directory missing/replaced") + return + } + for name, body := range f.files { + file, err := os.OpenFile(filepath.Join(f.output, name), os.O_WRONLY|os.O_CREATE|os.O_EXCL|syscall.O_NOFOLLOW, 0600) + if err != nil { + f.t.Error(err) + return + } + _, we := file.Write(body) + ce := file.Close() + if we != nil || ce != nil { + f.t.Error("evidence write/close failed") + return + } + } + if !complete { + f.t.Error("Cline project gate or exact task cleanup incomplete") + } +} diff --git a/cli/cmd/agentplugins/project_cline_native_fixture_linux_test.go b/cli/cmd/agentplugins/project_cline_native_fixture_linux_test.go new file mode 100644 index 000000000..c4f66926b --- /dev/null +++ b/cli/cmd/agentplugins/project_cline_native_fixture_linux_test.go @@ -0,0 +1,353 @@ +//go:build linux + +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "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/installer" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/ports" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +const clineGatePin = "1ff8841d17d4d83763134c193d414e4f7fcf2ef56af759e5ebbe96727fc2c8f7" +const clineGateStream = 128 << 10 +const clineGateEnvelope = 32 << 20 + +type clineGateBuffer struct { + mu sync.Mutex + body []byte + overflow bool + cancel context.CancelFunc +} + +func (b *clineGateBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + n := min(len(p), clineGateStream-len(b.body)) + b.body = append(b.body, p[:n]...) + if n != len(p) { + b.overflow = true + b.cancel() + return n, fmt.Errorf("cline gate stream limit") + } + return n, nil +} +func (b *clineGateBuffer) snapshot() ([]byte, bool) { + b.mu.Lock() + defer b.mu.Unlock() + return append([]byte(nil), b.body...), b.overflow +} + +type clineGateCapture struct { + out, stderr []byte + exit int + err error + overflow bool + elapsed time.Duration + argv []string +} + +func captureClineGate(parent context.Context, executable, cwd string, env, args []string, timeout time.Duration) clineGateCapture { + ctx, cancel := context.WithTimeout(parent, timeout) + defer cancel() + out, stderr := &clineGateBuffer{cancel: cancel}, &clineGateBuffer{cancel: cancel} + cmd := exec.CommandContext(ctx, executable, args...) + cmd.Dir, cmd.Env, cmd.Stdin = cwd, append([]string(nil), env...), nil + cmd.Stdout, cmd.Stderr, cmd.WaitDelay = out, stderr, 250*time.Millisecond + start := time.Now() + err := cmd.Run() + result := clineGateCapture{exit: -1, err: err, elapsed: time.Since(start), argv: append([]string{executable}, args...)} + var a, b bool + result.out, a = out.snapshot() + result.stderr, b = stderr.snapshot() + result.overflow = a || b + if cmd.ProcessState != nil { + result.exit = cmd.ProcessState.ExitCode() + } + if ctx.Err() != nil { + result.err = fmt.Errorf("child deadline/cancellation: %w", ctx.Err()) + } + return result +} + +type clineGate struct { + t *testing.T + ctx context.Context + cancel context.CancelFunc + root, home, user, A, B, cache, binary, output string + journey projectJourney + runner *projectClineNoNative + files map[string][]byte + bytes int + commands []map[string]any + used map[string]bool + stages map[string]map[string]string + evidence map[string]any + current string + rootIdentity, outputIdentity os.FileInfo +} + +func requireClineGate(t *testing.T) { + t.Helper() + if os.Getenv("AGENTPLUGINS_CLINE_PROJECT_NATIVE_E2E") != "1" { + t.Skip("opt-in current-source Cline read-only project config gate") + } + if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" || os.Getuid() <= 0 || os.Getuid() != os.Geteuid() || os.Getgid() <= 0 || os.Getgid() != os.Getegid() { + t.Fatal("original nonroot Linux amd64 identity required") + } + for key, want := range map[string]string{"GITHUB_ACTIONS": "true", "RUNNER_ENVIRONMENT": "github-hosted", "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED": "1"} { + if os.Getenv(key) != want { + t.Fatal("genuine disposable hosted admission required") + } + } +} +func newClineGate(t *testing.T) *clineGate { + t.Helper() + parent := os.Getenv("AGENTPLUGINS_CLINE_FIXTURE_BASE") + if parent == "" { + parent = t.TempDir() + } + root, err := os.MkdirTemp(parent, "uap-cline-project-") + if err != nil { + t.Fatal(err) + } + root, err = filepath.EvalSymlinks(root) + if err != nil { + t.Fatal(err) + } + f := &clineGate{t: t, root: root, files: map[string][]byte{}, used: map[string]bool{}, stages: map[string]map[string]string{}} + f.rootIdentity, err = os.Lstat(root) + if err != nil { + t.Fatal(err) + } + work := 360 + if raw := os.Getenv("AGENTPLUGINS_CLINE_WORK_SECONDS"); raw != "" { + parsed, parseErr := strconv.Atoi(raw) + if parseErr != nil || parsed < 1 || parsed > 360 { + t.Fatal("bounded Cline work deadline required") + } + work = parsed + } + f.ctx, f.cancel = context.WithTimeout(context.Background(), time.Duration(work)*time.Second) + f.home, f.user, f.A, f.B = filepath.Join(root, "home"), filepath.Join(root, "user-cwd"), filepath.Join(root, "A"), filepath.Join(root, "B") + f.cache = filepath.Join(root, "installer-state") + f.binary, f.output = os.Getenv("AGENTPLUGINS_CLINE_BIN"), os.Getenv("AGENTPLUGINS_CLINE_EVIDENCE") + if f.output != "" { + resolved, e := filepath.EvalSymlinks(f.output) + if e != nil || resolved != f.output { + t.Fatal("exact evidence output required") + } + f.outputIdentity, err = os.Lstat(f.output) + if err != nil || !f.outputIdentity.IsDir() { + t.Fatal("ordinary evidence output required") + } + } + for _, p := range []string{f.home, f.user, f.A, f.B, f.cache, filepath.Join(root, "tmp"), filepath.Join(root, "sources")} { + if err := os.MkdirAll(p, 0700); err != nil { + t.Fatal(err) + } + } + for _, key := range []string{"HOME", "USERPROFILE"} { + t.Setenv(key, f.home) + } + for key, sub := range map[string]string{"XDG_CONFIG_HOME": "config", "XDG_CACHE_HOME": "cache", "XDG_DATA_HOME": "data", "XDG_STATE_HOME": "state"} { + t.Setenv(key, filepath.Join(f.home, sub)) + } + t.Setenv("PATH", "/usr/bin:/bin") + for _, key := range []string{"CLINE_MCP_SETTINGS_PATH", "CLINE_DATA_DIR", "CLINE_DIR"} { + t.Setenv(key, "") + } + t.Setenv("CLINE_NO_AUTO_UPDATE", "1") + 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.runner = &projectClineNoNative{} + app = configureClineGate(t, app, filepath.Join(f.home, ".cline"), f.runner) + f.journey = projectJourney{app: clineGateSecurity(app), home: f.home, cache: f.cache, a: f.A, b: f.B} + f.evidence = map[string]any{"schema_version": 1, "client": "cline", "version": "3.0.73", "source_commit": os.Getenv("EXPECTED_COMMIT"), "source_tree": os.Getenv("AGENTPLUGINS_INSTALLER_TREE"), "collection_complete": false, + "original_uid": os.Getuid(), "original_gid": os.Getgid(), "task_home": f.home, "fixture_root": f.root, + "source_composition": "public Cobra and production scoped installer/file transactions; explicit disposable client authority and inert security admission; not automatic IDE detection", + "network": "not isolated; only source-established read-only config dispatch; no connection-attempt or initialization claim"} + for _, key := range []string{"native_project_activation_qualified", "native_mcp_initialization_qualified", "native_runtime_qualified", "native_model_use_qualified", "native_ide_qualified", "native_trust_qualified", "same_name_active_isolation_qualified", "native_skill_execution_qualified", "native_authentication_qualified", "native_session_policy_qualified"} { + f.evidence[key] = false + } + for _, label := range []string{"source_install", "unique_discovery", "stable_lifecycle", "update", "missing_repair", "remove", "same_name_precedence", "foreign_preservation"} { + f.stages[label] = map[string]string{"status": "not_evaluated", "reason": "not reached"} + } + return f +} +func configureClineGate(t *testing.T, app agentpluginscli.App, user string, runner *projectClineNoNative) agentpluginscli.App { + t.Helper() + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{StateRoot: filepath.Dir(app.ManagedRoot), ManagedRoot: app.ManagedRoot, ProjectRoot: app.ProjectRoot, Registry: app.ClientRegistry, Runner: runner, EnableNativeObserver: true, HelperExecutable: helper}) + if err != nil { + t.Fatal(err) + } + // Preserve the production kernel, stager and transition objects. + activator, ok := app.Lifecycle.Activator.(providers.Activator) + if !ok || activator.NativeConfig == nil { + t.Fatal("production activator kernel missing") + } + observer, ok := app.Lifecycle.NativeObserver.(providers.NativeIdentityObserver) + if !ok || observer.NativeConfig == nil { + t.Fatal("production observer kernel missing") + } + activator.Runner, observer.Runner = runner, runner + app.Installer, app.Detector = engine, projectClineDetector{user} + app.Lifecycle.Activator, app.Lifecycle.NativeObserver = activator, observer + 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 configureClineGate(t, selected, user, runner), nil + } + return app +} +func clineGateSecurity(app agentpluginscli.App) agentpluginscli.App { + app.SecurityIndex, app.DirectoryClient, app.DiscoveryClient = nil, nil, nil + app.SecurityEvaluator = projectJourneySecurity{} + app.SecurityEvaluatorForDryRun = func(string) ports.PackageSecurityEvaluator { return projectJourneySecurity{} } + 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 clineGateSecurity(selected), nil + } + return app +} +func (f *clineGate) env(override string) []string { + env := []string{"HOME=" + f.home, "USERPROFILE=" + f.home, "PATH=/usr/bin:/bin", "LANG=C.UTF-8", "TMPDIR=" + filepath.Join(f.root, "tmp"), + "XDG_CONFIG_HOME=" + filepath.Join(f.home, "config"), "XDG_CACHE_HOME=" + filepath.Join(f.home, "cache"), "XDG_DATA_HOME=" + filepath.Join(f.home, "data"), "XDG_STATE_HOME=" + filepath.Join(f.home, "state"), + "CLINE_NO_AUTO_UPDATE=1", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=" + filepath.Join(f.home, "git-config"), "GIT_TERMINAL_PROMPT=0"} + if override != "" { + if override != f.A && override != f.B { + f.t.Fatal("unreviewed MCP override") + } + env = append(env, "CLINE_MCP_SETTINGS_PATH="+f.projectMCP(override)) + } + return env +} +func (f *clineGate) keep(name string, body []byte) { + f.t.Helper() + _, exists := f.files[name] + if filepath.Base(name) != name || name == "" || exists || len(f.files) >= 120 || len(body) > 1<<20 || f.bytes+len(body) > clineGateEnvelope-(1<<20) { + f.t.Fatal("bounded evidence admission refused") + } + f.files[name] = append([]byte(nil), body...) + f.bytes += len(body) +} +func clineGateSHA(body []byte) string { h := sha256.Sum256(body); return hex.EncodeToString(h[:]) } +func clineGateError(err error) string { + if err == nil { + return "" + } + if len(err.Error()) > 512 { + return err.Error()[:512] + } + return err.Error() +} +func (f *clineGate) record(label, class, cwd string, argv []string, r clineGateCapture) { + f.t.Helper() + f.admit(label, class) + f.used[label] = true + f.keep(label+".stdout.log", r.out) + f.keep(label+".stderr.log", r.stderr) + if len(r.argv) != 0 { + argv = r.argv + } + f.commands = append(f.commands, map[string]any{"label": label, "class": class, "in_process": class == "public_uap", "argv": argv, "cwd": cwd, "exit": r.exit, "error": clineGateError(r.err), "overflow": r.overflow, "elapsed_ms": r.elapsed.Milliseconds(), + "stdout_file": label + ".stdout.log", "stderr_file": label + ".stderr.log", "stdout_bytes": len(r.out), "stderr_bytes": len(r.stderr), "stdout_sha256": clineGateSHA(r.out), "stderr_sha256": clineGateSHA(r.stderr)}) +} +func (f *clineGate) uap(label, project string, args ...string) map[string]any { + f.t.Helper() + f.admit(label, "public_uap") + ctx, cancel := context.WithCancel(f.ctx) + defer cancel() + stdout, stderr := &clineGateBuffer{cancel: cancel}, &clineGateBuffer{cancel: cancel} + app := f.journey.app + app.Input, app.Output, app.ErrorOutput, app.Terminal = strings.NewReader(""), stdout, stderr, false + root := agentpluginscli.NewRoot(app) + root.SetOut(stdout) + root.SetErr(stderr) + args = append(args, "--target", "cline", "--format", "json") + if project != "" { + args = append(args, "--scope", "project", "--project-root", project) + } + root.SetArgs(args) + start := time.Now() + err := root.ExecuteContext(ctx) + out, a := stdout.snapshot() + diag, b := stderr.snapshot() + r := clineGateCapture{out: out, stderr: diag, exit: 0, err: err, overflow: a || b, elapsed: time.Since(start)} + if err != nil { + r.exit = 1 + } + f.record(label, "public_uap", project, args, r) + if err != nil || r.overflow || !clineGateAcceptProgress(label, diag) || f.runner.calls != 0 { + f.t.Fatalf("public UAP stage %s failed: error=%v stdout=%q stderr=%q", label, err, out, diag) + } + value := clineGateObject(f.t, out) + if value["result"] != "success" { + f.t.Fatal("public UAP did not report success") + } + if project != "" && args[0] != "remove" { + f.assertPrepared(project, "") + } + return value +} +func (f *clineGate) binding(project, name string) domain.ClientBinding { + f.t.Helper() + for _, installation := range f.journey.state(f.t, project).Installations { + if name != "" && installation.DeclaredName != name { + continue + } + for _, binding := range installation.Clients { + if binding.ClientID == "cline" && binding.Materialization != domain.MaterializationAbsent { + return binding + } + } + } + f.t.Fatal("exact binding not found") + return domain.ClientBinding{} +} +func (f *clineGate) assertPrepared(project, name string) { + b := f.binding(project, name) + if b.Scope != "project" || b.NativeProfileRoot != project || b.Activation != domain.ActivationPrepared || b.Verification != domain.VerificationInstalled || b.NativeActivationAttempt != "" || b.PendingNativeIntent != nil || b.PackageRevision == nil || b.PackageRevision.TreeDigest == "" || b.ClientBindingID == "" || len(b.NativeObjects) == 0 { + f.t.Fatalf("project lifecycle/receipt not Prepared: %+v", b) + } +} +func (f *clineGate) requireSnapshot(want map[string]string, roots ...string) { + f.t.Helper() + if got := f.snapshot(roots...); !reflect.DeepEqual(got, want) { + f.t.Fatal("owned/foreign bytes changed across read-only or other-project stage") + } +} diff --git a/cli/cmd/agentplugins/project_cline_native_lifecycle_linux_test.go b/cli/cmd/agentplugins/project_cline_native_lifecycle_linux_test.go new file mode 100644 index 000000000..532e84ea0 --- /dev/null +++ b/cli/cmd/agentplugins/project_cline_native_lifecycle_linux_test.go @@ -0,0 +1,353 @@ +//go:build linux + +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +func (f *clineGate) source(id, plugin, skill, marker string) string { + root := filepath.Join(f.root, "sources", id) + manifest, _ := json.Marshal(map[string]any{"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": plugin, "version": "1.0.0"}) + mcp, _ := json.Marshal(map[string]any{"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "mcpServers": map[string]any{skill: map[string]any{"type": "streamable-http", "url": "http://127.0.0.1:9/" + marker}}}) + projectJourneyWrite(f.t, filepath.Join(root, "plugin.json"), string(manifest)) + projectJourneyWrite(f.t, filepath.Join(root, "mcp.json"), string(mcp)) + projectJourneyWrite(f.t, filepath.Join(root, "skills", skill, "SKILL.md"), fmt.Sprintf("---\nname: %s\ndescription: %s\ndisable-model-invocation: true\n---\n%s\nInert text only. No command, model, hook or server execution.\n", skill, marker, marker)) + return root +} +func (f *clineGate) seedForeign() { + for _, item := range []struct{ root, path, label string }{{f.home, f.userMCP(), "user"}, {f.A, f.projectMCP(f.A), "a"}, {f.B, f.projectMCP(f.B), "b"}} { + body, _ := json.Marshal(map[string]any{"mcpServers": map[string]any{"foreign-" + item.label: map[string]any{"transport": map[string]any{"type": "streamableHttp", "url": "http://127.0.0.1:9/FOREIGN-" + item.label}}}, "foreignPreference": "retain-" + item.label}) + projectJourneyWrite(f.t, item.path, string(body)) + projectJourneyWrite(f.t, f.skill(item.root, "foreign-"+item.label), fmt.Sprintf("---\nname: foreign-%s\ndescription: Foreign fixture\ndisable-model-invocation: true\n---\nRetain exact unrelated inert bytes.\n", item.label)) + } + for _, project := range []string{f.A, f.B} { + projectJourneyWrite(f.t, filepath.Join(project, ".uap", "data", "caller-owned", "keep.txt"), "caller-owned fixture data\n") + } +} +func (f *clineGate) foreign(project string) { + label, path, skillRoot := "a", f.projectMCP(project), project + if project == "" { + label, path, skillRoot = "user", f.userMCP(), f.home + } else if project == f.B { + label = "b" + } else if project != f.A { + f.t.Fatal("foreign preservation outside selected scope") + } + body := clineGateObject(f.t, []byte(projectJourneyRead(f.t, path))) + servers, ok := body["mcpServers"].(map[string]any) + entry, eok := servers["foreign-"+label].(map[string]any) + transport, tok := entry["transport"].(map[string]any) + if !ok || !eok || !tok || body["foreignPreference"] != "retain-"+label || transport["url"] != "http://127.0.0.1:9/FOREIGN-"+label || transport["type"] != "streamableHttp" || len(entry) != 1 || len(transport) != 2 { + f.t.Fatal("unrelated MCP preference/member changed") + } + expected := fmt.Sprintf("---\nname: foreign-%s\ndescription: Foreign fixture\ndisable-model-invocation: true\n---\nRetain exact unrelated inert bytes.\n", label) + if projectJourneyRead(f.t, f.skill(skillRoot, "foreign-"+label)) != expected { + f.t.Fatal("unrelated skill bytes changed") + } + if project != "" && projectJourneyRead(f.t, filepath.Join(project, ".uap", "data", "caller-owned", "keep.txt")) != "caller-owned fixture data\n" { + f.t.Fatal("caller data changed") + } +} +func (f *clineGate) emitted(name, path string) { + body := []byte(projectJourneyRead(f.t, path)) + object := clineGateObject(f.t, body) + servers, ok := object["mcpServers"].(map[string]any) + if !ok || len(servers) == 0 { + f.t.Fatal("actual UAP enabled config missing") + } + for _, value := range servers { + entry, a := value.(map[string]any) + transport, b := entry["transport"].(map[string]any) + url, c := transport["url"].(string) + _, disabled := entry["disabled"] + if !a || !b || !c || disabled || transport["type"] != "streamableHttp" || !strings.HasPrefix(url, "http://127.0.0.1:9/") { + f.t.Fatal("emitted file is not actual enabled remote-only codec contract") + } + } + f.keep(name, body) +} +func clineGateProgress(label string) (string, bool) { + names := map[string]string{"add-user": "uap-cline-user-package", "add-A": "uap-cline-a-package", "add-B": "uap-cline-b-package", "add-same-user": "uap-cline-same", "add-same-A": "uap-cline-same", "add-same-B": "uap-cline-same"} + if name, ok := names[label]; ok { + return "Resolving and validating one Agent Plugin package for every selected target...\nInstalling " + name + " for 1 selected clients...\n", true + } + switch label { + case "repeat-add-A": + return "Resolving and validating one Agent Plugin package for every selected target...\nApplying transactional client package...\n", true + case "unchanged-update-A", "update-A": + return "Resolving and validating one updated Agent Plugin package for every selected target...\n", true + case "intact-repair-A", "missing-repair-A": + return "Resolving and validating each unique exact installed package revision once...\n", true + case "remove-A": + return "", true + } + return "", false +} +func clineGateAcceptProgress(label string, body []byte) bool { + expected, ok := clineGateProgress(label) + return ok && string(body) == expected +} +func clineGateMutation(t *testing.T, result map[string]any, want bool) { + t.Helper() + count := 0 + var visit func(any) + visit = func(value any) { + switch v := value.(type) { + case map[string]any: + for key, item := range v { + if key == "no_change" && item == true && want { + t.Fatal("actual mutation classified no_change") + } + if key == "mutated" { + count++ + if item != want { + t.Fatalf("mutated=%v,want%t", item, want) + } + } + visit(item) + } + case []any: + for _, item := range v { + visit(item) + } + } + } + visit(result) + if count == 0 { + t.Fatal("actual public mutation field absent") + } +} +func (f *clineGate) pass(stage, reason string) { + f.stages[stage] = map[string]string{"status": "passed", "reason": reason} +} +func (f *clineGate) prepareUnique() string { + f.begin("source_install") + f.seedForeign() + user := f.source("user", "uap-cline-user-package", "uap-cline-user", "USER") + a := f.source("A", "uap-cline-a-package", "uap-cline-a", "A-ONE") + b := f.source("B", "uap-cline-b-package", "uap-cline-b", "B-ONE") + clineGateMutation(f.t, f.uap("add-user", "", "add", user, "--no-save"), true) + clineGateMutation(f.t, f.uap("add-A", f.A, "add", a, "--no-save"), true) + clineGateMutation(f.t, f.uap("add-B", f.B, "add", b, "--no-save"), true) + f.emitted("prepared-user-config.json", f.userMCP()) + f.emitted("prepared-A-config.json", f.projectMCP(f.A)) + f.emitted("prepared-B-config.json", f.projectMCP(f.B)) + f.inspect("prepared-A-inspection.json", f.A) + f.inspect("prepared-B-inspection.json", f.B) + f.pass("source_install", "public UAP created exact enabled remote config and scoped receipts; explicit inert security admission") + return a +} +func (f *clineGate) observeUnique() { + f.begin("unique_discovery") + raw := string(f.native("version", f.user, "", "--version")) + if raw != "3.0.73\n" { + f.t.Fatalf("native exact version differs:%q", raw) + } + f.evidence["client_version_measured"] = "3.0.73" + f.checkSkills("initial-A-skills", f.A, "A-ONE", false) + f.checkSkills("initial-B-skills", f.B, "B-ONE", false) + f.checkMCP("default-A", f.A, "", f.userMCP(), "foreign-user", "uap-cline-user") + f.checkMCP("default-B", f.B, "", f.userMCP(), "foreign-user", "uap-cline-user") + f.checkMCP("override-A", f.A, f.A, f.projectMCP(f.A), "foreign-a", "uap-cline-a") + f.checkMCP("override-B", f.B, f.B, f.projectMCP(f.B), "foreign-b", "uap-cline-b") + f.checkMCP("cross-A-from-B", f.B, f.A, f.projectMCP(f.A), "foreign-a", "uap-cline-a") + f.checkMCP("restart-default-A", f.A, "", f.userMCP(), "foreign-user", "uap-cline-user") + f.pass("unique_discovery", "populated exact A/B/user skill paths and default/explicit/cross-cwd/restart unedited enabled source files observed") +} +func (f *clineGate) uniqueLifecycle(source string) { + userRoots := []string{filepath.Join(f.home, ".cline", "skills"), f.userMCP(), filepath.Join(f.cache, "state-v2.json")} + otherRoots := []string{filepath.Join(f.B, ".cline", "skills"), f.projectMCP(f.B), filepath.Join(f.B, ".uap")} + userBefore, otherBefore := f.snapshot(userRoots...), f.snapshot(otherRoots...) + stableRoots := []string{filepath.Join(f.A, ".cline", "skills"), f.projectMCP(f.A), filepath.Join(f.A, ".uap", "state-v2.json")} + stable := f.snapshot(stableRoots...) + nativeRoots := []string{filepath.Join(f.A, ".cline", "skills"), f.projectMCP(f.A)} + nativeStable := f.snapshot(nativeRoots...) + f.begin("stable_lifecycle") + for _, step := range []struct{ label, verb, selector string }{{"repeat-add-A", "add", source}, {"unchanged-update-A", "update", "uap-cline-a-package"}, {"intact-repair-A", "repair", "uap-cline-a-package"}} { + args := []string{step.verb, step.selector} + if step.verb == "add" { + args = append(args, "--no-save") + } + clineGateMutation(f.t, f.uap(step.label, f.A, args...), step.verb == "repair") + if step.verb != "repair" { + f.requireSnapshot(stable, stableRoots...) + } else { + f.requireSnapshot(nativeStable, nativeRoots...) + } + } + f.checkSkills("stable-A-skills", f.A, "A-ONE", false) + f.checkMCP("stable-A-mcp", f.A, f.A, f.projectMCP(f.A), "foreign-a", "uap-cline-a") + f.pass("stable_lifecycle", "replay/update unchanged; intact repair truthfully restages with mutation and exact native bytes") + f.begin("update") + f.source("A", "uap-cline-a-package", "uap-cline-a", "A-TWO") + clineGateMutation(f.t, f.uap("update-A", f.A, "update", source), true) + f.checkSkills("updated-A-skills", f.A, "A-TWO", false) + f.checkMCP("updated-A-mcp", f.A, f.A, f.projectMCP(f.A), "foreign-a", "uap-cline-a") + f.emitted("updated-A-config.json", f.projectMCP(f.A)) + f.pass("update", "same-version update changed owned bytes and receipt; listing read-only") + f.begin("missing_repair") + if err := os.RemoveAll(filepath.Dir(f.skill(f.A, "uap-cline-a"))); err != nil { + f.t.Fatal(err) + } + clineGateMutation(f.t, f.uap("missing-repair-A", f.A, "repair", "uap-cline-a-package"), true) + f.checkSkills("repaired-A-skills", f.A, "A-TWO", false) + f.checkMCP("repaired-A-mcp", f.A, f.A, f.projectMCP(f.A), "foreign-a", "uap-cline-a") + f.emitted("repaired-A-config.json", f.projectMCP(f.A)) + f.assertPrepared(f.A, "uap-cline-a-package") + f.inspect("repaired-A-inspection.json", f.A) + f.pass("missing_repair", "restored installed A-TWO from unchanged selected local source; remote immutable-pin proof remains separate TLS Git fixture") + f.begin("remove") + clineGateMutation(f.t, f.uap("remove-A", f.A, "remove", "uap-cline-a-package"), true) + f.checkSkills("removed-A-skills", f.A, "", true) + f.checkMCP("removed-A-mcp", f.A, f.A, f.projectMCP(f.A), "foreign-a") + f.emitted("removed-A-config.json", f.projectMCP(f.A)) + f.foreign(f.A) + f.foreign(f.B) + f.requireSnapshot(userBefore, userRoots...) + f.requireSnapshot(otherBefore, otherRoots...) + snapshots, _ := json.MarshalIndent(map[string]any{"user": userBefore, "B": otherBefore}, "", " ") + f.keep("foreign-snapshots.json", snapshots) + f.pass("remove", "owned skill/member absent; foreign A/user/B bytes retained") + f.pass("foreign_preservation", "original user and B state/native bytes exact across A lifecycle") +} +func (f *clineGate) sameNameForbidden(project string) []string { + roots := make([]string, 0, 2) + for _, candidate := range []string{f.A, f.B} { + if candidate != project { + roots = append(roots, filepath.Join(candidate, ".cline", "skills")) + } + } + return roots +} +func (f *clineGate) sameName() { + f.begin("same_name_precedence") + const plugin = "uap-cline-same" + for _, item := range []struct{ id, project, marker string }{{"same-user", "", "SAME-USER"}, {"same-A", f.A, "SAME-A"}, {"same-B", f.B, "SAME-B"}} { + source := f.source(item.id, plugin, "uap-cline-shared", item.marker) + before := f.snapshot(f.ownedRoots()...) + clineGateMutation(f.t, f.uap("add-"+item.id, item.project, "add", source, "--no-save"), true) + for path, want := range before { + own := item.project + if own == "" { + own = f.home + } + if !strings.HasPrefix(path, own+string(filepath.Separator)) && (item.project != "" || !strings.HasPrefix(path, f.cache+string(filepath.Separator))) { + if f.snapshot(path)[path] != want { + f.t.Fatal("same-name add changed another scope") + } + } + } + if item.project != "" { + f.assertPrepared(item.project, plugin) + } + f.foreign(item.project) + } + f.evidence["same_name_owned_bindings"] = map[string]any{"user": f.binding("", plugin), "A": f.binding(f.A, plugin), "B": f.binding(f.B, plugin)} + winners := map[string]any{} + for _, item := range []struct{ label, project, cwd string }{{"same-user", "", f.user}, {"same-A", f.A, f.A}, {"same-B", f.B, f.B}} { + body := f.native(item.label+"-skills", item.cwd, "", "config", "skills", "--json") + rows, err := clineGateRows(body) + if err != nil { + f.t.Fatal(err) + } + selected := []map[string]any{} + wants := []clineGateSkill{} + markers := map[string]string{f.skill("", "uap-cline-shared"): "SAME-USER", f.skill(f.A, "uap-cline-shared"): "SAME-A", f.skill(f.B, "uap-cline-shared"): "SAME-B"} + for _, row := range rows { + if row["name"] != "uap-cline-shared" { + continue + } + path, ok := clineGateText(row["path"]) + if !ok { + f.t.Fatal("same-name path missing") + } + if path != f.skill("", "uap-cline-shared") && (item.project == "" || path != f.skill(item.project, "uap-cline-shared")) { + f.t.Fatal("same-name row leaked sibling/foreign path") + } + selected = append(selected, row) + wants = append(wants, clineGateSkill{path: path, name: "uap-cline-shared", marker: markers[path]}) + } + if len(selected) == 0 { + f.t.Fatal("same-name inventory empty") + } + if err := clineGateSkills(body, wants, f.sameNameForbidden(item.project)); err != nil { + f.t.Fatal(err) + } + winners[item.label] = selected + names := []string{"foreign-user", "uap-cline-user", "uap-cline-shared"} + path, override := f.userMCP(), "" + if item.project != "" { + label := "a" + if item.project == f.B { + label = "b" + } + names = []string{"foreign-" + label, "uap-cline-shared"} + if item.project == f.B { + names = append(names, "uap-cline-b") + } + path, override = f.projectMCP(item.project), item.project + } + f.checkMCP(item.label+"-mcp", item.cwd, override, path, names...) + f.emitted(item.label+"-config.json", path) + } + body, _ := json.MarshalIndent(winners, "", " ") + f.keep("same-name-winners.json", body) + f.stages["same_name_precedence"] = map[string]string{"status": "blocked", "reason": "actual rows separate from three UAP receipts; independent active versions/session policy unqualified"} + f.evidence["same_name_observation_complete"] = true +} +func (f *clineGate) inspect(name, project string) { + before := f.snapshot(f.ownedRoots()...) + app, err := f.journey.app.ForScope("project", project) + if err != nil { + f.t.Fatal(err) + } + view, err := app.Installer.Inspect(f.ctx) + if err != nil || view.Recovery.Required { + f.t.Fatalf("fresh read-only inspection:%v %+v", err, view.Recovery) + } + count := 0 + for _, installation := range view.Installations { + for _, binding := range installation.Bindings { + if binding.ClientID != "cline" || binding.Materialization == "absent" { + continue + } + count++ + if binding.Scope != "project" || binding.Activation != "prepared" || binding.Verification != "installation_verified" || binding.ProjectionStatus == "projection_missing" || binding.ProjectionStatus == "projection_mismatch" || binding.TreeDigest == "" { + f.t.Fatalf("fresh SDK Prepared projection differs:%+v", binding) + } + } + } + if count == 0 || f.runner.calls != 0 { + f.t.Fatal("fresh SDK omitted binding or invoked native") + } + f.requireSnapshot(before, f.ownedRoots()...) + body, err := json.MarshalIndent(view, "", " ") + if err != nil { + f.t.Fatal(err) + } + f.keep(name, append(body, '\n')) +} +func TestClineCurrentSourceNativeProjectConfigLifecycle(t *testing.T) { + requireClineGate(t) + f := newClineGate(t) + defer f.finish() + f.gitRoots() + source := f.prepareUnique() + f.observeUnique() + f.uniqueLifecycle(source) + f.sameName() + if f.runner.calls != 0 { + t.Fatal("public UAP invoked native/MCP command") + } + if !reflect.DeepEqual(f.used, clineGateExpectedUsed()) { + t.Fatal("finite command table not completed") + } + f.evidence["read_only_unique_marker_discovery_qualified"] = true +} diff --git a/cli/cmd/agentplugins/project_cline_native_observe_linux_test.go b/cli/cmd/agentplugins/project_cline_native_observe_linux_test.go new file mode 100644 index 000000000..fa1546971 --- /dev/null +++ b/cli/cmd/agentplugins/project_cline_native_observe_linux_test.go @@ -0,0 +1,265 @@ +//go:build linux + +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + "unicode/utf8" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" +) + +func clineGateJSON(body []byte) (any, error) { + if len(body) > clineGateStream || !utf8.Valid(body) { + return nil, fmt.Errorf("bounded UTF-8 native JSON required") + } + d := json.NewDecoder(bytes.NewReader(body)) + d.UseNumber() + value, err := shared.DecodeUniqueJSONValue(d) + if err != nil { + return nil, err + } + if _, err = d.Token(); !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("native JSON trailing data") + } + return value, nil +} +func clineGateObject(t *testing.T, body []byte) map[string]any { + t.Helper() + value, err := clineGateJSON(body) + object, ok := value.(map[string]any) + if err != nil || !ok { + t.Fatalf("whole public CLI JSON object required: %v", err) + } + return object +} +func clineGateRows(body []byte) ([]map[string]any, error) { + value, err := clineGateJSON(body) + if err != nil { + return nil, err + } + rows, ok := value.([]any) + if !ok || len(rows) > 128 { + return nil, fmt.Errorf("bounded native array required") + } + result := make([]map[string]any, 0, len(rows)) + for _, row := range rows { + m, ok := row.(map[string]any) + if !ok { + return nil, fmt.Errorf("native row object required") + } + result = append(result, m) + } + return result, nil +} +func clineGateText(value any) (string, bool) { + s, ok := value.(string) + return s, ok && len(s) <= 8192 && utf8.ValidString(s) && !strings.ContainsRune(s, 0) +} +func clineGateMCP(body []byte, path string, expected []string) error { + rows, err := clineGateRows(body) + if err != nil { + return err + } + if len(rows) != len(expected) { + return fmt.Errorf("native MCP inventory differs from exact selected enabled file") + } + counts := map[string]int{} + for _, row := range rows { + name, a := clineGateText(row["name"]) + actual, b := clineGateText(row["path"]) + if len(row) != 4 || !a || !b || actual != path || filepath.Clean(actual) != actual || !filepath.IsAbs(actual) || row["disabled"] != false || row["transportType"] != "streamableHttp" { + return fmt.Errorf("native MCP enabled/path/transport row differs") + } + counts[name]++ + } + for _, name := range expected { + if counts[name] != 1 { + return fmt.Errorf("missing/duplicate selected MCP member") + } + } + return nil +} + +type clineGateSkill struct{ path, name, marker string } + +func clineGateSkills(body []byte, expected []clineGateSkill, forbidden []string) error { + rows, err := clineGateRows(body) + if err != nil { + return err + } + counts := map[string]int{} + for _, row := range rows { + path, a := clineGateText(row["path"]) + name, b := clineGateText(row["name"]) + description, c := clineGateText(row["description"]) + instructions, d := clineGateText(row["instructions"]) + front, e := row["frontmatter"].(map[string]any) + if !a || !b || !c || !d || !e || !filepath.IsAbs(path) || filepath.Clean(path) != path { + return fmt.Errorf("native skill path/shape differs") + } + for _, blocked := range forbidden { + if path == blocked || strings.HasPrefix(path, blocked+string(filepath.Separator)) { + return fmt.Errorf("sibling/removed skill exposed") + } + } + for _, want := range expected { + if path == want.path { + if name != want.name || description != want.marker || !strings.Contains(instructions, want.marker) || front["disable-model-invocation"] != true { + return fmt.Errorf("native skill marker/invocation differs") + } + counts[path]++ + } + } + } + for _, want := range expected { + if counts[want.path] != 1 { + return fmt.Errorf("missing/duplicate selected skill path") + } + } + return nil +} +func (f *clineGate) userMCP() string { + return filepath.Join(f.home, ".cline", "data", "settings", "cline_mcp_settings.json") +} +func (f *clineGate) projectMCP(project string) string { + return filepath.Join(project, ".uap", "clients", "cline", "mcp.json") +} +func (f *clineGate) skill(project, name string) string { + if project == "" { + project = f.home + } + return filepath.Join(project, ".cline", "skills", name, "SKILL.md") +} +func (f *clineGate) ownedRoots() []string { + return []string{filepath.Join(f.home, ".cline", "skills"), f.userMCP(), filepath.Join(f.cache, "state-v2.json"), + filepath.Join(f.A, ".cline", "skills"), f.projectMCP(f.A), filepath.Join(f.A, ".uap", "state-v2.json"), + filepath.Join(f.B, ".cline", "skills"), f.projectMCP(f.B), filepath.Join(f.B, ".uap", "state-v2.json")} +} +func (f *clineGate) snapshot(roots ...string) map[string]string { + f.t.Helper() + result := map[string]string{} + entries, total := 0, int64(0) + for _, root := range roots { + if resolved, err := filepath.EvalSymlinks(root); err == nil && resolved != root { + f.t.Fatal("snapshot aliases outside exact source") + } else if err != nil && !os.IsNotExist(err) { + f.t.Fatal(err) + } + if root != f.root && !strings.HasPrefix(root, f.root+string(filepath.Separator)) { + f.t.Fatal("snapshot outside fixture authority") + } + err := filepath.WalkDir(root, func(path string, _ os.DirEntry, walkErr error) error { + if os.IsNotExist(walkErr) && path == root { + result[path] = "absent" + return nil + } + if walkErr != nil { + return walkErr + } + entries++ + if entries > 2048 { + return fmt.Errorf("snapshot entry bound") + } + info, err := os.Lstat(path) + if err != nil { + return err + } + if info.IsDir() { + return nil + } + st, ok := info.Sys().(*syscall.Stat_t) + if !ok || !info.Mode().IsRegular() || st.Nlink != 1 || info.Size() > 1<<20 || total+info.Size() > 8<<20 { + return fmt.Errorf("snapshot type/byte bound") + } + file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) + if err != nil { + return err + } + opened, err := file.Stat() + if err != nil || !os.SameFile(info, opened) { + _ = file.Close() + return fmt.Errorf("snapshot opened identity differs") + } + body, re := io.ReadAll(io.LimitReader(file, (1<<20)+1)) + after, se := file.Stat() + ce := file.Close() + current, pe := os.Lstat(path) + if re != nil || se != nil || ce != nil || pe != nil || int64(len(body)) != info.Size() || !os.SameFile(info, after) || !os.SameFile(info, current) || !after.ModTime().Equal(info.ModTime()) || !current.ModTime().Equal(info.ModTime()) { + return fmt.Errorf("snapshot bytes/identity changed") + } + total += int64(len(body)) + result[path] = fmt.Sprintf("%o:%d:%s", info.Mode(), len(body), clineGateSHA(body)) + return nil + }) + if err != nil { + f.t.Fatal(err) + } + } + return result +} +func reflectClineArgs(args []string) bool { + return len(args) == 1 && args[0] == "--version" || len(args) == 3 && args[0] == "config" && (args[1] == "mcp" || args[1] == "skills") && args[2] == "--json" +} +func (f *clineGate) native(label, cwd, override string, args ...string) []byte { + f.t.Helper() + if cwd != f.user && cwd != f.A && cwd != f.B { + f.t.Fatal("unknown native cwd") + } + if !reflectClineArgs(args) { + f.t.Fatal("unreviewed Cline command") + } + f.admit(label, "native") + before := f.snapshot(f.ownedRoots()...) + if err := clineGateBinaryPin(f.binary); err != nil { + f.t.Fatal(err) + } + argv := append([]string{"--auto-approve", "false", "--cwd", cwd}, args...) + r := captureClineGate(f.ctx, f.binary, cwd, f.env(override), argv, 30*time.Second) + f.record(label, "native", cwd, append([]string{f.binary}, argv...), r) + f.requireSnapshot(before, f.ownedRoots()...) + receipt := f.commands[len(f.commands)-1] + receipt["task_home"], receipt["mcp_settings_override"], receipt["automatic_update_disabled"] = f.home, "", true + if override != "" { + receipt["mcp_settings_override"] = f.projectMCP(override) + } + receipt["owned_before"], receipt["owned_after"] = before, f.snapshot(f.ownedRoots()...) + if r.err != nil || r.exit != 0 || r.overflow || len(r.stderr) != 0 { + f.t.Fatalf("native stage %s failed; see exact bounded streams", label) + } + return r.out +} +func (f *clineGate) checkMCP(label, cwd, override, path string, names ...string) { + body := f.native(label, cwd, override, "config", "mcp", "--json") + if err := clineGateMCP(body, path, names); err != nil { + f.t.Fatalf("%s: %v", label, err) + } +} +func (f *clineGate) checkSkills(label, project, marker string, removed bool) { + want := []clineGateSkill{{path: f.skill("", "uap-cline-user"), name: "uap-cline-user", marker: "USER"}} + forbidden := []string{filepath.Join(f.B, ".cline", "skills")} + name := "uap-cline-a" + if project == f.B { + forbidden = []string{filepath.Join(f.A, ".cline", "skills")} + name = "uap-cline-b" + } + path := f.skill(project, name) + if removed { + forbidden = append(forbidden, path) + } else { + want = append(want, clineGateSkill{path: path, name: name, marker: marker}) + } + if err := clineGateSkills(f.native(label, project, "", "config", "skills", "--json"), want, forbidden); err != nil { + f.t.Fatalf("%s: %v", label, err) + } +} diff --git a/cli/cmd/agentplugins/project_context_test.go b/cli/cmd/agentplugins/project_context_test.go new file mode 100644 index 000000000..090822d93 --- /dev/null +++ b/cli/cmd/agentplugins/project_context_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func projectCompositionFixture(t *testing.T) (agentpluginscli.App, string, string, string) { + t.Helper() + home := projectJourneyTemp(t) + user := filepath.Join(home, "user-uap") + a, b := projectJourneyTemp(t), projectJourneyTemp(t) + registry, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + app, err := newAgentpluginsCLIApp(home, user, nil, nil, nil, registry) + if err != nil { + t.Fatal(err) + } + return app, user, a, b +} + +func TestProjectRuntimeSeparatesMutableStateAndSharesContentCache(t *testing.T) { + app, user, a, b := projectCompositionFixture(t) + for _, root := range []string{a, b} { + project, err := app.ForScope(domain.ScopeProject, root) + if err != nil { + t.Fatal(err) + } + if project.Scope != domain.ScopeProject || project.ProjectRoot != root || project.DependenciesRoot != root || project.ManagedRoot != filepath.Join(root, ".uap", "managed") || project.EnvironmentLockPath != filepath.Join(root, ".uap", "environment.lock") { + t.Fatalf("wrong selected context: %+v", project) + } + if project.UserHome != app.UserHome || project.EnvironmentStoreRoot != filepath.Join(user, "store") || project.SourceAcquirer == nil { + t.Fatal("project composition lost original profile/cache context") + } + if project.StateMigrator != nil || project.LegacyLifecycle != nil || project.LegacyStateLock != nil { + t.Fatal("project composition exposes user migration routes") + } + if _, err := project.Installer.Inspect(context.Background()); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(root, ".uap")); !os.IsNotExist(err) { + t.Fatalf("read-only composition created namespace: %v", err) + } + } + if app.Scope != domain.ScopeUser || app.ProjectRoot != "" || app.DependenciesRoot != user { + t.Fatal("project selection modified user runtime") + } +} + +func TestProjectCLIInitAndTransferSelectOnlyExplicitProject(t *testing.T) { + app, user, a, b := projectCompositionFixture(t) + run := func(args ...string) error { + t.Helper() + out := &bytes.Buffer{} + root := agentpluginscli.NewRoot(app) + root.SetOut(out) + root.SetErr(out) + root.SetArgs(args) + return root.Execute() + } + if err := run("deps", "init", "--scope", "project", "--project-root", a, "--format", "json"); err != nil { + t.Fatal(err) + } + manifest := filepath.Join(a, "uap.toml") + before, err := os.ReadFile(manifest) + if err != nil { + t.Fatal(err) + } + for _, other := range []string{user, b} { + if _, err := os.Stat(filepath.Join(other, "uap.toml")); !os.IsNotExist(err) { + t.Fatalf("init touched other environment %s: %v", other, err) + } + } + if err := run("deps", "export", a, "--scope", "project", "--project-root", a, "--format", "json"); err == nil || !strings.Contains(err.Error(), "different exchange directory") { + t.Fatalf("same-pair transfer = %v", err) + } + after, err := os.ReadFile(manifest) + if err != nil || !bytes.Equal(before, after) { + t.Fatalf("refused transfer changed manifest: %v", err) + } + if err := run("deps", "init", "--project-root", b); err == nil || !strings.Contains(err.Error(), "requires --scope project") { + t.Fatalf("user/root mismatch = %v", err) + } +} + +func TestProjectCLIUnsupportedTargetRejectsBeforeSourceAcquisition(t *testing.T) { + app, user, a, _ := projectCompositionFixture(t) + root := agentpluginscli.NewRoot(app) + root.SetOut(&bytes.Buffer{}) + root.SetErr(&bytes.Buffer{}) + root.SetArgs([]string{"add", "https://invalid.example/never-acquire", "--target", "chatgpt", "--no-save", "--scope", "project", "--project-root", a}) + if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "does not support project scope") { + t.Fatalf("unsupported project boundary = %v", err) + } + for _, path := range []string{user, filepath.Join(a, ".uap"), filepath.Join(a, "uap.toml")} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("unsupported request created %s: %v", path, err) + } + } +} diff --git a/cli/cmd/agentplugins/project_copilot_e2e_test.go b/cli/cmd/agentplugins/project_copilot_e2e_test.go new file mode 100644 index 000000000..bf5be2708 --- /dev/null +++ b/cli/cmd/agentplugins/project_copilot_e2e_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Public saved CLI, real TLS Git and the production source/store/planner, +// installer/native transactions. Detection/security are inert host boundaries. +// The trusted test executable is copied as launcher bytes but never executed; +// this test certifies no Copilot, MCP, model, trust or native discovery behavior. +func TestProjectCopilotComposedSavedLifecycle(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, ".copilot") + journey.app = configureProjectCopilotJourney(journey.app, user) + foreign := `{"mcpServers":{"foreign":{"type":"http","url":"https://foreign.test/mcp","enabled":false}},"policy":"keep"}` + for _, project := range []string{user, journey.a, journey.b} { + projectJourneyWrite(t, filepath.Join(project, ".github", "mcp.json"), foreign) + projectJourneyWrite(t, filepath.Join(project, ".github", "skills", "foreign", "SKILL.md"), "foreign Copilot skill\n") + } + projectJourneyWrite(t, filepath.Join(journey.a, ".mcp.json"), foreign) + userBefore := securityFixtureTree(t, user) + for _, project := range []string{journey.a, journey.b} { + journey.run(t, project, "add", transport.endpoint, "--alias", "docs", "--target", "copilot") + projectCopilotOwned(t, journey, project, projectJourneySkill) + } + projectJourneyWrite(t, filepath.Join(journey.b, "B-retained-marker"), "other project\n") + bBefore := securityFixtureTree(t, journey.b) + statePath := filepath.Join(journey.namespace(journey.a), "state-v2.json") + stateBefore := projectJourneyRead(t, statePath) + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + if stateBefore != projectJourneyRead(t, statePath) { + t.Fatal("unchanged Copilot reconciliation rewrote ownership") + } + installation, binding := projectCopilotOwned(t, journey, journey.a, projectJourneySkill) + projectJourneyDataMarker(t, journey, journey.a, installation) + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "TEST immutable drift\n") + if err := os.RemoveAll(filepath.Join(journey.a, ".github", "skills", "docs")); err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance disposable Copilot source") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "copilot") + projectCopilotOwned(t, journey, journey.a, projectJourneySkill) + projectCopilotRecover(t, journey) + projectCopilotUpdateRemove(t, journey, installation, binding) + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) { + t.Fatal("Copilot A lifecycle mutated the user profile or B") + } +} + +func projectCopilotUpdateRemove(t *testing.T, journey projectJourney, installation domain.Installation, binding domain.ClientBinding) { + t.Helper() + journey.run(t, journey.a, "update", "docs", "--target", "copilot") + updated, current := projectCopilotOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator || updated.Source.SourceBindingID != installation.Source.SourceBindingID { + t.Fatal("Copilot update lost exact source/binding authority or did not advance") + } + status := journey.run(t, journey.a, "deps", "status") + for _, expected := range []string{`"copilot"`, `"project"`, `"activation_prepared"`} { + if !strings.Contains(status, expected) { + t.Fatalf("Copilot status omitted %s: %s", expected, status) + } + } + if strings.Contains(status, "--activation-complete") || strings.Contains(status, `"attest_activation"`) || !strings.Contains(status, `"review_project_activation"`) { + t.Fatalf("prepared project status suggested unsupported promotion: %s", status) + } + journey.run(t, journey.a, "remove", "docs", "--target", "copilot") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(journey.a, ".github", "skills", "docs")) + config := projectJourneyRead(t, filepath.Join(journey.a, ".mcp.json")) + if strings.Contains(config, `"docs"`) || !strings.Contains(config, "foreign.test") || !strings.Contains(config, `"enabled":false`) { + t.Fatalf("Copilot exact removal changed foreign settings: %s", config) + } + if projectJourneyRead(t, filepath.Join(journey.a, ".github", "skills", "foreign", "SKILL.md")) != "foreign Copilot skill\n" { + t.Fatal("Copilot removal changed an unrelated project skill") + } + for key, receipt := range installation.DataReceipts { + if updated.DataReceipts[key].Locator != receipt.Locator || projectJourneyRead(t, filepath.Join(receipt.Locator, "keep.txt")) != "retained environment data\n" { + t.Fatal("Copilot update/remove changed retained data authority") + } + } + if len(journey.state(t, journey.a).EnvironmentReceipts) != 0 { + t.Fatal("Copilot remove retained saved environment ownership") + } + after := securityFixtureTree(t, journey.a) + _, _ = journey.execute(journey.a, "remove", "docs", "--target", "copilot") + if !reflect.DeepEqual(after, securityFixtureTree(t, journey.a)) { + t.Fatal("repeat absent Copilot removal changed bytes") + } +} + +func projectCopilotRecover(t *testing.T, journey projectJourney) { + t.Helper() + github := securityFixtureTree(t, filepath.Join(journey.a, ".github")) + config := projectJourneyRead(t, filepath.Join(journey.a, ".mcp.json")) + staging, active := projectJourneyInterruptDirectoryJournal(t, journey) + for range 2 { + journey.run(t, journey.a, "deps", "recover", "--yes") + } + journey.inspectNoRecovery(t, journey.a) + projectJourneyAbsent(t, active) + if !reflect.DeepEqual(github, securityFixtureTree(t, filepath.Join(journey.a, ".github"))) || config != projectJourneyRead(t, filepath.Join(journey.a, ".mcp.json")) || projectJourneyRead(t, filepath.Join(staging, "unpublished.txt")) != "caller-owned unpublished bytes\n" { + t.Fatal("Copilot bounded pending-journal recovery changed native/caller bytes") + } +} diff --git a/cli/cmd/agentplugins/project_copilot_fixture_test.go b/cli/cmd/agentplugins/project_copilot_fixture_test.go new file mode 100644 index 000000000..ff3aeebd0 --- /dev/null +++ b/cli/cmd/agentplugins/project_copilot_fixture_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type projectCopilotDetector struct{ user string } + +func (d projectCopilotDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientCopilot, ConfigRoot: d.user, + Version: "1.0.103", Status: domain.DetectionDetected}}, nil +} + +func configureProjectCopilotJourney(app agentpluginscli.App, user string) agentpluginscli.App { + app.Detector = projectCopilotDetector{user: user} + 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 configureProjectCopilotJourney(selected, user), nil + } + return app +} + +func projectCopilotOwned(t *testing.T, journey projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, binding := journey.owned(t, project) + if binding.ClientID != "copilot" || binding.Scope != "project" || binding.NativeProfileRoot != project || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("Copilot binding lost project authority or conservative native status: %+v", binding) + } + for _, path := range []string{filepath.Join(project, ".github", "skills", "docs", "SKILL.md"), filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md")} { + if projectJourneyRead(t, path) != expected { + t.Fatalf("Copilot managed/native skill differs at %s", path) + } + } + return installation, binding +} diff --git a/cli/cmd/agentplugins/project_git_diagnostic_contract_test.go b/cli/cmd/agentplugins/project_git_diagnostic_contract_test.go new file mode 100644 index 000000000..c03a9352c --- /dev/null +++ b/cli/cmd/agentplugins/project_git_diagnostic_contract_test.go @@ -0,0 +1,254 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "testing" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/sourceacquisition" +) + +type projectGitDiagnosticDelegate func(context.Context, sourceacquisition.Command) ([]byte, error) + +func (f projectGitDiagnosticDelegate) Run(ctx context.Context, command sourceacquisition.Command) ([]byte, error) { + return f(ctx, command) +} + +type projectGitInspectionTrap struct{} +type projectGitDiagnosticContextKey struct{} + +func (*projectGitInspectionTrap) Error() string { + panic("argv/stdout/stderr/secret cause must not be inspected") +} + +func projectGitDiagnosticFixture(t *testing.T) (projectGitDiagnosticRunner, string, *[]string) { + t.Helper() + root := projectJourneyTemp(t) + dir := filepath.Join(root, "agentplugins-git-inert") + if err := os.Mkdir(dir, 0700); err != nil { + t.Fatal(err) + } + logs := []string{} + d := &projectGitDiagnostic{counts: map[string]uint16{}, endpoint: "https://127.0.0.1:12345/repo.git", log: func(record string) { logs = append(logs, record) }} + return projectGitDiagnosticRunner{recorder: d, root: root}, dir, &logs +} + +func TestProjectGitDiagnosticPreservesOneCallInputsAndExactResult(t *testing.T) { + r, dir, logs := projectGitDiagnosticFixture(t) + ctx := context.WithValue(t.Context(), projectGitDiagnosticContextKey{}, "inert") + command := sourceacquisition.Command{Dir: dir, Args: []string{"init", "--quiet", filepath.Join(dir, "repository")}, Stdin: []byte("same input"), MaxOutputBytes: 17} + body, original := []byte("same returned bytes"), &projectGitInspectionTrap{} + calls := 0 + r.delegate = projectGitDiagnosticDelegate(func(got context.Context, c sourceacquisition.Command) ([]byte, error) { + calls++ + if got != ctx || !reflect.DeepEqual(c, command) || &c.Args[0] != &command.Args[0] || &c.Stdin[0] != &command.Stdin[0] { + t.Fatal("decorator changed context/command authority") + } + return body, original + }) + got, err := r.Run(ctx, command) + if calls != 1 || reflect.ValueOf(err) != reflect.ValueOf(original) || &got[0] != &body[0] || len(*logs) != 1 { + t.Fatal("original call/result/error identity changed") + } + if strings.Contains((*logs)[0], "secret") || !strings.Contains((*logs)[0], "phase=init") || !strings.Contains((*logs)[0], "cause=cause_omitted") { + t.Fatal("nonnative argv/stream-bearing error was inspected") + } +} + +func TestProjectGitDiagnosticOnlyAdmitsNativeKnownFixtureAuthority(t *testing.T) { + for _, kind := range []string{"explicit", "origin-registered", "origin-unregistered", "unknown-grammar", "external-endpoint", "userinfo", "query", "empty-query", "fragment", "outside-workspace"} { + t.Run(kind, func(t *testing.T) { + r, dir, logs := projectGitDiagnosticFixture(t) + repo := filepath.Join(dir, "repository") + returned := errors.New("read stable proc identity failed") + r.delegate = projectGitDiagnosticDelegate(func(context.Context, sourceacquisition.Command) ([]byte, error) { return nil, returned }) + if kind == "origin-registered" { + returned = nil + _, err := r.Run(t.Context(), sourceacquisition.Command{Dir: dir, Args: []string{"-C", repo, "remote", "add", "origin", r.recorder.endpoint}}) + if err != nil { + t.Fatal(err) + } + returned = errors.New("read stable proc identity failed") + } + args := make([]string, 0, 10) + args = append(args, "-C", repo, "ls-remote", "--symref", "--", r.recorder.endpoint, "HEAD") + switch kind { + case "origin-registered", "origin-unregistered": + args = append(args[:0], "-C", repo, "fetch", "--quiet", "--depth=1", "--no-tags", "--no-recurse-submodules", "origin", strings.Repeat("1", 40)) + case "unknown-grammar": + args = append(args, "unreviewed") + case "external-endpoint": + r.recorder.endpoint = "https://outside.example/repo.git" + args[5] = r.recorder.endpoint + case "userinfo": + r.recorder.endpoint = "https://name:password@127.0.0.1:12345/repo.git" + args[5] = r.recorder.endpoint + case "query": + r.recorder.endpoint += "?key=inert" + args[5] = r.recorder.endpoint + case "empty-query": + r.recorder.endpoint += "?" + args[5] = r.recorder.endpoint + case "fragment": + r.recorder.endpoint += "#" + args[5] = r.recorder.endpoint + case "outside-workspace": + r.root = projectJourneyTemp(t) + } + positive := kind == "explicit" || kind == "origin-registered" + if !positive { + returned = &projectGitInspectionTrap{} + } + _, err := r.Run(t.Context(), sourceacquisition.Command{Dir: dir, Args: args, NativeCredentials: true}) + if reflect.ValueOf(err) != reflect.ValueOf(returned) || len(*logs) != 1 { + t.Fatal("delegate outcome or accounting changed") + } + omitted := strings.Contains((*logs)[0], "cause=cause_omitted") + if omitted == positive { + t.Fatalf("positiveauthority=%v log=%s", positive, (*logs)[0]) + } + }) + } +} + +func TestProjectGitDiagnosticCauseInputRedactionAndFinalASCIIBytesAreBounded(t *testing.T) { + for _, input := range []string{strings.Repeat("x", 8193), string([]byte{0xff}), "Authorization: Bearer private", "password=inert", "token:private", "cookie=value", "process cleanup failed (client diagnostic: opaque_payload_927)"} { + if got := projectGitDiagnosticCause(errors.New(input), "/known/root"); got != "cause_omitted" { + t.Fatal("unsafe/oversized input was admitted") + } + } + got := projectGitDiagnosticCause(errors.New("read /known/root/agentplugins-git-inert/repository failed; https://user:private@elsewhere.example/path?q=inert"), "/known/root", "/known/root/agentplugins-git-inert/repository") + for _, secret := range []string{"/known/root", "private", "elsewhere.example", "https://"} { + if strings.Contains(got, secret) { + t.Fatal("URI/locator leaked") + } + } + if !strings.Contains(got, "") { + t.Fatal("URI was not explicitly redacted") + } + got = projectGitDiagnosticCause(errors.New(strings.Repeat("é\x00\n", 1500))) + if len(got) > 2048 || !strings.Contains(got, "truncated") { + t.Fatalf("final bounded quote differs: %d", len(got)) + } + for _, value := range []byte(got) { + if value < 0x20 || value > 0x7e { + t.Fatal("control or nonASCII output escaped the quote") + } + } +} + +func TestProjectGitDiagnosticClassesCountsAndUnknownPhaseNeverInferAcquisition(t *testing.T) { + for _, tc := range []struct { + err error + class string + }{{context.Canceled, "context_canceled"}, {context.DeadlineExceeded, "context_deadline"}, {processadapter.ErrOrdinaryContainmentUnsupported, "ordinary_unsupported"}, {processadapter.ErrStdoutLimitExceeded, "stdout_limit"}, {processadapter.ErrOrdinaryForcedCleanup, "ordinary_forced_cleanup"}, {errors.Join(context.Canceled, processadapter.ErrOrdinaryForcedCleanup), "context_canceled"}, {&exec.ExitError{}, "other_git_or_process"}, {&projectGitInspectionTrap{}, "other_git_or_process"}} { + if got, status := projectGitDiagnosticClass(tc.err); got != tc.class || status != "unavailable" { + t.Fatal(got, tc.class) + } + } + r, dir, logs := projectGitDiagnosticFixture(t) + var original error + r.delegate = projectGitDiagnosticDelegate(func(context.Context, sourceacquisition.Command) ([]byte, error) { return []byte("unchanged"), original }) + command := sourceacquisition.Command{Dir: dir, Args: []string{"init", "--quiet", filepath.Join(dir, "repository")}} + if _, err := r.Run(t.Context(), command); err != nil || len(*logs) != 0 { + t.Fatal("success emitted diagnostics") + } + sequence := r.recorder.failureSequence() + r.recorder.outsideInvocations(sequence) + if len(*logs) != 1 || !strings.Contains((*logs)[0], "remaining source phase undetermined") || !strings.Contains((*logs)[0], "init:1") { + t.Fatal("prior command counts became phase proof") + } + r.recorder.counts["init"] = 65535 + original = &projectGitInspectionTrap{} + for range 8 { + if _, err := r.Run(t.Context(), command); reflect.ValueOf(err) != reflect.ValueOf(original) { + t.Fatal("diagnostic saturation changed real error") + } + } + if len(*logs) != 4 || r.recorder.counts["init"] != 65535 { + t.Fatal("diagnostics/counts not saturated") + } + r.recorder.outsideInvocations(sequence) + if len(*logs) != 4 { + t.Fatal("unknown phase bypassed diagnostic cap") + } + r, _, logs = projectGitDiagnosticFixture(t) + r.recorder.outsideInvocations(0) + if len(*logs) != 1 || !strings.Contains((*logs)[0], "init:0") || !strings.Contains((*logs)[0], "undetermined") { + t.Fatal("pre-command failure inferred snapshot") + } +} + +type projectGitDiagnosticExitError struct { + projectGitInspectionTrap + code int +} + +func (err *projectGitDiagnosticExitError) GitTransportExitCode() int { return err.code } + +func TestProjectGitDiagnosticRecognizesPositiveExitWithoutReadingError(t *testing.T) { + runner, dir, logs := projectGitDiagnosticFixture(t) + original := errors.Join(&projectGitDiagnosticExitError{code: 128}) + body := []byte("secret original output") + runner.delegate = projectGitDiagnosticDelegate(func(context.Context, sourceacquisition.Command) ([]byte, error) { + return body, original + }) + got, err := runner.Run(t.Context(), sourceacquisition.Command{Dir: dir, Args: []string{"unknown", "secret argument"}}) + if reflect.ValueOf(err) != reflect.ValueOf(original) || &got[0] != &body[0] || len(*logs) != 1 { + t.Fatal("positive exit diagnostics changed delegated identity or count") + } + if !strings.Contains((*logs)[0], "class=git_exit exit_status=128 cause=cause_omitted") || strings.Contains((*logs)[0], "secret") { + t.Fatal("positive exit category read an argv/stream-bearing error") + } +} + +func TestProjectGitDiagnosticOperationGrammarIsClosed(t *testing.T) { + endpoint, dir, sha := "https://127.0.0.1:12345/repo.git", filepath.Join(string(os.PathSeparator), "inert", "agentplugins-git-1"), strings.Repeat("1", 40) + repo := filepath.Join(dir, "repository") + cases := []struct { + label string + args []string + }{ + {"init", []string{"init", "--quiet", repo}}, + {"remote-add", []string{"remote", "add", "origin", endpoint}}, + {"config", []string{"config", "core.autocrlf", "false"}}, + {"ls-remote", []string{"ls-remote", "--symref", "--", endpoint, "HEAD"}}, + {"ls-remote", []string{"ls-remote", "--refs", "--", endpoint, "refs/heads/main", "refs/tags/v1"}}, + {"check-ref-format", []string{"check-ref-format", "refs/heads/main"}}, + {"fetch", []string{"fetch", "--quiet", "--depth=1", "--no-tags", "--no-recurse-submodules", "origin", sha}}, + {"fetch", []string{"fetch", "--quiet", "--depth=1", "--no-tags", "--no-recurse-submodules", "--", endpoint, sha}}, + {"fetch", []string{"fetch", "--quiet", "--depth=1", "--filter=blob:none", "--no-tags", "--no-recurse-submodules", "origin", sha}}, + {"rev-parse", []string{"rev-parse", "--verify", "FETCH_HEAD^{commit}"}}, + {"ls-tree", []string{"ls-tree", "-r", "-z", sha, "--", "."}}, + {"sparse-init", []string{"sparse-checkout", "init", "--cone"}}, + {"sparse-set", []string{"sparse-checkout", "set", "--cone", "--", "package"}}, + {"checkout", []string{"checkout", "--quiet", "--detach", sha}}, + {"show", []string{"show", sha + ":plugin.json"}}, + } + for _, tc := range cases { + args := tc.args + if tc.label != "init" { + args = append([]string{"-C", repo}, args...) + } + label, known, _ := projectGitDiagnosticOperation(sourceacquisition.Command{Dir: dir, Args: args}, endpoint) + if !known || label != tc.label { + t.Fatal(tc.label, label, known) + } + label, known, _ = projectGitDiagnosticOperation(sourceacquisition.Command{Dir: dir, Args: append(args, "extra")}, endpoint) + if known || label != "unknown" { + t.Fatal("unreviewed extra argument admitted", tc.label) + } + } + for _, args := range [][]string{{"-C", repo, "config", "private", ""}, {"-C", repo, "remote", "set-url", "origin", endpoint}, {"-C", repo, "show", sha + ":../secret"}, {"-C", repo, "checkout", "--quiet", "--detach", "main"}} { + if label, known, _ := projectGitDiagnosticOperation(sourceacquisition.Command{Dir: dir, Args: args}, endpoint); known || label != "unknown" { + t.Fatal("unknown grammar admitted", fmt.Sprint(args)) + } + } +} diff --git a/cli/cmd/agentplugins/project_git_diagnostic_test.go b/cli/cmd/agentplugins/project_git_diagnostic_test.go new file mode 100644 index 000000000..9c77480f1 --- /dev/null +++ b/cli/cmd/agentplugins/project_git_diagnostic_test.go @@ -0,0 +1,280 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net/url" + "path/filepath" + "regexp" + "slices" + "strconv" + "strings" + "sync" + "unicode/utf8" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/sourceacquisition" +) + +var projectGitDiagnosticLabels = []string{"init", "remote-add", "config", "ls-remote", "check-ref-format", "fetch", "rev-parse", "ls-tree", "sparse-init", "sparse-set", "checkout", "show", "unknown"} +var projectGitDiagnosticURI = regexp.MustCompile(`[A-Za-z][A-Za-z0-9+.-]*://[^\s"'<>]+`) +var projectGitDiagnosticCredential = regexp.MustCompile(`(?i)(authorization|proxy-authorization|password|passwd|token|secret|cookie|credential)[ \t]*[:=]`) +var projectGitDiagnosticSHA = regexp.MustCompile(`^[0-9a-f]{40}$`) + +type projectGitDiagnostic struct { + mu sync.Mutex + counts map[string]uint16 + failures uint64 + logged int + lastConfiguredRepo string + endpoint string + log func(string) +} + +type projectGitDiagnosticRunner struct { + recorder *projectGitDiagnostic + root string + delegate sourceacquisition.Runner +} + +func (r projectGitDiagnosticRunner) Run(ctx context.Context, command sourceacquisition.Command) ([]byte, error) { + delegate := r.delegate + if delegate == nil { + delegate = sourceacquisition.OSRunner{} + } + body, err := delegate.Run(ctx, command) + phase, known, endpointExplicit := projectGitDiagnosticOperation(command, r.recorder.endpoint) + d := r.recorder + d.mu.Lock() + defer d.mu.Unlock() + if d.counts[phase] < 65535 { + d.counts[phase]++ + } + if !known { + d.lastConfiguredRepo = "" + } + owned := projectGitDiagnosticWorkspace(r.root, command.Dir) + repo := filepath.Join(command.Dir, "repository") + if phase == "remote-add" { + d.lastConfiguredRepo = "" + if err == nil && owned && projectGitDiagnosticEndpoint(d.endpoint) { + d.lastConfiguredRepo = repo + } + } + if err == nil { + return body, nil + } + d.failures++ + if d.logged >= 4 { + return body, err + } + d.logged++ + cause := "cause_omitted" + // Non-native OSRunner errors include argv/streams: never inspect Error(). + if command.NativeCredentials && known && owned && projectGitDiagnosticEndpoint(d.endpoint) && (endpointExplicit || phase == "fetch" && d.lastConfiguredRepo == repo) { + cause = projectGitDiagnosticCause(err, d.endpoint, r.root, command.Dir, repo) + } + class, status := projectGitDiagnosticClass(err) + d.log(fmt.Sprintf("project Git diagnostic phase=%s class=%s exit_status=%s cause=%s counts=%s", phase, class, status, cause, d.countSnapshot())) + return body, err +} + +func (d *projectGitDiagnostic) failureSequence() uint64 { + d.mu.Lock() + defer d.mu.Unlock() + return d.failures +} + +func (d *projectGitDiagnostic) outsideInvocations(sequence uint64) { + d.mu.Lock() + defer d.mu.Unlock() + if d.failures != sequence || d.logged >= 4 { + return + } + d.logged++ + d.log("project Git diagnostic: no observed Runner failure; remaining source phase undetermined; counts=" + d.countSnapshot()) +} + +func (d *projectGitDiagnostic) countSnapshot() string { + values := make([]string, 0, len(projectGitDiagnosticLabels)) + for _, label := range projectGitDiagnosticLabels { + values = append(values, fmt.Sprintf("%s:%d", label, d.counts[label])) + } + return strings.Join(values, ",") +} + +func projectGitDiagnosticClass(err error) (string, string) { + switch { + case errors.Is(err, context.Canceled): + return "context_canceled", "unavailable" + case errors.Is(err, context.DeadlineExceeded): + return "context_deadline", "unavailable" + case errors.Is(err, processadapter.ErrOrdinaryContainmentUnsupported): + return "ordinary_unsupported", "unavailable" + case errors.Is(err, processadapter.ErrStdoutLimitExceeded): + return "stdout_limit", "unavailable" + case errors.Is(err, processadapter.ErrOrdinaryForcedCleanup): + return "ordinary_forced_cleanup", "unavailable" + } + if class := processadapter.OrdinarySupervisorFailureClass(err); class != "" { + return class, "unavailable" + } + var exit interface{ GitTransportExitCode() int } + if errors.As(err, &exit) { + return "git_exit", strconv.Itoa(exit.GitTransportExitCode()) + } + return "other_git_or_process", "unavailable" +} + +func projectGitDiagnosticEndpoint(endpoint string) bool { + u, err := url.Parse(endpoint) + if err != nil || u.Scheme != "https" || u.Hostname() != "127.0.0.1" || u.User != nil || strings.ContainsAny(endpoint, "?#") || u.Path != "/repo.git" || u.RawPath != "" { + return false + } + port, err := strconv.Atoi(u.Port()) + return err == nil && port > 0 && port <= 65535 +} + +func projectGitDiagnosticWorkspace(root, dir string) bool { + if !filepath.IsAbs(root) || filepath.Clean(root) != root || filepath.Dir(dir) != root || filepath.Clean(dir) != dir { + return false + } + name := filepath.Base(dir) + if !strings.HasPrefix(name, "agentplugins-git-") && !strings.HasPrefix(name, "agentplugins-direct-ref-") && !strings.HasPrefix(name, "agentplugins-discovery-") { + return false + } + physical, err := filepath.EvalSymlinks(dir) + return err == nil && physical == dir +} + +func projectGitDiagnosticCause(err error, locators ...string) string { + value := err.Error() + if len(value) > 8192 || !utf8.ValidString(value) { + return "cause_omitted" + } + // process.OS can attach stderr beneath the native transport wrapper. + if strings.Contains(value, "client diagnostic:") { + return "cause_omitted" + } + if projectGitDiagnosticCredential.MatchString(value) { + return "cause_omitted" + } + for i := len(locators) - 1; i >= 0; i-- { + locator := locators[i] + if len(locator) >= 4 { + value = strings.ReplaceAll(value, locator, "") + } + } + value = projectGitDiagnosticURI.ReplaceAllString(value, "") + quoted := strconv.QuoteToASCII(value) + if len(quoted) > 2048 { + quoted = quoted[:2028] + "...[truncated]" + } + return quoted +} + +func projectGitDiagnosticOperation(command sourceacquisition.Command, endpoint string) (phase string, known, explicit bool) { + args := command.Args + repo := filepath.Join(command.Dir, "repository") + if slices.Equal(args, []string{"init", "--quiet", repo}) { + return "init", true, false + } + if len(args) < 3 || args[0] != "-C" || args[1] != repo { + return "unknown", false, false + } + args = args[2:] + switch args[0] { + case "remote": + if slices.Equal(args, []string{"remote", "add", "origin", endpoint}) { + return "remote-add", true, false + } + case "config": + if len(args) == 3 { + want, ok := map[string]string{"core.autocrlf": "false", "core.filemode": "true", "core.symlinks": "true", "fetch.recurseSubmodules": "false"}[args[1]] + if ok && want == args[2] { + return "config", true, false + } + } + case "fetch": + if projectGitDiagnosticFetch(args, endpoint) { + return "fetch", true, slices.Contains(args, endpoint) + } + case "ls-remote": + if slices.Equal(args, []string{"ls-remote", "--symref", "--", endpoint, "HEAD"}) { + return "ls-remote", true, true + } + if len(args) >= 5 && len(args) <= 6 && args[1] == "--refs" && args[2] == "--" && args[3] == endpoint && projectGitDiagnosticRefs(args[4:]) { + return "ls-remote", true, true + } + case "check-ref-format": + if len(args) == 2 && projectGitDiagnosticRefs(args[1:]) { + return "check-ref-format", true, false + } + case "rev-parse": + if slices.Equal(args, []string{"rev-parse", "--verify", "FETCH_HEAD^{commit}"}) { + return "rev-parse", true, false + } + case "checkout": + if len(args) == 4 && slices.Equal(args[:3], []string{"checkout", "--quiet", "--detach"}) && projectGitDiagnosticSHA.MatchString(args[3]) { + return "checkout", true, false + } + case "ls-tree": + if len(args) == 6 && slices.Equal(args[:3], []string{"ls-tree", "-r", "-z"}) && projectGitDiagnosticSHA.MatchString(args[3]) && args[4] == "--" && projectGitDiagnosticPath(args[5]) { + return "ls-tree", true, false + } + case "sparse-checkout": + if slices.Equal(args, []string{"sparse-checkout", "init", "--cone"}) { + return "sparse-init", true, false + } + if len(args) == 5 && slices.Equal(args[:4], []string{"sparse-checkout", "set", "--cone", "--"}) && projectGitDiagnosticPath(args[4]) { + return "sparse-set", true, false + } + case "show": + if len(args) == 2 { + revision, path, ok := strings.Cut(args[1], ":") + if ok && projectGitDiagnosticSHA.MatchString(revision) && projectGitDiagnosticPath(path) { + return "show", true, false + } + } + } + return "unknown", false, false +} + +func projectGitDiagnosticFetch(args []string, endpoint string) bool { + if len(args) < 7 || !slices.Equal(args[:3], []string{"fetch", "--quiet", "--depth=1"}) { + return false + } + tail := args[3:] + if len(tail) > 0 && tail[0] == "--filter=blob:none" { + tail = tail[1:] + } + if len(tail) == 4 && slices.Equal(tail[:3], []string{"--no-tags", "--no-recurse-submodules", "origin"}) { + return projectGitDiagnosticSHA.MatchString(tail[3]) + } + return len(tail) == 5 && slices.Equal(tail[:4], []string{"--no-tags", "--no-recurse-submodules", "--", endpoint}) && projectGitDiagnosticSHA.MatchString(tail[4]) +} + +func projectGitDiagnosticRefs(refs []string) bool { + for _, ref := range refs { + if (!strings.HasPrefix(ref, "refs/heads/") && !strings.HasPrefix(ref, "refs/tags/")) || strings.ContainsAny(ref, "\x00\r\n ") || strings.Contains(ref, "..") { + return false + } + } + return len(refs) > 0 +} + +func projectGitDiagnosticPath(path string) bool { + if path == "." { + return true + } + if path == "" || strings.HasPrefix(path, "/") || strings.ContainsAny(path, "\x00\r\n\\") { + return false + } + for _, segment := range strings.Split(path, "/") { + if segment == "" || segment == "." || segment == ".." { + return false + } + } + return true +} diff --git a/cli/cmd/agentplugins/project_grok_e2e_test.go b/cli/cmd/agentplugins/project_grok_e2e_test.go new file mode 100644 index 000000000..5bc11807d --- /dev/null +++ b/cli/cmd/agentplugins/project_grok_e2e_test.go @@ -0,0 +1,154 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type projectGrokDetector struct{ user, executable string } + +func (d projectGrokDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientGrok, ConfigRoot: d.user, ExecutablePath: d.executable, Version: "1.0.67", Status: domain.DetectionDetected}}, nil +} + +type projectGrokRunner struct { + t *testing.T + calls int + executable string +} + +func (r *projectGrokRunner) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + if !reflect.DeepEqual(command.Argv, []string{r.executable, "inspect", "--json"}) || command.Dir == "" { + r.t.Fatal("unexpected global/native command", command) + } + plugins := []any{} + root := filepath.Join(command.Dir, ".grok", "plugins") + entries, err := os.ReadDir(root) + if err != nil && !os.IsNotExist(err) { + return legacyports.CommandResult{}, err + } + for _, entry := range entries { + manifest := filepath.Join(root, entry.Name(), ".grok-plugin", "plugin.json") + body, err := os.ReadFile(manifest) + if err != nil { + return legacyports.CommandResult{}, err + } + var object map[string]any + if err := json.Unmarshal(body, &object); err != nil { + return legacyports.CommandResult{}, err + } + plugins = append(plugins, map[string]any{"name": object["name"], "scope": "project", "path": filepath.Join(root, entry.Name()), "enabled": false}) + } + body, _ := json.Marshal(map[string]any{"grokVersion": "1.0.67", "cwd": command.Dir, "projectRoot": command.Dir + string(filepath.Separator), "projectTrusted": false, "skills": []any{}, "plugins": plugins}) + return legacyports.CommandResult{Stdout: body}, nil +} +func configureProjectGrokJourney(app agentpluginscli.App, user string, runner *projectGrokRunner) agentpluginscli.App { + executable, err := os.Executable() + if err != nil { + runner.t.Fatal(err) + } + engine, err := installer.New(installer.Config{StateRoot: filepath.Dir(app.ManagedRoot), ManagedRoot: app.ManagedRoot, ProjectRoot: app.ProjectRoot, Registry: app.ClientRegistry, Runner: runner, EnableNativeObserver: true, HelperExecutable: executable}) + if err != nil { + runner.t.Fatal(err) + } + app.Installer = engine + app.Detector = projectGrokDetector{user: user, executable: runner.executable} + app.Lifecycle.Activator = providers.Activator{Registry: app.ClientRegistry, Runner: runner} + app.Lifecycle.NativeObserver = providers.NativeIdentityObserver{Registry: app.ClientRegistry, Runner: runner} + 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 configureProjectGrokJourney(selected, user, runner), nil + } + return app +} + +// Real TLS Git acquisition, production saved CLI/planner/staging/store/state +// and exact directory ownership, with inert inspect-only client transport. +func TestProjectGrokComposedSavedLifecycle(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, ".grok") + projectJourneyWrite(t, filepath.Join(user, "keep"), "original user\n") + runner := &projectGrokRunner{t: t, executable: filepath.Join(journey.home, "grok-not-executed")} + journey.app = configureProjectGrokJourney(journey.app, user, runner) + userBefore := securityFixtureTree(t, user) + for _, project := range []string{journey.a, journey.b} { + journey.run(t, project, "add", transport.endpoint, "--alias", "docs", "--target", "grok") + projectGrokOwned(t, journey, project, projectJourneySkill) + } + bBefore := securityFixtureTree(t, journey.b) + installation, binding := journey.owned(t, journey.a) + if len(installation.DataReceipts) != 0 { + t.Fatal("skills-only projection invented MCP persistent data") + } + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + beforeAttest := securityFixtureTree(t, filepath.Join(journey.a, ".grok")) + if _, err := journey.execute(journey.a, "add", transport.endpoint, "--alias", "docs", "--target", "grok", "--activation-complete"); err == nil { + t.Fatal("persisted prepared Grok binding accepted activation attestation") + } + if !reflect.DeepEqual(beforeAttest, securityFixtureTree(t, filepath.Join(journey.a, ".grok"))) { + t.Fatal("refused attestation changed native project bytes") + } + _, refused := journey.owned(t, journey.a) + if refused.Activation == domain.ActivationActive || refused.Verification == domain.VerificationInstalled || refused.TargetLocator != binding.TargetLocator { + t.Fatal("refused attestation promoted loading or changed artifact ownership") + } + // A rejected attestation records a failed attempt in existing core state. + // An explicit ordinary add retries that attempt without native trust or loading. + journey.run(t, journey.a, "add", transport.endpoint, "--alias", "docs", "--target", "grok") + projectGrokOwned(t, journey, journey.a, projectJourneySkill) + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "TEST immutable drift\n") + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance disposable Grok source") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "grok") + projectGrokOwned(t, journey, journey.a, projectJourneySkill) + journey.run(t, journey.a, "update", "docs", "--target", "grok") + updated, current := projectGrokOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator { + t.Fatal("Grok update lost ownership or exact pin") + } + status := journey.run(t, journey.a, "deps", "status") + if !strings.Contains(status, `"activation_prepared"`) || strings.Contains(status, `"activation_active"`) { + t.Fatal("status promoted unqualified native loading", status) + } + journey.run(t, journey.a, "remove", "docs", "--target", "grok") + projectJourneyAbsent(t, current.TargetLocator) + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) { + t.Fatal("Grok A lifecycle changed original user or B") + } + if runner.calls == 0 { + t.Fatal("composed contract did not exercise read-only source discovery") + } +} +func projectGrokOwned(t *testing.T, journey projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, binding := journey.owned(t, project) + if binding.ClientID != "grok" || binding.Scope != "project" || binding.NativeProfileRoot != filepath.Join(project, ".grok") || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationPackageValid { + t.Fatalf("Grok binding lost conservative project authority %+v", binding) + } + if projectJourneyRead(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md")) != expected { + t.Fatal("Grok exact immutable skill differs") + } + if _, err := os.Stat(filepath.Join(binding.TargetLocator, "mcp.json")); !os.IsNotExist(err) { + t.Fatal("Grok bundled MCP projected", err) + } + return installation, binding +} diff --git a/cli/cmd/agentplugins/project_kimi_e2e_test.go b/cli/cmd/agentplugins/project_kimi_e2e_test.go new file mode 100644 index 000000000..67839c4ea --- /dev/null +++ b/cli/cmd/agentplugins/project_kimi_e2e_test.go @@ -0,0 +1,118 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Public saved CLI, real TLS Git and the production source/store/planner, +// installer/native transactions. Detection/security are inert host boundaries. +// The trusted test executable is copied as launcher bytes but never executed; +// this test certifies no Kimi, MCP, model, trust or native discovery behavior. +func TestProjectKimiComposedSavedLifecycle(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, ".kimi") + journey.app = configureProjectKimiJourney(journey.app, user, filepath.Join(journey.home, "kimi")) + foreign := `{"mcpServers":{"foreign":{"transport":"http","url":"https://foreign.test/mcp","enabled":false}},"policy":"keep"}` + for _, project := range []string{user, journey.a, journey.b} { + native := filepath.Join(project, ".kimi-code") + if project == user { + native = user + } else { + if err := os.Mkdir(filepath.Join(project, ".git"), 0700); err != nil { + t.Fatal(err) + } + } + projectJourneyWrite(t, filepath.Join(native, "mcp.json"), foreign) + projectJourneyWrite(t, filepath.Join(native, "skills", "foreign", "SKILL.md"), "foreign Kimi skill\n") + } + projectJourneyWrite(t, filepath.Join(journey.a, ".mcp.json"), foreign) + userBefore := securityFixtureTree(t, user) + for _, project := range []string{journey.a, journey.b} { + journey.run(t, project, "add", transport.endpoint, "--alias", "docs", "--target", "kimi") + projectKimiOwned(t, journey, project, projectJourneySkill) + } + projectJourneyWrite(t, filepath.Join(journey.b, "B-retained-marker"), "other project\n") + bBefore := securityFixtureTree(t, journey.b) + statePath := filepath.Join(journey.namespace(journey.a), "state-v2.json") + stateBefore := projectJourneyRead(t, statePath) + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + if stateBefore != projectJourneyRead(t, statePath) { + t.Fatal("unchanged Kimi reconciliation rewrote ownership") + } + installation, binding := projectKimiOwned(t, journey, journey.a, projectJourneySkill) + projectJourneyDataMarker(t, journey, journey.a, installation) + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "TEST immutable drift\n") + if err := os.RemoveAll(filepath.Join(journey.a, ".kimi-code", "skills", "docs")); err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance disposable Kimi source") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "kimi") + projectKimiOwned(t, journey, journey.a, projectJourneySkill) + projectKimiRecover(t, journey) + projectKimiUpdateRemove(t, journey, installation, binding) + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) { + t.Fatal("Kimi A lifecycle mutated the user profile or B") + } +} + +func projectKimiUpdateRemove(t *testing.T, journey projectJourney, installation domain.Installation, binding domain.ClientBinding) { + t.Helper() + journey.run(t, journey.a, "update", "docs", "--target", "kimi") + updated, current := projectKimiOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator || updated.Source.SourceBindingID != installation.Source.SourceBindingID { + t.Fatal("Kimi update lost exact source/binding authority or did not advance") + } + status := journey.run(t, journey.a, "deps", "status") + for _, expected := range []string{`"kimi"`, `"project"`, `"activation_prepared"`} { + if !strings.Contains(status, expected) { + t.Fatalf("Kimi status omitted %s: %s", expected, status) + } + } + journey.run(t, journey.a, "remove", "docs", "--target", "kimi") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(journey.a, ".kimi-code", "skills", "docs")) + config := projectJourneyRead(t, filepath.Join(journey.a, ".kimi-code", "mcp.json")) + if strings.Contains(config, `"docs"`) || !strings.Contains(config, "foreign.test") || !strings.Contains(config, `"enabled":false`) { + t.Fatalf("Kimi exact removal changed foreign settings: %s", config) + } + if projectJourneyRead(t, filepath.Join(journey.a, ".kimi-code", "skills", "foreign", "SKILL.md")) != "foreign Kimi skill\n" { + t.Fatal("Kimi removal changed an unrelated project skill") + } + for key, receipt := range installation.DataReceipts { + if updated.DataReceipts[key].Locator != receipt.Locator || projectJourneyRead(t, filepath.Join(receipt.Locator, "keep.txt")) != "retained environment data\n" { + t.Fatal("Kimi update/remove changed retained data authority") + } + } + if len(journey.state(t, journey.a).EnvironmentReceipts) != 0 { + t.Fatal("Kimi remove retained saved environment ownership") + } + after := securityFixtureTree(t, journey.a) + _, _ = journey.execute(journey.a, "remove", "docs", "--target", "kimi") + if !reflect.DeepEqual(after, securityFixtureTree(t, journey.a)) { + t.Fatal("repeat absent Kimi removal changed bytes") + } +} + +func projectKimiRecover(t *testing.T, journey projectJourney) { + t.Helper() + github := securityFixtureTree(t, filepath.Join(journey.a, ".kimi-code")) + config := projectJourneyRead(t, filepath.Join(journey.a, ".kimi-code", "mcp.json")) + staging, active := projectJourneyInterruptDirectoryJournal(t, journey) + for range 2 { + journey.run(t, journey.a, "deps", "recover", "--yes") + } + journey.inspectNoRecovery(t, journey.a) + projectJourneyAbsent(t, active) + if !reflect.DeepEqual(github, securityFixtureTree(t, filepath.Join(journey.a, ".kimi-code"))) || config != projectJourneyRead(t, filepath.Join(journey.a, ".kimi-code", "mcp.json")) || projectJourneyRead(t, filepath.Join(staging, "unpublished.txt")) != "caller-owned unpublished bytes\n" { + t.Fatal("Kimi bounded pending-journal recovery changed native/caller bytes") + } +} diff --git a/cli/cmd/agentplugins/project_kimi_fixture_test.go b/cli/cmd/agentplugins/project_kimi_fixture_test.go new file mode 100644 index 000000000..26c962b95 --- /dev/null +++ b/cli/cmd/agentplugins/project_kimi_fixture_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type projectKimiDetector struct{ user, executable string } + +func (d projectKimiDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientKimi, ConfigRoot: d.user, + Version: "2.3.1", ExecutablePath: d.executable, Status: domain.DetectionDetected}}, nil +} + +func configureProjectKimiJourney(app agentpluginscli.App, user, executable string) agentpluginscli.App { + app.Detector = projectKimiDetector{user: user, executable: executable} + 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 configureProjectKimiJourney(selected, user, executable), nil + } + return app +} + +func projectKimiOwned(t *testing.T, journey projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, binding := journey.owned(t, project) + if binding.ClientID != "kimi" || binding.Scope != "project" || binding.NativeProfileRoot != filepath.Join(project, ".kimi-code") || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("Kimi binding lost project authority or conservative native status: %+v", binding) + } + for _, path := range []string{filepath.Join(project, ".kimi-code", "skills", "docs", "SKILL.md"), filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md")} { + if projectJourneyRead(t, path) != expected { + t.Fatalf("Kimi managed/native skill differs at %s", path) + } + } + return installation, binding +} diff --git a/cli/cmd/agentplugins/project_kimi_imperative_test.go b/cli/cmd/agentplugins/project_kimi_imperative_test.go new file mode 100644 index 000000000..4bb50a0d0 --- /dev/null +++ b/cli/cmd/agentplugins/project_kimi_imperative_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectKimiImperativeLocalLifecyclePreservesUserAndB(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, ".kimi-code") + journey.app = configureProjectKimiJourney(journey.app, user, filepath.Join(journey.home, "kimi")) + for _, root := range []string{journey.a, journey.b} { + if err := os.Mkdir(filepath.Join(root, ".git"), 0700); err != nil { + t.Fatal(err) + } + } + projectJourneyWrite(t, filepath.Join(user, "mcp.json"), `{"mcpServers":{"foreign":{"url":"https://foreign.test","enabled":false}}}`) + projectJourneyWrite(t, filepath.Join(journey.b, ".kimi-code", "skills", "foreign", "SKILL.md"), "other project\n") + beforeUser, beforeB := securityFixtureTree(t, user), securityFixtureTree(t, journey.b) + journey.run(t, journey.a, "add", transport.repo, "--target", "kimi", "--no-save") + state := journey.state(t, journey.a) + if len(state.Installations) != 1 || len(state.EnvironmentReceipts) != 0 { + t.Fatalf("imperative route changed saved ownership: %+v", state) + } + installation := state.Installations[0] + for _, binding := range installation.Clients { + if binding.ClientID != "kimi" || binding.Scope != "project" || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("imperative Kimi overclaimed or lost scope: %+v", binding) + } + if projectJourneyRead(t, filepath.Join(journey.a, ".kimi-code", "skills", "docs", "SKILL.md")) != projectJourneySkill { + t.Fatal("imperative skill projection missing") + } + } + journey.run(t, journey.a, "remove", installation.InstallationID, "--target", "kimi") + projectJourneyAbsent(t, filepath.Join(journey.a, ".kimi-code", "skills", "docs")) + if !reflect.DeepEqual(beforeUser, securityFixtureTree(t, user)) || !reflect.DeepEqual(beforeB, securityFixtureTree(t, journey.b)) { + t.Fatal("imperative project lifecycle changed user or B") + } +} diff --git a/cli/cmd/agentplugins/project_kimi_transfer_test.go b/cli/cmd/agentplugins/project_kimi_transfer_test.go new file mode 100644 index 000000000..8bdac3be9 --- /dev/null +++ b/cli/cmd/agentplugins/project_kimi_transfer_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// Cold-cache portable import uses the production transport/store/scope path; +// native Kimi and the inert trusted launcher are never executed. +func TestProjectKimiColdSavedImportPreservesPinnedAgreement(t *testing.T) { + transport := newProjectJourneyTransport(t, "archive") + original := newProjectJourney(t, transport) + original.app = configureProjectKimiJourney(original.app, filepath.Join(original.home, ".kimi-code"), filepath.Join(original.home, "kimi")) + if err := os.Mkdir(filepath.Join(original.a, ".git"), 0700); err != nil { + t.Fatal(err) + } + original.run(t, original.a, "add", transport.endpoint, "--alias", "docs", "--target", "kimi") + installation, _ := projectKimiOwned(t, original, original.a, projectJourneySkill) + pair := original.assertPortablePair(t, original.a) + exchange := projectJourneyTemp(t) + original.run(t, original.a, "deps", "export", exchange, "--yes") + cold := newProjectJourney(t, transport) + cold.app = configureProjectKimiJourney(cold.app, filepath.Join(cold.home, ".kimi-code"), filepath.Join(cold.home, "kimi")) + if err := os.Mkdir(filepath.Join(cold.a, ".git"), 0700); err != nil { + t.Fatal(err) + } + projectJourneyAbsent(t, cold.cache, filepath.Join(cold.a, ".uap")) + cold.run(t, cold.a, "deps", "import", exchange, "--install", "--yes") + restored, _ := projectKimiOwned(t, cold, cold.a, projectJourneySkill) + if restored.Source.SourceBindingID != installation.Source.SourceBindingID || restored.Source.TreeDigest != installation.Source.TreeDigest || pair != cold.assertPortablePair(t, cold.a) { + t.Fatal("Kimi cold restore changed exact source/portable agreement") + } + cold.run(t, cold.a, "remove", "docs", "--target", "kimi") +} diff --git a/cli/cmd/agentplugins/project_lifecycle_assertions_test.go b/cli/cmd/agentplugins/project_lifecycle_assertions_test.go new file mode 100644 index 000000000..a0c79155d --- /dev/null +++ b/cli/cmd/agentplugins/project_lifecycle_assertions_test.go @@ -0,0 +1,170 @@ +package main + +import ( + "bytes" + "context" + "os" + "path/filepath" + "reflect" + "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/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" +) + +func (journey projectJourney) execute(project string, args ...string) (string, error) { + var output, stderr bytes.Buffer + app := journey.app + app.Input, app.Output, app.ErrorOutput, app.Terminal = strings.NewReader(""), &output, &stderr, false + root := agentpluginscli.NewRoot(app) + root.SetOut(&output) + root.SetErr(&stderr) + args = append(args, "--format", "json") + if project != "" { + args = append(args, "--scope", "project", "--project-root", project) + } + root.SetArgs(args) + err := root.Execute() + return output.String() + stderr.String(), err +} + +func (journey projectJourney) run(t *testing.T, project string, args ...string) string { + t.Helper() + var sequence uint64 + if journey.transport != nil && journey.transport.diagnostic != nil { + sequence = journey.transport.diagnostic.failureSequence() + } + output, err := journey.execute(project, args...) + if err != nil { + if journey.transport != nil && journey.transport.diagnostic != nil { + journey.transport.diagnostic.outsideInvocations(sequence) + } + t.Fatalf("%v in project %q: %v\n%s", args, project, err, output) + } + return output +} + +func (journey projectJourney) state(t *testing.T, project string) domain.StateFileV2 { + t.Helper() + scope := domain.ScopeUser + if project != "" { + scope = domain.ScopeProject + } + app, err := journey.app.ForScope(scope, project) + if err != nil { + t.Fatal(err) + } + state, err := app.StateStore.Load() + if err != nil { + t.Fatal(err) + } + return state +} + +func (journey projectJourney) owned(t *testing.T, project string) (domain.Installation, domain.ClientBinding) { + t.Helper() + state := journey.state(t, project) + if len(state.Installations) != 1 || len(state.Installations[0].Clients) != 1 || len(state.EnvironmentReceipts) != 1 { + t.Fatalf("expected one owned package, binding and environment receipt: %+v", state) + } + installation := state.Installations[0] + for _, binding := range installation.Clients { + return installation, binding + } + t.Fatal("binding missing") + return domain.Installation{}, domain.ClientBinding{} +} + +func (journey projectJourney) namespace(project string) string { + if project == "" { + return journey.cache + } + return filepath.Join(project, ".uap") +} + +func (journey projectJourney) nativeRoot(project string) string { + if project == "" { + return filepath.Join(journey.home, ".gemini") + } + return filepath.Join(project, ".gemini") +} + +func (journey projectJourney) assertOwnedContext(t *testing.T, project string) domain.Installation { + t.Helper() + installation, binding := journey.owned(t, project) + scope, environmentID := domain.ScopeUser, environment.EnvironmentUserDefault + activation := domain.ActivationActive + if project != "" { + scope, environmentID, activation = domain.ScopeProject, environment.EnvironmentProjectDefault, domain.ActivationPrepared + } + state := journey.state(t, project) + if binding.Scope != string(scope) || binding.NativeProfileRoot != journey.nativeRoot(project) || binding.Activation != activation || binding.Verification != domain.VerificationInstalled || state.EnvironmentReceipts[0].EnvironmentID != environmentID { + t.Fatalf("scope/lifecycle/owner context mismatch: %+v, %+v", binding, state.EnvironmentReceipts) + } + if err := pathpolicy.RequireContainedChild(journey.namespace(project), binding.TargetLocator); err != nil { + t.Fatal(err) + } + if got := projectJourneyRead(t, filepath.Join(journey.nativeRoot(project), "skills", "docs", "SKILL.md")); got != projectJourneySkill { + t.Fatalf("native skill bytes = %q", got) + } + settings := projectJourneyRead(t, filepath.Join(journey.nativeRoot(project), "settings.json")) + for _, retained := range []string{"retained foreign settings", "foreign.test", `"docs"`} { + if !strings.Contains(settings, retained) { + t.Fatalf("native settings missing %q: %s", retained, settings) + } + } + return installation +} + +func (journey projectJourney) assertOtherScopeUnchanged(t *testing.T, project string, native map[string]string, state string) { + t.Helper() + if !reflect.DeepEqual(native, securityFixtureTree(t, journey.nativeRoot(project))) || state != projectJourneyRead(t, filepath.Join(journey.namespace(project), "state-v2.json")) { + t.Fatalf("operation changed another environment %q", project) + } + installation, binding := journey.owned(t, project) + if projectJourneyRead(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md")) != projectJourneySkill { + t.Fatal("operation changed another environment's managed bytes") + } + for _, receipt := range installation.DataReceipts { + if projectJourneyRead(t, filepath.Join(receipt.Locator, "keep.txt")) != "retained environment data\n" { + t.Fatal("operation changed another environment's persistent data") + } + } +} + +func (journey projectJourney) assertPortablePair(t *testing.T, root string) [2]string { + t.Helper() + pair := [2]string{projectJourneyRead(t, filepath.Join(root, "uap.toml")), projectJourneyRead(t, filepath.Join(root, "uap.lock"))} + for _, body := range pair { + for _, path := range []string{journey.home, journey.cache, journey.a, journey.b, journey.transport.repo, journey.transport.remote} { + if strings.Contains(body, path) || strings.Contains(body, filepath.ToSlash(path)) || strings.Contains(body, strings.ReplaceAll(path, `\`, `\\`)) { + t.Fatalf("portable agreement contains machine path %q", path) + } + } + } + return pair +} + +func (journey projectJourney) inspectNoRecovery(t *testing.T, project string) { + t.Helper() + app, err := journey.app.ForScope(domain.ScopeProject, project) + if err != nil { + t.Fatal(err) + } + view, err := app.Installer.Inspect(context.Background()) + if err != nil || view.Recovery.Required { + t.Fatalf("project inspection requires recovery: %+v, %v", view.Recovery, err) + } +} + +func projectJourneyAbsent(t *testing.T, paths ...string) { + t.Helper() + for _, path := range paths { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected absent path %s: %v", path, err) + } + } +} diff --git a/cli/cmd/agentplugins/project_lifecycle_e2e_test.go b/cli/cmd/agentplugins/project_lifecycle_e2e_test.go new file mode 100644 index 000000000..7dbd21cd7 --- /dev/null +++ b/cli/cmd/agentplugins/project_lifecycle_e2e_test.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/environmentstore" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// All composition, CLI selection, package loading, immutable storage, planner, +// installer and native filesystem transactions are production implementations. +// Git/ZIP use real TLS; only host detection and security assessment are inert +// fixture boundaries. No Gemini session or model process is certified here. +func TestProjectComposedSourceLifecycleSeparatesUserAndProjects(t *testing.T) { + for _, kind := range []string{"git", "zip"} { + t.Run(kind, func(t *testing.T) { + transport := newProjectJourneyTransport(t, kind) + journey := newProjectJourney(t, transport) + for _, root := range []string{"", journey.a, journey.b} { + journey.run(t, root, "add", transport.endpoint, "--alias", "docs", "--target", "gemini") + journey.assertOwnedContext(t, root) + } + projectJourneySharedIdentity(t, journey) + projectJourneyRepairAndRemove(t, journey) + }) + } +} + +func projectJourneySharedIdentity(t *testing.T, journey projectJourney) { + t.Helper() + user, _ := journey.owned(t, "") + for _, root := range []string{journey.a, journey.b} { + installation := journey.assertOwnedContext(t, root) + if installation.Source.SourceBindingID != user.Source.SourceBindingID || installation.Source.CanonicalSource != user.Source.CanonicalSource || installation.Source.TreeDigest != user.Source.TreeDigest { + t.Fatal("scope changed portable source/package identity") + } + projectJourneyDataMarker(t, journey, root, installation) + } + projectJourneyDataMarker(t, journey, "", user) + userPair := journey.assertPortablePair(t, journey.cache) + if userPair != journey.assertPortablePair(t, journey.a) || userPair != journey.assertPortablePair(t, journey.b) { + t.Fatal("same selection produced a machine-specific agreement") + } + store := environmentstore.Store{Root: filepath.Join(journey.cache, "store")} + cached, err := store.Get(context.Background(), domain.TreeDigestAlgorithm, user.Source.TreeDigest) + if err != nil { + t.Fatal(err) + } + if projectJourneyRead(t, filepath.Join(cached.Root, "skills", "docs", "SKILL.md")) != projectJourneySkill { + t.Fatal("shared immutable cache does not contain the installed pin") + } + for _, root := range []string{journey.a, journey.b} { + projectJourneyAbsent(t, filepath.Join(root, ".uap", "store")) + } +} + +func projectJourneyDataMarker(t *testing.T, journey projectJourney, root string, installation domain.Installation) { + t.Helper() + if len(installation.DataReceipts) != 1 { + t.Fatalf("expected scoped persistent data: %+v", installation.DataReceipts) + } + for _, receipt := range installation.DataReceipts { + if err := pathpolicy.RequireContainedChild(filepath.Join(journey.namespace(root), "plugin-data"), receipt.Locator); err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(receipt.Locator, "keep.txt"), "retained environment data\n") + } +} + +func projectJourneyRepairAndRemove(t *testing.T, journey projectJourney) { + t.Helper() + userNative, bNative := securityFixtureTree(t, journey.nativeRoot("")), securityFixtureTree(t, journey.nativeRoot(journey.b)) + userState := projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) + bState := projectJourneyRead(t, filepath.Join(journey.namespace(journey.b), "state-v2.json")) + statePath := filepath.Join(journey.namespace(journey.a), "state-v2.json") + before := projectJourneyRead(t, statePath) + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + if before != projectJourneyRead(t, statePath) { + t.Fatal("unchanged locked reconciliation rewrote project receipts") + } + installation, binding := journey.owned(t, journey.a) + managedSkill := filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md") + projectJourneyWrite(t, managedSkill, "drifted managed package\n") + projectJourneyWrite(t, filepath.Join(journey.transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, journey.transport.repo, "add", ".") + projectJourneyGit(t, journey.transport.repo, "commit", "-m", "advance disposable project source") + projectJourneyGit(t, journey.transport.repo, "push", "--quiet", journey.transport.remote, "main") + journey.transport.mu.Lock() + journey.transport.archive = projectJourneyArchive(t, journey.transport.repo) + journey.transport.mu.Unlock() + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "gemini") + if projectJourneyRead(t, managedSkill) != projectJourneySkill { + t.Fatal("repair followed moving source instead of installed pin") + } + journey.assertOwnedContext(t, journey.a) + projectJourneyRecoverInterruptedJournal(t, journey) + projectJourneyUpdateSelectedProject(t, journey, installation, binding) + journey.run(t, journey.a, "remove", "docs", "--target", "gemini") + projectJourneyAssertRemoval(t, journey, installation, binding) + journey.assertOtherScopeUnchanged(t, "", userNative, userState) + journey.assertOtherScopeUnchanged(t, journey.b, bNative, bState) +} + +func projectJourneyUpdateSelectedProject(t *testing.T, journey projectJourney, previous domain.Installation, binding domain.ClientBinding) { + t.Helper() + journey.run(t, journey.a, "update", "docs", "--target", "gemini") + updated, selected := journey.owned(t, journey.a) + if updated.Source.SourceBindingID != previous.Source.SourceBindingID || updated.Source.TreeDigest == previous.Source.TreeDigest || updated.Source.ResolvedRevision == previous.Source.ResolvedRevision || selected.ClientBindingID != binding.ClientBindingID || selected.TargetLocator != binding.TargetLocator { + t.Fatal("project update did not advance within its existing source/binding authority") + } + if selected.Activation != domain.ActivationPrepared || selected.Verification != domain.VerificationInstalled || selected.NativeProfileRoot != journey.nativeRoot(journey.a) { + t.Fatalf("project update lost conservative scoped lifecycle: %+v", selected) + } + for _, root := range []string{selected.TargetLocator, journey.nativeRoot(journey.a)} { + if projectJourneyRead(t, filepath.Join(root, "skills", "docs", "SKILL.md")) != projectJourneyAdvancedSkill { + t.Fatal("selected project did not receive updated pinned bytes") + } + } + for key, receipt := range previous.DataReceipts { + if updated.DataReceipts[key].Locator != receipt.Locator || projectJourneyRead(t, filepath.Join(receipt.Locator, "keep.txt")) != "retained environment data\n" { + t.Fatal("project update replaced persistent data authority") + } + } +} + +func projectJourneyAssertRemoval(t *testing.T, journey projectJourney, installation domain.Installation, binding domain.ClientBinding) { + t.Helper() + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(journey.nativeRoot(journey.a), "skills", "docs")) + settings := projectJourneyRead(t, filepath.Join(journey.nativeRoot(journey.a), "settings.json")) + if strings.Contains(settings, `"docs"`) || !strings.Contains(settings, "foreign.test") || !strings.Contains(settings, "retained foreign settings") { + t.Fatalf("exact removal changed foreign MCP configuration: %s", settings) + } + for _, receipt := range installation.DataReceipts { + if projectJourneyRead(t, filepath.Join(receipt.Locator, "keep.txt")) != "retained environment data\n" { + t.Fatal("removal consumed retained project data") + } + } + state := journey.state(t, journey.a) + if len(state.EnvironmentReceipts) != 0 { + t.Fatal("removed project retained an environment ownership receipt") + } + if projectJourneyRead(t, filepath.Join(journey.nativeRoot(journey.a), "skills", "foreign", "SKILL.md")) != "foreign skill\n" { + t.Fatal("foreign native skill changed") + } +} diff --git a/cli/cmd/agentplugins/project_lifecycle_fixture_test.go b/cli/cmd/agentplugins/project_lifecycle_fixture_test.go new file mode 100644 index 000000000..b5bf65a77 --- /dev/null +++ b/cli/cmd/agentplugins/project_lifecycle_fixture_test.go @@ -0,0 +1,204 @@ +package main + +import ( + "archive/zip" + "bytes" + "context" + "encoding/pem" + "net/http" + "net/http/cgi" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "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 projectJourneyDetector struct{ root string } + +func (detector projectJourneyDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientGemini, ConfigRoot: detector.root, Status: domain.DetectionDetected}}, nil +} + +type projectJourneySecurity struct{} + +func (projectJourneySecurity) Evaluate(context.Context, domain.SecurityEvaluationInput) (domain.SecurityAssessment, error) { + return domain.SecurityAssessment{}, nil +} + +type projectJourneyTransport struct { + server *httptest.Server + endpoint string + repo string + remote string + mu sync.Mutex + archive []byte + diagnostic *projectGitDiagnostic +} + +type projectJourney struct { + app agentpluginscli.App + home string + cache string + a, b string + transport *projectJourneyTransport +} + +const projectJourneySkill = "---\nname: docs\ndescription: Inert project documentation\n---\nUAP-PROJECT-SOURCE-FIXTURE\n" +const projectJourneyAdvancedSkill = "---\nname: docs\ndescription: Inert project documentation\n---\nUAP-PROJECT-UPDATED-FIXTURE\n" + +// Resolve existing fixture roots before composition and exact receipt assertions. +// Native observations and production path validation remain unchanged. +func projectJourneyTemp(t *testing.T) string { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + return root +} + +func projectJourneyWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func projectJourneyGit(t *testing.T, root string, args ...string) { + t.Helper() + command := exec.CommandContext(context.Background(), "git", append([]string{"-c", "commit.gpgsign=false", "-c", "core.autocrlf=false"}, args...)...) + command.Dir = root + command.Env = append(os.Environ(), "GIT_AUTHOR_NAME=UAP fixture", "GIT_AUTHOR_EMAIL=fixture@example.test", "GIT_COMMITTER_NAME=UAP fixture", "GIT_COMMITTER_EMAIL=fixture@example.test") + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", args, err, output) + } +} + +func newProjectJourneyTransport(t *testing.T, kind string) *projectJourneyTransport { + t.Helper() + gitHome := projectJourneyTemp(t) + for _, name := range []string{"HOME", "USERPROFILE", "XDG_CONFIG_HOME"} { + t.Setenv(name, gitHome) + } + projectJourneyWrite(t, filepath.Join(gitHome, ".gitconfig"), "[credential]\n helper =\n[http]\n schannelUseSSLCAInfo = true\n") + transport := &projectJourneyTransport{repo: projectJourneyTemp(t)} + projectJourneyWrite(t, filepath.Join(transport.repo, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"project-demo","version":"1.0.0"}`) + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneySkill) + projectJourneyWrite(t, filepath.Join(transport.repo, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"go","args":["version"],"env":{"DATA":"${PLUGIN_DATA}"}}}}`) + projectJourneyGit(t, transport.repo, "init", "-b", "main") + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "disposable project source") + remoteRoot := projectJourneyTemp(t) + transport.remote = filepath.Join(remoteRoot, "repo.git") + projectJourneyGit(t, transport.repo, "clone", "--quiet", "--bare", transport.repo, transport.remote) + git, err := exec.LookPath("git") + if err != nil { + t.Fatal(err) + } + backend := &cgi.Handler{Path: git, Args: []string{"http-backend"}, Env: []string{"GIT_PROJECT_ROOT=" + remoteRoot, "GIT_HTTP_EXPORT_ALL=1"}} + transport.archive = projectJourneyArchive(t, transport.repo) + transport.server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/repo.git/") { + backend.ServeHTTP(w, r) + return + } + transport.mu.Lock() + defer transport.mu.Unlock() + _, _ = w.Write(transport.archive) + })) + t.Cleanup(transport.server.Close) + ca := filepath.Join(projectJourneyTemp(t), "ca.pem") + projectJourneyWrite(t, ca, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: transport.server.Certificate().Raw}))) + t.Setenv("GIT_SSL_CAINFO", ca) + transport.endpoint = transport.server.URL + "/package.zip" + if kind == "git" { + transport.endpoint = transport.server.URL + "/repo.git" + } + return transport +} + +func projectJourneyArchive(t *testing.T, root string) []byte { + t.Helper() + var body bytes.Buffer + writer := zip.NewWriter(&body) + for _, name := range []string{"plugin.json", "mcp.json", "skills/docs/SKILL.md"} { + file, err := writer.Create("provider-wrapper/" + name) + if err != nil { + t.Fatal(err) + } + if _, err := file.Write([]byte(projectJourneyRead(t, filepath.Join(root, filepath.FromSlash(name))))); err != nil { + t.Fatal(err) + } + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + return body.Bytes() +} + +func newProjectJourney(t *testing.T, transport *projectJourneyTransport) projectJourney { + t.Helper() + if transport.diagnostic == nil { + transport.diagnostic = &projectGitDiagnostic{counts: map[string]uint16{}, endpoint: transport.server.URL + "/repo.git", log: func(record string) { t.Log(record) }} + } + journey := projectJourney{home: projectJourneyTemp(t), a: projectJourneyTemp(t), b: projectJourneyTemp(t), transport: transport} + journey.cache = filepath.Join(journey.home, "uap") + for _, native := range []string{filepath.Join(journey.home, ".gemini"), filepath.Join(journey.a, ".gemini"), filepath.Join(journey.b, ".gemini")} { + projectJourneyWrite(t, filepath.Join(native, "settings.json"), "{\n// retained foreign settings\n\"mcpServers\":{\"foreign\":{\"url\":\"https://foreign.test/mcp\"}},\"theme\":\"foreign\"\n}\n") + projectJourneyWrite(t, filepath.Join(native, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + registry, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + app, err := newAgentpluginsCLIApp(journey.home, journey.cache, nil, nil, nil, registry) + if err != nil { + t.Fatal(err) + } + journey.app = configureProjectJourney(app, journey.home, transport) + return journey +} + +// Override only host boundaries. Reapply them after the production ForScope +// composer rebuilds its runtime; keep installer, native kernel and store intact. +func configureProjectJourney(app agentpluginscli.App, home string, transport *projectJourneyTransport) agentpluginscli.App { + app.Detector = projectJourneyDetector{root: filepath.Join(home, ".gemini")} + 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(), Runner: projectGitDiagnosticRunner{recorder: transport.diagnostic, root: home}} + app.SourceAcquirerForDryRun = func(root string) agentpluginscli.SourceAcquirer { + return sourceacquisition.Acquirer{TempRoot: root, HTTPClient: transport.server.Client(), Runner: projectGitDiagnosticRunner{recorder: transport.diagnostic, root: root}} + } + 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 configureProjectJourney(selected, home, transport), nil + } + return app +} + +func projectJourneyRead(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} diff --git a/cli/cmd/agentplugins/project_lifecycle_recovery_test.go b/cli/cmd/agentplugins/project_lifecycle_recovery_test.go new file mode 100644 index 000000000..14e9a91ed --- /dev/null +++ b/cli/cmd/agentplugins/project_lifecycle_recovery_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/dirswap" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" +) + +func projectJourneyRecoverInterruptedJournal(t *testing.T, journey projectJourney) { + t.Helper() + before := securityFixtureTree(t, journey.nativeRoot(journey.a)) + statePath := filepath.Join(journey.namespace(journey.a), "state-v2.json") + state := projectJourneyRead(t, statePath) + staging, active := projectJourneyInterruptDirectoryJournal(t, journey) + selected, err := journey.app.ForScope(domain.ScopeProject, journey.a) + if err != nil { + t.Fatal(err) + } + view, err := selected.Installer.Inspect(context.Background()) + if err != nil || !view.Recovery.Required || len(view.Recovery.Journals) != 1 { + t.Fatalf("interrupted project journal not observed: %+v, %v", view.Recovery, err) + } + journey.run(t, journey.a, "deps", "recover", "--yes") + journey.inspectNoRecovery(t, journey.a) + projectJourneyAbsent(t, active, filepath.Join(journey.namespace(journey.a), "operations-v2", "project-interrupted-journal.json")) + // Directory rollback preserves unpublished staging owned by its caller; + // recovery consumes the durable journal without adopting those bytes. + if projectJourneyRead(t, filepath.Join(staging, "unpublished.txt")) != "caller-owned unpublished bytes\n" { + t.Fatal("recovery consumed unpublished caller staging") + } + if !reflect.DeepEqual(before, securityFixtureTree(t, journey.nativeRoot(journey.a))) || state != projectJourneyRead(t, statePath) { + t.Fatal("project journal recovery changed committed native/package ownership") + } + journey.run(t, journey.a, "deps", "recover", "--yes") +} + +// Reuse the existing directory transaction's fault seam and durable receipt, +// matching the installer pending-journal fixture. No alternate recovery engine +// or product-only injection mechanism is introduced. +func projectJourneyInterruptDirectoryJournal(t *testing.T, journey projectJourney) (string, string) { + t.Helper() + const operation = "project-interrupted-journal" + owned := filepath.Join(journey.namespace(journey.a), "managed") + active := filepath.Join(owned, "pending-"+operation) + staging := filepath.Join(owned, ".agentplugins-staging-"+operation) + if err := os.MkdirAll(staging, 0700); err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(staging, "unpublished.txt"), "caller-owned unpublished bytes\n") + operations := filepath.Join(journey.namespace(journey.a), "operations-v2") + manager := dirswap.Manager{Namespace: journey.namespace(journey.a), JournalDir: operations, Fault: func(phase string) error { + if phase == dirswap.PhaseBackupPending { + return errors.New("disposable interrupted project transaction") + } + return nil + }} + receipt, err := manager.Apply(context.Background(), dirswap.Input{EnvironmentID: environment.EnvironmentProjectDefault, OperationID: operation, ClientBindingID: "project-journal-fixture", Sequence: 1, + OwnedBase: owned, ActivePath: active, StagingPath: staging, RequireAbsent: true}) + if err == nil || receipt.OperationID != operation { + t.Fatalf("fixture did not interrupt transaction: %+v, %v", receipt, err) + } + receipt.Phase = dirswap.PhaseIntent + body, err := json.Marshal(receipt) + if err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(operations, operation+".json"), string(body)) + return staging, active +} diff --git a/cli/cmd/agentplugins/project_lifecycle_transfer_test.go b/cli/cmd/agentplugins/project_lifecycle_transfer_test.go new file mode 100644 index 000000000..36ed26816 --- /dev/null +++ b/cli/cmd/agentplugins/project_lifecycle_transfer_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "path/filepath" + "reflect" + "strings" + "testing" +) + +func TestProjectComposedAgreementColdRestoreUsesPortablePins(t *testing.T) { + for _, kind := range []string{"git", "zip"} { + t.Run(kind, func(t *testing.T) { + transport := newProjectJourneyTransport(t, kind) + original := newProjectJourney(t, transport) + original.run(t, original.a, "add", transport.endpoint, "--alias", "docs", "--target", "gemini") + installation := original.assertOwnedContext(t, original.a) + pair := original.assertPortablePair(t, original.a) + exchange := projectJourneyTemp(t) + original.run(t, original.a, "deps", "export", exchange, "--yes") + if pair != original.assertPortablePair(t, exchange) { + t.Fatal("export changed the exact portable agreement") + } + before := securityFixtureTree(t, original.a) + if _, err := original.execute(original.a, "deps", "export", original.a, "--yes"); err == nil || !strings.Contains(err.Error(), "different exchange directory") { + t.Fatalf("same-pair export = %v", err) + } + if !reflect.DeepEqual(before, securityFixtureTree(t, original.a)) { + t.Fatal("refused same-pair transfer wrote project state") + } + cold := newProjectJourney(t, transport) + projectJourneyAbsent(t, cold.cache, filepath.Join(cold.a, ".uap")) + cold.run(t, cold.a, "deps", "import", exchange, "--install", "--yes") + restored := cold.assertOwnedContext(t, cold.a) + if restored.Source.SourceBindingID != installation.Source.SourceBindingID || restored.Source.TreeDigest != installation.Source.TreeDigest || pair != cold.assertPortablePair(t, cold.a) { + t.Fatal("cold project import changed the pinned source or agreement") + } + cold.inspectNoRecovery(t, cold.a) + if kind == "zip" { + projectJourneyColdChangedArchiveRefused(t, original, exchange, pair) + } + }) + } +} + +func projectJourneyColdChangedArchiveRefused(t *testing.T, original projectJourney, exchange string, pair [2]string) { + t.Helper() + transport := original.transport + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), "changed bytes after export\n") + transport.mu.Lock() + transport.archive = projectJourneyArchive(t, transport.repo) + transport.mu.Unlock() + refused := newProjectJourney(t, transport) + before := securityFixtureTree(t, refused.nativeRoot(refused.a)) + if _, err := refused.execute(refused.a, "deps", "import", exchange, "--install", "--yes"); err == nil || !strings.Contains(strings.ToLower(err.Error()), "sha256") { + t.Fatalf("changed pinned archive was not refused: %v", err) + } + projectJourneyAbsent(t, filepath.Join(refused.a, "uap.toml"), filepath.Join(refused.a, "uap.lock"), filepath.Join(refused.a, ".uap", "state-v2.json"), filepath.Join(refused.cache, "store")) + if !reflect.DeepEqual(before, securityFixtureTree(t, refused.nativeRoot(refused.a))) || pair != original.assertPortablePair(t, exchange) { + t.Fatal("refused cold archive restore changed native state or exported pins") + } +} diff --git a/cli/cmd/agentplugins/project_opencode_e2e_test.go b/cli/cmd/agentplugins/project_opencode_e2e_test.go new file mode 100644 index 000000000..8148a845a --- /dev/null +++ b/cli/cmd/agentplugins/project_opencode_e2e_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Public saved CLI and production composition, filesystem/native transactions, +// source acquisition and immutable store. Only the detected native host version +// comes from an inert TEST process; no OpenCode/MCP/model runtime is certified. +func TestProjectOpenCodeComposedSavedLifecycle(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, "opencode-user") + journey.app = configureProjectOpenCodeJourney(journey.app, projectOpenCodeDetector{root: user, executable: projectOpenCodeProbeFixture(t)}) + foreign := "{ // retained OpenCode settings\n\"mcp\":{\"foreign\":{\"type\":\"remote\",\"url\":\"https://foreign.test/mcp\",\"enabled\":false}}}\n" + for _, root := range []string{user, journey.a, journey.b} { + projectJourneyWrite(t, filepath.Join(root, "opencode.jsonc"), foreign) + projectJourneyWrite(t, filepath.Join(root, ".opencode", "skills", "foreign", "SKILL.md"), "foreign OpenCode skill\n") + } + userBefore := securityFixtureTree(t, user) + for _, project := range []string{journey.a, journey.b} { + journey.run(t, project, "add", transport.endpoint, "--alias", "docs", "--target", "opencode") + projectOpenCodeOwned(t, journey, project, projectJourneySkill) + } + bBefore := securityFixtureTree(t, journey.b) + statePath := filepath.Join(journey.namespace(journey.a), "state-v2.json") + stateBefore := projectJourneyRead(t, statePath) + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + if stateBefore != projectJourneyRead(t, statePath) { + t.Fatal("unchanged OpenCode sync rewrote saved ownership") + } + installation, binding := projectOpenCodeOwned(t, journey, journey.a, projectJourneySkill) + projectJourneyDataMarker(t, journey, journey.a, installation) + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "drifted immutable bytes\n") + if err := os.RemoveAll(filepath.Dir(projectOpenCodeNativeSkill(journey.a))); err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance disposable OpenCode source") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "opencode") + projectOpenCodeOwned(t, journey, journey.a, projectJourneySkill) + projectOpenCodeRecover(t, journey) + projectOpenCodeUpdateAndRemove(t, journey, installation, binding) + if len(journey.state(t, journey.a).EnvironmentReceipts) != 0 || !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) { + t.Fatal("OpenCode lifecycle retained removed ownership or changed user/project B") + } +} + +func projectOpenCodeUpdateAndRemove(t *testing.T, journey projectJourney, installation domain.Installation, binding domain.ClientBinding) { + t.Helper() + journey.run(t, journey.a, "update", "docs", "--target", "opencode") + updated, selected := projectOpenCodeOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || updated.Source.SourceBindingID != installation.Source.SourceBindingID || selected.ClientBindingID != binding.ClientBindingID { + t.Fatal("OpenCode update lost saved source/binding authority or failed to advance") + } + status := journey.run(t, journey.a, "deps", "status") + for _, fact := range []string{`"project"`, `"opencode"`, `"activation_prepared"`} { + if !strings.Contains(status, fact) { + t.Fatalf("saved OpenCode status lost %s: %s", fact, status) + } + } + journey.run(t, journey.a, "remove", "docs", "--target", "opencode") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Dir(projectOpenCodeNativeSkill(journey.a))) + config := projectJourneyRead(t, filepath.Join(journey.a, "opencode.jsonc")) + if strings.Contains(config, `"docs"`) || !strings.Contains(config, "foreign.test") || !strings.Contains(config, "retained OpenCode settings") { + t.Fatalf("OpenCode remove changed foreign native configuration: %s", config) + } + if projectJourneyRead(t, filepath.Join(journey.a, ".opencode", "skills", "foreign", "SKILL.md")) != "foreign OpenCode skill\n" { + t.Fatal("OpenCode remove changed foreign project skill") + } + for key, receipt := range installation.DataReceipts { + if updated.DataReceipts[key].Locator != receipt.Locator || projectJourneyRead(t, filepath.Join(receipt.Locator, "keep.txt")) != "retained environment data\n" { + t.Fatal("OpenCode update/remove lost retained project data authority") + } + } + after := securityFixtureTree(t, journey.a) + _, _ = journey.execute(journey.a, "remove", "docs", "--target", "opencode") + if !reflect.DeepEqual(after, securityFixtureTree(t, journey.a)) { + t.Fatal("repeat OpenCode removal changed an absent managed target") + } +} + +func projectOpenCodeRecover(t *testing.T, journey projectJourney) { + t.Helper() + before := securityFixtureTree(t, filepath.Join(journey.a, ".opencode")) + config := projectJourneyRead(t, filepath.Join(journey.a, "opencode.jsonc")) + staging, active := projectJourneyInterruptDirectoryJournal(t, journey) + journey.run(t, journey.a, "deps", "recover", "--yes") + journey.inspectNoRecovery(t, journey.a) + projectJourneyAbsent(t, active) + if projectJourneyRead(t, filepath.Join(staging, "unpublished.txt")) != "caller-owned unpublished bytes\n" || !reflect.DeepEqual(before, securityFixtureTree(t, filepath.Join(journey.a, ".opencode"))) || config != projectJourneyRead(t, filepath.Join(journey.a, "opencode.jsonc")) { + t.Fatal("OpenCode project recovery changed native configuration or caller staging") + } + journey.run(t, journey.a, "deps", "recover", "--yes") +} diff --git a/cli/cmd/agentplugins/project_opencode_fixture_test.go b/cli/cmd/agentplugins/project_opencode_fixture_test.go new file mode 100644 index 000000000..b803465fc --- /dev/null +++ b/cli/cmd/agentplugins/project_opencode_fixture_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type projectOpenCodeDetector struct{ root, executable string } + +func (d projectOpenCodeDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientOpenCode, ConfigRoot: d.root, + ExecutablePath: d.executable, Version: "1.18.34", Status: domain.DetectionDetected}}, nil +} + +// The compiled inert TEST process implements only --version. It exercises the +// production host-preparation boundary without pretending to be native proof. +func projectOpenCodeProbeFixture(t *testing.T) string { + t.Helper() + name := "TEST-opencode-version-only" + if runtime.GOOS == "windows" { + name += ".exe" + } + path := filepath.Join(projectJourneyTemp(t), name) + command := exec.CommandContext(t.Context(), "go", "build", "-ldflags", "-X main.version=1.18.34 -X main.mode=version-only", "-o", path, + "../../../install/integrationctl/agentplugins/adapters/clientdetect/testdata/opencode_probe.go") + if body, err := command.CombinedOutput(); err != nil { + t.Fatalf("inert version-contract fixture build: %s, %v", body, err) + } + return path +} + +func configureProjectOpenCodeJourney(app agentpluginscli.App, detector projectOpenCodeDetector) agentpluginscli.App { + app.Detector = detector + 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 configureProjectOpenCodeJourney(selected, detector), nil + } + return app +} + +func projectOpenCodeNativeSkill(project string) string { + return filepath.Join(project, ".opencode", "skills", "docs", "SKILL.md") +} + +func projectOpenCodeOwned(t *testing.T, journey projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, binding := journey.owned(t, project) + if binding.ClientID != string(domain.ClientOpenCode) || binding.Scope != string(domain.ScopeProject) || binding.NativeProfileRoot != project || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("project OpenCode binding lost scoped conservative authority: %+v", binding) + } + for _, path := range []string{projectOpenCodeNativeSkill(project), filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md")} { + if body := projectJourneyRead(t, path); body != expected { + t.Fatalf("OpenCode owned skill at %s = %q", path, body) + } + } + return installation, binding +} diff --git a/cli/cmd/agentplugins/project_opencode_progress_proof_test.go b/cli/cmd/agentplugins/project_opencode_progress_proof_test.go new file mode 100644 index 000000000..c4c2eca77 --- /dev/null +++ b/cli/cmd/agentplugins/project_opencode_progress_proof_test.go @@ -0,0 +1,154 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "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" +) + +// This proves the public source's first/repeated imperative add branch with the +// production composer, filesystem, planner, native kernel and persisted state. +// Only --version is an inert compiled TEST process; no vendor/MCP/model runs. +func TestOpenCodeProjectPublicRepeatProgressSource(t *testing.T) { + base := projectJourneyTemp(t) + home, project := filepath.Join(base, "home"), filepath.Join(base, "A") + for _, dir := range []string{home, project} { + if err := os.MkdirAll(dir, 0700); err != nil { + t.Fatal(err) + } + } + pkg := filepath.Join(base, "source-A") + openCodeProgressPackage(t, pkg) + registry, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + app, err := newAgentpluginsCLIApp(home, filepath.Join(home, "uap"), nil, nil, nil, registry) + if err != nil { + t.Fatal(err) + } + user := filepath.Join(home, "opencode-user") + projectJourneyWrite(t, filepath.Join(user, "opencode.jsonc"), "{// foreign user\n\"mcp\":{}}\n") + projectJourneyWrite(t, filepath.Join(project, "opencode.jsonc"), "{// foreign project\n\"mcp\":{},\"default_agent\":\"uap-inert-project\"}\n") + app = configureOpenCodeProgressApp(app, projectOpenCodeDetector{root: user, executable: projectOpenCodeProbeFixture(t)}) + var installedProject, installedHome map[string]string + for _, label := range []string{"first-add", "repeat-add"} { + var output, stderr bytes.Buffer + selected := app + selected.Input, selected.Output, selected.ErrorOutput, selected.Terminal = strings.NewReader(""), &output, &stderr, false + command := agentpluginscli.NewRoot(selected) + command.SetArgs([]string{"add", pkg, "--no-save", "--target", "opencode", "--scope", "project", "--project-root", project, "--format", "json"}) + err := command.ExecuteContext(context.Background()) + if err != nil { + t.Fatalf("%s: %v", label, err) + } + expected := "Resolving and validating one Agent Plugin package for every selected target...\nInstalling uap-commands-fixture for 1 selected clients...\n" + if label == "repeat-add" { + expected = "Resolving and validating one Agent Plugin package for every selected target...\nApplying transactional client package...\n" + } + if stderr.String() != expected { + t.Fatalf("%s progress: got %q, want %q", label, stderr.String(), expected) + } + var envelope struct { + Result string `json:"result"` + Data struct { + Result struct { + Mutated *bool `json:"mutated"` + } `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(output.Bytes(), &envelope); err != nil || envelope.Result != "success" { + t.Fatalf("%s public result: %v %s", label, err, output.Bytes()) + } + if label == "repeat-add" { + if envelope.Data.Result.Mutated == nil || *envelope.Data.Result.Mutated { + t.Fatal("repeat add reported actual mutation or omitted the required flag") + } + for label, pair := range map[string][2]map[string]string{"project": {installedProject, openCodeProgressProjectSnapshot(t, project)}, "home": {installedHome, securityFixtureTree(t, home)}} { + if !reflect.DeepEqual(pair[0], pair[1]) { + for path, before := range pair[0] { + if pair[1][path] != before { + t.Logf("%s changed %s: %s -> %s", label, path, before, pair[1][path]) + } + } + for path, after := range pair[1] { + if _, exists := pair[0][path]; !exists { + t.Logf("%s added %s: %s", label, path, after) + } + } + t.Fatal("repeat add changed project/state/skill/native configuration or original user bytes") + } + } + } + t.Logf("%s exact_stderr=%q result=success", label, stderr.String()) + scoped, err := app.ForScope(domain.ScopeProject, project) + if err != nil { + t.Fatal(err) + } + state, err := scoped.StateStore.Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("real saved state: %+v %v", state, err) + } + for _, binding := range state.Installations[0].Clients { + if binding.Scope != "project" || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("real Prepared binding differs: %+v", binding) + } + } + installedProject, installedHome = openCodeProgressProjectSnapshot(t, project), securityFixtureTree(t, home) + } +} + +func openCodeProgressProjectSnapshot(t *testing.T, project string) map[string]string { + t.Helper() + entries := securityFixtureTree(t, project) + // Every mutation admission refreshes this process-lock payload, even a + // read-only replay. Preserve its presence/type/mode; compare every other + // project file, including installed state and native configuration, exactly. + key := filepath.Join(".uap", "mutation.lock") + mode, _, ok := strings.Cut(entries[key], "/") + if !ok { + t.Fatal("expected exact project mutation lock is missing") + } + entries[key] = mode + "/ephemeral-process-lock" + return entries +} + +func configureOpenCodeProgressApp(app agentpluginscli.App, detector projectOpenCodeDetector) agentpluginscli.App { + app.Detector = detector + app.SecurityIndex, app.DirectoryClient, app.DiscoveryClient = nil, nil, nil + app.SecurityEvaluator = projectJourneySecurity{} + app.SecurityEvaluatorForDryRun = func(string) ports.PackageSecurityEvaluator { return projectJourneySecurity{} } + 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 configureOpenCodeProgressApp(selected, detector), nil + } + return app +} + +func openCodeProgressPackage(t *testing.T, target string) { + t.Helper() + root := filepath.Join("..", "..", "..", "repotests", "testdata", "agentplugins_commands_fixture") + for _, name := range []string{"plugin.json", "skills/fixture-guide/SKILL.md", "io.github.777genius.agentplugins/commands/echo.md", "io.github.777genius.agentplugins/commands/wizard.md"} { + body, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(name))) + if err != nil { + t.Fatal(err) + } + projectJourneyWrite(t, filepath.Join(target, filepath.FromSlash(name)), string(body)) + } + projectJourneyWrite(t, filepath.Join(target, "mcp.json"), "{\"$schema\":\"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json\",\"mcpServers\":{\"scope-marker\":{\"type\":\"streamable-http\",\"url\":\"http://127.0.0.1:9/A-one\"}}}") +} diff --git a/cli/cmd/agentplugins/project_qoder_e2e_test.go b/cli/cmd/agentplugins/project_qoder_e2e_test.go new file mode 100644 index 000000000..6e8aad245 --- /dev/null +++ b/cli/cmd/agentplugins/project_qoder_e2e_test.go @@ -0,0 +1,141 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type projectQoderDetector struct{ root string } + +func (d projectQoderDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientQoder, ConfigRoot: d.root, Status: domain.DetectionDetected}}, nil +} + +type projectQoderNoNative struct{ calls int } + +func (r *projectQoderNoNative) Run(_ context.Context, c legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("unexpected Qoder native command %v", c.Argv) +} + +func configureProjectQoder(t *testing.T, app agentpluginscli.App, user string, runner *projectQoderNoNative) agentpluginscli.App { + t.Helper() + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{StateRoot: filepath.Dir(app.ManagedRoot), ManagedRoot: app.ManagedRoot, ProjectRoot: app.ProjectRoot, Registry: app.ClientRegistry, Runner: runner, EnableNativeObserver: true, HelperExecutable: helper}) + if err != nil { + t.Fatal(err) + } + app.Installer = engine + app.Detector = projectQoderDetector{user} + app.Lifecycle.Activator = providers.Activator{Registry: app.ClientRegistry, Runner: runner} + app.Lifecycle.NativeObserver = providers.NativeIdentityObserver{Registry: app.ClientRegistry, Runner: runner} + 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 configureProjectQoder(t, selected, user, runner), nil + } + return app +} + +// Actual saved CLI/TLS Git, immutable pin, typed state and directory +// transactions. Every native-client command is forbidden by the injected runner. +func TestProjectQoderSavedLifecycleSeparatesUserAndProjects(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + user := filepath.Join(journey.home, ".qoder") + projectJourneyWrite(t, filepath.Join(user, "plugins", "installed_plugins_v2.json"), "original unrelated global registry") + // Keep an actual original user installation/state as an independent sentinel. + // Qoder user registry mutation is intentionally outside this project slice. + journey.run(t, "", "add", transport.endpoint, "--alias", "user-docs", "--target", "gemini") + userGemini := securityFixtureTree(t, filepath.Join(journey.home, ".gemini")) + runner := &projectQoderNoNative{} + journey.app = configureProjectQoder(t, journey.app, user, runner) + userBefore := securityFixtureTree(t, user) + userStateBefore := projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) + for _, project := range []string{journey.a, journey.b} { + journey.run(t, project, "add", transport.endpoint, "--alias", "docs", "--target", "qoder") + projectQoderOwned(t, journey, project, projectJourneySkill) + } + bBefore := securityFixtureTree(t, journey.b) + installation, binding := journey.owned(t, journey.a) + for _, args := range [][]string{ + {"add", transport.endpoint, "--alias", "docs", "--target", "qoder"}, + {"update", "docs", "--target", "qoder"}, + {"repair", installation.InstallationID, "--target", "qoder"}, + } { + state := projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) + if _, err := journey.execute(journey.a, append(append([]string(nil), args...), "--activation-complete")...); err == nil { + t.Fatal("saved Qoder request accepted attestation") + } + if state != projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) { + t.Fatal("refused attestation changed state") + } + output := journey.run(t, journey.a, args...) + if strings.Contains(output, `"mutated":true`) || state != projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) { + t.Fatal("intact saved Prepared operation changed state", output) + } + } + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + view := filepath.Join(binding.TargetLocator, ".agentplugins-qoder-native") + projectJourneyWrite(t, filepath.Join(view, "skills", "docs", "SKILL.md"), "managed view drift") + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance disposable Qoder source") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "qoder") + projectQoderOwned(t, journey, journey.a, projectJourneySkill) + if err := os.RemoveAll(binding.TargetLocator); err != nil { + t.Fatal(err) + } + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "qoder") + projectQoderOwned(t, journey, journey.a, projectJourneySkill) + journey.run(t, journey.a, "update", "docs", "--target", "qoder") + updated, current := projectQoderOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator { + t.Fatal("saved update lost immutable source or binding authority") + } + calls := runner.calls + status := journey.run(t, journey.a, "deps", "status") + if runner.calls != calls || !strings.Contains(status, "activation_prepared") || strings.Contains(status, "activation_active") { + t.Fatal("status called native or promoted activation", status) + } + journey.run(t, journey.a, "remove", "docs", "--target", "qoder") + projectJourneyAbsent(t, current.TargetLocator) + if runner.calls != 0 || !reflect.DeepEqual(userGemini, securityFixtureTree(t, filepath.Join(journey.home, ".gemini"))) || !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) || userStateBefore != projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) { + t.Fatal("A lifecycle changed original user state/native registry or B") + } +} + +func projectQoderOwned(t *testing.T, j projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, b := j.owned(t, project) + if b.ClientID != "qoder" || b.Scope != "project" || b.NativeProfileRoot != filepath.Join(project, ".uap", "managed", "clients", "qoder") || b.Activation != domain.ActivationPrepared || b.Verification != domain.VerificationInstalled || len(b.NativeObjects) != 1 { + t.Fatalf("Qoder lost exact Prepared authority %+v", b) + } + if projectJourneyRead(t, filepath.Join(b.TargetLocator, ".agentplugins-qoder-native", "skills", "docs", "SKILL.md")) != expected { + t.Fatal("native import view differs from selected immutable skill") + } + for _, surface := range []string{"mcp.json", ".mcp.json", "hooks", "agents"} { + if _, err := os.Lstat(filepath.Join(b.TargetLocator, ".agentplugins-qoder-native", surface)); !os.IsNotExist(err) { + t.Fatal("unqualified native resource projected", surface, err) + } + } + return installation, b +} diff --git a/cli/cmd/agentplugins/project_retained_data_guidance_test.go b/cli/cmd/agentplugins/project_retained_data_guidance_test.go new file mode 100644 index 000000000..ea2fc8e16 --- /dev/null +++ b/cli/cmd/agentplugins/project_retained_data_guidance_test.go @@ -0,0 +1,162 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// This replays only captured argv through the real public CLI composition. +// The shell used to check presentation runs printf, never agentplugins/purge. +func TestProjectRetainedDataGuidancePreservesSelectedRoot(t *testing.T) { + if _, err := exec.LookPath("sh"); err != nil { + t.Skip("POSIX argument-presentation check requires sh; no Windows shell qualification claimed") + } + transport := newProjectJourneyTransport(t, "zip") + for _, route := range []string{"imperative-json", "imperative-human", "saved-human"} { + t.Run(route, func(t *testing.T) { + journey := newProjectJourney(t, transport) + root := filepath.Join(t.TempDir(), "project A 'literal' $cash") + if err := os.Rename(journey.a, root); err != nil { + t.Fatal(err) + } + var err error + journey.a, err = filepath.EvalSymlinks(root) + if err != nil { + t.Fatal(err) + } + installation := prepareRetainedGuidanceJourney(t, journey, route == "saved-human") + selector, format := installation.InstallationID, "human" + switch route { + case "saved-human": + selector = "docs" + case "imperative-json": + format = "json" + } + output := executeRetainedGuidanceCLI(t, journey, journey.a, format, "remove", selector, "--target", "gemini") + command := retainedGuidanceCommand(t, output, route) + args := captureRetainedGuidanceArguments(t, command) + want := []string{"agentplugins", "remove", installation.InstallationID, "--purge-data", "--scope", "project", "--project-root", journey.a} + if !reflect.DeepEqual(args, want) { + t.Fatalf("follow-up lost selected argv: got %q want %q", args, want) + } + replayRetainedGuidance(t, journey, installation, args[1:]) + }) + } +} + +func prepareRetainedGuidanceJourney(t *testing.T, journey projectJourney, saved bool) domain.Installation { + t.Helper() + var selected domain.Installation + for _, root := range []string{"", journey.a, journey.b} { + args := []string{"add", journey.transport.endpoint, "--alias", "docs", "--target", "gemini"} + if !saved { + args = []string{"add", journey.transport.repo, "--no-save", "--target", "gemini"} + } + journey.run(t, root, args...) + state := journey.state(t, root) + if len(state.Installations) != 1 || len(state.Installations[0].Clients) != 1 { + t.Fatalf("expected one installed fixture in %q: %+v", root, state) + } + projectJourneyDataMarker(t, journey, root, state.Installations[0]) + if root == journey.a { + selected = state.Installations[0] + } + } + return selected +} + +func executeRetainedGuidanceCLI(t *testing.T, journey projectJourney, project, format string, args ...string) string { + t.Helper() + var output, stderr bytes.Buffer + app := journey.app + app.Input, app.Output, app.ErrorOutput, app.Terminal = strings.NewReader(""), &output, &stderr, false + root := agentpluginscli.NewRoot(app) + root.SetOut(&output) + root.SetErr(&stderr) + args = append(args, "--format", format) + if project != "" { + args = append(args, "--scope", "project", "--project-root", project) + } + root.SetArgs(args) + if err := root.Execute(); err != nil { + t.Fatalf("public CLI %q: %v\n%s\n%s", args, err, output.String(), stderr.String()) + } + return output.String() +} + +func retainedGuidanceCommand(t *testing.T, output, route string) string { + t.Helper() + if route == "imperative-json" { + var response struct { + Data struct { + DataRetained bool `json:"data_retained"` + Action string `json:"retained_data_action"` + } `json:"data"` + } + if err := json.Unmarshal([]byte(output), &response); err != nil || !response.Data.DataRetained { + t.Fatalf("expected retained project JSON: %v\n%s", err, output) + } + output = response.Data.Action + } + prefix, suffix := "run `", "` to delete retained plugin data" + if route == "saved-human" { + prefix, suffix = "PLUGIN_DATA preserved; after removal completes, run ", " to delete retained data" + } + for _, line := range strings.Split(output, "\n") { + if start := strings.Index(line, prefix); start >= 0 && strings.HasSuffix(line, suffix) { + return strings.TrimSuffix(line[start+len(prefix):], suffix) + } + } + t.Fatalf("missing retained-data follow-up in %s: %s", route, output) + return "" +} + +func captureRetainedGuidanceArguments(t *testing.T, command string) []string { + t.Helper() + if !strings.HasPrefix(command, "agentplugins remove ") || strings.ContainsAny(command, "\n\r") { + t.Fatalf("unexpected command presentation: %q", command) + } + // All values are test-owned. set captures shell word boundaries; the + // generated agentplugins command is never executed by this shell. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + process := exec.CommandContext(ctx, "sh", "-c", "set -- "+command+"; printf '%s\\0' \"$@\"") + process.Env = []string{"PATH=/usr/bin:/bin"} + process.Stdin = strings.NewReader("") + body, err := process.Output() + if err != nil || len(body) == 0 || body[len(body)-1] != 0 { + t.Fatalf("inert shell argument capture: %v %q", err, body) + } + return strings.Split(string(body[:len(body)-1]), "\x00") +} + +func replayRetainedGuidance(t *testing.T, journey projectJourney, installation domain.Installation, args []string) { + t.Helper() + retained := journey.state(t, journey.a) + if len(retained.Installations) != 1 || !retained.Installations[0].DataRetained || len(retained.Installations[0].Clients) != 0 { + t.Fatalf("normal removal did not retain data: %+v", retained) + } + userBefore, bBefore := securityFixtureTree(t, journey.home), securityFixtureTree(t, journey.b) + t.Chdir(t.TempDir()) + output := executeRetainedGuidanceCLI(t, journey, "", "json", args...) + if !strings.Contains(output, `"status":"purged"`) || len(journey.state(t, journey.a).Installations) != 0 { + t.Fatalf("emitted follow-up did not purge selected project: %s", output) + } + for _, receipt := range installation.DataReceipts { + projectJourneyAbsent(t, receipt.Locator) + } + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, journey.home)) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) { + t.Fatal("project follow-up changed user/B native state, retained data or shared cache") + } +} diff --git a/cli/cmd/agentplugins/project_skills_e2e_test.go b/cli/cmd/agentplugins/project_skills_e2e_test.go new file mode 100644 index 000000000..88a832ef2 --- /dev/null +++ b/cli/cmd/agentplugins/project_skills_e2e_test.go @@ -0,0 +1,270 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + "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/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" +) + +type projectSkillsSource struct { + input string + transport *projectJourneyTransport + advance func() +} + +func newProjectSkillsSource(t *testing.T, kind string) projectSkillsSource { + t.Helper() + if kind == "git" { + transport := newProjectJourneyTransport(t, "git") + return projectSkillsSource{input: "uap-fixture/skills@main", transport: transport, advance: func() { + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance raw project skill") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + }} + } + var mu sync.Mutex + body := projectJourneySkill + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + defer mu.Unlock() + switch r.URL.Path { + case "/.well-known/agent-skills/index.json": + _, _ = fmt.Fprint(w, `{"skills":[{"name":"docs","files":["SKILL.md"]}]}`) + case "/.well-known/agent-skills/docs/SKILL.md": + _, _ = fmt.Fprint(w, body) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + return projectSkillsSource{input: server.URL, transport: &projectJourneyTransport{server: server}, advance: func() { + mu.Lock() + defer mu.Unlock() + body = projectJourneyAdvancedSkill + }} +} + +// The raw GitHub adapter intentionally strips user Git config. Its explicit +// URL fixture seam maps one public coordinate to a disposable bare repository; +// real native Git still resolves and fetches every immutable source object. +func projectSkillsGitAcquirer(app agentpluginscli.App, remote string) agentpluginscli.App { + acquirer := app.SourceAcquirer.(sourceacquisition.Acquirer) + acquirer.URLForRepo = func(string) string { return remote } + app.SourceAcquirer = acquirer + app.SourceAcquirerForDryRun = func(root string) agentpluginscli.SourceAcquirer { + preview := acquirer + preview.TempRoot = root + return preview + } + 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 projectSkillsGitAcquirer(selected, remote), nil + } + return app +} + +func projectSkillPin(t *testing.T, journey projectJourney, project string) environment.LockedPackage { + t.Helper() + root := project + if root == "" { + root = journey.cache + } + var lock environment.Lock + if err := json.Unmarshal([]byte(projectJourneyRead(t, filepath.Join(root, "uap.lock"))), &lock); err != nil { + t.Fatal(err) + } + if len(lock.Packages) != 1 { + t.Fatalf("expected exact raw Skill pin: %+v", lock) + } + return lock.Packages[0] +} + +func assertProjectSkill(t *testing.T, journey projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, binding := journey.owned(t, project) + scope, environmentID := domain.ScopeUser, environment.EnvironmentUserDefault + if project != "" { + scope, environmentID = domain.ScopeProject, environment.EnvironmentProjectDefault + } + if binding.Scope != string(scope) || binding.NativeProfileRoot != journey.nativeRoot(project) || binding.Verification != domain.VerificationInstalled { + t.Fatalf("raw Skill lost selected scope: %+v", binding) + } + if project != "" && binding.Activation != domain.ActivationPrepared { + t.Fatalf("fixture claimed native project activation: %+v", binding) + } + if journey.state(t, project).EnvironmentReceipts[0].EnvironmentID != environmentID { + t.Fatal("raw Skill receipt uses wrong environment") + } + if err := pathpolicy.RequireContainedChild(journey.namespace(project), binding.TargetLocator); err != nil { + t.Fatal(err) + } + for _, root := range []string{binding.TargetLocator, journey.nativeRoot(project)} { + if got := projectJourneyRead(t, filepath.Join(root, "skills", "docs", "SKILL.md")); got != expected { + t.Fatalf("wrong raw Skill bytes in %s: %q", root, got) + } + } + if got := projectJourneyRead(t, filepath.Join(journey.nativeRoot(project), "skills", "foreign", "SKILL.md")); got != "foreign skill\n" { + t.Fatal("raw Skill changed foreign skill") + } + return installation, binding +} + +// Exercise production CLI composition, source acquisition, saved environment and +// filesystem transaction. Fixture detection is not native client discovery. +func TestProjectSkillsComposedGitAndHTTPSLifecycle(t *testing.T) { + for _, kind := range []string{"git", "https"} { + t.Run(kind, func(t *testing.T) { + source := newProjectSkillsSource(t, kind) + journey := newProjectJourney(t, source.transport) + if kind == "git" { + journey.app = projectSkillsGitAcquirer(journey.app, source.transport.remote) + } + initial := securityFixtureTree(t, journey.a) + journey.run(t, journey.a, "skills", "add", source.input, "--skill", "docs", "--target", "gemini", "--no-ui", "--dry-run") + if !reflect.DeepEqual(initial, securityFixtureTree(t, journey.a)) { + t.Fatal("raw Skill preview wrote project files") + } + projectJourneyAbsent(t, filepath.Join(journey.cache, "state-v2.json"), filepath.Join(journey.cache, "uap.toml"), filepath.Join(journey.cache, "store"), filepath.Join(journey.b, ".uap")) + for _, root := range []string{"", journey.a, journey.b} { + journey.run(t, root, "skills", "add", source.input, "--skill", "docs", "--target", "gemini", "--no-ui", "--yes") + assertProjectSkill(t, journey, root, projectJourneySkill) + } + pin := projectSkillPin(t, journey, journey.a) + if (kind == "git" && (pin.Source.Git == nil || pin.Source.Git.Kind != environment.SkillKind)) || (kind == "https" && pin.Source.Web == nil) { + t.Fatalf("lost raw Skill provenance: %+v", pin) + } + for _, root := range []string{journey.cache, journey.b} { + for _, name := range []string{"uap.toml", "uap.lock"} { + if projectJourneyRead(t, filepath.Join(root, name)) != projectJourneyRead(t, filepath.Join(journey.a, name)) { + t.Fatal("scope leaked into portable raw Skill agreement") + } + } + } + userNative, bNative := securityFixtureTree(t, journey.nativeRoot("")), securityFixtureTree(t, journey.nativeRoot(journey.b)) + userState := projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) + bState := projectJourneyRead(t, filepath.Join(journey.namespace(journey.b), "state-v2.json")) + statePath := filepath.Join(journey.namespace(journey.a), "state-v2.json") + before := projectJourneyRead(t, statePath) + journey.run(t, journey.a, "deps", "sync", "--locked", "--yes") + if before != projectJourneyRead(t, statePath) { + t.Fatal("stable raw Skill replay rewrote project receipt") + } + installation, binding := journey.owned(t, journey.a) + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "drift.txt"), "managed projection drift\n") + source.advance() + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "gemini") + assertProjectSkill(t, journey, journey.a, projectJourneySkill) + projectJourneyAbsent(t, filepath.Join(binding.TargetLocator, "drift.txt")) + journey.run(t, journey.a, "update", "docs", "--target", "gemini") + updated, selected := assertProjectSkill(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.InstallationID != installation.InstallationID || selected.ClientBindingID != binding.ClientBindingID || selected.TargetLocator != binding.TargetLocator || updated.Source.TreeDigest == installation.Source.TreeDigest { + t.Fatal("raw Skill update replaced ownership or failed to advance bytes") + } + journey.run(t, journey.a, "remove", "docs", "--target", "gemini") + projectJourneyAbsent(t, binding.TargetLocator, filepath.Join(journey.nativeRoot(journey.a), "skills", "docs")) + if len(journey.state(t, journey.a).EnvironmentReceipts) != 0 { + t.Fatal("removed raw Skill retained ownership receipt") + } + journey.assertOtherScopeUnchanged(t, "", userNative, userState) + journey.assertOtherScopeUnchanged(t, journey.b, bNative, bState) + }) + } +} + +func TestProjectSkillsRejectUnsupportedBeforeAcquisition(t *testing.T) { + app, user, a, _ := projectCompositionFixture(t) + journey := projectJourney{app: app} + if _, err := journey.execute(a, "skills", "add", "https://invalid.example/never-acquire", "--skill", "docs", "--target", "chatgpt", "--yes"); err == nil || !strings.Contains(err.Error(), "does not support project scope") { + t.Fatalf("unsupported raw Skills boundary: %v", err) + } + selected, err := app.ForScope(domain.ScopeProject, a) + if err != nil { + t.Fatal(err) + } + _, err = selected.PrepareSkills(context.Background(), agentpluginscli.SkillsBatchRequest{Selections: []agentpluginscli.SkillsSelection{{Website: "https://invalid.example/never-acquire", Name: "docs"}}, TargetIDs: []string{"chatgpt"}}) + if err == nil || !strings.Contains(err.Error(), "does not support project scope") { + t.Fatalf("direct batch bypassed project qualification: %v", err) + } + projectJourneyAbsent(t, user, filepath.Join(a, ".uap"), filepath.Join(a, "uap.toml")) +} + +func TestProjectSkillsPreparedPlanRejectsReplacedRootBeforeLock(t *testing.T) { + source := newProjectSkillsSource(t, "https") + journey := newProjectJourney(t, source.transport) + app, err := journey.app.ForScope(domain.ScopeProject, journey.a) + if err != nil { + t.Fatal(err) + } + request := agentpluginscli.SkillsBatchRequest{Selections: []agentpluginscli.SkillsSelection{{Website: source.input, Name: "docs"}}, TargetIDs: []string{"gemini"}} + prepared, err := app.PrepareSkills(context.Background(), request) + if err != nil { + t.Fatal(err) + } + defer prepared.Close() + original := journey.a + "-original" + if err = os.Rename(journey.a, original); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.RemoveAll(original) }) + if err = os.Mkdir(journey.a, 0700); err != nil { + t.Fatal(err) + } + if _, err = prepared.Apply(context.Background()); !errors.Is(err, agentpluginscli.ErrSkillsPlanStale) { + t.Fatalf("replaced project accepted by prepared batch: %v", err) + } + if _, err = app.PrepareSkills(context.Background(), request); err == nil { + t.Fatal("direct prepare accepted replaced project namespace") + } + if _, err = app.StartSkillsPicker(context.Background(), agentpluginscli.SkillsPickerRequest{Input: "invalid/never-acquire"}); err == nil { + t.Fatal("public picker accepted replaced project namespace") + } + entries, err := os.ReadDir(journey.a) + if err != nil || len(entries) != 0 { + t.Fatalf("replaced project acquired lock or state: %v %v", entries, err) + } + projectJourneyAbsent(t, filepath.Join(original, ".uap"), filepath.Join(journey.cache, "store")) +} + +func TestProjectSkillsSavedUnsupportedTargetRefusesBeforeNewSource(t *testing.T) { + app, user, a, _ := projectCompositionFixture(t) + spy := &projectChatGPTBoundaries{} + app = projectChatGPTApp(app, spy) + projectJourneyWrite(t, filepath.Join(a, "uap.toml"), "schema_version = 1\n[dependencies.docs]\ngit = \"https://github.com/acme/docs.git\"\nref = \"main\"\n[targets]\nchatgpt = [\"docs\"]\n") + before := securityFixtureTree(t, a) + journey := projectJourney{app: app} + if _, err := journey.execute(a, "skills", "add", "acme/new-skills", "--skill", "new", "--target", "gemini", "--yes"); err == nil || err.Error() != "chatgpt does not support project scope" { + t.Fatalf("saved target not rejected before catalog acquisition: %v", err) + } + selected, err := app.ForScope(domain.ScopeProject, a) + if err != nil { + t.Fatal(err) + } + _, err = selected.PrepareSkills(context.Background(), agentpluginscli.SkillsBatchRequest{Selections: []agentpluginscli.SkillsSelection{{Website: "https://invalid.example/never-acquire", Name: "new"}}, TargetIDs: []string{"gemini"}}) + if err == nil || err.Error() != "chatgpt does not support project scope" { + t.Fatalf("saved target not rejected before direct acquisition: %v", err) + } + if !reflect.DeepEqual(before, securityFixtureTree(t, a)) { + t.Fatal("unsupported saved target changed project intent") + } + projectChatGPTAssertNoEffects(t, spy, user, a) +} diff --git a/cli/cmd/agentplugins/project_skills_picker_test.go b/cli/cmd/agentplugins/project_skills_picker_test.go new file mode 100644 index 000000000..3a6555846 --- /dev/null +++ b/cli/cmd/agentplugins/project_skills_picker_test.go @@ -0,0 +1,116 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli/skillspicker" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type projectSkillsPickerDetector struct{ clients []domain.DetectedClient } + +func (d projectSkillsPickerDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return append([]domain.DetectedClient(nil), d.clients...), nil +} + +// Start the actual public picker bridge from production project composition. +// Reading target choices must show selected project roots and remain read-only. +func TestProjectSkillsPickerReportsScopedDestinations(t *testing.T) { + source := newProjectSkillsSource(t, "git") + journey := newProjectJourney(t, source.transport) + journey.app = projectSkillsGitAcquirer(journey.app, source.transport.remote) + for _, unsafe := range []bool{false, true} { + t.Run(map[bool]string{false: "available", true: "unsafe-user-profile"}[unsafe], func(t *testing.T) { + app, err := journey.app.ForScope(domain.ScopeProject, journey.a) + if err != nil { + t.Fatal(err) + } + detected := make([]domain.DetectedClient, 0, 4) + for _, id := range []domain.ClientID{domain.ClientClaude, domain.ClientCodex, domain.ClientGemini, domain.ClientChatGPT} { + root := filepath.Join(journey.home, "."+string(id)) + if unsafe && id == domain.ClientCodex { + root = "relative-profile" + } + detected = append(detected, domain.DetectedClient{ClientID: id, ConfigRoot: root, Status: domain.DetectionDetected}) + } + app.Detector = projectSkillsPickerDetector{clients: detected} + before := securityFixtureTree(t, journey.a) + server, err := app.StartSkillsPicker(context.Background(), agentpluginscli.SkillsPickerRequest{Input: source.input}) + if err != nil { + t.Fatal(err) + } + defer server.Close() + endpoint, err := url.Parse(server.URL()) + if err != nil { + t.Fatal(err) + } + fragment, err := url.ParseQuery(endpoint.Fragment) + if err != nil { + t.Fatal(err) + } + endpoint.Fragment = "" + origin := strings.TrimSuffix(endpoint.String(), "/") + request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, origin+"/api/v1/targets", http.NoBody) + if err != nil { + t.Fatal(err) + } + request.Header.Set("Authorization", "Bearer "+fragment.Get("capability")) + request.Header.Set("Origin", origin) + response, err := (&http.Client{Timeout: 5 * time.Second}).Do(request) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := response.Body.Close(); err != nil { + t.Error(err) + } + }() + if response.StatusCode != http.StatusOK { + t.Fatalf("target API status: %d", response.StatusCode) + } + var body skillspicker.Targets + if err = json.NewDecoder(response.Body).Decode(&body); err != nil { + t.Fatal(err) + } + targets := map[string]skillspicker.Target{} + for _, target := range body.Targets { + if target.Scope != "project" { + t.Fatalf("picker exposed wrong scope: %+v", target) + } + targets[target.Id] = target + } + for _, id := range []string{"claude", "codex", "gemini"} { + target, ok := targets[id] + if !ok { + t.Fatalf("project target missing: %s", id) + } + if unsafe && id == "codex" { + if target.Available || target.DisplayPath != "" || target.Reason == nil { + t.Fatalf("unsafe profile gained fallback destination: %+v", target) + } + continue + } + if !target.Available || !strings.HasPrefix(target.DisplayPath, journey.a+string(filepath.Separator)) || strings.Contains(target.DisplayPath, journey.home) { + t.Fatalf("picker substituted user path: %+v", target) + } + } + refused, ok := targets["chatgpt"] + if !ok || refused.Available || refused.Reason == nil || refused.DisplayPath != "" { + t.Fatalf("unsupported project target not disabled: %+v", refused) + } + if !reflect.DeepEqual(before, securityFixtureTree(t, journey.a)) { + t.Fatal("reading picker targets changed project files") + } + projectJourneyAbsent(t, filepath.Join(journey.cache, "state-v2.json"), filepath.Join(journey.a, ".uap"), filepath.Join(journey.b, ".uap")) + }) + } +} diff --git a/cli/cmd/agentplugins/project_vscode_e2e_test.go b/cli/cmd/agentplugins/project_vscode_e2e_test.go new file mode 100644 index 000000000..fb9a03724 --- /dev/null +++ b/cli/cmd/agentplugins/project_vscode_e2e_test.go @@ -0,0 +1,141 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/cli/internal/agentpluginscli" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type projectWorkspaceDetector struct{ user string } + +func (d projectWorkspaceDetector) Detect(context.Context) ([]domain.DetectedClient, error) { + return []domain.DetectedClient{{ClientID: domain.ClientVSCode, ConfigRoot: d.user, Version: "1.143.1", Status: domain.DetectionDetected}}, nil +} + +type projectWorkspaceNoNative struct{ calls int } + +func (r *projectWorkspaceNoNative) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("unexpected VS Code/Copilot/MCP command: %v", command.Argv) +} + +func configureWorkspaceJourney(t *testing.T, app agentpluginscli.App, user string, runner *projectWorkspaceNoNative) agentpluginscli.App { + t.Helper() + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{ProjectRoot: app.ProjectRoot, StateRoot: filepath.Dir(app.ManagedRoot), ManagedRoot: app.ManagedRoot, + Registry: app.ClientRegistry, Runner: runner, HelperExecutable: helper, EnableNativeObserver: true}) + if err != nil { + t.Fatal(err) + } + app.Installer, app.Detector = engine, projectWorkspaceDetector{user: user} + kernel := nativeconfig.New() + app.Lifecycle.Activator = providers.Activator{Registry: app.ClientRegistry, Runner: runner, NativeConfig: &kernel} + app.Lifecycle.NativeObserver = providers.NativeIdentityObserver{Registry: app.ClientRegistry, Runner: runner, NativeConfig: &kernel} + 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 configureWorkspaceJourney(t, selected, user, runner), nil + } + return app +} + +// Public Cobra, TLS Git acquisition/pins, SDK and native file transactions run. +// The vendor boundary refuses every process; workbench metadata has its separate +// genuine pinned observation and is never fabricated by this fixture. +func TestProjectVSCodeSavedLifecyclePreservesUserBAndCopilotFiles(t *testing.T) { + transport := newProjectJourneyTransport(t, "git") + journey := newProjectJourney(t, transport) + journey.run(t, "", "add", transport.endpoint, "--alias", "original-user", "--target", "gemini") + userState := projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) + geminiUser := securityFixtureTree(t, filepath.Join(journey.home, ".gemini")) + user := filepath.Join(journey.home, "Code") + projectJourneyWrite(t, filepath.Join(user, "User", "mcp.json"), `{"servers":{"original-user":{"type":"http","url":"https://foreign.test/user"}}}`) + userBefore := securityFixtureTree(t, user) + runner := &projectWorkspaceNoNative{} + journey.app = configureWorkspaceJourney(t, journey.app, user, runner) + for _, project := range []string{journey.a, journey.b} { + projectJourneyWrite(t, filepath.Join(project, ".vscode", "mcp.json"), "{\n// retained workspace comment\n\"inputs\":[],\"servers\":{\"foreign\":{\"type\":\"stdio\",\"command\":\"inert\",\"env\":{\"PORT\":8123},},},\n}\n") + projectJourneyWrite(t, filepath.Join(project, ".github", "mcp.json"), "Copilot control bytes") + projectJourneyWrite(t, filepath.Join(project, ".github", "skills", "foreign", "SKILL.md"), "foreign skill") + journey.run(t, project, "add", transport.endpoint, "--alias", "review", "--target", "vscode") + workspaceJourneyOwned(t, journey, project, projectJourneySkill) + } + bBefore := securityFixtureTree(t, journey.b) + installation, binding := workspaceJourneyOwned(t, journey, journey.a, projectJourneySkill) + beforeState := projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) + config := projectJourneyRead(t, filepath.Join(journey.a, ".vscode", "mcp.json")) + for _, args := range [][]string{ + {"add", transport.endpoint, "--alias", "review", "--target", "vscode"}, + {"deps", "sync", "--locked", "--yes"}, + } { + journey.run(t, journey.a, args...) + if projectJourneyRead(t, filepath.Join(journey.a, ".uap", "state-v2.json")) != beforeState || projectJourneyRead(t, filepath.Join(journey.a, ".vscode", "mcp.json")) != config { + t.Fatal("unchanged saved workspace replay changed persisted bytes") + } + } + if _, err := journey.execute(journey.a, "add", transport.endpoint, "--alias", "review", "--target", "vscode", "--activation-complete"); err == nil { + t.Fatal("workspace accepted operator attestation as discovery") + } + projectJourneyWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "managed package drift") + projectJourneyWrite(t, filepath.Join(transport.repo, "skills", "docs", "SKILL.md"), projectJourneyAdvancedSkill) + projectJourneyGit(t, transport.repo, "add", ".") + projectJourneyGit(t, transport.repo, "commit", "-m", "advance workspace source") + projectJourneyGit(t, transport.repo, "push", "--quiet", transport.remote, "main") + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "vscode") + workspaceJourneyOwned(t, journey, journey.a, projectJourneySkill) + if err := os.RemoveAll(filepath.Join(journey.a, ".github", "skills", "docs")); err != nil { + t.Fatal(err) + } + journey.run(t, journey.a, "repair", installation.InstallationID, "--target", "vscode") + workspaceJourneyOwned(t, journey, journey.a, projectJourneySkill) + journey.run(t, journey.a, "update", "review", "--target", "vscode") + updated, current := workspaceJourneyOwned(t, journey, journey.a, projectJourneyAdvancedSkill) + if updated.Source.TreeDigest == installation.Source.TreeDigest || current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator { + t.Fatal("workspace update changed authority or ignored moving source") + } + status := journey.run(t, journey.a, "deps", "status") + if !strings.Contains(status, "activation_prepared") || strings.Contains(status, "activation_active") { + t.Fatal("workspace status promoted file preparation", status) + } + journey.run(t, journey.a, "remove", "review", "--target", "vscode") + projectJourneyAbsent(t, current.TargetLocator, filepath.Join(journey.a, ".github", "skills", "docs")) + if projectJourneyRead(t, filepath.Join(journey.a, ".github", "mcp.json")) != "Copilot control bytes" || !strings.Contains(projectJourneyRead(t, filepath.Join(journey.a, ".vscode", "mcp.json")), "retained workspace comment") { + t.Fatal("workspace removal changed Copilot or foreign JSONC authority") + } + if !reflect.DeepEqual(userBefore, securityFixtureTree(t, user)) || !reflect.DeepEqual(geminiUser, securityFixtureTree(t, filepath.Join(journey.home, ".gemini"))) || !reflect.DeepEqual(bBefore, securityFixtureTree(t, journey.b)) || userState != projectJourneyRead(t, filepath.Join(journey.cache, "state-v2.json")) || runner.calls != 0 { + t.Fatal("workspace A lifecycle changed original user/B or invoked a vendor") + } +} + +func workspaceJourneyOwned(t *testing.T, journey projectJourney, project, expected string) (domain.Installation, domain.ClientBinding) { + t.Helper() + installation, binding := journey.owned(t, project) + if binding.ClientID != "vscode" || binding.Scope != "project" || binding.NativeProfileRoot != filepath.Join(project, ".vscode") || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("workspace binding lost file-only Prepared authority: %+v", binding) + } + if projectJourneyRead(t, filepath.Join(project, ".github", "skills", "docs", "SKILL.md")) != expected { + t.Fatal("workspace skill differs from pinned package") + } + body := projectJourneyRead(t, filepath.Join(project, ".vscode", "mcp.json")) + if !strings.Contains(body, `"servers"`) || !strings.Contains(body, `"docs"`) || !strings.Contains(body, `"PORT":8123`) { + t.Fatal("workspace codec lost owned registration or foreign numeric environment") + } + return installation, binding +} diff --git a/cli/cmd/agentplugins/release_root_test.go b/cli/cmd/agentplugins/release_root_test.go index 9a45d4185..ccdc6eeac 100644 --- a/cli/cmd/agentplugins/release_root_test.go +++ b/cli/cmd/agentplugins/release_root_test.go @@ -176,6 +176,7 @@ func TestReleaseCompletionProcessStderr(t *testing.T) { t.Fatal(err) } home := t.TempDir() + coverageDir := t.TempDir() marker := "glpat-" + strings.Repeat("b", 20) for _, protocol := range []string{"__complete", "__completeNoDesc"} { cases := []struct { @@ -209,7 +210,7 @@ func TestReleaseCompletionProcessStderr(t *testing.T) { for i, tc := range cases { child := exec.Command(exe, append([]string{"-test.run=^TestReleaseCompletionProcessStderr$", "--"}, tc.args...)...) child.Dir = home - child.Env = []string{"UAP_COMPLETION_PROCESS_FIXTURE=1", "HOME=" + home, "TMPDIR=" + home, "XDG_CONFIG_HOME=" + home, "PATH=" + home, "BASH_COMP_DEBUG_FILE=" + home + "/completion-debug", "GOMAXPROCS=2"} + child.Env = []string{"UAP_COMPLETION_PROCESS_FIXTURE=1", "HOME=" + home, "TMPDIR=" + home, "XDG_CONFIG_HOME=" + home, "PATH=" + home, "BASH_COMP_DEBUG_FILE=" + home + "/completion-debug", "GOMAXPROCS=2", "GOCOVERDIR=" + coverageDir} var stdout, stderr bytes.Buffer child.Stdout, child.Stderr = &stdout, &stderr err := child.Run() diff --git a/cli/cmd/plugin-kit-ai/release_compat_test.go b/cli/cmd/plugin-kit-ai/release_compat_test.go index eccc6f44c..9005a62fc 100644 --- a/cli/cmd/plugin-kit-ai/release_compat_test.go +++ b/cli/cmd/plugin-kit-ai/release_compat_test.go @@ -377,6 +377,7 @@ func TestReleaseCompletionProcessStderr(t *testing.T) { t.Fatal(err) } home := t.TempDir() + coverageDir := t.TempDir() marker := "glpat-" + strings.Repeat("b", 20) for _, protocol := range []string{"__complete", "__completeNoDesc"} { cases := []struct { @@ -410,7 +411,7 @@ func TestReleaseCompletionProcessStderr(t *testing.T) { for i, tc := range cases { child := exec.Command(exe, append([]string{"-test.run=^TestReleaseCompletionProcessStderr$", "--"}, tc.args...)...) child.Dir = home - child.Env = []string{"UAP_COMPLETION_PROCESS_FIXTURE=1", "HOME=" + home, "TMPDIR=" + home, "XDG_CONFIG_HOME=" + home, "PATH=" + home, "BASH_COMP_DEBUG_FILE=" + home + "/completion-debug", "GOMAXPROCS=2"} + child.Env = []string{"UAP_COMPLETION_PROCESS_FIXTURE=1", "HOME=" + home, "TMPDIR=" + home, "XDG_CONFIG_HOME=" + home, "PATH=" + home, "BASH_COMP_DEBUG_FILE=" + home + "/completion-debug", "GOMAXPROCS=2", "GOCOVERDIR=" + coverageDir} var stdout, stderr bytes.Buffer child.Stdout, child.Stderr = &stdout, &stderr err := child.Run() diff --git a/cli/internal/agentpluginscli/add.go b/cli/internal/agentpluginscli/add.go index a9f559d4d..57f42ad26 100644 --- a/cli/internal/agentpluginscli/add.go +++ b/cli/internal/agentpluginscli/add.go @@ -21,6 +21,10 @@ func newAddCommand(app App, opts *options) *cobra.Command { Short: "Plan and install one Agent Plugins 1.0 package for one or more clients", Args: addCommandArgs(opts), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } return executeAddCommand(cmd, app, opts, args[0], activationComplete, authComplete) }, } @@ -183,7 +187,7 @@ func runAddLoaded(ctx context.Context, cmd *cobra.Command, app App, opts *option InstallIntent: opts.installIntents[selected.ClientID], Envelope: loaded.envelope, Client: selected, Scope: domain.InstallScope(opts.scope), DryRun: opts.dryRun, Confirmed: false, Interactive: app.Terminal, - Hints: loaded.hints, BackendExecutable: backendExecutable(selected, detectedMap), + Hints: loaded.hints, BackendExecutable: backendExecutable(selected, detectedMap, app.scope()), ActivationComplete: activationComplete, AuthComplete: authComplete, PersistAuthoritativeObservations: true, OriginMode: loaded.origin, DirectoryResolution: cloneDirectoryOrigin(loaded.directory), diff --git a/cli/internal/agentpluginscli/add_multi.go b/cli/internal/agentpluginscli/add_multi.go index 419100ce6..36cbf0641 100644 --- a/cli/internal/agentpluginscli/add_multi.go +++ b/cli/internal/agentpluginscli/add_multi.go @@ -97,7 +97,7 @@ func runAddManyWithClients(ctx context.Context, cmd *cobra.Command, app App, opt return err } writeProgress(app, opts.format, "Resolving and validating one Agent Plugin package for every selected target...") - loaded, err := app.loadPackageFor(ctx, source, withDetectedClients(app.addResolutionRequest(source, expandAffectedSurfaceTargets(targets)), detected)) + loaded, err := app.loadPackageFor(ctx, source, withDetectedClients(app.addResolutionRequest(source, expandAffectedSurfaceTargets(targets, app.scope())), detected)) if err != nil { return err } @@ -150,7 +150,7 @@ func runAddManyLoaded(ctx context.Context, cmd *cobra.Command, app App, opts *op return runAddLoaded(ctx, cmd, app, &selectedOptions, loaded, activationComplete, authComplete, clients, needsInstallConfirmation) } if state, err := app.StateStore.Load(); err == nil { - if installation, ok := locallyMatchedInstallation(state, loaded.envelope.Manifest.Name); ok && installationHasTarget(installation, executionTargets[0], string(domain.ScopeUser)) { + if installation, ok := locallyMatchedInstallation(state, loaded.envelope.Manifest.Name); ok && installationHasTarget(installation, executionTargets[0], string(app.scope())) { return runAddLoaded(ctx, cmd, app, &selectedOptions, loaded, false, false, clients, needsInstallConfirmation) } } @@ -178,9 +178,9 @@ func runAddManyLoaded(ctx context.Context, cmd *cobra.Command, app App, opts *op } input := usecase.AddInput{ InstallIntent: opts.installIntents[client.ClientID], - Envelope: clientPackage.envelope, Client: client, Scope: domain.ScopeUser, + Envelope: clientPackage.envelope, Client: client, Scope: app.scope(), DryRun: true, Interactive: app.Terminal, Hints: clientPackage.hints, - BackendExecutable: backendExecutable(client, detected), + BackendExecutable: backendExecutable(client, detected, app.scope()), ActivationComplete: activationComplete, AuthComplete: authComplete, PersistAuthoritativeObservations: false, OriginMode: loaded.origin, DirectoryResolution: cloneDirectoryOrigin(loaded.directory), diff --git a/cli/internal/agentpluginscli/add_targets.go b/cli/internal/agentpluginscli/add_targets.go index eb52f5421..44548b15c 100644 --- a/cli/internal/agentpluginscli/add_targets.go +++ b/cli/internal/agentpluginscli/add_targets.go @@ -88,7 +88,7 @@ func normalizeTarget(value string) domain.ClientID { return id } -func backendExecutable(selected domain.DetectedClient, clients map[domain.ClientID]domain.DetectedClient) string { +func backendExecutable(selected domain.DetectedClient, clients map[domain.ClientID]domain.DetectedClient, scopes ...domain.InstallScope) string { if path := strings.TrimSpace(selected.ExecutablePath); path != "" { return path } @@ -96,7 +96,7 @@ func backendExecutable(selected domain.DetectedClient, clients map[domain.Client if definition.Capabilities.PackageMode == domain.PackageNative { return selected.ExecutablePath } - for _, sibling := range domain.BackendSiblings(selected.ClientID) { + for _, sibling := range domain.BackendSiblingsForScope(selected.ClientID, selectedScope(scopes)) { if path := strings.TrimSpace(clients[sibling].ExecutablePath); path != "" { return path } @@ -107,7 +107,7 @@ func backendExecutable(selected domain.DetectedClient, clients map[domain.Client // detectedSharedClient resolves a logical surface through an already detected // native sibling backend. It does not invent that backend from a logical // surface when the backend itself is absent. -func detectedSharedClient(target domain.ClientID, clients map[domain.ClientID]domain.DetectedClient) (domain.DetectedClient, bool) { +func detectedSharedClient(target domain.ClientID, clients map[domain.ClientID]domain.DetectedClient, scopes ...domain.InstallScope) (domain.DetectedClient, bool) { client, exact := clients[target] if exact && client.Status == domain.DetectionDetected { return client, true @@ -116,7 +116,7 @@ func detectedSharedClient(target domain.ClientID, clients map[domain.ClientID]do if !ok || definition.Capabilities.PackageMode == domain.PackageNative { return client, exact } - for _, sibling := range domain.BackendSiblings(target) { + for _, sibling := range domain.BackendSiblingsForScope(target, selectedScope(scopes)) { peer, found := clients[sibling] if !found || peer.Status != domain.DetectionDetected { continue diff --git a/cli/internal/agentpluginscli/app.go b/cli/internal/agentpluginscli/app.go index 449a3c0a8..e852e2a40 100644 --- a/cli/internal/agentpluginscli/app.go +++ b/cli/internal/agentpluginscli/app.go @@ -39,6 +39,10 @@ type SourceAcquirer interface { } type App struct { + // ForScope composes an isolated runtime after command flags select its context. + ForScope func(domain.InstallScope, string) (App, error) `json:"-"` + Scope domain.InstallScope `json:"-"` + ProjectRoot string `json:"-"` chatGPTPreparation bool chatGPTAppID string Prompter prompt.Prompter @@ -96,6 +100,7 @@ type options struct { installIntents map[domain.ClientID]domain.InstallIntent color terminaltheme.Policy target string + projectRoot string scope string dryRun bool format string diff --git a/cli/internal/agentpluginscli/binding.go b/cli/internal/agentpluginscli/binding.go index 6fecc77bb..b92eab7d6 100644 --- a/cli/internal/agentpluginscli/binding.go +++ b/cli/internal/agentpluginscli/binding.go @@ -36,6 +36,13 @@ func newBindingChangeCommand(app App, opts *options, mode usecase.BindingChangeM Short: short, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } + if err := rejectProjectCommand(app, "binding changes"); err != nil { + return err + } if err := validateCommonOptions(opts); err != nil { return err } diff --git a/cli/internal/agentpluginscli/client_policy.go b/cli/internal/agentpluginscli/client_policy.go index a104c4220..96057ab2c 100644 --- a/cli/internal/agentpluginscli/client_policy.go +++ b/cli/internal/agentpluginscli/client_policy.go @@ -2,8 +2,8 @@ package agentpluginscli import "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" -func clientTraits(id domain.ClientID) domain.ClientTraits { - return domain.ClientTraitsFor(id) +func clientTraits(id domain.ClientID, scopes ...domain.InstallScope) domain.ClientTraits { + return (domain.SelectedDelivery{}).EffectiveTraitsForScope(id, selectedScope(scopes)) } func clientDefinition(id domain.ClientID) (domain.ClientDefinition, bool) { @@ -32,26 +32,26 @@ func reportsMCPToolNamespaceCollision(id domain.ClientID) bool { return clientTraits(id).ReportsMCPToolNamespaceCollision } -func nativeConfigLifecycle(id domain.ClientID) bool { - return clientTraits(id).LifecycleKind == domain.LifecycleNativeConfig +func nativeConfigLifecycle(id domain.ClientID, scopes ...domain.InstallScope) bool { + return clientTraits(id, scopes...).LifecycleKind == domain.LifecycleNativeConfig } -func sharesPhysicalBackend(id domain.ClientID) bool { - return len(domain.BackendSiblings(id)) > 0 +func sharesPhysicalBackend(id domain.ClientID, scopes ...domain.InstallScope) bool { + return len(domain.BackendSiblingsForScope(id, selectedScope(scopes))) > 0 } -func nativeCLIRegistryOwner(id domain.ClientID) domain.ClientID { - if owner, ok := nativePackageSibling(id); ok { +func nativeCLIRegistryOwner(id domain.ClientID, scopes ...domain.InstallScope) domain.ClientID { + if owner, ok := nativePackageSibling(id, scopes...); ok { return owner } return id } -func nativePackageSibling(id domain.ClientID) (domain.ClientID, bool) { - candidates := append([]domain.ClientID{id}, domain.BackendSiblings(id)...) +func nativePackageSibling(id domain.ClientID, scopes ...domain.InstallScope) (domain.ClientID, bool) { + candidates := append([]domain.ClientID{id}, domain.BackendSiblingsForScope(id, selectedScope(scopes))...) for _, candidate := range candidates { definition, ok := clientDefinition(candidate) - if ok && definition.Capabilities.PackageMode == domain.PackageNative && len(domain.BackendSiblings(candidate)) > 0 { + if ok && definition.Capabilities.PackageMode == domain.PackageNative && len(domain.BackendSiblingsForScope(candidate, selectedScope(scopes))) > 0 { return candidate, true } } @@ -113,17 +113,17 @@ func skipVersionProbeForPrepare(id domain.ClientID, intents map[domain.ClientID] return intents[id] == domain.InstallIntentPrepare && allowsHostedPrepare(id) } -func appendBackendSiblings(values []string, id domain.ClientID) []string { - for _, sibling := range domain.BackendSiblings(id) { +func appendBackendSiblings(values []string, id domain.ClientID, scopes ...domain.InstallScope) []string { + for _, sibling := range domain.BackendSiblingsForScope(id, selectedScope(scopes)) { values = append(values, string(sibling)) } return values } -func expandBackendSiblingTargets(targets []domain.ClientID) []domain.ClientID { +func expandBackendSiblingTargets(targets []domain.ClientID, scopes ...domain.InstallScope) []domain.ClientID { result := append([]domain.ClientID(nil), targets...) for _, target := range targets { - result = append(result, domain.BackendSiblings(target)...) + result = append(result, domain.BackendSiblingsForScope(target, selectedScope(scopes))...) } return result } diff --git a/cli/internal/agentpluginscli/dependency_git_diagnostic_contract_test.go b/cli/internal/agentpluginscli/dependency_git_diagnostic_contract_test.go new file mode 100644 index 000000000..9acc8cb1b --- /dev/null +++ b/cli/internal/agentpluginscli/dependency_git_diagnostic_contract_test.go @@ -0,0 +1,99 @@ +package agentpluginscli + +import ( + "context" + "errors" + "os/exec" + "reflect" + "testing" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/sourceacquisition" +) + +type dependencyDiagnosticDelegate func(context.Context, sourceacquisition.Command) ([]byte, error) + +func (delegate dependencyDiagnosticDelegate) Run(ctx context.Context, command sourceacquisition.Command) ([]byte, error) { + return delegate(ctx, command) +} + +type dependencyDiagnosticSecretError struct{} +type dependencyDiagnosticContextKey struct{} + +func (*dependencyDiagnosticSecretError) Error() string { + panic("Git argv/stream/credential error text must not be read") +} + +func TestDependencyGitDiagnosticPreservesCallAndOmitsAllPayloads(t *testing.T) { + ctx := context.WithValue(t.Context(), dependencyDiagnosticContextKey{}, "secret context") + command := sourceacquisition.Command{Dir: "/secret/path", Args: []string{"-C", "/secret/repository", "fetch", "secret argument"}, Stdin: []byte("secret input")} + body, original := []byte("secret output"), &dependencyDiagnosticSecretError{} + calls, reports := 0, 0 + runner := &dependencyGitDiagnosticRunner{delegate: dependencyDiagnosticDelegate(func(got context.Context, input sourceacquisition.Command) ([]byte, error) { + calls++ + if got != ctx || !reflect.DeepEqual(input, command) || &input.Args[0] != &command.Args[0] || &input.Stdin[0] != &command.Stdin[0] { + t.Fatal("diagnostic changed context/command authority") + } + return body, original + }), report: func(phase, class, status string) { + reports++ + if phase != "fetch" || class != "other_git_or_process" || status != "unavailable" { + t.Fatalf("diagnostic emitted nonfixed payload: phase=%q class=%q", phase, class) + } + }} + for range 6 { + got, err := runner.Run(ctx, command) + if reflect.ValueOf(err) != reflect.ValueOf(original) || &got[0] != &body[0] { + t.Fatal("diagnostic changed original bytes/error") + } + } + if calls != 6 || reports != 4 { + t.Fatalf("diagnostic call/log bounds changed: calls=%d reports=%d", calls, reports) + } +} + +func TestDependencyGitDiagnosticSuccessIsSilentAndClassesAreFixed(t *testing.T) { + runner := &dependencyGitDiagnosticRunner{delegate: dependencyDiagnosticDelegate(func(context.Context, sourceacquisition.Command) ([]byte, error) { + return []byte("success output"), nil + }), report: func(string, string, string) { t.Fatal("successful Git invocation logged") }} + if _, err := runner.Run(t.Context(), sourceacquisition.Command{}); err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + err error + want string + }{{context.Canceled, "context_canceled"}, {context.DeadlineExceeded, "context_deadline"}, {processadapter.ErrOrdinaryContainmentUnsupported, "ordinary_unsupported"}, {processadapter.ErrStdoutLimitExceeded, "stdout_limit"}, {processadapter.ErrOrdinaryForcedCleanup, "ordinary_forced_cleanup"}, {errors.Join(context.Canceled, processadapter.ErrOrdinaryForcedCleanup), "context_canceled"}, {&exec.ExitError{}, "other_git_or_process"}, {errors.New("secret error"), "other_git_or_process"}} { + if got, status := dependencyGitDiagnosticClass(tc.err); got != tc.want || status != "unavailable" { + t.Fatalf("diagnostic class=%q want=%q", got, tc.want) + } + } + if got := dependencyGitDiagnosticPhase([]string{"secret command"}); got != "unknown" { + t.Fatalf("unknown argv became diagnostic payload: %q", got) + } +} + +type dependencyDiagnosticExitError struct { + dependencyDiagnosticSecretError + code int +} + +func (err *dependencyDiagnosticExitError) GitTransportExitCode() int { return err.code } + +func TestDependencyGitDiagnosticRecognizesPositiveExitWithoutReadingError(t *testing.T) { + original := errors.Join(&dependencyDiagnosticExitError{code: 128}) + body := []byte("secret original output") + command := sourceacquisition.Command{Args: []string{"fetch", "secret argument"}} + reports := 0 + runner := &dependencyGitDiagnosticRunner{delegate: dependencyDiagnosticDelegate(func(context.Context, sourceacquisition.Command) ([]byte, error) { + return body, original + }), report: func(phase, class, status string) { + reports++ + if phase != "fetch" || class != "git_exit" || status != "128" { + t.Fatal("positive Git exit did not retain its fixed category/status") + } + }} + got, err := runner.Run(t.Context(), command) + if reflect.ValueOf(err) != reflect.ValueOf(original) || &got[0] != &body[0] || reports != 1 { + t.Fatal("positive exit diagnostics changed delegated identity or count") + } +} diff --git a/cli/internal/agentpluginscli/dependency_git_diagnostic_test.go b/cli/internal/agentpluginscli/dependency_git_diagnostic_test.go new file mode 100644 index 000000000..339e53150 --- /dev/null +++ b/cli/internal/agentpluginscli/dependency_git_diagnostic_test.go @@ -0,0 +1,64 @@ +package agentpluginscli + +import ( + "context" + "errors" + "strconv" + "sync/atomic" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/sourceacquisition" +) + +// This fixture decorator exposes only fixed labels. OSRunner errors may contain +// argv and streams, so even a disposable repository's error text is never read. +type dependencyGitDiagnosticRunner struct { + delegate sourceacquisition.Runner + report func(string, string, string) + failures atomic.Uint32 +} + +func (runner *dependencyGitDiagnosticRunner) Run(ctx context.Context, command sourceacquisition.Command) ([]byte, error) { + body, err := runner.delegate.Run(ctx, command) + if err != nil && runner.failures.Add(1) <= 4 { + class, status := dependencyGitDiagnosticClass(err) + runner.report(dependencyGitDiagnosticPhase(command.Args), class, status) + } + return body, err +} + +func dependencyGitDiagnosticPhase(args []string) string { + if len(args) > 2 && args[0] == "-C" { + args = args[2:] + } + if len(args) > 0 { + switch args[0] { + case "init", "remote", "config", "fetch", "rev-parse", "ls-tree", "sparse-checkout", "checkout", "ls-remote", "show", "check-ref-format": + return args[0] + } + } + return "unknown" +} + +func dependencyGitDiagnosticClass(err error) (string, string) { + switch { + case errors.Is(err, context.Canceled): + return "context_canceled", "unavailable" + case errors.Is(err, context.DeadlineExceeded): + return "context_deadline", "unavailable" + case errors.Is(err, processadapter.ErrOrdinaryContainmentUnsupported): + return "ordinary_unsupported", "unavailable" + case errors.Is(err, processadapter.ErrStdoutLimitExceeded): + return "stdout_limit", "unavailable" + case errors.Is(err, processadapter.ErrOrdinaryForcedCleanup): + return "ordinary_forced_cleanup", "unavailable" + } + if class := processadapter.OrdinarySupervisorFailureClass(err); class != "" { + return class, "unavailable" + } + var exit interface{ GitTransportExitCode() int } + if errors.As(err, &exit) { + return "git_exit", strconv.Itoa(exit.GitTransportExitCode()) + } + return "other_git_or_process", "unavailable" +} diff --git a/cli/internal/agentpluginscli/deps_all_clients_e2e_test.go b/cli/internal/agentpluginscli/deps_all_clients_e2e_test.go index 3192b882d..c05684e5f 100644 --- a/cli/internal/agentpluginscli/deps_all_clients_e2e_test.go +++ b/cli/internal/agentpluginscli/deps_all_clients_e2e_test.go @@ -64,6 +64,9 @@ func newDependencyHarness(t *testing.T, repo string, clients []domain.DetectedCl f.app.DependenciesRoot, f.app.EnvironmentLockPath, f.app.EnvironmentStoreRoot = root, filepath.Join(root, "environment.lock"), filepath.Join(root, "store") f.app.SecurityEvaluator = dependencyTestScanner{} source := &dependencyTestSource{Acquirer: sourceacquisition.Acquirer{TempRoot: t.TempDir(), URLForRepo: func(string) string { return repo }}} + source.Runner = &dependencyGitDiagnosticRunner{delegate: sourceacquisition.OSRunner{}, report: func(phase, class, status string) { + t.Logf("dependency Git diagnostic phase=%s class=%s exit_status=%s", phase, class, status) + }} f.app.SourceAcquirer = source if err := os.MkdirAll(root, 0700); err != nil { t.Fatal(err) diff --git a/cli/internal/agentpluginscli/deps_apply.go b/cli/internal/agentpluginscli/deps_apply.go index 71a0622a0..c6c07e5e0 100644 --- a/cli/internal/agentpluginscli/deps_apply.go +++ b/cli/internal/agentpluginscli/deps_apply.go @@ -30,12 +30,12 @@ type dependencySyncRequest struct { attestation *environment.Attestation } -func dependencyActions(view installer.Inspection, requirements environment.Requirements, lock environment.Lock) ([]dependencyAction, error) { - observations, err := environment.Observe(view) +func dependencyActions(view installer.Inspection, requirements environment.Requirements, lock environment.Lock, environmentIDs ...string) ([]dependencyAction, error) { + observations, err := environment.ObserveForEnvironment(view, selectedEnvironmentID(environmentIDs)) if err != nil { return nil, err } - prepared, err := (environment.Coordinator{}).PrepareSync(requirements, lock, observations) + prepared, err := (environment.Coordinator{EnvironmentID: selectedEnvironmentID(environmentIDs)}).PrepareSync(requirements, lock, observations) return prepared.Bindings, err } @@ -78,7 +78,7 @@ func runDependencySyncConverge(cmd *cobra.Command, app App, opts *options, locke if err != nil { return err } - actions, err := dependencyActions(state, request.requirements, request.lock) + actions, err := dependencyActions(state, request.requirements, request.lock, app.environmentID()) if err != nil { return err } @@ -182,7 +182,7 @@ func applyDependencySync(cmd *cobra.Command, app App, opts *options, request dep return err } adapter := &dependencyInstaller{app: app, opts: opts, detected: detected, cmd: cmd, consent: request.consent, beforeApply: request.checkInputs} - results, applyErr := (environment.Coordinator{Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: environment.EnvironmentUserDefault, ManifestDigest: lock.ManifestDigest, Bindings: actions}, true) + results, applyErr := (environment.Coordinator{EnvironmentID: app.environmentID(), Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: app.environmentID(), ManifestDigest: lock.ManifestDigest, Bindings: actions}, true) if renderErr := renderDependencyResults(cmd, opts, prepared, results, applyErr); renderErr != nil && applyErr == nil { return renderErr } @@ -221,6 +221,9 @@ func (app App) canonicalizeDependencySyncRequirements(ctx context.Context, requi } func requireDependencyMutationReady(app App) error { + if err := app.checkProjectNamespace(); err != nil { + return err + } if guard, ok := app.StateStore.(interface{ RequireMutationReady() error }); ok { return guard.RequireMutationReady() } @@ -358,14 +361,14 @@ func (request dependencySyncRequest) recheck(cmd *cobra.Command, app App, opts * } if len(request.aliases) > 0 { var observed []environment.BindingObservation - observed, err = environment.Observe(state) + observed, err = environment.ObserveForEnvironment(state, app.environmentID()) if err == nil { var selected environment.PreparedSync - selected, err = (environment.Coordinator{}).PrepareSyncFor(requirements, lock, observed, request.aliases, request.attestation) + selected, err = (environment.Coordinator{EnvironmentID: app.environmentID()}).PrepareSyncFor(requirements, lock, observed, request.aliases, request.attestation) actions = selected.Bindings } } else { - actions, err = dependencyActions(state, requirements, lock) + actions, err = dependencyActions(state, requirements, lock, app.environmentID()) } if err != nil { return nil, err diff --git a/cli/internal/agentpluginscli/deps_candidates.go b/cli/internal/agentpluginscli/deps_candidates.go index c34c6a93d..245bc5105 100644 --- a/cli/internal/agentpluginscli/deps_candidates.go +++ b/cli/internal/agentpluginscli/deps_candidates.go @@ -12,7 +12,7 @@ import ( func dependencyDirectoryRequest(app App, distribution string, sequence uint64, targets []string) domain.DirectoryResolveRequest { host := directoryEnvironment(nil) - request := domain.DirectoryResolveRequest{Selector: distribution, DesiredReleaseSequence: sequence, Scope: domain.ScopeUser, Operation: domain.DirectoryInstall, SchemaVersion: "1.0.0", InstallerVersion: app.Version, OS: host.OS, Architecture: host.Architecture} + request := domain.DirectoryResolveRequest{Selector: distribution, DesiredReleaseSequence: sequence, Scope: app.scope(), Operation: domain.DirectoryInstall, SchemaVersion: "1.0.0", InstallerVersion: app.Version, OS: host.OS, Architecture: host.Architecture} for _, target := range targets { request.Targets = append(request.Targets, domain.ClientID(target)) } diff --git a/cli/internal/agentpluginscli/deps_command_constructors.go b/cli/internal/agentpluginscli/deps_command_constructors.go new file mode 100644 index 000000000..b969b578a --- /dev/null +++ b/cli/internal/agentpluginscli/deps_command_constructors.go @@ -0,0 +1,73 @@ +package agentpluginscli + +import "github.com/spf13/cobra" + +func newDependencyInitCommand(app App, opts *options) *cobra.Command { + return &cobra.Command{Use: "init", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, err := app.forOptions(opts) + if err != nil { + return err + } + if err := validateDepsOptions(opts); err != nil { + return err + } + return initializeDependencies(cmd, app, opts) + }} +} + +func newDependencyLockCommand(app App, opts *options, update bool) *cobra.Command { + command := &cobra.Command{Use: "lock", Args: cobra.NoArgs} + if update { + command.Use = "update " + command.Args = cobra.ExactArgs(1) + } + command.RunE = func(cmd *cobra.Command, args []string) error { + app, err := app.forOptions(opts) + if err != nil { + return err + } + if err := validateDepsOptions(opts); err != nil { + return err + } + alias := "" + if update { + alias = args[0] + } + result, err := runDependencyLock(cmd.Context(), app, opts, alias, true) + if err != nil { + return err + } + return renderDependencyLock(cmd, opts, result) + } + return command +} + +func newDependencyStatusCommand(app App, opts *options) *cobra.Command { + return &cobra.Command{Use: "status", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, err := app.forOptions(opts) + if err != nil { + return err + } + if err := validateDepsOptions(opts); err != nil { + return err + } + return runDependencyStatus(cmd, app, opts) + }} +} + +func newDependencySyncCommand(app App, opts *options, locked, yes *bool) *cobra.Command { + command := &cobra.Command{Use: "sync", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, err := app.forOptions(opts) + if err != nil { + return err + } + if err := validateDepsOptions(opts); err != nil { + return err + } + return runDependencySync(cmd, app, opts, *locked, *yes) + }} + command.Flags().BoolVar(&opts.externalUninstalled, "external-uninstalled", false, "attest removed targets were uninstalled manually or never imported") + command.Flags().BoolVar(locked, "locked", false, "require an exact existing lock and never resolve refs") + command.Flags().BoolVar(yes, "yes", false, "confirm the complete preflighted sync without an interactive prompt") + return command +} diff --git a/cli/internal/agentpluginscli/deps_commands.go b/cli/internal/agentpluginscli/deps_commands.go index ffac67f2a..4cfa25638 100644 --- a/cli/internal/agentpluginscli/deps_commands.go +++ b/cli/internal/agentpluginscli/deps_commands.go @@ -19,64 +19,10 @@ import ( ) func newDepsCommand(app App, opts *options) *cobra.Command { - command := &cobra.Command{Use: "deps", Short: "Manage the portable user dependency environment"} + command := &cobra.Command{Use: "deps", Short: "Manage the portable dependency environment"} var locked, yes bool - command.AddCommand(&cobra.Command{ - Use: "init", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - if err := validateDepsOptions(opts); err != nil { - return err - } - return initializeDependencies(cmd, app, opts) - }, - }) - command.AddCommand(&cobra.Command{ - Use: "lock", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - if err := validateDepsOptions(opts); err != nil { - return err - } - result, err := runDependencyLock(cmd.Context(), app, opts, "", true) - if err != nil { - return err - } - return renderDependencyLock(cmd, opts, result) - }, - }) - command.AddCommand(&cobra.Command{ - Use: "update ", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if err := validateDepsOptions(opts); err != nil { - return err - } - result, err := runDependencyLock(cmd.Context(), app, opts, args[0], true) - if err != nil { - return err - } - return renderDependencyLock(cmd, opts, result) - }, - }) - command.AddCommand(&cobra.Command{ - Use: "status", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - if err := validateDepsOptions(opts); err != nil { - return err - } - return runDependencyStatus(cmd, app, opts) - }, - }) - sync := &cobra.Command{ - Use: "sync", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { - if err := validateDepsOptions(opts); err != nil { - return err - } - return runDependencySync(cmd, app, opts, locked, yes) - }, - } - sync.Flags().BoolVar(&opts.externalUninstalled, "external-uninstalled", false, "attest removed targets were uninstalled manually or never imported") - sync.Flags().BoolVar(&locked, "locked", false, "require an exact existing lock and never resolve refs") - sync.Flags().BoolVar(&yes, "yes", false, "confirm the complete preflighted sync without an interactive prompt") - command.AddCommand(sync) - command.AddCommand(newDependencyTransferCommand(app, opts, "export")) - command.AddCommand(newDependencyTransferCommand(app, opts, "import")) - command.AddCommand(newDependencyTreeCommand(app, opts)) - command.AddCommand(newDependencyRecoverCommand(app, opts, &yes)) + command.AddCommand(newDependencyInitCommand(app, opts), newDependencyLockCommand(app, opts, false), newDependencyLockCommand(app, opts, true), newDependencyStatusCommand(app, opts), newDependencySyncCommand(app, opts, &locked, &yes)) + command.AddCommand(newDependencyTransferCommand(app, opts, "export"), newDependencyTransferCommand(app, opts, "import"), newDependencyTreeCommand(app, opts), newDependencyRecoverCommand(app, opts, &yes)) return command } @@ -88,9 +34,6 @@ func validateEnvironmentOptions(command string, opts *options) error { if opts.target != "" { return fmt.Errorf("%s uses uap.toml targets only; inherited --target is not allowed", command) } - if opts.scope != "user" { - return fmt.Errorf("%s supports --scope user only", command) - } return validateCommonOptions(opts) } @@ -106,6 +49,9 @@ func runDependencyLock(ctx context.Context, app App, opts *options, updateAlias if err := requireDependencyMutationReady(app); err != nil { return dependencyLockResult{}, err } + if err := app.preflightProjectDependencyTargets(); err != nil { + return dependencyLockResult{}, err + } if allowWrite && !opts.dryRun { release, lockErr := processlock.Lock{Path: app.EnvironmentLockPath}.Acquire(ctx) if lockErr != nil { @@ -113,14 +59,8 @@ func runDependencyLock(ctx context.Context, app App, opts *options, updateAlias } defer func() { _ = release() }() } - if app.Installer != nil { - view, err := app.Installer.Inspect(ctx) - if err != nil { - return dependencyLockResult{}, err - } - if view.Recovery.Required { - return dependencyLockResult{}, installer.ErrRecoveryRequired - } + if err := requireDependencyLockRecoveryClear(ctx, app); err != nil { + return dependencyLockResult{}, err } manifestPath, lockPath := app.dependencyPaths() manifestBefore, manifestReadErr := os.ReadFile(manifestPath) @@ -169,6 +109,20 @@ func publishResolvedDependencyLock(ctx context.Context, app App, manifestPath, l return nil } +func requireDependencyLockRecoveryClear(ctx context.Context, app App) error { + if app.Installer == nil { + return nil + } + view, err := app.Installer.Inspect(ctx) + if err != nil { + return err + } + if view.Recovery.Required { + return installer.ErrRecoveryRequired + } + return nil +} + func derefLock(lock *environment.Lock) environment.Lock { if lock == nil { return environment.Lock{} @@ -296,6 +250,9 @@ func initializeDependencies(cmd *cobra.Command, app App, opts *options) error { func validateDependencyTargets(app App, requirements environment.Requirements) error { for target := range requirements.Targets { + if app.scope() == domain.ScopeProject && !supportsDependencyTarget(app, target) { + return fmt.Errorf("%s does not support project scope", target) + } if definition, ok := domain.ClientDefinitionFor(domain.ClientID(target)); ok && (definition.PlansWithoutHostPresence || definition.Traits.RequiresPersonalMappingForPrepare) { return fmt.Errorf("%s is prepared manually and cannot be a saved dependency target; use agentplugins add --target %s (not saved)", definition.DisplayName, target) } @@ -331,7 +288,7 @@ func resolveDependencyLock(ctx context.Context, app App, opts *options, requirem if hadPrevious { previousPin = &previous } - locked, err := (environment.Coordinator{Resolver: resolver}).Resolve(ctx, requirements, derefLock(previousPin), updateAlias) + locked, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: resolver}).Resolve(ctx, requirements, derefLock(previousPin), updateAlias) if err != nil { return requirements, environment.Lock{}, environment.Lock{}, err } @@ -346,6 +303,10 @@ func resolveDependencyLock(ctx context.Context, app App, opts *options, requirem func newDependencyRecoverCommand(app App, opts *options, yes *bool) *cobra.Command { recovery := &cobra.Command{ Use: "recover", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateDepsOptions(opts); err != nil { return err } @@ -370,5 +331,5 @@ func supportedDependencyTargets(app App) []string { // all explicitly registered legacy capabilities; this flow admits only the // owner's qualified dependency targets. func supportsDependencyTarget(app App, target string) bool { - return app.Installer != nil && app.Installer.SupportsClient(target) && installer.ClientSupported(app.ClientRegistry, target) + return app.Installer != nil && installer.ClientScopeSupported(app.ClientRegistry, target, app.scope()) } diff --git a/cli/internal/agentpluginscli/deps_repair.go b/cli/internal/agentpluginscli/deps_repair.go index 4ccfb8325..dc36c48e0 100644 --- a/cli/internal/agentpluginscli/deps_repair.go +++ b/cli/internal/agentpluginscli/deps_repair.go @@ -22,7 +22,7 @@ func buildDependencyRepairs(ctx context.Context, app App, state domain.StateFile if authority == nil { return nil, errors.New("mixed owned and unmanaged repair requires separate selections") } - binding, err := selectedRepairBinding(installation, target, domain.ScopeUser) + binding, err := selectedRepairBinding(installation, target, app.scope()) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func applyDependencyRepairs(cmd *cobra.Command, app App, opts *options, actions return renderDependencySync(cmd, opts, actions, true) } adapter := &dependencyInstaller{app: app, opts: opts, cmd: cmd, detected: detected, consent: consent} - results, err := (environment.Coordinator{Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: environment.EnvironmentUserDefault, Bindings: actions}, true) + results, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: app.environmentID(), Bindings: actions}, true) if renderErr := renderDependencyResults(cmd, opts, actions, results, err); err == nil { err = renderErr } diff --git a/cli/internal/agentpluginscli/deps_replay.go b/cli/internal/agentpluginscli/deps_replay.go index f45eab4a4..d7ca1e0e5 100644 --- a/cli/internal/agentpluginscli/deps_replay.go +++ b/cli/internal/agentpluginscli/deps_replay.go @@ -29,7 +29,7 @@ func resolveDependencySyncCandidate(ctx context.Context, app App, opts *options, if err != nil { return requirements, environment.Lock{}, err } - lock, err := (environment.Coordinator{Resolver: dependencyResolver{app: app, requirements: requirements, dryRun: opts.dryRun}}).Resolve(ctx, requirements, previous, "") + lock, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: dependencyResolver{app: app, requirements: requirements, dryRun: opts.dryRun}}).Resolve(ctx, requirements, previous, "") if err == nil { err = verifyDependencyDeclarations(ctx, app, lock) } diff --git a/cli/internal/agentpluginscli/deps_resolver.go b/cli/internal/agentpluginscli/deps_resolver.go index 7e5c280e1..e0cbcb9f0 100644 --- a/cli/internal/agentpluginscli/deps_resolver.go +++ b/cli/internal/agentpluginscli/deps_resolver.go @@ -303,7 +303,7 @@ func (app App) canonicalizeDependencyRequirements(ctx context.Context, requireme } selection, err := domain.ResolveDirectory(bundle.Snapshot, domain.DirectoryResolveRequest{ - Selector: requirement.Registry.Distribution, DesiredReleaseSequence: sequence, Targets: targets, Scope: domain.ScopeUser, + Selector: requirement.Registry.Distribution, DesiredReleaseSequence: sequence, Targets: targets, Scope: app.scope(), Operation: domain.DirectoryInstall, SchemaVersion: "1.0.0", InstallerVersion: app.Version, }) if err != nil { diff --git a/cli/internal/agentpluginscli/deps_retained.go b/cli/internal/agentpluginscli/deps_retained.go index e95068a57..93ee30465 100644 --- a/cli/internal/agentpluginscli/deps_retained.go +++ b/cli/internal/agentpluginscli/deps_retained.go @@ -70,7 +70,7 @@ func selectLockedLifecycle(app App, state domain.StateFileV2, requirements envir if !exists { return selection, nil } - installation, err := savedAliasInstallation(state, alias, selector) + installation, err := savedAliasInstallation(state, alias, selector, app.environmentID()) if err != nil { return selection, err } diff --git a/cli/internal/agentpluginscli/deps_status.go b/cli/internal/agentpluginscli/deps_status.go index e9c1b36fb..695dddf98 100644 --- a/cli/internal/agentpluginscli/deps_status.go +++ b/cli/internal/agentpluginscli/deps_status.go @@ -20,6 +20,7 @@ import ( ) type dependencyStatusFact struct { + Scope domain.InstallScope `json:"-"` Alias string `json:"alias,omitempty"` ClientID string `json:"client_id,omitempty"` InstallationID string `json:"installation_id,omitempty"` @@ -51,10 +52,16 @@ type dependencyStatus struct { func runDependencyStatus(cmd *cobra.Command, app App, opts *options) error { manifestPath, lockPath := app.dependencyPaths() - status := dependencyStatus{Scope: "user", ManifestPath: manifestPath, LockPath: lockPath, SupportedTargets: supportedDependencyTargets(app), Facts: []dependencyStatusFact{}} + status := dependencyStatus{Scope: string(app.scope()), ManifestPath: manifestPath, LockPath: lockPath, SupportedTargets: supportedDependencyTargets(app), Facts: []dependencyStatusFact{}} status.ExcludedTargets = []dependencyExcludedTarget{} for _, definition := range domain.ClientDefinitions() { reason := installer.ClientSupportExclusionReason(app.ClientRegistry, string(definition.ID)) + if app.scope() == domain.ScopeProject { + reason = "" + if !supportsDependencyTarget(app, string(definition.ID)) { + reason = "project_scope_unsupported" + } + } if reason != "" && reason != "not_registered" { status.ExcludedTargets = append(status.ExcludedTargets, dependencyExcludedTarget{ID: string(definition.ID), Reason: reason}) } @@ -141,7 +148,8 @@ func canonicalizeRequirementsFromLock(requirements *environment.Requirements, lo return nil } -func appendBindingStatusFacts(fact *dependencyStatusFact, binding installer.InspectedBinding, pkg environment.LockedPackage) { +func appendBindingStatusFacts(fact *dependencyStatusFact, binding installer.InspectedBinding, pkg environment.LockedPackage, environmentIDs ...string) { + mergeDependencyGuidanceScope(fact, binding.Scope) _, lifecycle := environment.LifecycleFacts(binding) fact.Facts = append(fact.Facts, lifecycle...) fact.NextActions = dependencyNextActions(*fact) @@ -149,7 +157,7 @@ func appendBindingStatusFacts(fact *dependencyStatusFact, binding installer.Insp if binding.TreeDigest != pkg.TreeDigest || binding.Revision != lockedCommit(pkg) { fact.Facts = append(fact.Facts, "not_synced") } - if binding.Owner == nil || binding.Owner.EnvironmentID != environment.EnvironmentUserDefault { + if binding.Owner == nil || binding.Owner.EnvironmentID != selectedEnvironmentID(environmentIDs) { fact.Facts = append(fact.Facts, "foreign") } if binding.ProjectionStatus != "" { @@ -238,7 +246,7 @@ func runDependencyRecover(cmd *cobra.Command, app App, opts *options, yes bool) return lockErr } defer func() { _ = release() }() - result, recoverErr := app.Installer.RecoverEnvironment(cmd.Context(), observed, environment.EnvironmentUserDefault) + result, recoverErr := app.Installer.RecoverEnvironment(cmd.Context(), observed, app.environmentID()) if opts.format == "json" { if renderErr := renderJSON(cmd.OutOrStdout(), map[string]any{"result": result, "recovery": observed.Recovery}); renderErr != nil && recoverErr == nil { return renderErr @@ -285,7 +293,7 @@ func renderDependencyStatus(cmd *cobra.Command, opts *options, status dependency func inspectDependencyFacts(ctx context.Context, app App, state installer.Inspection, lock environment.Lock, validLock bool) []dependencyStatusFact { receipts := map[string]domain.EnvironmentReceipt{} for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID == environment.EnvironmentUserDefault { + if receipt.EnvironmentID == app.environmentID() { receipts[receipt.Alias+"\x00"+receipt.ClientID] = receipt } } @@ -301,7 +309,7 @@ func inspectDependencyFacts(ctx context.Context, app App, state installer.Inspec } } for _, receipt := range receipts { - fact := dependencyStatusFact{Alias: receipt.Alias, ClientID: receipt.ClientID, PackageDigest: receipt.PackageDigest, Facts: []string{"not_synced"}} + fact := dependencyStatusFact{Scope: app.scope(), Alias: receipt.Alias, ClientID: receipt.ClientID, PackageDigest: receipt.PackageDigest, Facts: []string{"not_synced"}} appendStoreStatus(ctx, app, &fact, domain.TreeDigestAlgorithm) appendReceiptStatus(&fact, state, receipt) facts = append(facts, fact) @@ -310,7 +318,7 @@ func inspectDependencyFacts(ctx context.Context, app App, state installer.Inspec } func desiredDependencyFact(ctx context.Context, app App, state installer.Inspection, pkg environment.LockedPackage, target string, receipt domain.EnvironmentReceipt, owned bool) dependencyStatusFact { - fact := dependencyStatusFact{Alias: pkg.Alias, ClientID: target, PackageDigest: pkg.TreeDigest} + fact := dependencyStatusFact{Scope: app.scope(), Alias: pkg.Alias, ClientID: target, PackageDigest: pkg.TreeDigest} appendStoreStatus(ctx, app, &fact, pkg.DigestAlgorithm) if !owned { fact.Facts = append(fact.Facts, "not_synced") @@ -324,7 +332,7 @@ func desiredDependencyFact(ctx context.Context, app App, state installer.Inspect } fact.InstallationID, fact.BindingID, fact.Owner = receipt.InstallationID, receipt.ClientBindingID, receipt.EnvironmentID if binding, found := findInspectedEnvironmentBinding(state, receipt); found { - appendBindingStatusFacts(&fact, binding, pkg) + appendBindingStatusFacts(&fact, binding, pkg, app.environmentID()) } else { fact.Facts = append(fact.Facts, "partial") } @@ -362,6 +370,7 @@ func appendReceiptStatus(fact *dependencyStatusFact, state installer.Inspection, fact.Facts = append(fact.Facts, "partial") return } + mergeDependencyGuidanceScope(fact, binding.Scope) fact.Revision, fact.ProjectionDigest = binding.Revision, binding.ProjectionDigest if binding.ProjectionStatus != "" { fact.Facts = append(fact.Facts, binding.ProjectionStatus) @@ -378,7 +387,11 @@ func dependencyNextActions(fact dependencyStatusFact, sdk ...installer.NextActio } for _, item := range fact.Facts { if item == "activation_manual" || item == "activation_prepared" { - out = append(out, dependencyNextActionDTO{Kind: "attest_activation", Reason: fmt.Sprintf("agentplugins add --target %s --activation-complete", fact.ClientID)}) + if fact.Scope == domain.ScopeProject { + out = append(out, dependencyNextActionDTO{Kind: "review_project_activation", Reason: "review the selected project's client instructions and native loading, then retry supported verification in the same selected project; prepared files do not establish native activation"}) + } else { + out = append(out, dependencyNextActionDTO{Kind: "attest_activation", Reason: fmt.Sprintf("agentplugins add --target %s --activation-complete", fact.ClientID)}) + } } if item == "auth_pending" || item == "auth_failed" { return append(out, dependencyNextActionDTO{Kind: "attest_authentication", Reason: fmt.Sprintf("agentplugins add --target %s --auth-complete", fact.ClientID)}) @@ -386,3 +399,15 @@ func dependencyNextActions(fact dependencyStatusFact, sdk ...installer.NextActio } return out } + +// Never downgrade a project context to user guidance because inspected metadata +// disagrees. This chooses conservative text only; it confers no authority. +func mergeDependencyGuidanceScope(fact *dependencyStatusFact, bindingScope string) { + if fact.Scope == domain.ScopeProject || bindingScope == string(domain.ScopeProject) { + fact.Scope = domain.ScopeProject + return + } + if fact.Scope == "" { + fact.Scope = domain.InstallScope(bindingScope) + } +} diff --git a/cli/internal/agentpluginscli/deps_sync.go b/cli/internal/agentpluginscli/deps_sync.go index 8c315ccc1..1cb7df2f5 100644 --- a/cli/internal/agentpluginscli/deps_sync.go +++ b/cli/internal/agentpluginscli/deps_sync.go @@ -219,8 +219,8 @@ func (adapter *dependencyInstaller) Prepare(ctx context.Context, action environm if err != nil { return nil, err } - request := installer.Request{OperationID: operationID, Operation: installer.Operation(action.Action), ClientID: action.ClientID, ClientConfigRoot: client.ConfigRoot, - ClientExecutable: backendExecutable(client, adapter.detected), InstallationID: action.InstallationID, + request := installer.Request{Scope: adapter.app.scope(), ProjectRoot: adapter.app.ProjectRoot, OperationID: operationID, Operation: installer.Operation(action.Action), ClientID: action.ClientID, ClientConfigRoot: client.ConfigRoot, + ClientExecutable: backendExecutable(client, adapter.detected, adapter.app.scope()), ClientVersion: client.Version, InstallationID: action.InstallationID, Selector: action.InstallationID, EnvironmentAuthority: action.Authority, ExternalUninstalled: action.Action == environment.ActionRemove && adapter.opts.externalUninstalled, ActivationComplete: action.ActivationComplete, AuthComplete: action.AuthComplete} if err := adapter.populateDependencyTargets(view, action.InstallationID, &request); err != nil { return nil, err @@ -266,7 +266,7 @@ func verifyLockedDirectory(ctx context.Context, app App, loaded loadedPackage, l evidenceEnv.InstallerVersion = app.Version evidenceEnv.DependencyIdentity = deterministicDependencyIdentity(loaded.envelope, targets) request := domain.DirectoryResolveRequest{Selector: source.DistributionID, DesiredReleaseSequence: source.ReleaseSequence, - Operation: domain.DirectoryInstall, Scope: domain.ScopeUser, Targets: targets, SchemaVersion: "1.0.0", InstallerVersion: app.Version, + Operation: domain.DirectoryInstall, Scope: app.scope(), Targets: targets, SchemaVersion: "1.0.0", InstallerVersion: app.Version, OS: evidenceEnv.OS, Architecture: evidenceEnv.Architecture, DependencyIdentity: evidenceEnv.DependencyIdentity} if recorded == nil { request.Purpose = domain.DirectoryResolveLockedRestore @@ -321,8 +321,8 @@ func repairOwnedDependencies(cmd *cobra.Command, app App, opts *options, selecto if !owned { return false, nil } - if opts.scope != "" && opts.scope != "user" { - return true, errors.New("environment repair supports user scope only") + if opts.scope != "" && opts.scope != "user" && opts.scope != "project" { + return true, errors.New("environment repair supports user or project scope") } if app.Installer == nil { return true, errors.New("typed SDK installer is unavailable") @@ -350,7 +350,7 @@ func repairOwnedDependencies(cmd *cobra.Command, app App, opts *options, selecto func resolveDependencyBindingIdentity(view installer.Inspection, action *environment.PreparedBinding) error { for _, receipt := range view.EnvironmentReceipts { - if receipt.EnvironmentID == environment.EnvironmentUserDefault && receipt.Alias == action.Alias { + if receipt.EnvironmentID == action.Authority.EnvironmentID && receipt.Alias == action.Alias { if action.InstallationID == "" { action.InstallationID = receipt.InstallationID action.Authority.InstallationID = receipt.InstallationID @@ -369,11 +369,14 @@ func (adapter *dependencyInstaller) populateDependencyTargets(view installer.Ins continue } for _, binding := range installation.Bindings { + if binding.ClientID == request.ClientID { + continue + } sibling, ok := adapter.detected[domain.ClientID(binding.ClientID)] if !ok { return fmt.Errorf("installed sibling %s is not detected", binding.ClientID) } - request.KnownTargets = append(request.KnownTargets, installer.TargetFacts{ClientID: binding.ClientID, BindingID: binding.BindingID, ConfigRoot: sibling.ConfigRoot, Executable: backendExecutable(sibling, adapter.detected)}) + request.KnownTargets = append(request.KnownTargets, installer.TargetFacts{ClientID: binding.ClientID, BindingID: binding.BindingID, ConfigRoot: sibling.ConfigRoot, Executable: backendExecutable(sibling, adapter.detected, adapter.app.scope())}) } } return nil diff --git a/cli/internal/agentpluginscli/deps_transfer.go b/cli/internal/agentpluginscli/deps_transfer.go index 38f7a9117..fcb2302a6 100644 --- a/cli/internal/agentpluginscli/deps_transfer.go +++ b/cli/internal/agentpluginscli/deps_transfer.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" ) @@ -37,10 +38,14 @@ type dependencyTransfer struct { func newDependencyTransferCommand(app App, opts *options, operation string) *cobra.Command { var yes, install bool command := &cobra.Command{ - Use: operation + " [directory]", Short: operation + " the exact global dependency agreement", - Long: "Transfer only uap.toml and uap.lock between the global user environment and an exchange directory (default: current directory). Import never resolves pins. Import with --install requires strict locked preflight to succeed before saving; existing drift can require importing alone followed by install --locked.", + Use: operation + " [directory]", Short: operation + " the exact selected dependency agreement", + Long: "Transfer only uap.toml and uap.lock between the selected environment and an exchange directory (default: current directory). Import never resolves pins. Import with --install requires strict locked preflight to succeed before saving; existing drift can require importing alone followed by install --locked.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateDepsOptions(opts); err != nil { return err } @@ -79,6 +84,17 @@ func prepareDependencyTransfer(app App, opts *options, operation string, args [] if err != nil { return transfer, err } + if app.scope() == domain.ScopeProject { + same := filepath.Clean(global) == filepath.Clean(exchange) + if a, err := os.Stat(global); err == nil { + if b, err := os.Stat(exchange); err == nil { + same = same || os.SameFile(a, b) + } + } + if same { + return transfer, errors.New("project agreement is already in this directory; choose a different exchange directory") + } + } source, destination := global, exchange if operation == "import" { source, destination = exchange, global @@ -98,6 +114,7 @@ func prepareDependencyTransfer(app App, opts *options, operation string, args [] transfer.destination.diagnostic = err.Error() } transfer.output = newDependencyTransferOutput(transfer, operation, opts.dryRun, install) + transfer.output.Scope = string(app.scope()) return transfer, nil } diff --git a/cli/internal/agentpluginscli/deps_transfer_install.go b/cli/internal/agentpluginscli/deps_transfer_install.go index 9f1956cc6..a3cc3678e 100644 --- a/cli/internal/agentpluginscli/deps_transfer_install.go +++ b/cli/internal/agentpluginscli/deps_transfer_install.go @@ -96,7 +96,7 @@ func prepareDependencyTransferInstall(cmd *cobra.Command, app App, opts *options if err != nil { return request, err } - actions, err := dependencyActions(state, request.requirements, request.lock) + actions, err := dependencyActions(state, request.requirements, request.lock, app.environmentID()) if err != nil { return request, err } @@ -118,7 +118,7 @@ func applyDependencyTransferInstall(cmd *cobra.Command, app App, opts *options, return finishDependencyTransferInstall(cmd, opts, transfer, request, nil, err) } adapter := &dependencyInstaller{app: app, opts: opts, detected: request.detected, cmd: cmd, consent: request.consent, beforeApply: request.checkInputs} - results, applyErr := (environment.Coordinator{Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: environment.EnvironmentUserDefault, ManifestDigest: request.lock.ManifestDigest, Bindings: actions}, true) + results, applyErr := (environment.Coordinator{EnvironmentID: app.environmentID(), Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: app.environmentID(), ManifestDigest: request.lock.ManifestDigest, Bindings: actions}, true) return finishDependencyTransferInstall(cmd, opts, transfer, request, results, applyErr) } diff --git a/cli/internal/agentpluginscli/deps_tree.go b/cli/internal/agentpluginscli/deps_tree.go index baac5386f..7c31cf6bc 100644 --- a/cli/internal/agentpluginscli/deps_tree.go +++ b/cli/internal/agentpluginscli/deps_tree.go @@ -25,6 +25,10 @@ type dependencyTree struct { func newDependencyTreeCommand(app App, opts *options) *cobra.Command { return &cobra.Command{Use: "tree", Short: "Show the recorded dependency graph", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateDepsOptions(opts); err != nil { return err } diff --git a/cli/internal/agentpluginscli/engine_boundary.go b/cli/internal/agentpluginscli/engine_boundary.go index 44b3bd665..e27b12993 100644 --- a/cli/internal/agentpluginscli/engine_boundary.go +++ b/cli/internal/agentpluginscli/engine_boundary.go @@ -39,6 +39,13 @@ func newSwitchCommand(app App, opts *options) *cobra.Command { Short: "Move a complete Agent Plugin installation to a reviewed source", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } + if err := rejectProjectCommand(app, "switch"); err != nil { + return err + } if err := validateCommonOptions(opts); err != nil { return err } diff --git a/cli/internal/agentpluginscli/engine_switch.go b/cli/internal/agentpluginscli/engine_switch.go index a15de7ba9..f5205a077 100644 --- a/cli/internal/agentpluginscli/engine_switch.go +++ b/cli/internal/agentpluginscli/engine_switch.go @@ -179,7 +179,7 @@ func (session *switchSession) groupInputs() ([]usecase.AddInput, error) { } inputs = append(inputs, usecase.AddInput{ Envelope: clientPackage.envelope, Client: client, Scope: domain.ScopeUser, Hints: clientPackage.hints, - InstallationID: session.installation.InstallationID, BackendExecutable: backendExecutable(client, session.detected), + InstallationID: session.installation.InstallationID, BackendExecutable: backendExecutable(client, session.detected, session.app.scope()), OriginMode: session.loaded.origin, DirectoryResolution: cloneDirectoryOrigin(session.loaded.directory), DistributionSuspended: session.loaded.distributionSuspended, ReleaseRevoked: session.loaded.releaseRevoked, }) diff --git a/cli/internal/agentpluginscli/environment_session.go b/cli/internal/agentpluginscli/environment_session.go index 50fec1c31..1cf786bb8 100644 --- a/cli/internal/agentpluginscli/environment_session.go +++ b/cli/internal/agentpluginscli/environment_session.go @@ -110,11 +110,11 @@ func runSavedEnvironment(cmd *cobra.Command, app App, opts *options, snapshot en if err != nil { return err } - observed, err := environment.Observe(view) + observed, err := environment.ObserveForEnvironment(view, app.environmentID()) if err != nil { return err } - prepared, err := (environment.Coordinator{}).PrepareSyncFor(requirements, locked, observed, aliases, attest) + prepared, err := (environment.Coordinator{EnvironmentID: app.environmentID()}).PrepareSyncFor(requirements, locked, observed, aliases, attest) if err != nil { return err } diff --git a/cli/internal/agentpluginscli/group_progress_board.go b/cli/internal/agentpluginscli/group_progress_board.go index 8202007a9..7cc19f76d 100644 --- a/cli/internal/agentpluginscli/group_progress_board.go +++ b/cli/internal/agentpluginscli/group_progress_board.go @@ -37,6 +37,7 @@ type groupProgressRow struct { // groupProgressBoard owns only terminal presentation. The use case emits // observed checkpoints and remains independent of terminal control sequences. type groupProgressBoard struct { + scope domain.InstallScope mu sync.Mutex writer io.Writer theme terminaltheme.Theme @@ -57,11 +58,11 @@ func startGroupProgressBoard(app App, opts *options, selected []domain.DetectedC if !terminaltheme.IsTerminal(terminaltheme.Unwrap(writer)) { return nil } - return newGroupProgressBoard(writer, selected) + return newGroupProgressBoard(writer, selected, app.scope()) } -func newGroupProgressBoard(writer io.Writer, selected []domain.DetectedClient) *groupProgressBoard { - board := &groupProgressBoard{writer: writer, theme: terminaltheme.For(writer)} +func newGroupProgressBoard(writer io.Writer, selected []domain.DetectedClient, scopes ...domain.InstallScope) *groupProgressBoard { + board := &groupProgressBoard{scope: selectedScope(scopes), writer: writer, theme: terminaltheme.For(writer)} for _, client := range selected { label := prompt.SafeText(clientDisplayName(client)) if width := ansi.StringWidth(label); width > board.width { @@ -84,7 +85,7 @@ func (board *groupProgressBoard) observe(event usecase.GroupProgressEvent) { changed := false for index := range board.rows { row := &board.rows[index] - if !progressRowMatches(row.id, event.ClientID) || row.step == progressFinished { + if !progressRowMatches(row.id, event.ClientID, board.scope) || row.step == progressFinished { continue } var next groupProgressStep @@ -241,9 +242,9 @@ func progressPipeline(theme terminaltheme.Theme, row groupProgressRow) string { return strings.Join(parts, arrow) } -func progressRowMatches(row, event domain.ClientID) bool { +func progressRowMatches(row, event domain.ClientID, scopes ...domain.InstallScope) bool { if row == event { return true } - return domain.SharesBackend(row, event) + return domain.SameClientBackendForScope(row, event, selectedScope(scopes)) } diff --git a/cli/internal/agentpluginscli/install.go b/cli/internal/agentpluginscli/install.go index 2e9691f04..50d55f3b1 100644 --- a/cli/internal/agentpluginscli/install.go +++ b/cli/internal/agentpluginscli/install.go @@ -24,6 +24,10 @@ func newInstallCommand(app App, opts *options) *cobra.Command { Long: "Install/repair the declared environment from uap.toml and uap.lock: install everything declared on a first run, leave already-matching items alone, and converge missing or drifted state to the exact locked pins. With a source argument, install behaves as agentplugins add for that one package.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if len(args) == 1 { return executeAddCommand(cmd, app, opts, args[0], activationComplete, authComplete) } @@ -137,7 +141,7 @@ func applyDeclaredEnvironmentRepairs(cmd *cobra.Command, app App, opts *options, return err } adapter := &dependencyInstaller{app: app, opts: opts, cmd: cmd, detected: detected, consent: consent} - results, applyErr := (environment.Coordinator{Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: environment.EnvironmentUserDefault, Bindings: prepared}, true) + results, applyErr := (environment.Coordinator{EnvironmentID: app.environmentID(), Mutator: adapter}).ApplySync(cmd.Context(), environment.PreparedSync{EnvironmentID: app.environmentID(), Bindings: prepared}, true) if renderErr := renderDependencyResults(cmd, opts, prepared, results, applyErr); renderErr != nil && applyErr == nil { return renderErr } diff --git a/cli/internal/agentpluginscli/installer_facade.go b/cli/internal/agentpluginscli/installer_facade.go index c38c21f0e..075df8197 100644 --- a/cli/internal/agentpluginscli/installer_facade.go +++ b/cli/internal/agentpluginscli/installer_facade.go @@ -23,7 +23,7 @@ func useInstallerFacadeForLocalAdd(app App, opts *options, loaded loadedPackage, if !domain.ClientTraitsFor(targets[0]).QualifiedLocalInstallerFacade { return false } - if !automatedMutation(app, opts) || opts.scope != string(domain.ScopeUser) || loaded.origin != domain.OriginModeDirect { + if !automatedMutation(app, opts) || loaded.origin != domain.OriginModeDirect { return false } if loaded.envelope.FormatID != domain.FormatIDAgentPluginsV1 || loaded.envelope.Source.SourceBindingHint != "direct-local" || loaded.envelope.SnapshotRoot == "" { @@ -49,7 +49,7 @@ func runInstallerFacadeLocalAdd(ctx context.Context, cmd *cobra.Command, app App return err } installation, exists := locallyMatchedInstallation(state, loaded.envelope.Manifest.Name) - groupOutput := !exists || !installationHasTarget(installation, selected.ClientID, string(domain.ScopeUser)) + groupOutput := !exists || !installationHasTarget(installation, selected.ClientID, string(app.scope())) operationID, err := newOperationGroupID() if err != nil { return err @@ -60,11 +60,11 @@ func runInstallerFacadeLocalAdd(ctx context.Context, cmd *cobra.Command, app App Reason: "authorized by the CLI security policy", } request := installer.Request{ - Operation: installer.OpInstall, PackageRoot: loaded.envelope.SnapshotRoot, + Scope: app.scope(), ProjectRoot: app.ProjectRoot, Operation: installer.OpInstall, PackageRoot: loaded.envelope.SnapshotRoot, SourceRoot: loaded.envelope.Source.CanonicalSource, ExecutableFiles: append([]string{}, loaded.envelope.ExecutableFiles...), ClientID: string(selected.ClientID), ClientConfigRoot: selected.ConfigRoot, - ClientExecutable: backendExecutable(selected, detected), OperationID: operationID, + ClientExecutable: backendExecutable(selected, detected, app.scope()), ClientVersion: selected.Version, OperationID: operationID, RequiredComponents: requiredFacadeComponents(loaded.envelope), Assessment: &assessment, } prepared, err := app.Installer.Prepare(ctx, request) @@ -113,7 +113,7 @@ func facadePlanAddResult(plan installer.Plan, envelope domain.PackageEnvelope) u out := usecase.AddResult{ InstallationID: plan.InstallationID, Plan: domain.DeliveryPlan{ - ClientID: clientID, Scope: domain.ScopeUser, Status: domain.PlanStatus(delivery.Status), + ClientID: clientID, Scope: plan.Scope, TargetContext: plan.TargetContext, Status: domain.PlanStatus(delivery.Status), PackageMode: domain.PackageMode(delivery.PackageMode), DeclaredName: envelope.Manifest.Name, DeclaredVersion: envelope.Manifest.Version, ActivePath: plan.TargetPath, InstallIntent: domain.InstallIntent(delivery.InstallIntent), PhysicalArtifactID: delivery.PhysicalArtifactID, diff --git a/cli/internal/agentpluginscli/interactive_directory.go b/cli/internal/agentpluginscli/interactive_directory.go index c25795cc7..db13dc9e3 100644 --- a/cli/internal/agentpluginscli/interactive_directory.go +++ b/cli/internal/agentpluginscli/interactive_directory.go @@ -109,7 +109,7 @@ func (session *directoryTargetSession) subsetResolves(candidate []domain.Detecte func (session *directoryTargetSession) baseRequest(targets []domain.ClientID) domain.DirectoryResolveRequest { return domain.DirectoryResolveRequest{ - Selector: session.resolveSelector, Targets: targets, Scope: domain.ScopeUser, + Selector: session.resolveSelector, Targets: targets, Scope: session.app.scope(), InstallerVersion: session.app.Version, ClientVersions: session.environment.ClientVersions, OS: runtime.GOOS, Architecture: runtime.GOARCH, DependencyIdentity: session.environment.DependencyIdentity, SchemaVersion: "1.0.0", Operation: session.operation, Recorded: session.recorded, diff --git a/cli/internal/agentpluginscli/interactive_loaded.go b/cli/internal/agentpluginscli/interactive_loaded.go index 35e3de42e..9b485d9c9 100644 --- a/cli/internal/agentpluginscli/interactive_loaded.go +++ b/cli/internal/agentpluginscli/interactive_loaded.go @@ -96,8 +96,8 @@ func (session *loadedTargetSession) classify(client domain.DetectedClient) (doma if err := prepareLoadedPackageForClient(&candidate, client.ClientID); err != nil { return client, &targetSkip{client.ClientID, "package binding preparation failed; ask the package publisher to check its client mapping"}, false } - plan, err := session.app.Planner.Plan(session.ctx, domain.PlanRequest{ - Envelope: candidate.envelope, Client: client, Scope: domain.ScopeUser, PhysicalArtifactID: session.physicalID, + plan, err := session.app.Planner.Plan(session.ctx, domain.PlanRequest{ProjectRoot: session.app.ProjectRoot, + Envelope: candidate.envelope, Client: client, Scope: session.app.scope(), PhysicalArtifactID: session.physicalID, Detected: session.clientMap, }) if err != nil || plan.Status == domain.PlanUnsupported { @@ -142,7 +142,7 @@ func (session *loadedTargetSession) preflight(client domain.DetectedClient, plan return client, nil, true } err := session.preflighter.PreflightActivation(domain.ActivationRequest{ - Client: client, Plan: plan, BackendExecutable: backendExecutable(client, session.clientMap), VerifyOnly: true, + Client: client, Plan: plan, BackendExecutable: backendExecutable(client, session.clientMap, session.app.scope()), VerifyOnly: true, }) if err == nil { return client, nil, true diff --git a/cli/internal/agentpluginscli/lifecycle.go b/cli/internal/agentpluginscli/lifecycle.go index 729fa63f6..04b7459be 100644 --- a/cli/internal/agentpluginscli/lifecycle.go +++ b/cli/internal/agentpluginscli/lifecycle.go @@ -25,6 +25,10 @@ func newUpdateCommand(app App, opts *options) *cobra.Command { Short: "Safely update a tracked Agent Plugin across selected or installed clients", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -69,6 +73,10 @@ func newRepairCommand(app App, opts *options) *cobra.Command { Use: "repair ", Short: "Explicitly restore a missing or modified managed package", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -157,7 +165,7 @@ func runRepair(ctx context.Context, cmd *cobra.Command, app App, opts *options, } input := usecase.AddInput{Envelope: loaded.envelope, Client: selected, Scope: domain.InstallScope(opts.scope), DryRun: opts.dryRun, InstallationID: installation.InstallationID, Interactive: app.Terminal, Hints: loaded.hints, - BackendExecutable: backendExecutable(selected, detectedMap), EnvironmentAuthority: authority} + BackendExecutable: backendExecutable(selected, detectedMap, app.scope()), EnvironmentAuthority: authority} planned, err := service.Repair(ctx, input) if err != nil { if planned.Plan.Status == domain.PlanUnsupported { @@ -304,7 +312,7 @@ func runUpdate(ctx context.Context, cmd *cobra.Command, app App, opts *options, input := usecase.AddInput{ Envelope: loaded.envelope, Client: selected, Scope: domain.InstallScope(opts.scope), DryRun: opts.dryRun, Interactive: app.Terminal, Hints: loaded.hints, - BackendExecutable: backendExecutable(selected, detectedMap), + BackendExecutable: backendExecutable(selected, detectedMap, app.scope()), PersistAuthoritativeObservations: true, EnvironmentAuthority: authority, } @@ -359,6 +367,10 @@ func newRemoveCommand(app App, opts *options) *cobra.Command { Short: "Safely remove selected targets while retaining plugin data by default", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -531,7 +543,7 @@ func runRemove(ctx context.Context, cmd *cobra.Command, app App, opts *options, Selector: installation.InstallationID, Client: selected, Scope: domain.InstallScope(opts.scope), DryRun: opts.dryRun, Interactive: app.Terminal, ExternalUninstalled: opts.externalUninstalled, - BackendExecutable: backendExecutable(selected, detectedMap), + BackendExecutable: backendExecutable(selected, detectedMap, app.scope()), EnvironmentAuthority: authority, } planned, err := service.Remove(ctx, input) diff --git a/cli/internal/agentpluginscli/outdated.go b/cli/internal/agentpluginscli/outdated.go index 873ee5f36..7613b37cc 100644 --- a/cli/internal/agentpluginscli/outdated.go +++ b/cli/internal/agentpluginscli/outdated.go @@ -56,6 +56,10 @@ func newOutdatedCommand(app App, opts *options) *cobra.Command { Short: "Check immutable release identities without changing installations", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -340,7 +344,7 @@ func inspectDirectoryOutdated( } return item } - return resolveDirectoryOutdated(bundle, detected, installerVersion, installation, origin, item) + return resolveDirectoryOutdated(bundle, detected, installerVersion, installation, origin, item, domain.InstallScope(scope)) } func directoryOutdatedPreflight(item outdatedInstallation, detectionErr error, installation domain.Installation, scope string) (status, reason string, cloneInstalled, stop bool) { @@ -363,6 +367,7 @@ func resolveDirectoryOutdated( installation domain.Installation, origin *domain.DirectoryOrigin, item outdatedInstallation, + scopes ...domain.InstallScope, ) outdatedInstallation { _, clientMap, err := preflightSelectedTargets(context.Background(), App{Detector: staticDetectedClientSource(detected)}, item.Targets, detectedClientValues(detected), false) if err != nil { @@ -372,7 +377,7 @@ func resolveDirectoryOutdated( environment := directoryEnvironment(clientMap) environment.InstallerVersion = installerVersion selection, err := domain.ResolveDirectory(bundle.Snapshot, domain.DirectoryResolveRequest{ - Selector: origin.ProductID, Targets: expandAffectedSurfaceTargets(item.Targets), Scope: domain.ScopeUser, + Selector: origin.ProductID, Targets: expandAffectedSurfaceTargets(item.Targets, selectedScope(scopes)), Scope: selectedScope(scopes), InstallerVersion: installerVersion, ClientVersions: environment.ClientVersions, OS: environment.OS, Architecture: environment.Architecture, DependencyIdentity: environment.DependencyIdentity, SchemaVersion: "1.0.0", Operation: domain.DirectoryUpdate, diff --git a/cli/internal/agentpluginscli/preflight.go b/cli/internal/agentpluginscli/preflight.go index 2586a0568..94807371d 100644 --- a/cli/internal/agentpluginscli/preflight.go +++ b/cli/internal/agentpluginscli/preflight.go @@ -42,7 +42,7 @@ func preflightSelectedTargets(ctx context.Context, app App, targets []domain.Cli if err := selectedDetectionError(target, detected); err != nil { return nil, nil, err } - client, ok := detectedSharedClient(target, detected) + client, ok := detectedSharedClient(target, detected, app.scope()) if plansWithoutHostPresence(target) && !ok { client = syntheticUndetectedClient(target) detected[target] = client @@ -62,14 +62,17 @@ func preflightSelectedTargets(ctx context.Context, app App, targets []domain.Cli // the other member of the Copilot/VS Code shared backend. This is safe only for // compatibility and lifecycle checks of an already-recorded physical binding; // explicit user targets must continue through the strict preflight above. -func preflightInstalledBindings(bindingTargets []domain.ClientID, detected map[domain.ClientID]domain.DetectedClient) ([]domain.DetectedClient, error) { +func preflightInstalledBindings(bindingTargets []domain.ClientID, detected map[domain.ClientID]domain.DetectedClient, scopes ...domain.InstallScope) ([]domain.DetectedClient, error) { selected := make([]domain.DetectedClient, 0, len(bindingTargets)) seen := make(map[domain.ClientID]struct{}, len(bindingTargets)) for _, target := range bindingTargets { if err := selectedDetectionError(target, detected); err != nil { return nil, err } - client, ok := clientplanner.DetectedPhysicalClient(target, detected) + client, ok := detected[target] + if selectedScope(scopes) == domain.ScopeUser { + client, ok = clientplanner.DetectedPhysicalClient(target, detected) + } if plansWithoutHostPresence(target) && !ok { client = syntheticUndetectedClient(target) ok = true @@ -102,7 +105,7 @@ func resolveInstalledBindingTargets(ctx context.Context, app App, bindingTargets for _, client := range clients { detected[client.ClientID] = client } - resolved, err := preflightInstalledBindings(bindingTargets, detected) + resolved, err := preflightInstalledBindings(bindingTargets, detected, app.scope()) if err != nil { return nil, err } diff --git a/cli/internal/agentpluginscli/project_claude_prepared_update_test.go b/cli/internal/agentpluginscli/project_claude_prepared_update_test.go new file mode 100644 index 000000000..8845a38e1 --- /dev/null +++ b/cli/internal/agentpluginscli/project_claude_prepared_update_test.go @@ -0,0 +1,221 @@ +package agentpluginscli + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/dirswap" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/processlock" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + clientplanner "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/plannertest" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providerstest" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/transaction" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type preparedClaudeCLIRunner struct { + t *testing.T + project, user, executable string + calls int +} + +func (r *preparedClaudeCLIRunner) Run(_ context.Context, cmd legacyports.Command) (legacyports.CommandResult, error) { + r.t.Helper() + if !reflect.DeepEqual(cmd.Argv, []string{r.executable, "plugin", "list", "--json"}) || cmd.Dir != r.project { + r.t.Fatalf("unexpected fixture command=%+v", cmd) + } + found := false + for _, v := range cmd.Env { + if v == "CLAUDE_CONFIG_DIR="+r.user { + found = true + } + } + if !found { + r.t.Fatal("original user profile was replaced") + } + r.calls++ + entries, err := os.ReadDir(filepath.Join(r.project, ".claude", "skills")) + if err != nil && !os.IsNotExist(err) { + return legacyports.CommandResult{}, err + } + count := 0 + for _, e := range entries { + if e.IsDir() { + count++ + } + } + if count == 0 { + return legacyports.CommandResult{Stdout: []byte("[]")}, nil + } + if count != 1 { + return legacyports.CommandResult{}, fmt.Errorf("unexpected fixture project directories=%d", count) + } + return legacyports.CommandResult{Stdout: []byte(`[{"id":"(suppressed)@skills-dir","version":"unknown","scope":"project","enabled":false,"installPath":"","notes":["1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies."]}]`)}, nil +} + +// The public saved-binding update command uses grouped activation even for one +// target. The runner is inert; real native failure evidence is retained separately. +func TestProjectClaudePreparedCLIGroupedUpdateReportsActualMutation(t *testing.T) { + client := fixtureClient(t, domain.ClientClaude) + f := newCLIFixture(t, []domain.DetectedClient{client}) + physicalRoot, resolveErr := filepath.EvalSymlinks(f.root) + if resolveErr != nil { + t.Fatal(resolveErr) + } + f.root = physicalRoot + project := filepath.Join(f.root, "A") + other := filepath.Join(f.root, "B") + for _, p := range []string{project, other, client.ConfigRoot} { + if err := os.MkdirAll(p, 0700); err != nil { + t.Fatal(err) + } + } + client.ExecutablePath = filepath.Join(f.root, "inert-claude") + if err := os.WriteFile(client.ExecutablePath, []byte("inert fixture, never executed"), 0700); err != nil { + t.Fatal(err) + } + f.app.Detector = staticDetector{clients: []domain.DetectedClient{client}} + userState := f.store.Path + projectForeign := filepath.Join(project, ".claude", "settings.local.json") + for _, p := range []string{filepath.Join(client.ConfigRoot, "foreign.json"), filepath.Join(other, ".claude", "settings.local.json"), filepath.Join(other, ".uap", "state-v2.json"), userState, projectForeign} { + if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte("preserve exact foreign bytes\n"), 0600); err != nil { + t.Fatal(err) + } + } + foreign := map[string]string{} + for _, p := range []string{client.ConfigRoot, other, userState, projectForeign} { + for name, body := range preparedClaudeCLIFiles(t, p) { + foreign[name] = body + } + } + stateRoot := filepath.Join(project, ".uap") + f.store = statev2.Store{Path: filepath.Join(stateRoot, "state-v2.json"), ProjectRoot: project} + f.app.Scope, f.app.ProjectRoot = domain.ScopeProject, project + f.app.ManagedRoot = filepath.Join(stateRoot, "managed") + f.app.StateStore = f.store + f.app.EnvironmentLockPath = filepath.Join(stateRoot, "environment.lock") + planner := plannertest.NewPlanner(clientplanner.Planner{ProjectRoot: project, ManagedRoot: f.app.ManagedRoot}) + f.app.Planner, f.app.Targets = planner, planner + runner := &preparedClaudeCLIRunner{t: t, project: project, user: client.ConfigRoot, executable: client.ExecutablePath} + f.app.Lifecycle.ProjectRoot = project + f.app.Lifecycle.StateStore = f.store + f.app.Lifecycle.Planner, f.app.Lifecycle.Targets = planner, planner + f.app.Lifecycle.Activator = providerstest.NewActivator(providers.Activator{Runner: runner}) + f.app.Lifecycle.NativeObserver = providerstest.NewObserver(providers.NativeIdentityObserver{Runner: runner, Stager: f.app.Lifecycle.Stager}) + f.app.Lifecycle.Lock = processlock.Lock{Path: filepath.Join(stateRoot, "mutation.lock")} + f.app.Lifecycle.Kernel = transaction.Kernel{StateStore: f.store, Directory: dirswap.Manager{JournalDir: filepath.Join(stateRoot, "operations-v2")}} + var err error + f.app.Installer, err = installer.New(installer.Config{ProjectRoot: project, StateRoot: stateRoot, Registry: f.app.ClientRegistry, Runner: runner, HelperExecutable: client.ExecutablePath, EnableNativeObserver: true, TrustedLocalPackages: true}) + if err != nil { + t.Fatal(err) + } + f.app.Lifecycle.CheckNamespace = f.app.Installer.CheckProjectNamespace + pkg := writeCLIPlugin(t) + if err := os.MkdirAll(filepath.Join(pkg, "skills", "guide"), 0700); err != nil { + t.Fatal(err) + } + skill := filepath.Join(pkg, "skills", "guide", "SKILL.md") + writeSkill := func(marker string) { + t.Helper() + if err := os.WriteFile(skill, []byte("---\nname: guide\ndescription: Inert marker\n---\n"+marker+"\n"), 0600); err != nil { + t.Fatal(err) + } + } + writeSkill("version one") + run := func(args ...string) map[string]any { + t.Helper() + args = append(args, "--target", "claude", "--scope", "project", "--project-root", project, "--format", "json") + stdout, stderr, err := f.execute(false, args...) + if err != nil { + t.Fatalf("%v: %v\n%s\n%s", args, err, stdout, stderr) + } + var envelope map[string]any + if err := json.Unmarshal([]byte(stdout), &envelope); err != nil { + t.Fatal(err) + } + data := envelope["data"].(map[string]any) + if targets, ok := data["targets"].([]any); ok { + data = targets[0].(map[string]any)["output"].(map[string]any) + } + result := data["result"].(map[string]any) + activation := result["activation"].(map[string]any) + if activation["activation"] != "prepared" || activation["verification"] != "installation_verified" { + t.Fatalf("project observation promoted=%+v", result) + } + return result + } + run("add", pkg, "--no-save") + before, err := os.ReadFile(f.store.Path) + if err != nil { + t.Fatal(err) + } + state, err := f.store.Load() + if err != nil { + t.Fatal(err) + } + var managed string + for _, b := range state.Installations[0].Clients { + managed = b.TargetLocator + } + treeBefore := preparedClaudeCLIFiles(t, managed) + for _, args := range [][]string{{"add", pkg, "--no-save"}, {"update", "demo"}} { + calls := runner.calls + result := run(args...) + if result["mutated"] != false || result["no_change"] != true || runner.calls <= calls { + t.Fatalf("unchanged Prepared did not reobserve without mutation=%+v calls%d/%d", result, runner.calls, calls) + } + after, err := os.ReadFile(f.store.Path) + if err != nil || string(after) != string(before) { + t.Fatalf("unchanged state rewritten: %v", err) + } + if !reflect.DeepEqual(treeBefore, preparedClaudeCLIFiles(t, managed)) { + t.Fatal("unchanged update changed owned bytes") + } + } + writeSkill("version two") + changed := run("update", "demo") + if changed["mutated"] != true || changed["no_change"] == true || reflect.DeepEqual(treeBefore, preparedClaudeCLIFiles(t, managed)) { + t.Fatalf("changed Prepared bytes misclassified=%+v", changed) + } + for path, want := range foreign { + body, err := os.ReadFile(path) + if err != nil || string(body) != want { + t.Fatalf("foreign file changed %s: %v", path, err) + } + } +} +func preparedClaudeCLIFiles(t *testing.T, root string) map[string]string { + t.Helper() + out := map[string]string{} + if err := filepath.WalkDir(root, func(path string, e os.DirEntry, err error) error { + if err != nil { + return err + } + if e.IsDir() { + return nil + } + if e.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("unexpected fixture symlink %s", path) + } + b, err := os.ReadFile(path) + if err == nil { + out[path] = string(b) + } + return err + }); err != nil { + t.Fatal(err) + } + return out +} diff --git a/cli/internal/agentpluginscli/project_context.go b/cli/internal/agentpluginscli/project_context.go new file mode 100644 index 000000000..cf2971432 --- /dev/null +++ b/cli/internal/agentpluginscli/project_context.go @@ -0,0 +1,172 @@ +package agentpluginscli + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" +) + +func (app App) scope() domain.InstallScope { + if app.Scope == domain.ScopeProject || app.ProjectRoot != "" { + return domain.ScopeProject + } + return domain.ScopeUser +} + +func (app App) environmentID() string { + if app.scope() == domain.ScopeProject { + return environment.EnvironmentProjectDefault + } + return environment.EnvironmentUserDefault +} + +func (app App) forOptions(opts *options) (App, error) { + if err := validateCommonOptions(opts); err != nil { + return App{}, err + } + scope := domain.InstallScope(opts.scope) + if scope == domain.ScopeUser && app.scope() == domain.ScopeUser { + return app, nil + } + root := "" + if scope == domain.ScopeProject { + var err error + root, err = canonicalProjectRoot(opts.projectRoot) + if err != nil { + return App{}, err + } + } + selected, err := app.composeScope(scope, root) + if err != nil { + return App{}, err + } + if selected.scope() != scope || selected.ProjectRoot != root { + return App{}, fmt.Errorf("selected runtime does not match requested scope and project root") + } + if scope == domain.ScopeProject { + if err := selected.checkProjectNamespace(); err != nil { + return App{}, err + } + if err := selected.checkProjectTargets(opts.target); err != nil { + return App{}, err + } + } + return selected, nil +} + +func canonicalProjectRoot(root string) (string, error) { + if root == "" { + var err error + root, err = os.Getwd() + if err != nil { + return "", err + } + } + absolute, err := filepath.Abs(root) + if err != nil { + return "", err + } + root, err = filepath.EvalSymlinks(absolute) + if err != nil { + return "", fmt.Errorf("select existing project root: %w", err) + } + info, err := os.Stat(root) + if err != nil || !info.IsDir() { + return "", fmt.Errorf("project root must be an existing directory") + } + if _, err := domain.NewTargetContext(domain.ScopeProject, root, ""); err != nil { + return "", err + } + return root, nil +} + +func (app App) composeScope(scope domain.InstallScope, root string) (App, error) { + if app.scope() == scope && app.ProjectRoot == root { + return app, nil + } + if app.ForScope == nil { + return App{}, fmt.Errorf("this composition supports user scope only; project scope runtime is unavailable") + } + selected, err := app.ForScope(scope, root) + if err != nil { + return App{}, err + } + selected.Input, selected.Output, selected.ErrorOutput = app.Input, app.Output, app.ErrorOutput + selected.Prompter, selected.PromptFactory, selected.Terminal = app.Prompter, app.PromptFactory, app.Terminal + return selected, nil +} + +func (app App) checkProjectTargets(targetOption string) error { + targets, err := parseTargetOption(targetOption) + if err != nil { + return err + } + for _, target := range targets { + if !clients.SupportsScope(app.ClientRegistry, target, domain.ScopeProject) { + return fmt.Errorf("%s does not support project scope", target) + } + } + return nil +} + +// Reject unsupported project declarations before a lock creates mutable state. +// The locked operation still reads and validates the authoritative manifest. +func (app App) preflightProjectDependencyTargets() error { + if app.scope() != domain.ScopeProject { + return nil + } + manifest, _ := app.dependencyPaths() + requirements, err := readDependencyManifest(manifest) + if err != nil { + return err + } + return validateDependencyTargets(app, requirements) +} + +func rejectProjectCommand(app App, command string) error { + if app.scope() == domain.ScopeProject { + return fmt.Errorf("%s is not supported for project scope", command) + } + return nil +} + +func selectedEnvironmentID(ids []string) string { + if len(ids) > 0 && ids[0] != "" { + return ids[0] + } + return environment.EnvironmentUserDefault +} + +func selectedScope(scopes []domain.InstallScope) domain.InstallScope { + if len(scopes) > 0 && scopes[0] != "" { + return scopes[0] + } + return domain.ScopeUser +} +func scopeFromProjectRoot(root string) domain.InstallScope { + if root != "" { + return domain.ScopeProject + } + return domain.ScopeUser +} + +func (app App) checkProjectNamespace() error { + if app.scope() != domain.ScopeProject { + return nil + } + if app.Installer == nil || app.StateStore == nil || app.Lifecycle.Paths == nil { + return fmt.Errorf("project runtime requires an installer, scoped state store and path policy") + } + if err := app.Installer.CheckProjectNamespace(); err != nil { + return err + } + if err := app.Lifecycle.Paths.RequireContainedChild(filepath.Join(app.ProjectRoot, ".uap"), app.EnvironmentLockPath); err != nil { + return fmt.Errorf("unsafe project environment lock: %w", err) + } + _, err := app.StateStore.Load() + return err +} diff --git a/cli/internal/agentpluginscli/project_host_version_test.go b/cli/internal/agentpluginscli/project_host_version_test.go new file mode 100644 index 000000000..bea24e8c2 --- /dev/null +++ b/cli/internal/agentpluginscli/project_host_version_test.go @@ -0,0 +1,68 @@ +package agentpluginscli + +import ( + "errors" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +type versionBoundaryCodex struct { + *codex.Adapter + observed *string +} + +func (adapter *versionBoundaryCodex) CheckPlanPrecondition(in clients.PlanInput, _ *domain.DeliveryPlan) error { + *adapter.observed = in.Client.Version + return errors.New("inert host-version boundary reached") +} + +// Windsurf's direct no-save path already retains the domain client. This proves +// the existing qualified facade builder forwards its own selected metadata too. +func TestProjectHostVersionCrossesExistingNoSaveFacadeBoundary(t *testing.T) { + client := fixtureClient(t, domain.ClientCodex) + client.ExecutablePath = filepath.Join(t.TempDir(), "codex") + client.Version = "selected-host-version" + f := newCLIFixture(t, []domain.DetectedClient{client}) + observed := "" + registry, err := clients.NewRegistry(&versionBoundaryCodex{Adapter: codex.New(), observed: &observed}) + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{StateRoot: filepath.Join(f.root, "facade"), Registry: registry}) + if err != nil { + t.Fatal(err) + } + f.app.Installer = engine + f.app.Lifecycle.Planner = nil + _, _, err = f.execute(false, "add", writeCLIPlugin(t), "--no-save", "--target", "codex", "--format", "json") + if err == nil || !strings.Contains(err.Error(), "inert host-version boundary reached") || observed != client.Version { + t.Fatalf("selected host evidence=%q,%v", observed, err) + } +} + +func TestProjectDependencyKnownFactsRetainOnlyUnselectedSiblings(t *testing.T) { + root := t.TempDir() + sibling := domain.DetectedClient{ClientID: domain.ClientGemini, ConfigRoot: filepath.Join(root, "gemini"), ExecutablePath: filepath.Join(root, "gemini-cli")} + adapter := dependencyInstaller{app: App{Scope: domain.ScopeProject, ProjectRoot: root}, detected: map[domain.ClientID]domain.DetectedClient{domain.ClientGemini: sibling}} + view := installer.Inspection{Installations: []installer.InspectedInstallation{{InstallationID: "selected", Bindings: []installer.InspectedBinding{{ClientID: "windsurf", BindingID: "selected-binding"}, {ClientID: "gemini", BindingID: "sibling-binding"}}}}} + request := installer.Request{ClientID: "windsurf", ClientVersion: "3000.11.205"} + if err := adapter.populateDependencyTargets(view, "selected", &request); err != nil { + t.Fatal(err) + } + if len(request.KnownTargets) != 1 || request.KnownTargets[0] != (installer.TargetFacts{ClientID: "gemini", BindingID: "sibling-binding", ConfigRoot: sibling.ConfigRoot, Executable: sibling.ExecutablePath}) { + t.Fatalf("selected/sibling facts mixed: %+v", request.KnownTargets) + } + if request.ClientVersion != "3000.11.205" || request.ClientConfigRoot != "" { + t.Fatal("primary host evidence changed") + } + delete(adapter.detected, domain.ClientGemini) + if err := adapter.populateDependencyTargets(view, "selected", &installer.Request{ClientID: "windsurf"}); err == nil { + t.Fatal("unselected sibling missing facts accepted") + } +} diff --git a/cli/internal/agentpluginscli/project_prepared_guidance_test.go b/cli/internal/agentpluginscli/project_prepared_guidance_test.go new file mode 100644 index 000000000..15935001f --- /dev/null +++ b/cli/internal/agentpluginscli/project_prepared_guidance_test.go @@ -0,0 +1,80 @@ +package agentpluginscli + +import ( + "context" + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +func TestProjectPreparedGuidancePreservesUserSDKAndAuthentication(t *testing.T) { + for _, activation := range []string{"activation_manual", "activation_prepared"} { + fact := dependencyStatusFact{Scope: domain.ScopeProject, ClientID: "grok", Facts: []string{activation, "auth_failed"}} + sdk := installer.NextAction{Kind: "inspect_sdk", Reason: "explicit SDK instruction"} + actions := dependencyNextActions(fact, sdk) + if len(actions) != 3 || actions[0].Kind != sdk.Kind || actions[0].Reason != sdk.Reason || actions[1].Kind != "review_project_activation" || actions[2].Kind != "attest_authentication" { + t.Fatalf("project/SDK/auth guidance %+v", actions) + } + if strings.Contains(actions[1].Reason, "--activation-complete") { + t.Fatal("project review offered unsupported attestation") + } + fact.Scope = domain.ScopeUser + user := dependencyNextActions(fact) + if len(user) != 2 || user[0].Kind != "attest_activation" || !strings.Contains(user[0].Reason, "--activation-complete") || user[1].Kind != "attest_authentication" { + t.Fatalf("user behavior changed %+v", user) + } + } + body, err := json.Marshal(dependencyStatusFact{Scope: domain.ScopeProject}) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(body, &fields); err != nil { + t.Fatal(err) + } + if _, exists := fields["scope"]; exists { + t.Fatal("internal guidance scope changed fact wire schema") + } +} +func TestProjectPreparedGuidanceDesiredAndRetainedScopes(t *testing.T) { + for _, desired := range []bool{false, true} { + for _, scopes := range [][2]domain.InstallScope{{domain.ScopeProject, domain.ScopeUser}, {domain.ScopeUser, domain.ScopeProject}, {domain.ScopeProject, domain.ScopeProject}, {domain.ScopeUser, domain.ScopeUser}} { + root := t.TempDir() + app := App{Scope: scopes[0], EnvironmentStoreRoot: filepath.Join(root, "content-store")} + if scopes[0] == domain.ScopeProject { + app.ProjectRoot = root + } + receipt := domain.EnvironmentReceipt{EnvironmentID: app.environmentID(), Alias: "docs", ClientID: "grok", InstallationID: "install", ClientBindingID: "binding", SourceKey: "source", PackageDigest: "sha256:" + strings.Repeat("a", 64)} + binding := installer.InspectedBinding{ClientID: "grok", BindingID: "binding", Scope: string(scopes[1]), Materialization: string(domain.MaterializationMaterialized), Activation: string(domain.ActivationPrepared), Verification: string(domain.VerificationPackageValid), TreeDigest: receipt.PackageDigest} + state := installer.Inspection{EnvironmentReceipts: []domain.EnvironmentReceipt{receipt}, Installations: []installer.InspectedInstallation{{InstallationID: "install", SourceKey: "source", Bindings: []installer.InspectedBinding{binding}}}} + lock := environment.Lock{} + if desired { + lock.Packages = []environment.LockedPackage{{Alias: "docs", Targets: []string{"grok"}, SourceKey: "source", TreeDigest: receipt.PackageDigest}} + } + facts := inspectDependencyFacts(context.Background(), app, state, lock, desired) + if len(facts) != 1 { + t.Fatalf("expected one status fact: %+v", facts) + } + project := scopes[0] == domain.ScopeProject || scopes[1] == domain.ScopeProject + want := "attest_activation" + if project { + want = "review_project_activation" + } + if len(facts[0].NextActions) != 1 || facts[0].NextActions[0].Kind != want { + t.Fatalf("desired%t scopes%v actions%+v", desired, scopes, facts[0].NextActions) + } + } + } +} +func TestProjectPreparedGuidanceMissingBindingDoesNotInventActivation(t *testing.T) { + fact := dependencyStatusFact{Scope: domain.ScopeProject, ClientID: "grok"} + appendReceiptStatus(&fact, installer.Inspection{}, domain.EnvironmentReceipt{InstallationID: "missing", ClientBindingID: "missing"}) + if len(fact.NextActions) != 0 { + t.Fatalf("missing identity manufactured activation action %+v", fact.NextActions) + } +} diff --git a/cli/internal/agentpluginscli/read.go b/cli/internal/agentpluginscli/read.go index 9b40e16c1..7882748c0 100644 --- a/cli/internal/agentpluginscli/read.go +++ b/cli/internal/agentpluginscli/read.go @@ -108,6 +108,10 @@ func newListCommand(app App, opts *options) *cobra.Command { Short: "List tracked Agent Plugins installations", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -130,6 +134,10 @@ func newInfoCommand(app App, opts *options) *cobra.Command { Short: "Show lifecycle state without exposing local paths", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -174,6 +182,10 @@ func newDoctorCommand(app App, opts *options) *cobra.Command { Short: "Read-only health check for clients, state, and open operations", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } @@ -331,7 +343,7 @@ func doctorFindings(ctx context.Context, app App, detected []domain.DetectedClie findings = append(findings, scopedFinding("unknown", "chatgpt_registration_unverified", installation, binding.ClientID, "the .app.json mapping is package-valid, but remote ChatGPT registration cannot be observed locally", "verify the mapped connection and plugin status in ChatGPT Plugins; rerun add with --activation-complete only after checking it in a new chat")) } } - if visible && client.Status == domain.DetectionDetected && bindingClient == nativeCLIRegistryOwner(bindingClient) && sharesPhysicalBackend(bindingClient) && strings.TrimSpace(client.ExecutablePath) == "" { + if visible && client.Status == domain.DetectionDetected && bindingClient == nativeCLIRegistryOwner(bindingClient, domain.InstallScope(binding.Scope)) && sharesPhysicalBackend(bindingClient, domain.InstallScope(binding.Scope)) && strings.TrimSpace(client.ExecutablePath) == "" { findings = append(findings, scopedFinding("degraded", "copilot_cli_missing", installation, binding.ClientID, "GitHub Copilot CLI is unavailable for automatic Copilot activation", "install GitHub Copilot CLI, ensure copilot is on PATH, and rerun doctor")) } switch binding.Activation { @@ -412,7 +424,7 @@ func checkManagedIntegrity(ctx context.Context, app App, client domain.DetectedC } func checkNativeProjectionIntegrity(ctx context.Context, app App, client domain.DetectedClient, target domain.DeliveryTarget, installation domain.Installation, binding domain.ClientBinding, expectedDigest string) []doctorFinding { - if !nativeConfigLifecycle(domain.ClientID(binding.ClientID)) { + if !nativeConfigLifecycle(domain.ClientID(binding.ClientID), domain.InstallScope(binding.Scope)) { return nil } if !hasOwnedNativeProjection(binding.NativeObjects) { @@ -427,7 +439,7 @@ func checkNativeProjectionIntegrity(ctx context.Context, app App, client domain. outcome, err := app.Lifecycle.Activator.Activate(ctx, domain.ActivationRequest{ Client: client, Plan: domain.DeliveryPlan{ - ClientID: domain.ClientID(binding.ClientID), Scope: domain.InstallScope(binding.Scope), + ClientID: domain.ClientID(binding.ClientID), Scope: domain.InstallScope(binding.Scope), TargetContext: target.TargetContext, NativeRegistryRoot: target.NativeRegistryRoot, PhysicalArtifactID: binding.PhysicalArtifact, DeclaredName: installation.DeclaredName, TargetRoot: target.TargetRoot, ActivePath: target.ActivePath, Components: []domain.ComponentDecision{{Kind: domain.ComponentSkill, Support: domain.SupportProjected}}, @@ -488,6 +500,10 @@ func newVersionCommand(app App, opts *options) *cobra.Command { Short: "Print the agentplugins binary version", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } diff --git a/cli/internal/agentpluginscli/read_directory.go b/cli/internal/agentpluginscli/read_directory.go index e69c04639..9146a2d86 100644 --- a/cli/internal/agentpluginscli/read_directory.go +++ b/cli/internal/agentpluginscli/read_directory.go @@ -113,7 +113,7 @@ func inspectDirectoryProduct(ctx context.Context, app App, state domain.StateFil return publicProductInspection{}, fmt.Errorf("load signed Directory: %w", err) } selection, err := domain.ResolveDirectory(bundle.Snapshot, domain.DirectoryResolveRequest{Selector: selector, Targets: targets, - Scope: domain.ScopeUser, InstallerVersion: app.Version, SchemaVersion: "1.0.0", Operation: domain.DirectoryInstall}) + Scope: app.scope(), InstallerVersion: app.Version, SchemaVersion: "1.0.0", Operation: domain.DirectoryInstall}) if err != nil { return publicProductInspection{}, err } @@ -179,9 +179,9 @@ func inspectInstalledProduct(ctx context.Context, app App, state domain.StateFil view.CurrentReleaseSequence, view.CurrentEvidence = current.ReleaseSequence, current.Evidence } } - targets := installationTargets(installation, string(domain.ScopeUser)) + targets := installationTargets(installation, string(app.scope())) selection, err := domain.ResolveDirectory(bundle.Snapshot, domain.DirectoryResolveRequest{Selector: origin.DistributionID, - Targets: targets, Scope: domain.ScopeUser, InstallerVersion: app.Version, SchemaVersion: "1.0.0", Operation: domain.DirectoryInstall}) + Targets: targets, Scope: app.scope(), InstallerVersion: app.Version, SchemaVersion: "1.0.0", Operation: domain.DirectoryInstall}) if err == nil { view.CurrentDistribution, view.CurrentRevision = selection.DistributionID, selection.Source.Revision view.CurrentRepository, view.CurrentPackagePath = selection.Source.Repository, selection.Source.Path diff --git a/cli/internal/agentpluginscli/read_reconciliation.go b/cli/internal/agentpluginscli/read_reconciliation.go index 33a5571df..84ab959ae 100644 --- a/cli/internal/agentpluginscli/read_reconciliation.go +++ b/cli/internal/agentpluginscli/read_reconciliation.go @@ -33,7 +33,7 @@ func reconcileInstalledInfo(ctx context.Context, app App, installation domain.In } } } - clients, err := detectClientsForInfoReconciliation(ctx, app.Detector, probeTargets) + clients, err := detectClientsForInfoReconciliation(ctx, app.Detector, probeTargets, app.scope()) if err != nil { return err } @@ -95,11 +95,11 @@ func bindingMatchesSelectedSurface(view publicClient, binding domain.ClientBindi func reconciliationClientsForBinding(binding domain.ClientBinding, detected map[domain.ClientID]domain.DetectedClient) (domain.DetectedClient, domain.DetectedClient, bool) { owner := domain.ClientID(binding.ClientID) - if !sharesPhysicalBackend(owner) { + if !sharesPhysicalBackend(owner, domain.InstallScope(binding.Scope)) { client, ok := detected[owner] return client, client, ok } - registryOwner := nativeCLIRegistryOwner(owner) + registryOwner := nativeCLIRegistryOwner(owner, domain.InstallScope(binding.Scope)) copilot, ok := detected[registryOwner] if !ok || copilot.Status != domain.DetectionDetected || strings.TrimSpace(copilot.ExecutablePath) == "" { return domain.DetectedClient{ClientID: owner}, domain.DetectedClient{ClientID: owner}, false @@ -118,10 +118,10 @@ type clientIdentityReconciliation struct { evidence *publicNativeDiscoveryEvidence } -func detectClientsForInfoReconciliation(ctx context.Context, detector ports.ClientDetector, targets []domain.ClientID) ([]domain.DetectedClient, error) { +func detectClientsForInfoReconciliation(ctx context.Context, detector ports.ClientDetector, targets []domain.ClientID, scopes ...domain.InstallScope) ([]domain.DetectedClient, error) { probeSet := make(map[domain.ClientID]struct{}, len(targets)) for _, target := range targets { - probeSet[nativeCLIRegistryOwner(target)] = struct{}{} + probeSet[nativeCLIRegistryOwner(target, scopes...)] = struct{}{} } probeTargets := make([]domain.ClientID, 0, len(probeSet)) for target := range probeSet { @@ -165,7 +165,7 @@ func reconcileClientIdentity(ctx context.Context, app App, installation domain.I ClientID: bindingClient.ClientID, Scope: domain.InstallScope(binding.Scope), DeclaredName: installation.DeclaredName, DeclaredVersion: declaredVersion, PhysicalArtifactID: expectedArtifact, TargetAnchor: target.TargetAnchor, TargetRoot: target.TargetRoot, ActivePath: target.ActivePath, - NativeRegistryRoot: observerClient.ConfigRoot, NativeRegistryExecutable: observerClient.ExecutablePath, + TargetContext: target.TargetContext, NativeRegistryRoot: target.NativeRegistryRoot, NativeRegistryExecutable: observerClient.ExecutablePath, } observation, observeErr := app.Lifecycle.NativeObserver.ObserveNativeIdentity(ctx, observerClient, plan, &binding) result.receiptReconciled = observation.ReceiptReconciled @@ -177,7 +177,7 @@ func reconcileClientIdentity(ctx context.Context, app App, installation domain.I return result } if observation.NativeDiscoveryAttempted { - result.evidence = nativeCommandEvidence(observerClient, installation.DeclaredName, expectedArtifact, result.clientVersion, result.nativeDiscoveryReconciled) + result.evidence = nativeCommandEvidence(observerClient, installation.DeclaredName, expectedArtifact, result.clientVersion, result.nativeDiscoveryReconciled, domain.InstallScope(binding.Scope)) } if result.state == domain.NativeIdentityManaged && !result.nativeDiscoveryReconciled && observation.NativeDiscoveryState != "" { result.state = observation.NativeDiscoveryState @@ -221,12 +221,12 @@ func hasReconciledOwnershipReceipt(binding domain.ClientBinding, physicalArtifac return latestSequence > 0 && latestDigest == digest && latestPhase == "committed" } -func nativeCommandEvidence(client domain.DetectedClient, declaredName, physicalArtifact, version string, discovered bool) *publicNativeDiscoveryEvidence { - if !sharesPhysicalBackend(client.ClientID) || +func nativeCommandEvidence(client domain.DetectedClient, declaredName, physicalArtifact, version string, discovered bool, scopes ...domain.InstallScope) *publicNativeDiscoveryEvidence { + if !sharesPhysicalBackend(client.ClientID, scopes...) || strings.TrimSpace(client.ExecutablePath) == "" || strings.TrimSpace(physicalArtifact) == "" { return nil } - cli := string(nativeCLIRegistryOwner(client.ClientID)) + cli := string(nativeCLIRegistryOwner(client.ClientID, scopes...)) return &publicNativeDiscoveryEvidence{ Basis: "native_client_command", VersionOperation: publicVersionOperation{ diff --git a/cli/internal/agentpluginscli/remove_multi.go b/cli/internal/agentpluginscli/remove_multi.go index e6220c81c..37dff12b2 100644 --- a/cli/internal/agentpluginscli/remove_multi.go +++ b/cli/internal/agentpluginscli/remove_multi.go @@ -132,9 +132,9 @@ func (session *removeManySession) buildInputs() error { session.detected[target] = client } inputs = append(inputs, usecase.RemoveInput{ - Selector: session.installationSelector, Client: client, Scope: domain.ScopeUser, + Selector: session.installationSelector, Client: client, Scope: session.app.scope(), DryRun: true, Interactive: false, ExternalUninstalled: session.opts.externalUninstalled, - BackendExecutable: backendExecutable(client, session.detected), + BackendExecutable: backendExecutable(client, session.detected, session.app.scope()), EnvironmentAuthority: authority, }) selected = append(selected, client) @@ -258,7 +258,7 @@ func (session *removeManySession) recordRetainedData() { Scope: receipt.Scope, State: receipt.State, }) } - session.result.RetainedDataAction = fmt.Sprintf("run `agentplugins remove %s --purge-data` to delete retained plugin data", session.installation.InstallationID) + session.result.RetainedDataAction = fmt.Sprintf("run `%s` to delete retained plugin data", retainedDataPurgeCommand(session.app, session.installation.InstallationID)) } func multiRemovalNeedsManualStep(result removeMultiResult) bool { diff --git a/cli/internal/agentpluginscli/repair_multi.go b/cli/internal/agentpluginscli/repair_multi.go index d161cf548..497d67cb6 100644 --- a/cli/internal/agentpluginscli/repair_multi.go +++ b/cli/internal/agentpluginscli/repair_multi.go @@ -112,7 +112,7 @@ func (session *repairManySession) collectRevisionRequests() error { } func (session *repairManySession) recordTargetRevision(target domain.ClientID) error { - binding, err := selectedRepairBinding(session.installation, target, domain.ScopeUser) + binding, err := selectedRepairBinding(session.installation, target, session.app.scope()) if err != nil { return fmt.Errorf("preflight target %s: %w; no target was changed", target, err) } @@ -126,10 +126,10 @@ func (session *repairManySession) recordTargetRevision(target domain.ClientID) e } session.targetRevision[target] = key if existing := session.requests[key]; existing != nil { - existing.targets = expandAffectedSurfaceTargets(append(existing.targets, bindingSurfaceTargets(binding)...)) + existing.targets = expandAffectedSurfaceTargets(append(existing.targets, bindingSurfaceTargets(binding)...), session.app.scope()) return nil } - request.targets = expandAffectedSurfaceTargets(bindingSurfaceTargets(binding)) + request.targets = expandAffectedSurfaceTargets(bindingSurfaceTargets(binding), session.app.scope()) session.requests[key] = &request return nil } @@ -273,10 +273,10 @@ func (session *repairManySession) targetInput(target domain.ClientID) (usecase.A } client := session.detected[target] return usecase.AddInput{ - Envelope: clientPackage.envelope, Client: client, Scope: domain.ScopeUser, + Envelope: clientPackage.envelope, Client: client, Scope: session.app.scope(), Interactive: false, Hints: clientPackage.hints, InstallationID: session.installation.InstallationID, - BackendExecutable: backendExecutable(client, session.detected), + BackendExecutable: backendExecutable(client, session.detected, session.app.scope()), OriginMode: loaded.origin, DirectoryResolution: cloneDirectoryOrigin(loaded.directory), DistributionSuspended: loaded.distributionSuspended, ReleaseRevoked: loaded.releaseRevoked, EnvironmentAuthority: authority, diff --git a/cli/internal/agentpluginscli/retained_data_guidance.go b/cli/internal/agentpluginscli/retained_data_guidance.go new file mode 100644 index 000000000..f1c96e1c7 --- /dev/null +++ b/cli/internal/agentpluginscli/retained_data_guidance.go @@ -0,0 +1,13 @@ +package agentpluginscli + +import "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + +// Follow-up commands retain the already validated selection even when copied +// into a different working directory. User command wording stays unchanged. +func retainedDataPurgeCommand(app App, installationID string) string { + command := "agentplugins remove " + installationID + " --purge-data" + if app.scope() == domain.ScopeProject { + command += " --scope project --project-root " + quoteResumeArgument(app.ProjectRoot) + } + return command +} diff --git a/cli/internal/agentpluginscli/root.go b/cli/internal/agentpluginscli/root.go index 3ec1b9cc7..dc77035c8 100644 --- a/cli/internal/agentpluginscli/root.go +++ b/cli/internal/agentpluginscli/root.go @@ -22,7 +22,8 @@ func NewRoot(app App) *cobra.Command { root.SetErr(app.errorOutput()) flags := root.PersistentFlags() flags.StringVar(&opts.target, "target", "", "target client(s), comma-separated: "+supportedTargetHelp()) - flags.StringVar(&opts.scope, "scope", "user", "installation scope (user only in this release)") + flags.StringVar(&opts.scope, "scope", "user", "installation scope: user or project") + flags.StringVar(&opts.projectRoot, "project-root", "", "existing project directory (project scope only; default: current directory)") flags.BoolVar(&opts.dryRun, "dry-run", false, "show the exact plan without changes") flags.StringVar(&opts.format, "format", "human", "output format: human or json") flags.BoolVar(&opts.plain, "plain", false, "use accessible line prompts (independent of color)") @@ -60,8 +61,8 @@ func validateCommonOptions(opts *options) error { if opts.scope != "user" && opts.scope != "project" { return fmt.Errorf("--scope must be user or project") } - if opts.scope == "project" { - return fmt.Errorf("--scope project is not supported by the current client adapters; the public CLI supports user scope only") + if opts.scope != "project" && opts.projectRoot != "" { + return fmt.Errorf("--project-root requires --scope project") } if _, err := parseTargetOption(opts.target); err != nil { return err diff --git a/cli/internal/agentpluginscli/saved_add.go b/cli/internal/agentpluginscli/saved_add.go index bd439af62..e580d0e14 100644 --- a/cli/internal/agentpluginscli/saved_add.go +++ b/cli/internal/agentpluginscli/saved_add.go @@ -187,7 +187,7 @@ func rejectKnownOwnedAdd(app App, source string) error { key = "github:" + strings.ToLower(matches[1]) + ":" + matches[3] } for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID != environment.EnvironmentUserDefault { + if receipt.EnvironmentID != app.environmentID() { continue } if key != "" && receipt.SourceKey == key || isDirectorySelector(source) && receipt.SourceKey == "directory:"+strings.ToLower(source) { @@ -223,6 +223,10 @@ func savedAddTargets(cmd *cobra.Command, app App, opts *options, source string, return nil, err } } + return validateSavedAddTargets(app, opts, source, targets) +} + +func validateSavedAddTargets(app App, opts *options, source string, targets []domain.ClientID) ([]domain.ClientID, error) { if len(targets) == 0 { return nil, errors.New("automated installation requires --target") } @@ -234,7 +238,7 @@ func savedAddTargets(cmd *cobra.Command, app App, opts *options, source string, return nil, fmt.Errorf("target %s cannot be saved; retry agentplugins add %s --target %s --no-save", target, source, opts.target) } } - if opts.scope != "user" { + if opts.scope != "user" && opts.scope != "project" { return nil, errors.New("saved dependencies support user scope only") } @@ -360,7 +364,7 @@ func runSavedAdd(cmd *cobra.Command, app App, opts *options, source string, targ } } previous := seedSavedAdd(snapshot, requirement, pin) - locked, err := (environment.Coordinator{Resolver: dependencyResolver{app: app, requirements: requirements, dryRun: opts.dryRun}}).Resolve(cmd.Context(), requirements, previous, "") + locked, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: dependencyResolver{app: app, requirements: requirements, dryRun: opts.dryRun}}).Resolve(cmd.Context(), requirements, previous, "") if err != nil { return err } diff --git a/cli/internal/agentpluginscli/saved_add_e2e_test.go b/cli/internal/agentpluginscli/saved_add_e2e_test.go index ba2bf1eaa..17b897d65 100644 --- a/cli/internal/agentpluginscli/saved_add_e2e_test.go +++ b/cli/internal/agentpluginscli/saved_add_e2e_test.go @@ -95,6 +95,15 @@ func newSavedJourney(t *testing.T, repo string) savedJourney { f.app.EnvironmentStoreRoot = filepath.Join(root, "store") f.app.SecurityEvaluator = dependencyTestScanner{} source := &savedJourneySource{Acquirer: sourceacquisition.Acquirer{TempRoot: t.TempDir(), URLForRepo: func(string) string { return repo }}} + diagnostic := &dependencyGitDiagnosticRunner{delegate: sourceacquisition.OSRunner{}, report: func(phase, class, status string) { + t.Logf("saved-source Git diagnostic: phase=%s class=%s exit_status=%s", phase, class, status) + }} + source.Runner = diagnostic + t.Cleanup(func() { + if t.Failed() { + t.Logf("saved-source Git diagnostic: delegated_failures=%d", diagnostic.failures.Load()) + } + }) f.app.SourceAcquirer = source return savedJourney{cli: f, runner: runner, source: source} } diff --git a/cli/internal/agentpluginscli/saved_lifecycle.go b/cli/internal/agentpluginscli/saved_lifecycle.go index 7ab3b7b55..1086e2551 100644 --- a/cli/internal/agentpluginscli/saved_lifecycle.go +++ b/cli/internal/agentpluginscli/saved_lifecycle.go @@ -17,8 +17,8 @@ import ( // resolveSavedAlias resolves intent and live ownership together. A name never // silently selects an alias when an unrelated installation has the same name. -func resolveSavedAlias(state domain.StateFileV2, requirements environment.Requirements, selector string) (string, *domain.Installation, error) { - aliases := matchingSavedAliases(state, requirements, selector) +func resolveSavedAlias(state domain.StateFileV2, requirements environment.Requirements, selector string, environmentIDs ...string) (string, *domain.Installation, error) { + aliases := matchingSavedAliases(state, requirements, selector, environmentIDs...) if len(aliases) == 0 { return "", nil, nil } @@ -29,7 +29,7 @@ func resolveSavedAlias(state domain.StateFileV2, requirements environment.Requir for key := range aliases { alias = key } - installation, err := savedAliasInstallation(state, alias, selector) + installation, err := savedAliasInstallation(state, alias, selector, environmentIDs...) return alias, installation, err } @@ -37,12 +37,12 @@ func ambiguousSavedSelector(selector string) error { return fmt.Errorf("ambiguous dependency %q; specify an installation ID", selector) } -func matchingSavedAliases(state domain.StateFileV2, requirements environment.Requirements, selector string) map[string]bool { +func matchingSavedAliases(state domain.StateFileV2, requirements environment.Requirements, selector string, environmentIDs ...string) map[string]bool { aliases := map[string]bool{} if requirement, ok := requirements.Dependencies[selector]; ok { source := mustSourceKey(requirement) for _, receipt := range state.EnvironmentReceipts { - if source != "" && receipt.EnvironmentID == environment.EnvironmentUserDefault && receipt.SourceKey == source { + if source != "" && receipt.EnvironmentID == selectedEnvironmentID(environmentIDs) && receipt.SourceKey == source { aliases[receipt.Alias] = true } } @@ -55,7 +55,7 @@ func matchingSavedAliases(state domain.StateFileV2, requirements environment.Req names[installation.InstallationID] = installation.DeclaredName } for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID != environment.EnvironmentUserDefault { + if receipt.EnvironmentID != selectedEnvironmentID(environmentIDs) { continue } if receipt.Alias == selector || receipt.InstallationID == selector || names[receipt.InstallationID] == selector { @@ -65,20 +65,20 @@ func matchingSavedAliases(state domain.StateFileV2, requirements environment.Req return aliases } -func ownsSavedAlias(state domain.StateFileV2, installationID, alias string) bool { +func ownsSavedAlias(state domain.StateFileV2, installationID, alias string, environmentIDs ...string) bool { for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID == environment.EnvironmentUserDefault && receipt.InstallationID == installationID && receipt.Alias == alias { + if receipt.EnvironmentID == selectedEnvironmentID(environmentIDs) && receipt.InstallationID == installationID && receipt.Alias == alias { return true } } return false } -func savedAliasInstallation(state domain.StateFileV2, alias, selector string) (*domain.Installation, error) { +func savedAliasInstallation(state domain.StateFileV2, alias, selector string, environmentIDs ...string) (*domain.Installation, error) { var selected *domain.Installation for index := range state.Installations { installation := &state.Installations[index] - owned := ownsSavedAlias(state, installation.InstallationID, alias) + owned := ownsSavedAlias(state, installation.InstallationID, alias, environmentIDs...) if (installation.InstallationID == selector || installation.DeclaredName == selector) && !owned { return nil, ambiguousSavedSelector(selector) } @@ -93,10 +93,10 @@ func savedAliasInstallation(state domain.StateFileV2, alias, selector string) (* return selected, nil } -func savedAliasTargets(requirements environment.Requirements, state domain.StateFileV2, alias string) []string { +func savedAliasTargets(requirements environment.Requirements, state domain.StateFileV2, alias string, environmentIDs ...string) []string { declaration := alias for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID == environment.EnvironmentUserDefault && receipt.Alias == alias { + if receipt.EnvironmentID == selectedEnvironmentID(environmentIDs) && receipt.Alias == alias { for key, requirement := range requirements.Dependencies { if receipt.SourceKey != "" && mustSourceKey(requirement) == receipt.SourceKey { declaration = key @@ -111,7 +111,7 @@ func savedAliasTargets(requirements environment.Requirements, state domain.State } } for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID == environment.EnvironmentUserDefault && receipt.Alias == alias { + if receipt.EnvironmentID == selectedEnvironmentID(environmentIDs) && receipt.Alias == alias { targets[receipt.ClientID] = true } } @@ -192,7 +192,7 @@ func runSavedLifecycle(cmd *cobra.Command, app App, opts *options, selector, ope if err := runSavedEnvironment(cmd, preview, opts, snapshot, requirements, locked, manifest, []string{selection.alias}, nil); err != nil { return true, err } - return true, selection.renderNextAction(cmd, opts, requirements, locked, operation) + return true, selection.renderNextAction(cmd, app, opts, requirements, locked, operation) } func selectSavedLifecycle(app App, opts *options, selector string) (savedLifecycleSelection, error) { @@ -206,7 +206,7 @@ func selectSavedLifecycle(app App, opts *options, selector string) (savedLifecyc if err != nil && !os.IsNotExist(err) { return selection, err } - selection.alias, selection.installation, err = resolveSavedAlias(state, requirements, selector) + selection.alias, selection.installation, err = resolveSavedAlias(state, requirements, selector, app.environmentID()) if err != nil { return selection, err } @@ -214,12 +214,12 @@ func selectSavedLifecycle(app App, opts *options, selector string) (savedLifecyc if err != nil || selection.alias == "" { return selection, err } - if opts.scope != "" && opts.scope != "user" { + if opts.scope != "" && opts.scope != "user" && opts.scope != "project" { return selection, errors.New("saved dependencies support user scope only") } - targets := savedAliasTargets(requirements, state, selection.alias) + targets := savedAliasTargets(requirements, state, selection.alias, app.environmentID()) if selection.declaration != "" { - targets = savedAliasTargets(requirements, state, selection.declaration) + targets = savedAliasTargets(requirements, state, selection.declaration, app.environmentID()) } if selection.retained != nil { targets = selection.retained.Targets @@ -265,7 +265,7 @@ func (selection savedLifecycleSelection) candidate(cmd *cobra.Command, app App, switch operation { case "remove": requirements = removeSavedTargets(requirements, dependencyRootKey(requirements, snapshot.Lock, selection.alias), selection.targets) - locked, err = (environment.Coordinator{Resolver: dependencyResolver{app: app, requirements: requirements, dryRun: opts.dryRun}}).Resolve(cmd.Context(), requirements, snapshot.Lock, "") + locked, err = (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: dependencyResolver{app: app, requirements: requirements, dryRun: opts.dryRun}}).Resolve(cmd.Context(), requirements, snapshot.Lock, "") case "update": requirements, locked, err = selection.update(cmd, app, opts, snapshot) default: @@ -281,7 +281,7 @@ func (selection savedLifecycleSelection) candidate(cmd *cobra.Command, app App, func (selection savedLifecycleSelection) update(cmd *cobra.Command, app App, opts *options, snapshot environmentSnapshot) (environment.Requirements, environment.Lock, error) { requirements := snapshot.Requirements key := dependencyRootKey(requirements, snapshot.Lock, selection.alias) - desired := savedAliasTargets(requirements, domain.StateFileV2{}, key) + desired := savedAliasTargets(requirements, domain.StateFileV2{}, key, app.environmentID()) if strings.TrimSpace(opts.target) != "" && !slices.Equal(selection.targets, desired) { return requirements, snapshot.Lock, fmt.Errorf("update %s must select every target of the alias", selection.alias) } @@ -299,11 +299,11 @@ func (selection savedLifecycleSelection) update(cmd *cobra.Command, app App, opt if (requirement.Git != nil && requirement.Git.Selector.Kind == environment.SelectorCommit) || (requirement.Archive != nil && requirement.Archive.SHA256 != "") { updateAlias = "" } - locked, err := (environment.Coordinator{Resolver: dependencyResolver{requirements: requirements, app: app, dryRun: opts.dryRun}}).Resolve(cmd.Context(), requirements, snapshot.Lock, updateAlias) + locked, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: dependencyResolver{requirements: requirements, app: app, dryRun: opts.dryRun}}).Resolve(cmd.Context(), requirements, snapshot.Lock, updateAlias) return requirements, locked, err } -func (selection savedLifecycleSelection) renderNextAction(cmd *cobra.Command, opts *options, requirements environment.Requirements, locked environment.Lock, operation string) error { +func (selection savedLifecycleSelection) renderNextAction(cmd *cobra.Command, app App, opts *options, requirements environment.Requirements, locked environment.Lock, operation string) error { if opts.format == "json" { return nil } @@ -325,8 +325,8 @@ func (selection savedLifecycleSelection) renderNextAction(cmd *cobra.Command, op _, err := fmt.Fprintln(cmd.OutOrStdout(), "Exact archive checksum retained; choose a new checksum in uap.toml") return err } - if operation == "remove" && selection.installation != nil && len(savedAliasTargets(requirements, domain.StateFileV2{}, selection.alias)) == 0 { - _, err := fmt.Fprintf(cmd.OutOrStdout(), "PLUGIN_DATA preserved; after removal completes, run agentplugins remove %s --purge-data to delete retained data\n", selection.installation.InstallationID) + if operation == "remove" && selection.installation != nil && len(savedAliasTargets(requirements, domain.StateFileV2{}, selection.alias, app.environmentID())) == 0 { + _, err := fmt.Fprintf(cmd.OutOrStdout(), "PLUGIN_DATA preserved; after removal completes, run %s to delete retained data\n", retainedDataPurgeCommand(app, selection.installation.InstallationID)) return err } return nil @@ -363,7 +363,7 @@ func updateSavedRegistry(cmd *cobra.Command, app App, opts *options, snapshot en } // Validate the authoritative old graph before rebuilding edges from the new // immutable manifests. Replacing a node inside old edges creates a false conflict. - locked, err := (environment.Coordinator{Resolver: dependencyResolver{app: app, dryRun: opts.dryRun, requirements: requirements}}).Resolve(cmd.Context(), requirements, snapshot.Lock, "") + locked, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: dependencyResolver{app: app, dryRun: opts.dryRun, requirements: requirements}}).Resolve(cmd.Context(), requirements, snapshot.Lock, "") return requirements, locked, err } diff --git a/cli/internal/agentpluginscli/search.go b/cli/internal/agentpluginscli/search.go index e4636e483..75438e72a 100644 --- a/cli/internal/agentpluginscli/search.go +++ b/cli/internal/agentpluginscli/search.go @@ -69,6 +69,10 @@ func newSearchCommand(app App, opts *options) *cobra.Command { Short: "Search signed Agent Plugin discovery metadata without downloading packages", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } diff --git a/cli/internal/agentpluginscli/skills.go b/cli/internal/agentpluginscli/skills.go index 8735981d7..4770099f0 100644 --- a/cli/internal/agentpluginscli/skills.go +++ b/cli/internal/agentpluginscli/skills.go @@ -60,6 +60,22 @@ func (selection *skillsAddOptions) run(cmd *cobra.Command, app App, opts *option if err := selection.validate(opts, interactive); err != nil { return renderSkillsCommandIssue(cmd, opts, err) } + var err error + app, err = app.forOptions(opts) + if err != nil { + return renderSkillsCommandIssue(cmd, opts, err) + } + requested, err := parseTargetOption(opts.target) + if err != nil { + return renderSkillsCommandIssue(cmd, opts, err) + } + targets := make([]string, 0, len(requested)) + for _, target := range requested { + targets = append(targets, string(target)) + } + if err := app.checkSkillsContext(targets); err != nil { + return renderSkillsCommandIssue(cmd, opts, err) + } if err := cmd.Context().Err(); err != nil { return err } @@ -82,7 +98,7 @@ func (selection *skillsAddOptions) run(cmd *cobra.Command, app App, opts *option return err } defer session.cleanup() - browser := selection.ui || interactive && !selection.noUI && !opts.dryRun && len(selection.names) == 0 && !request.PathSet && !session.source.ExactPath && opts.target == "" + browser := selection.useBrowserPicker(interactive, opts, request.PathSet, session.source.ExactPath) if browser { handled, err := runSkillsBrowser(cmd, app, opts, session, selection.ui) if handled || err != nil { @@ -91,6 +107,10 @@ func (selection *skillsAddOptions) run(cmd *cobra.Command, app App, opts *option } return runSkillsTerminal(cmd, app, opts, session, selection.names, interactive, selection.yes) } +func (selection *skillsAddOptions) useBrowserPicker(interactive bool, opts *options, pathSet, exactPath bool) bool { + return selection.ui || interactive && !selection.noUI && !opts.dryRun && len(selection.names) == 0 && !pathSet && !exactPath && opts.target == "" +} + func runSkillsBrowser(cmd *cobra.Command, app App, opts *options, session *skillsCatalogSession, explicit bool) (bool, error) { if !skillspicker.Bundled() { if explicit { diff --git a/cli/internal/agentpluginscli/skills_batch.go b/cli/internal/agentpluginscli/skills_batch.go index 89f4aff70..b3a6108ad 100644 --- a/cli/internal/agentpluginscli/skills_batch.go +++ b/cli/internal/agentpluginscli/skills_batch.go @@ -60,10 +60,20 @@ func (p *PreparedSkillsBatch) Check(ctx context.Context) error { return p.check(ctx) } -func (p *PreparedSkillsBatch) check(ctx context.Context) error { +func (p *PreparedSkillsBatch) checkInputs() error { + if err := p.app.checkSkillsContext(nil); err != nil { + return fmt.Errorf("%w: %w", ErrSkillsPlanStale, err) + } if err := p.request.checkInputs(); err != nil { return fmt.Errorf("%w: %w", ErrSkillsPlanStale, err) } + return nil +} + +func (p *PreparedSkillsBatch) check(ctx context.Context) error { + if err := p.checkInputs(); err != nil { + return err + } actions, detected, paths, err := prepareSkillsBindings(ctx, p.preview, p.request.requirements, p.request.lock, p.request.aliases, p.request.prepared) if err != nil { return fmt.Errorf("%w: %w", ErrSkillsPlanStale, err) @@ -92,6 +102,10 @@ func (p *PreparedSkillsBatch) Apply(ctx context.Context) (SkillsBatchResult, err if p.closed || p.applied { return result, errors.New("skill plan closed or already applied") } + // Check the original authority before creating a lock in the selected root. + if err := p.checkInputs(); err != nil { + return result, err + } unlock, err := (processlock.Lock{Path: p.app.EnvironmentLockPath}).Acquire(ctx) if err != nil { return result, err @@ -103,12 +117,15 @@ func (p *PreparedSkillsBatch) Apply(ctx context.Context) (SkillsBatchResult, err if err = ensureDependencyStore(ctx, p.app, p.request.selectedLock()); err != nil { return result, err } + if err = p.checkInputs(); err != nil { + return result, err + } if err = p.request.publish(); err != nil { return result, err } p.applied = true - adapter := &dependencyInstaller{app: p.app, opts: &options{}, detected: p.request.detected, beforeApply: p.request.checkInputs} - results, applyErr := (environment.Coordinator{Mutator: adapter}).ApplySync(ctx, environment.PreparedSync{EnvironmentID: environment.EnvironmentUserDefault, ManifestDigest: p.request.lock.ManifestDigest, Bindings: p.request.prepared}, true) + adapter := &dependencyInstaller{app: p.app, opts: p.app.skillsOptions(), detected: p.request.detected, beforeApply: p.checkInputs} + results, applyErr := (environment.Coordinator{EnvironmentID: p.app.environmentID(), Mutator: adapter}).ApplySync(ctx, environment.PreparedSync{EnvironmentID: p.app.environmentID(), ManifestDigest: p.request.lock.ManifestDigest, Bindings: p.request.prepared}, true) cursor := 0 failedItem := false for _, action := range p.actions { diff --git a/cli/internal/agentpluginscli/skills_batch_prepare.go b/cli/internal/agentpluginscli/skills_batch_prepare.go index a9629695d..b5402b781 100644 --- a/cli/internal/agentpluginscli/skills_batch_prepare.go +++ b/cli/internal/agentpluginscli/skills_batch_prepare.go @@ -17,6 +17,9 @@ func (app App) PrepareSkills(ctx context.Context, input SkillsBatchRequest) (pla if len(input.Selections) == 0 || len(input.TargetIDs) == 0 { return nil, errors.New("explicit skills and targets required") } + if err := app.checkSkillsContext(input.TargetIDs); err != nil { + return nil, err + } preview, cleanup, err := dependencyPreviewApp(app, &options{dryRun: true}) if err != nil { return nil, err @@ -58,7 +61,7 @@ func (candidate *skillsBatchCandidate) resolve(ctx context.Context, app App) (en if err := validateDependencyTargets(app, candidate.requirements); err != nil { return environment.Lock{}, nil, nil, err } - locked, err := (environment.Coordinator{Resolver: dependencyResolver{app: app, requirements: candidate.requirements, dryRun: true}}).Resolve(ctx, candidate.requirements, candidate.previous, "") + locked, err := (environment.Coordinator{EnvironmentID: app.environmentID(), Resolver: dependencyResolver{app: app, requirements: candidate.requirements, dryRun: true}}).Resolve(ctx, candidate.requirements, candidate.previous, "") if err != nil { return environment.Lock{}, nil, nil, err } @@ -107,11 +110,11 @@ func prepareSkillsBindings(ctx context.Context, app App, requirements environmen if view.Recovery.Required { return nil, nil, nil, installer.ErrRecoveryRequired } - observed, err := environment.Observe(view) + observed, err := environment.ObserveForEnvironment(view, app.environmentID()) if err != nil { return nil, nil, nil, err } - prepared, err := (environment.Coordinator{}).PrepareSyncFor(requirements, lock, observed, aliases, nil) + prepared, err := (environment.Coordinator{EnvironmentID: app.environmentID()}).PrepareSyncFor(requirements, lock, observed, aliases, nil) if err != nil { return nil, nil, nil, err } @@ -123,7 +126,7 @@ func prepareSkillsBindings(ctx context.Context, app App, requirements environmen return nil, nil, nil, err } paths := skillBindingPaths(view, prepared.Bindings) - adapter := &dependencyInstaller{app: app, opts: &options{}, detected: detected, previewPaths: paths} + adapter := &dependencyInstaller{app: app, opts: app.skillsOptions(), detected: detected, previewPaths: paths} if len(warnings) > 0 { adapter.previewWarnings = warnings[0] } diff --git a/cli/internal/agentpluginscli/skills_picker.go b/cli/internal/agentpluginscli/skills_picker.go index 3a7ec5c1c..3fcfedbb3 100644 --- a/cli/internal/agentpluginscli/skills_picker.go +++ b/cli/internal/agentpluginscli/skills_picker.go @@ -61,6 +61,9 @@ func (app App) skillSourceAcquirer(scratch string) (skillsource.Acquirer, error) return skillsource.Acquirer{Acquirer: a}, nil } func (app App) openSkillsCatalog(ctx context.Context, input SkillsPickerRequest) (*skillsCatalogSession, error) { + if err := app.checkSkillsContext(nil); err != nil { + return nil, err + } scratch, err := os.MkdirTemp("", "agentplugins-skills-") if err != nil { return nil, err @@ -198,6 +201,9 @@ func (s *skillsCatalogSession) preview(ctx context.Context, id string) (skillspi return preview, nil } func (app App) skillsTargets(ctx context.Context) ([]skillspicker.Target, error) { + if err := app.checkSkillsContext(nil); err != nil { + return nil, err + } if app.Detector == nil { return nil, errors.New("client detector unavailable") } @@ -211,19 +217,34 @@ func (app App) skillsTargets(ctx context.Context) ([]skillspicker.Target, error) } targets := []skillspicker.Target{} for _, id := range domain.SupportedClientIDs() { - if !supportsDependencyTarget(app, string(id)) { + supported := supportsDependencyTarget(app, string(id)) + if !supported && app.scope() != domain.ScopeProject { continue } definition, _ := domain.ClientDefinitionFor(id) client, ok := byID[id] - available := ok && client.DetectionError == nil && client.Status == domain.DetectionDetected - target := skillspicker.Target{Id: string(id), Label: definition.DisplayName, Scope: "user", DisplayPath: client.ConfigRoot, Available: available} - if !available { + available := supported && ok && client.DetectionError == nil && client.Status == domain.DetectionDetected + target := skillspicker.Target{Id: string(id), Label: definition.DisplayName, Scope: string(app.scope()), Available: available} + if app.scope() == domain.ScopeUser { + target.DisplayPath = client.ConfigRoot + } + if !supported { + reason := "This client does not support project installation" + target.Reason = &reason + } else if !available { reason := "This client is not detected; install or configure it before selecting it" if client.DetectionError != nil { reason = "Client configuration could not be inspected safely" } target.Reason = &reason + } else { + path, err := app.skillsTargetPath(client) + if err != nil { + reason := "Client project configuration could not be inspected safely" + target.Available, target.Reason = false, &reason + } else { + target.DisplayPath = path + } } targets = append(targets, target) } diff --git a/cli/internal/agentpluginscli/skills_scope.go b/cli/internal/agentpluginscli/skills_scope.go new file mode 100644 index 000000000..fa376c48d --- /dev/null +++ b/cli/internal/agentpluginscli/skills_scope.go @@ -0,0 +1,59 @@ +package agentpluginscli + +import ( + "errors" + "fmt" + "os" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" +) + +// The public picker and batch APIs enforce the same scope boundary as CLI entry. +// In particular, a disposable preview cannot replace the original namespace guard. +func (app App) checkSkillsContext(targets []string) error { + if app.scope() == domain.ScopeProject { + root, err := canonicalProjectRoot(app.ProjectRoot) + if err != nil { + return err + } + if root != app.ProjectRoot { + return fmt.Errorf("skills project root must be canonical") + } + } + if err := app.checkProjectNamespace(); err != nil { + return err + } + // A first Skills add creates the manifest; an existing declaration still + // needs scope qualification before any new source is acquired. + if err := app.preflightProjectDependencyTargets(); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + requirements := environment.Requirements{Targets: map[string][]string{}} + for _, target := range targets { + requirements.Targets[target] = nil + } + return validateDependencyTargets(app, requirements) +} + +func (app App) skillsOptions() *options { + return &options{scope: string(app.scope()), projectRoot: app.ProjectRoot} +} + +func (app App) skillsTargetPath(client domain.DetectedClient) (string, error) { + if app.scope() != domain.ScopeProject { + return client.ConfigRoot, nil + } + target, err := domain.NewTargetContext(domain.ScopeProject, app.ProjectRoot, client.ConfigRoot) + if err != nil { + return "", err + } + layout, ok := clients.As[clients.ProjectTargetLayout](app.ClientRegistry, client.ClientID) + if !ok { + return "", fmt.Errorf("%s does not support project scope", client.ClientID) + } + definition, _ := domain.ClientDefinitionFor(client.ClientID) + _, root, err := layout.ProjectTargetRoot(client, definition.Capabilities.PackageMode, app.ManagedRoot, target) + return root, err +} diff --git a/cli/internal/agentpluginscli/source.go b/cli/internal/agentpluginscli/source.go index adea48893..b12a88e44 100644 --- a/cli/internal/agentpluginscli/source.go +++ b/cli/internal/agentpluginscli/source.go @@ -497,9 +497,9 @@ func (app App) acquireDirectory(ctx context.Context, selector string, request pa resolveSelector = request.Selector } productID, _ := directorySelectorProductID(bundle.Snapshot, resolveSelector) - affectedTargets := expandAffectedSurfaceTargets(request.Targets) + affectedTargets := expandAffectedSurfaceTargets(request.Targets, app.scope()) resolveRequest := domain.DirectoryResolveRequest{ - Selector: resolveSelector, Targets: affectedTargets, Scope: domain.ScopeUser, + Selector: resolveSelector, Targets: affectedTargets, Scope: app.scope(), InstallerVersion: app.Version, ClientVersions: environment.ClientVersions, OS: environment.OS, Architecture: environment.Architecture, DependencyIdentity: environment.DependencyIdentity, SchemaVersion: "1.0.0", Operation: operation, Recorded: request.Recorded, @@ -508,7 +508,7 @@ func (app App) acquireDirectory(ctx context.Context, selector string, request pa preparingChatGPT := false var retainedMapping *domain.ChatGPTLocalMapping installation, _ := locallyMatchedInstallation(state, resolveSelector) - intents := lifecycleInstallIntents(installation, "user", nil) + intents := lifecycleInstallIntents(installation, string(app.scope()), nil) for _, target := range affectedTargets { if requiresPersonalMapping(target) && (productID == "context7" || app.chatGPTPreparation || intents[target] == domain.InstallIntentPrepare) { preparingChatGPT = true diff --git a/cli/internal/agentpluginscli/state_migration.go b/cli/internal/agentpluginscli/state_migration.go index 4fed7b3b8..81baf9aa9 100644 --- a/cli/internal/agentpluginscli/state_migration.go +++ b/cli/internal/agentpluginscli/state_migration.go @@ -17,6 +17,13 @@ func newMigrateStateCommand(app App, opts *options) *cobra.Command { Short: "Explicitly migrate legacy plugin-kit-ai state into Agent Plugins state", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } + if err := rejectProjectCommand(app, "migrate-state"); err != nil { + return err + } if err := validateCommonOptions(opts); err != nil { return err } diff --git a/cli/internal/agentpluginscli/update_all.go b/cli/internal/agentpluginscli/update_all.go index 993aea51a..3e24f349c 100644 --- a/cli/internal/agentpluginscli/update_all.go +++ b/cli/internal/agentpluginscli/update_all.go @@ -11,7 +11,6 @@ import ( "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/directoryv1" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" - "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment" ) type updateAllInstallation struct { @@ -90,7 +89,7 @@ func (session *updateAllSession) load() (bool, error) { session.installations = installations session.ownedAliases = map[string][]string{} for _, receipt := range state.EnvironmentReceipts { - if receipt.EnvironmentID != environment.EnvironmentUserDefault { + if receipt.EnvironmentID != session.app.environmentID() { continue } aliases := session.ownedAliases[receipt.InstallationID] diff --git a/cli/internal/agentpluginscli/update_multi.go b/cli/internal/agentpluginscli/update_multi.go index b516c03fd..1a93debde 100644 --- a/cli/internal/agentpluginscli/update_multi.go +++ b/cli/internal/agentpluginscli/update_multi.go @@ -126,7 +126,7 @@ func loadUpdatePackage(ctx context.Context, app App, opts *options, installation if err != nil { return nil, nil, loadedPackage{}, err } - installedClients, err := preflightInstalledBindings(installedBindingTargets(installation, opts.scope), detected) + installedClients, err := preflightInstalledBindings(installedBindingTargets(installation, opts.scope), detected, app.scope()) if err != nil { return nil, nil, loadedPackage{}, err } @@ -219,8 +219,8 @@ func (prepared *preparedUpdateMany) clientInput(client domain.DetectedClient, de return usecase.AddInput{}, err } return usecase.AddInput{ - Envelope: clientPackage.envelope, Client: client, Scope: domain.ScopeUser, - Interactive: false, Hints: clientPackage.hints, BackendExecutable: backendExecutable(client, detected), + Envelope: clientPackage.envelope, Client: client, Scope: scopeFromProjectRoot(prepared.service.ProjectRoot), + Interactive: false, Hints: clientPackage.hints, BackendExecutable: backendExecutable(client, detected, scopeFromProjectRoot(prepared.service.ProjectRoot)), OriginMode: prepared.loaded.origin, DirectoryResolution: cloneDirectoryOrigin(prepared.loaded.directory), DistributionSuspended: prepared.loaded.distributionSuspended, ReleaseRevoked: prepared.loaded.releaseRevoked, EnvironmentAuthority: authority, @@ -372,7 +372,7 @@ func bindingSurfaceTargets(binding domain.ClientBinding) []domain.ClientID { // preflight can heal omissions instead of silently perpetuating them. values := append([]string(nil), binding.AffectedSurfaces...) values = append(values, binding.ClientID) - values = appendBackendSiblings(values, domain.ClientID(binding.ClientID)) + values = appendBackendSiblings(values, domain.ClientID(binding.ClientID), domain.InstallScope(binding.Scope)) result := make([]domain.ClientID, 0, len(values)) seen := map[domain.ClientID]struct{}{} for _, value := range values { @@ -386,8 +386,8 @@ func bindingSurfaceTargets(binding domain.ClientBinding) []domain.ClientID { return result } -func expandAffectedSurfaceTargets(targets []domain.ClientID) []domain.ClientID { - result := expandBackendSiblingTargets(targets) +func expandAffectedSurfaceTargets(targets []domain.ClientID, scopes ...domain.InstallScope) []domain.ClientID { + result := expandBackendSiblingTargets(targets, scopes...) seen := make(map[domain.ClientID]struct{}, len(result)) unique := result[:0] for _, target := range result { diff --git a/cli/internal/agentpluginscli/validate.go b/cli/internal/agentpluginscli/validate.go index a70eb121a..570182015 100644 --- a/cli/internal/agentpluginscli/validate.go +++ b/cli/internal/agentpluginscli/validate.go @@ -49,6 +49,10 @@ func newValidateCommand(app App, opts *options) *cobra.Command { Short: "Validate one Agent Plugins package without installing it", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + app, scopeErr := app.forOptions(opts) + if scopeErr != nil { + return scopeErr + } if err := validateCommonOptions(opts); err != nil { return err } diff --git a/cli/internal/authoring/bootstrap/process_linux_test.go b/cli/internal/authoring/bootstrap/process_linux_test.go index ceb99933a..81acef03d 100644 --- a/cli/internal/authoring/bootstrap/process_linux_test.go +++ b/cli/internal/authoring/bootstrap/process_linux_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap" + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" "golang.org/x/sys/unix" ) @@ -120,6 +121,7 @@ func TestFinalVerificationToLaunchReplacementUsesCapturedStaging(t *testing.T) { } func TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant(t *testing.T) { + requireOrdinaryBootstrapPreflight(t) root, project := generated(t) plan, err := (bootstrap.Service{}).Plan(root, project) if err != nil { @@ -332,6 +334,7 @@ func TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant(t *testing } func TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment(t *testing.T) { + requireOrdinaryBootstrapPreflight(t) root, project := generated(t) plan, err := (bootstrap.Service{}).Plan(root, project) if err != nil { @@ -393,6 +396,7 @@ test -z "${npm_config_proxy:-}" } func TestDefaultRunnerCancellationStopsDescendants(t *testing.T) { + requireOrdinaryBootstrapPreflight(t) root, project := generated(t) plan, err := (bootstrap.Service{}).Plan(root, project) if err != nil { @@ -950,3 +954,13 @@ func linuxPidfdAlive(pidfd int) bool { err := unix.PidfdSendSignal(pidfd, 0, nil, 0) return err == nil || errors.Is(err, syscall.EPERM) } + +func requireOrdinaryBootstrapPreflight(t *testing.T) { + t.Helper() + if err := (processadapter.OS{}).OrdinaryPreflight(); err != nil { + if errors.Is(err, processadapter.ErrOrdinaryContainmentUnsupported) { + t.Skipf("ordinary containment runtime is not qualified on this host: %v", err) + } + t.Fatalf("ordinary containment preflight failed: %v", err) + } +} diff --git a/cli/internal/authoring/commands/public_contract.go b/cli/internal/authoring/commands/public_contract.go index ac2e07f19..ff57ab8d2 100644 --- a/cli/internal/authoring/commands/public_contract.go +++ b/cli/internal/authoring/commands/public_contract.go @@ -97,7 +97,7 @@ func selectPublic(root *cobra.Command, args []string) selection { } } switch f.Name { - case "scope", "accept-security-risk", "security-details": + case "scope", "project-root", "accept-security-risk", "security-details": s.invalid = true } } @@ -497,7 +497,7 @@ func publicHelp(s selection) *report.CommandHelp { } seen[f.Name] = true switch f.Name { - case "scope", "accept-security-risk", "security-details": + case "scope", "project-root", "accept-security-risk", "security-details": return } label := "--" + f.Name diff --git a/cli/internal/authoringcli/command_test.go b/cli/internal/authoringcli/command_test.go index 818a6a606..f6cb8f2a6 100644 --- a/cli/internal/authoringcli/command_test.go +++ b/cli/internal/authoringcli/command_test.go @@ -120,7 +120,7 @@ func TestEntrypointParityAndIsolation(t *testing.T) { } func TestInstallerFlagsRejectedBeforeRunner(t *testing.T) { - for _, flag := range []string{"--scope=user", "--accept-security-risk=false", "--security-details=false"} { + for _, flag := range []string{"--project-root=TEST-installer-only", "--scope=user", "--accept-security-risk=false", "--security-details=false"} { for _, before := range []bool{true, false} { t.Run(fmt.Sprint(flag, before), func(t *testing.T) { calls := 0 diff --git a/cli/internal/authoringcli/flags.go b/cli/internal/authoringcli/flags.go index fb8bfaae7..6aeb4bbe3 100644 --- a/cli/internal/authoringcli/flags.go +++ b/cli/internal/authoringcli/flags.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/pflag" ) -var installerOnly = [...]string{"scope", "accept-security-risk", "security-details"} +var installerOnly = [...]string{"scope", "project-root", "accept-security-risk", "security-details"} // flagSets includes ancestor definitions even when a child shadows a name. func flagSets(cmd *cobra.Command, visit func(*pflag.FlagSet)) { diff --git a/cli/tools/authoring-docs/export_test.go b/cli/tools/authoring-docs/export_test.go index 430a01823..bef2fad57 100644 --- a/cli/tools/authoring-docs/export_test.go +++ b/cli/tools/authoring-docs/export_test.go @@ -3,6 +3,8 @@ package main import ( "bytes" "encoding/json" + "errors" + "fmt" "os" "path/filepath" "reflect" @@ -13,12 +15,67 @@ import ( "github.com/spf13/cobra" ) +// auditedAncestorFixtureTrees models the audited ancestor facts for tests of the +// historical source fixture. It is not a rebuilt historical binary and is never +// a production exporter loader. Only the two reviewed live ancestor differences +// are admitted; the unchanged projection golden rejects every other visible fact. +func auditedAncestorFixtureTrees() ([]*cobra.Command, error) { + roots, err := trees() + if err != nil { + return nil, err + } + return modelAuditedAncestorFixture(roots) +} + +func modelAuditedAncestorFixture(roots []*cobra.Command) ([]*cobra.Command, error) { + if err := modelAuditedAncestorFacts(roots); err != nil { + return nil, err + } + if err := validateProjection(roots); err != nil { + return nil, err + } + return roots, nil +} + +// This step does not render or inspect callbacks, allowing the hook-trap test +// to install every trap before its first render. Other fixture callers use the +// loader above, which also requires the unchanged full projection golden. +func modelAuditedAncestorFacts(roots []*cobra.Command) error { + var installer *cobra.Command + for _, root := range roots { + if root.CommandPath() == "agentplugins author" { + installer = root.Parent() + } + } + if installer == nil || installer.CommandPath() != "agentplugins" { + return fmt.Errorf("audited ancestor fixture requires the attached installer ancestor") + } + live := map[string]flagFact{} + for _, fact := range flags(installer.PersistentFlags()) { + live[fact.Name] = fact + } + for _, want := range []flagFact{ + {Name: "scope", Type: "string", Default: "user", Usage: "installation scope: user or project"}, + {Name: "project-root", Type: "string", Default: "", Usage: "existing project directory (project scope only; default: current directory)"}, + } { + if got := live[want.Name]; got != want { + return fmt.Errorf("audited ancestor fixture does not admit changed %s facts", want.Name) + } + } + installer.PersistentFlags().Lookup("scope").Usage = "installation scope (user only in this release)" + installer.PersistentFlags().Lookup("project-root").Hidden = true + return nil +} + func rendered(t *testing.T) (map[string][]byte, manifest) { t.Helper() roots, err := trees() if err != nil { t.Fatal(err) } + if err := modelAuditedAncestorFacts(roots); err != nil { + t.Fatal(err) + } // Every executable hook is a trap: generation must only inspect definitions. var trap func(*cobra.Command) trap = func(c *cobra.Command) { @@ -51,8 +108,8 @@ func rendered(t *testing.T) (map[string][]byte, manifest) { return files, m } -func TestReviewedProjectionMatchesCurrentConstruction(t *testing.T) { - roots, err := trees() +func TestReviewedProjectionMatchesAuditedAncestorFixture(t *testing.T) { + roots, err := auditedAncestorFixtureTrees() if err != nil { t.Fatal(err) } @@ -61,6 +118,50 @@ func TestReviewedProjectionMatchesCurrentConstruction(t *testing.T) { } } +func TestCurrentProjectAncestorRejectsHistoricalExportBeforeOutput(t *testing.T) { + checkout := newSourceFixture(t) + out := filepath.Join(t.TempDir(), "output") + err := export(checkout, checkoutSHA(t, checkout), out) + if err == nil || !strings.Contains(err.Error(), "loaded documentation projection mismatch") { + t.Fatalf("current construction accepted historical source facts: %v", err) + } + if _, err := os.Stat(out); !os.IsNotExist(err) { + t.Fatal("mismatched live construction created output") + } +} + +func TestAuditedAncestorFixtureRejectsAdditionalDrift(t *testing.T) { + for _, drift := range []struct { + name string + change func([]*cobra.Command) + }{ + {"scope-usage", func(roots []*cobra.Command) { roots[1].Parent().PersistentFlags().Lookup("scope").Usage += " changed" }}, + {"project-root-default", func(roots []*cobra.Command) { + roots[1].Parent().PersistentFlags().Lookup("project-root").DefValue = "/other" + }}, + {"project-root-hidden", func(roots []*cobra.Command) { roots[1].Parent().PersistentFlags().Lookup("project-root").Hidden = true }}, + {"additional-inherited-flag", func(roots []*cobra.Command) { + roots[1].Parent().PersistentFlags().Bool("unexpected", false, "unreviewed inherited flag") + }}, + {"other-inherited-help", func(roots []*cobra.Command) { roots[1].Parent().PersistentFlags().Lookup("target").Usage += " changed" }}, + {"author-help", func(roots []*cobra.Command) { roots[1].Short += " changed" }}, + {"plugin-command", func(roots []*cobra.Command) { + roots[0].AddCommand(&cobra.Command{Use: "unexpected", Short: "unreviewed command"}) + }}, + } { + t.Run(drift.name, func(t *testing.T) { + roots, err := trees() + if err != nil { + t.Fatal(err) + } + drift.change(roots) + if _, err = modelAuditedAncestorFixture(roots); err == nil { + t.Fatal("fixture accepted additional construction drift") + } + }) + } +} + func TestDeterministicSurfacesAndNoActions(t *testing.T) { sandbox := t.TempDir() t.Setenv("HOME", sandbox) @@ -210,8 +311,8 @@ func TestSourceIdentityAndNoOverwrite(t *testing.T) { if err := os.WriteFile(marker, []byte("keep v1"), 0600); err != nil { t.Fatal(err) } - if err := export(checkout, sha, dest); err == nil { - t.Fatal("accepted existing output") + if err := exportTrees(checkout, sha, dest, auditedAncestorFixtureTrees); !errors.Is(err, os.ErrExist) { + t.Fatalf("existing output was not refused at the no-overwrite gate: %v", err) } body, _ := os.ReadFile(marker) if string(body) != "keep v1" { @@ -228,11 +329,11 @@ func TestExportBytesAndLinks(t *testing.T) { base := t.TempDir() a, b := filepath.Join(base, "a"), filepath.Join(base, "b") for _, dest := range []string{a, b} { - if err := export(checkout, sha, dest); err != nil { + if err := exportTrees(checkout, sha, dest, auditedAncestorFixtureTrees); err != nil { t.Fatal(err) } } - roots, err := trees() + roots, err := auditedAncestorFixtureTrees() if err != nil { t.Fatal(err) } @@ -245,7 +346,7 @@ func TestExportBytesAndLinks(t *testing.T) { t.Fatal(err) } if len(m.Surfaces) != 2 || len(m.Surfaces[0].Commands) != 21 || len(m.Surfaces[1].Commands) != 19 || m.FactoryBaseline != factoryBaselineSHA || m.SourceSHA != sha || m.Released || m.Status != "prepared-not-release" { - t.Fatal("untouched tree inventory/provenance") + t.Fatal("audited fixture tree inventory/provenance") } for _, surface := range m.Surfaces { for _, entry := range surface.Commands { @@ -269,7 +370,7 @@ func TestExportBytesAndLinks(t *testing.T) { } seen++ if !bytes.Equal(first, expected[filepath.ToSlash(rel)]) { - t.Fatalf("disk differs from untouched tree: %s", rel) + t.Fatalf("disk differs from audited fixture tree: %s", rel) } second, err := os.ReadFile(filepath.Join(b, rel)) if err != nil { diff --git a/cli/tools/authoring-docs/source_test.go b/cli/tools/authoring-docs/source_test.go index dd6bb4816..080b82f13 100644 --- a/cli/tools/authoring-docs/source_test.go +++ b/cli/tools/authoring-docs/source_test.go @@ -135,13 +135,13 @@ func TestCommittedSourceAndDocsOnlyCommit(t *testing.T) { dir := newSourceFixture(t) first := checkoutSHA(t, dir) a := filepath.Join(t.TempDir(), "a") - if err := export(dir, first, a); err != nil { + if err := exportTrees(dir, first, a, auditedAncestorFixtureTrees); err != nil { t.Fatal(err) } writeFixture(t, dir, "docs/later.md", []byte("docs only\n")) second := commitFixture(t, dir) b := filepath.Join(t.TempDir(), "b") - if err := export(dir, second, b); err != nil { + if err := exportTrees(dir, second, b, auditedAncestorFixtureTrees); err != nil { t.Fatal(err) } for _, base := range []struct{ path, sha string }{{a, first}, {b, second}} { @@ -249,7 +249,7 @@ func TestLoadedProjectionMismatch(t *testing.T) { for _, fact := range []string{"help", "flag", "markdown"} { t.Run(fact, func(t *testing.T) { rejectedBeforeOutput(t, dir, checkoutSHA(t, dir), "loaded documentation projection mismatch", func() ([]*cobra.Command, error) { - roots, err := trees() + roots, err := auditedAncestorFixtureTrees() if err != nil { return nil, err } diff --git a/docs/AGENT_SKILLS_INSTALLER.md b/docs/AGENT_SKILLS_INSTALLER.md index 2589000cb..5aafc3a69 100644 --- a/docs/AGENT_SKILLS_INSTALLER.md +++ b/docs/AGENT_SKILLS_INSTALLER.md @@ -58,6 +58,32 @@ Reloading the live session recovers its operation result. A lost apply response never implies success: the page recovers the existing operation or offers an explicit retry of the same confirmation. +## Install Skills for one project + +In the current source build, the terminal flow and browser picker use the same +explicit environment selection as complete packages: + +```sh +agentplugins skills add owner/repository --ui \ + --scope project --project-root /path/to/project +agentplugins skills add owner/repository --skill review --target codex --no-ui --yes \ + --scope project --project-root /path/to/project +``` + +The directory must already exist. The default is still `user`; with project +scope and no `--project-root`, the invocation directory is selected without a +parent-directory search. Only targets admitted for that scope can be selected in the picker. Unavailable +targets can remain visible with a reason. Its reviewed selection remains bound +to that project when applying; a stale +or changed environment requires a new review. + +Saved Skill intent and pins live in the selected project's `uap.toml` and +`uap.lock`. Receipts, mutation locks, recovery and retained data remain local +under `.uap/`. Pass the same scope and project on later list, update, repair, +remove and dependency commands. Complete packages and Skills share the same +client ownership, trust and activation rules. See [Project installation](PROJECT_INSTALLATION.md) +and the [current client support table](PROJECT_CLIENT_SUPPORT.md). + ## Discovery and validation Search in the picker matches skill names, descriptions and repository paths. diff --git a/docs/CHATGPT_PROJECT_SCOPE.md b/docs/CHATGPT_PROJECT_SCOPE.md new file mode 100644 index 000000000..ef37ee11b --- /dev/null +++ b/docs/CHATGPT_PROJECT_SCOPE.md @@ -0,0 +1,43 @@ +# ChatGPT and project scope + +UAP does not qualify ChatGPT for native project installation. Selecting +`--scope project` with `--target chatgpt` returns: + +```text +chatgpt does not support project scope +``` + +The refusal applies to saved additions, imperative `--no-save` additions, +positional `install`, updates, repairs, removal, mixed target requests and +personal preparation options. Completion or external-uninstall attestations +cannot establish a supported project lifecycle. Omitting `--project-root` +uses the invocation directory and preserves this same refusal. + +Do not add `chatgpt` to a project `uap.toml` target table. Dependency lock, +update, sync and environment install reject that declaration before source +acquisition or project mutation. Lock/update perform an early read-only target +check before creating an environment lock, then retain the normal authoritative +manifest reread and validation inside the locked operation. + +Unsupported project requests do not create a local preparation artifact, +`uap.lock`, mutable `.uap` state or native configuration. An existing portable +manifest remains unchanged. UAP does not substitute a user profile, connect an +app, grant trust, or claim that a remote ChatGPT installation was observed. + +## Existing user preparation + +The existing user-scoped personal/manual preparation workflow remains +available under its existing validation requirements. For example, a validated +personal Context7 registration can be prepared explicitly in user scope: + +```sh +agentplugins add context7 --target chatgpt --scope user --chatgpt-app-id +``` + +Use the registration ID obtained through the existing ChatGPT setup flow. +This produces local preparation and manual next steps; it does not certify +remote app connection, account/workspace installation, authentication or native +project discovery. ChatGPT remains excluded from saved dependency targets. + +The accepted scope decision and evidence are recorded in +[the reviewed client plan](plans/project-install-e2e/client-plans.md#13-chatgpt--explicit-unsupported-project-installation). diff --git a/docs/CLAUDE_PROJECT_INSTALLATION.md b/docs/CLAUDE_PROJECT_INSTALLATION.md new file mode 100644 index 000000000..cdd724d20 --- /dev/null +++ b/docs/CLAUDE_PROJECT_INSTALLATION.md @@ -0,0 +1,59 @@ +# Claude Code project installation in current source + +Use `agentplugins add --target claude --scope project --project-root ` +to select an existing project explicitly. Default installation scope remains +user. This document describes current source; historical released binaries and +their qualification do not change when newer source is merged. + +UAP projects selected portable skills, supported commands and MCP declarations +into `.claude/skills//` as a Claude Code skills-directory plugin. +It preserves the publisher's manifest name. It does not change HOME, replace the +native user profile, edit trust, or turn existing enablement settings on. + +## Read the result + +- **Prepared, installation verified:** exact owned files are installed, but Claude + reports project suppression, disabled state or a same-name user copy instead. + The fresh operation provides a next action. This is not loaded-plugin proof. +- **Active, installation verified:** a fresh Claude list reports the exact enabled + project path. This proves discovery, not MCP initialization, authentication or + successful model/tool operation. +- **Failed/unknown:** native output or ownership could not be recognized safely. + UAP preserves the uncertainty and refuses to manufacture activation. + +Plain status remains host-free: it shows saved lifecycle and verifies the owned +projection, without launching Claude. It does not claim to have freshly checked +trust or name precedence. Repeating add/update/repair reobserves native state. + +Start Claude Code from the selected project root. Whole plugins in a skills +directory do not inherit ordinary skills' parent-directory discovery. Review +Claude's trust dialog yourself before requesting a new verification; installation +does not grant trust. Existing native policy and per-server MCP approvals remain +applicable. + +A same-name plugin under the user's skills directory can take precedence over +the project copy. A project enablement boolean addresses the id and does not +select one copy. UAP preserves the user installation and reports the blocked +project result. It does not promise independent simultaneously active versions +of one manifest name. Desktop worktree discovery can use the main checkout; +independent worktree activation is not qualified here. + +Remove targets the exact owned project directory through the normal verified +transaction. Recognized inactive/suppressed/shadowed files can be removed without +native uninstall or changing the user copy. Changed, foreign, symlinked or +ambiguous ownership still refuses automatic deletion. A previously active +binding with contradictory native evidence requires a supported confirmed +reobservation before removal. Plugin data retains the existing receipt policy. + +## Evidence boundary + +The pinned 2.1.263 Linux spike observes untrusted listing and configuration-id +behavior. The new genuine prepared-lifecycle harness retains the hosted guard +and requires its own successful run. Trusted project discovery, MCP startup, +authentication, model requests, and desktop/worktree behavior need separate +observed qualification. Existing user native runtime requirements are unchanged. + +Official native rules: [plugin loading](https://code.claude.com/docs/en/plugins/loading), +[creating plugins](https://code.claude.com/docs/en/plugins/create), and +[settings](https://code.claude.com/docs/en/settings). Current documentation does +not upgrade a historical pinned test result. diff --git a/docs/CLINE_PROJECT_INSTALLATION.md b/docs/CLINE_PROJECT_INSTALLATION.md new file mode 100644 index 000000000..03c9f450b --- /dev/null +++ b/docs/CLINE_PROJECT_INSTALLATION.md @@ -0,0 +1,67 @@ +# Cline project component preparation + +Current source; not a released or native-runtime qualification statement. +Project installation writes owned skill directories to `.cline/skills/` and +prepares selected MCP registrations in `.uap/clients/cline/mcp.json`. The managed +package, state, data and receipts belong to the selected project's `.uap` +namespace. Original user Cline configuration remains a separate authority. + +Results remain **Prepared** even when the exact installed component files are +verified. File verification does not prove native session discovery, effective +trust, IDE behavior, enabled skill execution or MCP initialization. Activation +attestation cannot turn this project preparation into native activation. + +## Use the prepared configuration + +Start Cline in the selected project and inspect its skills. For MCP, the action +returned by add/repair specifies the exact working directory, argument array +including `--auto-approve false`, and process environment values: + +- `CLINE_MCP_SETTINGS_PATH`: the selected project's absolute prepared MCP file. +- `CLINE_NO_AUTO_UPDATE`: `1` for the suggested process. + +These are structured instructions for review; UAP does not execute that Cline +session, run a prompt, connect a server, grant trust or alter authentication. +The explicit MCP file selects the whole source. User registrations are not +merged or copied into that session. A later process without the override reads +its ordinary user settings again. IDE forwarding is a separate unverified path. + +The historical hosted Cline 3.0.73 research positively observed project skill +paths and explicit MCP file selection. Its raw output and exact source closure +are [preserved here](evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/README.md). +That collector did not run UAP's project lifecycle. The separate +[current-source Cline 3.0.73 observation](evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/README.md) +now records public UAP user/A/B installation, replay, update, repair, removal +and read-only native metadata at its exact source. It observed enabled MCP +registrations in the actual prepared files and unique project skill paths. +Same-name user/A/B skill reads selected the user skill in that table, so a +project receipt does not promise the same-name project skill is effective. +Project activation, MCP initialization and independent active-session versions +remain unqualified. + +## Ownership and maintenance + +UAP merges only its recorded MCP members and swaps only its recorded skill +directories. It preserves unrelated settings/members, the user profile, other +projects and persistent plugin data. Project paths do not follow ambient +`CLINE_MCP_SETTINGS_PATH` or `CLINE_DATA_DIR`. Project/profile overlap, aliases, +foreign targets, changed owned bytes and inconsistent receipts are refused. + +Repair can restore a missing exact owned component or rematerialize the pinned +managed package. An externally edited native skill or MCP member is preserved +and requires review rather than being silently overwritten. Removal verifies +recorded file ownership and does not invoke native/global uninstall. Existing +sessions can retain configuration until restarted. + +## SDK host admission boundary + +Both the production CLI and the default SDK compose mandatory local ownership +checks for project component delivery. Setting `EnableNativeObserver=false` +disables optional live native discovery; it does not disable exact skill/MCP +file checks. Those local checks require no client runner and preserve Prepared +semantics. Foreign names or changed owned files are refused before managed +publication, including after a fresh SDK instance reopens the project. + +The installed-file observer and a native client's effective configuration are +different evidence. Neither the default SDK nor enabling a live observer can +turn Cline's project preparation into an active session through attestation. diff --git a/docs/OPENCODE_PROJECT_SCOPE.md b/docs/OPENCODE_PROJECT_SCOPE.md new file mode 100644 index 000000000..40bf9875c --- /dev/null +++ b/docs/OPENCODE_PROJECT_SCOPE.md @@ -0,0 +1,41 @@ +# OpenCode project scope + +OpenCode project delivery installs MCP entries into the selected project's +`opencode.json` or `opencode.jsonc`, and skill directories into +`.opencode/skills/`. Rendered portable commands use the existing skill projection. +UAP keeps its package, data and machine receipts under that project's `.uap/`. +The detected user OpenCode profile remains separate. + +```sh +agentplugins add owner/repo --alias docs --target opencode \ + --scope project --project-root /path/to/project +agentplugins deps sync --locked --scope project --project-root /path/to/project +agentplugins repair --target opencode \ + --scope project --project-root /path/to/project +agentplugins remove docs --target opencode \ + --scope project --project-root /path/to/project +``` + +`--project-root` selects an existing directory explicitly. When omitted in +project scope, UAP uses the invocation directory. It does not search upward for +a different project. The default scope remains user. + +Existing JSONC selection, comments and foreign settings are preserved. If both +config variants exist, UAP refuses the ambiguity. Exact logical MCP keys are +preserved, including names containing slashes or internal spaces; skill names +remain safe directory identifiers. Disabled foreign MCP entries reserve their +keys and are not adopted. Removal consumes only exact recorded native ownership +and leaves unrelated entries, skills and retained application data intact. + +Successful file configuration reports **prepared activation with installed +verification**. This proves the selected native files and ownership. OpenCode +merges user/project/ancestor, custom-directory, inline and managed settings, and +skill discovery also considers compatible directories. These settings can +change what a fresh native session sees. UAP does not grant trust, change your +OpenCode home or overwrite higher-priority policy to force discovery. Activation +attestation cannot convert the file observation into native runtime proof. + +Start OpenCode from the selected project and inspect its effective configuration +and MCP listing before relying on a component. The implementation evidence +separates local production contracts, independent review and hosted native +qualification in [the checkpoint record](plans/project-install-e2e/opencode-implementation-evidence.md). diff --git a/docs/PHASE6_CLI_EXPORTER_PREPARATION.md b/docs/PHASE6_CLI_EXPORTER_PREPARATION.md index 81fd61483..0cda8bf7f 100644 --- a/docs/PHASE6_CLI_EXPORTER_PREPARATION.md +++ b/docs/PHASE6_CLI_EXPORTER_PREPARATION.md @@ -150,6 +150,18 @@ they use a disposable committed source-contract fixture. Mutation tests always use private fixture Git metadata and never mutate the source checkout. Existing Windows/macOS holds remain unchanged; no restricted Windows reproduction was attempted or rerouted. +The project-installation source introduces two inherited installer help facts: +`--project-root` and the project-aware `--scope` wording. The production exporter +continues to reject this current construction against the historical input pins +and unchanged projection golden before creating output. Historical success tests +use a clearly named test-only audited-ancestor fixture loader. It admits exactly +those two known live ancestor differences, models the audited ancestor facts, +and requires the entire resulting projection to match the unchanged golden; +additional visible drift still fails. This models historical facts for formatter, +source-integrity and filesystem tests. It is not proof of a rebuilt historical +binary and is never used by production `trees` or `export`. Current-source export +qualification still requires a separate review of its actual construction inputs. + ROOT mechanical integration and independent medium review remain downstream. D2b must preserve v1 source and routes, use a separately accepted integrated source identity, and consume these prepared records only after D1 integration. diff --git a/docs/PROJECT_CLIENT_SUPPORT.md b/docs/PROJECT_CLIENT_SUPPORT.md new file mode 100644 index 000000000..14bf2a9f3 --- /dev/null +++ b/docs/PROJECT_CLIENT_SUPPORT.md @@ -0,0 +1,53 @@ +# Current source project support + +**Source only / unreleased.** This table describes the project-installation +feature source. It does not change the availability or evidence of published +releases. Select `--scope project` and an existing project directory; the default +remains `user`. [Project installation](PROJECT_INSTALLATION.md) explains the +portable agreement, maintenance and recovery commands. + +Every admitted target keeps project state, receipts, locks and retained data +under that project's `.uap/`, with the original user profile remaining user +authority. Client paths below are relative to the selected project. Configuration +ownership and native discovery are separate: Prepared means further client +selection, reload, trust or session work can remain. + +## Delivery by client + +| Client | Project files or preparation | Result and native boundary | +| --- | --- | --- | +| Claude Code | Projected plugin in `.claude/skills/` | Exact observed native identity controls readiness. Known untrusted/suppressed results remain Prepared with installed-file verification. See [Claude guidance](CLAUDE_PROJECT_INSTALLATION.md). | +| Codex | Skills in `.agents/skills`; exact MCP members in `.codex/config.toml` | Prepared component delivery. The native user plugin catalog/cache is separate; untrusted project MCP can remain disabled. | +| Cursor | `.cursor/skills` and owned `.cursor/mcp.json` members | Prepared. Editor/plugin UI activation, effective discovery and MCP startup require their own evidence. | +| Gemini | `.gemini/skills` and owned `.gemini/settings.json` members | Prepared components; no invented project extension registry. Native trust and higher-priority settings can affect selection. | +| OpenCode | `.opencode/skills` and root `opencode.json` or `opencode.jsonc` | Prepared; simultaneous competing config files are refused. User, ancestor and explicit settings can affect effective selection. See [OpenCode guidance](OPENCODE_PROJECT_SCOPE.md). | +| Copilot CLI | `.github/skills`; existing ordinary `.mcp.json`, otherwise `.github/mcp.json` | Prepared. Project MCP ownership is distinct from VS Code; overlapping skill names require exact ownership and cannot be adopted silently. | +| Kiro | `.kiro/skills` and `.kiro/settings/mcp.json` | Prepared. Custom-agent resource selection can change which workspace configuration is loaded. | +| Grok Build | Skills-only prepared plugin in `.grok/plugins/` | Prepared with package validation; project MCP is unsupported. Disabled/untrusted native evidence cannot become Active by attestation. | +| Qoder | Managed bundle in `.uap/managed/clients/qoder/` with a closed `.agentplugins-qoder-native` manual import view | Prepared skills only. Explicit native import creates a separate globally cached copy; UAP removal removes only its owned preparation. See [Qoder guidance](QODER_PROJECT_INSTALLATION.md). | +| Cline | `.cline/skills`; prepared `.uap/clients/cline/mcp.json` | Always Prepared. Explicit `CLINE_MCP_SETTINGS_PATH` selects the whole MCP file for one process; a process without that override uses its ordinary selection. The observed same-name user skill shadows project copies. See [Cline guidance and exact-source evidence](CLINE_PROJECT_INSTALLATION.md). | +| Windsurf / Devin | Current observed Devin Local: `.devin/skills` and `.devin/mcp_config.json` | Prepared; new delivery requires the observed `devin` 3000.11.205 harness. Legacy Cascade and an unrelated UI executable are not substitutes. | +| Kimi Code | `.kimi-code/skills` and `.kimi-code/mcp.json` | Prepared; new delivery requires observed `kimi` 2.3.1 and the selected skill directory's nearest Git root. The user plugin registry stays separate. | +| ChatGPT | No supported native project filesystem installation | Explicit refusal before source acquisition, including `--no-save`; the existing user/manual path is preserved. See [ChatGPT scope](CHATGPT_PROJECT_SCOPE.md). | +| VS Code | `.github/skills` and exact `servers` members in `.vscode/mcp.json`, including native JSONC syntax | Prepared with installed-file verification for one local folder. Open that folder and review workspace/server trust; observed metadata does not prove MCP startup or model execution. See [VS Code preparation](PROJECT_INSTALLATION.md#vs-code-project-preparation) and [pinned metadata evidence](evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/README.md). | + +## What the verification establishes + +Local ownership checks run even when optional live native discovery is disabled. +They verify exact recorded files/members before mutation and preserve foreign +content, other projects and retained data. Repair can restore missing owned +components or a damaged managed package. An externally edited native component +requires review; it is not silently overwritten. Freshly reopened clients of the +SDK use the same persisted ownership and recovery rules. + +A fixture that exercises the public CLI, real source acquisition, transactions +and saved state establishes those installer contracts. Actual native inventory, +MCP initialization, tool calls, authentication and model behavior require separate +observed evidence. User-scope native passes do not qualify project behavior. + +Pinned observations and per-client limits are preserved in the +[implementation and research records](plans/project-install-e2e/client-plans.md). +Claude's observed untrusted suppression, Codex's trust-gated configuration, +Cline's process-wide source selection and Qoder's global native cache remain +explicit restrictions. Installation does not grant trust or promise arbitrary +user/admin/session configuration is overridden. diff --git a/docs/PROJECT_INSTALLATION.md b/docs/PROJECT_INSTALLATION.md new file mode 100644 index 000000000..02bc46e97 --- /dev/null +++ b/docs/PROJECT_INSTALLATION.md @@ -0,0 +1,164 @@ +# Install plugins for one project + +**Source only / unreleased:** project scope requires a compatible CLI built +from this feature's source. These instructions do not describe a newly +published CLI release. + +Use `--scope project` to keep a project's plugin declarations, installed +ownership and native client configuration separate from your user environment. +Select the existing project directory with `--project-root`: + +```sh +agentplugins add https://git.example.com/team/review.git \ + --alias review --target cursor \ + --scope project --project-root /path/to/project +``` + +The same complete-package sources work in user and project scope: Git over +HTTPS or SSH, supported provider browser links, ready HTTPS archives, and local +packages. See [Direct complete package sources](DIRECT_PACKAGE_SOURCES.md) for +selection syntax, private Git credentials and archive limits. A local directory +or local archive uses the existing imperative flow; it does not become a portable +dependency declaration. + +## Select the environment on every command + +The default scope remains `user`. An existing `uap.toml` in your current folder +does not silently change that default. With `--scope project`, omitting +`--project-root` selects the invocation directory; UAP does not search parent +folders for a Git repository or another manifest. `--project-root` requires +`--scope project` and an existing directory. + +```sh +cd /path/to/project +agentplugins deps status --scope project +agentplugins install --locked --scope project +agentplugins update review --target cursor --scope project +agentplugins list --scope project +agentplugins remove review --target cursor --scope project +``` + +`deps status` reports the selected scope, agreement paths, supported targets +and excluded targets. Dependency commands take their targets from `uap.toml`; +they do not accept an inherited `--target` override. Explicit add, update, +repair and removal commands retain their normal target selection rules. + +Clients have different native project capabilities. See the +[current project support table](PROJECT_CLIENT_SUPPORT.md) for each target's +paths, components and remaining activation boundary. UAP selects the admitted +adapter for that target and refuses unsupported project requests before source +acquisition. In particular, [ChatGPT has no native project installation](CHATGPT_PROJECT_SCOPE.md), +including with `--no-save`. A client's user installation path is never used as +a substitute for missing project support. + +## VS Code project preparation + +Select VS Code explicitly for an existing local project folder: + +```sh +agentplugins add ./my-plugin --target vscode --scope project \ + --project-root /path/to/project --dry-run +``` + +Review the plan, then omit `--dry-run` to apply it. The project adapter prepares +skills in `.github/skills/` and exact MCP `servers` members in +`.vscode/mcp.json`. That file accepts its native comments and trailing commas; +UAP does not select an alternative `mcp.jsonc`. Unrelated servers, inputs, +sandbox settings and comments retain their ownership. + +The result is **Prepared**, with verification of the installed files. Open the +selected folder in VS Code, review workspace and server trust, and inspect its +skills and MCP configuration. The pinned workbench observation establishes +component metadata and project separation; it does not establish MCP startup, +tool calls, authentication or model execution. Remote workspaces and multi-root +workspaces are outside this project adapter's admitted scope. + +Go SDK composition must select the project registry with `all.Project()` or +include the explicit `vscode.NewProject()` adapter. The default user registry +does not admit VS Code project requests. The CLI selects the appropriate registry +from the resolved scope before planning or source acquisition. + +## Portable intent and local state + +| Location | Contents | Transfer with the project agreement? | +| --- | --- | --- | +| `/uap.toml` | Dependency intent and target assignments | Yes | +| `/uap.lock` | Exact source revisions, archive checksums and package digests | Yes | +| `/.uap/` | Local receipts, locks, recovery journals, managed packages and retained plugin data | No | +| Client-specific project paths | Native skills and the exact MCP entries owned by UAP | Recreate from the agreement | +| User content store | Verified immutable source packages shared across environments | Optional cache, not installed ownership | + +Keep the portable pair in version control when you want collaborators to +restore the same dependency selection. Treat `.uap/` as local generated state +and add it to your ignore policy as appropriate; UAP does not edit an existing +`.gitignore` automatically. Back up plugin data separately when needed. + +The original user client profile remains user authority. UAP does not rewrite +HOME or turn the project into a replacement user profile. Project A, project B +and the user environment have separate receipts and mutation locks. Project +removal consumes only exact recorded ownership and retains plugin data and +the shared content store by default. + +## Maintain and recover the selected project + +An intact locked install is a no-op. Repair restores missing owned components +and damaged managed packages from the recorded pin. It preserves externally +edited native skill directories and MCP entries and reports their ownership +conflict for review. `update` explicitly advances a moving source; `repair` +restores the installed pin instead of following the newest branch or archive +endpoint. + +```sh +agentplugins deps sync --locked --scope project --project-root /path/to/project +agentplugins repair --target cursor \ + --scope project --project-root /path/to/project +agentplugins deps recover --scope project --project-root /path/to/project +``` + +Run recovery for the same selected project after an interrupted operation, +then run locked install/sync again. A recovery journal does not authorize +changes to another project's paths. UAP refuses conflicting ownership, +changed native entries, unsafe aliases and changed project identity instead +of adopting or deleting foreign files. + +## Restore in another checkout + +Copy the project's `uap.toml` and `uap.lock` into the new existing project +directory, install the required clients, and run: + +```sh +agentplugins install --locked --scope project --project-root /path/to/new-project +``` + +The explicit transfer commands support project scope too: + +```sh +agentplugins deps export /path/to/exchange \ + --scope project --project-root /path/to/project +agentplugins deps import /path/to/exchange --install \ + --scope project --project-root /path/to/new-project +``` + +Choose an exchange directory different from the selected project; its agreement +already lives at that project root. Export/import preserves the exact two +files, validates the complete pair, and uses the existing confirmation and +locked preflight rules. It does not transfer receipts, native settings, +credentials or plugin data. Cold-cache restore acquires and verifies the +exact recorded bytes; availability and private-source access are still needed. + +## Native activation remains visible + +Installed project files establish configuration and ownership. A native client +may additionally require a fresh session, workspace trust, an explicit session +configuration, authentication or manual UI action. A prepared result remains +prepared until that client's supported activation boundary is satisfied. +`--activation-complete` does not override a project adapter's unsupported, +shadowed or unobserved discovery result. + +UAP does not grant workspace trust or authenticate a client to make installation +look active. Inspect the client's effective project configuration and follow +the reported next action. [OpenCode's project guide](OPENCODE_PROJECT_SCOPE.md) +illustrates the difference between installed configuration and observed native +runtime. The [current project support table](PROJECT_CLIENT_SUPPORT.md) links +client guidance and evidence. The [reviewed client plans](plans/project-install-e2e/client-plans.md) +retain the original proposals and their later observation-driven amendments. diff --git a/docs/PROJECT_INSTALL_OWNER_SCOPE.md b/docs/PROJECT_INSTALL_OWNER_SCOPE.md new file mode 100644 index 000000000..633598c32 --- /dev/null +++ b/docs/PROJECT_INSTALL_OWNER_SCOPE.md @@ -0,0 +1,54 @@ +# Project installation: owner scope clarification + +Accepted owner instruction, 2026-10-23. This document records the scope of the +current project-installation program; it does not change release availability. + +## Supported clients and the narrow VS Code exception + +The owner explicitly requested project installation for **all supported agents**. +The program therefore covers the current supported client set: Claude, Codex, +Cursor, Gemini, OpenCode, Copilot, Kiro, Grok, Qoder, Cline, Windsurf, Kimi, +ChatGPT, and VS Code. Each client receives its own implementation PR stacked +on the project-installation base PR. + +The repository's 2026-10-16 VS Code freeze required a new explicit owner +instruction before reopening work. The current all-supported-agents request is +that instruction for **project-scope installation only**. It authorizes the +narrow adapter, configuration, protocol, fixture, and test changes necessary to +implement and verify that project-installation path. Preserve unrelated VS Code +implementation, behavior, and source; the rest of the freeze remains in force. +This exception does not independently authorize new VS Code hooks, unrelated +profile features, publishing, or a release. + +## Implementation and review gates + +Use the existing dependency solver, manifest and lock formats, immutable +package store, digest/security checks, state receipts, and transaction engine. +Keep project mutable ownership separate from the existing user environment. +Preserve the user profile as user authority; a directory inside a project must +not be disguised as a new HOME or native user profile. + +Before coding each slice, perform two rounds of independent plan review and +fixes. Review implementation independently, address findings, and commit/push +incrementally. A fixture proves its contract; real-client discovery, startup, +MCP operation, authentication, and release evidence require their own observed +proof. A client without a native project registry must expose its supported +component or explicit preparation path and state the remaining limitation. + +For this program, the owner's current model selection is **GPT-6.1 with high or +xhigh reasoning** for workers and reviewers. This supersedes older repository +model instructions for this program only. It does not relax review, evidence, +filesystem authority, or release gates. + +Preserve useful legacy implementation and tests under +[the capability-preservation contract](AUTHORING_CAPABILITY_PRESERVATION.md). +Existing source, historical releases, current executable availability, and +future qualification remain distinct. + +## Registry security providers + +The same owner update defers Gen Agent Trust Hub, Socket, and Snyk registry +provider integration. The concrete deferred work and acceptance criteria are +recorded in [Registry security providers backlog](REGISTRY_SECURITY_PROVIDERS_BACKLOG.md). +The current project program does not add provider dependencies, provider CI +jobs, or third-party scans. diff --git a/docs/QODER_PROJECT_INSTALLATION.md b/docs/QODER_PROJECT_INSTALLATION.md new file mode 100644 index 000000000..61361c537 --- /dev/null +++ b/docs/QODER_PROJECT_INSTALLATION.md @@ -0,0 +1,67 @@ +# Qoder project skill bundle preparation + +Current source; this page is not a release or native runtime qualification. +Project installation prepares a selected skill bundle in the project's UAP +namespace. It preserves the original user's Qoder profile and does not register +a native plugin, rewrite Qoder's registry, redirect HOME or delete native caches. + +## Files and supported components + +UAP owns one managed package under: + +`/.uap/managed/clients/qoder//` + +Its importable native view is the exact subdirectory: + +`/.agentplugins-qoder-native/` + +That view contains a generated `.qoder-plugin/plugin.json` and only the selected +`skills//` trees, including their selected assets and eligible portable +commands rendered as skills. Explicit-only commands are withheld when Qoder +cannot enforce their invocation rule. MCP, hooks, agents and other native +resources are unsupported for this project route. Preserved source metadata +outside the native view does not become an importable native resource. + +Results remain **Prepared**. Installed-file verification describes the exact +bundle and its managed receipt. It does not establish native registration, +selected skill loading, trust, authentication, MCP initialization or a running +session. Activation attestation cannot promote this route to native activation. + +## Optional separate native import + +The result includes the exact native view and selected project working directory. +An operator may review a separate native import from that project directory: + +```text +qoder plugins install --scope project --json +``` + +UAP does not execute this command. Review existing native registrations and +cache contents first. The [preserved genuine Qoder1.1.78 observation](evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/README.md) +found project-attributed rows in a global listing, payloads in the original user's +shared cache and changes to the user's registry. Lists were identical across +user/A/B working directories. Those fixtures were non-Git directories and used +different versions; Git-root behavior, same-name/same-version cache behavior and +actual runtime skill selection remain unqualified. + +Import only the exact native view. Importing the whole managed package would +cross a different resource boundary and is not the supported preparation path. + +## Maintenance and ownership + +UAP validates the selected project, exact managed locator and receipt before +update, repair or removal. It refuses copied authority, project/profile overlap, +aliases, foreign claims and inconsistent ownership. Repair uses the selected +immutable pin; updating to new source is a separate operation. Ordinary status +reads saved UAP state without probing Qoder. + +Removal deletes only UAP's exact project bundle. A separately imported native +cached copy has its own native lifetime: UAP removal does not unregister or delete +it. Native cleanup is a separate operation requiring review of Qoder's native +scope and shared cache. No executable MCP/stdio registration into the bundle is +created by this skills-only route. Persistent caller-owned project data, other +projects and the original user profile remain separate authorities. + +The existing user-scope Qoder adapter retains its native CLI lifecycle and +conservative global-list ownership rules. This project preparation does not +qualify unrestricted same-name user/project native coexistence. diff --git a/docs/REGISTRY_SECURITY_PROVIDERS_BACKLOG.md b/docs/REGISTRY_SECURITY_PROVIDERS_BACKLOG.md new file mode 100644 index 000000000..e2f1a5708 --- /dev/null +++ b/docs/REGISTRY_SECURITY_PROVIDERS_BACKLOG.md @@ -0,0 +1,72 @@ +# Registry security providers backlog + +Status: **DEFERRED by owner, 2026-10-23**. This is a future integration backlog, +not an executable feature, release claim, or instruction to scan packages. +The owner deferred adding Gen Agent Trust Hub, Socket, and Snyk registry +providers while current source/project-installation work proceeds. + +Do not add new provider dependencies, provider CI jobs, credentials, hosted +scanners, package uploads, or automatic third-party calls as part of that work. +Keep the existing signed LintAI assessment and exact-package verification +contract. See [current security and launch evidence](E2E_AND_COMPETITIVE_LAUNCH_PLAN.md) +for its versioned policy, signed subject identity, and local fallback behavior. +A vendor logo or an advisory registry verdict is not evidence that the exact +installed package bytes were assessed. + +## Provider-specific prerequisites + +The following are researched constraints, checked against the linked primary +documentation on 2026-10-23. Recheck them before any future implementation. + +| Provider | Confirmed integration boundary | Deferred work before admission | +| --- | --- | --- | +| Gen Agent Trust Hub | The documented Skill Scanner lookup API accepts a ClawHub skill URL. That documentation does not establish an arbitrary UAP/Git package upload or exact-revision assessment contract. | Confirm a supported UAP subject/API with the provider, access terms, immutable byte identity, coverage, response provenance, and permitted retention. Do not infer general registry support from the ClawHub example. | +| Socket | Skills.sh scanning is experimental and requires contacting Socket for access; custom skills are marked planned in its support table. | Obtain supported custom-registry/skill access and a documented subject/result API. Pin the accepted coverage and maturity; do not treat ordinary npm scanning as a portable agent-plugin assessment. | +| Snyk | Snyk documents registry-partner integrations using its skill-analysis API, including Vercel and Tessl. That establishes partner use, not automatically granted UAP access or a general public submission contract. | Obtain designated registry API access and terms directly from Snyk; confirm payload, asynchronous job/result identity, complete subject coverage, and privacy/retention. Do not substitute a generic dependency scan or SAST upload for this provider contract. | + +Primary sources: + +- [Gen Skill Scanner and lookup example](https://ai.gendigital.com/skill-scanner). +- [Socket ecosystem and extension-scanning support](https://docs.socket.dev/docs/language-support). +- [Snyk and Vercel registry integration](https://snyk.io/blog/snyk-vercel-securing-agent-skill-ecosystem/). +- [Snyk and Tessl Batch Skill Analysis API](https://snyk.io/blog/snyk-tessl-partnership/). + +## Skills.sh advisory data is a separate integration + +The [Skills.sh API reference](https://www.skills.sh/docs/api) documents +Vercel OIDC authentication and a security-audit endpoint returning partner +verdicts. Its documented audit response identifies a skill by source/slug and +includes provider, status, summary, and assessment time. It does **not** include +an assessed content hash. The detail endpoint documents a content hash, but +joining that response to a separate audit does not prove that the audit covered +the same bytes. The API reference and its examples also differ in how they show +authentication for the audit request; verify the supported access contract +instead of assuming public anonymous access from a successful request. + +Any future Skills.sh consumer must therefore label this data advisory unless +the provider supplies a verifiable immutable subject binding. It must remain +separate from UAP's signed exact-package assessment, from provider-specific +direct access, and from an authorization to submit package content. Missing +audits, a 404, inaccessible authentication, and a stale assessment are +unavailable evidence; none means a package passed. + +## Future acceptance criteria + +Before reopening any provider, propose a separate reviewed change with the +following concrete contract: + +| Requirement | Required evidence and behavior | +| --- | --- | +| Subject identity | Bind canonical source, immutable revision, package/subdirectory, and the exact digest of assessed bytes. Explain any normalized or excluded files. | +| Coverage | State whether the assessment includes skill instructions, scripts/binaries, MCP manifests, native extensions, dependencies, and fetched content. Uncovered components remain explicitly unassessed. | +| Provider identity | Record authenticated provider identity, API/schema version, scanner/policy version where available, signed or otherwise authenticated result, and a traceable result/job ID. | +| Freshness and TTL | Record assessment time and validity policy. Reuse only when immutable subject, required coverage, and TTL match; ref changes and changed bytes require a new matching assessment. | +| Status | Preserve pass, warning, failure, pending, unsupported, unavailable, and error distinctions. Do not turn absent, stale, rate-limited, or failed evidence into pass. Define blocking policy separately from vendor wording. | +| Access and data handling | Obtain supported access/credentials and owner approval for the actual data sent. Document source secrecy, retention/deletion, quotas, asynchronous polling, timeouts and retry bounds. Never embed a shared secret in a distributed client. | +| Existing gates | Keep signed LintAI/exact-byte verification authoritative for its current contract. A provider outage or advisory result must not silently remove existing checks. | +| Tests and claims | Use contract fixtures for API/status/freshness behavior and a separately recorded authorized provider trial for live proof. Document known gaps and release status accurately. | + +This backlog does not authorize implementing the contract, running a provider +trial, obtaining credentials, uploading source, or publishing a new security +claim. Reopening requires a future owner instruction after prerequisites are +concrete and reviewable. diff --git a/docs/UAP_DEPENDENCIES.md b/docs/UAP_DEPENDENCIES.md index ad2ce1ccc..6c05076bc 100644 --- a/docs/UAP_DEPENDENCIES.md +++ b/docs/UAP_DEPENDENCIES.md @@ -4,8 +4,12 @@ later). The restore guide is on the docs site: [Restore on another machine](https://universal-agent-plugins.com/docs/en/use/manage.html#restore-on-another-machine). -`agentplugins deps` manages one current-user environment in -`AGENTPLUGINS_HOME` (default: `os.UserConfigDir()/agentplugins`). `uap.toml` records intent and `uap.lock` records exact +`agentplugins deps` manages the selected dependency environment. By default it +uses the current-user environment in `AGENTPLUGINS_HOME` (default: +`os.UserConfigDir()/agentplugins`). With `--scope project`, it uses the selected +existing project's `uap.toml` and `uap.lock`, with local mutable state under +`.uap/`. See [Project installation](PROJECT_INSTALLATION.md) for root selection, +client boundaries and restore commands. `uap.toml` records intent and `uap.lock` records exact sources, package versions, package digests and dependency edges in one lock. The files are portable; local receipts, the verified package store, profile paths and `PLUGIN_DATA` are not. @@ -96,8 +100,9 @@ author's selected agents. Package-local locks do not select consumer pins. Both files still contribute to the immutable package tree digest when present. `plugin.json` remains authoritative for standard identity and components; `uap.toml` is a UAP manager contract, not an Agent Plugins 1.0 component. -Project-scoped installation and `[dev-dependencies]` are not supported by this -workflow yet; the latter is rejected as an unknown field. +Project-scoped installation uses this same manifest and lock contract through +the qualified project adapters. `[dev-dependencies]` remains unsupported and is +rejected as an unknown field. `uap.lock` remains version 1 and is the sole dependency lock. Each node records its exact `version` and immediate `required_by` consumers/selectors, with no @@ -156,7 +161,7 @@ locked state. It accepts `--locked`, `--yes` and `--external-uninstalled` with the same fail-closed meaning as `deps sync`, and with a positional source it behaves as `agentplugins add` for that one package. -The saved dependency workflow supports the 11 qualified local clients: Codex, Claude, +The default user-scope saved dependency workflow supports the 11 qualified local clients: Codex, Claude, Cursor, Copilot, Kiro, Gemini, OpenCode, Cline, Windsurf, Grok Build and Kimi Code. `deps status` prints the scope, exact manifest/lock paths, supported targets and `excluded_targets`, including before `deps init`. ChatGPT is listed @@ -166,7 +171,7 @@ and is reported as `dependency_management_deferred`; its existing imperative adapter and explicitly registered SDK APIs remain available. Initialization creates a private root when necessary and never overwrites an existing manifest. -Each mutating target must be detected with an explicit local profile. CLI +Each mutating target must be detected with an explicit local profile. In user scope, CLI registries (Codex, Claude, Cursor, Copilot, Kiro, Grok) also need an explicit absolute executable, as does OpenCode's host-version probe. Native configuration clients (Gemini, Cline, Windsurf, Kimi) do not require an executable. @@ -183,13 +188,13 @@ component support alone does not trigger repeated lifecycle updates. | Status fact | Meaning and follow-up | | --- | --- | -| `activation_manual` | Prepared materialized artifact; verify activation in the client, then use `agentplugins add --target --activation-complete`. | -| `activation_prepared` | Validated prepared artifact; complete the client's manual steps and attest activation with the same command. | +| `activation_manual` | Prepared materialized artifact; follow the client's manual steps and use `--activation-complete` only where that adapter supports attestation. | +| `activation_prepared` | Validated prepared artifact; follow the reported next action. Project configuration can remain prepared until native discovery is observed. | | `auth_unverified`, `auth_pending`, `auth_failed` | Authentication remains separate from activation; complete authentication and use `--auth-complete` when required. | | `partial` | Failed/unverified activation or incomplete materialization; inspect and retry the failed lifecycle after addressing the cause. | Manual readiness remains visible and stops automatic repeated installation. -It does not claim observed native activation. For Cursor, explicit `--activation-complete` uses the existing composed provider's +It does not claim observed native activation. For user-scope Cursor, explicit `--activation-complete` uses the existing composed provider's operator-attestation contract. Its active/installation-verified state is attested, not observed runtime discovery. Authentication can still remain `auth_unverified`; this does not require repeating activation. Codex with an unknown @@ -197,8 +202,9 @@ plugin-list contract also retains manual readiness rather than repeating update on every sync. Failed/not-run verification and unmaterialized packages never qualify as ready. Persisted lifecycle convergence semantics are unchanged. -Dependency commands always use manifest targets. They reject inherited -`--target` and `--scope project`. Use `--dry-run` for a temporary preview; +Dependency commands always use manifest targets. They reject an inherited +`--target` override and accept explicit `--scope project` for the selected +project environment. Use `--dry-run` for a temporary preview; it does not write the lock, store, state, profiles or persistent scanner cache. Native-only/YAML packages and implicit compatibility fallbacks are not part of this dependency flow. @@ -260,9 +266,12 @@ agentplugins deps import --install ``` The optional `[directory]` argument overrides the exchange folder, which defaults -to the current working directory. Export always reads the global user environment; -import always writes it. `AGENTPLUGINS_HOME` selects that environment as usual. -A project manifest in the current folder does not become export's source. +to the current working directory. Without scope flags, export reads the global +user environment and import writes it; `AGENTPLUGINS_HOME` selects that +environment as usual. A project manifest in the current folder does not change +the default scope. With `--scope project`, both commands use the selected +project environment. Choose a separate exchange directory because the portable +pair already lives in the project root; see [project restore](PROJECT_INSTALLATION.md#restore-in-another-checkout). Plain export/import transfers only `uap.toml` and `uap.lock`, preserving their exact bytes, comments and formatting. It validates the complete source pair @@ -343,7 +352,7 @@ removed. If manual external uninstall is still pending, repeat `remove` with manifest table has gone. Package data and the content store remain retained. Saved targets must be registered local clients supported by the typed installer. -ChatGPT mixed with local targets is rejected before source acquisition: prepare +In user scope, ChatGPT mixed with local targets is rejected before source acquisition: prepare ChatGPT separately, or explicitly choose the imperative `--no-save` flow. Explicit local paths, discovery sources, ChatGPT-only preparation and `--no-save` keep the imperative flow; JSON reports `environment.saved=false` and its reason. `--no-save` never overrides an existing @@ -358,7 +367,7 @@ reconciliation. An external editor can still race a rename; the session rechecks published bytes before applying each binding and refuses a changed plan. -ChatGPT uses the existing manual preparation path, separate from reproducible +In user scope, ChatGPT uses the existing manual preparation path, separate from reproducible local profile installation: `agentplugins add --target chatgpt` prepares an eligible package and marketplace without saving a dependency. App bindings must come from signed Directory evidence or the verified personal mapping path. @@ -367,8 +376,12 @@ in a new chat, and can attest with `--activation-complete`. External uninstall precedes `remove --external-uninstalled`. This does not install a local stdio server into ChatGPT or claim automatic remote connector registration. +All ChatGPT project requests are explicitly unsupported; `--no-save` does not +override that boundary. See [ChatGPT and project scope](CHATGPT_PROJECT_SCOPE.md). + These flows share one installer lifecycle, ownership receipts, immutable package -store and locks. They do not enable project scope, expand the separately qualified +store and locks. Project scope selects separate mutable ownership through that +same lifecycle. These changes do not expand the separately qualified local-file facade route, or publish a CLI/npm/site release. Adapter contract tests use disposable profiles, fixture native processes and a small trusted helper fixture whose real digest/copy/metadata contracts are exercised; they are not qualification diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/cancellation-syscalls.excerpt.log b/docs/evidence/process-containment/linux-proc-children-unavailable/cancellation-syscalls.excerpt.log new file mode 100644 index 000000000..a5f56a0ee --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/cancellation-syscalls.excerpt.log @@ -0,0 +1,14 @@ +1428749 1792878579.424069 execve("/dev/shm/uapt/go-build1319459080/b001/bootstrap.test", ["/dev/shm/uapt/go-build1319459080"..., "-test.paniconexit0", "-test.timeout=10m0s", "-test.run=^TestDefaultRunnerKeep"..., "-test.count=1", "-test.v=true"], 0x3206498d9b88 /* 82 vars */ +1428756 1792878579.544515 execve("/proc/self/exe", ["/proc/self/exe"], 0xa0321a6a008 /* 83 vars */ +1428763 1792878579.568213 execve("/dev/shm/uapt/TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant4174212331/003/bin/npm", ["/dev/shm/uapt/TestDefaultRunnerK"..., "ci", "--ignore-scripts", "--no-audit", "--no-fund"], 0x3fbb539ce150 /* 13 vars */ +1428772 1792878579.665144 execve("/dev/shm/uapt/TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant4174212331/003/bin/npm", ["/dev/shm/uapt/TestDefaultRunnerK"..., "--grandchild-parent"], 0x114f8e9a22a0 /* 13 vars */ +1428779 1792878579.689767 execve("/dev/shm/uapt/TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant4174212331/003/bin/npm", ["/dev/shm/uapt/TestDefaultRunnerK"..., "--grandchild-check"], 0x662722bb960 /* 13 vars */ +1428797 1792878579.746632 execve("/dev/shm/uapt/TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant4174212331/003/bin/npm", ["/dev/shm/uapt/TestDefaultRunnerK"..., "--setsid-descendant"], 0x114f8e4960e0 /* 13 vars */ +1428756 1792878579.799316 kill(-1428763, SIGKILL +1428756 1792878579.806135 waitid(P_PID, 1428763, {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=1428763, si_uid=0, si_status=SIGKILL, si_utime=0, si_stime=0}, WNOHANG|WEXITED|WNOWAIT, NULL) = 0 <0.000064> +1428756 1792878579.822151 waitid(P_PIDFD, 11, {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=1428763, si_uid=0, si_status=SIGKILL, si_utime=0, si_stime=0}, WEXITED, {ru_utime={tv_sec=0, tv_usec=29747}, ru_stime={tv_sec=0, tv_usec=72126}, ...}) = 0 <0.000036> +1428756 1792878579.822715 kill(1428756, SIGKILL) = ? +1428753 1792878579.830825 waitid(P_PIDFD, 28, {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=1428756, si_uid=0, si_status=SIGKILL, si_utime=0, si_stime=0}, WEXITED, {ru_utime={tv_sec=0, tv_usec=35102}, ru_stime={tv_sec=0, tv_usec=104861}, ...}) = 0 <0.000034> +1428749 1792878580.840909 pidfd_send_signal(21, 0, NULL, 0) = 0 <0.000019> +1428764 1792878580.845447 pidfd_send_signal(21, SIGKILL, NULL, 0) = 0 <0.000408> +1428797 1792878580.850760 +++ killed by SIGKILL +++ diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/README.md b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/README.md new file mode 100644 index 000000000..a299cb085 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/README.md @@ -0,0 +1,11 @@ +# Supported hosted Linux bootstrap runtime result + +[Authoring native offline40143431447, Linux amd64 job121019719395](https://github.com/777genius/universal-agent-plugins/actions/runs/40143431447/job/121019719395) succeeded on committed source1bc5b898d8085e707fb405ed0c8f84ca2454157f, treee6f411497b048f54b8bd2d8ac4d3603710a2e57a. The artifact's head.txt and original test records match this source. This source contains the independently reviewed ordinary-containment prerequisite and helper-dispatch correction. + +The three reviewed real bootstrap runtime tests each **ran and passed**, with no bootstrap package skip/failure: captured staging plus detached setsid containment0.08s, locked command plus sanitized environment0.07s, and descendant cancellation0.04s. The full bootstrap package passed0.318s. These are actual supported-host process assertions, distinct from the local unsupported prerequisite SKIPs preserved in the parent directory. No deadline or assertion was relaxed. + +The original archive is12579492503 (109006bytes, SHA256078da3d0930e82647a0ed78139f423932621edc6eff5eaad2a5ba0036af6795e). Its length, hash and CRC were independently verified after read-only download. `bootstrap-go-test.jsonl` contains every original byte line for the exact bootstrap package, preserving order; `provenance.json` records the extraction predicate, full source JSONL hash/length and counts. `archive-members.json` identifies every original archive member. Original metadata/summary/build descriptions are retained byte-for-byte. Auxiliary full-package JSONL and discovery remain in the identified original artifact. + +The workflow's exact package inventory excludes install/integrationctl/adapters/process, so this result does **not** qualify the separate nineteen process runtime tests. The unchanged Required/test workflow for main-base PRs invokes make test-required; Makefile runs ./cli/... (bootstrap) and ./install/integrationctl/... (process) without a test-name filter. The actual current-source full lane must still demonstrate its own supported-host result. No new workflow, selector or retry was added for this evidence. Other authoring summary facts, including one unrelated optional socket-fixture skip, are preserved rather than promoted to broader qualification. + +No native vendor command ran locally during this evidence retrieval. This result does not establish native-client discovery, MCP/model/auth/trust operation or release qualification. diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/agentplugins-build.json b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/agentplugins-build.json new file mode 100644 index 000000000..36398d2f1 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/agentplugins-build.json @@ -0,0 +1,248 @@ +{ + "GoVersion": "go1.26.10", + "Path": "github.com/777genius/plugin-kit-ai/cli/cmd/agentplugins", + "Main": { + "Path": "github.com/777genius/plugin-kit-ai/cli", + "Version": "v0.0.0-20261024233519-1bc5b898d808" + }, + "Deps": [ + { + "Path": "charm.land/bubbles/v2", + "Version": "v2.0.0", + "Sum": "h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=" + }, + { + "Path": "charm.land/bubbletea/v2", + "Version": "v2.0.2", + "Sum": "h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=" + }, + { + "Path": "charm.land/huh/v2", + "Version": "v2.0.3", + "Sum": "h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=" + }, + { + "Path": "charm.land/lipgloss/v2", + "Version": "v2.0.1", + "Sum": "h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins", + "Version": "(devel)" + }, + { + "Path": "github.com/atotto/clipboard", + "Version": "v0.1.4", + "Sum": "h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=" + }, + { + "Path": "github.com/catppuccin/go", + "Version": "v0.2.0", + "Sum": "h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=" + }, + { + "Path": "github.com/charmbracelet/colorprofile", + "Version": "v0.4.2", + "Sum": "h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=" + }, + { + "Path": "github.com/charmbracelet/ultraviolet", + "Version": "v0.0.0-20260205113103-524a6607adb8", + "Sum": "h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=" + }, + { + "Path": "github.com/charmbracelet/x/ansi", + "Version": "v0.11.6", + "Sum": "h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=" + }, + { + "Path": "github.com/charmbracelet/x/exp/ordered", + "Version": "v0.1.0", + "Sum": "h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=" + }, + { + "Path": "github.com/charmbracelet/x/exp/strings", + "Version": "v0.0.0-20240722160745-212f7b056ed0", + "Sum": "h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=" + }, + { + "Path": "github.com/charmbracelet/x/term", + "Version": "v0.2.2", + "Sum": "h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=" + }, + { + "Path": "github.com/charmbracelet/x/termios", + "Version": "v0.1.1", + "Sum": "h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=" + }, + { + "Path": "github.com/charmbracelet/x/windows", + "Version": "v0.2.2", + "Sum": "h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=" + }, + { + "Path": "github.com/clipperhouse/displaywidth", + "Version": "v0.11.0", + "Sum": "h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=" + }, + { + "Path": "github.com/clipperhouse/uax29/v2", + "Version": "v2.7.0", + "Sum": "h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=" + }, + { + "Path": "github.com/creachadair/tomledit", + "Version": "v0.0.29", + "Sum": "h1:dB5CbdwJMpn/fmfAPTAAleXF/KJwY0Ggc1eL/zvZRgk=" + }, + { + "Path": "github.com/dlclark/regexp2", + "Version": "v1.12.0", + "Sum": "h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=" + }, + { + "Path": "github.com/dustin/go-humanize", + "Version": "v1.0.1", + "Sum": "h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=" + }, + { + "Path": "github.com/lucasb-eyer/go-colorful", + "Version": "v1.3.0", + "Sum": "h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=" + }, + { + "Path": "github.com/mattn/go-runewidth", + "Version": "v0.0.20", + "Sum": "h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=" + }, + { + "Path": "github.com/mitchellh/hashstructure/v2", + "Version": "v2.0.2", + "Sum": "h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=" + }, + { + "Path": "github.com/muesli/cancelreader", + "Version": "v0.2.2", + "Sum": "h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=" + }, + { + "Path": "github.com/pelletier/go-toml/v2", + "Version": "v2.3.0", + "Sum": "h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=" + }, + { + "Path": "github.com/rivo/uniseg", + "Version": "v0.4.7", + "Sum": "h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=" + }, + { + "Path": "github.com/santhosh-tekuri/jsonschema/v6", + "Version": "v6.0.3", + "Sum": "h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=" + }, + { + "Path": "github.com/spf13/cobra", + "Version": "v1.10.2", + "Sum": "h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=" + }, + { + "Path": "github.com/spf13/pflag", + "Version": "v1.0.9", + "Sum": "h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=" + }, + { + "Path": "github.com/tailscale/hujson", + "Version": "v0.0.0-20260302212456-ecc657c15afd", + "Sum": "h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=" + }, + { + "Path": "github.com/xo/terminfo", + "Version": "v0.0.0-20220910002029-abceb7e1c41e", + "Sum": "h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=" + }, + { + "Path": "golang.org/x/mod", + "Version": "v0.40.0", + "Sum": "h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=" + }, + { + "Path": "golang.org/x/sync", + "Version": "v0.22.0", + "Sum": "h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=" + }, + { + "Path": "golang.org/x/sys", + "Version": "v0.44.0", + "Sum": "h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=" + }, + { + "Path": "golang.org/x/term", + "Version": "v0.34.0", + "Sum": "h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=" + }, + { + "Path": "golang.org/x/text", + "Version": "v0.40.0", + "Sum": "h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=" + }, + { + "Path": "gopkg.in/yaml.v3", + "Version": "v3.0.1", + "Sum": "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=" + } + ], + "Settings": [ + { + "Key": "-buildmode", + "Value": "exe" + }, + { + "Key": "-compiler", + "Value": "gc" + }, + { + "Key": "-trimpath", + "Value": "true" + }, + { + "Key": "DefaultGODEBUG", + "Value": "cryptocustomrand=1,httpservecontentmaxranges=1,tlssecpmlkem=0,urlstrictcolons=0" + }, + { + "Key": "CGO_ENABLED", + "Value": "1" + }, + { + "Key": "GOARCH", + "Value": "amd64" + }, + { + "Key": "GOOS", + "Value": "linux" + }, + { + "Key": "GOAMD64", + "Value": "v1" + }, + { + "Key": "vcs", + "Value": "git" + }, + { + "Key": "vcs.revision", + "Value": "1bc5b898d8085e707fb405ed0c8f84ca2454157f" + }, + { + "Key": "vcs.time", + "Value": "2026-10-24T23:35:19Z" + }, + { + "Key": "vcs.modified", + "Value": "false" + } + ] +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/archive-members.json b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/archive-members.json new file mode 100644 index 000000000..b45847853 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/archive-members.json @@ -0,0 +1,54 @@ +{ + "agentplugins-build.json": { + "bytes": 6193, + "sha256": "d217f05dda21e30d40f69a816eda2101b3ebff6966fb94f6add98f3ae4a14fc6" + }, + "dependencies.txt": { + "bytes": 22540, + "sha256": "34a879b9de3da81133a0284048ee0449cf0b3574ed978e4542a45cb7ff4b799d" + }, + "go-env.json": { + "bytes": 164, + "sha256": "a6ee8aaad98ac1d26093df24d37bb2606d7e96131a7792eead0905059faf1b78" + }, + "go-test.json": { + "bytes": 1295649, + "sha256": "91f2da2314e636b9640c3b56fe7eaff84e07dc69efc5788cbdd9f7de103c8c0b" + }, + "go-test.stderr": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "head.txt": { + "bytes": 41, + "sha256": "afdf566ecae42ade538799692923421cc16ff95e1d09fb317f7bdb97acf20285" + }, + "node-version.txt": { + "bytes": 9, + "sha256": "33e26f35db6e57ce0c486ddc815fe966867e4f649e418eaf323f75f4720eb8e9" + }, + "packages.txt": { + "bytes": 1091, + "sha256": "55c4fa388eabf217b6ad4c3ef321efa0aabb33135ea38ecca127fe4a975dfa6c" + }, + "packageview-exit.txt": { + "bytes": 2, + "sha256": "9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa" + }, + "plugin-kit-ai-build.json": { + "bytes": 3277, + "sha256": "9db49cd66a056be1d2472d2a409403ee063c7625c3d96118f7cb19210d61b934" + }, + "summary.json": { + "bytes": 16609, + "sha256": "9a3d00df8c41e16f20bebb74b070f4f2a66de92c65b808fc594b0beaec4cae4f" + }, + "test-discovery.json": { + "bytes": 68510, + "sha256": "7f4638e11d86f1272b5e4d2810e2ba5dd6f194472e8059655c024f28d71e64f5" + }, + "test-exit.txt": { + "bytes": 2, + "sha256": "9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa" + } +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/bootstrap-go-test.jsonl b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/bootstrap-go-test.jsonl new file mode 100644 index 000000000..3b27ac83f --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/bootstrap-go-test.jsonl @@ -0,0 +1,104 @@ +{"Time":"2026-10-24T23:37:59.266047056Z","Action":"start","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap"} +{"Time":"2026-10-24T23:37:59.273620133Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate"} +{"Time":"2026-10-24T23:37:59.273643697Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate","Output":"=== RUN TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate\n"} +{"Time":"2026-10-24T23:37:59.279292608Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/missing_lock"} +{"Time":"2026-10-24T23:37:59.279310291Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/missing_lock","Output":"=== RUN TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/missing_lock\n"} +{"Time":"2026-10-24T23:37:59.284632609Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/missing_lock","Output":"--- PASS: TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/missing_lock (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.284649251Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/missing_lock","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.284660542Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/changed_package"} +{"Time":"2026-10-24T23:37:59.284665751Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/changed_package","Output":"=== RUN TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/changed_package\n"} +{"Time":"2026-10-24T23:37:59.289954249Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/changed_package","Output":"--- PASS: TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/changed_package (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.289982022Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/changed_package","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.289988824Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/python_ambiguity"} +{"Time":"2026-10-24T23:37:59.289993724Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/python_ambiguity","Output":"=== RUN TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/python_ambiguity\n"} +{"Time":"2026-10-24T23:37:59.294230663Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/python_ambiguity","Output":"--- PASS: TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/python_ambiguity (0.00s)\n"} +{"Time":"2026-10-24T23:37:59.294244318Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/python_ambiguity","Elapsed":0} +{"Time":"2026-10-24T23:37:59.29426116Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/go_ambiguity"} +{"Time":"2026-10-24T23:37:59.29426652Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/go_ambiguity","Output":"=== RUN TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/go_ambiguity\n"} +{"Time":"2026-10-24T23:37:59.299521301Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/go_ambiguity","Output":"--- PASS: TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/go_ambiguity (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.299535718Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/go_ambiguity","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.29954233Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/manager_ambiguity"} +{"Time":"2026-10-24T23:37:59.299546929Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/manager_ambiguity","Output":"=== RUN TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/manager_ambiguity\n"} +{"Time":"2026-10-24T23:37:59.303690693Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/manager_ambiguity","Output":"--- PASS: TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/manager_ambiguity (0.00s)\n"} +{"Time":"2026-10-24T23:37:59.303704048Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate/manager_ambiguity","Elapsed":0} +{"Time":"2026-10-24T23:37:59.304054286Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate","Output":"--- PASS: TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate (0.03s)\n"} +{"Time":"2026-10-24T23:37:59.304063333Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanRecognizesOnlyExactGeneratedLockedNodeTemplate","Elapsed":0.03} +{"Time":"2026-10-24T23:37:59.304070336Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyUsesIsolatedHomesAndCommitsOnlyNodeModules"} +{"Time":"2026-10-24T23:37:59.304074825Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyUsesIsolatedHomesAndCommitsOnlyNodeModules","Output":"=== RUN TestApplyUsesIsolatedHomesAndCommitsOnlyNodeModules\n"} +{"Time":"2026-10-24T23:37:59.310452485Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyUsesIsolatedHomesAndCommitsOnlyNodeModules","Output":"--- PASS: TestApplyUsesIsolatedHomesAndCommitsOnlyNodeModules (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.310466331Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyUsesIsolatedHomesAndCommitsOnlyNodeModules","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.310473224Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyFailureRemovesPartialStaging"} +{"Time":"2026-10-24T23:37:59.310477552Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyFailureRemovesPartialStaging","Output":"=== RUN TestApplyFailureRemovesPartialStaging\n"} +{"Time":"2026-10-24T23:37:59.316588859Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyFailureRemovesPartialStaging","Output":"--- PASS: TestApplyFailureRemovesPartialStaging (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.31660581Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyFailureRemovesPartialStaging","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.316613154Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges"} +{"Time":"2026-10-24T23:37:59.316618384Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges","Output":"=== RUN TestPlanAndApplyRejectSourceIdentityChanges\n"} +{"Time":"2026-10-24T23:37:59.316624145Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_symlink"} +{"Time":"2026-10-24T23:37:59.316628804Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_symlink","Output":"=== RUN TestPlanAndApplyRejectSourceIdentityChanges/root_symlink\n"} +{"Time":"2026-10-24T23:37:59.321521545Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_symlink","Output":"--- PASS: TestPlanAndApplyRejectSourceIdentityChanges/root_symlink (0.00s)\n"} +{"Time":"2026-10-24T23:37:59.32154052Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_symlink","Elapsed":0} +{"Time":"2026-10-24T23:37:59.321547744Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_symlink"} +{"Time":"2026-10-24T23:37:59.321552723Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_symlink","Output":"=== RUN TestPlanAndApplyRejectSourceIdentityChanges/source_symlink\n"} +{"Time":"2026-10-24T23:37:59.32688971Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_symlink","Output":"--- PASS: TestPlanAndApplyRejectSourceIdentityChanges/source_symlink (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.326906872Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_symlink","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.32691606Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_directory_symlink"} +{"Time":"2026-10-24T23:37:59.326921039Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_directory_symlink","Output":"=== RUN TestPlanAndApplyRejectSourceIdentityChanges/source_directory_symlink\n"} +{"Time":"2026-10-24T23:37:59.33170189Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_directory_symlink","Output":"--- PASS: TestPlanAndApplyRejectSourceIdentityChanges/source_directory_symlink (0.00s)\n"} +{"Time":"2026-10-24T23:37:59.331718261Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/source_directory_symlink","Elapsed":0} +{"Time":"2026-10-24T23:37:59.331725675Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/lock_replacement"} +{"Time":"2026-10-24T23:37:59.331737166Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/lock_replacement","Output":"=== RUN TestPlanAndApplyRejectSourceIdentityChanges/lock_replacement\n"} +{"Time":"2026-10-24T23:37:59.336841193Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/lock_replacement","Output":"--- PASS: TestPlanAndApplyRejectSourceIdentityChanges/lock_replacement (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.336858034Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/lock_replacement","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.336867392Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_replacement"} +{"Time":"2026-10-24T23:37:59.336872421Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_replacement","Output":"=== RUN TestPlanAndApplyRejectSourceIdentityChanges/root_replacement\n"} +{"Time":"2026-10-24T23:37:59.341451333Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_replacement","Output":"--- PASS: TestPlanAndApplyRejectSourceIdentityChanges/root_replacement (0.00s)\n"} +{"Time":"2026-10-24T23:37:59.341467303Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges/root_replacement","Elapsed":0} +{"Time":"2026-10-24T23:37:59.341475037Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges","Output":"--- PASS: TestPlanAndApplyRejectSourceIdentityChanges (0.02s)\n"} +{"Time":"2026-10-24T23:37:59.34148198Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestPlanAndApplyRejectSourceIdentityChanges","Elapsed":0.02} +{"Time":"2026-10-24T23:37:59.341493432Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges"} +{"Time":"2026-10-24T23:37:59.341499203Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges","Output":"=== RUN TestApplyRejectsLateSourceAndDestinationChanges\n"} +{"Time":"2026-10-24T23:37:59.341504793Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/source"} +{"Time":"2026-10-24T23:37:59.341509632Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/source","Output":"=== RUN TestApplyRejectsLateSourceAndDestinationChanges/source\n"} +{"Time":"2026-10-24T23:37:59.347802744Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/source","Output":"--- PASS: TestApplyRejectsLateSourceAndDestinationChanges/source (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.347826408Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/source","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.347835625Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_file"} +{"Time":"2026-10-24T23:37:59.347840955Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_file","Output":"=== RUN TestApplyRejectsLateSourceAndDestinationChanges/destination_file\n"} +{"Time":"2026-10-24T23:37:59.355185403Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_file","Output":"--- PASS: TestApplyRejectsLateSourceAndDestinationChanges/destination_file (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.355202405Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_file","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.35521035Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_symlink"} +{"Time":"2026-10-24T23:37:59.355214969Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_symlink","Output":"=== RUN TestApplyRejectsLateSourceAndDestinationChanges/destination_symlink\n"} +{"Time":"2026-10-24T23:37:59.362181835Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_symlink","Output":"--- PASS: TestApplyRejectsLateSourceAndDestinationChanges/destination_symlink (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.362206321Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges/destination_symlink","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.362215919Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges","Output":"--- PASS: TestApplyRejectsLateSourceAndDestinationChanges (0.02s)\n"} +{"Time":"2026-10-24T23:37:59.36222217Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyRejectsLateSourceAndDestinationChanges","Elapsed":0.02} +{"Time":"2026-10-24T23:37:59.362228372Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyReportsCleanupFailureWithoutHidingCommit"} +{"Time":"2026-10-24T23:37:59.362233161Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyReportsCleanupFailureWithoutHidingCommit","Output":"=== RUN TestApplyReportsCleanupFailureWithoutHidingCommit\n"} +{"Time":"2026-10-24T23:37:59.370133525Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyReportsCleanupFailureWithoutHidingCommit","Output":"--- PASS: TestApplyReportsCleanupFailureWithoutHidingCommit (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.37015725Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyReportsCleanupFailureWithoutHidingCommit","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.370165996Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyJoinsProcessAndCleanupFailures"} +{"Time":"2026-10-24T23:37:59.370170525Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyJoinsProcessAndCleanupFailures","Output":"=== RUN TestApplyJoinsProcessAndCleanupFailures\n"} +{"Time":"2026-10-24T23:37:59.378862392Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyJoinsProcessAndCleanupFailures","Output":"--- PASS: TestApplyJoinsProcessAndCleanupFailures (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.378883051Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestApplyJoinsProcessAndCleanupFailures","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.378896166Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestAtomicCommitNeverReplacesDestination"} +{"Time":"2026-10-24T23:37:59.378900985Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestAtomicCommitNeverReplacesDestination","Output":"=== RUN TestAtomicCommitNeverReplacesDestination\n"} +{"Time":"2026-10-24T23:37:59.379177675Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestAtomicCommitNeverReplacesDestination","Output":"--- PASS: TestAtomicCommitNeverReplacesDestination (0.00s)\n"} +{"Time":"2026-10-24T23:37:59.379231756Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestAtomicCommitNeverReplacesDestination","Elapsed":0} +{"Time":"2026-10-24T23:37:59.379261322Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestFinalVerificationToLaunchReplacementUsesCapturedStaging"} +{"Time":"2026-10-24T23:37:59.379266451Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestFinalVerificationToLaunchReplacementUsesCapturedStaging","Output":"=== RUN TestFinalVerificationToLaunchReplacementUsesCapturedStaging\n"} +{"Time":"2026-10-24T23:37:59.387819338Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestFinalVerificationToLaunchReplacementUsesCapturedStaging","Output":"--- PASS: TestFinalVerificationToLaunchReplacementUsesCapturedStaging (0.01s)\n"} +{"Time":"2026-10-24T23:37:59.387846379Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestFinalVerificationToLaunchReplacementUsesCapturedStaging","Elapsed":0.01} +{"Time":"2026-10-24T23:37:59.387853933Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant"} +{"Time":"2026-10-24T23:37:59.387858191Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant","Output":"=== RUN TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant\n"} +{"Time":"2026-10-24T23:37:59.468324482Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant","Output":"--- PASS: TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant (0.08s)\n"} +{"Time":"2026-10-24T23:37:59.468336735Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant","Elapsed":0.08} +{"Time":"2026-10-24T23:37:59.468346744Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment"} +{"Time":"2026-10-24T23:37:59.468351353Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment","Output":"=== RUN TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment\n"} +{"Time":"2026-10-24T23:37:59.538378508Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment","Output":"--- PASS: TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment (0.07s)\n"} +{"Time":"2026-10-24T23:37:59.538405418Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment","Elapsed":0.07} +{"Time":"2026-10-24T23:37:59.538412552Z","Action":"run","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerCancellationStopsDescendants"} +{"Time":"2026-10-24T23:37:59.538416249Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerCancellationStopsDescendants","Output":"=== RUN TestDefaultRunnerCancellationStopsDescendants\n"} +{"Time":"2026-10-24T23:37:59.583009467Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerCancellationStopsDescendants","Output":"--- PASS: TestDefaultRunnerCancellationStopsDescendants (0.04s)\n"} +{"Time":"2026-10-24T23:37:59.583041637Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Test":"TestDefaultRunnerCancellationStopsDescendants","Elapsed":0.04} +{"Time":"2026-10-24T23:37:59.583050484Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Output":"PASS\n"} +{"Time":"2026-10-24T23:37:59.584309439Z","Action":"output","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Output":"ok \tgithub.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap\t0.318s\n"} +{"Time":"2026-10-24T23:37:59.584326711Z","Action":"pass","Package":"github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap","Elapsed":0.318} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/go-env.json b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/go-env.json new file mode 100644 index 000000000..07910a538 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/go-env.json @@ -0,0 +1,8 @@ +{ + "GOARCH": "amd64", + "GOHOSTARCH": "amd64", + "GOHOSTOS": "linux", + "GOOS": "linux", + "GOROOT": "/opt/hostedtoolcache/go/1.26.10/x64", + "GOVERSION": "go1.26.10" +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/go-test.stderr b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/go-test.stderr new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/head.txt b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/head.txt new file mode 100644 index 000000000..79d6a405a --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/head.txt @@ -0,0 +1 @@ +1bc5b898d8085e707fb405ed0c8f84ca2454157f diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/packages.txt b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/packages.txt new file mode 100644 index 000000000..950a79a83 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/packages.txt @@ -0,0 +1,15 @@ +github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packageview +github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packagedigest +github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/conformance +github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader +github.com/777genius/plugin-kit-ai/cli/internal/authoringcli +github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap +github.com/777genius/plugin-kit-ai/cli/internal/authoring/commands +github.com/777genius/plugin-kit-ai/cli/internal/authoring/jsonmaint +github.com/777genius/plugin-kit-ai/cli/internal/authoring/mcpruntime +github.com/777genius/plugin-kit-ai/cli/internal/authoring/nativeimport +github.com/777genius/plugin-kit-ai/cli/internal/authoring/project +github.com/777genius/plugin-kit-ai/cli/internal/authoring/readiness +github.com/777genius/plugin-kit-ai/cli/internal/authoring/report +github.com/777genius/plugin-kit-ai/cli/internal/authoring/scaffold +github.com/777genius/plugin-kit-ai/cli/internal/authoring/skills diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/plugin-kit-ai-build.json b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/plugin-kit-ai-build.json new file mode 100644 index 000000000..4d1d0e228 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/plugin-kit-ai-build.json @@ -0,0 +1,146 @@ +{ + "GoVersion": "go1.26.10", + "Path": "github.com/777genius/plugin-kit-ai/cli/cmd/plugin-kit-ai", + "Main": { + "Path": "github.com/777genius/plugin-kit-ai/cli", + "Version": "v0.0.0-20261024233519-1bc5b898d808" + }, + "Deps": [ + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/plugininstall", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/sdk", + "Version": "(devel)" + }, + { + "Path": "github.com/cpuguy83/go-md2man/v2", + "Version": "v2.0.6", + "Sum": "h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=" + }, + { + "Path": "github.com/creachadair/tomledit", + "Version": "v0.0.29", + "Sum": "h1:dB5CbdwJMpn/fmfAPTAAleXF/KJwY0Ggc1eL/zvZRgk=" + }, + { + "Path": "github.com/dlclark/regexp2", + "Version": "v1.12.0", + "Sum": "h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=" + }, + { + "Path": "github.com/pelletier/go-toml/v2", + "Version": "v2.3.0", + "Sum": "h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=" + }, + { + "Path": "github.com/russross/blackfriday/v2", + "Version": "v2.1.0", + "Sum": "h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=" + }, + { + "Path": "github.com/santhosh-tekuri/jsonschema/v6", + "Version": "v6.0.3", + "Sum": "h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=" + }, + { + "Path": "github.com/spf13/cobra", + "Version": "v1.10.2", + "Sum": "h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=" + }, + { + "Path": "github.com/spf13/pflag", + "Version": "v1.0.9", + "Sum": "h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=" + }, + { + "Path": "github.com/tailscale/hujson", + "Version": "v0.0.0-20260302212456-ecc657c15afd", + "Sum": "h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=" + }, + { + "Path": "go.yaml.in/yaml/v3", + "Version": "v3.0.4", + "Sum": "h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=" + }, + { + "Path": "golang.org/x/mod", + "Version": "v0.40.0", + "Sum": "h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=" + }, + { + "Path": "golang.org/x/sys", + "Version": "v0.44.0", + "Sum": "h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=" + }, + { + "Path": "golang.org/x/text", + "Version": "v0.40.0", + "Sum": "h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=" + }, + { + "Path": "gopkg.in/yaml.v3", + "Version": "v3.0.1", + "Sum": "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=" + } + ], + "Settings": [ + { + "Key": "-buildmode", + "Value": "exe" + }, + { + "Key": "-compiler", + "Value": "gc" + }, + { + "Key": "-trimpath", + "Value": "true" + }, + { + "Key": "DefaultGODEBUG", + "Value": "cryptocustomrand=1,httpservecontentmaxranges=1,tlssecpmlkem=0,urlstrictcolons=0" + }, + { + "Key": "CGO_ENABLED", + "Value": "1" + }, + { + "Key": "GOARCH", + "Value": "amd64" + }, + { + "Key": "GOOS", + "Value": "linux" + }, + { + "Key": "GOAMD64", + "Value": "v1" + }, + { + "Key": "vcs", + "Value": "git" + }, + { + "Key": "vcs.revision", + "Value": "1bc5b898d8085e707fb405ed0c8f84ca2454157f" + }, + { + "Key": "vcs.time", + "Value": "2026-10-24T23:35:19Z" + }, + { + "Key": "vcs.modified", + "Value": "false" + } + ] +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/provenance.json b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/provenance.json new file mode 100644 index 000000000..5b7050f9f --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/provenance.json @@ -0,0 +1,59 @@ +{ + "schema_version": 1, + "run_id": 40143431447, + "job_id": 121019719395, + "job_conclusion": "success", + "job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40143431447/job/121019719395", + "workflow": "Authoring native offline", + "artifact_id": 12579492503, + "artifact_name": "authoring-native-linux-amd64-40143431447-1", + "zip_bytes": 109006, + "zip_sha256": "078da3d0930e82647a0ed78139f423932621edc6eff5eaad2a5ba0036af6795e", + "verification": "Original ZIP byte length/SHA256 and CRC verified; every extracted file hashed; exact head.txt checked; each named bootstrap test has one pass and no package skip/fail.", + "source_commit": "1bc5b898d8085e707fb405ed0c8f84ca2454157f", + "source_tree": "e6f411497b048f54b8bd2d8ac4d3603710a2e57a", + "extraction": { + "source": "go-test.json", + "source_bytes": 1295649, + "source_sha256": "91f2da2314e636b9640c3b56fe7eaff84e07dc69efc5788cbdd9f7de103c8c0b", + "predicate": "Keep each original complete JSONL byte line exactly when json.loads(line).get(\"Package\") equals github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap; preserve original line order and line endings.", + "retained_lines": 104, + "filtered_out_lines": 5027 + }, + "runtime_tests": { + "TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant": { + "Time": "2026-10-24T23:37:59.468336735Z", + "Action": "pass", + "Package": "github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap", + "Test": "TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant", + "Elapsed": 0.08 + }, + "TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment": { + "Time": "2026-10-24T23:37:59.538405418Z", + "Action": "pass", + "Package": "github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap", + "Test": "TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment", + "Elapsed": 0.07 + }, + "TestDefaultRunnerCancellationStopsDescendants": { + "Time": "2026-10-24T23:37:59.583041637Z", + "Action": "pass", + "Package": "github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap", + "Test": "TestDefaultRunnerCancellationStopsDescendants", + "Elapsed": 0.04 + } + }, + "limits": "These three actual Linux bootstrap runtime tests passed; all nineteen ordinary process adapter tests were outside this workflow package inventory. Their qualification still requires the existing Required/test main-base full workspace lane. Local unsupported prerequisite SKIPs remain unqualified. This evidence does not establish native-client, MCP/model/auth/trust or release qualification.", + "preserved_file_sha256": { + "agentplugins-build.json": "d217f05dda21e30d40f69a816eda2101b3ebff6966fb94f6add98f3ae4a14fc6", + "archive-members.json": "11759bdfe9d0ea18e56820287403e49b890003b6251327198cba40b725fa3c8e", + "bootstrap-go-test.jsonl": "dcf2786b18df0b7c115624ae2ba5a1f20c289df5ff9b13d238902c2cad3c2ae6", + "go-env.json": "a6ee8aaad98ac1d26093df24d37bb2606d7e96131a7792eead0905059faf1b78", + "go-test.stderr": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "head.txt": "afdf566ecae42ade538799692923421cc16ff95e1d09fb317f7bdb97acf20285", + "packages.txt": "55c4fa388eabf217b6ad4c3ef321efa0aabb33135ea38ecca127fe4a975dfa6c", + "plugin-kit-ai-build.json": "9db49cd66a056be1d2472d2a409403ee063c7625c3d96118f7cb19210d61b934", + "summary.json": "9a3d00df8c41e16f20bebb74b070f4f2a66de92c65b808fc594b0beaec4cae4f", + "test-exit.txt": "9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa" + } +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/summary.json b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/summary.json new file mode 100644 index 000000000..9c8d530a1 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/summary.json @@ -0,0 +1,509 @@ +{ + "status": "proven", + "errors": [], + "skips": [ + { + "package": "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packagedigest", + "test": "TestSnapshotRejectsPortablePathAndContentHazards/special_file", + "output": "=== RUN TestSnapshotRejectsPortablePathAndContentHazards/special_file\n snapshot_test.go:89: Unix sockets unavailable: listen unix /home/runner/work/_temp/authoring-native-40143431447-1/tmp/TestSnapshotRejectsPortablePathAndContentHazardsspecial_file1631843451/001/socket: bind: invalid argument\n--- SKIP: TestSnapshotRejectsPortablePathAndContentHazards/special_file (0.00s)\n", + "classification": "optional-unproven" + } + ], + "e2e": [ + { + "package": "github.com/777genius/plugin-kit-ai/cli/internal/authoring/commands", + "test": "TestNativeBinaryVerticalSlice", + "entrypoint": "plugin-kit-ai", + "sha256": "246515a82996d53f5ed94d1c50435ea49b45527e098d63e49c093a48936520fe", + "revision": "1bc5b898d8085e707fb405ed0c8f84ca2454157f", + "read_profile": "packageview-local-linux-v1", + "templates": [ + "skill", + "mcp-remote", + "mcp-stdio", + "hybrid", + "hybrid-remote" + ], + "sdk": { + "@modelcontextprotocol/sdk": "1.32.1", + "package-lock-sha256": "8849941f9d0e58d2bf831fdd15ae129f6e72a3b500123a2c514f89bb1fca82b7" + } + }, + { + "package": "github.com/777genius/plugin-kit-ai/cli/internal/authoring/commands", + "test": "TestNativeBinaryVerticalSlice", + "entrypoint": "agentplugins", + "sha256": "b193d01f4709eac7d3c6ba84d7a93b13cecaa21a399e7b791ca46f6be7f7eaef", + "revision": "1bc5b898d8085e707fb405ed0c8f84ca2454157f", + "read_profile": "packageview-local-linux-v1", + "templates": [ + "skill", + "mcp-remote", + "mcp-stdio", + "hybrid", + "hybrid-remote" + ], + "sdk": { + "@modelcontextprotocol/sdk": "1.32.1", + "package-lock-sha256": "8849941f9d0e58d2bf831fdd15ae129f6e72a3b500123a2c514f89bb1fca82b7" + } + } + ], + "support_packages": [], + "scope": "linux-windows-offline-authoring-checkpoint", + "release_gates": { + "macos_writable": "unproven" + }, + "head": "1bc5b898d8085e707fb405ed0c8f84ca2454157f", + "go": { + "GOARCH": "amd64", + "GOHOSTARCH": "amd64", + "GOHOSTOS": "linux", + "GOOS": "linux", + "GOROOT": "/opt/hostedtoolcache/go/1.26.10/x64", + "GOVERSION": "go1.26.10" + }, + "host": { + "os": "linux", + "arch": "amd64", + "release": "6.17.0-1022-azure", + "runner_arch": "X64" + }, + "go_sha256": "9c2a37ae5b21ba1d5a004bb9ab4d0c64de6fc5778e9d30487adccea62678b8b5", + "binary_sha256": { + "agentplugins": "b193d01f4709eac7d3c6ba84d7a93b13cecaa21a399e7b791ca46f6be7f7eaef", + "plugin-kit-ai": "246515a82996d53f5ed94d1c50435ea49b45527e098d63e49c093a48936520fe" + }, + "executable_machines": { + "agentplugins": [ + "linux", + "amd64" + ], + "plugin-kit-ai": [ + "linux", + "amd64" + ], + "go": [ + "linux", + "amd64" + ] + }, + "builds": { + "agentplugins": { + "GoVersion": "go1.26.10", + "Path": "github.com/777genius/plugin-kit-ai/cli/cmd/agentplugins", + "Main": { + "Path": "github.com/777genius/plugin-kit-ai/cli", + "Version": "v0.0.0-20261024233519-1bc5b898d808" + }, + "Deps": [ + { + "Path": "charm.land/bubbles/v2", + "Version": "v2.0.0", + "Sum": "h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=" + }, + { + "Path": "charm.land/bubbletea/v2", + "Version": "v2.0.2", + "Sum": "h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=" + }, + { + "Path": "charm.land/huh/v2", + "Version": "v2.0.3", + "Sum": "h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=" + }, + { + "Path": "charm.land/lipgloss/v2", + "Version": "v2.0.1", + "Sum": "h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins", + "Version": "(devel)" + }, + { + "Path": "github.com/atotto/clipboard", + "Version": "v0.1.4", + "Sum": "h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=" + }, + { + "Path": "github.com/catppuccin/go", + "Version": "v0.2.0", + "Sum": "h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=" + }, + { + "Path": "github.com/charmbracelet/colorprofile", + "Version": "v0.4.2", + "Sum": "h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=" + }, + { + "Path": "github.com/charmbracelet/ultraviolet", + "Version": "v0.0.0-20260205113103-524a6607adb8", + "Sum": "h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=" + }, + { + "Path": "github.com/charmbracelet/x/ansi", + "Version": "v0.11.6", + "Sum": "h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=" + }, + { + "Path": "github.com/charmbracelet/x/exp/ordered", + "Version": "v0.1.0", + "Sum": "h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=" + }, + { + "Path": "github.com/charmbracelet/x/exp/strings", + "Version": "v0.0.0-20240722160745-212f7b056ed0", + "Sum": "h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=" + }, + { + "Path": "github.com/charmbracelet/x/term", + "Version": "v0.2.2", + "Sum": "h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=" + }, + { + "Path": "github.com/charmbracelet/x/termios", + "Version": "v0.1.1", + "Sum": "h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=" + }, + { + "Path": "github.com/charmbracelet/x/windows", + "Version": "v0.2.2", + "Sum": "h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=" + }, + { + "Path": "github.com/clipperhouse/displaywidth", + "Version": "v0.11.0", + "Sum": "h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=" + }, + { + "Path": "github.com/clipperhouse/uax29/v2", + "Version": "v2.7.0", + "Sum": "h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=" + }, + { + "Path": "github.com/creachadair/tomledit", + "Version": "v0.0.29", + "Sum": "h1:dB5CbdwJMpn/fmfAPTAAleXF/KJwY0Ggc1eL/zvZRgk=" + }, + { + "Path": "github.com/dlclark/regexp2", + "Version": "v1.12.0", + "Sum": "h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=" + }, + { + "Path": "github.com/dustin/go-humanize", + "Version": "v1.0.1", + "Sum": "h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=" + }, + { + "Path": "github.com/lucasb-eyer/go-colorful", + "Version": "v1.3.0", + "Sum": "h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=" + }, + { + "Path": "github.com/mattn/go-runewidth", + "Version": "v0.0.20", + "Sum": "h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=" + }, + { + "Path": "github.com/mitchellh/hashstructure/v2", + "Version": "v2.0.2", + "Sum": "h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=" + }, + { + "Path": "github.com/muesli/cancelreader", + "Version": "v0.2.2", + "Sum": "h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=" + }, + { + "Path": "github.com/pelletier/go-toml/v2", + "Version": "v2.3.0", + "Sum": "h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=" + }, + { + "Path": "github.com/rivo/uniseg", + "Version": "v0.4.7", + "Sum": "h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=" + }, + { + "Path": "github.com/santhosh-tekuri/jsonschema/v6", + "Version": "v6.0.3", + "Sum": "h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=" + }, + { + "Path": "github.com/spf13/cobra", + "Version": "v1.10.2", + "Sum": "h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=" + }, + { + "Path": "github.com/spf13/pflag", + "Version": "v1.0.9", + "Sum": "h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=" + }, + { + "Path": "github.com/tailscale/hujson", + "Version": "v0.0.0-20260302212456-ecc657c15afd", + "Sum": "h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=" + }, + { + "Path": "github.com/xo/terminfo", + "Version": "v0.0.0-20220910002029-abceb7e1c41e", + "Sum": "h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=" + }, + { + "Path": "golang.org/x/mod", + "Version": "v0.40.0", + "Sum": "h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=" + }, + { + "Path": "golang.org/x/sync", + "Version": "v0.22.0", + "Sum": "h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=" + }, + { + "Path": "golang.org/x/sys", + "Version": "v0.44.0", + "Sum": "h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=" + }, + { + "Path": "golang.org/x/term", + "Version": "v0.34.0", + "Sum": "h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=" + }, + { + "Path": "golang.org/x/text", + "Version": "v0.40.0", + "Sum": "h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=" + }, + { + "Path": "gopkg.in/yaml.v3", + "Version": "v3.0.1", + "Sum": "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=" + } + ], + "Settings": [ + { + "Key": "-buildmode", + "Value": "exe" + }, + { + "Key": "-compiler", + "Value": "gc" + }, + { + "Key": "-trimpath", + "Value": "true" + }, + { + "Key": "DefaultGODEBUG", + "Value": "cryptocustomrand=1,httpservecontentmaxranges=1,tlssecpmlkem=0,urlstrictcolons=0" + }, + { + "Key": "CGO_ENABLED", + "Value": "1" + }, + { + "Key": "GOARCH", + "Value": "amd64" + }, + { + "Key": "GOOS", + "Value": "linux" + }, + { + "Key": "GOAMD64", + "Value": "v1" + }, + { + "Key": "vcs", + "Value": "git" + }, + { + "Key": "vcs.revision", + "Value": "1bc5b898d8085e707fb405ed0c8f84ca2454157f" + }, + { + "Key": "vcs.time", + "Value": "2026-10-24T23:35:19Z" + }, + { + "Key": "vcs.modified", + "Value": "false" + } + ] + }, + "plugin-kit-ai": { + "GoVersion": "go1.26.10", + "Path": "github.com/777genius/plugin-kit-ai/cli/cmd/plugin-kit-ai", + "Main": { + "Path": "github.com/777genius/plugin-kit-ai/cli", + "Version": "v0.0.0-20261024233519-1bc5b898d808" + }, + "Deps": [ + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/plugininstall", + "Version": "(devel)" + }, + { + "Path": "github.com/777genius/plugin-kit-ai/sdk", + "Version": "(devel)" + }, + { + "Path": "github.com/cpuguy83/go-md2man/v2", + "Version": "v2.0.6", + "Sum": "h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=" + }, + { + "Path": "github.com/creachadair/tomledit", + "Version": "v0.0.29", + "Sum": "h1:dB5CbdwJMpn/fmfAPTAAleXF/KJwY0Ggc1eL/zvZRgk=" + }, + { + "Path": "github.com/dlclark/regexp2", + "Version": "v1.12.0", + "Sum": "h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=" + }, + { + "Path": "github.com/pelletier/go-toml/v2", + "Version": "v2.3.0", + "Sum": "h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=" + }, + { + "Path": "github.com/russross/blackfriday/v2", + "Version": "v2.1.0", + "Sum": "h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=" + }, + { + "Path": "github.com/santhosh-tekuri/jsonschema/v6", + "Version": "v6.0.3", + "Sum": "h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=" + }, + { + "Path": "github.com/spf13/cobra", + "Version": "v1.10.2", + "Sum": "h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=" + }, + { + "Path": "github.com/spf13/pflag", + "Version": "v1.0.9", + "Sum": "h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=" + }, + { + "Path": "github.com/tailscale/hujson", + "Version": "v0.0.0-20260302212456-ecc657c15afd", + "Sum": "h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=" + }, + { + "Path": "go.yaml.in/yaml/v3", + "Version": "v3.0.4", + "Sum": "h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=" + }, + { + "Path": "golang.org/x/mod", + "Version": "v0.40.0", + "Sum": "h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=" + }, + { + "Path": "golang.org/x/sys", + "Version": "v0.44.0", + "Sum": "h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=" + }, + { + "Path": "golang.org/x/text", + "Version": "v0.40.0", + "Sum": "h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=" + }, + { + "Path": "gopkg.in/yaml.v3", + "Version": "v3.0.1", + "Sum": "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=" + } + ], + "Settings": [ + { + "Key": "-buildmode", + "Value": "exe" + }, + { + "Key": "-compiler", + "Value": "gc" + }, + { + "Key": "-trimpath", + "Value": "true" + }, + { + "Key": "DefaultGODEBUG", + "Value": "cryptocustomrand=1,httpservecontentmaxranges=1,tlssecpmlkem=0,urlstrictcolons=0" + }, + { + "Key": "CGO_ENABLED", + "Value": "1" + }, + { + "Key": "GOARCH", + "Value": "amd64" + }, + { + "Key": "GOOS", + "Value": "linux" + }, + { + "Key": "GOAMD64", + "Value": "v1" + }, + { + "Key": "vcs", + "Value": "git" + }, + { + "Key": "vcs.revision", + "Value": "1bc5b898d8085e707fb405ed0c8f84ca2454157f" + }, + { + "Key": "vcs.time", + "Value": "2026-10-24T23:35:19Z" + }, + { + "Key": "vcs.modified", + "Value": "false" + } + ] + } + }, + "filesystem": { + "type": "ext4", + "mount_options": "rw,relatime", + "profile": "packageview-local-linux-v1" + }, + "packages": { + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packagedigest": "pass", + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader": "pass", + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/conformance": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoringcli": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/jsonmaint": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/mcpruntime": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/nativeimport": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/project": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/readiness": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/report": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/scaffold": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/skills": "pass", + "github.com/777genius/plugin-kit-ai/cli/internal/authoring/commands": "pass", + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packageview": "pass" + } +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/test-exit.txt b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/test-exit.txt new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/test-exit.txt @@ -0,0 +1 @@ +0 diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/live-thread-proc-proof.json b/docs/evidence/process-containment/linux-proc-children-unavailable/live-thread-proc-proof.json new file mode 100644 index 000000000..d04174b32 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/live-thread-proc-proof.json @@ -0,0 +1,20 @@ +{ + "pid": 1432581, + "captured_utc": "2026-10-24T21:55:06Z", + "task_entries": [ + "1432581" + ], + "thread_stat_before": "1432581 (python3) R 230 1432581 1432581 0 -1 4194560 6776 662 0 0 4 4 0 0 20 0 1 0 4995644 40361984 2368 18446744073709551615 4329472 32290168 140736656772704 0 0 0 0 16781317 2 0 0 0 17 5 0 0 0 0 0 23068672 32290168 939307008 140736656780220 140736656789644 140736656789644 140736656793518 0\n", + "children": { + "1432581": { + "errno": 2, + "error": "[Errno 2] No such file or directory: '/proc/1432581/task/1432581/children'" + } + }, + "thread_self_children": { + "errno": 2, + "error": "[Errno 2] No such file or directory: '/proc/thread-self/children'" + }, + "thread_stat_after": "1432581 (python3) R 230 1432581 1432581 0 -1 4194560 6785 662 0 0 4 4 0 0 20 0 1 0 4995644 40361984 2400 18446744073709551615 4329472 32290168 140736656772704 0 0 0 0 16781317 2 0 0 0 17 5 0 0 0 0 0 23068672 32290168 939307008 140736656780220 140736656789644 140736656789644 140736656793518 0\n", + "method": "read-only self proc checks; no child process launched" +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-final-lint.log b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-final-lint.log new file mode 100644 index 000000000..6a3ebaa7e --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-final-lint.log @@ -0,0 +1 @@ +0 issues. diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-final-tests.log b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-final-tests.log new file mode 100644 index 000000000..e56a098ed --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-final-tests.log @@ -0,0 +1,116 @@ +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/same_live_task_has_no_interface +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_exits_after_initial_identity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_is_replaced +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/missing_stat_inside_present_task_is_unknown +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/permission_is_not_unsupported +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/unknown_read_failure +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/ESRCH_for_still_live_task_is_not_unsupported +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_before_identity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_after_identity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/available_empty_interface +--- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/same_live_task_has_no_interface (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_exits_after_initial_identity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_is_replaced (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/missing_stat_inside_present_task_is_unknown (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/permission_is_not_unsupported (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/unknown_read_failure (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/ESRCH_for_still_live_task_is_not_unsupported (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_before_identity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_after_identity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/available_empty_interface (0.00s) +=== RUN TestOrdinaryMissingLiveChildrenRefusesBeforeWrappingOrLaunch +--- PASS: TestOrdinaryMissingLiveChildrenRefusesBeforeWrappingOrLaunch (0.00s) +=== RUN TestOrdinaryActualUnsupportedPreflightCannotLaunch +--- PASS: TestOrdinaryActualUnsupportedPreflightCannotLaunch (0.00s) +=== RUN TestBoundedProcObservationsAndStrictIdentities +--- PASS: TestBoundedProcObservationsAndStrictIdentities (0.00s) +=== RUN TestOrdinaryAvailableEmptyProcInterfacePermitsWrapping +--- PASS: TestOrdinaryAvailableEmptyProcInterfacePermitsWrapping (0.00s) +=== RUN TestProcTaskEnumerationAndNamesAreBounded +--- PASS: TestProcTaskEnumerationAndNamesAreBounded (0.02s) +=== RUN TestOSRunnerPreservesCommandOutputAndExitCode + osrunner_test.go:51: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565784 has no children interface (open /proc/1565784/task/1565784/children: no such file or directory) +--- SKIP: TestOSRunnerPreservesCommandOutputAndExitCode (0.00s) +=== RUN TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess + osrunner_test.go:70: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565784 has no children interface (open /proc/1565784/task/1565784/children: no such file or directory) +--- SKIP: TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess (0.00s) +=== RUN TestOSRunnerBoundsStdoutAndReturnsLimitError + osrunner_test.go:85: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565784 has no children interface (open /proc/1565784/task/1565784/children: no such file or directory) +--- SKIP: TestOSRunnerBoundsStdoutAndReturnsLimitError (0.00s) +=== RUN TestOSRunnerStdoutLimitStopsProducerBeforeRemainingOutput + osrunner_test.go:110: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565784 has no children interface (open /proc/1565784/task/1565784/children: no such file or directory) +--- SKIP: TestOSRunnerStdoutLimitStopsProducerBeforeRemainingOutput (0.00s) +=== RUN TestOSRunnerStdoutLimitPreservesNonzeroExit + osrunner_test.go:131: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565784 has no children interface (open /proc/1565784/task/1565784/children: no such file or directory) +--- SKIP: TestOSRunnerStdoutLimitPreservesNonzeroExit (0.00s) +=== RUN TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands + osrunner_test.go:149: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565784 has no children interface (open /proc/1565784/task/1565784/children: no such file or directory) +--- SKIP: TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands (0.00s) +=== RUN TestBoundedProcessWaitReturnsAtTheRealDeadline +--- PASS: TestBoundedProcessWaitReturnsAtTheRealDeadline (0.00s) +=== RUN TestOSRunnerBoundsStderrWhileRetainingDiagnosticEdges + osrunner_test.go:968: kernel duplex containment is not delegated to this test environment: safe duplex process containment unavailable; manual activation required: create duplex cgroup containment: delegated cgroup v2 subtree is unavailable: mkdir /sys/fs/cgroup/agentplugins-duplex-2941973900: read-only file system +--- SKIP: TestOSRunnerBoundsStderrWhileRetainingDiagnosticEdges (0.00s) +=== RUN TestOSRunnerSurvivesRemovedOwnExecutable + osrunner_tree_linux_test.go:45: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOSRunnerSurvivesRemovedOwnExecutable (0.00s) +=== RUN TestOSRunnerCancellationContainsImmediateSetsidDescendant + osrunner_tree_linux_test.go:133: kernel duplex containment is not delegated to this test environment: safe duplex process containment unavailable; manual activation required: create duplex cgroup containment: delegated cgroup v2 subtree is unavailable: mkdir /sys/fs/cgroup/agentplugins-duplex-2304544976: read-only file system +--- SKIP: TestOSRunnerCancellationContainsImmediateSetsidDescendant (0.00s) +=== RUN TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup + osrunner_tree_linux_test.go:204: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup (0.00s) +=== RUN TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable + osrunner_tree_linux_test.go:241: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable (0.00s) +=== RUN TestOrdinarySupervisorDescriptorsAreClosedBeforeCommandExec + osrunner_tree_linux_test.go:257: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOrdinarySupervisorDescriptorsAreClosedBeforeCommandExec (0.00s) +=== RUN TestSupervisorEmptyBoundaryRequiresQuiescentErrorFreePasses +--- PASS: TestSupervisorEmptyBoundaryRequiresQuiescentErrorFreePasses (0.00s) +=== RUN TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage +=== RUN TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/reused_PID +=== RUN TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/changed_parent +--- PASS: TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage (0.00s) + --- PASS: TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/reused_PID (0.00s) + --- PASS: TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/changed_parent (0.00s) +=== RUN TestLinuxSupervisorTraversalEnforcesProcessAndDeadlineBounds +--- PASS: TestLinuxSupervisorTraversalEnforcesProcessAndDeadlineBounds (0.00s) +=== RUN TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit + osrunner_tree_linux_test.go:401: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit (0.00s) +=== RUN TestOSRunnerRapidShellCommandsWithShortLivedChildrenPreserveNaturalExit + osrunner_tree_linux_test.go:415: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOSRunnerRapidShellCommandsWithShortLivedChildrenPreserveNaturalExit (0.00s) +=== RUN TestOSRunnerCleansSameProcessGroupMemberAfterLeaderNaturalExit + osrunner_tree_linux_test.go:519: kernel duplex containment is not delegated to this test environment: safe duplex process containment unavailable; manual activation required: create duplex cgroup containment: delegated cgroup v2 subtree is unavailable: mkdir /sys/fs/cgroup/agentplugins-duplex-521185841: read-only file system +--- SKIP: TestOSRunnerCleansSameProcessGroupMemberAfterLeaderNaturalExit (0.00s) +=== RUN TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit + osrunner_tree_linux_test.go:572: kernel duplex containment is not delegated to this test environment: safe duplex process containment unavailable; manual activation required: create duplex cgroup containment: delegated cgroup v2 subtree is unavailable: mkdir /sys/fs/cgroup/agentplugins-duplex-3025812953: read-only file system +--- SKIP: TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit (0.00s) +=== RUN TestOSRunnerDescendantGraceStillRejectsForcedCleanup + osrunner_tree_linux_test.go:602: kernel duplex containment is not delegated to this test environment: safe duplex process containment unavailable; manual activation required: create duplex cgroup containment: delegated cgroup v2 subtree is unavailable: mkdir /sys/fs/cgroup/agentplugins-duplex-588463277: read-only file system +--- SKIP: TestOSRunnerDescendantGraceStillRejectsForcedCleanup (0.00s) +=== RUN TestOSRunnerPreservesDiagnosticAndStatusWhileCleaningNaturalExitGroup + osrunner_tree_linux_test.go:633: kernel duplex containment is not delegated to this test environment: safe duplex process containment unavailable; manual activation required: create duplex cgroup containment: delegated cgroup v2 subtree is unavailable: mkdir /sys/fs/cgroup/agentplugins-duplex-900567211: read-only file system +--- SKIP: TestOSRunnerPreservesDiagnosticAndStatusWhileCleaningNaturalExitGroup (0.00s) +=== RUN TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession + osrunner_tree_linux_test.go:710: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565784/1565786 has no children interface (open /proc/1565784/task/1565786/children: no such file or directory) +--- SKIP: TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession (0.00s) +PASS +ok github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process 0.037s +=== RUN TestFinalVerificationToLaunchReplacementUsesCapturedStaging +--- PASS: TestFinalVerificationToLaunchReplacementUsesCapturedStaging (0.01s) +=== RUN TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant + process_linux_test.go:124: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565833/1565836 has no children interface (open /proc/1565833/task/1565836/children: no such file or directory) +--- SKIP: TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant (0.00s) +=== RUN TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment + process_linux_test.go:337: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565833/1565836 has no children interface (open /proc/1565833/task/1565836/children: no such file or directory) +--- SKIP: TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment (0.00s) +=== RUN TestDefaultRunnerCancellationStopsDescendants + process_linux_test.go:399: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1565833/1565836 has no children interface (open /proc/1565833/task/1565836/children: no such file or directory) +--- SKIP: TestDefaultRunnerCancellationStopsDescendants (0.00s) +PASS +ok github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap 0.025s diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-inert-tests.log b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-inert-tests.log new file mode 100644 index 000000000..ae6477e7e --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-inert-tests.log @@ -0,0 +1,46 @@ +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/same_live_task_has_no_interface +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_exits_after_initial_identity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_is_replaced +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/missing_stat_inside_present_task_is_unknown +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/permission_is_not_unsupported +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/unknown_read_failure +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/ESRCH_for_still_live_task_is_not_unsupported +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_before_identity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_after_identity +=== RUN TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/available_empty_interface +--- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/same_live_task_has_no_interface (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_exits_after_initial_identity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/task_is_replaced (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/missing_stat_inside_present_task_is_unknown (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/permission_is_not_unsupported (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/unknown_read_failure (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/ESRCH_for_still_live_task_is_not_unsupported (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_before_identity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/malformed_after_identity (0.00s) + --- PASS: TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity/available_empty_interface (0.00s) +=== RUN TestOrdinaryMissingLiveChildrenRefusesBeforeWrappingOrLaunch +--- PASS: TestOrdinaryMissingLiveChildrenRefusesBeforeWrappingOrLaunch (0.00s) +=== RUN TestOrdinaryActualUnsupportedPreflightCannotLaunch +--- PASS: TestOrdinaryActualUnsupportedPreflightCannotLaunch (0.00s) +=== RUN TestBoundedProcObservationsAndStrictIdentities +--- PASS: TestBoundedProcObservationsAndStrictIdentities (0.00s) +=== RUN TestOrdinaryAvailableEmptyProcInterfacePermitsWrapping +--- PASS: TestOrdinaryAvailableEmptyProcInterfacePermitsWrapping (0.00s) +=== RUN TestProcTaskEnumerationAndNamesAreBounded +--- PASS: TestProcTaskEnumerationAndNamesAreBounded (0.01s) +=== RUN TestBoundedProcessWaitReturnsAtTheRealDeadline +--- PASS: TestBoundedProcessWaitReturnsAtTheRealDeadline (0.00s) +=== RUN TestSupervisorEmptyBoundaryRequiresQuiescentErrorFreePasses +--- PASS: TestSupervisorEmptyBoundaryRequiresQuiescentErrorFreePasses (0.00s) +=== RUN TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage +=== RUN TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/reused_PID +=== RUN TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/changed_parent +--- PASS: TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage (0.00s) + --- PASS: TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/reused_PID (0.00s) + --- PASS: TestLinuxSupervisorTraversalRejectsChangedChildIdentityAndParentage/changed_parent (0.00s) +=== RUN TestLinuxSupervisorTraversalEnforcesProcessAndDeadlineBounds +--- PASS: TestLinuxSupervisorTraversalEnforcesProcessAndDeadlineBounds (0.00s) +PASS +ok github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process 0.021s diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-runtime-tests.log b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-runtime-tests.log new file mode 100644 index 000000000..0df78f503 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-runtime-tests.log @@ -0,0 +1,70 @@ +=== RUN TestOSRunnerPreservesCommandOutputAndExitCode + osrunner_test.go:46: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerPreservesCommandOutputAndExitCode (0.00s) +=== RUN TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess + osrunner_test.go:66: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess (0.00s) +=== RUN TestOSRunnerBoundsStdoutAndReturnsLimitError + osrunner_test.go:81: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerBoundsStdoutAndReturnsLimitError (0.00s) +=== RUN TestOSRunnerStdoutLimitStopsProducerBeforeRemainingOutput + osrunner_test.go:99: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerStdoutLimitStopsProducerBeforeRemainingOutput (0.00s) +=== RUN TestOSRunnerStdoutLimitPreservesNonzeroExit + osrunner_test.go:127: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerStdoutLimitPreservesNonzeroExit (0.00s) +=== RUN TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands + osrunner_test.go:145: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands (0.00s) +=== RUN TestOSRunnerBoundsStderrWhileRetainingDiagnosticEdges + osrunner_test.go:968: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerBoundsStderrWhileRetainingDiagnosticEdges (0.00s) +=== RUN TestOSRunnerSurvivesRemovedOwnExecutable + osrunner_tree_linux_test.go:28: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerSurvivesRemovedOwnExecutable (0.00s) +=== RUN TestOSRunnerCancellationContainsImmediateSetsidDescendant + osrunner_tree_linux_test.go:133: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerCancellationContainsImmediateSetsidDescendant (0.00s) +=== RUN TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup + osrunner_tree_linux_test.go:204: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup (0.00s) +=== RUN TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable + osrunner_tree_linux_test.go:241: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable (0.00s) +=== RUN TestOrdinarySupervisorDescriptorsAreClosedBeforeCommandExec + osrunner_tree_linux_test.go:257: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOrdinarySupervisorDescriptorsAreClosedBeforeCommandExec (0.00s) +=== RUN TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit + osrunner_tree_linux_test.go:401: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit (0.00s) +=== RUN TestOSRunnerRapidShellCommandsWithShortLivedChildrenPreserveNaturalExit + osrunner_tree_linux_test.go:415: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerRapidShellCommandsWithShortLivedChildrenPreserveNaturalExit (0.00s) +=== RUN TestOSRunnerCleansSameProcessGroupMemberAfterLeaderNaturalExit + osrunner_tree_linux_test.go:519: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerCleansSameProcessGroupMemberAfterLeaderNaturalExit (0.00s) +=== RUN TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit + osrunner_tree_linux_test.go:572: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit (0.00s) +=== RUN TestOSRunnerDescendantGraceStillRejectsForcedCleanup + osrunner_tree_linux_test.go:602: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerDescendantGraceStillRejectsForcedCleanup (0.00s) +=== RUN TestOSRunnerPreservesDiagnosticAndStatusWhileCleaningNaturalExitGroup + osrunner_tree_linux_test.go:633: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerPreservesDiagnosticAndStatusWhileCleaningNaturalExitGroup (0.00s) +=== RUN TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession + osrunner_tree_linux_test.go:687: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496355/1496355 has no children interface (open /proc/1496355/task/1496355/children: no such file or directory) +--- SKIP: TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession (0.00s) +PASS +ok github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process 0.004s +=== RUN TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant + process_linux_test.go:124: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496449/1496449 has no children interface (open /proc/1496449/task/1496449/children: no such file or directory) +--- SKIP: TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant (0.00s) +=== RUN TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment + process_linux_test.go:337: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496449/1496449 has no children interface (open /proc/1496449/task/1496449/children: no such file or directory) +--- SKIP: TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment (0.00s) +=== RUN TestDefaultRunnerCancellationStopsDescendants + process_linux_test.go:399: ordinary containment runtime is not qualified on this host: ordinary containment proc prerequisite is unavailable: live task 1496449/1496449 has no children interface (open /proc/1496449/task/1496449/children: no such file or directory) +--- SKIP: TestDefaultRunnerCancellationStopsDescendants (0.00s) +PASS +ok github.com/777genius/plugin-kit-ai/cli/internal/authoring/bootstrap 0.011s diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-validation-provenance.json b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-validation-provenance.json new file mode 100644 index 000000000..782df7872 --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/prerequisite-validation-provenance.json @@ -0,0 +1,43 @@ +{ + "source_base": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "source_state": "working candidate before frozen code review", + "go": "1.26.10", + "parallelism": "-p=1", + "inert_result": "PASS0.021s", + "runtime_result": "22 exact reviewed tests unsupported/skipped before descendant launch; NOT runtime containment qualification", + "runtime_log_boundary": "captured before final task-directory count refinement; prerequisite and22 guard locations unchanged; final inert actual OS.Run refusal rechecked after refinement", + "custom_runner_result": "PASS0.020s", + "vendor_or_auth_executed": false, + "members": { + "prerequisite-inert-tests.log": { + "bytes": 4116, + "sha256": "68487327795092933e10e5bd82d1419ba08c9dee7bb46a3d79177c038390f31a" + }, + "prerequisite-runtime-tests.log": { + "bytes": 9259, + "sha256": "cc9e5be9e6ca3362a81936ac1734930365a2d7bd88866ff7ea27fcfb9f216470" + } + }, + "final_followup": { + "base_commit": "4a1b536eadc9f4a5ff5485834ec5d2bad0fbbf39", + "source_state": "working final helper-placement/style correction before freeze", + "process_result": "PASS0.037s (inert); runtime unqualified", + "bootstrap_result": "PASS0.025s (custom Runner); runtime unqualified", + "runtime_skips": { + "ordinary_prerequisite": 16, + "existing_duplex_prerequisite": 6, + "total": 22 + }, + "lint": "golangci-lint2.13.2, process+bootstrap changed-source only, concurrency1, GOMEMLIMIT512MiB,90s,0issues", + "members": { + "prerequisite-final-lint.log": { + "bytes": 10, + "sha256": "e92606b0bf483111dff0a120c315ea165821348f31365020e2468a0059095c47" + }, + "prerequisite-final-tests.log": { + "bytes": 13777, + "sha256": "ff6ad342ce8e4fbd14cc4adb04ee0c2988a54b15960d281512102766ab52dfd3" + } + } + } +} diff --git a/docs/evidence/process-containment/linux-proc-children-unavailable/provenance.json b/docs/evidence/process-containment/linux-proc-children-unavailable/provenance.json new file mode 100644 index 000000000..d12c0f00b --- /dev/null +++ b/docs/evidence/process-containment/linux-proc-children-unavailable/provenance.json @@ -0,0 +1,12 @@ +{ + "source_commit": "4a1634f4906453eac0b87eddbcc54246226dcfdd", + "command": "strace -f -ttt -T -o /bootstrap-cancel-once.strace -e trace=wait4,waitid,kill,pidfd_send_signal,exit_group,clone,clone3,execve,close,pipe2,fcntl,dup3 python3 /run_go.py go test -p=1 ./cli/internal/authoring/bootstrap -run ^TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant$ -count=1 -v", + "result": "FAIL package1.440s, cleanup deadline1s; sole reaper retained ownership", + "full_trace_bytes": 2168606, + "full_trace_sha256": "c8b8fefef2f4cce4149fa41696b35e24e0517b246ca49ed514effaf55f601df4", + "full_trace_local_path": "/dev/shm/uap-project-runtime-5e2d9d9f7f5b/bootstrap-cancel-once.strace", + "excerpt_predicate": "literal exec of test, supervisor or npm fixture; manager process-group kill; supervisor self-kill; manager/supervisor reap records; fallback captured pidfd21 signal; setsid PID1428797 SIGKILL exit", + "excerpt_bytes": 2473, + "excerpt_sha256": "2abf1dc8b6c75980b1b665e40c19e8cf23bfa2a06b3e71c250cce35330cae733", + "qualification": "failed cancellation containment on environment lacking proc children interface; strace instrumentation disclosed; no vendor/auth execution" +} diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-empty-list.stderr.log b/docs/evidence/project-install/qoder-40011845496/plugins-empty-list.stderr.log new file mode 100644 index 000000000..315da0863 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-empty-list.stderr.log @@ -0,0 +1 @@ +[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-empty-list.stdout.log b/docs/evidence/project-install/qoder-40011845496/plugins-empty-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-empty-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-help.stderr.log b/docs/evidence/project-install/qoder-40011845496/plugins-help.stderr.log new file mode 100644 index 000000000..315da0863 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-help.stderr.log @@ -0,0 +1 @@ +[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-help.stdout.log b/docs/evidence/project-install/qoder-40011845496/plugins-help.stdout.log new file mode 100644 index 000000000..6da1ffc6b --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-help.stdout.log @@ -0,0 +1,20 @@ +Usage: qodercli plugins|plugin [options] [command] + +Manage plugins + +Options: + -h, --help display help for command + +Commands: + list [options] List installed plugins. + validate [options] Validates a plugin from a local directory + or manifest. + install|i [options] Install a plugin from available + marketplaces, a remote HTTPS .zip URL, or + a local directory path. + uninstall|remove [options] Uninstall a plugin. + enable [options] Enable a disabled plugin. + disable [options] [plugin] Disable an enabled plugin. + update [options] Update a plugin to the latest version + from its marketplace. + marketplace|mp Manage plugin marketplaces. diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-install-help.stderr.log b/docs/evidence/project-install/qoder-40011845496/plugins-install-help.stderr.log new file mode 100644 index 000000000..315da0863 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-install-help.stderr.log @@ -0,0 +1 @@ +[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-install-help.stdout.log b/docs/evidence/project-install/qoder-40011845496/plugins-install-help.stdout.log new file mode 100644 index 000000000..f7407c261 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-install-help.stdout.log @@ -0,0 +1,16 @@ +Usage: qodercli plugins install|i [options] + +Install a plugin from available marketplaces, a remote HTTPS .zip URL, or a +local directory path. + +Arguments: + plugin Plugin name, plugin@marketplace, remote HTTPS .zip URL, + or local plugin directory path. + +Options: + -s, --scope Installation scope: "user" (global, default), "project" + (shared, committed to VCS), or "local" (personal, not + committed). (choices: "user", "project", "local", + default: "user") + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-list-help.stderr.log b/docs/evidence/project-install/qoder-40011845496/plugins-list-help.stderr.log new file mode 100644 index 000000000..315da0863 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-list-help.stderr.log @@ -0,0 +1 @@ +[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-list-help.stdout.log b/docs/evidence/project-install/qoder-40011845496/plugins-list-help.stdout.log new file mode 100644 index 000000000..9cb9a927e --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-list-help.stdout.log @@ -0,0 +1,12 @@ +Usage: qodercli plugins list [options] + +List installed plugins. + +Options: + --json Output as JSON. (default: false) + --available Include available plugins from marketplaces + (requires --json). (default: false) + -o, --output-format The format of the CLI output. (choices: "text", + "json") + --plugin-dir Additional plugin directories to scan. + -h, --help display help for command diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-uninstall-help.stderr.log b/docs/evidence/project-install/qoder-40011845496/plugins-uninstall-help.stderr.log new file mode 100644 index 000000000..315da0863 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-uninstall-help.stderr.log @@ -0,0 +1 @@ +[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write diff --git a/docs/evidence/project-install/qoder-40011845496/plugins-uninstall-help.stdout.log b/docs/evidence/project-install/qoder-40011845496/plugins-uninstall-help.stdout.log new file mode 100644 index 000000000..4e9297a4d --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/plugins-uninstall-help.stdout.log @@ -0,0 +1,15 @@ +Usage: qodercli plugins uninstall|remove [options] + +Uninstall a plugin. + +Arguments: + plugin Plugin name to uninstall. + +Options: + -s, --scope Scope to uninstall from: "user" (global, default), + "project" (shared, committed to VCS), or "local" + (personal, not committed). (choices: "user", "project", + "local", default: "user") + --keep-data Preserve the plugin data directory. (default: false) + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-install/qoder-40011845496/provenance.json b/docs/evidence/project-install/qoder-40011845496/provenance.json new file mode 100644 index 000000000..f2ee7c00e --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/provenance.json @@ -0,0 +1,27 @@ +{ + "schema_version": 1, + "repository": "777genius/universal-agent-plugins", + "run_id": 40011845496, + "job_id": 120613942005, + "artifact_id": 12532010944, + "artifact_zip_sha256": "918333b4690c1076d1714a19e036c7e66d62625d58881ae216449590c980ac54", + "source_commit": "1dfe89a5eb79386333d50b82e1705c26659e79fc", + "success": false, + "project_qualified": false, + "recovery_redownload_verified": true, + "files": { + "plugins-empty-list.stderr.log": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "plugins-empty-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "plugins-help.stderr.log": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "plugins-help.stdout.log": "2604593dc2f18c63c963feb585665ee40ebfc18a3b592515102bb493b7856b29", + "plugins-install-help.stderr.log": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "plugins-install-help.stdout.log": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e", + "plugins-list-help.stderr.log": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "plugins-list-help.stdout.log": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b", + "plugins-uninstall-help.stderr.log": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "plugins-uninstall-help.stdout.log": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9", + "qoder-precode-research.json": "641fb54a8558958c4e09391226b7741dbe87e8a2567701b17e74494570cba560", + "version.stderr.log": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "version.stdout.log": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde" + } +} diff --git a/docs/evidence/project-install/qoder-40011845496/qoder-precode-research.json b/docs/evidence/project-install/qoder-40011845496/qoder-precode-research.json new file mode 100644 index 000000000..8ade0e2d3 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/qoder-precode-research.json @@ -0,0 +1,169 @@ +{ + "evidence_kind": "qoder-precode-version-help-empty-list", + "version": "1.1.78", + "target": "linux-amd64", + "source_commit": "1dfe89a5eb79386333d50b82e1705c26659e79fc", + "expected_probes": [ + "version", + "plugins-help", + "plugins-list-help", + "plugins-install-help", + "plugins-uninstall-help", + "plugins-empty-list" + ], + "probes": [ + { + "probe": "version", + "argv": [ + "/tmp/qoder-native-research-y0ow3shf/binary/qoder", + "--version" + ], + "cwd": "/tmp/qoder-native-research-y0ow3shf/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.591, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 7, + "stderr_bytes": 83, + "stdout_sha256": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde", + "stderr_sha256": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "accepted": true + }, + { + "probe": "plugins-help", + "argv": [ + "/tmp/qoder-native-research-y0ow3shf/binary/qoder", + "plugins", + "--help" + ], + "cwd": "/tmp/qoder-native-research-y0ow3shf/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.592, + "stdout_file": "plugins-help.stdout.log", + "stderr_file": "plugins-help.stderr.log", + "stdout_bytes": 958, + "stderr_bytes": 83, + "stdout_sha256": "2604593dc2f18c63c963feb585665ee40ebfc18a3b592515102bb493b7856b29", + "stderr_sha256": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "accepted": true + }, + { + "probe": "plugins-list-help", + "argv": [ + "/tmp/qoder-native-research-y0ow3shf/binary/qoder", + "plugins", + "list", + "--help" + ], + "cwd": "/tmp/qoder-native-research-y0ow3shf/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.594, + "stdout_file": "plugins-list-help.stdout.log", + "stderr_file": "plugins-list-help.stderr.log", + "stdout_bytes": 531, + "stderr_bytes": 83, + "stdout_sha256": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b", + "stderr_sha256": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "accepted": true + }, + { + "probe": "plugins-install-help", + "argv": [ + "/tmp/qoder-native-research-y0ow3shf/binary/qoder", + "plugins", + "install", + "--help" + ], + "cwd": "/tmp/qoder-native-research-y0ow3shf/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.592, + "stdout_file": "plugins-install-help.stdout.log", + "stderr_file": "plugins-install-help.stderr.log", + "stdout_bytes": 681, + "stderr_bytes": 83, + "stdout_sha256": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e", + "stderr_sha256": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "accepted": true + }, + { + "probe": "plugins-uninstall-help", + "argv": [ + "/tmp/qoder-native-research-y0ow3shf/binary/qoder", + "plugins", + "uninstall", + "--help" + ], + "cwd": "/tmp/qoder-native-research-y0ow3shf/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.612, + "stdout_file": "plugins-uninstall-help.stdout.log", + "stderr_file": "plugins-uninstall-help.stderr.log", + "stdout_bytes": 606, + "stderr_bytes": 83, + "stdout_sha256": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9", + "stderr_sha256": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "accepted": true + }, + { + "probe": "plugins-empty-list", + "argv": [ + "/tmp/qoder-native-research-y0ow3shf/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-native-research-y0ow3shf/project", + "error": null, + "exit": 0, + "elapsed_seconds": 1.356, + "stdout_file": "plugins-empty-list.stdout.log", + "stderr_file": "plugins-empty-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 83, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78", + "accepted": false + } + ], + "success": false, + "unobserved": [ + "nonempty project record schema", + "project installation/ownership", + "skill execution", + "MCP initialization", + "model/account/trust", + "native session restart" + ], + "runtime_network_isolation_claimed": false, + "acquisition": { + "url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.78/qodercli-linux-x64.tar.gz", + "version": "1.1.78", + "archive_integrity": "sha256:5e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea", + "archive_bytes": 78115289, + "binary_sha256": "0da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953", + "binary_bytes": 182982112, + "installer_executed": false + }, + "native_environment": { + "HOME": "/tmp/qoder-native-research-y0ow3shf/home", + "USERPROFILE": "/tmp/qoder-native-research-y0ow3shf/home", + "XDG_CONFIG_HOME": "/tmp/qoder-native-research-y0ow3shf/home/config", + "XDG_CACHE_HOME": "/tmp/qoder-native-research-y0ow3shf/home/cache", + "XDG_STATE_HOME": "/tmp/qoder-native-research-y0ow3shf/home/state", + "XDG_DATA_HOME": "/tmp/qoder-native-research-y0ow3shf/home/data", + "NPM_CONFIG_CACHE": "/tmp/qoder-native-research-y0ow3shf/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/qoder-native-research-y0ow3shf/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/qoder-native-research-y0ow3shf/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/qoder-native-research-y0ow3shf/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "TMPDIR": "/tmp/qoder-native-research-y0ow3shf/tmp", + "LANG": "C.UTF-8" + } +} diff --git a/docs/evidence/project-install/qoder-40011845496/version.stderr.log b/docs/evidence/project-install/qoder-40011845496/version.stderr.log new file mode 100644 index 000000000..315da0863 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/version.stderr.log @@ -0,0 +1 @@ +[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write diff --git a/docs/evidence/project-install/qoder-40011845496/version.stdout.log b/docs/evidence/project-install/qoder-40011845496/version.stdout.log new file mode 100644 index 000000000..8cf600ff1 --- /dev/null +++ b/docs/evidence/project-install/qoder-40011845496/version.stdout.log @@ -0,0 +1 @@ +1.1.78 diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-empty-list.stderr.log b/docs/evidence/project-install/qoder-40079977099/plugins-empty-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-empty-list.stdout.log b/docs/evidence/project-install/qoder-40079977099/plugins-empty-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/plugins-empty-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-help.stderr.log b/docs/evidence/project-install/qoder-40079977099/plugins-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-help.stdout.log b/docs/evidence/project-install/qoder-40079977099/plugins-help.stdout.log new file mode 100644 index 000000000..6da1ffc6b --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/plugins-help.stdout.log @@ -0,0 +1,20 @@ +Usage: qodercli plugins|plugin [options] [command] + +Manage plugins + +Options: + -h, --help display help for command + +Commands: + list [options] List installed plugins. + validate [options] Validates a plugin from a local directory + or manifest. + install|i [options] Install a plugin from available + marketplaces, a remote HTTPS .zip URL, or + a local directory path. + uninstall|remove [options] Uninstall a plugin. + enable [options] Enable a disabled plugin. + disable [options] [plugin] Disable an enabled plugin. + update [options] Update a plugin to the latest version + from its marketplace. + marketplace|mp Manage plugin marketplaces. diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-install-help.stderr.log b/docs/evidence/project-install/qoder-40079977099/plugins-install-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-install-help.stdout.log b/docs/evidence/project-install/qoder-40079977099/plugins-install-help.stdout.log new file mode 100644 index 000000000..f7407c261 --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/plugins-install-help.stdout.log @@ -0,0 +1,16 @@ +Usage: qodercli plugins install|i [options] + +Install a plugin from available marketplaces, a remote HTTPS .zip URL, or a +local directory path. + +Arguments: + plugin Plugin name, plugin@marketplace, remote HTTPS .zip URL, + or local plugin directory path. + +Options: + -s, --scope Installation scope: "user" (global, default), "project" + (shared, committed to VCS), or "local" (personal, not + committed). (choices: "user", "project", "local", + default: "user") + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-list-help.stderr.log b/docs/evidence/project-install/qoder-40079977099/plugins-list-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-list-help.stdout.log b/docs/evidence/project-install/qoder-40079977099/plugins-list-help.stdout.log new file mode 100644 index 000000000..9cb9a927e --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/plugins-list-help.stdout.log @@ -0,0 +1,12 @@ +Usage: qodercli plugins list [options] + +List installed plugins. + +Options: + --json Output as JSON. (default: false) + --available Include available plugins from marketplaces + (requires --json). (default: false) + -o, --output-format The format of the CLI output. (choices: "text", + "json") + --plugin-dir Additional plugin directories to scan. + -h, --help display help for command diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-uninstall-help.stderr.log b/docs/evidence/project-install/qoder-40079977099/plugins-uninstall-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-install/qoder-40079977099/plugins-uninstall-help.stdout.log b/docs/evidence/project-install/qoder-40079977099/plugins-uninstall-help.stdout.log new file mode 100644 index 000000000..4e9297a4d --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/plugins-uninstall-help.stdout.log @@ -0,0 +1,15 @@ +Usage: qodercli plugins uninstall|remove [options] + +Uninstall a plugin. + +Arguments: + plugin Plugin name to uninstall. + +Options: + -s, --scope Scope to uninstall from: "user" (global, default), + "project" (shared, committed to VCS), or "local" + (personal, not committed). (choices: "user", "project", + "local", default: "user") + --keep-data Preserve the plugin data directory. (default: false) + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-install/qoder-40079977099/provenance.json b/docs/evidence/project-install/qoder-40079977099/provenance.json new file mode 100644 index 000000000..3a9a65ad5 --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/provenance.json @@ -0,0 +1,10 @@ +{ + "repository": "777genius/universal-agent-plugins", + "run_id": 40079977099, + "job_id": 120831313308, + "artifact_id": 12555655148, + "source_commit": "62cdc6124804ec8c4030828fb976dc65b74bad47", + "artifact_zip_sha256": "1c5e0f3400451ebc1d8841f4df4a09f2049f2b8276c7526cf50eaf2161e85306", + "original_member_count": 13, + "all_12_streams_size_and_sha256_reverified": true +} diff --git a/docs/evidence/project-install/qoder-40079977099/qoder-precode-research.json b/docs/evidence/project-install/qoder-40079977099/qoder-precode-research.json new file mode 100644 index 000000000..d0463a2cf --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/qoder-precode-research.json @@ -0,0 +1,169 @@ +{ + "evidence_kind": "qoder-precode-version-help-empty-list", + "version": "1.1.78", + "target": "linux-amd64", + "source_commit": "62cdc6124804ec8c4030828fb976dc65b74bad47", + "expected_probes": [ + "version", + "plugins-help", + "plugins-list-help", + "plugins-install-help", + "plugins-uninstall-help", + "plugins-empty-list" + ], + "probes": [ + { + "probe": "version", + "argv": [ + "/tmp/qoder-native-research-eww69ey1/binary/qoder", + "--version" + ], + "cwd": "/tmp/qoder-native-research-eww69ey1/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.354, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 7, + "stderr_bytes": 0, + "stdout_sha256": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "plugins-help", + "argv": [ + "/tmp/qoder-native-research-eww69ey1/binary/qoder", + "plugins", + "--help" + ], + "cwd": "/tmp/qoder-native-research-eww69ey1/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.334, + "stdout_file": "plugins-help.stdout.log", + "stderr_file": "plugins-help.stderr.log", + "stdout_bytes": 958, + "stderr_bytes": 0, + "stdout_sha256": "2604593dc2f18c63c963feb585665ee40ebfc18a3b592515102bb493b7856b29", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "plugins-list-help", + "argv": [ + "/tmp/qoder-native-research-eww69ey1/binary/qoder", + "plugins", + "list", + "--help" + ], + "cwd": "/tmp/qoder-native-research-eww69ey1/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.347, + "stdout_file": "plugins-list-help.stdout.log", + "stderr_file": "plugins-list-help.stderr.log", + "stdout_bytes": 531, + "stderr_bytes": 0, + "stdout_sha256": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "plugins-install-help", + "argv": [ + "/tmp/qoder-native-research-eww69ey1/binary/qoder", + "plugins", + "install", + "--help" + ], + "cwd": "/tmp/qoder-native-research-eww69ey1/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.339, + "stdout_file": "plugins-install-help.stdout.log", + "stderr_file": "plugins-install-help.stderr.log", + "stdout_bytes": 681, + "stderr_bytes": 0, + "stdout_sha256": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "plugins-uninstall-help", + "argv": [ + "/tmp/qoder-native-research-eww69ey1/binary/qoder", + "plugins", + "uninstall", + "--help" + ], + "cwd": "/tmp/qoder-native-research-eww69ey1/project", + "error": null, + "exit": 0, + "elapsed_seconds": 0.336, + "stdout_file": "plugins-uninstall-help.stdout.log", + "stderr_file": "plugins-uninstall-help.stderr.log", + "stdout_bytes": 606, + "stderr_bytes": 0, + "stdout_sha256": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "plugins-empty-list", + "argv": [ + "/tmp/qoder-native-research-eww69ey1/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-native-research-eww69ey1/project", + "error": null, + "exit": 0, + "elapsed_seconds": 1.208, + "stdout_file": "plugins-empty-list.stdout.log", + "stderr_file": "plugins-empty-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + } + ], + "success": true, + "unobserved": [ + "nonempty project record schema", + "project installation/ownership", + "skill execution", + "MCP initialization", + "model/account/trust", + "native session restart" + ], + "runtime_network_isolation_claimed": false, + "acquisition": { + "url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.78/qodercli-linux-x64.tar.gz", + "version": "1.1.78", + "archive_integrity": "sha256:5e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea", + "archive_bytes": 78115289, + "binary_sha256": "0da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953", + "binary_bytes": 182982112, + "installer_executed": false + }, + "native_environment": { + "HOME": "/tmp/qoder-native-research-eww69ey1/home", + "USERPROFILE": "/tmp/qoder-native-research-eww69ey1/home", + "XDG_CONFIG_HOME": "/tmp/qoder-native-research-eww69ey1/home/config", + "XDG_CACHE_HOME": "/tmp/qoder-native-research-eww69ey1/home/cache", + "XDG_STATE_HOME": "/tmp/qoder-native-research-eww69ey1/home/state", + "XDG_DATA_HOME": "/tmp/qoder-native-research-eww69ey1/home/data", + "NPM_CONFIG_CACHE": "/tmp/qoder-native-research-eww69ey1/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/qoder-native-research-eww69ey1/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/qoder-native-research-eww69ey1/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/qoder-native-research-eww69ey1/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "TMPDIR": "/tmp/qoder-native-research-eww69ey1/tmp", + "LANG": "C.UTF-8" + } +} diff --git a/docs/evidence/project-install/qoder-40079977099/version.stderr.log b/docs/evidence/project-install/qoder-40079977099/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-install/qoder-40079977099/version.stdout.log b/docs/evidence/project-install/qoder-40079977099/version.stdout.log new file mode 100644 index 000000000..8cf600ff1 --- /dev/null +++ b/docs/evidence/project-install/qoder-40079977099/version.stdout.log @@ -0,0 +1 @@ +1.1.78 diff --git a/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/01-claude-version.log b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/01-claude-version.log new file mode 100644 index 000000000..870ddd4c2 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/01-claude-version.log @@ -0,0 +1 @@ +2.1.263 (Claude Code) diff --git a/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/README.md b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/README.md new file mode 100644 index 000000000..bb2db1955 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/README.md @@ -0,0 +1,16 @@ +# Claude project installation failure evidence + +The genuine pinned Claude Code 2.1.263 macOS arm64 project subtest failed at +production UAP add/activation. The three other user/runtime subtests in the +same job passed, but do not qualify this new project lane. + +See [provenance](provenance.json) for the exact commit/tree, official executable +pin, ZIP digest and run/job links. The three original project sub-artifacts +were verified against their summary and preserved byte-for-byte. + +The returned error says the plugin list is unrecognized. No raw project list +was captured before the test stopped. This evidence therefore establishes the +failure, not its parsing cause. A separately reviewed diagnostic-only amendment +will capture bounded read-only A/B lists after the first installer error while +retaining this failure, all parser assertions and the disposable-host guard. +Further repair/update/removal stages remain not evaluated. diff --git a/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/add-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/add-installer-stderr.log new file mode 100644 index 000000000..33ee16dc6 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/add-installer-stderr.log @@ -0,0 +1,3 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... +agentplugins: the Claude Code plugin list output is not recognized diff --git a/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/add-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/add-installer.json new file mode 100644 index 000000000..2afe5dc9b --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"failure","data":{"operation_id":"op-02ce0626fde3db8a727c95b549ff085a","batch":true,"status":"external_partial_failure","succeeded":0,"failed":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"claude","status":"external_failed","next_action":"retry client activation and verify client visibility; verify this plugin's authentication requirements before using it","error":{"stage":"activation","message":"the Claude Code plugin list output is not recognized"},"output":{"operation_id":"op-02ce0626fde3db8a727c95b549ff085a","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"retry client activation and verify client visibility; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"87aa2f4c-b061-4fbe-8c58-17fa7158be47","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-a3a0da15cba7","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"failed","authentication":"not_checked","policy":"allowed","verification":"failed","user_actions":["retry client activation and verify client visibility"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_failed","failure":{"stage":"activation","message":"the Claude Code plugin list output is not recognized"}}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/claude-project-plugin-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/claude-project-plugin-evidence.json new file mode 100644 index 000000000..e118a330d --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/claude-project-plugin-evidence.json @@ -0,0 +1,83 @@ +{ + "arch": "arm64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "add-installer-stderr.log": "26dadd703e77c67cc75810566807dd62b05f3f6fab0276a30b775a15ed4fae8a", + "add-installer.json": "e8944a5eca69d99f6f2998a92598650e79e1c35f35da3500529be2e5098f1b42" + }, + "client": "claude", + "client_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-23T20:12:39.009874Z", + "installer_sha256": "2c556b1b6c620d2d7af35eb01b84ff78160c81aea1c5b65f7aaeb05080fc4109", + "native_surface": "fresh exact project plugin listing and details; no session/model/trust/MCP initialization", + "os": "darwin", + "scanner": { + "archive_sha256": "be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57", + "platform": "darwin-arm64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "2d8102782861ca3005ca4b8c5572ebde26b91d08", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "c28ad3005b9668fb381f6606745cb041ca9863fd" + }, + "stages": { + "absent_repair": { + "status": "not_evaluated", + "reason": "not reached" + }, + "command_skill_discovery": { + "status": "not_evaluated", + "reason": "not reached" + }, + "explicit_command_withheld": { + "status": "not_evaluated", + "reason": "not reached" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no client declares explicit-invocation enforcement yet, so explicit-only commands are withheld" + }, + "install": { + "status": "failed", + "reason": "Attempt started but did not complete its assertions; inspect the test failure and transcript", + "artifacts": [ + "add-installer.json" + ] + }, + "intact_repair": { + "status": "not_evaluated", + "reason": "not reached" + }, + "invocation_change_update": { + "status": "not_evaluated", + "reason": "not reached" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "skills-only fixture; no MCP process or model session" + }, + "project_scope_preservation": { + "status": "not_evaluated", + "reason": "not reached" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no model, login or OAuth is used" + }, + "remove": { + "status": "not_evaluated", + "reason": "not reached" + }, + "unchanged_update": { + "status": "not_evaluated", + "reason": "not reached" + } + }, + "started_utc": "2026-10-23T20:12:37.712742Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/provenance.json b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/provenance.json new file mode 100644 index 000000000..9ef54563f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-darwin-arm64-40011624952/provenance.json @@ -0,0 +1,26 @@ +{ + "schema_version": 1, + "claim": "Observed failing project installation; not a successful project lifecycle", + "workflow_run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011624952", + "workflow_job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011624952/job/120613172265", + "artifact_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011624952/artifacts/12528049546", + "artifact_zip_sha256": "c45098c244379dd4e0b62f2dfb7e235b415959428e19cd235cf083652fb5caf7", + "artifact_original_prefix": "uap-claude-native-evidence-58929984/", + "source_commit": "2d8102782861ca3005ca4b8c5572ebde26b91d08", + "source_tree": "c28ad3005b9668fb381f6606745cb041ca9863fd", + "client_asset": { + "source": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-arm64/-/claude-code-darwin-arm64-2.1.263.tgz", + "version": "2.1.263", + "archive": "claude-code-darwin-arm64-2.1.263.tgz", + "archive_integrity": "sha512-yLv8MtgulGMGCWTwDUSmlEL0+94sxKP3vJm0SAXZX+0qVQ09PrjVSRC0ibtVeW7xymyhvP1JaUimOyp+t2wO5g==", + "binary_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9" + }, + "installer_sha256": "2c556b1b6c620d2d7af35eb01b84ff78160c81aea1c5b65f7aaeb05080fc4109", + "project_test": "TestAgentpluginsClaudeNativeProjectPluginLifecycle", + "project_test_result": "FAIL", + "project_test_seconds": 1.58, + "failure": "Production add returned external_partial_failure during activation: the Claude Code plugin list output is not recognized", + "diagnostic_gap": "No raw native project plugin list was captured before the failing assertion; this artifact cannot determine the response shape", + "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", + "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/README.md b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/README.md new file mode 100644 index 000000000..034a719f8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/README.md @@ -0,0 +1,41 @@ +# Pinned Claude project discovery spike + +This Linux amd64 spike ran the official Claude Code 2.1.263 binary in disposable +user/A/B directories on 2026-10-24. It establishes the untrusted native listing +schema, nested-cwd discovery boundary, enablement-id behavior, and native +skills-directory uninstall refusal. It did not run the UAP prepared lifecycle, +grant trust, log in, send a model prompt, or initialize an MCP server. + +`observations.json` preserves selected command records, exact stdout/stderr, +explicit fixture-only environment, cwd, exit and timing. `provenance.json` records +official package identity and measured digests. The archive SHA-512 matched +`package-metadata.json` before extraction. Paths name disposable fixture authority; +no current user profile or credential values were supplied. + +The untrusted native row is `(suppressed)@skills-dir`, version `unknown`, scope +`project`, enabled false, empty path and a nonempty explanatory notes array. It +supplies no installed package identity. With a same-name user copy, the user row +and suppression row coexist. Trusted winner selection remains unobserved; current +official rules describe user skills-directory precedence separately. + +Running the list command from A/nested omitted the parent whole plugin. Project +enablement toggled the same user-origin id inside A without changing B. Native +uninstall refused this origin and instructed directory removal. Removing only +the disposable A directory removed A's suppression row; B remained suppressed. + +For reproduction in a new disposable scratch directory, acquire the pinned +archive from the URL in provenance, verify its metadata integrity, safely extract +its regular `package/claude` file into `bin/package/claude`, and run `spike.py` +then `followup.py`. The scripts deliberately use a separate fixture-user HOME, +never a project's directory as a synthetic native user profile. They write only +their explicit fixture trees. No native trust is granted. A new reproduction is +a new observation and must keep its own hashes and outputs. + +The genuine source-only UAP harness is now named +`TestAgentpluginsClaudeNativeProjectPreparedLifecycle`. Its independent evidence +contract distinguishes passed owned-file lifecycle from blocked native skill +discovery and unevaluated MCP/model operation. A hosted run is still required; +these spike records cannot substitute for that qualification. The historic +[macOS project failure](../claude-2.1.263-darwin-arm64-40011624952/README.md) +remains preserved; its original raw output was absent, so this spike does not +retroactively assign its exact parsing cause. diff --git a/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/followup.py b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/followup.py new file mode 100644 index 000000000..945547cab --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/followup.py @@ -0,0 +1,36 @@ +from spike import * +import shutil + +run('08-interactive-no-prompt',[],A,timeout=10) +run('09-disable-help',['plugin','disable','--help'],A) +run('10-disable-project',['plugin','disable','uap-scope-spike@skills-dir','--scope','project'],A) +run('11-project-disabled-A',['plugin','list','--json'],A) +run('12-project-disabled-B',['plugin','list','--json'],B) +run('13-enable-project',['plugin','enable','uap-scope-spike@skills-dir','--scope','project'],A) +run('14-project-reenabled-A',['plugin','list','--json'],A) +run('15-disable-user',['plugin','disable','uap-scope-spike@skills-dir','--scope','user'],USER) +run('16-user-disabled-home',['plugin','list','--json'],USER) +run('17-user-disabled-A',['plugin','list','--json'],A) +run('18-user-disabled-B',['plugin','list','--json'],B) +run('19-native-uninstall-project',['plugin','uninstall','uap-scope-spike@skills-dir','--scope','project'],A) +run('20-after-uninstall-project-A',['plugin','list','--json'],A) +run('21-after-uninstall-project-B',['plugin','list','--json'],B) + +# Move the disposable user fixture outside its discovery directory, then inspect +# the project-only case. This is fixture mutation, not native activation/trust. +saved=BASE/'saved-user-fixture' +shutil.move(str(CONFIG/'skills/fixture-user'),str(saved)) +run('22-project-only-A',['plugin','list','--json'],A) +run('23-project-only-B',['plugin','list','--json'],B) +run('24-project-only-A-nested',['plugin','list','--json'],A/'nested') +shutil.rmtree(A/'.claude/skills/fixture-A') +run('25-owned-A-directory-removed',['plugin','list','--json'],A) +run('26-B-after-A-removal',['plugin','list','--json'],B) + +inventory=[] +for root in [USER,A,B]: + for p in sorted(root.rglob('*')): + if p.is_file(): + body=p.read_bytes() + inventory.append({'path':str(p.relative_to(BASE)),'size':len(body),'sha256':hashlib.sha256(body).hexdigest()}) +(OUT/'final-fixture-inventory.json').write_text(json.dumps(inventory,indent=2)+'\n') diff --git a/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/observations.json b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/observations.json new file mode 100644 index 000000000..455fa576a --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/observations.json @@ -0,0 +1,521 @@ +{ + "kind": "isolated_native_spike_not_uap_lifecycle", + "observed_date": "2026-10-24", + "platform": "linux-amd64", + "records": [ + { + "label": "00-version", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "--version" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.015, + "stdout": "2.1.263 (Claude Code)\n", + "stderr": "", + "source_record_sha256": "e0a770af20bea7d12b9221bcae5c47a47a0d9ae3f36691f5825ad875af589f89" + }, + { + "label": "02-empty-A", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.288, + "stdout": "[]\n", + "stderr": "", + "source_record_sha256": "9bcaf42989f194c48266db4360d1a0065b6c374970843e63a5d1d31dbdf34535" + }, + { + "label": "03-user-only-home", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.254, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": true,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\"\n }\n]\n", + "stderr": "", + "source_record_sha256": "eedcb1ec72921d7e87f0bece4e538a5c22e0d87ccb81dee863134120deb83ccc" + }, + { + "label": "05-user-and-A", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.313, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": true,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\"\n },\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "afccf5baedabe3c3fb7671cdc352687d90f2621e562596439e3f4bbe853dc7b1" + }, + { + "label": "07-user-and-A-nested", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A/nested", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.284, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": true,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\"\n }\n]\n", + "stderr": "", + "source_record_sha256": "fdca6df1b3b495cb641d25112288a1d50571a907495b4b80590426f52df327f3" + }, + { + "label": "10-disable-project", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "disable", + "uap-scope-spike@skills-dir", + "--scope", + "project" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 1.502, + "stdout": "\u2714 Successfully disabled plugin: uap-scope-spike\n", + "stderr": "", + "source_record_sha256": "02b7934358e58979cd4934e709b8ccec9244e26d1b8e7b8d28378bdfd306a21a" + }, + { + "label": "11-project-disabled-A", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.798, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": false,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\"\n },\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "b376d33a0dfcf60f25fa116fdff715b2ab9cc7d8d8a1a3c073bf24b3068b9e10" + }, + { + "label": "12-project-disabled-B", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/B", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.412, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": true,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\"\n },\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "b2bb1cedc98b7626556ba05f70133a24fa6dfe41bfe5a821cda62eb288fc8c40" + }, + { + "label": "15-disable-user", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "disable", + "uap-scope-spike@skills-dir", + "--scope", + "user" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.754, + "stdout": "\u2714 Successfully disabled plugin: uap-scope-spike\n", + "stderr": "", + "source_record_sha256": "aba148de9b2e67269902f86eb050d963633512080df22255408f7b4533739a96" + }, + { + "label": "16-user-disabled-home", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.828, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": false,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\"\n }\n]\n", + "stderr": "", + "source_record_sha256": "86663de5ad08a8e50f4a8815f669b716cfa50df750de7a406da19bb53844c13d" + }, + { + "label": "17-user-disabled-A", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.975, + "stdout": "[\n {\n \"id\": \"uap-scope-spike@skills-dir\",\n \"version\": \"1.0.0\",\n \"scope\": \"user\",\n \"enabled\": true,\n \"installPath\": \"/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude/skills/fixture-user\",\n \"notes\": [\n \"Disabled in ~/.claude/settings.json but still loads \u2014 project settings enable it, which overrides your user setting\"\n ]\n },\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "d197edae717aadfed7a785daac6a63b66835635a7d198d6b4ca45be5e226fcd6" + }, + { + "label": "19-native-uninstall-project", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "uninstall", + "uap-scope-spike@skills-dir", + "--scope", + "project" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 1, + "timed_out": false, + "elapsed_seconds": 0.637, + "stdout": "", + "stderr": "\u2718 Failed to uninstall plugin \"uap-scope-spike@skills-dir\": This plugin is loaded from ~/.claude/skills/ with no marketplace backing \u2014 it cannot be uninstalled. Delete the directory to remove it; `claude plugin disable` to turn it off; edits there take effect after /reload-plugins.\n", + "source_record_sha256": "6576d1bd5a8a175b9a9412be6b861d2c8ae0756eccef80b5befdb4722dc061c6" + }, + { + "label": "22-project-only-A", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.629, + "stdout": "[\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "e6ea15c7304d6d4185620d3164486803126faea5e6b1d43f17d370e95dd6aa25" + }, + { + "label": "23-project-only-B", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/B", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.963, + "stdout": "[\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "b656b9d137c96074631b9e4a40ffba9392db32eb81baf604ab7879e484c46714" + }, + { + "label": "24-project-only-A-nested", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A/nested", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.571, + "stdout": "[]\n", + "stderr": "", + "source_record_sha256": "40362b75e08bc125da3870a274e070c93e822a4646771cbd939687e2619cd038" + }, + { + "label": "25-owned-A-directory-removed", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/A", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.69, + "stdout": "[]\n", + "stderr": "", + "source_record_sha256": "bffc370f06e4966965706bf2715551f5535c38e6bb88895075b4e2f95629de5b" + }, + { + "label": "26-B-after-A-removal", + "argv": [ + "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/bin/package/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/projects/B", + "env": { + "HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user", + "USER": "spike-fixture", + "LOGNAME": "spike-fixture", + "PATH": "/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin", + "LANG": "C.UTF-8", + "TERM": "xterm-256color", + "TMPDIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/tmp", + "XDG_CONFIG_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.config", + "XDG_CACHE_HOME": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.cache", + "CLAUDE_CONFIG_DIR": "/workspace/scratch/5e2d9d9f7f5b/claude-project-spike/fixture-user/.claude", + "DISABLE_AUTOUPDATER": "1", + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" + }, + "exit_code": 0, + "timed_out": false, + "elapsed_seconds": 0.467, + "stdout": "[\n {\n \"id\": \"(suppressed)@skills-dir\",\n \"version\": \"unknown\",\n \"scope\": \"project\",\n \"enabled\": false,\n \"installPath\": \"\",\n \"notes\": [\n \"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies.\"\n ]\n }\n]\n", + "stderr": "", + "source_record_sha256": "80508ca19698c732fedc641ca512230c821b2ae456d98541d9df9d12978fc598" + } + ] +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/package-metadata.json b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/package-metadata.json new file mode 100644 index 000000000..b339c11a0 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/package-metadata.json @@ -0,0 +1 @@ +{"os":["linux"],"cpu":["x64"],"dist":{"shasum":"203a38b6ee9184f13957353f3e259f033288546a","tarball":"https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.263.tgz","fileCount":4,"integrity":"sha512-0IrvpLd/0FP0acQw59T4Cvx/r4nwAXKBrW0WyhIXymzYWurPCLztB+Icu9MkeewAUI+p3PTXsSfmilv/n6XlAQ==","signatures":[{"sig":"MEYCIQCBvXwQzy0Lz3qnmR23zj/FkyrveRs9GjyBJlRH92NIKwIhAJS5NS6AUO/aBN+5PB4fr/fKZYejjl1u3m3PE/YSdExv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIEcqWv+rnqmWIxzVIs2MoIVP3Js91nRQvUFy9twp1DSYAiA42/NFiZO8p5WOgTHuXgMOBKpxt1Qpu6T496kZA57deA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":215662650},"libc":["glibc"],"name":"@anthropic-ai/claude-code-linux-x64","_from":"file:staged-npm/anthropic-ai-claude-code-linux-x64-2.1.263.tgz","license":"SEE LICENSE IN LICENSE.md","_npmUser":{"name":"wolffiex","email":"wolffiex@anthropic.com"},"_resolved":"/home/runner/work/claude-cli-internal/claude-cli-internal/staged-npm/anthropic-ai-claude-code-linux-x64-2.1.263.tgz","_integrity":"sha512-0IrvpLd/0FP0acQw59T4Cvx/r4nwAXKBrW0WyhIXymzYWurPCLztB+Icu9MkeewAUI+p3PTXsSfmilv/n6XlAQ==","_npmVersion":"11.19.0","description":"Native binary for Claude Code on linux-x64","directories":{},"maintainers":[{"name":"zak-anthropic","email":"zak@anthropic.com"},{"name":"dylanc-anthropic","email":"dylanc@anthropic.com"},{"name":"benjmann","email":"ben@anthropic.com"},{"name":"nikhil-anthropic","email":"nikhil@anthropic.com"},{"name":"ejlangev-ant","email":"ethan.langevin@anthropic.com"},{"name":"jv-anthropic","email":"jv@anthropic.com"},{"name":"ollie-ant-2025","email":"ollie@anthropic.com"},{"name":"packy-anthropic","email":"packy@anthropic.com"},{"name":"noahz-anthropic","email":"noahz@anthropic.com"},{"name":"sbidasaria","email":"sbidasaria@anthropic.com"},{"name":"wolffiex","email":"wolffiex@anthropic.com"},{"name":"felixrieseberg-anthropic","email":"f@anthropic.com"},{"name":"joan-anthropic","email":"joan@anthropic.com"}],"_nodeVersion":"24.20.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/claude-code-linux-x64_2.1.263_1788660504986_0.9451069824318128","host":"s3://npm-registry-packages-npm-production"},"_id":"@anthropic-ai/claude-code-linux-x64@2.1.263","version":"2.1.263"} \ No newline at end of file diff --git a/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/provenance.json b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/provenance.json new file mode 100644 index 000000000..66a6c1e4d --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/provenance.json @@ -0,0 +1,16 @@ +{ + "package": "@anthropic-ai/claude-code-linux-x64", + "version": "2.1.263", + "source_url": "https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.263.tgz", + "archive_sha256": "8b6207348ad56fdcde085a0ad1f7cff0dfe06ce2c6c1bf97f69f1a1a7b6d0945", + "binary_sha256": "26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba", + "integrity_verification": "downloaded archive SHA512 equals official npm metadata integrity", + "production_source_inspected": "0cf52a6067b03bc36d3bb1f45ac2c6356f95ae52", + "project_trust_granted": false, + "model_requests": false, + "mcp_initialization": "not_evaluated", + "uap_prepared_lifecycle": "not_evaluated_by_this_spike", + "trusted_precedence": "not_evaluated", + "ordinary_directory_fixture_not_git_worktree": true, + "disclosure": "Records contain only explicitly constructed disposable fixture environment and paths; stdout/stderr strings retained exactly. No ambient credentials were supplied. Native process network calls were not claimed to be blocked. Archive/binary removed after verification to relieve scratch exhaustion." +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/spike.py b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/spike.py new file mode 100644 index 000000000..bde7316b8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/spike.py @@ -0,0 +1,54 @@ +from pathlib import Path +import hashlib, json, os, signal, subprocess, time + +BASE=Path(__file__).resolve().parent +BIN=BASE/'bin/package/claude' +USER=BASE/'fixture-user' +CONFIG=USER/'.claude' +A=BASE/'projects/A' +B=BASE/'projects/B' +OUT=BASE/'observations' +for p in [USER,CONFIG,A,B,A/'nested',B/'nested',OUT]: p.mkdir(parents=True,exist_ok=True) + +ENV={ + 'HOME':str(USER), 'USER':'spike-fixture', 'LOGNAME':'spike-fixture', + 'PATH':'/usr/local/bin:/usr/bin:/bin:/opt/codex/runtimes/codex-primary-runtime/dependencies/node/bin', + 'LANG':'C.UTF-8','TERM':'xterm-256color', 'TMPDIR':str(BASE/'tmp'), + 'XDG_CONFIG_HOME':str(USER/'.config'), 'XDG_CACHE_HOME':str(USER/'.cache'), + 'CLAUDE_CONFIG_DIR':str(CONFIG), 'DISABLE_AUTOUPDATER':'1', + 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC':'1', +} +Path(ENV['TMPDIR']).mkdir(exist_ok=True) + +def run(label,args,cwd=A,timeout=20): + start=time.time() + p=subprocess.Popen([str(BIN),*args],cwd=cwd,env=ENV,stdout=subprocess.PIPE,stderr=subprocess.PIPE,start_new_session=True) + timed_out=False + try: stdout,stderr=p.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + timed_out=True; os.killpg(p.pid,signal.SIGKILL); stdout,stderr=p.communicate() + record={'label':label,'argv':[str(BIN),*args],'cwd':str(cwd),'env':ENV,'exit_code':p.returncode,'timed_out':timed_out,'elapsed_seconds':round(time.time()-start,3),'stdout':stdout.decode(errors='replace'),'stderr':stderr.decode(errors='replace')} + (OUT/(label+'.json')).write_text(json.dumps(record,indent=2)+'\n') + print(json.dumps({k:v for k,v in record.items() if k not in ['env','argv']},ensure_ascii=False),flush=True) + return record + +def plugin(root,folder,version,marker): + p=root/'skills'/folder + (p/'.claude-plugin').mkdir(parents=True,exist_ok=True) + (p/'skills/marker').mkdir(parents=True,exist_ok=True) + (p/'.claude-plugin/plugin.json').write_text(json.dumps({'name':'uap-scope-spike','version':version,'description':'Disposable inert scope discovery fixture','skills':'./skills/'},indent=2)+'\n') + (p/'skills/marker/SKILL.md').write_text(f'---\nname: marker\ndescription: Inert scope marker {marker}.\n---\n{marker}\n') + return p + +if __name__=='__main__': + run('00-version',['--version'],USER) + run('01-list-help',['plugin','list','--help'],USER) + run('02-empty-A',['plugin','list','--json'],A) + plugin(CONFIG,'fixture-user','1.0.0','USER-V1') + run('03-user-only-home',['plugin','list','--json'],USER) + run('04-user-only-A',['plugin','list','--json'],A) + plugin(A/'.claude','fixture-A','2.0.0','PROJECT-A-V2') + run('05-user-and-A',['plugin','list','--json'],A) + plugin(B/'.claude','fixture-B','3.0.0','PROJECT-B-V3') + run('06-user-and-B',['plugin','list','--json'],B) + run('07-user-and-A-nested',['plugin','list','--json'],A/'nested') diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/README.md b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/README.md new file mode 100644 index 000000000..4474fcce1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/README.md @@ -0,0 +1,11 @@ +# Claude 2.1.263 hosted Prepared lifecycle success + +[Native Client Runtime run40139625617](https://github.com/777genius/universal-agent-plugins/actions/runs/40139625617) built committed source428bdf5d49e63b78bc4783df6b766c87c8e678ca, tree94f69fd2a2e60040c0718dfec6ea4a778be78a6a, without source patches. Both the Darwin arm64 and Windows amd64 Claude jobs succeeded. The official pinned binary reported `2.1.263 (Claude Code)`. Original runner provenance, source identity, binary hashes, raw project command stdout/stderr and lifecycle results are retained below each platform directory. + +Each platform ran and passed all four required tests, with no skipped tests: `TestAgentpluginsClaudeNativeCommandsLifecycle`, `TestAgentpluginsClaudeNativeLifecycle`, `TestAgentpluginsClaudeNativeProjectPreparedLifecycle` and `TestAgentpluginsClaudeNativeRuntimeLifecycle`. The project test passed initial add, repeated add, unchanged update, changed invocation update, intact repair, missing-owned-directory repair, inactive removal, projected command checks and user/A/B preservation. In particular, unchanged update now retained exact state and owned projection bytes after fresh native reobservation; the earlier genuine failure evidence remains preserved separately. + +The project result remains **Prepared+Installed under native suppression**. Fresh raw native lists retained the untrusted project suppression boundary. Project skill discovery and command-skill discovery are **blocked**. Project MCP initialization, real model command use and explicit invocation enforcement are **not evaluated**. Filesystem projection is not proof of native loading. Existing user-gate successes do not promote these project claims. No trust was granted. + +The original workflow records a disposable GitHub-hosted machine with explicit child environments and fresh profiles, rather than OS/process isolation from other runner activity. Network access was not blocked. Existing user runtime gates use scripted loopback model endpoints; they do not prove real model/OAuth operation. This run predates the later Linux ordinary-containment prerequisite changes and does not qualify their runtime behavior. + +The artifact ZIP lengths, SHA256 hashes and CRCs were verified locally. Every original runner and project `artifact_sha256` entry was independently checked against the extracted bytes. The full member manifests identify every archived file. All55 project files per platform, runner metadata, native test logs and small user-gate evidence summaries are preserved byte-for-byte here. Auxiliary user raw files, including mock gateway payloads, remain in the original GitHub artifacts and are identified by the ZIP/member hashes; they are not duplicated in this directory. No native client was executed locally during this preservation. `provenance.json` records the exact preservation boundary and member hashes. diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin-archive-manifest.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin-archive-manifest.json new file mode 100644 index 000000000..81f27c925 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin-archive-manifest.json @@ -0,0 +1,608 @@ +{ + "artifact_id": 12578221090, + "zip_bytes": 398277, + "zip_sha256": "269077ce2ae9e88aefa1d8a539fb194ea93fd4ec4c66f384a76865121e181b48", + "zip_crc_checked": true, + "members": [ + { + "name": "native-tests.log", + "bytes": 1173, + "sha256": "f17f0fa3abf048f94d59a4bd20fe6ec03f01c85bc2a94c3c423c654c83d725e1" + }, + { + "name": "runner-evidence.json", + "bytes": 19703, + "sha256": "3668152bea3c6b223698cdb3a50eb6b616cf1eb762ca3a0d59e9cccbd0721fa4" + }, + { + "name": "uap-claude-native-evidence-2149198670/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-claude-native-evidence-2149198670/02-add-v1.log", + "bytes": 4408, + "sha256": "7b8be042a11b1b11d1ef71cd1280120ba206076cd6c0098d3ae57ca6467c1d1d" + }, + { + "name": "uap-claude-native-evidence-2149198670/03-list-after-install.log", + "bytes": 296, + "sha256": "e05a4bd907e99c330aaa797daeea395af983773dfcdff46ab521830eaa6ff232" + }, + { + "name": "uap-claude-native-evidence-2149198670/04-details-after-install.log", + "bytes": 601, + "sha256": "f6d3b225f3f8d90d64fafe36b5ebe8c1ed46bf6867fa68f9897c08bbcda50b64" + }, + { + "name": "uap-claude-native-evidence-2149198670/05-mcp-list-after-install.log", + "bytes": 1266, + "sha256": "32fe2e78e244a328f5cf83f8a6c8e72e9e2db3bd5e18e774b7381fca1bf3ae9a" + }, + { + "name": "uap-claude-native-evidence-2149198670/06-update-v2.log", + "bytes": 2157, + "sha256": "67a15913bde0f4e8c6ad220f77820fc2fc197a25f44709cc669dca7d7ef7b870" + }, + { + "name": "uap-claude-native-evidence-2149198670/07-list-after-version-update.log", + "bytes": 296, + "sha256": "00e0bfb35c45c0e78a5d790bafc172fe707a7e14c38cbe59826d48906417dfa6" + }, + { + "name": "uap-claude-native-evidence-2149198670/08-update-same-version.log", + "bytes": 2157, + "sha256": "c462167e4c64a1c2b74e7d68723241786b3738c9f141a0f5efd8cdd22db73cfd" + }, + { + "name": "uap-claude-native-evidence-2149198670/09-update-unchanged.log", + "bytes": 2175, + "sha256": "f6865e9acd1507ea421527c2b2e9fe0d224cce0fe95dc03c53bdbe6d904c7420" + }, + { + "name": "uap-claude-native-evidence-2149198670/10-repair-absent.log", + "bytes": 1981, + "sha256": "f2451e8eb2ae4d68ab7f2a25fce150ffc421b08aa01d813316925e3aedfac704" + }, + { + "name": "uap-claude-native-evidence-2149198670/11-list-after-repair.log", + "bytes": 296, + "sha256": "00e0bfb35c45c0e78a5d790bafc172fe707a7e14c38cbe59826d48906417dfa6" + }, + { + "name": "uap-claude-native-evidence-2149198670/12-details-after-repair.log", + "bytes": 601, + "sha256": "61091ab19d98cc042ceb8919e12a7bd1efcef34cc8166fb2ca79296909c16831" + }, + { + "name": "uap-claude-native-evidence-2149198670/13-remove-blocked-by-foreign-content.log", + "bytes": 1106, + "sha256": "e36c3fb960fade53bafca2570d4a57e660dc125d2563280d63ab2666a77bb450" + }, + { + "name": "uap-claude-native-evidence-2149198670/14-remove.log", + "bytes": 1008, + "sha256": "ca0cf263cacd02d9b12ff11c283b48ed9560bfdfdb013375619639dc409fb103" + }, + { + "name": "uap-claude-native-evidence-2149198670/15-list-after-remove.log", + "bytes": 275, + "sha256": "d31b52f9ea36ed5c5e6b6b5ac9a95f465059a7d197a1e3995d3b1e98baecb305" + }, + { + "name": "uap-claude-native-evidence-2149198670/16-repeat-remove.log", + "bytes": 63, + "sha256": "9f7e8a6d254b5cfd57ccbb292dd645cb88a62042fc82f7c940dacec0abbcff53" + }, + { + "name": "uap-claude-native-evidence-2149198670/evidence.json", + "bytes": 4197, + "sha256": "a91556466bdd6c52344217ee64117154160760106e936e685d8fad2506f58a6c" + }, + { + "name": "uap-claude-native-evidence-2149198670/remove-inventory-diff.json", + "bytes": 1969, + "sha256": "cbbb57653f25f743adecf46538fb2dff02040b6f50faee23b7e33d56c94d969f" + }, + { + "name": "uap-claude-native-evidence-3075974034/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-claude-native-evidence-3075974034/02-skills-after-add-list.log", + "bytes": 298, + "sha256": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73" + }, + { + "name": "uap-claude-native-evidence-3075974034/03-skills-after-add-details.log", + "bytes": 624, + "sha256": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527" + }, + { + "name": "uap-claude-native-evidence-3075974034/04-skills-after-unchanged-update-list.log", + "bytes": 298, + "sha256": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73" + }, + { + "name": "uap-claude-native-evidence-3075974034/05-skills-after-unchanged-update-details.log", + "bytes": 624, + "sha256": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527" + }, + { + "name": "uap-claude-native-evidence-3075974034/06-skills-after-invocation-change-list.log", + "bytes": 298, + "sha256": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73" + }, + { + "name": "uap-claude-native-evidence-3075974034/07-skills-after-invocation-change-details.log", + "bytes": 632, + "sha256": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482" + }, + { + "name": "uap-claude-native-evidence-3075974034/08-skills-after-intact-repair-list.log", + "bytes": 298, + "sha256": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73" + }, + { + "name": "uap-claude-native-evidence-3075974034/09-skills-after-intact-repair-details.log", + "bytes": 632, + "sha256": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482" + }, + { + "name": "uap-claude-native-evidence-3075974034/10-list-before-absent-repair.log", + "bytes": 298, + "sha256": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73" + }, + { + "name": "uap-claude-native-evidence-3075974034/11-skills-after-absent-repair-list.log", + "bytes": 298, + "sha256": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73" + }, + { + "name": "uap-claude-native-evidence-3075974034/12-skills-after-absent-repair-details.log", + "bytes": 632, + "sha256": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482" + }, + { + "name": "uap-claude-native-evidence-3075974034/13-skills-after-remove-list.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-3075974034/add-installer-stderr.log", + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + { + "name": "uap-claude-native-evidence-3075974034/add-installer.json", + "bytes": 3724, + "sha256": "199b3d3dd999aa86bdf07f64e15403944d2f96ca494dd53ef6edbeffbe336fed" + }, + { + "name": "uap-claude-native-evidence-3075974034/commands-evidence.json", + "bytes": 5612, + "sha256": "67284c1c082f6887c7e3e07c6e325cb803cc39f5597e7d6b0b300a9705241723" + }, + { + "name": "uap-claude-native-evidence-3075974034/remove-installer-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-3075974034/remove-installer.json", + "bytes": 1304, + "sha256": "e781b71e7d52dbf68710b9de262ab44d521fc34915ce906aab79e00f8190e26f" + }, + { + "name": "uap-claude-native-evidence-3075974034/repair-absent-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-3075974034/repair-absent-installer.json", + "bytes": 2155, + "sha256": "c09caa33c6cfe0c33176f7dcf20a9ca69ade51518f05156793e3a115d32dac7c" + }, + { + "name": "uap-claude-native-evidence-3075974034/repair-intact-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-3075974034/repair-intact-installer.json", + "bytes": 2155, + "sha256": "8c7187b6cbbc4fbac632959eebdecc91ea865df6dedc2669c42da211ee9aabb4" + }, + { + "name": "uap-claude-native-evidence-3075974034/update-invocation-change-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-3075974034/update-invocation-change-installer.json", + "bytes": 2322, + "sha256": "ba4334a94932b3d4a9700d1624f0066e3ea7791a05e5eb61a6d612d97de370c8" + }, + { + "name": "uap-claude-native-evidence-3075974034/update-unchanged-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-3075974034/update-unchanged-installer.json", + "bytes": 2340, + "sha256": "f57967e66fda2b95cdde9e378e34c34926ab5ef58d1998e7486738db26a2a8e1" + }, + { + "name": "uap-claude-native-evidence-666876926/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-claude-native-evidence-666876926/add-installer-stderr.log", + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + { + "name": "uap-claude-native-evidence-666876926/add-installer.json", + "bytes": 4227, + "sha256": "51b38d67f9d7c913e1abe012775efcddd3a6d587a3a97a1896cdc70ac8ad44e9" + }, + { + "name": "uap-claude-native-evidence-666876926/add-repeated-installer-stderr.log", + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + { + "name": "uap-claude-native-evidence-666876926/add-repeated-installer.json", + "bytes": 2756, + "sha256": "5ea1994ece17a0b46e98b034a60e6e992c81760a9adbcf537668c5b4b007e7e0" + }, + { + "name": "uap-claude-native-evidence-666876926/after-add-native-A.json", + "bytes": 401, + "sha256": "72ff1eb64df99ecb676641312b08b887be65ae2b92b35f7cf4ebcf660e22902d" + }, + { + "name": "uap-claude-native-evidence-666876926/after-add-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-add-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-666876926/after-add-native-B.json", + "bytes": 402, + "sha256": "57b81cedb3c7c40e638404f6b7655f25ff51003c70e005319350d4821f580da3" + }, + { + "name": "uap-claude-native-evidence-666876926/after-add-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-add-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.json", + "bytes": 402, + "sha256": "cd2e882f234ff656e80d5cb2db4a94e61ef39c386a569e67acb8880fd60b1fc4" + }, + { + "name": "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.json", + "bytes": 402, + "sha256": "e309ac269bfb1a785ec1fce86bfa5c23e9c2a1b3da73e5ad66328e7df97b32d2" + }, + { + "name": "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-remove-native-A.json", + "bytes": 402, + "sha256": "956209b3fa2d84c9910bb0033d2a555ad9ef122eb5025117de27a872038bb2a6" + }, + { + "name": "uap-claude-native-evidence-666876926/after-remove-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-remove-native-A.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-remove-native-B.json", + "bytes": 402, + "sha256": "cba3fb0fde3f2a19b36801eb2115729a2e1bfe8a63ea0bc6be3d50944101cc93" + }, + { + "name": "uap-claude-native-evidence-666876926/after-remove-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-remove-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repair-native-A.json", + "bytes": 401, + "sha256": "6e1e7e35642ca32841a00017c5fc4b6b561079977d2312c44bc2a12595df7999" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repair-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repair-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repair-native-B.json", + "bytes": 402, + "sha256": "512a3b1df605cf7f22c6d8cc6b33c965a8d5d053606ad5fafdf23dea4db3f709" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repair-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repair-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repeated-add-native-A.json", + "bytes": 401, + "sha256": "bca261df54aa2ddddf4a9525be887a96d622534bbccf98e66e73e564ae0e1698" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repeated-add-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repeated-add-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repeated-add-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repeated-add-native-B.json", + "bytes": 402, + "sha256": "8f7a7c1ce4565e162cdca887103d7f5964cb8a66ac8255ba573fb2915b7ceec5" + }, + { + "name": "uap-claude-native-evidence-666876926/after-repeated-add-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/after-update-native-A.json", + "bytes": 401, + "sha256": "f0a9c71d7d67b1368019d820c1969196c0c03b83e6e22c97b53e30cea50008e8" + }, + { + "name": "uap-claude-native-evidence-666876926/after-update-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-update-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-666876926/after-update-native-B.json", + "bytes": 402, + "sha256": "d3b4637bcea7ec7fad007f69e74b8f52b439465d6cce831ca182bfead906c009" + }, + { + "name": "uap-claude-native-evidence-666876926/after-update-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/after-update-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-666876926/binding-after-add.json", + "bytes": 379, + "sha256": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb" + }, + { + "name": "uap-claude-native-evidence-666876926/binding-after-repair.json", + "bytes": 379, + "sha256": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb" + }, + { + "name": "uap-claude-native-evidence-666876926/binding-after-update.json", + "bytes": 379, + "sha256": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb" + }, + { + "name": "uap-claude-native-evidence-666876926/claude-project-plugin-evidence.json", + "bytes": 9918, + "sha256": "867a019d5f4f6492f541a9b99f489ee62882daf9d494e18acaacedc54e2cc8a4" + }, + { + "name": "uap-claude-native-evidence-666876926/remove-installer-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-666876926/remove-installer.json", + "bytes": 1446, + "sha256": "f0e26700d32f82303cf79fa2cafb4fb09a34f0731970dc4e603df4c0cfdf4910" + }, + { + "name": "uap-claude-native-evidence-666876926/repair-absent-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-666876926/repair-absent-installer.json", + "bytes": 2666, + "sha256": "296aca41b8ab64984bac576992b172638d7cb451cd53c10ef00213f89fb64df6" + }, + { + "name": "uap-claude-native-evidence-666876926/repair-intact-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-666876926/repair-intact-installer.json", + "bytes": 2666, + "sha256": "617e2e61b96c53300cd3173ddc8ffa3f578d31a1ad37976d7a92a7a23c64cbae" + }, + { + "name": "uap-claude-native-evidence-666876926/update-invocation-change-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-666876926/update-invocation-change-installer.json", + "bytes": 2833, + "sha256": "8efe9fffa841055fe6215b8ffa8a9d4f2b97f92538788363180131f4644bea76" + }, + { + "name": "uap-claude-native-evidence-666876926/update-unchanged-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-666876926/update-unchanged-installer.json", + "bytes": 2851, + "sha256": "18bb4f448f7b907d05aa92bbe8f68047edd962dde4ba937a4e4aed1401b53e02" + }, + { + "name": "uap-native-evidence-1860064462/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-native-evidence-1860064462/02-add.log", + "bytes": 3994, + "sha256": "ff86207443db268006df50f9682df6c8db46f2b1f142f03eacdb9dad185de242" + }, + { + "name": "uap-native-evidence-1860064462/03-installed.log", + "bytes": 275, + "sha256": "b30c8d0bfd92af75139c4e1b736210397d668d9b6470021cde142a4523955e17" + }, + { + "name": "uap-native-evidence-1860064462/04-runtime-A.log", + "bytes": 15178, + "sha256": "6d34ed72d4781fdb5afa1001688e74f3edf3f51e2e9c7a369397413656b354f9" + }, + { + "name": "uap-native-evidence-1860064462/05-update.log", + "bytes": 2131, + "sha256": "5b6267d8f62bb7f09371f49077fcd1c43bd685e82152f932c8ee7f524f1a7374" + }, + { + "name": "uap-native-evidence-1860064462/06-runtime-B.log", + "bytes": 15170, + "sha256": "ae9a8c59e9a0e6587118d83c17a2bc77e62e78e3fbb1f56f578170c782be8b60" + }, + { + "name": "uap-native-evidence-1860064462/07-same-version-update.log", + "bytes": 2131, + "sha256": "20944bbc5ca720f558618af5bf08c907fd1d59f29b8bb000de01db8b7745ad5c" + }, + { + "name": "uap-native-evidence-1860064462/08-runtime-C.log", + "bytes": 15174, + "sha256": "9c0948300572cb8e278b4293bbc0d6c723b4eb1efea0f78e32e3939fc9c2b434" + }, + { + "name": "uap-native-evidence-1860064462/09-repair.log", + "bytes": 1955, + "sha256": "488bd52f56fea47ca5a51ce48eb7efe3b246ea1d98055138bb94a056f43b29b7" + }, + { + "name": "uap-native-evidence-1860064462/10-runtime-repair.log", + "bytes": 15174, + "sha256": "4f5175b3179892d6c483a325c6d7e6d80cd20a3687de8df0c997a78d08eceb3d" + }, + { + "name": "uap-native-evidence-1860064462/11-remove.log", + "bytes": 1272, + "sha256": "fe9290c8337d3d0df4326823e0a690b88f7cca7637e1c06135ef8107092c09b1" + }, + { + "name": "uap-native-evidence-1860064462/events.jsonl", + "bytes": 17058, + "sha256": "701e0666ee52f1d6ad5962f93109f432becbf3c4da87f025367bc3cf83a6c937" + }, + { + "name": "uap-native-evidence-1860064462/12-removed.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-native-evidence-1860064462/evidence.json", + "bytes": 3032, + "sha256": "c86cb6675ef9cfdce375ce94f9d26da5b52114c565b2bde37327911fa84162ec" + }, + { + "name": "uap-native-evidence-1860064462/http-events.jsonl", + "bytes": 5867, + "sha256": "e179fac70a37e344636a7989ba0862f4df8a0bfba37ca11a98a0ae3cd0b80959" + }, + { + "name": "uap-native-evidence-1860064462/runtime-A-gateway.json", + "bytes": 255807, + "sha256": "88ec27468869e016ca8f0e325fddfb67f1d9da77e4c130de41e0684df855dd0b" + }, + { + "name": "uap-native-evidence-1860064462/runtime-C-gateway.json", + "bytes": 255804, + "sha256": "e7419a57e2397e289d5bf648524a2b1d453c348d6414e47cba864ee84cec8dfc" + }, + { + "name": "uap-native-evidence-1860064462/runtime-B-gateway.json", + "bytes": 255804, + "sha256": "d40d943e5dd95c591f97c4d3cb9cbaf3fdcd96405318cc0cc488e99ac0378f74" + }, + { + "name": "uap-native-evidence-1860064462/runtime-repair-gateway.json", + "bytes": 255804, + "sha256": "60fce8f594fb239ff89b637208122acbad7165df4637cb480027625f8c482a1c" + } + ] +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/native-tests.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/native-tests.log new file mode 100644 index 000000000..96a75a4cf --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/native-tests.log @@ -0,0 +1,13 @@ +=== RUN TestAgentpluginsClaudeNativeCommandsLifecycle + agentplugins_claude_commands_native_e2e_test.go:46: claude native evidence: /Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-3075974034 +--- PASS: TestAgentpluginsClaudeNativeCommandsLifecycle (9.46s) +=== RUN TestAgentpluginsClaudeNativeLifecycle + agentplugins_claude_native_e2e_test.go:376: claude native evidence: /Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-2149198670 +--- PASS: TestAgentpluginsClaudeNativeLifecycle (10.49s) +=== RUN TestAgentpluginsClaudeNativeProjectPreparedLifecycle + agentplugins_claude_project_native_e2e_test.go:16: claude native evidence: /Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926 +--- PASS: TestAgentpluginsClaudeNativeProjectPreparedLifecycle (10.66s) +=== RUN TestAgentpluginsClaudeNativeRuntimeLifecycle + agentplugins_claude_runtime_fixture_test.go:224: native evidence: /Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-native-evidence-1860064462 +--- PASS: TestAgentpluginsClaudeNativeRuntimeLifecycle (19.78s) +PASS diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/runner-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/runner-evidence.json new file mode 100644 index 000000000..fdf2a6901 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/runner-evidence.json @@ -0,0 +1,181 @@ +{ + "schema_version": 1, + "client": "claude", + "target": "darwin-arm64", + "started_utc": "2026-10-24T22:42:55Z", + "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", + "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof", + "status": "passed", + "scratch": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz", + "commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a", + "client_asset": { + "source": "https://registry.npmjs.org/@anthropic-ai/claude-code-darwin-arm64/-/claude-code-darwin-arm64-2.1.263.tgz", + "version": "2.1.263", + "archive": "claude-code-darwin-arm64-2.1.263.tgz", + "archive_integrity": "sha512-yLv8MtgulGMGCWTwDUSmlEL0+94sxKP3vJm0SAXZX+0qVQ09PrjVSRC0ibtVeW7xymyhvP1JaUimOyp+t2wO5g==", + "binary_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9" + }, + "scanner_asset": { + "source": "https://github.com/777genius/lintai/releases/tag/v0.1.3", + "version": "v0.1.3", + "archive": "lintai-v0.1.3-aarch64-apple-darwin.tar.gz", + "archive_integrity": "sha256:be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57" + }, + "ripgrep_asset": { + "source": "https://github.com/BurntSushi/ripgrep/releases/tag/15.2.0", + "version": "15.2.0", + "archive": "ripgrep-15.2.0-aarch64-apple-darwin.tar.gz", + "archive_integrity": "sha256:3750b2e93f37e0c692657da574d7019a101c0084da05a790c83fd335bad973e4", + "binary_sha256": "a326a1fb48074202e9ad41e4cd1e389eeea372c8c6f7d7e80da81176d5d9430e" + }, + "harness_build_sha256": { + "native-probe": "1f3cac519ee35fc85276bf03f1f750fc8615745f09ad6e6543189386e93eebb2", + "repotests": "b929dab9a163afbb3b5bf880492f9cf7b90c47df4f7982bf6d4050a0012ccedf" + }, + "installer_sha256": "f706f052e81912db4330675cd614f2b25c2b2db2ae6aff0775733f671cc0a612", + "test_command": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/repotests", + "-test.v", + "-test.timeout=15m", + "-test.run=^(?:TestAgentpluginsClaudeNativeCommandsLifecycle|TestAgentpluginsClaudeNativeLifecycle|TestAgentpluginsClaudeNativeProjectPreparedLifecycle|TestAgentpluginsClaudeNativeRuntimeLifecycle)$" + ], + "required_tests": [ + "TestAgentpluginsClaudeNativeCommandsLifecycle", + "TestAgentpluginsClaudeNativeLifecycle", + "TestAgentpluginsClaudeNativeProjectPreparedLifecycle", + "TestAgentpluginsClaudeNativeRuntimeLifecycle" + ], + "project_components": true, + "exit_code": 0, + "passed_tests": [ + "TestAgentpluginsClaudeNativeCommandsLifecycle", + "TestAgentpluginsClaudeNativeLifecycle", + "TestAgentpluginsClaudeNativeProjectPreparedLifecycle", + "TestAgentpluginsClaudeNativeRuntimeLifecycle" + ], + "skipped_tests": [], + "finished_utc": "2026-10-24T22:44:36Z", + "artifact_sha256": { + "native-tests.log": "f17f0fa3abf048f94d59a4bd20fe6ec03f01c85bc2a94c3c423c654c83d725e1", + "uap-claude-native-evidence-3075974034/07-skills-after-invocation-change-details.log": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482", + "uap-claude-native-evidence-3075974034/repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-3075974034/commands-evidence.json": "67284c1c082f6887c7e3e07c6e325cb803cc39f5597e7d6b0b300a9705241723", + "uap-claude-native-evidence-3075974034/13-skills-after-remove-list.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-3075974034/06-skills-after-invocation-change-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "uap-claude-native-evidence-3075974034/08-skills-after-intact-repair-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "uap-claude-native-evidence-3075974034/03-skills-after-add-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "uap-claude-native-evidence-3075974034/update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-3075974034/repair-intact-installer.json": "8c7187b6cbbc4fbac632959eebdecc91ea865df6dedc2669c42da211ee9aabb4", + "uap-claude-native-evidence-3075974034/11-skills-after-absent-repair-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "uap-claude-native-evidence-3075974034/update-invocation-change-installer.json": "ba4334a94932b3d4a9700d1624f0066e3ea7791a05e5eb61a6d612d97de370c8", + "uap-claude-native-evidence-3075974034/09-skills-after-intact-repair-details.log": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482", + "uap-claude-native-evidence-3075974034/repair-absent-installer.json": "c09caa33c6cfe0c33176f7dcf20a9ca69ade51518f05156793e3a115d32dac7c", + "uap-claude-native-evidence-3075974034/04-skills-after-unchanged-update-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "uap-claude-native-evidence-3075974034/add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "uap-claude-native-evidence-3075974034/update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-3075974034/add-installer.json": "199b3d3dd999aa86bdf07f64e15403944d2f96ca494dd53ef6edbeffbe336fed", + "uap-claude-native-evidence-3075974034/repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-3075974034/remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-3075974034/12-skills-after-absent-repair-details.log": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482", + "uap-claude-native-evidence-3075974034/update-unchanged-installer.json": "f57967e66fda2b95cdde9e378e34c34926ab5ef58d1998e7486738db26a2a8e1", + "uap-claude-native-evidence-3075974034/02-skills-after-add-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "uap-claude-native-evidence-3075974034/10-list-before-absent-repair.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "uap-claude-native-evidence-3075974034/remove-installer.json": "e781b71e7d52dbf68710b9de262ab44d521fc34915ce906aab79e00f8190e26f", + "uap-claude-native-evidence-3075974034/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-3075974034/05-skills-after-unchanged-update-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "uap-native-evidence-1860064462/evidence.json": "c86cb6675ef9cfdce375ce94f9d26da5b52114c565b2bde37327911fa84162ec", + "uap-native-evidence-1860064462/11-remove.log": "fe9290c8337d3d0df4326823e0a690b88f7cca7637e1c06135ef8107092c09b1", + "uap-native-evidence-1860064462/runtime-A-gateway.json": "88ec27468869e016ca8f0e325fddfb67f1d9da77e4c130de41e0684df855dd0b", + "uap-native-evidence-1860064462/08-runtime-C.log": "9c0948300572cb8e278b4293bbc0d6c723b4eb1efea0f78e32e3939fc9c2b434", + "uap-native-evidence-1860064462/07-same-version-update.log": "20944bbc5ca720f558618af5bf08c907fd1d59f29b8bb000de01db8b7745ad5c", + "uap-native-evidence-1860064462/06-runtime-B.log": "ae9a8c59e9a0e6587118d83c17a2bc77e62e78e3fbb1f56f578170c782be8b60", + "uap-native-evidence-1860064462/runtime-repair-gateway.json": "60fce8f594fb239ff89b637208122acbad7165df4637cb480027625f8c482a1c", + "uap-native-evidence-1860064462/05-update.log": "5b6267d8f62bb7f09371f49077fcd1c43bd685e82152f932c8ee7f524f1a7374", + "uap-native-evidence-1860064462/04-runtime-A.log": "6d34ed72d4781fdb5afa1001688e74f3edf3f51e2e9c7a369397413656b354f9", + "uap-native-evidence-1860064462/events.jsonl": "701e0666ee52f1d6ad5962f93109f432becbf3c4da87f025367bc3cf83a6c937", + "uap-native-evidence-1860064462/10-runtime-repair.log": "4f5175b3179892d6c483a325c6d7e6d80cd20a3687de8df0c997a78d08eceb3d", + "uap-native-evidence-1860064462/03-installed.log": "b30c8d0bfd92af75139c4e1b736210397d668d9b6470021cde142a4523955e17", + "uap-native-evidence-1860064462/02-add.log": "ff86207443db268006df50f9682df6c8db46f2b1f142f03eacdb9dad185de242", + "uap-native-evidence-1860064462/12-removed.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-native-evidence-1860064462/runtime-C-gateway.json": "e7419a57e2397e289d5bf648524a2b1d453c348d6414e47cba864ee84cec8dfc", + "uap-native-evidence-1860064462/http-events.jsonl": "e179fac70a37e344636a7989ba0862f4df8a0bfba37ca11a98a0ae3cd0b80959", + "uap-native-evidence-1860064462/09-repair.log": "488bd52f56fea47ca5a51ce48eb7efe3b246ea1d98055138bb94a056f43b29b7", + "uap-native-evidence-1860064462/runtime-B-gateway.json": "d40d943e5dd95c591f97c4d3cb9cbaf3fdcd96405318cc0cc488e99ac0378f74", + "uap-native-evidence-1860064462/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-2149198670/remove-inventory-diff.json": "cbbb57653f25f743adecf46538fb2dff02040b6f50faee23b7e33d56c94d969f", + "uap-claude-native-evidence-2149198670/evidence.json": "a91556466bdd6c52344217ee64117154160760106e936e685d8fad2506f58a6c", + "uap-claude-native-evidence-2149198670/14-remove.log": "ca0cf263cacd02d9b12ff11c283b48ed9560bfdfdb013375619639dc409fb103", + "uap-claude-native-evidence-2149198670/07-list-after-version-update.log": "00e0bfb35c45c0e78a5d790bafc172fe707a7e14c38cbe59826d48906417dfa6", + "uap-claude-native-evidence-2149198670/03-list-after-install.log": "e05a4bd907e99c330aaa797daeea395af983773dfcdff46ab521830eaa6ff232", + "uap-claude-native-evidence-2149198670/06-update-v2.log": "67a15913bde0f4e8c6ad220f77820fc2fc197a25f44709cc669dca7d7ef7b870", + "uap-claude-native-evidence-2149198670/05-mcp-list-after-install.log": "32fe2e78e244a328f5cf83f8a6c8e72e9e2db3bd5e18e774b7381fca1bf3ae9a", + "uap-claude-native-evidence-2149198670/10-repair-absent.log": "f2451e8eb2ae4d68ab7f2a25fce150ffc421b08aa01d813316925e3aedfac704", + "uap-claude-native-evidence-2149198670/12-details-after-repair.log": "61091ab19d98cc042ceb8919e12a7bd1efcef34cc8166fb2ca79296909c16831", + "uap-claude-native-evidence-2149198670/08-update-same-version.log": "c462167e4c64a1c2b74e7d68723241786b3738c9f141a0f5efd8cdd22db73cfd", + "uap-claude-native-evidence-2149198670/13-remove-blocked-by-foreign-content.log": "e36c3fb960fade53bafca2570d4a57e660dc125d2563280d63ab2666a77bb450", + "uap-claude-native-evidence-2149198670/09-update-unchanged.log": "f6865e9acd1507ea421527c2b2e9fe0d224cce0fe95dc03c53bdbe6d904c7420", + "uap-claude-native-evidence-2149198670/04-details-after-install.log": "f6d3b225f3f8d90d64fafe36b5ebe8c1ed46bf6867fa68f9897c08bbcda50b64", + "uap-claude-native-evidence-2149198670/15-list-after-remove.log": "d31b52f9ea36ed5c5e6b6b5ac9a95f465059a7d197a1e3995d3b1e98baecb305", + "uap-claude-native-evidence-2149198670/11-list-after-repair.log": "00e0bfb35c45c0e78a5d790bafc172fe707a7e14c38cbe59826d48906417dfa6", + "uap-claude-native-evidence-2149198670/16-repeat-remove.log": "9f7e8a6d254b5cfd57ccbb292dd645cb88a62042fc82f7c940dacec0abbcff53", + "uap-claude-native-evidence-2149198670/02-add-v1.log": "7b8be042a11b1b11d1ef71cd1280120ba206076cd6c0098d3ae57ca6467c1d1d", + "uap-claude-native-evidence-2149198670/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-666876926/after-remove-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-update-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-666876926/after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.json": "cd2e882f234ff656e80d5cb2db4a94e61ef39c386a569e67acb8880fd60b1fc4", + "uap-claude-native-evidence-666876926/add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "uap-claude-native-evidence-666876926/claude-project-plugin-evidence.json": "867a019d5f4f6492f541a9b99f489ee62882daf9d494e18acaacedc54e2cc8a4", + "uap-claude-native-evidence-666876926/after-repair-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/binding-after-repair.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "uap-claude-native-evidence-666876926/after-remove-native-B.json": "cba3fb0fde3f2a19b36801eb2115729a2e1bfe8a63ea0bc6be3d50944101cc93", + "uap-claude-native-evidence-666876926/after-repair-native-A.json": "6e1e7e35642ca32841a00017c5fc4b6b561079977d2312c44bc2a12595df7999", + "uap-claude-native-evidence-666876926/after-repeated-add-native-A.json": "bca261df54aa2ddddf4a9525be887a96d622534bbccf98e66e73e564ae0e1698", + "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-repair-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-666876926/update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-666876926/after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-update-native-A.json": "f0a9c71d7d67b1368019d820c1969196c0c03b83e6e22c97b53e30cea50008e8", + "uap-claude-native-evidence-666876926/after-update-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-add-native-B.json": "57b81cedb3c7c40e638404f6b7655f25ff51003c70e005319350d4821f580da3", + "uap-claude-native-evidence-666876926/after-remove-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-add-native-A.json": "72ff1eb64df99ecb676641312b08b887be65ae2b92b35f7cf4ebcf660e22902d", + "uap-claude-native-evidence-666876926/after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/repair-intact-installer.json": "617e2e61b96c53300cd3173ddc8ffa3f578d31a1ad37976d7a92a7a23c64cbae", + "uap-claude-native-evidence-666876926/after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-666876926/update-invocation-change-installer.json": "8efe9fffa841055fe6215b8ffa8a9d4f2b97f92538788363180131f4644bea76", + "uap-claude-native-evidence-666876926/after-remove-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/repair-absent-installer.json": "296aca41b8ab64984bac576992b172638d7cb451cd53c10ef00213f89fb64df6", + "uap-claude-native-evidence-666876926/after-update-native-B.json": "d3b4637bcea7ec7fad007f69e74b8f52b439465d6cce831ca182bfead906c009", + "uap-claude-native-evidence-666876926/after-update-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-666876926/add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "uap-claude-native-evidence-666876926/update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-666876926/binding-after-update.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "uap-claude-native-evidence-666876926/binding-after-add.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "uap-claude-native-evidence-666876926/add-repeated-installer.json": "5ea1994ece17a0b46e98b034a60e6e992c81760a9adbcf537668c5b4b007e7e0", + "uap-claude-native-evidence-666876926/add-installer.json": "51b38d67f9d7c913e1abe012775efcddd3a6d587a3a97a1896cdc70ac8ad44e9", + "uap-claude-native-evidence-666876926/repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-666876926/after-repair-native-B.json": "512a3b1df605cf7f22c6d8cc6b33c965a8d5d053606ad5fafdf23dea4db3f709", + "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-repair-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/after-repeated-add-native-B.json": "8f7a7c1ce4565e162cdca887103d7f5964cb8a66ac8255ba573fb2915b7ceec5", + "uap-claude-native-evidence-666876926/after-repair-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-remove-native-A.json": "956209b3fa2d84c9910bb0033d2a555ad9ef122eb5025117de27a872038bb2a6", + "uap-claude-native-evidence-666876926/remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-update-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.json": "e309ac269bfb1a785ec1fce86bfa5c23e9c2a1b3da73e5ad66328e7df97b32d2", + "uap-claude-native-evidence-666876926/update-unchanged-installer.json": "18bb4f448f7b907d05aa92bbe8f68047edd962dde4ba937a4e4aed1401b53e02", + "uap-claude-native-evidence-666876926/after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-666876926/after-remove-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-666876926/remove-installer.json": "f0e26700d32f82303cf79fa2cafb4fb09a34f0731970dc4e603df4c0cfdf4910", + "uap-claude-native-evidence-666876926/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-666876926/after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + } +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-2149198670/evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-2149198670/evidence.json new file mode 100644 index 000000000..224c8561b --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-2149198670/evidence.json @@ -0,0 +1,70 @@ +{ + "client_binary_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:44:05.385116Z", + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_binary_sha256": "f706f052e81912db4330675cd614f2b25c2b2db2ae6aff0775733f671cc0a612", + "installer_patch_sha256": "", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a", + "stages": { + "foreign_content_inside_managed_directory_blocked": { + "status": "passed", + "reason": "exact ownership-verification refusal (\"artifact digest mismatch\") when foreign content is planted inside the plugin's own managed directory; the foreign file survived the blocked attempt and was removed only by the test before the real remove below" + }, + "install": { + "status": "passed" + }, + "mcp_transport_discovery": { + "status": "passed", + "reason": "claude mcp list classified remote-http as (HTTP) and remote-sse as (SSE) and attempted a real (loopback-refused) connection to each, distinct from plugin details' shallow key enumeration; no live tool call or successful connection claimed" + }, + "owned_repair": { + "status": "passed", + "reason": "managed directory re-staged from the still-present, exact-revision-matching local source after deletion; restored content bytes, .mcp.json, and Claude plugin details verified; receipt records the physically absent before-state" + }, + "remove": { + "status": "passed", + "reason": "managed directory removed; sibling foreign plugin's sentinel content and its sole remaining claude plugin list entry both confirmed; exact inventory diff recorded (removed=8 all plugin-owned, added=0 installer-state only, changed=2)" + }, + "repeat_remove": { + "status": "passed", + "reason": "exact expected refusal message" + }, + "same_version_refresh": { + "status": "passed" + }, + "skill_discovery": { + "status": "passed", + "reason": "exact plugin-attributed skill enumerated by the real CLI with exact body bytes verified on disk; no model use claimed" + }, + "stdio_discovery": { + "status": "passed", + "reason": "local stdio enumerated by native CLI; no successful tool call claimed" + }, + "unchanged": { + "status": "passed", + "reason": "mutated:false plus byte-identical managed-directory tree digest and state-file digest before/after a byte-identical re-apply" + }, + "version_update": { + "status": "passed" + } + }, + "transcript_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "02-add-v1.log": "7b8be042a11b1b11d1ef71cd1280120ba206076cd6c0098d3ae57ca6467c1d1d", + "03-list-after-install.log": "e05a4bd907e99c330aaa797daeea395af983773dfcdff46ab521830eaa6ff232", + "04-details-after-install.log": "f6d3b225f3f8d90d64fafe36b5ebe8c1ed46bf6867fa68f9897c08bbcda50b64", + "05-mcp-list-after-install.log": "32fe2e78e244a328f5cf83f8a6c8e72e9e2db3bd5e18e774b7381fca1bf3ae9a", + "06-update-v2.log": "67a15913bde0f4e8c6ad220f77820fc2fc197a25f44709cc669dca7d7ef7b870", + "07-list-after-version-update.log": "00e0bfb35c45c0e78a5d790bafc172fe707a7e14c38cbe59826d48906417dfa6", + "08-update-same-version.log": "c462167e4c64a1c2b74e7d68723241786b3738c9f141a0f5efd8cdd22db73cfd", + "09-update-unchanged.log": "f6865e9acd1507ea421527c2b2e9fe0d224cce0fe95dc03c53bdbe6d904c7420", + "10-repair-absent.log": "f2451e8eb2ae4d68ab7f2a25fce150ffc421b08aa01d813316925e3aedfac704", + "11-list-after-repair.log": "00e0bfb35c45c0e78a5d790bafc172fe707a7e14c38cbe59826d48906417dfa6", + "12-details-after-repair.log": "61091ab19d98cc042ceb8919e12a7bd1efcef34cc8166fb2ca79296909c16831", + "13-remove-blocked-by-foreign-content.log": "e36c3fb960fade53bafca2570d4a57e660dc125d2563280d63ab2666a77bb450", + "14-remove.log": "ca0cf263cacd02d9b12ff11c283b48ed9560bfdfdb013375619639dc409fb103", + "15-list-after-remove.log": "d31b52f9ea36ed5c5e6b6b5ac9a95f465059a7d197a1e3995d3b1e98baecb305", + "16-repeat-remove.log": "9f7e8a6d254b5cfd57ccbb292dd645cb88a62042fc82f7c940dacec0abbcff53" + } +} \ No newline at end of file diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-3075974034/commands-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-3075974034/commands-evidence.json new file mode 100644 index 000000000..16639e040 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-3075974034/commands-evidence.json @@ -0,0 +1,97 @@ +{ + "acquisition": "local_directory", + "arch": "arm64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "02-skills-after-add-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "03-skills-after-add-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "04-skills-after-unchanged-update-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "05-skills-after-unchanged-update-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "06-skills-after-invocation-change-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "07-skills-after-invocation-change-details.log": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482", + "08-skills-after-intact-repair-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "09-skills-after-intact-repair-details.log": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482", + "10-list-before-absent-repair.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "11-skills-after-absent-repair-list.log": "e45cd54f06b996c52bb8d67753043215c498a4b01e94e30005c585d229e03c73", + "12-skills-after-absent-repair-details.log": "c32582063bb87ca7aac4508ce1b649191ae435252cf33afbc0372e9d8133d482", + "13-skills-after-remove-list.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-installer.json": "199b3d3dd999aa86bdf07f64e15403944d2f96ca494dd53ef6edbeffbe336fed", + "remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "remove-installer.json": "e781b71e7d52dbf68710b9de262ab44d521fc34915ce906aab79e00f8190e26f", + "repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-absent-installer.json": "c09caa33c6cfe0c33176f7dcf20a9ca69ade51518f05156793e3a115d32dac7c", + "repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-intact-installer.json": "8c7187b6cbbc4fbac632959eebdecc91ea865df6dedc2669c42da211ee9aabb4", + "update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-invocation-change-installer.json": "ba4334a94932b3d4a9700d1624f0066e3ea7791a05e5eb61a6d612d97de370c8", + "update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-unchanged-installer.json": "f57967e66fda2b95cdde9e378e34c34926ab5ef58d1998e7486738db26a2a8e1" + }, + "client": "claude", + "client_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:43:55.196422Z", + "installer_sha256": "f706f052e81912db4330675cd614f2b25c2b2db2ae6aff0775733f671cc0a612", + "native_surface": "claude plugin list --json and plugin details; installed SKILL.md bytes", + "os": "darwin", + "scanner": { + "archive_sha256": "be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57", + "platform": "darwin-arm64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a" + }, + "stages": { + "absent_repair": { + "status": "passed", + "reason": "the deleted managed artifact was rebuilt with the rendered command skill" + }, + "command_skill_discovery": { + "status": "passed", + "reason": "the client itself lists the ordinary command as a plugin skill with the exact rendered bytes" + }, + "explicit_command_withheld": { + "status": "passed", + "reason": "the explicit-only command is no planned component, carries an explicit_invocation_not_enforceable warning and is absent from the client's own skill listing" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no client declares explicit-invocation enforcement yet, so explicit-only commands are withheld" + }, + "install": { + "status": "passed", + "artifacts": [ + "add-installer.json" + ] + }, + "intact_repair": { + "status": "passed", + "reason": "repair of an intact package with current command rules succeeded and left the client skills unchanged" + }, + "invocation_change_update": { + "status": "passed", + "reason": "after the commands swapped invocation flags, update delivered the newly ordinary command and the client no longer lists the newly explicit-only one" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no model, login or OAuth is used" + }, + "remove": { + "status": "passed", + "reason": "the client lists no plugin skill after remove" + }, + "unchanged_update": { + "status": "passed", + "reason": "update of the unchanged package: mutated=false, byte-identical installer state, same client skills and bytes" + } + }, + "started_utc": "2026-10-24T22:43:45.980991Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/01-claude-version.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/01-claude-version.log new file mode 100644 index 000000000..870ddd4c2 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/01-claude-version.log @@ -0,0 +1 @@ +2.1.263 (Claude Code) diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-installer.json new file mode 100644 index 000000000..8b753586c --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-24d57d17b96a48765b8fd514c44c9e25","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-24d57d17b96a48765b8fd514c44c9e25","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-8dfb14e1a114","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-repeated-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-repeated-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-repeated-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-repeated-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-repeated-installer.json new file mode 100644 index 000000000..c497551f7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/add-repeated-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-7d9b29cbd1c5a6cd858d74de26244014","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"cache"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-8dfb14e1a114","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":false,"no_change":true}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.json new file mode 100644 index 000000000..f5857f7c9 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A", + "deadline": false, + "elapsed_ms": 217, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.json new file mode 100644 index 000000000..b3d68f867 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-B", + "deadline": false, + "elapsed_ms": 196, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.json new file mode 100644 index 000000000..3a2bf9786 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A", + "deadline": false, + "elapsed_ms": 239, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.json new file mode 100644 index 000000000..d6aa293a8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-B", + "deadline": false, + "elapsed_ms": 219, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.json new file mode 100644 index 000000000..a0f6eeae0 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A", + "deadline": false, + "elapsed_ms": 234, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.json new file mode 100644 index 000000000..93106c236 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-B", + "deadline": false, + "elapsed_ms": 296, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.json new file mode 100644 index 000000000..7ef0ac4fd --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A", + "deadline": false, + "elapsed_ms": 207, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.json new file mode 100644 index 000000000..511473d0c --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-B", + "deadline": false, + "elapsed_ms": 202, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.json new file mode 100644 index 000000000..ef52c8a22 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A", + "deadline": false, + "elapsed_ms": 220, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.json new file mode 100644 index 000000000..fbbed86d4 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-B", + "deadline": false, + "elapsed_ms": 222, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.json new file mode 100644 index 000000000..6493d896d --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A", + "deadline": false, + "elapsed_ms": 212, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.json new file mode 100644 index 000000000..6d0639284 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-B", + "deadline": false, + "elapsed_ms": 212, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/after-update-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-add.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-add.json new file mode 100644 index 000000000..99ffb34d7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-add.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_838e85319a22435b58a580f8", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A/.claude/skills/uap-commands-fixture-8dfb14e1a114", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-repair.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-repair.json new file mode 100644 index 000000000..99ffb34d7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-repair.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_838e85319a22435b58a580f8", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A/.claude/skills/uap-commands-fixture-8dfb14e1a114", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-update.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-update.json new file mode 100644 index 000000000..99ffb34d7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/binding-after-update.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_838e85319a22435b58a580f8", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A/.claude/skills/uap-commands-fixture-8dfb14e1a114", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/claude-project-plugin-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/claude-project-plugin-evidence.json new file mode 100644 index 000000000..bc8205af7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/claude-project-plugin-evidence.json @@ -0,0 +1,152 @@ +{ + "arch": "arm64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-installer.json": "51b38d67f9d7c913e1abe012775efcddd3a6d587a3a97a1896cdc70ac8ad44e9", + "add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-repeated-installer.json": "5ea1994ece17a0b46e98b034a60e6e992c81760a9adbcf537668c5b4b007e7e0", + "after-add-native-A.json": "72ff1eb64df99ecb676641312b08b887be65ae2b92b35f7cf4ebcf660e22902d", + "after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-add-native-B.json": "57b81cedb3c7c40e638404f6b7655f25ff51003c70e005319350d4821f580da3", + "after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-owned-directory-loss-native-A.json": "cd2e882f234ff656e80d5cb2db4a94e61ef39c386a569e67acb8880fd60b1fc4", + "after-owned-directory-loss-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-owned-directory-loss-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-owned-directory-loss-native-B.json": "e309ac269bfb1a785ec1fce86bfa5c23e9c2a1b3da73e5ad66328e7df97b32d2", + "after-owned-directory-loss-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-owned-directory-loss-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-remove-native-A.json": "956209b3fa2d84c9910bb0033d2a555ad9ef122eb5025117de27a872038bb2a6", + "after-remove-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-remove-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-remove-native-B.json": "cba3fb0fde3f2a19b36801eb2115729a2e1bfe8a63ea0bc6be3d50944101cc93", + "after-remove-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-remove-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-repair-native-A.json": "6e1e7e35642ca32841a00017c5fc4b6b561079977d2312c44bc2a12595df7999", + "after-repair-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repair-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-repair-native-B.json": "512a3b1df605cf7f22c6d8cc6b33c965a8d5d053606ad5fafdf23dea4db3f709", + "after-repair-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repair-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-repeated-add-native-A.json": "bca261df54aa2ddddf4a9525be887a96d622534bbccf98e66e73e564ae0e1698", + "after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-repeated-add-native-B.json": "8f7a7c1ce4565e162cdca887103d7f5964cb8a66ac8255ba573fb2915b7ceec5", + "after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-update-native-A.json": "f0a9c71d7d67b1368019d820c1969196c0c03b83e6e22c97b53e30cea50008e8", + "after-update-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-update-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-update-native-B.json": "d3b4637bcea7ec7fad007f69e74b8f52b439465d6cce831ca182bfead906c009", + "after-update-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-update-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "binding-after-add.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "binding-after-repair.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "binding-after-update.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "remove-installer.json": "f0e26700d32f82303cf79fa2cafb4fb09a34f0731970dc4e603df4c0cfdf4910", + "repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-absent-installer.json": "296aca41b8ab64984bac576992b172638d7cb451cd53c10ef00213f89fb64df6", + "repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-intact-installer.json": "617e2e61b96c53300cd3173ddc8ffa3f578d31a1ad37976d7a92a7a23c64cbae", + "update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-invocation-change-installer.json": "8efe9fffa841055fe6215b8ffa8a9d4f2b97f92538788363180131f4644bea76", + "update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-unchanged-installer.json": "18bb4f448f7b907d05aa92bbe8f68047edd962dde4ba937a4e4aed1401b53e02" + }, + "client": "claude", + "client_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:44:16.355146Z", + "installer_sha256": "f706f052e81912db4330675cd614f2b25c2b2db2ae6aff0775733f671cc0a612", + "native_surface": "fresh bounded native project lists prove suppression; owned file lifecycle is prepared; no trust/model/MCP initialization", + "os": "darwin", + "project_contract": "untrusted_prepared_v1", + "scanner": { + "archive_sha256": "be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57", + "platform": "darwin-arm64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a" + }, + "stages": { + "absent_repair": { + "status": "passed", + "reason": "production repair rebuilt missing exact-owned directory; fresh native list again suppressed it" + }, + "command_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "install": { + "status": "passed", + "reason": "production initial add installed owned files as Prepared+Installed; native project scan remains suppressed", + "artifacts": [ + "add-installer.json", + "binding-after-add.json" + ] + }, + "intact_repair": { + "status": "passed", + "reason": "intact repair preserved prepared files and explicit native blocker" + }, + "invocation_change_update": { + "status": "passed", + "reason": "production update changed owned command projection while native discovery stayed blocked" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "native_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "native_suppression_boundary": { + "status": "passed", + "reason": "bounded raw pinned native A/B lists retained the untrusted project suppression boundary" + }, + "prepared_command_projection": { + "status": "passed", + "reason": "owned projected files contain exact rendered ordinary command bytes; this is filesystem evidence" + }, + "prepared_explicit_command_withheld": { + "status": "passed", + "reason": "plan and owned projected files withhold the explicit-only command; no native loaded-skill claim" + }, + "project_scope_preservation": { + "status": "passed", + "reason": "foreign user/cache/project A/project B hashes remained exact; fresh project-B native lists never exposed A" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "remove": { + "status": "passed", + "reason": "production inactive removal deleted only owned project files; fresh native list no longer suppresses a managed directory" + }, + "unchanged_add": { + "status": "passed", + "reason": "repeated add reobserved real suppression without file/state mutation" + }, + "unchanged_update": { + "status": "passed", + "reason": "unchanged prepared update retained exact state and projected bytes" + } + }, + "started_utc": "2026-10-24T22:44:05.929846Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/remove-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/remove-installer-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/remove-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/remove-installer.json new file mode 100644 index 000000000..758ea818b --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/remove-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"remove","result":"success","data":{"operation_id":"op-0d2ea69b08ce3a71409191fbb4b93046","batch":true,"status":"data_retained","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","plugin_data_preserved":true,"data_retained":true,"retained_data":[{"data_receipt_id":"data_5d4aba7e2464063759e579b1","physical_backend_id":"uap-commands-fixture-8dfb14e1a114","scope":"project","state":"owned"}],"retained_data_action":"run `agentplugins remove e03ee339-b058-4834-9465-b22ded4e3120 --purge-data --scope project --project-root /Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-claude-native-evidence-666876926/project-A` to delete retained plugin data","dry_run":false,"targets":[{"target":"claude","status":"external_completed","output":{"operation_id":"op-0d2ea69b08ce3a71409191fbb4b93046","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plugin":"uap-commands-fixture","client_id":"claude","requires_confirmation":false,"mutated":true,"deactivation":{"activation":"not_required","artifact_removal_allowed":true,"external_removal_complete":true},"affected_surfaces":["claude"],"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-absent-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-absent-installer-stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-absent-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-absent-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-absent-installer.json new file mode 100644 index 000000000..1bfece609 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-absent-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-5e3da87f3f4e4d0217724fe7a1bf527e","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-5e3da87f3f4e4d0217724fe7a1bf527e","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-8dfb14e1a114","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-intact-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-intact-installer-stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-intact-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-intact-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-intact-installer.json new file mode 100644 index 000000000..55853a05e --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/repair-intact-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-4308934b770f712656f80c67b003499a","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-4308934b770f712656f80c67b003499a","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-8dfb14e1a114","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer.json new file mode 100644 index 000000000..48a34c9a9 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-2be8184e26e9c8ab1e4f5253fe74a9fe","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","dry_run":false,"targets":[{"target":"claude","selected":true,"status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-2be8184e26e9c8ab1e4f5253fe74a9fe","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-8dfb14e1a114","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-unchanged-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-unchanged-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-unchanged-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-unchanged-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-unchanged-installer.json new file mode 100644 index 000000000..fb979c7a5 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-claude-native-evidence-666876926/update-unchanged-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-4494bf79bf716dffef07697606d42496","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","dry_run":false,"targets":[{"target":"claude","selected":true,"status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-4494bf79bf716dffef07697606d42496","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"e03ee339-b058-4834-9465-b22ded4e3120","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-8dfb14e1a114","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":false,"no_change":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-native-evidence-1860064462/evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-native-evidence-1860064462/evidence.json new file mode 100644 index 000000000..107ebe9c0 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/darwin/uap-native-evidence-1860064462/evidence.json @@ -0,0 +1,56 @@ +{ + "arch": "arm64", + "client_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9", + "client_version": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:44:36.145662Z", + "installer_data_retention": "passed", + "installer_sha256": "f706f052e81912db4330675cd614f2b25c2b2db2ae6aff0775733f671cc0a612", + "oauth": "not_evaluated", + "os": "darwin", + "profile_root": "/Users/runner/work/_temp/uap-native-hosted-avqt_6qz/evidence/uap-native-evidence-1860064462/claude-config", + "provider": "scripted_loopback", + "real_model": "not_evaluated", + "runtime_scope": "stdio_default+stdio_explicit+HTTP+skill", + "scanner": { + "archive_sha256": "be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57", + "platform": "darwin-arm64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a" + }, + "stages": { + "install": "passed", + "remove": "passed", + "repair": "passed", + "runtime_A": "passed", + "runtime_B": "passed", + "same_version_refresh": "passed" + }, + "stdio_cwd_argv_env_data": "passed", + "stdio_runtime": "passed", + "transcript_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "02-add.log": "ff86207443db268006df50f9682df6c8db46f2b1f142f03eacdb9dad185de242", + "03-installed.log": "b30c8d0bfd92af75139c4e1b736210397d668d9b6470021cde142a4523955e17", + "04-runtime-A.log": "6d34ed72d4781fdb5afa1001688e74f3edf3f51e2e9c7a369397413656b354f9", + "05-update.log": "5b6267d8f62bb7f09371f49077fcd1c43bd685e82152f932c8ee7f524f1a7374", + "06-runtime-B.log": "ae9a8c59e9a0e6587118d83c17a2bc77e62e78e3fbb1f56f578170c782be8b60", + "07-same-version-update.log": "20944bbc5ca720f558618af5bf08c907fd1d59f29b8bb000de01db8b7745ad5c", + "08-runtime-C.log": "9c0948300572cb8e278b4293bbc0d6c723b4eb1efea0f78e32e3939fc9c2b434", + "09-repair.log": "488bd52f56fea47ca5a51ce48eb7efe3b246ea1d98055138bb94a056f43b29b7", + "10-runtime-repair.log": "4f5175b3179892d6c483a325c6d7e6d80cd20a3687de8df0c997a78d08eceb3d", + "11-remove.log": "fe9290c8337d3d0df4326823e0a690b88f7cca7637e1c06135ef8107092c09b1", + "12-removed.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "runtime-A-gateway.json": "88ec27468869e016ca8f0e325fddfb67f1d9da77e4c130de41e0684df855dd0b", + "runtime-B-gateway.json": "d40d943e5dd95c591f97c4d3cb9cbaf3fdcd96405318cc0cc488e99ac0378f74", + "runtime-C-gateway.json": "e7419a57e2397e289d5bf648524a2b1d453c348d6414e47cba864ee84cec8dfc", + "runtime-repair-gateway.json": "60fce8f594fb239ff89b637208122acbad7165df4637cb480027625f8c482a1c" + } +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/provenance.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/provenance.json new file mode 100644 index 000000000..c976debd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/provenance.json @@ -0,0 +1,207 @@ +{ + "schema_version": 1, + "run_id": 40139625617, + "run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40139625617", + "source_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "source_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a", + "retrieval": "Read-only GitHub workflow jobs/logs/artifacts connectors; downloaded signed artifact ZIPs; no native process executed locally during preservation.", + "verification": "ZIP length, SHA256 and CRC; every ZIP member hash recorded; every original runner artifact_sha256 and project artifact_sha256 entry verified against extracted bytes; all four required tests passed with no skipped tests; project/runner installer SHA256 match.", + "preservation": "All project evidence files, runner-evidence.json, native-tests.log, user-gate evidence summaries and full ZIP member manifests retained byte-for-byte. User-gate auxiliary raw files (including scripted mock gateway payloads) remain in the original GitHub artifacts, identified by ZIP hashes and member manifests; they are not duplicated here.", + "qualification": "Hosted untrusted Prepared project lifecycle and existing user gates only. Project native discovery remains blocked; project MCP initialization, real model use and explicit invocation enforcement are not evaluated. This run predates bootstrap containment changes and cannot qualify their Linux runtime behavior.", + "platforms": { + "darwin": { + "job_id": 121008404659, + "job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40139625617/job/121008404659", + "job_conclusion": "success", + "artifact_id": 12578221090, + "zip_bytes": 398277, + "zip_sha256": "269077ce2ae9e88aefa1d8a539fb194ea93fd4ec4c66f384a76865121e181b48", + "project_directory": "uap-claude-native-evidence-666876926", + "project_files_preserved": 55, + "runner_hash_entries_verified": 119, + "project_hash_entries_verified": 54, + "user_summaries_preserved": [ + "uap-native-evidence-1860064462/evidence.json", + "uap-claude-native-evidence-3075974034/commands-evidence.json", + "uap-claude-native-evidence-2149198670/evidence.json" + ], + "project_stages": { + "absent_repair": "passed", + "command_skill_discovery": "blocked", + "explicit_invocation_enforced": "not_evaluated", + "install": "passed", + "intact_repair": "passed", + "invocation_change_update": "passed", + "native_mcp_initialization": "not_evaluated", + "native_skill_discovery": "blocked", + "native_suppression_boundary": "passed", + "prepared_command_projection": "passed", + "prepared_explicit_command_withheld": "passed", + "project_scope_preservation": "passed", + "real_model_command_use": "not_evaluated", + "remove": "passed", + "unchanged_add": "passed", + "unchanged_update": "passed" + } + }, + "windows": { + "job_id": 121008404557, + "job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40139625617/job/121008404557", + "job_conclusion": "success", + "artifact_id": 12578331155, + "zip_bytes": 412211, + "zip_sha256": "82d3058711293d884878d695cc157e7ba5b2445cf5b7fa0dfbc1b59bead89c0b", + "project_directory": "uap-claude-native-evidence-2732888560", + "project_files_preserved": 55, + "runner_hash_entries_verified": 123, + "project_hash_entries_verified": 54, + "user_summaries_preserved": [ + "uap-native-evidence-781463327/evidence.json", + "uap-claude-native-evidence-925369014/evidence.json", + "uap-claude-native-evidence-3306580122/commands-evidence.json" + ], + "project_stages": { + "absent_repair": "passed", + "command_skill_discovery": "blocked", + "explicit_invocation_enforced": "not_evaluated", + "install": "passed", + "intact_repair": "passed", + "invocation_change_update": "passed", + "native_mcp_initialization": "not_evaluated", + "native_skill_discovery": "blocked", + "native_suppression_boundary": "passed", + "prepared_command_projection": "passed", + "prepared_explicit_command_withheld": "passed", + "project_scope_preservation": "passed", + "real_model_command_use": "not_evaluated", + "remove": "passed", + "unchanged_add": "passed", + "unchanged_update": "passed" + } + } + }, + "preserved_file_sha256": { + "darwin/native-tests.log": "f17f0fa3abf048f94d59a4bd20fe6ec03f01c85bc2a94c3c423c654c83d725e1", + "darwin/runner-evidence.json": "3668152bea3c6b223698cdb3a50eb6b616cf1eb762ca3a0d59e9cccbd0721fa4", + "darwin/uap-claude-native-evidence-2149198670/evidence.json": "a91556466bdd6c52344217ee64117154160760106e936e685d8fad2506f58a6c", + "darwin/uap-claude-native-evidence-3075974034/commands-evidence.json": "67284c1c082f6887c7e3e07c6e325cb803cc39f5597e7d6b0b300a9705241723", + "darwin/uap-claude-native-evidence-666876926/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "darwin/uap-claude-native-evidence-666876926/add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "darwin/uap-claude-native-evidence-666876926/add-installer.json": "51b38d67f9d7c913e1abe012775efcddd3a6d587a3a97a1896cdc70ac8ad44e9", + "darwin/uap-claude-native-evidence-666876926/add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "darwin/uap-claude-native-evidence-666876926/add-repeated-installer.json": "5ea1994ece17a0b46e98b034a60e6e992c81760a9adbcf537668c5b4b007e7e0", + "darwin/uap-claude-native-evidence-666876926/after-add-native-A.json": "72ff1eb64df99ecb676641312b08b887be65ae2b92b35f7cf4ebcf660e22902d", + "darwin/uap-claude-native-evidence-666876926/after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "darwin/uap-claude-native-evidence-666876926/after-add-native-B.json": "57b81cedb3c7c40e638404f6b7655f25ff51003c70e005319350d4821f580da3", + "darwin/uap-claude-native-evidence-666876926/after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.json": "cd2e882f234ff656e80d5cb2db4a94e61ef39c386a569e67acb8880fd60b1fc4", + "darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.json": "e309ac269bfb1a785ec1fce86bfa5c23e9c2a1b3da73e5ad66328e7df97b32d2", + "darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-owned-directory-loss-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-remove-native-A.json": "956209b3fa2d84c9910bb0033d2a555ad9ef122eb5025117de27a872038bb2a6", + "darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-remove-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-remove-native-B.json": "cba3fb0fde3f2a19b36801eb2115729a2e1bfe8a63ea0bc6be3d50944101cc93", + "darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-remove-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-repair-native-A.json": "6e1e7e35642ca32841a00017c5fc4b6b561079977d2312c44bc2a12595df7999", + "darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-repair-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "darwin/uap-claude-native-evidence-666876926/after-repair-native-B.json": "512a3b1df605cf7f22c6d8cc6b33c965a8d5d053606ad5fafdf23dea4db3f709", + "darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-repair-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.json": "bca261df54aa2ddddf4a9525be887a96d622534bbccf98e66e73e564ae0e1698", + "darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.json": "8f7a7c1ce4565e162cdca887103d7f5964cb8a66ac8255ba573fb2915b7ceec5", + "darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/after-update-native-A.json": "f0a9c71d7d67b1368019d820c1969196c0c03b83e6e22c97b53e30cea50008e8", + "darwin/uap-claude-native-evidence-666876926/after-update-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-update-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "darwin/uap-claude-native-evidence-666876926/after-update-native-B.json": "d3b4637bcea7ec7fad007f69e74b8f52b439465d6cce831ca182bfead906c009", + "darwin/uap-claude-native-evidence-666876926/after-update-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/after-update-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "darwin/uap-claude-native-evidence-666876926/binding-after-add.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "darwin/uap-claude-native-evidence-666876926/binding-after-repair.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "darwin/uap-claude-native-evidence-666876926/binding-after-update.json": "71b7df6e6c1f9263122897b21e9bcc1cc2fe0b389aa00efaf6c1864f09386ccb", + "darwin/uap-claude-native-evidence-666876926/claude-project-plugin-evidence.json": "867a019d5f4f6492f541a9b99f489ee62882daf9d494e18acaacedc54e2cc8a4", + "darwin/uap-claude-native-evidence-666876926/remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "darwin/uap-claude-native-evidence-666876926/remove-installer.json": "f0e26700d32f82303cf79fa2cafb4fb09a34f0731970dc4e603df4c0cfdf4910", + "darwin/uap-claude-native-evidence-666876926/repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "darwin/uap-claude-native-evidence-666876926/repair-absent-installer.json": "296aca41b8ab64984bac576992b172638d7cb451cd53c10ef00213f89fb64df6", + "darwin/uap-claude-native-evidence-666876926/repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "darwin/uap-claude-native-evidence-666876926/repair-intact-installer.json": "617e2e61b96c53300cd3173ddc8ffa3f578d31a1ad37976d7a92a7a23c64cbae", + "darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "darwin/uap-claude-native-evidence-666876926/update-invocation-change-installer.json": "8efe9fffa841055fe6215b8ffa8a9d4f2b97f92538788363180131f4644bea76", + "darwin/uap-claude-native-evidence-666876926/update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "darwin/uap-claude-native-evidence-666876926/update-unchanged-installer.json": "18bb4f448f7b907d05aa92bbe8f68047edd962dde4ba937a4e4aed1401b53e02", + "darwin/uap-native-evidence-1860064462/evidence.json": "c86cb6675ef9cfdce375ce94f9d26da5b52114c565b2bde37327911fa84162ec", + "darwin-archive-manifest.json": "71d540c7feb10c66b3c6280b3cd84d88560eaf4f316ee47ba776caf06cb9efa3", + "windows/native-tests.log": "2f878922f80a61b465090809221c194dbc7b070f88d520eb65661719646f6c6b", + "windows/runner-evidence.json": "d18dad995c1ffd33b94f4030d41ab0eb27c8ea826084b03e264ac37759492cd7", + "windows/uap-claude-native-evidence-2732888560/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "windows/uap-claude-native-evidence-2732888560/add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "windows/uap-claude-native-evidence-2732888560/add-installer.json": "d5f08d4a2f8e1a4dd449e9e25cbd516c5d0c8af8ca4a88f1dcf956aeb6a5bac3", + "windows/uap-claude-native-evidence-2732888560/add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "windows/uap-claude-native-evidence-2732888560/add-repeated-installer.json": "279666535285314035828281596734a479a99065a61f0415cac5c672f0982f77", + "windows/uap-claude-native-evidence-2732888560/after-add-native-A.json": "c954a66f02f81c47dfbbbf638ba0100a5edb922f492b51c2583cccbba15c94fd", + "windows/uap-claude-native-evidence-2732888560/after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "windows/uap-claude-native-evidence-2732888560/after-add-native-B.json": "03885704640af29ed024e08372bdbf0ea41ff2347e2afba0328e2b7855365dd3", + "windows/uap-claude-native-evidence-2732888560/after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.json": "3c8078261e72425b8498ea91eab2fd469d743bf85eb5f8596bdab0a9d7a2935b", + "windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.json": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49", + "windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-remove-native-A.json": "57f48b45edcece506ad6e8cd3ad71590c02ec899ef1ebd0c59c994d2e1054dfb", + "windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-remove-native-B.json": "192ad8bd8acc8cf5d81d203e31557b58be01d57e17ed4f3b4581c5662f5b8892", + "windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-repair-native-A.json": "6a3ddf809682666feba66ff1d22d8c2976ce1f2ee869941dda35efaf0e7fe06d", + "windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "windows/uap-claude-native-evidence-2732888560/after-repair-native-B.json": "aa8954efdf2045c6a88163ec896ab2606e7e113d1541bbf771d670a8d87bdf81", + "windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.json": "2c20a4e194c156a533731a16c174dd6de54e38631f117268c43265dfc3f86f7d", + "windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.json": "df920e269331dfe1a656f384724cf995be3fe8a44854a16b4cd379642bc8a4fb", + "windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/after-update-native-A.json": "91663e368949a1c8c6f0e52eb4d0562c44b49b3e45cf51ca3f81ac5ad114c189", + "windows/uap-claude-native-evidence-2732888560/after-update-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-update-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "windows/uap-claude-native-evidence-2732888560/after-update-native-B.json": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49", + "windows/uap-claude-native-evidence-2732888560/after-update-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/after-update-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "windows/uap-claude-native-evidence-2732888560/binding-after-add.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "windows/uap-claude-native-evidence-2732888560/binding-after-repair.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "windows/uap-claude-native-evidence-2732888560/binding-after-update.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "windows/uap-claude-native-evidence-2732888560/claude-project-plugin-evidence.json": "7941439ed3874237fa632ba9f8f6d4a37a2d04794b0c9058de3bad12f0e1abbb", + "windows/uap-claude-native-evidence-2732888560/remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "windows/uap-claude-native-evidence-2732888560/remove-installer.json": "4f74431cc01a43fa65e2904877292f363658fb8ddd8622101e6792fb3c292889", + "windows/uap-claude-native-evidence-2732888560/repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "windows/uap-claude-native-evidence-2732888560/repair-absent-installer.json": "18f8bcd176b315d17beab6ff211b8deac6c269673fc98cf81fb0a00a32a29f65", + "windows/uap-claude-native-evidence-2732888560/repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "windows/uap-claude-native-evidence-2732888560/repair-intact-installer.json": "85eaecbd1ef71d6465e63ff4e793cb9eeb1361b2c51b37f8a856ab5621a2477e", + "windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer.json": "aab992c663a101a1240a1b1f2b3857667e914bc7f0f378f09eeeb970b97e1a80", + "windows/uap-claude-native-evidence-2732888560/update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "windows/uap-claude-native-evidence-2732888560/update-unchanged-installer.json": "ac8b802cd981c43a09bcbc841dca65f2bbd7b961a3ff8e355821f2a531d722a9", + "windows/uap-claude-native-evidence-3306580122/commands-evidence.json": "073efc68ddea9bbe8d55184e7047f1af42383e5f9bd3ce5797a66f64bfe4c832", + "windows/uap-claude-native-evidence-925369014/evidence.json": "8d3070f13beeb2b9b4ede291d62efc6916caa6a9c692a7c54dc360bb5d5167ed", + "windows/uap-native-evidence-781463327/evidence.json": "7ffca73c45406e05d727e3f206d40caed451809727e953cfb253c7dca5fecc9c", + "windows-archive-manifest.json": "509c3f3bb60c4c3f48a83f2863ac3807284538bfb94555f25b97a8647e6dea05" + } +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows-archive-manifest.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows-archive-manifest.json new file mode 100644 index 000000000..31d448c3c --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows-archive-manifest.json @@ -0,0 +1,628 @@ +{ + "artifact_id": 12578331155, + "zip_bytes": 412211, + "zip_sha256": "82d3058711293d884878d695cc157e7ba5b2445cf5b7fa0dfbc1b59bead89c0b", + "zip_crc_checked": true, + "members": [ + { + "name": "native-tests.log", + "bytes": 1117, + "sha256": "2f878922f80a61b465090809221c194dbc7b070f88d520eb65661719646f6c6b" + }, + { + "name": "runner-evidence.json", + "bytes": 20716, + "sha256": "d18dad995c1ffd33b94f4030d41ab0eb27c8ea826084b03e264ac37759492cd7" + }, + { + "name": "uap-claude-native-evidence-2732888560/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-claude-native-evidence-2732888560/add-installer-stderr.log", + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + { + "name": "uap-claude-native-evidence-2732888560/add-installer.json", + "bytes": 4227, + "sha256": "d5f08d4a2f8e1a4dd449e9e25cbd516c5d0c8af8ca4a88f1dcf956aeb6a5bac3" + }, + { + "name": "uap-claude-native-evidence-2732888560/add-repeated-installer-stderr.log", + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + { + "name": "uap-claude-native-evidence-2732888560/add-repeated-installer.json", + "bytes": 2756, + "sha256": "279666535285314035828281596734a479a99065a61f0415cac5c672f0982f77" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-add-native-A.json", + "bytes": 389, + "sha256": "c954a66f02f81c47dfbbbf638ba0100a5edb922f492b51c2583cccbba15c94fd" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-add-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-add-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-add-native-B.json", + "bytes": 390, + "sha256": "03885704640af29ed024e08372bdbf0ea41ff2347e2afba0328e2b7855365dd3" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-add-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-add-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.json", + "bytes": 390, + "sha256": "3c8078261e72425b8498ea91eab2fd469d743bf85eb5f8596bdab0a9d7a2935b" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.json", + "bytes": 390, + "sha256": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-remove-native-A.json", + "bytes": 390, + "sha256": "57f48b45edcece506ad6e8cd3ad71590c02ec899ef1ebd0c59c994d2e1054dfb" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-remove-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-remove-native-A.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-remove-native-B.json", + "bytes": 390, + "sha256": "192ad8bd8acc8cf5d81d203e31557b58be01d57e17ed4f3b4581c5662f5b8892" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-remove-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-remove-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repair-native-A.json", + "bytes": 389, + "sha256": "6a3ddf809682666feba66ff1d22d8c2976ce1f2ee869941dda35efaf0e7fe06d" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repair-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repair-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repair-native-B.json", + "bytes": 390, + "sha256": "aa8954efdf2045c6a88163ec896ab2606e7e113d1541bbf771d670a8d87bdf81" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repair-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repair-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repeated-add-native-A.json", + "bytes": 389, + "sha256": "2c20a4e194c156a533731a16c174dd6de54e38631f117268c43265dfc3f86f7d" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repeated-add-native-B.json", + "bytes": 390, + "sha256": "df920e269331dfe1a656f384724cf995be3fe8a44854a16b4cd379642bc8a4fb" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-update-native-A.json", + "bytes": 389, + "sha256": "91663e368949a1c8c6f0e52eb4d0562c44b49b3e45cf51ca3f81ac5ad114c189" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-update-native-A.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-update-native-A.stdout.log", + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-update-native-B.json", + "bytes": 390, + "sha256": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-update-native-B.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/after-update-native-B.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-2732888560/binding-after-add.json", + "bytes": 375, + "sha256": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61" + }, + { + "name": "uap-claude-native-evidence-2732888560/binding-after-repair.json", + "bytes": 375, + "sha256": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61" + }, + { + "name": "uap-claude-native-evidence-2732888560/binding-after-update.json", + "bytes": 375, + "sha256": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61" + }, + { + "name": "uap-claude-native-evidence-2732888560/claude-project-plugin-evidence.json", + "bytes": 9922, + "sha256": "7941439ed3874237fa632ba9f8f6d4a37a2d04794b0c9058de3bad12f0e1abbb" + }, + { + "name": "uap-claude-native-evidence-2732888560/remove-installer-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-2732888560/remove-installer.json", + "bytes": 1441, + "sha256": "4f74431cc01a43fa65e2904877292f363658fb8ddd8622101e6792fb3c292889" + }, + { + "name": "uap-claude-native-evidence-2732888560/repair-absent-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-2732888560/repair-absent-installer.json", + "bytes": 2666, + "sha256": "18f8bcd176b315d17beab6ff211b8deac6c269673fc98cf81fb0a00a32a29f65" + }, + { + "name": "uap-claude-native-evidence-2732888560/repair-intact-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-2732888560/update-invocation-change-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-2732888560/repair-intact-installer.json", + "bytes": 2666, + "sha256": "85eaecbd1ef71d6465e63ff4e793cb9eeb1361b2c51b37f8a856ab5621a2477e" + }, + { + "name": "uap-claude-native-evidence-2732888560/update-invocation-change-installer.json", + "bytes": 2833, + "sha256": "aab992c663a101a1240a1b1f2b3857667e914bc7f0f378f09eeeb970b97e1a80" + }, + { + "name": "uap-claude-native-evidence-2732888560/update-unchanged-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-3306580122/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-claude-native-evidence-3306580122/02-skills-after-add-list.log", + "bytes": 292, + "sha256": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235" + }, + { + "name": "uap-claude-native-evidence-2732888560/update-unchanged-installer.json", + "bytes": 2851, + "sha256": "ac8b802cd981c43a09bcbc841dca65f2bbd7b961a3ff8e355821f2a531d722a9" + }, + { + "name": "uap-claude-native-evidence-3306580122/03-skills-after-add-details.log", + "bytes": 624, + "sha256": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527" + }, + { + "name": "uap-claude-native-evidence-3306580122/04-skills-after-unchanged-update-list.log", + "bytes": 292, + "sha256": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235" + }, + { + "name": "uap-claude-native-evidence-3306580122/05-skills-after-unchanged-update-details.log", + "bytes": 624, + "sha256": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527" + }, + { + "name": "uap-claude-native-evidence-3306580122/06-skills-after-invocation-change-list.log", + "bytes": 292, + "sha256": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235" + }, + { + "name": "uap-claude-native-evidence-3306580122/07-skills-after-invocation-change-details.log", + "bytes": 632, + "sha256": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032" + }, + { + "name": "uap-claude-native-evidence-3306580122/08-skills-after-intact-repair-list.log", + "bytes": 292, + "sha256": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235" + }, + { + "name": "uap-claude-native-evidence-3306580122/09-skills-after-intact-repair-details.log", + "bytes": 632, + "sha256": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032" + }, + { + "name": "uap-claude-native-evidence-3306580122/10-list-before-absent-repair.log", + "bytes": 292, + "sha256": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235" + }, + { + "name": "uap-claude-native-evidence-3306580122/11-skills-after-absent-repair-list.log", + "bytes": 292, + "sha256": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235" + }, + { + "name": "uap-claude-native-evidence-3306580122/12-skills-after-absent-repair-details.log", + "bytes": 632, + "sha256": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032" + }, + { + "name": "uap-claude-native-evidence-3306580122/13-skills-after-remove-list.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-claude-native-evidence-3306580122/add-installer-stderr.log", + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + { + "name": "uap-claude-native-evidence-3306580122/add-installer.json", + "bytes": 3724, + "sha256": "6d26e78cd7954b1118cf2515c8928a24412d32822395f650f672bc5c2f96fecd" + }, + { + "name": "uap-claude-native-evidence-3306580122/commands-evidence.json", + "bytes": 5616, + "sha256": "073efc68ddea9bbe8d55184e7047f1af42383e5f9bd3ce5797a66f64bfe4c832" + }, + { + "name": "uap-claude-native-evidence-3306580122/remove-installer-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uap-claude-native-evidence-3306580122/remove-installer.json", + "bytes": 1304, + "sha256": "32328b55490adbe5fad5f4858b16244dc6eeab2409014bce19d51043717b9e5f" + }, + { + "name": "uap-claude-native-evidence-3306580122/repair-absent-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-3306580122/repair-absent-installer.json", + "bytes": 2155, + "sha256": "a8df3d5ee904d95b67a65ce9518bcf9f30ccf457bedc42eebe77e2d61ab163ce" + }, + { + "name": "uap-claude-native-evidence-3306580122/repair-intact-installer-stderr.log", + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + { + "name": "uap-claude-native-evidence-3306580122/repair-intact-installer.json", + "bytes": 2155, + "sha256": "68397315a3f9a08ff7ba3069537587f3c7f7f6b538266a3ed814bf7a4da29e02" + }, + { + "name": "uap-claude-native-evidence-3306580122/update-invocation-change-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-3306580122/update-invocation-change-installer.json", + "bytes": 2322, + "sha256": "ecec0e12a5a8f6e0814bd0dd9b411d3f223e909d6748a21f45fc06d3c2266bbc" + }, + { + "name": "uap-claude-native-evidence-3306580122/update-unchanged-installer-stderr.log", + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + { + "name": "uap-claude-native-evidence-3306580122/update-unchanged-installer.json", + "bytes": 2340, + "sha256": "6a53239d38904b8191948faaf54b72b5f9e8333f5f8e9fa0a4bfd95805c7ef14" + }, + { + "name": "uap-claude-native-evidence-925369014/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-claude-native-evidence-925369014/02-add-v1.log", + "bytes": 4408, + "sha256": "a2db051b3f735c87f2fa98193671b5dfbfbbf33f75bb303862f1d1b1ce0d4873" + }, + { + "name": "uap-claude-native-evidence-925369014/03-list-after-install.log", + "bytes": 289, + "sha256": "8f2b17e0a3b6a54d26480ae929f1738806e70ff5734d10efbe646e7019059b96" + }, + { + "name": "uap-claude-native-evidence-925369014/04-details-after-install.log", + "bytes": 601, + "sha256": "f6d3b225f3f8d90d64fafe36b5ebe8c1ed46bf6867fa68f9897c08bbcda50b64" + }, + { + "name": "uap-claude-native-evidence-925369014/05-mcp-list-after-install.log", + "bytes": 1207, + "sha256": "d3b65ab1c676d469493812220f8885a96d497d95faf1d939a7d81246f0efb976" + }, + { + "name": "uap-claude-native-evidence-925369014/06-update-v2.log", + "bytes": 2157, + "sha256": "4666dd311eea6740440c435b3d058cbfba338a1f02fd6fed984ab343a305eb0c" + }, + { + "name": "uap-claude-native-evidence-925369014/08-update-same-version.log", + "bytes": 2157, + "sha256": "9f251a7130b0cceda2ff39bb331a93b6852ab69f3c68c8162c44cda0507c7e3a" + }, + { + "name": "uap-claude-native-evidence-925369014/07-list-after-version-update.log", + "bytes": 289, + "sha256": "fd8c6595cfee831286e5e74130c01389b1d902d1a147b3d97fe6f4207f290269" + }, + { + "name": "uap-claude-native-evidence-925369014/09-update-unchanged.log", + "bytes": 2175, + "sha256": "5736f9e2a75ea6ba77372e08d3039bd17a9b38286ec2377e8f526e3672cc81d2" + }, + { + "name": "uap-claude-native-evidence-925369014/10-repair-absent.log", + "bytes": 1981, + "sha256": "56e0bc5eb09991e09134e2ebb13d1991f7d676b6221658adaf4d2b30a78685ae" + }, + { + "name": "uap-claude-native-evidence-925369014/11-list-after-repair.log", + "bytes": 289, + "sha256": "fd8c6595cfee831286e5e74130c01389b1d902d1a147b3d97fe6f4207f290269" + }, + { + "name": "uap-claude-native-evidence-925369014/12-details-after-repair.log", + "bytes": 601, + "sha256": "61091ab19d98cc042ceb8919e12a7bd1efcef34cc8166fb2ca79296909c16831" + }, + { + "name": "uap-claude-native-evidence-925369014/13-remove-blocked-by-foreign-content.log", + "bytes": 1106, + "sha256": "8e716a4b90122a78be6230f6fa4eeec4b5a9d124079ac20a31451368300595aa" + }, + { + "name": "uap-claude-native-evidence-925369014/15-list-after-remove.log", + "bytes": 268, + "sha256": "43f621b29927cde88d013813ac4bde06667ffb93d90827a5aa4d84692a5378c9" + }, + { + "name": "uap-claude-native-evidence-925369014/14-remove.log", + "bytes": 1008, + "sha256": "1dc1b4f8c8986f3657fdc57ca1083b61d850215ee98fb5a616e575e1d729256e" + }, + { + "name": "uap-claude-native-evidence-925369014/16-repeat-remove.log", + "bytes": 63, + "sha256": "9f7e8a6d254b5cfd57ccbb292dd645cb88a62042fc82f7c940dacec0abbcff53" + }, + { + "name": "uap-claude-native-evidence-925369014/evidence.json", + "bytes": 4379, + "sha256": "8d3070f13beeb2b9b4ede291d62efc6916caa6a9c692a7c54dc360bb5d5167ed" + }, + { + "name": "uap-claude-native-evidence-925369014/remove-inventory-diff.json", + "bytes": 1921, + "sha256": "9929ab8191b587dcf75fc0efe6bd53ba026c4d7d8b6445616317ffce20b37a6b" + }, + { + "name": "uap-native-evidence-781463327/01-claude-version.log", + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + { + "name": "uap-native-evidence-781463327/02-add.log", + "bytes": 3994, + "sha256": "3d09e25e78d204599669c4bf2d4e17bc19735a28f2a0e2aafd4bd6706577becd" + }, + { + "name": "uap-native-evidence-781463327/03-installed.log", + "bytes": 268, + "sha256": "fa7d5a681b9679ec90048cb2debb3b80d6e19d27b6f6fd7b461dc68a38f95afa" + }, + { + "name": "uap-native-evidence-781463327/04-runtime-A-projection.log", + "bytes": 268, + "sha256": "fa7d5a681b9679ec90048cb2debb3b80d6e19d27b6f6fd7b461dc68a38f95afa" + }, + { + "name": "uap-native-evidence-781463327/05-runtime-A.log", + "bytes": 15522, + "sha256": "f21172d9902ea84d92dca3175cc4f3dc0642b2b133ceddb6f026e070dd156186" + }, + { + "name": "uap-native-evidence-781463327/06-update.log", + "bytes": 2131, + "sha256": "62deb607d232f07dc85e36cc5380473a2cc17dd19c562feb6e59b1ee70efaa02" + }, + { + "name": "uap-native-evidence-781463327/07-runtime-B-projection.log", + "bytes": 268, + "sha256": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e" + }, + { + "name": "uap-native-evidence-781463327/08-runtime-B.log", + "bytes": 15518, + "sha256": "99134edf57d1b15eb25ed81ffcbd1f58a81ed9a72b29767ca0bdda7460e01d97" + }, + { + "name": "uap-native-evidence-781463327/09-same-version-update.log", + "bytes": 2131, + "sha256": "20fdb76268aff018ca872048be00c42f213f1b7d84c1ed387e0e16beca205bcc" + }, + { + "name": "uap-native-evidence-781463327/10-runtime-C-projection.log", + "bytes": 268, + "sha256": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e" + }, + { + "name": "uap-native-evidence-781463327/11-runtime-C.log", + "bytes": 15519, + "sha256": "c09e5325a0a0180c6cf282fc8118586550c3b30aabf446797a5a27df62d7cdb2" + }, + { + "name": "uap-native-evidence-781463327/13-runtime-repair-projection.log", + "bytes": 268, + "sha256": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e" + }, + { + "name": "uap-native-evidence-781463327/12-repair.log", + "bytes": 1955, + "sha256": "896ee80367aa463c967659092e7387e65d34fa3a24c90fe9afe8ffd917c34cf5" + }, + { + "name": "uap-native-evidence-781463327/14-runtime-repair.log", + "bytes": 15519, + "sha256": "914eb35f1661b60fdf1d4c0617d4833a29245db1fda778fa8b647dfd6b0e80e3" + }, + { + "name": "uap-native-evidence-781463327/15-remove.log", + "bytes": 1272, + "sha256": "60ba161826a5e28ce459278fd9e76fa721e268bef27f0708d6bdbf84ce6467fc" + }, + { + "name": "uap-native-evidence-781463327/16-removed.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "uap-native-evidence-781463327/events.jsonl", + "bytes": 16746, + "sha256": "94d5c15206f9ca87fd6eedaece39e542c5c1346d0c45c69644f2c15e47b9b011" + }, + { + "name": "uap-native-evidence-781463327/evidence.json", + "bytes": 3443, + "sha256": "7ffca73c45406e05d727e3f206d40caed451809727e953cfb253c7dca5fecc9c" + }, + { + "name": "uap-native-evidence-781463327/http-events.jsonl", + "bytes": 5889, + "sha256": "bc47705bd2546fab28b6b9ee795559516383e2ac2e9c09fc5c0f9791f1b55a1b" + }, + { + "name": "uap-native-evidence-781463327/runtime-A-gateway.json", + "bytes": 267739, + "sha256": "0b6bcc549d3826263e88b445fd391ff10ea0f611be1959dc46e4d09a95aacacb" + }, + { + "name": "uap-native-evidence-781463327/runtime-B-gateway.json", + "bytes": 267736, + "sha256": "52882fd266d75b5941b69216b44ceef1d44f509dc7f7ad47c46e7a05a7a993c5" + }, + { + "name": "uap-native-evidence-781463327/runtime-C-gateway.json", + "bytes": 267736, + "sha256": "629b7dd8c267e7d13ceb4e452804db8d128466b2e0057c5658b25840f659e083" + }, + { + "name": "uap-native-evidence-781463327/runtime-repair-gateway.json", + "bytes": 267736, + "sha256": "4495b169c5adb0b33738fcdbb22cb62a049418aec6dc11e78acf97bd3a2e736c" + } + ] +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/native-tests.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/native-tests.log new file mode 100644 index 000000000..1e9e8d82f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/native-tests.log @@ -0,0 +1,13 @@ +=== RUN TestAgentpluginsClaudeNativeCommandsLifecycle + agentplugins_claude_commands_native_e2e_test.go:46: claude native evidence: D:\a\_temp\uap-native-hosted-wlb4x_pl\evidence\uap-claude-native-evidence-3306580122 +--- PASS: TestAgentpluginsClaudeNativeCommandsLifecycle (13.48s) +=== RUN TestAgentpluginsClaudeNativeLifecycle + agentplugins_claude_native_e2e_test.go:376: claude native evidence: D:\a\_temp\uap-native-hosted-wlb4x_pl\evidence\uap-claude-native-evidence-925369014 +--- PASS: TestAgentpluginsClaudeNativeLifecycle (17.33s) +=== RUN TestAgentpluginsClaudeNativeProjectPreparedLifecycle + agentplugins_claude_project_native_e2e_test.go:16: claude native evidence: D:\a\_temp\uap-native-hosted-wlb4x_pl\evidence\uap-claude-native-evidence-2732888560 +--- PASS: TestAgentpluginsClaudeNativeProjectPreparedLifecycle (14.77s) +=== RUN TestAgentpluginsClaudeNativeRuntimeLifecycle + agentplugins_claude_runtime_fixture_test.go:224: native evidence: D:\a\_temp\uap-native-hosted-wlb4x_pl\evidence\uap-native-evidence-781463327 +--- PASS: TestAgentpluginsClaudeNativeRuntimeLifecycle (18.76s) +PASS diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/runner-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/runner-evidence.json new file mode 100644 index 000000000..976e214ba --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/runner-evidence.json @@ -0,0 +1,189 @@ +{ + "schema_version": 1, + "client": "claude", + "target": "windows-amd64", + "started_utc": "2026-10-24T22:43:27Z", + "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", + "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof", + "status": "passed", + "scratch": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl", + "commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a", + "client_asset": { + "source": "https://registry.npmjs.org/@anthropic-ai/claude-code-win32-x64/-/claude-code-win32-x64-2.1.263.tgz", + "version": "2.1.263", + "archive": "claude-code-win32-x64-2.1.263.tgz", + "archive_integrity": "sha512-P1LnudAWj2ptyE0IZzCZKAe7nU7LxqlkcLdBfp8FsHlH5dp75Og4JJ6TClvfjanHn3shYW4CYN6oI1vSqZlkHw==", + "binary_sha256": "0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03" + }, + "scanner_asset": { + "source": "https://github.com/777genius/lintai/releases/tag/v0.1.3", + "version": "v0.1.3", + "archive": "lintai-v0.1.3-x86_64-pc-windows-msvc.zip", + "archive_integrity": "sha256:2f61f6a83a160afa3feed9ea1722b82d0d938ebff865a4e20d39b5f55270c911", + "binary_sha256": "9d486e5b66875384c12a147b8a19007beebe2691bc6d23a7867bf019b70be453" + }, + "ripgrep_asset": { + "source": "https://github.com/BurntSushi/ripgrep/releases/tag/15.2.0", + "version": "15.2.0", + "archive": "ripgrep-15.2.0-x86_64-pc-windows-msvc.zip", + "archive_integrity": "sha256:71b2fef860abe467217a538ff31de02f5258807c0129f771846f87bd029aafc5", + "binary_sha256": "14231169855ec5205cf5a1b6f1db358ff4aed4247c86b69ce8aae647c77f6680" + }, + "git_path": "C:\\Program Files\\Git\\ucrt64\\bin\\git.EXE", + "git_version": "git version 2.56.0.windows.2", + "git_sh_sha256": "6cc575e9112efe6253b6a6999c12aa0240cb3f511aeda3480ae026edc0dc5280", + "git_bash_sha256": "6cc575e9112efe6253b6a6999c12aa0240cb3f511aeda3480ae026edc0dc5280", + "harness_build_sha256": { + "native-probe.exe": "1759d2e057e4577a7bfc4e5fad5032a24258aefc25727d605217e87d6320fc21", + "repotests.exe": "39d6ab6dfa9b068cecceb788a8cbbb065f358b1562bdbd3a8c68e4617659602f" + }, + "installer_sha256": "8dff69684ba79422b0fbad72b91c978aa76dbb9bee1c66c3d8c7acdcfd5ca76f", + "test_command": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\repotests.exe", + "-test.v", + "-test.timeout=15m", + "-test.run=^(?:TestAgentpluginsClaudeNativeCommandsLifecycle|TestAgentpluginsClaudeNativeLifecycle|TestAgentpluginsClaudeNativeProjectPreparedLifecycle|TestAgentpluginsClaudeNativeRuntimeLifecycle)$" + ], + "required_tests": [ + "TestAgentpluginsClaudeNativeCommandsLifecycle", + "TestAgentpluginsClaudeNativeLifecycle", + "TestAgentpluginsClaudeNativeProjectPreparedLifecycle", + "TestAgentpluginsClaudeNativeRuntimeLifecycle" + ], + "project_components": true, + "exit_code": 0, + "passed_tests": [ + "TestAgentpluginsClaudeNativeCommandsLifecycle", + "TestAgentpluginsClaudeNativeLifecycle", + "TestAgentpluginsClaudeNativeProjectPreparedLifecycle", + "TestAgentpluginsClaudeNativeRuntimeLifecycle" + ], + "skipped_tests": [], + "finished_utc": "2026-10-24T22:45:48Z", + "artifact_sha256": { + "native-tests.log": "2f878922f80a61b465090809221c194dbc7b070f88d520eb65661719646f6c6b", + "uap-claude-native-evidence-2732888560/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-2732888560/add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "uap-claude-native-evidence-2732888560/add-installer.json": "d5f08d4a2f8e1a4dd449e9e25cbd516c5d0c8af8ca4a88f1dcf956aeb6a5bac3", + "uap-claude-native-evidence-2732888560/add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "uap-claude-native-evidence-2732888560/add-repeated-installer.json": "279666535285314035828281596734a479a99065a61f0415cac5c672f0982f77", + "uap-claude-native-evidence-2732888560/after-add-native-A.json": "c954a66f02f81c47dfbbbf638ba0100a5edb922f492b51c2583cccbba15c94fd", + "uap-claude-native-evidence-2732888560/after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-2732888560/after-add-native-B.json": "03885704640af29ed024e08372bdbf0ea41ff2347e2afba0328e2b7855365dd3", + "uap-claude-native-evidence-2732888560/after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.json": "3c8078261e72425b8498ea91eab2fd469d743bf85eb5f8596bdab0a9d7a2935b", + "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.json": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49", + "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-remove-native-A.json": "57f48b45edcece506ad6e8cd3ad71590c02ec899ef1ebd0c59c994d2e1054dfb", + "uap-claude-native-evidence-2732888560/after-remove-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-remove-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-remove-native-B.json": "192ad8bd8acc8cf5d81d203e31557b58be01d57e17ed4f3b4581c5662f5b8892", + "uap-claude-native-evidence-2732888560/after-remove-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-remove-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-repair-native-A.json": "6a3ddf809682666feba66ff1d22d8c2976ce1f2ee869941dda35efaf0e7fe06d", + "uap-claude-native-evidence-2732888560/after-repair-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-repair-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-2732888560/after-repair-native-B.json": "aa8954efdf2045c6a88163ec896ab2606e7e113d1541bbf771d670a8d87bdf81", + "uap-claude-native-evidence-2732888560/after-repair-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-repair-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-repeated-add-native-A.json": "2c20a4e194c156a533731a16c174dd6de54e38631f117268c43265dfc3f86f7d", + "uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-2732888560/after-repeated-add-native-B.json": "df920e269331dfe1a656f384724cf995be3fe8a44854a16b4cd379642bc8a4fb", + "uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/after-update-native-A.json": "91663e368949a1c8c6f0e52eb4d0562c44b49b3e45cf51ca3f81ac5ad114c189", + "uap-claude-native-evidence-2732888560/after-update-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-update-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "uap-claude-native-evidence-2732888560/after-update-native-B.json": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49", + "uap-claude-native-evidence-2732888560/after-update-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/after-update-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-2732888560/binding-after-add.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "uap-claude-native-evidence-2732888560/binding-after-repair.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "uap-claude-native-evidence-2732888560/binding-after-update.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "uap-claude-native-evidence-2732888560/claude-project-plugin-evidence.json": "7941439ed3874237fa632ba9f8f6d4a37a2d04794b0c9058de3bad12f0e1abbb", + "uap-claude-native-evidence-2732888560/remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-2732888560/remove-installer.json": "4f74431cc01a43fa65e2904877292f363658fb8ddd8622101e6792fb3c292889", + "uap-claude-native-evidence-2732888560/repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-2732888560/repair-absent-installer.json": "18f8bcd176b315d17beab6ff211b8deac6c269673fc98cf81fb0a00a32a29f65", + "uap-claude-native-evidence-2732888560/repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-2732888560/repair-intact-installer.json": "85eaecbd1ef71d6465e63ff4e793cb9eeb1361b2c51b37f8a856ab5621a2477e", + "uap-claude-native-evidence-2732888560/update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-2732888560/update-invocation-change-installer.json": "aab992c663a101a1240a1b1f2b3857667e914bc7f0f378f09eeeb970b97e1a80", + "uap-claude-native-evidence-2732888560/update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-2732888560/update-unchanged-installer.json": "ac8b802cd981c43a09bcbc841dca65f2bbd7b961a3ff8e355821f2a531d722a9", + "uap-claude-native-evidence-3306580122/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-3306580122/02-skills-after-add-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "uap-claude-native-evidence-3306580122/03-skills-after-add-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "uap-claude-native-evidence-3306580122/04-skills-after-unchanged-update-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "uap-claude-native-evidence-3306580122/05-skills-after-unchanged-update-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "uap-claude-native-evidence-3306580122/06-skills-after-invocation-change-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "uap-claude-native-evidence-3306580122/07-skills-after-invocation-change-details.log": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032", + "uap-claude-native-evidence-3306580122/08-skills-after-intact-repair-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "uap-claude-native-evidence-3306580122/09-skills-after-intact-repair-details.log": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032", + "uap-claude-native-evidence-3306580122/10-list-before-absent-repair.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "uap-claude-native-evidence-3306580122/11-skills-after-absent-repair-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "uap-claude-native-evidence-3306580122/12-skills-after-absent-repair-details.log": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032", + "uap-claude-native-evidence-3306580122/13-skills-after-remove-list.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-claude-native-evidence-3306580122/add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "uap-claude-native-evidence-3306580122/add-installer.json": "6d26e78cd7954b1118cf2515c8928a24412d32822395f650f672bc5c2f96fecd", + "uap-claude-native-evidence-3306580122/commands-evidence.json": "073efc68ddea9bbe8d55184e7047f1af42383e5f9bd3ce5797a66f64bfe4c832", + "uap-claude-native-evidence-3306580122/remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "uap-claude-native-evidence-3306580122/remove-installer.json": "32328b55490adbe5fad5f4858b16244dc6eeab2409014bce19d51043717b9e5f", + "uap-claude-native-evidence-3306580122/repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-3306580122/repair-absent-installer.json": "a8df3d5ee904d95b67a65ce9518bcf9f30ccf457bedc42eebe77e2d61ab163ce", + "uap-claude-native-evidence-3306580122/repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "uap-claude-native-evidence-3306580122/repair-intact-installer.json": "68397315a3f9a08ff7ba3069537587f3c7f7f6b538266a3ed814bf7a4da29e02", + "uap-claude-native-evidence-3306580122/update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-3306580122/update-invocation-change-installer.json": "ecec0e12a5a8f6e0814bd0dd9b411d3f223e909d6748a21f45fc06d3c2266bbc", + "uap-claude-native-evidence-3306580122/update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "uap-claude-native-evidence-3306580122/update-unchanged-installer.json": "6a53239d38904b8191948faaf54b72b5f9e8333f5f8e9fa0a4bfd95805c7ef14", + "uap-claude-native-evidence-925369014/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-claude-native-evidence-925369014/02-add-v1.log": "a2db051b3f735c87f2fa98193671b5dfbfbbf33f75bb303862f1d1b1ce0d4873", + "uap-claude-native-evidence-925369014/03-list-after-install.log": "8f2b17e0a3b6a54d26480ae929f1738806e70ff5734d10efbe646e7019059b96", + "uap-claude-native-evidence-925369014/04-details-after-install.log": "f6d3b225f3f8d90d64fafe36b5ebe8c1ed46bf6867fa68f9897c08bbcda50b64", + "uap-claude-native-evidence-925369014/05-mcp-list-after-install.log": "d3b65ab1c676d469493812220f8885a96d497d95faf1d939a7d81246f0efb976", + "uap-claude-native-evidence-925369014/06-update-v2.log": "4666dd311eea6740440c435b3d058cbfba338a1f02fd6fed984ab343a305eb0c", + "uap-claude-native-evidence-925369014/07-list-after-version-update.log": "fd8c6595cfee831286e5e74130c01389b1d902d1a147b3d97fe6f4207f290269", + "uap-claude-native-evidence-925369014/08-update-same-version.log": "9f251a7130b0cceda2ff39bb331a93b6852ab69f3c68c8162c44cda0507c7e3a", + "uap-claude-native-evidence-925369014/09-update-unchanged.log": "5736f9e2a75ea6ba77372e08d3039bd17a9b38286ec2377e8f526e3672cc81d2", + "uap-claude-native-evidence-925369014/10-repair-absent.log": "56e0bc5eb09991e09134e2ebb13d1991f7d676b6221658adaf4d2b30a78685ae", + "uap-claude-native-evidence-925369014/11-list-after-repair.log": "fd8c6595cfee831286e5e74130c01389b1d902d1a147b3d97fe6f4207f290269", + "uap-claude-native-evidence-925369014/12-details-after-repair.log": "61091ab19d98cc042ceb8919e12a7bd1efcef34cc8166fb2ca79296909c16831", + "uap-claude-native-evidence-925369014/13-remove-blocked-by-foreign-content.log": "8e716a4b90122a78be6230f6fa4eeec4b5a9d124079ac20a31451368300595aa", + "uap-claude-native-evidence-925369014/14-remove.log": "1dc1b4f8c8986f3657fdc57ca1083b61d850215ee98fb5a616e575e1d729256e", + "uap-claude-native-evidence-925369014/15-list-after-remove.log": "43f621b29927cde88d013813ac4bde06667ffb93d90827a5aa4d84692a5378c9", + "uap-claude-native-evidence-925369014/16-repeat-remove.log": "9f7e8a6d254b5cfd57ccbb292dd645cb88a62042fc82f7c940dacec0abbcff53", + "uap-claude-native-evidence-925369014/evidence.json": "8d3070f13beeb2b9b4ede291d62efc6916caa6a9c692a7c54dc360bb5d5167ed", + "uap-claude-native-evidence-925369014/remove-inventory-diff.json": "9929ab8191b587dcf75fc0efe6bd53ba026c4d7d8b6445616317ffce20b37a6b", + "uap-native-evidence-781463327/01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "uap-native-evidence-781463327/02-add.log": "3d09e25e78d204599669c4bf2d4e17bc19735a28f2a0e2aafd4bd6706577becd", + "uap-native-evidence-781463327/03-installed.log": "fa7d5a681b9679ec90048cb2debb3b80d6e19d27b6f6fd7b461dc68a38f95afa", + "uap-native-evidence-781463327/04-runtime-A-projection.log": "fa7d5a681b9679ec90048cb2debb3b80d6e19d27b6f6fd7b461dc68a38f95afa", + "uap-native-evidence-781463327/05-runtime-A.log": "f21172d9902ea84d92dca3175cc4f3dc0642b2b133ceddb6f026e070dd156186", + "uap-native-evidence-781463327/06-update.log": "62deb607d232f07dc85e36cc5380473a2cc17dd19c562feb6e59b1ee70efaa02", + "uap-native-evidence-781463327/07-runtime-B-projection.log": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e", + "uap-native-evidence-781463327/08-runtime-B.log": "99134edf57d1b15eb25ed81ffcbd1f58a81ed9a72b29767ca0bdda7460e01d97", + "uap-native-evidence-781463327/09-same-version-update.log": "20fdb76268aff018ca872048be00c42f213f1b7d84c1ed387e0e16beca205bcc", + "uap-native-evidence-781463327/10-runtime-C-projection.log": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e", + "uap-native-evidence-781463327/11-runtime-C.log": "c09e5325a0a0180c6cf282fc8118586550c3b30aabf446797a5a27df62d7cdb2", + "uap-native-evidence-781463327/12-repair.log": "896ee80367aa463c967659092e7387e65d34fa3a24c90fe9afe8ffd917c34cf5", + "uap-native-evidence-781463327/13-runtime-repair-projection.log": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e", + "uap-native-evidence-781463327/14-runtime-repair.log": "914eb35f1661b60fdf1d4c0617d4833a29245db1fda778fa8b647dfd6b0e80e3", + "uap-native-evidence-781463327/15-remove.log": "60ba161826a5e28ce459278fd9e76fa721e268bef27f0708d6bdbf84ce6467fc", + "uap-native-evidence-781463327/16-removed.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "uap-native-evidence-781463327/events.jsonl": "94d5c15206f9ca87fd6eedaece39e542c5c1346d0c45c69644f2c15e47b9b011", + "uap-native-evidence-781463327/evidence.json": "7ffca73c45406e05d727e3f206d40caed451809727e953cfb253c7dca5fecc9c", + "uap-native-evidence-781463327/http-events.jsonl": "bc47705bd2546fab28b6b9ee795559516383e2ac2e9c09fc5c0f9791f1b55a1b", + "uap-native-evidence-781463327/runtime-A-gateway.json": "0b6bcc549d3826263e88b445fd391ff10ea0f611be1959dc46e4d09a95aacacb", + "uap-native-evidence-781463327/runtime-B-gateway.json": "52882fd266d75b5941b69216b44ceef1d44f509dc7f7ad47c46e7a05a7a993c5", + "uap-native-evidence-781463327/runtime-C-gateway.json": "629b7dd8c267e7d13ceb4e452804db8d128466b2e0057c5658b25840f659e083", + "uap-native-evidence-781463327/runtime-repair-gateway.json": "4495b169c5adb0b33738fcdbb22cb62a049418aec6dc11e78acf97bd3a2e736c" + } +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/01-claude-version.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/01-claude-version.log new file mode 100644 index 000000000..870ddd4c2 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/01-claude-version.log @@ -0,0 +1 @@ +2.1.263 (Claude Code) diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-installer.json new file mode 100644 index 000000000..86eb5b292 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-75c7323c47b69ece9b1a8e4050633684","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-75c7323c47b69ece9b1a8e4050633684","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d4feed202bda","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-repeated-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-repeated-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-repeated-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-repeated-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-repeated-installer.json new file mode 100644 index 000000000..ee6ae031e --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/add-repeated-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-25dffdc9ea1e0ae685ffc6460a142093","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"cache"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d4feed202bda","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":false,"no_change":true}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.json new file mode 100644 index 000000000..ca83286ec --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A", + "deadline": false, + "elapsed_ms": 346, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.json new file mode 100644 index 000000000..1c15329eb --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-B", + "deadline": false, + "elapsed_ms": 340, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.json new file mode 100644 index 000000000..e19dca3b9 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A", + "deadline": false, + "elapsed_ms": 338, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-A.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.json new file mode 100644 index 000000000..6a9f3a185 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-B", + "deadline": false, + "elapsed_ms": 343, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-owned-directory-loss-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.json new file mode 100644 index 000000000..79396e758 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A", + "deadline": false, + "elapsed_ms": 343, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-A.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.json new file mode 100644 index 000000000..a24fcaf59 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-B", + "deadline": false, + "elapsed_ms": 335, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-remove-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.json new file mode 100644 index 000000000..35438c5d4 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A", + "deadline": false, + "elapsed_ms": 343, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.json new file mode 100644 index 000000000..fb2992061 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-B", + "deadline": false, + "elapsed_ms": 333, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repair-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.json new file mode 100644 index 000000000..6831f9238 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A", + "deadline": false, + "elapsed_ms": 340, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.json new file mode 100644 index 000000000..27d798184 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-B", + "deadline": false, + "elapsed_ms": 347, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-repeated-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.json new file mode 100644 index 000000000..9ac1b8358 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A", + "deadline": false, + "elapsed_ms": 337, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.json new file mode 100644 index 000000000..6a9f3a185 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-B", + "deadline": false, + "elapsed_ms": 343, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/after-update-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-add.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-add.json new file mode 100644 index 000000000..484416dc8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-add.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_29f33ec90be9ae91f3047980", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A\\.claude\\skills\\uap-commands-fixture-d4feed202bda", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-repair.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-repair.json new file mode 100644 index 000000000..484416dc8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-repair.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_29f33ec90be9ae91f3047980", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A\\.claude\\skills\\uap-commands-fixture-d4feed202bda", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-update.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-update.json new file mode 100644 index 000000000..484416dc8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/binding-after-update.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_29f33ec90be9ae91f3047980", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A\\.claude\\skills\\uap-commands-fixture-d4feed202bda", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/claude-project-plugin-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/claude-project-plugin-evidence.json new file mode 100644 index 000000000..9e4827312 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/claude-project-plugin-evidence.json @@ -0,0 +1,152 @@ +{ + "arch": "amd64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-installer.json": "d5f08d4a2f8e1a4dd449e9e25cbd516c5d0c8af8ca4a88f1dcf956aeb6a5bac3", + "add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-repeated-installer.json": "279666535285314035828281596734a479a99065a61f0415cac5c672f0982f77", + "after-add-native-A.json": "c954a66f02f81c47dfbbbf638ba0100a5edb922f492b51c2583cccbba15c94fd", + "after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-add-native-B.json": "03885704640af29ed024e08372bdbf0ea41ff2347e2afba0328e2b7855365dd3", + "after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-owned-directory-loss-native-A.json": "3c8078261e72425b8498ea91eab2fd469d743bf85eb5f8596bdab0a9d7a2935b", + "after-owned-directory-loss-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-owned-directory-loss-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-owned-directory-loss-native-B.json": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49", + "after-owned-directory-loss-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-owned-directory-loss-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-remove-native-A.json": "57f48b45edcece506ad6e8cd3ad71590c02ec899ef1ebd0c59c994d2e1054dfb", + "after-remove-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-remove-native-A.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-remove-native-B.json": "192ad8bd8acc8cf5d81d203e31557b58be01d57e17ed4f3b4581c5662f5b8892", + "after-remove-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-remove-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-repair-native-A.json": "6a3ddf809682666feba66ff1d22d8c2976ce1f2ee869941dda35efaf0e7fe06d", + "after-repair-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repair-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-repair-native-B.json": "aa8954efdf2045c6a88163ec896ab2606e7e113d1541bbf771d670a8d87bdf81", + "after-repair-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repair-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-repeated-add-native-A.json": "2c20a4e194c156a533731a16c174dd6de54e38631f117268c43265dfc3f86f7d", + "after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-repeated-add-native-B.json": "df920e269331dfe1a656f384724cf995be3fe8a44854a16b4cd379642bc8a4fb", + "after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-update-native-A.json": "91663e368949a1c8c6f0e52eb4d0562c44b49b3e45cf51ca3f81ac5ad114c189", + "after-update-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-update-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-update-native-B.json": "d542e82d62e4c3981b6a0cd3658f087815648becf5891a6a4f02f2f0db7cac49", + "after-update-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-update-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "binding-after-add.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "binding-after-repair.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "binding-after-update.json": "035ed5ab59f7a954deeb619ea4d95626674f97ac3d0779ca0643ac87b09a0d61", + "remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "remove-installer.json": "4f74431cc01a43fa65e2904877292f363658fb8ddd8622101e6792fb3c292889", + "repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-absent-installer.json": "18f8bcd176b315d17beab6ff211b8deac6c269673fc98cf81fb0a00a32a29f65", + "repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-intact-installer.json": "85eaecbd1ef71d6465e63ff4e793cb9eeb1361b2c51b37f8a856ab5621a2477e", + "update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-invocation-change-installer.json": "aab992c663a101a1240a1b1f2b3857667e914bc7f0f378f09eeeb970b97e1a80", + "update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-unchanged-installer.json": "ac8b802cd981c43a09bcbc841dca65f2bbd7b961a3ff8e355821f2a531d722a9" + }, + "client": "claude", + "client_sha256": "0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:45:30.0606628Z", + "installer_sha256": "8dff69684ba79422b0fbad72b91c978aa76dbb9bee1c66c3d8c7acdcfd5ca76f", + "native_surface": "fresh bounded native project lists prove suppression; owned file lifecycle is prepared; no trust/model/MCP initialization", + "os": "windows", + "project_contract": "untrusted_prepared_v1", + "scanner": { + "archive_sha256": "2f61f6a83a160afa3feed9ea1722b82d0d938ebff865a4e20d39b5f55270c911", + "binary_sha256": "9d486e5b66875384c12a147b8a19007beebe2691bc6d23a7867bf019b70be453", + "platform": "windows-amd64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a" + }, + "stages": { + "absent_repair": { + "status": "passed", + "reason": "production repair rebuilt missing exact-owned directory; fresh native list again suppressed it" + }, + "command_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "install": { + "status": "passed", + "reason": "production initial add installed owned files as Prepared+Installed; native project scan remains suppressed", + "artifacts": [ + "add-installer.json", + "binding-after-add.json" + ] + }, + "intact_repair": { + "status": "passed", + "reason": "intact repair preserved prepared files and explicit native blocker" + }, + "invocation_change_update": { + "status": "passed", + "reason": "production update changed owned command projection while native discovery stayed blocked" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "native_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "native_suppression_boundary": { + "status": "passed", + "reason": "bounded raw pinned native A/B lists retained the untrusted project suppression boundary" + }, + "prepared_command_projection": { + "status": "passed", + "reason": "owned projected files contain exact rendered ordinary command bytes; this is filesystem evidence" + }, + "prepared_explicit_command_withheld": { + "status": "passed", + "reason": "plan and owned projected files withhold the explicit-only command; no native loaded-skill claim" + }, + "project_scope_preservation": { + "status": "passed", + "reason": "foreign user/cache/project A/project B hashes remained exact; fresh project-B native lists never exposed A" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "remove": { + "status": "passed", + "reason": "production inactive removal deleted only owned project files; fresh native list no longer suppresses a managed directory" + }, + "unchanged_add": { + "status": "passed", + "reason": "repeated add reobserved real suppression without file/state mutation" + }, + "unchanged_update": { + "status": "passed", + "reason": "unchanged prepared update retained exact state and projected bytes" + } + }, + "started_utc": "2026-10-24T22:45:15.5547353Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/remove-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/remove-installer-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/remove-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/remove-installer.json new file mode 100644 index 000000000..63d9cb1c8 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/remove-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"remove","result":"success","data":{"operation_id":"op-109e07673ce83b68741b379f4e59a047","batch":true,"status":"data_retained","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","plugin_data_preserved":true,"data_retained":true,"retained_data":[{"data_receipt_id":"data_bdef1fdb32a327141d7e9504","physical_backend_id":"uap-commands-fixture-d4feed202bda","scope":"project","state":"owned"}],"retained_data_action":"run `agentplugins remove 47bce8b8-088d-452f-b343-7996d7105f06 --purge-data --scope project --project-root 'D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-claude-native-evidence-2732888560\\project-A'` to delete retained plugin data","dry_run":false,"targets":[{"target":"claude","status":"external_completed","output":{"operation_id":"op-109e07673ce83b68741b379f4e59a047","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plugin":"uap-commands-fixture","client_id":"claude","requires_confirmation":false,"mutated":true,"deactivation":{"activation":"not_required","artifact_removal_allowed":true,"external_removal_complete":true},"affected_surfaces":["claude"],"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-absent-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-absent-installer-stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-absent-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-absent-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-absent-installer.json new file mode 100644 index 000000000..a5426a928 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-absent-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-76ec7c2f11c5570a8270994ef1319ce6","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-76ec7c2f11c5570a8270994ef1319ce6","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d4feed202bda","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-intact-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-intact-installer-stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-intact-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-intact-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-intact-installer.json new file mode 100644 index 000000000..d4a20e2a0 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/repair-intact-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-9960cb20facd29e2693aae7f274888a4","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-9960cb20facd29e2693aae7f274888a4","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d4feed202bda","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer.json new file mode 100644 index 000000000..a3b76a92f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-invocation-change-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-7030a5a8cd3f3467ec3202076f909e1a","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","dry_run":false,"targets":[{"target":"claude","selected":true,"status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-7030a5a8cd3f3467ec3202076f909e1a","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d4feed202bda","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-unchanged-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-unchanged-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-unchanged-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-unchanged-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-unchanged-installer.json new file mode 100644 index 000000000..c1b5ea0eb --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-2732888560/update-unchanged-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-e47acc12bec2d996443f3472b084d67a","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","dry_run":false,"targets":[{"target":"claude","selected":true,"status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-e47acc12bec2d996443f3472b084d67a","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"47bce8b8-088d-452f-b343-7996d7105f06","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d4feed202bda","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":false,"no_change":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-3306580122/commands-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-3306580122/commands-evidence.json new file mode 100644 index 000000000..612767454 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-3306580122/commands-evidence.json @@ -0,0 +1,97 @@ +{ + "acquisition": "local_directory", + "arch": "amd64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "02-skills-after-add-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "03-skills-after-add-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "04-skills-after-unchanged-update-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "05-skills-after-unchanged-update-details.log": "3b2a2b11779d6134b2f5d88366123d51cbdb2dc9c048c32d5807f2724486e527", + "06-skills-after-invocation-change-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "07-skills-after-invocation-change-details.log": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032", + "08-skills-after-intact-repair-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "09-skills-after-intact-repair-details.log": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032", + "10-list-before-absent-repair.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "11-skills-after-absent-repair-list.log": "dd72d09e7ca019e94a83d02d2e5b8d60bac242352cb386020de1fad777c2f235", + "12-skills-after-absent-repair-details.log": "f5d337af34556fee5c032c1ebe98cb2a7952cc58012e60f8f7357e3db7117032", + "13-skills-after-remove-list.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-installer.json": "6d26e78cd7954b1118cf2515c8928a24412d32822395f650f672bc5c2f96fecd", + "remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "remove-installer.json": "32328b55490adbe5fad5f4858b16244dc6eeab2409014bce19d51043717b9e5f", + "repair-absent-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-absent-installer.json": "a8df3d5ee904d95b67a65ce9518bcf9f30ccf457bedc42eebe77e2d61ab163ce", + "repair-intact-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "repair-intact-installer.json": "68397315a3f9a08ff7ba3069537587f3c7f7f6b538266a3ed814bf7a4da29e02", + "update-invocation-change-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-invocation-change-installer.json": "ecec0e12a5a8f6e0814bd0dd9b411d3f223e909d6748a21f45fc06d3c2266bbc", + "update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-unchanged-installer.json": "6a53239d38904b8191948faaf54b72b5f9e8333f5f8e9fa0a4bfd95805c7ef14" + }, + "client": "claude", + "client_sha256": "0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:44:57.9587805Z", + "installer_sha256": "8dff69684ba79422b0fbad72b91c978aa76dbb9bee1c66c3d8c7acdcfd5ca76f", + "native_surface": "claude plugin list --json and plugin details; installed SKILL.md bytes", + "os": "windows", + "scanner": { + "archive_sha256": "2f61f6a83a160afa3feed9ea1722b82d0d938ebff865a4e20d39b5f55270c911", + "binary_sha256": "9d486e5b66875384c12a147b8a19007beebe2691bc6d23a7867bf019b70be453", + "platform": "windows-amd64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a" + }, + "stages": { + "absent_repair": { + "status": "passed", + "reason": "the deleted managed artifact was rebuilt with the rendered command skill" + }, + "command_skill_discovery": { + "status": "passed", + "reason": "the client itself lists the ordinary command as a plugin skill with the exact rendered bytes" + }, + "explicit_command_withheld": { + "status": "passed", + "reason": "the explicit-only command is no planned component, carries an explicit_invocation_not_enforceable warning and is absent from the client's own skill listing" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no client declares explicit-invocation enforcement yet, so explicit-only commands are withheld" + }, + "install": { + "status": "passed", + "artifacts": [ + "add-installer.json" + ] + }, + "intact_repair": { + "status": "passed", + "reason": "repair of an intact package with current command rules succeeded and left the client skills unchanged" + }, + "invocation_change_update": { + "status": "passed", + "reason": "after the commands swapped invocation flags, update delivered the newly ordinary command and the client no longer lists the newly explicit-only one" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no model, login or OAuth is used" + }, + "remove": { + "status": "passed", + "reason": "the client lists no plugin skill after remove" + }, + "unchanged_update": { + "status": "passed", + "reason": "update of the unchanged package: mutated=false, byte-identical installer state, same client skills and bytes" + } + }, + "started_utc": "2026-10-24T22:44:44.7728312Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-925369014/evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-925369014/evidence.json new file mode 100644 index 000000000..c39de6764 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-claude-native-evidence-925369014/evidence.json @@ -0,0 +1,75 @@ +{ + "client_binary_sha256": "0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:45:15.016475Z", + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_binary_sha256": "8dff69684ba79422b0fbad72b91c978aa76dbb9bee1c66c3d8c7acdcfd5ca76f", + "installer_patch_sha256": "", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a", + "stages": { + "foreign_content_inside_managed_directory_blocked": { + "status": "passed", + "reason": "exact ownership-verification refusal (\"artifact digest mismatch\") when foreign content is planted inside the plugin's own managed directory; the foreign file survived the blocked attempt and was removed only by the test before the real remove below" + }, + "install": { + "status": "passed" + }, + "mcp_transport_discovery": { + "status": "passed", + "reason": "claude mcp list classified remote-http as (HTTP) and remote-sse as (SSE) and attempted a real (loopback-refused) connection to each, distinct from plugin details' shallow key enumeration; no live tool call or successful connection claimed" + }, + "owned_repair": { + "status": "passed", + "reason": "managed directory re-staged from the still-present, exact-revision-matching local source after deletion; restored content bytes, .mcp.json, and Claude plugin details verified; receipt records the physically absent before-state" + }, + "remove": { + "status": "passed", + "reason": "managed directory removed; sibling foreign plugin's sentinel content and its sole remaining claude plugin list entry both confirmed; exact inventory diff recorded (removed=8 all plugin-owned, added=0 installer-state only, changed=2)" + }, + "repeat_remove": { + "status": "passed", + "reason": "exact expected refusal message" + }, + "same_version_refresh": { + "status": "passed" + }, + "skill_discovery": { + "status": "passed", + "reason": "exact plugin-attributed skill enumerated by the real CLI with exact body bytes verified on disk; no model use claimed" + }, + "stdio_discovery": { + "status": "passed", + "reason": "local stdio enumerated by native CLI; no successful tool call claimed" + }, + "unchanged": { + "status": "passed", + "reason": "mutated:false plus byte-identical managed-directory tree digest and state-file digest before/after a byte-identical re-apply" + }, + "version_update": { + "status": "passed" + } + }, + "stdio": { + "status": "passed", + "reason": "local stdio enumerated by native CLI; no successful tool call claimed" + }, + "stdio_runtime_cwd_argv_env_data": "not_evaluated", + "transcript_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "02-add-v1.log": "a2db051b3f735c87f2fa98193671b5dfbfbbf33f75bb303862f1d1b1ce0d4873", + "03-list-after-install.log": "8f2b17e0a3b6a54d26480ae929f1738806e70ff5734d10efbe646e7019059b96", + "04-details-after-install.log": "f6d3b225f3f8d90d64fafe36b5ebe8c1ed46bf6867fa68f9897c08bbcda50b64", + "05-mcp-list-after-install.log": "d3b65ab1c676d469493812220f8885a96d497d95faf1d939a7d81246f0efb976", + "06-update-v2.log": "4666dd311eea6740440c435b3d058cbfba338a1f02fd6fed984ab343a305eb0c", + "07-list-after-version-update.log": "fd8c6595cfee831286e5e74130c01389b1d902d1a147b3d97fe6f4207f290269", + "08-update-same-version.log": "9f251a7130b0cceda2ff39bb331a93b6852ab69f3c68c8162c44cda0507c7e3a", + "09-update-unchanged.log": "5736f9e2a75ea6ba77372e08d3039bd17a9b38286ec2377e8f526e3672cc81d2", + "10-repair-absent.log": "56e0bc5eb09991e09134e2ebb13d1991f7d676b6221658adaf4d2b30a78685ae", + "11-list-after-repair.log": "fd8c6595cfee831286e5e74130c01389b1d902d1a147b3d97fe6f4207f290269", + "12-details-after-repair.log": "61091ab19d98cc042ceb8919e12a7bd1efcef34cc8166fb2ca79296909c16831", + "13-remove-blocked-by-foreign-content.log": "8e716a4b90122a78be6230f6fa4eeec4b5a9d124079ac20a31451368300595aa", + "14-remove.log": "1dc1b4f8c8986f3657fdc57ca1083b61d850215ee98fb5a616e575e1d729256e", + "15-list-after-remove.log": "43f621b29927cde88d013813ac4bde06667ffb93d90827a5aa4d84692a5378c9", + "16-repeat-remove.log": "9f7e8a6d254b5cfd57ccbb292dd645cb88a62042fc82f7c940dacec0abbcff53" + } +} \ No newline at end of file diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-native-evidence-781463327/evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-native-evidence-781463327/evidence.json new file mode 100644 index 000000000..cfe965b12 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/windows/uap-native-evidence-781463327/evidence.json @@ -0,0 +1,60 @@ +{ + "arch": "amd64", + "client_sha256": "0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03", + "client_version": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T22:45:48.8176417Z", + "installer_data_retention": "passed", + "installer_sha256": "8dff69684ba79422b0fbad72b91c978aa76dbb9bee1c66c3d8c7acdcfd5ca76f", + "oauth": "not_evaluated", + "os": "windows", + "profile_root": "D:\\a\\_temp\\uap-native-hosted-wlb4x_pl\\evidence\\uap-native-evidence-781463327\\claude-config", + "provider": "scripted_loopback", + "real_model": "not_evaluated", + "runtime_scope": "stdio_default+stdio_explicit+HTTP+skill", + "scanner": { + "archive_sha256": "2f61f6a83a160afa3feed9ea1722b82d0d938ebff865a4e20d39b5f55270c911", + "binary_sha256": "9d486e5b66875384c12a147b8a19007beebe2691bc6d23a7867bf019b70be453", + "platform": "windows-amd64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "428bdf5d49e63b78bc4783df6b766c87c8e678ca", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "94f69fd2a2e60040c0718dfec6ea4a778be78a6a" + }, + "stages": { + "install": "passed", + "remove": "passed", + "repair": "passed", + "runtime_A": "passed", + "runtime_B": "passed", + "same_version_refresh": "passed" + }, + "stdio_cwd_argv_env_data": "passed", + "stdio_runtime": "passed", + "transcript_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "02-add.log": "3d09e25e78d204599669c4bf2d4e17bc19735a28f2a0e2aafd4bd6706577becd", + "03-installed.log": "fa7d5a681b9679ec90048cb2debb3b80d6e19d27b6f6fd7b461dc68a38f95afa", + "04-runtime-A-projection.log": "fa7d5a681b9679ec90048cb2debb3b80d6e19d27b6f6fd7b461dc68a38f95afa", + "05-runtime-A.log": "f21172d9902ea84d92dca3175cc4f3dc0642b2b133ceddb6f026e070dd156186", + "06-update.log": "62deb607d232f07dc85e36cc5380473a2cc17dd19c562feb6e59b1ee70efaa02", + "07-runtime-B-projection.log": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e", + "08-runtime-B.log": "99134edf57d1b15eb25ed81ffcbd1f58a81ed9a72b29767ca0bdda7460e01d97", + "09-same-version-update.log": "20fdb76268aff018ca872048be00c42f213f1b7d84c1ed387e0e16beca205bcc", + "10-runtime-C-projection.log": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e", + "11-runtime-C.log": "c09e5325a0a0180c6cf282fc8118586550c3b30aabf446797a5a27df62d7cdb2", + "12-repair.log": "896ee80367aa463c967659092e7387e65d34fa3a24c90fe9afe8ffd917c34cf5", + "13-runtime-repair-projection.log": "c9ce322b5d49f8ed9a85f1d069c247c9f75424ee3cbfeabc94e03a3a36f7294e", + "14-runtime-repair.log": "914eb35f1661b60fdf1d4c0617d4833a29245db1fda778fa8b647dfd6b0e80e3", + "15-remove.log": "60ba161826a5e28ce459278fd9e76fa721e268bef27f0708d6bdbf84ce6467fc", + "16-removed.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "runtime-A-gateway.json": "0b6bcc549d3826263e88b445fd391ff10ea0f611be1959dc46e4d09a95aacacb", + "runtime-B-gateway.json": "52882fd266d75b5941b69216b44ceef1d44f509dc7f7ad47c46e7a05a7a993c5", + "runtime-C-gateway.json": "629b7dd8c267e7d13ceb4e452804db8d128466b2e0057c5658b25840f659e083", + "runtime-repair-gateway.json": "4495b169c5adb0b33738fcdbb22cb62a049418aec6dc11e78acf97bd3a2e736c" + } +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/corroborating-run-40135056328.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/corroborating-run-40135056328.json new file mode 100644 index 000000000..c60c4cd11 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/corroborating-run-40135056328.json @@ -0,0 +1,205 @@ +{ + "run_id": 40135056328, + "commit": "4a1634f4906453eac0b87eddbcc54246226dcfdd", + "tree": "48eabec24a3b0122cf0ca8fc0c1be91fd4f40449", + "status_source": "Purpose-built job metadata; generic run metadata was stale queued", + "artifact_metadata_only": true, + "artifact_zip_downloaded_or_verified": false, + "artifacts": [ + { + "id": 12577706269, + "name": "native-client-claude-darwin-arm64", + "size_in_bytes": 382151, + "url": "https://api.github.com/repos/777genius/universal-agent-plugins/actions/artifacts/12577706269", + "archive_download_url": "https://api.github.com/repos/777genius/universal-agent-plugins/actions/artifacts/12577706269/zip", + "expired": false, + "created_at": "2026-10-24T22:07:39Z", + "expires_at": "2026-11-07T22:07:37Z", + "updated_at": "2026-10-24T22:07:39Z", + "digest": "sha256:437617ca07d58004642aecb7118f53085e846d5646bf6ed0a318611e58b91a28", + "workflow_run": { + "id": 40135056328, + "repository_id": 1193031149, + "head_repository_id": 1193031149, + "head_branch": "feat/project-claude-completion", + "head_sha": "4a1634f4906453eac0b87eddbcc54246226dcfdd" + } + }, + { + "id": 12577305767, + "name": "native-client-claude-windows-amd64", + "size_in_bytes": 396363, + "url": "https://api.github.com/repos/777genius/universal-agent-plugins/actions/artifacts/12577305767", + "archive_download_url": "https://api.github.com/repos/777genius/universal-agent-plugins/actions/artifacts/12577305767/zip", + "expired": false, + "created_at": "2026-10-24T22:04:35Z", + "expires_at": "2026-11-07T22:04:34Z", + "updated_at": "2026-10-24T22:04:35Z", + "digest": "sha256:df268258e3b4aef5292799f826fbe3f87d91fc9d2d01f5e0129bab9401654f74", + "workflow_run": { + "id": 40135056328, + "repository_id": 1193031149, + "head_repository_id": 1193031149, + "head_branch": "feat/project-claude-completion", + "head_sha": "4a1634f4906453eac0b87eddbcc54246226dcfdd" + } + } + ], + "jobs": [ + { + "id": 120994877989, + "name": "claude runtime (darwin-arm64)", + "status": "completed", + "conclusion": "failure", + "run_id": 40135056328, + "logs_url": null, + "steps": [ + { + "name": "Set up job", + "status": "completed", + "conclusion": "success", + "number": 1 + }, + { + "name": "Run actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", + "status": "completed", + "conclusion": "success", + "number": 2 + }, + { + "name": "Run actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e", + "status": "completed", + "conclusion": "success", + "number": 3 + }, + { + "name": "Verify native matrix safety and evidence gates", + "status": "completed", + "conclusion": "success", + "number": 4 + }, + { + "name": "Build exact source and execute pinned native client in disposable fixture", + "status": "completed", + "conclusion": "failure", + "number": 5 + }, + { + "name": "Upload native evidence including failed stages", + "status": "completed", + "conclusion": "success", + "number": 6 + }, + { + "name": "Post Run actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e", + "status": "completed", + "conclusion": "skipped", + "number": 11 + }, + { + "name": "Post Run actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", + "status": "completed", + "conclusion": "success", + "number": 12 + }, + { + "name": "Complete job", + "status": "completed", + "conclusion": "success", + "number": 13 + } + ] + }, + { + "id": 120994878222, + "name": "claude runtime (windows-amd64)", + "status": "completed", + "conclusion": "failure", + "run_id": 40135056328, + "logs_url": null, + "steps": [ + { + "name": "Set up job", + "status": "completed", + "conclusion": "success", + "number": 1 + }, + { + "name": "Run actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", + "status": "completed", + "conclusion": "success", + "number": 2 + }, + { + "name": "Run actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e", + "status": "completed", + "conclusion": "success", + "number": 3 + }, + { + "name": "Verify native matrix safety and evidence gates", + "status": "completed", + "conclusion": "success", + "number": 4 + }, + { + "name": "Build exact source and execute pinned native client in disposable fixture", + "status": "completed", + "conclusion": "failure", + "number": 5 + }, + { + "name": "Upload native evidence including failed stages", + "status": "completed", + "conclusion": "success", + "number": 6 + }, + { + "name": "Post Run actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e", + "status": "completed", + "conclusion": "skipped", + "number": 11 + }, + { + "name": "Post Run actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", + "status": "completed", + "conclusion": "success", + "number": 12 + }, + { + "name": "Complete job", + "status": "completed", + "conclusion": "success", + "number": 13 + } + ] + } + ], + "log_excerpts": [ + { + "job_id": 120994877989, + "source": "GitHub connector decoded job logs", + "lines": [ + "2026-10-24T22:06:02.7811610Z ref: 4a1634f4906453eac0b87eddbcc54246226dcfdd", + "2026-10-24T22:07:37.2982440Z --- PASS: TestAgentpluginsClaudeNativeCommandsLifecycle (7.96s)", + "2026-10-24T22:07:37.2984930Z --- PASS: TestAgentpluginsClaudeNativeLifecycle (9.97s)", + "2026-10-24T22:07:37.2996550Z agentplugins_claude_project_native_e2e_test.go:52: unchanged prepared update: mutated = true, want false", + "2026-10-24T22:07:37.2999280Z --- FAIL: TestAgentpluginsClaudeNativeProjectPreparedLifecycle (3.76s)", + "2026-10-24T22:07:37.3001530Z --- PASS: TestAgentpluginsClaudeNativeRuntimeLifecycle (10.01s)" + ] + }, + { + "job_id": 120994878222, + "source": "GitHub connector decoded job logs", + "lines": [ + "2026-10-24T22:01:52.6612372Z ref: 4a1634f4906453eac0b87eddbcc54246226dcfdd", + "2026-10-24T22:04:34.6024289Z --- PASS: TestAgentpluginsClaudeNativeCommandsLifecycle (12.54s)", + "2026-10-24T22:04:34.6026711Z --- PASS: TestAgentpluginsClaudeNativeLifecycle (15.22s)", + "2026-10-24T22:04:34.6029859Z agentplugins_claude_project_native_e2e_test.go:52: unchanged prepared update: mutated = true, want false", + "2026-10-24T22:04:34.6032374Z --- FAIL: TestAgentpluginsClaudeNativeProjectPreparedLifecycle (6.02s)", + "2026-10-24T22:04:34.6034391Z --- PASS: TestAgentpluginsClaudeNativeRuntimeLifecycle (18.16s)" + ] + } + ], + "qualification": "Both Claude jobs failed same unchanged_update mutation assertion; existing three user lifecycle tests passed; no run dispatched or retried" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/01-claude-version.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/01-claude-version.log new file mode 100644 index 000000000..870ddd4c2 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/01-claude-version.log @@ -0,0 +1 @@ +2.1.263 (Claude Code) diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-installer.json new file mode 100644 index 000000000..bec495e14 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-94fd59d8d973f13a7df814d0248f2c9a","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-94fd59d8d973f13a7df814d0248f2c9a","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"87059b36-2729-4d91-bbba-543a9a3c2691","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-4760574d1b66","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-repeated-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-repeated-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-repeated-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-repeated-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-repeated-installer.json new file mode 100644 index 000000000..cfe520b8b --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/add-repeated-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-ba7bfa01db1f88a328301b849e1638f5","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"cache"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"87059b36-2729-4d91-bbba-543a9a3c2691","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-4760574d1b66","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":false,"no_change":true}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.json new file mode 100644 index 000000000..2c51ee04b --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3367966848/project-A", + "deadline": false, + "elapsed_ms": 140, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.json new file mode 100644 index 000000000..f0224e68f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3367966848/project-B", + "deadline": false, + "elapsed_ms": 150, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.json new file mode 100644 index 000000000..c9f25f06b --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3367966848/project-A", + "deadline": false, + "elapsed_ms": 136, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.json new file mode 100644 index 000000000..abdc5e341 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/bin/claude", + "plugin", + "list", + "--json" + ], + "cwd": "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3367966848/project-B", + "deadline": false, + "elapsed_ms": 163, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/after-repeated-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/binding-after-add.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/binding-after-add.json new file mode 100644 index 000000000..c403eb221 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/binding-after-add.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_b3bccee22d1278f8439254f4", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "/Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3367966848/project-A/.claude/skills/uap-commands-fixture-4760574d1b66", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/claude-project-plugin-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/claude-project-plugin-evidence.json new file mode 100644 index 000000000..4d11e77b4 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/claude-project-plugin-evidence.json @@ -0,0 +1,121 @@ +{ + "arch": "arm64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-installer.json": "07b36551f923b028d96a54166902c72e929ea9a960f287e6b7dd9e9977ae0309", + "add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-repeated-installer.json": "22959cc99dbb6174ef5567cdac343ccdef05fa44d807993b9740942ccaf915c4", + "after-add-native-A.json": "b39af9a5a7b59f4f1ba8083d0ed83852ab2b48d27a70eeee1e3f01f0deb3f848", + "after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-add-native-B.json": "a45257a5196f296a05f5e601f873243fbd5fb4c682d8d2758a4365f10f381367", + "after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-repeated-add-native-A.json": "a2b8fc44102e5fd32077dca59f5e2020ec4c02cad975c084434995be1c7983e6", + "after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-repeated-add-native-B.json": "785efae85b9f4adf260afec63a9a425a54ce11eeff8f1f05d28974d4ff24a723", + "after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "binding-after-add.json": "5393b517857351c8d98767175f2e4d4c969606a5c0cd938ea3cd27de6b4eb97e", + "update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-unchanged-installer.json": "87e67551a11a77e55d67fb9be3e445b4ff0201696371777cccd719394b99254c" + }, + "client": "claude", + "client_sha256": "ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T21:13:58.128607Z", + "installer_sha256": "bc7c59dfaf35490d3154fe4fa185d54e084348290ee8935c640c122268a5d3b3", + "native_surface": "fresh bounded native project lists prove suppression; owned file lifecycle is prepared; no trust/model/MCP initialization", + "os": "darwin", + "project_contract": "untrusted_prepared_v1", + "scanner": { + "archive_sha256": "be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57", + "platform": "darwin-arm64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "75cece1488cdab0674b4817ea7a30ea95f7aad06", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "c302aa9bc97127b89504c9e6de049abfcf5ef580" + }, + "stages": { + "absent_repair": { + "status": "not_evaluated", + "reason": "not reached" + }, + "command_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "install": { + "status": "passed", + "reason": "production initial add installed owned files as Prepared+Installed; native project scan remains suppressed", + "artifacts": [ + "add-installer.json", + "binding-after-add.json" + ] + }, + "intact_repair": { + "status": "not_evaluated", + "reason": "not reached" + }, + "invocation_change_update": { + "status": "not_evaluated", + "reason": "not reached" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "native_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "native_suppression_boundary": { + "status": "not_evaluated", + "reason": "not reached" + }, + "prepared_command_projection": { + "status": "passed", + "reason": "owned projected files contain exact rendered ordinary command bytes; this is filesystem evidence" + }, + "prepared_explicit_command_withheld": { + "status": "passed", + "reason": "plan and owned projected files withhold the explicit-only command; no native loaded-skill claim" + }, + "project_scope_preservation": { + "status": "not_evaluated", + "reason": "not reached" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "remove": { + "status": "not_evaluated", + "reason": "not reached" + }, + "unchanged_add": { + "status": "passed", + "reason": "repeated add reobserved real suppression without file/state mutation" + }, + "unchanged_update": { + "status": "failed", + "reason": "Attempt started but did not complete its assertions; inspect the test failure and transcript", + "artifacts": [ + "update-unchanged-installer.json" + ] + } + }, + "started_utc": "2026-10-24T21:13:55.105418Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/native-tests.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/native-tests.log new file mode 100644 index 000000000..1ec4f16be --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/native-tests.log @@ -0,0 +1,16 @@ +=== RUN TestAgentpluginsClaudeNativeCommandsLifecycle + agentplugins_claude_commands_native_e2e_test.go:46: claude native evidence: /Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3537444963 +--- PASS: TestAgentpluginsClaudeNativeCommandsLifecycle (7.82s) +=== RUN TestAgentpluginsClaudeNativeLifecycle + agentplugins_claude_native_e2e_test.go:376: claude native evidence: /Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3023244141 +--- PASS: TestAgentpluginsClaudeNativeLifecycle (8.77s) +=== RUN TestAgentpluginsClaudeNativeProjectPreparedLifecycle + agentplugins_claude_project_native_e2e_test.go:16: claude native evidence: /Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-claude-native-evidence-3367966848 + agentplugins_claude_project_native_e2e_test.go:52: unchanged prepared update: mutated = true, want false + panic.go:694: prepared evidence stage invocation_change_update is "not_evaluated", want "passed" with reason + panic.go:694: native stages failed: unchanged_update: Attempt started but did not complete its assertions; inspect the test failure and transcript +--- FAIL: TestAgentpluginsClaudeNativeProjectPreparedLifecycle (3.17s) +=== RUN TestAgentpluginsClaudeNativeRuntimeLifecycle + agentplugins_claude_runtime_fixture_test.go:224: native evidence: /Users/runner/work/_temp/uap-native-hosted-4fkh5g8r/evidence/uap-native-evidence-849610538 +--- PASS: TestAgentpluginsClaudeNativeRuntimeLifecycle (9.59s) +FAIL diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/update-unchanged-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/update-unchanged-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/update-unchanged-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/update-unchanged-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/update-unchanged-installer.json new file mode 100644 index 000000000..d44d57550 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/darwin/update-unchanged-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-bf19b811b6d1fe508a8bf7122e1a68fa","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","dry_run":false,"targets":[{"target":"claude","selected":true,"status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-bf19b811b6d1fe508a8bf7122e1a68fa","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"87059b36-2729-4d91-bbba-543a9a3c2691","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-4760574d1b66","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"no_change":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/provenance.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/provenance.json new file mode 100644 index 000000000..45679c20c --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/provenance.json @@ -0,0 +1,203 @@ +{ + "run_id": 40131383052, + "commit": "75cece1488cdab0674b4817ea7a30ea95f7aad06", + "tree": "c302aa9bc97127b89504c9e6de049abfcf5ef580", + "method": "GitHub connector artifact download, independently verified exact ZIP byte length/SHA256 and every copied project raw member against original evidence artifact_sha256; ZIP text only, no vendor execution", + "qualification": "failed at unchanged_update; all subsequent prepared lifecycle stages not evaluated; native project discovery remains blocked; existing user required tests passed", + "platforms": [ + { + "platform": "darwin", + "artifact_id": 12575658957, + "artifact_zip_bytes": 382183, + "artifact_zip_sha256": "0359fc9c1dc6bf694a3fa1c89bbe585636a21c799eba5b0ea576d0bc6369ede4", + "artifact_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40131383052/artifacts/12575658957", + "members": { + "01-claude-version.log": { + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + "add-installer-stderr.log": { + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + "add-installer.json": { + "bytes": 4227, + "sha256": "07b36551f923b028d96a54166902c72e929ea9a960f287e6b7dd9e9977ae0309" + }, + "add-repeated-installer-stderr.log": { + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + "add-repeated-installer.json": { + "bytes": 2756, + "sha256": "22959cc99dbb6174ef5567cdac343ccdef05fa44d807993b9740942ccaf915c4" + }, + "after-add-native-A.json": { + "bytes": 402, + "sha256": "b39af9a5a7b59f4f1ba8083d0ed83852ab2b48d27a70eeee1e3f01f0deb3f848" + }, + "after-add-native-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-add-native-A.stdout.log": { + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + "after-add-native-B.json": { + "bytes": 403, + "sha256": "a45257a5196f296a05f5e601f873243fbd5fb4c682d8d2758a4365f10f381367" + }, + "after-add-native-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-add-native-B.stdout.log": { + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + "after-repeated-add-native-A.json": { + "bytes": 402, + "sha256": "a2b8fc44102e5fd32077dca59f5e2020ec4c02cad975c084434995be1c7983e6" + }, + "after-repeated-add-native-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-repeated-add-native-A.stdout.log": { + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + "after-repeated-add-native-B.json": { + "bytes": 403, + "sha256": "785efae85b9f4adf260afec63a9a425a54ce11eeff8f1f05d28974d4ff24a723" + }, + "after-repeated-add-native-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-repeated-add-native-B.stdout.log": { + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + "binding-after-add.json": { + "bytes": 380, + "sha256": "5393b517857351c8d98767175f2e4d4c969606a5c0cd938ea3cd27de6b4eb97e" + }, + "claude-project-plugin-evidence.json": { + "bytes": 5869, + "sha256": "76f592840795640b25cd1f3e1f80146107e4c10ae1fbebd0c10cd32600306f46" + }, + "native-tests.log": { + "bytes": 1543, + "sha256": "1299eef66c3b67d20b3382d18c35905a0199dbbbc6f70f7f51282a9a0c2b8fa8" + }, + "update-unchanged-installer-stderr.log": { + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + "update-unchanged-installer.json": { + "bytes": 2850, + "sha256": "87e67551a11a77e55d67fb9be3e445b4ff0201696371777cccd719394b99254c" + } + } + }, + { + "platform": "windows", + "artifact_id": 12575786665, + "artifact_zip_bytes": 396353, + "artifact_zip_sha256": "a944c0b0f27fe96c2eb32700830e6ec11b04ec3048cec3def9201f8c9479f60b", + "artifact_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40131383052/artifacts/12575786665", + "members": { + "01-claude-version.log": { + "bytes": 22, + "sha256": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185" + }, + "add-installer-stderr.log": { + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + "add-installer.json": { + "bytes": 4227, + "sha256": "771a4e14151e643fcfe04dc86a0f18322e7896075b2761f8e8d03471edcb6b13" + }, + "add-repeated-installer-stderr.log": { + "bytes": 156, + "sha256": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934" + }, + "add-repeated-installer.json": { + "bytes": 2756, + "sha256": "311517367fccd64249309271c2181947df09fcf212b24fcce95ca7639b5efa26" + }, + "after-add-native-A.json": { + "bytes": 388, + "sha256": "9b85d710633d1999859e16cf9a18caa874b1abdfed1d936507256efe250f63e9" + }, + "after-add-native-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-add-native-A.stdout.log": { + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + "after-add-native-B.json": { + "bytes": 389, + "sha256": "7e3bc30d01aa3656c142351c37c2d541e74f3ba28cbbb576d92997ce541e6355" + }, + "after-add-native-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-add-native-B.stdout.log": { + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + "after-repeated-add-native-A.json": { + "bytes": 388, + "sha256": "78512fd811c107b64d9bd703885133c1af587b43dbfbf074728276312b260de6" + }, + "after-repeated-add-native-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-repeated-add-native-A.stdout.log": { + "bytes": 419, + "sha256": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5" + }, + "after-repeated-add-native-B.json": { + "bytes": 389, + "sha256": "73c7e7c75753499c7d40e9f4dcb9a52ac9d2ad310b5f668ddcf9eafa25e4391c" + }, + "after-repeated-add-native-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "after-repeated-add-native-B.stdout.log": { + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + "binding-after-add.json": { + "bytes": 374, + "sha256": "abba1d1782c7125f3eac537ba29c73aac1c47ea7b4ef6b01e0561f3b9ddd9a2e" + }, + "claude-project-plugin-evidence.json": { + "bytes": 5873, + "sha256": "bcad25aac8981d1ee8f0f5142ca1451b6c727843bb9469571905582dfb9fab90" + }, + "native-tests.log": { + "bytes": 1479, + "sha256": "8f6c4685253d206815a9c60104f821c809a0be300c855ecf5575efc38019a434" + }, + "update-unchanged-installer-stderr.log": { + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + "update-unchanged-installer.json": { + "bytes": 2850, + "sha256": "d99b403497c03cdb88913c4fb53d11a09314a61b70c5090e6ff71d16b67f7742" + } + } + } + ] +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/01-claude-version.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/01-claude-version.log new file mode 100644 index 000000000..870ddd4c2 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/01-claude-version.log @@ -0,0 +1 @@ +2.1.263 (Claude Code) diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-installer.json new file mode 100644 index 000000000..7e2ffd59c --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-744dda1b5865f7c932c49f4cbd5defc4","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"claude","status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-744dda1b5865f7c932c49f4cbd5defc4","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"a2274231-75bb-4f28-b56f-0ba7a9840080","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d1621c20975b","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-repeated-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-repeated-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-repeated-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-repeated-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-repeated-installer.json new file mode 100644 index 000000000..2316575c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/add-repeated-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-ba14d6797ffe37a53296d552bb61d7b6","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"cache"},"next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"a2274231-75bb-4f28-b56f-0ba7a9840080","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d1621c20975b","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":false,"no_change":true}}} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.json new file mode 100644 index 000000000..792720576 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-rlup_566\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-rlup_566\\evidence\\uap-claude-native-evidence-830961986\\project-A", + "deadline": false, + "elapsed_ms": 250, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.json new file mode 100644 index 000000000..6557adfdf --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-rlup_566\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-rlup_566\\evidence\\uap-claude-native-evidence-830961986\\project-B", + "deadline": false, + "elapsed_ms": 243, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.json new file mode 100644 index 000000000..12e4cea57 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-rlup_566\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-rlup_566\\evidence\\uap-claude-native-evidence-830961986\\project-A", + "deadline": false, + "elapsed_ms": 247, + "error": "", + "exit": 0, + "expected_suppression": true, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.stdout.log new file mode 100644 index 000000000..e7587d59f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-A.stdout.log @@ -0,0 +1,12 @@ +[ + { + "id": "(suppressed)@skills-dir", + "version": "unknown", + "scope": "project", + "enabled": false, + "installPath": "", + "notes": [ + "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + ] + } +] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.json new file mode 100644 index 000000000..eeefe703f --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.json @@ -0,0 +1,16 @@ +{ + "argv": [ + "D:\\a\\_temp\\uap-native-hosted-rlup_566\\bin\\claude.exe", + "plugin", + "list", + "--json" + ], + "cwd": "D:\\a\\_temp\\uap-native-hosted-rlup_566\\evidence\\uap-claude-native-evidence-830961986\\project-B", + "deadline": false, + "elapsed_ms": 248, + "error": "", + "exit": 0, + "expected_suppression": false, + "stderr_limit": false, + "stdout_limit": false +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.stdout.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/after-repeated-add-native-B.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/binding-after-add.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/binding-after-add.json new file mode 100644 index 000000000..0a7d05ba4 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/binding-after-add.json @@ -0,0 +1,8 @@ +{ + "activation": "prepared", + "binding_id": "client_57c00da18aac7f299065afc8", + "evidence_kind": "owned_files_not_native_discovery", + "scope": "project", + "target_path": "D:\\a\\_temp\\uap-native-hosted-rlup_566\\evidence\\uap-claude-native-evidence-830961986\\project-A\\.claude\\skills\\uap-commands-fixture-d1621c20975b", + "verification": "installation_verified" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/claude-project-plugin-evidence.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/claude-project-plugin-evidence.json new file mode 100644 index 000000000..d38e13958 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/claude-project-plugin-evidence.json @@ -0,0 +1,121 @@ +{ + "arch": "amd64", + "artifact_sha256": { + "01-claude-version.log": "b223e2a39cb8534c8a0923ee0ec5050b0ed073905f830f89ef5b84a754e98185", + "add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-installer.json": "771a4e14151e643fcfe04dc86a0f18322e7896075b2761f8e8d03471edcb6b13", + "add-repeated-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "add-repeated-installer.json": "311517367fccd64249309271c2181947df09fcf212b24fcce95ca7639b5efa26", + "after-add-native-A.json": "9b85d710633d1999859e16cf9a18caa874b1abdfed1d936507256efe250f63e9", + "after-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-add-native-B.json": "7e3bc30d01aa3656c142351c37c2d541e74f3ba28cbbb576d92997ce541e6355", + "after-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "after-repeated-add-native-A.json": "78512fd811c107b64d9bd703885133c1af587b43dbfbf074728276312b260de6", + "after-repeated-add-native-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-A.stdout.log": "9446c13c1ee2bce54285bf413c503a78beffa1f7deb1fa166868e63ec4fa40b5", + "after-repeated-add-native-B.json": "73c7e7c75753499c7d40e9f4dcb9a52ac9d2ad310b5f668ddcf9eafa25e4391c", + "after-repeated-add-native-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "after-repeated-add-native-B.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "binding-after-add.json": "abba1d1782c7125f3eac537ba29c73aac1c47ea7b4ef6b01e0561f3b9ddd9a2e", + "update-unchanged-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-unchanged-installer.json": "d99b403497c03cdb88913c4fb53d11a09314a61b70c5090e6ff71d16b67f7742" + }, + "client": "claude", + "client_sha256": "0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03", + "client_version_measured": "2.1.263 (Claude Code)", + "finished_utc": "2026-10-24T21:14:58.6400134Z", + "installer_sha256": "b40474a495d276134785e9cc157d589811a7a6276a7a76598a8d8a232d023b92", + "native_surface": "fresh bounded native project lists prove suppression; owned file lifecycle is prepared; no trust/model/MCP initialization", + "os": "windows", + "project_contract": "untrusted_prepared_v1", + "scanner": { + "archive_sha256": "2f61f6a83a160afa3feed9ea1722b82d0d938ebff865a4e20d39b5f55270c911", + "binary_sha256": "9d486e5b66875384c12a147b8a19007beebe2691bc6d23a7867bf019b70be453", + "platform": "windows-amd64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "75cece1488cdab0674b4817ea7a30ea95f7aad06", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "c302aa9bc97127b89504c9e6de049abfcf5ef580" + }, + "stages": { + "absent_repair": { + "status": "not_evaluated", + "reason": "not reached" + }, + "command_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "explicit_invocation_enforced": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "install": { + "status": "passed", + "reason": "production initial add installed owned files as Prepared+Installed; native project scan remains suppressed", + "artifacts": [ + "add-installer.json", + "binding-after-add.json" + ] + }, + "intact_repair": { + "status": "not_evaluated", + "reason": "not reached" + }, + "invocation_change_update": { + "status": "not_evaluated", + "reason": "not reached" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "native_skill_discovery": { + "status": "blocked", + "reason": "project trust is not granted; filesystem projection does not prove native discovery" + }, + "native_suppression_boundary": { + "status": "not_evaluated", + "reason": "not reached" + }, + "prepared_command_projection": { + "status": "passed", + "reason": "owned projected files contain exact rendered ordinary command bytes; this is filesystem evidence" + }, + "prepared_explicit_command_withheld": { + "status": "passed", + "reason": "plan and owned projected files withhold the explicit-only command; no native loaded-skill claim" + }, + "project_scope_preservation": { + "status": "not_evaluated", + "reason": "not reached" + }, + "real_model_command_use": { + "status": "not_evaluated", + "reason": "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised" + }, + "remove": { + "status": "not_evaluated", + "reason": "not reached" + }, + "unchanged_add": { + "status": "passed", + "reason": "repeated add reobserved real suppression without file/state mutation" + }, + "unchanged_update": { + "status": "failed", + "reason": "Attempt started but did not complete its assertions; inspect the test failure and transcript", + "artifacts": [ + "update-unchanged-installer.json" + ] + } + }, + "started_utc": "2026-10-24T21:14:53.9220098Z" +} diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/native-tests.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/native-tests.log new file mode 100644 index 000000000..b7ce6a7e7 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/native-tests.log @@ -0,0 +1,16 @@ +=== RUN TestAgentpluginsClaudeNativeCommandsLifecycle + agentplugins_claude_commands_native_e2e_test.go:46: claude native evidence: D:\a\_temp\uap-native-hosted-rlup_566\evidence\uap-claude-native-evidence-2310002583 +--- PASS: TestAgentpluginsClaudeNativeCommandsLifecycle (10.71s) +=== RUN TestAgentpluginsClaudeNativeLifecycle + agentplugins_claude_native_e2e_test.go:376: claude native evidence: D:\a\_temp\uap-native-hosted-rlup_566\evidence\uap-claude-native-evidence-4058012141 +--- PASS: TestAgentpluginsClaudeNativeLifecycle (13.19s) +=== RUN TestAgentpluginsClaudeNativeProjectPreparedLifecycle + agentplugins_claude_project_native_e2e_test.go:16: claude native evidence: D:\a\_temp\uap-native-hosted-rlup_566\evidence\uap-claude-native-evidence-830961986 + agentplugins_claude_project_native_e2e_test.go:52: unchanged prepared update: mutated = true, want false + panic.go:694: prepared evidence stage absent_repair is "not_evaluated", want "passed" with reason + panic.go:694: native stages failed: unchanged_update: Attempt started but did not complete its assertions; inspect the test failure and transcript +--- FAIL: TestAgentpluginsClaudeNativeProjectPreparedLifecycle (4.96s) +=== RUN TestAgentpluginsClaudeNativeRuntimeLifecycle + agentplugins_claude_runtime_fixture_test.go:224: native evidence: D:\a\_temp\uap-native-hosted-rlup_566\evidence\uap-native-evidence-3080711710 +--- PASS: TestAgentpluginsClaudeNativeRuntimeLifecycle (14.29s) +FAIL diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/update-unchanged-installer-stderr.log b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/update-unchanged-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/update-unchanged-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/update-unchanged-installer.json b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/update-unchanged-installer.json new file mode 100644 index 000000000..2057d92fd --- /dev/null +++ b/docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/windows/update-unchanged-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-b7af639fc50f9fb3184f042b5530f085","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","dry_run":false,"targets":[{"target":"claude","selected":true,"status":"external_completed","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-b7af639fc50f9fb3184f042b5530f085","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"a2274231-75bb-4f28-b56f-0ba7a9840080","plan":{"client_id":"claude","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"active","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-d1621c20975b","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","start a new Claude Code session or run /reload-plugins"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification"]},"requires_confirmation":false,"mutated":true,"no_change":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/README.md b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/README.md new file mode 100644 index 000000000..337863093 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/README.md @@ -0,0 +1,23 @@ +# Codex project native lifecycle evidence + +This is an immutable record of the successful source-only macOS arm64 job for +Codex 0.153.4. See [provenance](provenance.json) for the exact source tree, +executable and downloadable artifact identities, and the +[independent interpretation](independent-review.md) for the observed boundaries. +The 28 project sub-artifacts were copied byte-for-byte after verification against +the original project summary; user-suite files with repeated names were excluded +by selecting the exact original project artifact prefix. + +Fresh native skills/list observations cover project A versus B after UAP add, +command-rule update, a missing owned skill, repair and removal. The passing test +also verifies exact delivered bytes and foreign user/cache/project preservation. +UAP remains prepared with installed objects; no attestation is promoted. + +Native config warnings explicitly state that untrusted project config is disabled. +Skills discovery still succeeds. This evidence does not establish project MCP +initialization, trust admission, model use, authentication, Windows/Linux results +or executable release qualification. + +The retained_data_action text in the raw remove result is preserved as observed. +Its missing project context was discovered from this run and is being corrected +separately; raw historical evidence must not be rewritten to hide that defect. diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/client-version-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/client-version-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/client-version.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/client-version.log new file mode 100644 index 000000000..485662c38 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/client-version.log @@ -0,0 +1 @@ +codex-cli 0.153.4 diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/codex-project-components-evidence.json b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/codex-project-components-evidence.json new file mode 100644 index 000000000..4c502082d --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/codex-project-components-evidence.json @@ -0,0 +1,86 @@ +{ + "acquisition": "local_directory", + "arch": "arm64", + "artifact_sha256": { + "client-version-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "client-version.log": "616dba4e87de2b1f07e61fe30ff4a5b3b075986c986c466ac5d244597eac0f36", + "project-add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "project-add-installer.json": "165c43bf626feecc6c90097b44be197a5f81d107d3a5915d594c47d811d1073d", + "project-remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-remove-installer.json": "5659ceb29de313ec7671af938a462ce73dae420fdb89c483a04c65213180aab9", + "project-repair-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "project-repair-installer.json": "ced2be3917951a64a127b779fa3f0ef95e07ac20e55a596d3d484a392de6c697", + "project-update-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "project-update-installer.json": "11256f6e61212c7fcc7132af6a71ac85bf6d31e7cb3b74e0201ca08ad39df3b4", + "skills-after-project-add-project-A-rpc.jsonl": "520ead946874bd3a1aab1c6c488aee88c162214b18583369d0329fc8c997d686", + "skills-after-project-add-project-A-stderr.log": "e95a63c66e9854f7353189aeecc9d37fc0b41d342b1cc56dc2b7f2578695a143", + "skills-after-project-add-project-B-rpc.jsonl": "de37d7ec76151f61503fb16e9ede0edcd2693e43edd6da03708bd53b385756e7", + "skills-after-project-add-project-B-stderr.log": "758b93fc17c3805597e5a60eb53bf64ecc3bc6d59f0efc0b932fe88ce4c416c0", + "skills-after-project-remove-project-B-rpc.jsonl": "90a21038ce093af8235ead7c0614fae733808f617a2e685c4ae67e21eadde4a7", + "skills-after-project-remove-project-B-stderr.log": "24b64dd9d796438f0f9728ba82a43ea3d789b7e869be9326167ee8c029a1b879", + "skills-after-project-remove-project-rpc.jsonl": "b8ab16ec2aaccb53b03787d5a1c5235d44e10f65a0fd0739ca7c596274a26cf9", + "skills-after-project-remove-project-stderr.log": "58ab0c90759cbfac889d063660576dcf27c6c382adc5e88e4b8f3adef71f9353", + "skills-after-project-repair-project-A-rpc.jsonl": "46e5c330b810fe51109a066da0d4cf876d26659656c02f29c62c99096ea26da2", + "skills-after-project-repair-project-A-stderr.log": "4af8beb44e8afa1d297f7bc3c71e4e246956c7a584ffa612d4e3a1d7c2417c64", + "skills-after-project-repair-project-B-rpc.jsonl": "bc812325c93aecaf06bb6c75c1e316893b0d408cffd3f75c866ca4fc09e1811d", + "skills-after-project-repair-project-B-stderr.log": "265bbf376257669f1f94fbb7834873480d3546f69c99350f3811b8888dfe9c8c", + "skills-after-project-update-project-A-rpc.jsonl": "45b1e321f94726809866e63f3ca72c53e27c23ca95cb1ff27008b807f54aeb0d", + "skills-after-project-update-project-A-stderr.log": "2cb773b2740a7c5e22bb5d4e69fc4eb731058900d6543f157585e37237d89423", + "skills-after-project-update-project-B-rpc.jsonl": "f3b989e21fa96b1cabac334fbf57e06f24b9f1a1c90d1f484dc60dfcecead1a3", + "skills-after-project-update-project-B-stderr.log": "9a8ca5bb30e4b769c244558a97b4c61fd404365e3b58699e226b8a2fef5469e8", + "skills-with-project-command-missing-rpc.jsonl": "28d4b101b7a255572e6ed373a3a8f9436ee38ebd07ec90896ae32af0065e5d1c", + "skills-with-project-command-missing-stderr.log": "52addf8d010ecb4d021faaa1760b3d9cc35df7a68cdd0b920e7cf0984fb21de6" + }, + "client": "codex", + "client_sha256": "b973d440acac501fd2594a43e7ca9ce41e0a65b9dfb28d0d7a7837c99e1261e3", + "client_version_measured": "codex-cli 0.153.4", + "finished_utc": "2026-10-23T19:56:18.077356Z", + "installer_sha256": "906be0c776457c34b7fa4b9beab8df77b9be3cd6015b9ef696932acc9c8b8e76", + "native_surface": "fresh app-server skills/list with exact selected-project repository paths; no thread/model/trust/MCP initialization", + "os": "darwin", + "scanner": { + "archive_sha256": "be8b263e2323074080d928ea7c2129458299a6d03f7a9f178dfc1aa8e6bc17ff", + "binary_sha256": "37342414aa7feadc09b8473d1bfafe95fd79ab99f284d37518c930fcb7986c57", + "platform": "darwin-arm64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "0d1319f947c7f4403691b3a60b1193baa0074ee9", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "4b8935ef80c3834f7145dd01289772a48379ba3f" + }, + "stages": { + "model_use": { + "status": "not_evaluated", + "reason": "discovery-only RPC; no thread or model request" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "skills-only fixture; no trust mutation or MCP initialization" + }, + "project_foreign_preservation": { + "status": "passed", + "reason": "user config/cache and both projects' foreign configuration and skills retain exact bytes" + }, + "project_install": { + "status": "passed", + "reason": "UAP project add remains prepared; genuine Codex discovers exact project-A repository skill paths and rendered bytes, with no fixture skills in project B" + }, + "project_missing_skill_repair": { + "status": "passed", + "reason": "fresh Codex observes the missing owned directory before repair and exact restored command skill after UAP repair" + }, + "project_remove": { + "status": "passed", + "reason": "fresh native Codex processes list no removed fixture skills in either project after UAP removal" + }, + "project_update": { + "status": "passed", + "reason": "UAP update replaces the command skill after invocation rules swap; fresh Codex discovery sees only the newly ordinary command with exact bytes" + } + }, + "started_utc": "2026-10-23T19:56:16.038838Z" +} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/independent-review.md b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/independent-review.md new file mode 100644 index 000000000..dc10233dd --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/independent-review.md @@ -0,0 +1,30 @@ +# Codex project native evidence — independent macOS interpretation + +Reviewer codex_claude, 2026-10-23. **PASS for the bounded native project-skills discovery and production file lifecycle actually exercised**, not full Codex project MCP/runtime qualification. + +## Immutable artifact and execution identity + +Root identified GitHub Actions run40011489662, job120612704190, artifact12527438580. Independently read the downloaded ZIP at `research/hosted-evidence/codex-darwin-40011489662/artifact.zip` (122 members, 505,497 total uncompressed bytes). Its measured SHA256 is `f51f7e9d15a4150d2a8ea8c26afd973b25ad6b1a69b9523363491c24d52e708b`. Independently verified all28 project-summary artifact digests against the exact `uap-native-evidence-689709854/` prefix; names shared with the separate user suites were not conflated. + +Runner/evidence source identity agrees: remote installer base/head `0d1319f947c7f4403691b3a60b1193baa0074ee9`, committed tree `4b8935ef80c3834f7145dd01289772a48379ba3f`, no source patch, installer SHA256 `906be0c776457c34b7fa4b9beab8df77b9be3cd6015b9ef696932acc9c8b8e76`. Measured genuine client is `codex-cli 0.153.4`, macOS arm64, binary SHA256 `b973d440acac501fd2594a43e7ca9ce41e0a65b9dfb28d0d7a7837c99e1261e3`, acquired from official rust-v0.153.4 aarch64-apple-darwin archive with recorded archive digest. This is a disposable hosted run; the runner explicitly claims no OS/process isolation and does not block network. The project subtest starts fresh app-server processes with explicit profiles and no thread/model/trust/MCP initialization. + +`native-tests.log` contains an actual `=== RUN TestAgentpluginsCodexNativeProjectComponentsLifecycle` followed by `--- PASS ... (2.25s)`, not SKIP. The summary's measured start/end and stage statuses correspond to the project records, separate from the two user/command suites also present in the ZIP. + +## Raw native observations + +Inspected each project RPC transcript, including outgoing initialize/skills-list requests and the received id2 `result.data` response. Each response selects the exact requested cwd and reports no skills-list errors. The transcript distinguishes repository scope and actual absolute project paths from user paths: + +- After production UAP project add, A's `.agents/skills` contains enabled repository `fixture-guide` and rendered `uap-commands-fixture-echo`; B contains neither fixture. A/B each retain their foreign repository marker and the separate user marker. +- After invocation-rule update, A lists `fixture-guide` and rendered `uap-commands-fixture-wizard`, with echo absent. B remains fixture-free. +- After removal of A's owned wizard directory, a fresh native response contains fixture-guide but no wizard. After UAP repair, fresh A lists wizard again, while B remains fixture-free. +- After UAP project removal, fresh A and B responses contain neither owned fixture and still contain their foreign repository/user markers. + +The independently inspected source subtest requires selected command/guide paths and rendered bytes, verifies A/B native discovery, exercises missing-directory repair, and checks foreign bytes at completion. The artifact's test PASS supports those assertions. Raw RPC establishes paths/scope/enabled/absence; byte equality and complete foreign config/cache preservation are asserted by the passing harness, rather than encoded as skill-body fields in skills/list itself. + +The production add/update/repair/remove JSON outputs each report success. Add/update/repair remain prepared with installation verification, rather than manufacturing active attested state. Remove reports data retained; this is retained receipt/file lifecycle evidence, not a native workload demonstrating plugin data read/write. + +## Concrete boundary retained + +Native configWarning messages explicitly state project-local config/hooks/exec policies are disabled until the project is trusted, while skills still load. No trust was granted to make this result pass. This successful untrusted repository skill discovery therefore does not establish that project MCP config is effective. The project fixture is skills-only, and summary marks native MCP initialization and model use not_evaluated. + +This evidence qualifies this macOS arm64 pinned-version project-skills discovery/add/update/missing-skill-repair/remove/foreign-preservation surface. It does not qualify Windows/Linux, every source transport, current-session MCP reload/removal behavior, account/authentication, model/tool use, project MCP initialization or all-client/release completion. Keep product project status conservative; root should append these bounded observations to aggregate evidence without pushing/canceling still-running leaf CI. diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-add-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-add-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-add-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-add-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-add-installer.json new file mode 100644 index 000000000..fdb67463a --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-b0e553086401dedea8b7834afbc4f8a2","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"codex","status":"external_completed","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-b0e553086401dedea8b7834afbc4f8a2","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"988e0c85-b7a5-4c0d-a079-d9ca1201f723","plan":{"client_id":"codex","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-040b8fdcd426","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-remove-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-remove-installer-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-remove-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-remove-installer.json new file mode 100644 index 000000000..7b62c409c --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-remove-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"remove","result":"success","data":{"operation_id":"op-a2eb81fb4b9e4d9bef21ecb089a1dec7","batch":true,"status":"data_retained","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","plugin_data_preserved":true,"data_retained":true,"retained_data":[{"data_receipt_id":"data_70de538638079e0046cb71f8","physical_backend_id":"uap-commands-fixture-040b8fdcd426","scope":"project","state":"owned"}],"retained_data_action":"run `agentplugins remove 988e0c85-b7a5-4c0d-a079-d9ca1201f723 --purge-data` to delete retained plugin data","dry_run":false,"targets":[{"target":"codex","status":"external_completed","output":{"operation_id":"op-a2eb81fb4b9e4d9bef21ecb089a1dec7","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","dry_run":false,"result":{"installation_id":"988e0c85-b7a5-4c0d-a079-d9ca1201f723","plugin":"uap-commands-fixture","client_id":"codex","requires_confirmation":false,"mutated":true,"deactivation":{"activation":"not_required","artifact_removal_allowed":true,"external_removal_complete":true},"affected_surfaces":["codex"],"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-repair-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-repair-installer-stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-repair-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-repair-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-repair-installer.json new file mode 100644 index 000000000..253348928 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-repair-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-8edef46d4b0b07af85aac7da52e70503","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","dry_run":false,"targets":[{"target":"codex","status":"external_completed","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-8edef46d4b0b07af85aac7da52e70503","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"988e0c85-b7a5-4c0d-a079-d9ca1201f723","plan":{"client_id":"codex","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-040b8fdcd426","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-update-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-update-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-update-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-update-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-update-installer.json new file mode 100644 index 000000000..56089e3b8 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/project-update-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-211f6167aff5981e064488dba53ea614","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","dry_run":false,"targets":[{"target":"codex","selected":true,"status":"external_completed","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-211f6167aff5981e064488dba53ea614","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"988e0c85-b7a5-4c0d-a079-d9ca1201f723","plan":{"client_id":"codex","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-040b8fdcd426","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/provenance.json b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/provenance.json new file mode 100644 index 000000000..d7e483a0f --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/provenance.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "claim": "Native project skills discovery and UAP file lifecycle only", + "workflow_run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662", + "workflow_job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662/job/120612704190", + "artifact_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662/artifacts/12527438580", + "artifact_zip_sha256": "f51f7e9d15a4150d2a8ea8c26afd973b25ad6b1a69b9523363491c24d52e708b", + "artifact_original_prefix": "uap-native-evidence-689709854/", + "preserved_project_artifact_count": 28, + "source_commit": "0d1319f947c7f4403691b3a60b1193baa0074ee9", + "source_tree": "4b8935ef80c3834f7145dd01289772a48379ba3f", + "client_asset": { + "source": "https://github.com/openai/codex/releases/tag/rust-v0.153.4", + "version": "rust-v0.153.4", + "archive": "codex-aarch64-apple-darwin.tar.gz", + "archive_integrity": "sha256:8cf911ea676523bfb2121ec561848d2aba564890ad536db4d8a3353f2b9850b1", + "binary_sha256": "b973d440acac501fd2594a43e7ca9ce41e0a65b9dfb28d0d7a7837c99e1261e3" + }, + "installer_sha256": "906be0c776457c34b7fa4b9beab8df77b9be3cd6015b9ef696932acc9c8b8e76", + "project_test": "TestAgentpluginsCodexNativeProjectComponentsLifecycle", + "project_test_result": "PASS", + "project_test_seconds": 2.25, + "required_tests": [ + "TestAgentpluginsCodexNativeCommandsLifecycle", + "TestAgentpluginsCodexNativeLifecycle", + "TestAgentpluginsCodexNativeProjectComponentsLifecycle" + ], + "passed_tests": [ + "TestAgentpluginsCodexNativeCommandsLifecycle", + "TestAgentpluginsCodexNativeLifecycle", + "TestAgentpluginsCodexNativeProjectComponentsLifecycle" + ], + "skipped_tests": [], + "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", + "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof", + "not_qualified": [ + "project MCP initialization", + "effective project trust or policy", + "model or tool workload", + "authentication", + "Windows or Linux native discovery", + "release qualification" + ] +} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-A-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-A-rpc.jsonl new file mode 100644 index 000000000..27413765e --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-A-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785376752}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785376752}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/fixture-guide/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"uap-commands-fixture-echo","description":"Reply with a fixed token and repeat the text after the command.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/uap-commands-fixture-echo/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-A-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-A-stderr.log new file mode 100644 index 000000000..f76a3f378 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-A-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:16.744460Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-B-rpc.jsonl new file mode 100644 index 000000000..90a40c2c4 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785376794}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785376794}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-B-stderr.log new file mode 100644 index 000000000..aaa567b63 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-add-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:16.793544Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-B-rpc.jsonl new file mode 100644 index 000000000..9fcea1b9a --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785378062}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785378062}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-B-stderr.log new file mode 100644 index 000000000..a9d353dfb --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:18.061051Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-rpc.jsonl new file mode 100644 index 000000000..93a910ae3 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785378018}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785378018}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-stderr.log new file mode 100644 index 000000000..c840ad0cb --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-remove-project-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:18.016803Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-A-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-A-rpc.jsonl new file mode 100644 index 000000000..df7e2c0e4 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-A-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785377792}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785377792}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/fixture-guide/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"uap-commands-fixture-wizard","description":"Run the fixed setup check only when the user asks for it.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/uap-commands-fixture-wizard/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-A-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-A-stderr.log new file mode 100644 index 000000000..253555a07 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-A-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:17.791248Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-B-rpc.jsonl new file mode 100644 index 000000000..b0ab9f8bf --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785377847}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785377847}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-B-stderr.log new file mode 100644 index 000000000..5cb60db4b --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-repair-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:17.845480Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-A-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-A-rpc.jsonl new file mode 100644 index 000000000..9fb8fae48 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-A-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785377334}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785377334}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/fixture-guide/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"uap-commands-fixture-wizard","description":"Run the fixed setup check only when the user asks for it.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/uap-commands-fixture-wizard/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-A-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-A-stderr.log new file mode 100644 index 000000000..cc148aade --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-A-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:17.332637Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-B-rpc.jsonl new file mode 100644 index 000000000..8cc11b04c --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785377376}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785377376}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-B-stderr.log new file mode 100644 index 000000000..316f35f7f --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-after-project-update-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:17.374882Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project-B as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-with-project-command-missing-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-with-project-command-missing-rpc.jsonl new file mode 100644 index 000000000..ef33983eb --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-with-project-command-missing-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Mac OS 15.7.9; arm64) dumb (uap_project_fixture; 1.0.0)","codexHome":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex","platformFamily":"unix","platformOs":"macos"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex\n To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml.\n","details":null},"emittedAtMs":1792785377425}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"sjc22-bt143-025cf1dd-cb8e-400a-b725-48f62ebe572f-827C257D950E.local","installationId":"efc06e3e-4546-4576-a582-a64fb24c25b3","environmentId":null},"emittedAtMs":1792785377425}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/fixture-guide/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.agents/skills/foreign-codex-project/SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.agents/skills/foreign-codex-project/SKILL.md","scope":"user","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/assets/imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/imagegen/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/assets/openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/openai-docs/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/assets/plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/plugin-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/review-agent/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/assets/skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-creator/SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer-small.svg","iconLarge":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/assets/skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"/Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/skills/.system/skill-installer/SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-with-project-command-missing-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-with-project-command-missing-stderr.log new file mode 100644 index 000000000..849cadd66 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/skills-with-project-command-missing-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T19:56:17.423042Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project/.codex + To load project-local config, hooks, and exec policies, add /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/project as a trusted project in /Users/runner/work/_temp/uap-native-hosted-_ocozw_w/evidence/uap-native-evidence-689709854/home/.codex/config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/README.md b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/README.md new file mode 100644 index 000000000..1ad8c26a1 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/README.md @@ -0,0 +1,27 @@ +# Codex Windows project native lifecycle evidence + +The genuine pinned Codex 0.153.4 Windows amd64 source-only project subtest passed +in 3.94 seconds. The runner required and passed all three selected Codex tests +with no skipped test. See [provenance](provenance.json) for exact source/client/ +installer identities and run/job/artifact links. + +All 28 declared project sub-artifact hashes were verified and the original +bytes preserved. Independent inspection of all nine fresh skills/list replies +matched the requested cwd and exact repository skill paths, enabled flags and +absence/presence through add, invocation-rule update, missing-skill repair and +remove. Project B remained free of the installed fixture. The foreign repository +marker was present in every response. [Checked observations](checked-discovery-observations.json) +record these assertions without replacing the raw transcripts. A second reviewer +independently verified the archive, all 28 hashes and exact responses; see the +[independent evidence review](independent-review.md). + +This Windows response did not list the separately seeded user .agents skill, +unlike the macOS artifact. Its native discovery/precedence is therefore not +qualified here. The passing harness separately checked exact preservation of +foreign user configuration, cache and skill bytes and project A/B files. + +Project configuration remained untrusted and native warnings were retained. +UAP remains prepared plus installed. This result does not establish project +MCP initialization, effective trust/policy, authentication, model/tool use or +a release. Historical retained-data follow-up text is copied unchanged; its +missing project scope is addressed by a separate reviewed CLI correction. diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/checked-discovery-observations.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/checked-discovery-observations.json new file mode 100644 index 000000000..6a3ae9080 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/checked-discovery-observations.json @@ -0,0 +1,94 @@ +[ + { + "artifact": "skills-after-project-add-project-A-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project", + "owned_skill_names": [ + "fixture-guide", + "uap-commands-fixture-echo" + ], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-add-project-B-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B", + "owned_skill_names": [], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-remove-project-B-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B", + "owned_skill_names": [], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-remove-project-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project", + "owned_skill_names": [], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-repair-project-A-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project", + "owned_skill_names": [ + "fixture-guide", + "uap-commands-fixture-wizard" + ], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-repair-project-B-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B", + "owned_skill_names": [], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-update-project-A-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project", + "owned_skill_names": [ + "fixture-guide", + "uap-commands-fixture-wizard" + ], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-after-project-update-project-B-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B", + "owned_skill_names": [], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + }, + { + "artifact": "skills-with-project-command-missing-rpc.jsonl", + "cwd": "D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project", + "owned_skill_names": [ + "fixture-guide" + ], + "foreign_repo_skill_present": true, + "user_skill_listed": false, + "native_errors": [], + "config_warning_count": 1 + } +] diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/client-version-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/client-version-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/client-version.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/client-version.log new file mode 100644 index 000000000..485662c38 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/client-version.log @@ -0,0 +1 @@ +codex-cli 0.153.4 diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/codex-project-components-evidence.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/codex-project-components-evidence.json new file mode 100644 index 000000000..b0994aa50 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/codex-project-components-evidence.json @@ -0,0 +1,86 @@ +{ + "acquisition": "local_directory", + "arch": "amd64", + "artifact_sha256": { + "client-version-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "client-version.log": "616dba4e87de2b1f07e61fe30ff4a5b3b075986c986c466ac5d244597eac0f36", + "project-add-installer-stderr.log": "c8a5af8ae5dd4c9d5dcb81109bff51448d8d744e9fd372a7e20b1c473ffce934", + "project-add-installer.json": "5338259e8dfbfc0828184add1086faef0c33125d6dbc625c140983dc3933142b", + "project-remove-installer-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-remove-installer.json": "5b6f2a0a6782090cd4d48c3328ca26d886ff0dc1755a483d3ce911fec79d64f9", + "project-repair-installer-stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "project-repair-installer.json": "d99d1067efe1a78debd34cc3b3722b5e497886fb1bb3620760ab02ecb086825f", + "project-update-installer-stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "project-update-installer.json": "063b4e9a6433b38e645122a4d3346855cdf7feb60284c6b472c66a75f19d7669", + "skills-after-project-add-project-A-rpc.jsonl": "eba0d1614e66e8d4a94f47999801fef80b5045f080a591b311a8e1084671813d", + "skills-after-project-add-project-A-stderr.log": "eb22bf6c481b4721a0fbec2d8f0e44a4a9892c65a13bb074b02cb1a6f2d39a90", + "skills-after-project-add-project-B-rpc.jsonl": "635fc0a26f171ec93eff732ca16ecffe9c6afb52d855fca4473f2b9413ccff51", + "skills-after-project-add-project-B-stderr.log": "e0c2a92bcd88fb3abfab0107f6f0fba303aacc279196a175b645c9f15f1d2db7", + "skills-after-project-remove-project-B-rpc.jsonl": "0f3afdadd46dee6ac888ed3c2778f7acab3c6a6ee9d7d3728cbf437dab13c856", + "skills-after-project-remove-project-B-stderr.log": "4918a33910da19c3bb2e09c6a06d53cf6b0e61efecd8790235b7daabe07ce1f4", + "skills-after-project-remove-project-rpc.jsonl": "b56e196c42779d5c968258ee0d96e82d34d816399cff002c88157866d3c780f9", + "skills-after-project-remove-project-stderr.log": "0ebb5cd2d734b49decdbba9cbd66feaa94f8d09c74ff0ebe3c4b2712170d0b21", + "skills-after-project-repair-project-A-rpc.jsonl": "d6fa181b938a7b35d2dbe5820a081cea797c2b27af4f190867fc323ed68947ec", + "skills-after-project-repair-project-A-stderr.log": "4ae36a540a22cfa7723c26de21bde0708563ce7e2844e2641a153bf25fc148b0", + "skills-after-project-repair-project-B-rpc.jsonl": "3c0fda86c90a50a9b55a6776fbb4bc932327c0b8662fcb003659124b7e5df7c6", + "skills-after-project-repair-project-B-stderr.log": "13d9b7da79f4f861c9f96e244b7cfbd2fec5160f6d391bf33b82712e19b16b6f", + "skills-after-project-update-project-A-rpc.jsonl": "40a3ce2e319392e04e6f6e99469e830e353269e893cd0b3f9f1533a79557cb51", + "skills-after-project-update-project-A-stderr.log": "1c7cdb7421a727b60518f82001e7710f3ff104a8abee5cf8a66621a7b17e7fc0", + "skills-after-project-update-project-B-rpc.jsonl": "b2b49095756ca4545951fbe7827da4fc61e755b11fc2aef8563169775f7d0485", + "skills-after-project-update-project-B-stderr.log": "02afdc26e0b23985cde0806412b2507e6e94f5a9944d8bec39107bde69239c16", + "skills-with-project-command-missing-rpc.jsonl": "9e126beb67593258cb5ca99e871c3650b537106b32824fa065a4b9b968e20cce", + "skills-with-project-command-missing-stderr.log": "c51b08d7843868931bad297661b7dce59e59516db06a17564ede6576c43f2a12" + }, + "client": "codex", + "client_sha256": "444a3f0008050605cae73cd9b7a2dcac61294062dfaab56dd20430fd6498518b", + "client_version_measured": "codex-cli 0.153.4", + "finished_utc": "2026-10-23T20:25:04.2686768Z", + "installer_sha256": "0cc170ea89ffb92aec3e75f9a097f79f5e7476492b30e4351887945489ec452f", + "native_surface": "fresh app-server skills/list with exact selected-project repository paths; no thread/model/trust/MCP initialization", + "os": "windows", + "scanner": { + "archive_sha256": "2f61f6a83a160afa3feed9ea1722b82d0d938ebff865a4e20d39b5f55270c911", + "binary_sha256": "9d486e5b66875384c12a147b8a19007beebe2691bc6d23a7867bf019b70be453", + "platform": "windows-amd64", + "route": "prepopulated supported scanner cache; ordinary CLI security assessment", + "source_tag": "v0.1.3", + "version": "0.1.3" + }, + "source_identity": { + "installer_base_commit": "0d1319f947c7f4403691b3a60b1193baa0074ee9", + "installer_patch_sha256": [], + "installer_source_state": "committed", + "installer_tree": "4b8935ef80c3834f7145dd01289772a48379ba3f" + }, + "stages": { + "model_use": { + "status": "not_evaluated", + "reason": "discovery-only RPC; no thread or model request" + }, + "native_mcp_initialization": { + "status": "not_evaluated", + "reason": "skills-only fixture; no trust mutation or MCP initialization" + }, + "project_foreign_preservation": { + "status": "passed", + "reason": "user config/cache and both projects' foreign configuration and skills retain exact bytes" + }, + "project_install": { + "status": "passed", + "reason": "UAP project add remains prepared; genuine Codex discovers exact project-A repository skill paths and rendered bytes, with no fixture skills in project B" + }, + "project_missing_skill_repair": { + "status": "passed", + "reason": "fresh Codex observes the missing owned directory before repair and exact restored command skill after UAP repair" + }, + "project_remove": { + "status": "passed", + "reason": "fresh native Codex processes list no removed fixture skills in either project after UAP removal" + }, + "project_update": { + "status": "passed", + "reason": "UAP update replaces the command skill after invocation rules swap; fresh Codex discovery sees only the newly ordinary command with exact bytes" + } + }, + "started_utc": "2026-10-23T20:25:00.6082889Z" +} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/independent-review.md b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/independent-review.md new file mode 100644 index 000000000..ce956b54d --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/independent-review.md @@ -0,0 +1,15 @@ +# Codex Windows native project evidence — independent interpretation + +Reviewer codex_claude, 2026-10-23. **PASS for the pinned Windows project-skills discovery/file lifecycle actually exercised.** This does not qualify project MCP/runtime/trust or user-skill discovery/precedence. + +Independently read the actual downloaded ZIP at `research/hosted-evidence/codex-windows-40011489662/artifact.zip`:122 members,502,622 uncompressed bytes, measured SHA256 `4251845ca9c73144c4a8d72d66ebad8e2fa224160b1567e7a48c122dd2c82406`, agreeing with root's provenance. Independently verified all28 recorded project artifact hashes within the exact `uap-native-evidence-1787987250/` prefix; similarly named user-suite logs were not conflated. The source is committed remote `0d1319f947c7f4403691b3a60b1193baa0074ee9`, tree `4b8935ef80c3834f7145dd01289772a48379ba3f`, no patch. Genuine measured Codex0.153.4 Windows amd64 binary SHA256 is `444a3f0008050605cae73cd9b7a2dcac61294062dfaab56dd20430fd6498518b`. + +The actual native-tests.log contains all three required Codex tests as RUN/PASS, with no SKIP: commands6.97s, user lifecycle12.21s and new project component lifecycle3.94s. Root's provenance identifies run40011489662/job120612704583/artifact12529611866. The hosted runner explicitly claims fresh profiles/explicit environments on a disposable machine, without OS/process isolation or blocked network; these limits remain visible. + +Inspected all nine raw project skills/list RPC transcripts. Every selected response has zero native list errors. Independently checked16 repository skill paths against each response's exact selected cwd plus `.agents/skills//SKILL.md`, normalizing only slash spelling for the Windows comparison. After add, A lists enabled repo fixture-guide and rendered echo; B lists no owned fixture. After update, A lists guide and wizard with echo gone. An independent fresh list observes wizard missing after deletion; repair restores wizard at the exact A path. After removal neither A nor B lists either owned fixture. Every A/B list retains its foreign repo marker. + +The user marker is **not listed on Windows**, unlike the macOS result. Do not infer user-skill discovery or precedence from this run. Foreign user config/cache and data bytes are separately checked by the passing fixture's file assertions; that proves preservation of those bytes, not native discovery of their contents. The raw responses match root's checked-discovery-observations summary with this important distinction retained. + +Every transcript includes the native warning that project config/hooks/exec policy are disabled until trusted while skills still load. No trust is granted by this test. Project UAP add/update/repair remain prepared plus installation-verified, and exact remove retains data. The passing test requires exact rendered skill bytes and foreign preservation; RPC itself establishes discovery/path/scope/enabled/absence rather than returning skill body bytes. + +Summary correctly marks model use and project MCP initialization not_evaluated. This Windows result does not establish loaded project MCP config, native account/authentication/model/tool workload, ordinary session policy, user-skill precedence, Linux behavior, all source transports, all clients or release qualification. No native process was executed during this independent artifact interpretation. diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-add-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-add-installer-stderr.log new file mode 100644 index 000000000..aa7b3c632 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-add-installer-stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package through the public installer facade... diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-add-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-add-installer.json new file mode 100644 index 000000000..62f1b04f9 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-add-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-328efee2f4fc5bd2c3bc6d197209453f","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"dry_run":false,"targets":[{"target":"codex","status":"external_completed","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-328efee2f4fc5bd2c3bc6d197209453f","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","security":{"schema_version":1,"subject":{"tree_digest":"sha256:b80d65f2b661df36b017fc90aa63cf4c83a714af2d2e32f20739f2e7f8d97368","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2"},"scanner":{"id":"lintai","version":"0.1.3"},"policy":{"id":"agent-plugin-install","version":2,"digest":"sha256:9cf869e299d847d7078aeca01f5a182fcdb0144bbf513c83290459991c79e037"},"outcome":"no_blocking_findings","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":4,"report_digest":"sha256:30e03793389957f60d615809a255a381c3b4749199ec9a4321f1e5db2a605356","evidence_source":"local_scan"},"next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"12c3d7cf-eb1f-46be-9ac3-a75395df5247","plan":{"client_id":"codex","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-f835af03f8c2","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-echo","support":"projected","source":"command","source_name":"echo"}],"user_actions":["verify the plugin's authentication requirements before using it","Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/wizard.md","item":"wizard","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-remove-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-remove-installer-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-remove-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-remove-installer.json new file mode 100644 index 000000000..3527008dd --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-remove-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"remove","result":"success","data":{"operation_id":"op-f815d115f940299f3dec99169b5014c9","batch":true,"status":"data_retained","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","plugin_data_preserved":true,"data_retained":true,"retained_data":[{"data_receipt_id":"data_1f92a459ab09d441fa853657","physical_backend_id":"uap-commands-fixture-f835af03f8c2","scope":"project","state":"owned"}],"retained_data_action":"run `agentplugins remove 12c3d7cf-eb1f-46be-9ac3-a75395df5247 --purge-data` to delete retained plugin data","dry_run":false,"targets":[{"target":"codex","status":"external_completed","output":{"operation_id":"op-f815d115f940299f3dec99169b5014c9","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","dry_run":false,"result":{"installation_id":"12c3d7cf-eb1f-46be-9ac3-a75395df5247","plugin":"uap-commands-fixture","client_id":"codex","requires_confirmation":false,"mutated":true,"deactivation":{"activation":"not_required","artifact_removal_allowed":true,"external_removal_complete":true},"affected_surfaces":["codex"],"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-repair-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-repair-installer-stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-repair-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-repair-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-repair-installer.json new file mode 100644 index 000000000..23a01f07d --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-repair-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-941f8d78825dd7f580cb562dbaa8e727","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","dry_run":false,"targets":[{"target":"codex","status":"external_completed","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-941f8d78825dd7f580cb562dbaa8e727","plugin":"uap-commands-fixture","version":"1.0.0","source":"local","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"12c3d7cf-eb1f-46be-9ac3-a75395df5247","plan":{"client_id":"codex","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-f835af03f8c2","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-update-installer-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-update-installer-stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-update-installer-stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-update-installer.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-update-installer.json new file mode 100644 index 000000000..97fc12d53 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/project-update-installer.json @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-3cbd54bac61697622b85623cd15840dd","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","dry_run":false,"targets":[{"target":"codex","selected":true,"status":"external_completed","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-3cbd54bac61697622b85623cd15840dd","plugin":"uap-commands-fixture","version":"1.0.0","source":"direct local source","tree_digest":"sha256:f9dd24e0c239c92cff9e3dafafccc807500496eba82acb4693dec6e151a1c6d2","manifest_digest":"sha256:bab4987eb53807dc5d89b290e285d55db7cceccd69c0b32c25bf9506db04f7e2","next_action":"Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"12c3d7cf-eb1f-46be-9ac3-a75395df5247","plan":{"client_id":"codex","scope":"project","status":"ready","package_mode":"compatibility_projection","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-commands-fixture-f835af03f8c2","components":[{"kind":"skill","name":"fixture-guide","support":"projected"},{"kind":"skill","name":"uap-commands-fixture-wizard","support":"projected","source":"command","source_name":"wizard"}],"user_actions":["verify the plugin's authentication requirements before using it","Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"],"warnings":["explicit_invocation_not_enforceable","authentication_not_catalog_verified"],"diagnostics":[{"severity":"warning","boundary":"command","code":"explicit_invocation_not_enforceable","path":"io.github.777genius.agentplugins/commands/echo.md","item":"echo","message":"this client cannot keep the command out of automatic invocation, so it is not delivered"}]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/provenance.json b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/provenance.json new file mode 100644 index 000000000..1e114a1e0 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/provenance.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "claim": "Native Windows project skills discovery and UAP file lifecycle only", + "workflow_run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662", + "workflow_job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662/job/120612704583", + "artifact_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662/artifacts/12529611866", + "artifact_zip_sha256": "4251845ca9c73144c4a8d72d66ebad8e2fa224160b1567e7a48c122dd2c82406", + "artifact_original_prefix": "uap-native-evidence-1787987250/", + "preserved_project_artifact_count": 28, + "source_commit": "0d1319f947c7f4403691b3a60b1193baa0074ee9", + "source_tree": "4b8935ef80c3834f7145dd01289772a48379ba3f", + "client_asset": { + "source": "https://github.com/openai/codex/releases/tag/rust-v0.153.4", + "version": "rust-v0.153.4", + "archive": "codex-x86_64-pc-windows-msvc.exe.zip", + "archive_integrity": "sha256:c016b0e6968b78586919c720d2685a03712f6d5f11bcd9d6f92c91eb8c41ba16", + "binary_sha256": "444a3f0008050605cae73cd9b7a2dcac61294062dfaab56dd20430fd6498518b" + }, + "installer_sha256": "0cc170ea89ffb92aec3e75f9a097f79f5e7476492b30e4351887945489ec452f", + "project_test": "TestAgentpluginsCodexNativeProjectComponentsLifecycle", + "project_test_result": "PASS", + "project_test_seconds": 3.94, + "required_tests": [ + "TestAgentpluginsCodexNativeCommandsLifecycle", + "TestAgentpluginsCodexNativeLifecycle", + "TestAgentpluginsCodexNativeProjectComponentsLifecycle" + ], + "passed_tests": [ + "TestAgentpluginsCodexNativeCommandsLifecycle", + "TestAgentpluginsCodexNativeLifecycle", + "TestAgentpluginsCodexNativeProjectComponentsLifecycle" + ], + "skipped_tests": [], + "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", + "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof", + "not_qualified": [ + "native Windows user-marker discovery/precedence", + "project MCP initialization", + "effective project trust or policy", + "model or tool workload", + "authentication", + "release qualification" + ] +} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-A-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-A-rpc.jsonl new file mode 100644 index 000000000..4773b1262 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-A-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787101825}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787101825}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\fixture-guide\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"uap-commands-fixture-echo","description":"Reply with a fixed token and repeat the text after the command.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\uap-commands-fixture-echo\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-A-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-A-stderr.log new file mode 100644 index 000000000..fdb493a25 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-A-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:01.802065Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-B-rpc.jsonl new file mode 100644 index 000000000..afd26aef2 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-b as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787101915}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787101915}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-B-stderr.log new file mode 100644 index 000000000..f5b09ab6a --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-add-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:01.910616Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-B\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-b as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-B-rpc.jsonl new file mode 100644 index 000000000..7d441bb0e --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-b as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787104232}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787104232}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-B-stderr.log new file mode 100644 index 000000000..14fa3d33b --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:04.227230Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-B\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-b as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-rpc.jsonl new file mode 100644 index 000000000..978557a00 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787104140}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787104140}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-stderr.log new file mode 100644 index 000000000..c95c32bc2 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-remove-project-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:04.136153Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-A-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-A-rpc.jsonl new file mode 100644 index 000000000..f2abc8725 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-A-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787103757}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787103757}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\fixture-guide\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"uap-commands-fixture-wizard","description":"Run the fixed setup check only when the user asks for it.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\uap-commands-fixture-wizard\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-A-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-A-stderr.log new file mode 100644 index 000000000..45022e382 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-A-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:03.753274Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-B-rpc.jsonl new file mode 100644 index 000000000..ee3bee821 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-b as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787103849}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787103849}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-B-stderr.log new file mode 100644 index 000000000..e45c54653 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-repair-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:03.843922Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-B\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-b as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-A-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-A-rpc.jsonl new file mode 100644 index 000000000..ee85d6332 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-A-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787102766}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787102766}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\fixture-guide\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"uap-commands-fixture-wizard","description":"Run the fixed setup check only when the user asks for it.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\uap-commands-fixture-wizard\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-A-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-A-stderr.log new file mode 100644 index 000000000..f267456d9 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-A-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:02.761728Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-B-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-B-rpc.jsonl new file mode 100644 index 000000000..60cbe012e --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-B-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-b as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787102851}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787102851}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B","skills":[{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project-B\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-B-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-B-stderr.log new file mode 100644 index 000000000..d2af2413f --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-after-project-update-project-B-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:02.847270Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-B\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project-b as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-with-project-command-missing-rpc.jsonl b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-with-project-command-missing-rpc.jsonl new file mode 100644 index 000000000..e8aad8ced --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-with-project-command-missing-rpc.jsonl @@ -0,0 +1,6 @@ +{"direction":"send","message":{"id":1,"method":"initialize","params":{"capabilities":{"experimentalApi":true},"clientInfo":{"name":"uap_project_fixture","version":"1.0.0"}}}} +{"direction":"receive","message":{"id":1,"result":{"userAgent":"uap_project_fixture/0.153.4 (Windows 10.0.26100; x86_64) dumb (uap_project_fixture; 1.0.0)","codexHome":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex","platformFamily":"windows","platformOs":"windows"}}} +{"direction":"send","message":{"id":2,"method":"skills/list","params":{"cwds":["D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project"],"forceReload":true}}} +{"direction":"receive","message":{"method":"configWarning","params":{"summary":"Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.\n 1. D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.codex\n To load project-local config, hooks, and exec policies, add d:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project as a trusted project in D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\config.toml.\n","details":null},"emittedAtMs":1792787102962}} +{"direction":"receive","message":{"method":"remoteControl/status/changed","params":{"status":"disabled","serverName":"runnervmdaln5","installationId":"305fbe99-f2e8-47ad-b318-17da04172ed6","environmentId":null},"emittedAtMs":1792787102962}} +{"direction":"receive","message":{"id":2,"result":{"data":[{"cwd":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project","skills":[{"name":"fixture-guide","description":"Describes the portable command lifecycle fixture.","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\fixture-guide\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"foreign-codex-project","description":"Inert foreign fixture","path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\project\\.agents\\skills\\foreign-codex-project\\SKILL.md","scope":"repo","enabled":true,"pluginId":null},{"name":"imagegen","description":"Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.","interface":{"displayName":"Image Gen","shortDescription":"Generate or edit images for websites, games, and more","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\assets\\imagegen.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $imagegen to make or edit an image for this project."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\imagegen\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"openai-docs","description":"Use for Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, customization, automations, and self-knowledge—including 'you,' 'your,' 'this app,' or 'this coding agent' when they refer to Codex—and for OpenAI APIs/products and ChatGPT Work. Also use for model choice/migration, prompting, SDKs, Responses, Realtime, agents, evals, and Chat/Work/Codex comparisons. Do not use for generic app/software tasks that merely mention Codex.","shortDescription":"Codex models/pricing, scheduled tasks, skills, settings, setup, troubleshooting, and self-knowledge; OpenAI APIs and ChatGPT Work. 'You'/'this app' means Codex only.","interface":{"displayName":"OpenAI Docs","shortDescription":"OpenAI and Codex docs for models, skills, tasks, and setup","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\assets\\openai.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\openai-docs\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"plugin-creator","description":"Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow.","interface":{"displayName":"Plugin Creator","shortDescription":"Scaffold plugins and marketplace entries","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\assets\\plugin-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $plugin-creator to scaffold a valid plugin in the personal marketplace, then validate it before handing it back."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\plugin-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"review-agent","description":"Perform a read-only, defect-first review of a specified code change and return every actionable finding. Use when another agent delegates review of uncommitted changes, a base-branch diff, a commit, or custom review instructions.","interface":{"displayName":"Review Agent","shortDescription":"Find actionable bugs in code changes","iconSmall":null,"iconLarge":null,"iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":"Use $review-agent to review the requested code changes and return actionable findings."},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\review-agent\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-creator","description":"Create or update a Codex skill with appropriately scoped instructions and any needed supporting resources.","shortDescription":"Create or update a skill","interface":{"displayName":"Skill Creator","shortDescription":"Create or update a skill","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\assets\\skill-creator.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-creator\\SKILL.md","scope":"system","enabled":true,"pluginId":null},{"name":"skill-installer","description":"Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).","shortDescription":"Install curated skills from openai/skills or other repos","interface":{"displayName":"Skill Installer","shortDescription":"Install curated skills from openai/skills or other repos","iconSmall":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer-small.svg","iconLarge":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\assets\\skill-installer.png","iconSmallUrl":null,"iconLargeUrl":null,"brandColor":null,"defaultPrompt":null},"path":"D:\\a\\_temp\\uap-native-hosted-qwjlc75b\\evidence\\uap-native-evidence-1787987250\\home\\.codex\\skills\\.system\\skill-installer\\SKILL.md","scope":"system","enabled":true,"pluginId":null}],"errors":[]}]}}} diff --git a/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-with-project-command-missing-stderr.log b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-with-project-command-missing-stderr.log new file mode 100644 index 000000000..7be440739 --- /dev/null +++ b/docs/evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/skills-with-project-command-missing-stderr.log @@ -0,0 +1,4 @@ +2026-10-23T20:25:02.957165Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load. + 1. D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project\.codex + To load project-local config, hooks, and exec policies, add d:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\project as a trusted project in D:\a\_temp\uap-native-hosted-qwjlc75b\evidence\uap-native-evidence-1787987250\home\.codex\config.toml. + diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/README.md b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/README.md new file mode 100644 index 000000000..30b7d6d3f --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/README.md @@ -0,0 +1,101 @@ +# Cline 3.0.73: current-source owned project lifecycle and native metadata + +Observed on 2026-10-25 from source +`3e175de8cfa433f2e9427a47c4e1d7a8e0da5a77`, tree +`72bc516716ae105baffb2dc5a7ec7b8d259f7b95` in +[run 40182752894](https://github.com/777genius/universal-agent-plugins/actions/runs/40182752894), +[job 121137092632](https://github.com/777genius/universal-agent-plugins/actions/runs/40182752894/job/121137092632). +The observed source is the reviewed Cline PR #486 collector plus its assembly +base; it is not a claim that every later assembly revision ran this gate. + +The provider artifact is +`cline-3.0.73-current-source-enabled-project-config-linux-amd64`, ID +`12596067396`, 22,632 bytes, SHA256 +`0537bb6a4e4b551bb06c34ad43ff89446f3a2f418c6c8595212035f3779d7f09`. +The provider ZIP contains only `research.zip`; all 106 inner flat text members +are preserved byte-for-byte here. Binary ZIPs are not committed. +`provenance.json` records both ZIP identities and every member's length/hash. + +## Intake verification and admitted result + +The worker independently verified the provider digest, outer/inner closure, +bounded flat unique member names, all 90 process/in-process stdout/stderr +length/hash pairs, the native artifact SHA256 membership, exact frontend/native +source commit/tree agreement, all fixed stage memberships and qualification +flags. The repository's pure `cline_project_envelope.validate` contract passes. +Original summaries remain unchanged. No native binary was executed during +artifact intake or interpretation. + +The genuine job passed the inert collector contracts, pinned acquisition, +current-source public lifecycle, 23 read-only native commands, exact task cleanup +and separate bounded publication admission. The publisher binary was Cline +3.0.73, SHA256 +`1ff8841d17d4d83763134c193d414e4f7fcf2ef56af759e5ebbe96727fc2c8f7`. +Original UID/GID were 1001/1001. Every native command used the same task HOME +outside A/B, literal `--auto-approve false`, explicit `--cwd`, and disabled +automatic updates. This collector records bounded direct-child management and +read-only source dispatch; it does **not** claim OS/network isolation or absence +of connection attempts. + +The admitted result is owned component installation plus native read-only +metadata discovery on Linux amd64 at the measured source. Project bindings +remain **Prepared + installation_verified**. Neither the native inventory nor +the user fixture's saved `active` file-installation label establishes MCP +initialization, skill/model execution, authentication, trust, IDE behavior, +active-session policy or independent active same-name versions. All ten such +qualification flags remain false in both original summaries. + +## What the actual native rows showed + +Here `` is the exact `fixture_root` recorded in the native summary. +Every selected MCP row below has `disabled:false`, `transportType:streamableHttp` +and the exact source `path`; these are enabled registrations read from actual +UAP-emitted files, not post-edited disabled shadow copies. + +| Observation | Actual native result | Meaning | +| --- | --- | --- | +| Initial A/B skills | A returns A's `.cline/skills/uap-cline-a/SKILL.md` plus the user skill; B returns B's corresponding path plus user. | Selected unique project skills are discovered; sibling project paths are absent from every row. | +| Default A/B and later default A restart | User MCP file `home/.cline/data/settings/cline_mcp_settings.json`, with user markers only. | Cwd alone does not select project MCP. Removing an explicit process override returns to the ordinary user source. | +| Explicit A/B MCP override | Exact A/B `.uap/clients/cline/mcp.json`, containing only that file's foreign and managed entries. | The prepared file selects the whole process MCP source; user entries are not merged. | +| A override while cwd is B | A's exact MCP file and entries. | MCP source selection follows the override independently of cwd; the suggested launch action must fix both. | +| Stable, updated and repaired A | Exact selected A skill metadata and enabled A MCP source, with preserved unrelated rows. | Discovery follows the owned file lifecycle; it does not turn the saved Prepared binding into a runtime session claim. | +| A after removal | Managed A skill and MCP entry absent; foreign A MCP/skills retained. | Fresh read-only metadata reflects owned removal without global uninstall. | +| Same-name user/A/B inventories | All three reads return the user `uap-cline-shared` skill URI with `SAME-USER`; separate UAP user/A/B bindings remain recorded. | The user skill shadows the project skill in this observed version/table. Independent active same-name versions remain blocked, not passed. | + +The same-name stage deliberately remains `blocked` even though observation +completed. A receipt proves local ownership, not effective native precedence. +No row is discarded by name before checking sibling paths. + +## Current-source installer behavior + +The fixture calls public Cobra commands with the production scoped installer, +real file transactions and persisted receipts. Client detection and security +admission are explicit disposable fixture inputs; automatic IDE detection and +production security-service operation are not proved. Public commands execute +in-process, rather than through a separately built public CLI executable. + +User/A/B first installation succeeded. Repeat A add and unchanged update report +`mutated:false` and preserve exact state/native bytes. Intact repair reports its +real package restage as `mutated:true` while preserving native bytes. A same-version +explicit update changes the owned body from `A-ONE` to `A-TWO` and changes its +receipt; a missing delivered skill is then restored from the unchanged selected +local source. This is not a mutable-local-source remote pin claim; immutable +remote pin replay has its separate TLS Git fixture. + +Fresh ordinary SDK inspection remains host-free and reports installed Prepared +project bindings. Removal reports retained plugin data and removes only exact +owned A components. Original user/B state and native inventories remain equal +through the A lifecycle; foreign A content also remains. The raw public JSON, +inspection records, prepared/updated/repaired/removed files and before/after +snapshots are preserved separately so these claims can be checked directly. + +## Independent interpretation + +The Claude/Codex critic independently verified the original provider ZIP, +106-member closure, 90 streams, exact source tree and publisher pin; checked all +23 native before/after owned snapshots, task HOME and false flags; and read the +actual initial/update/remove/same-name skill and MCP rows. It gave GO for the +bounded source-config discovery interpretation, with user-winning same-name +precedence kept blocked and all runtime/connection/trust/session claims excluded. +Final assembled-head replay remains pending; this receipt qualifies only its +identified source and observed component contract. diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-A.stderr.log new file mode 100644 index 000000000..6e74e9069 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-A.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Installing uap-cline-a-package for 1 selected clients... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-A.stdout.log new file mode 100644 index 000000000..b9729c6f5 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-4876ed2ca4232a4420a4a814875a7d0f","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-4876ed2ca4232a4420a4a814875a7d0f","plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-a-package-0f8e27b9e447","components":[{"kind":"skill","name":"uap-cline-a","support":"native"},{"kind":"mcp_server","name":"uap-cline-a","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-B.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-B.stderr.log new file mode 100644 index 000000000..02ff73402 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-B.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Installing uap-cline-b-package for 1 selected clients... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-B.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-B.stdout.log new file mode 100644 index 000000000..6a1ba3dcc --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-B.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-1fabc2548150121b4e5b523cc4b2b421","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-cline-b-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:a9537359413e1f94ab67eb0fcd1f7547a4788e9e4ec27ded5baffb723d3e27d5","manifest_digest":"sha256:94a8a14e0858dd89a236faae8a801f1b74d268beb12201b53654366683b2be83","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-1fabc2548150121b4e5b523cc4b2b421","plugin":"uap-cline-b-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:a9537359413e1f94ab67eb0fcd1f7547a4788e9e4ec27ded5baffb723d3e27d5","manifest_digest":"sha256:94a8a14e0858dd89a236faae8a801f1b74d268beb12201b53654366683b2be83","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"9442d271-4751-4ba1-9d4a-b3660dbca4aa","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-b-package-c803cde6e469","components":[{"kind":"skill","name":"uap-cline-b","support":"native"},{"kind":"mcp_server","name":"uap-cline-b","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-A.stderr.log new file mode 100644 index 000000000..941f6ba1a --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-A.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Installing uap-cline-same for 1 selected clients... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-A.stdout.log new file mode 100644 index 000000000..3d9c38185 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-0ae904764d52735b724a037b4879d329","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-cline-same","version":"1.0.0","source":"direct local source","tree_digest":"sha256:cda789985a308a53d3b46ced59b40c991181dae2d87e1a09beabb96e124f9a9e","manifest_digest":"sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-0ae904764d52735b724a037b4879d329","plugin":"uap-cline-same","version":"1.0.0","source":"direct local source","tree_digest":"sha256:cda789985a308a53d3b46ced59b40c991181dae2d87e1a09beabb96e124f9a9e","manifest_digest":"sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"2394af06-30fa-4427-8ed7-5ae22c7abe55","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-same-5b087a63e616","components":[{"kind":"skill","name":"uap-cline-shared","support":"native"},{"kind":"mcp_server","name":"uap-cline-shared","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-B.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-B.stderr.log new file mode 100644 index 000000000..941f6ba1a --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-B.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Installing uap-cline-same for 1 selected clients... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-B.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-B.stdout.log new file mode 100644 index 000000000..2dde712f2 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-B.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-e112bf197757a0975c6e6d0db7c0d33e","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-cline-same","version":"1.0.0","source":"direct local source","tree_digest":"sha256:a3d76a9360099acad193243758b1c2bad21a861575faa8e4dd2b2c02798d9d33","manifest_digest":"sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-e112bf197757a0975c6e6d0db7c0d33e","plugin":"uap-cline-same","version":"1.0.0","source":"direct local source","tree_digest":"sha256:a3d76a9360099acad193243758b1c2bad21a861575faa8e4dd2b2c02798d9d33","manifest_digest":"sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"d542b3cd-c6d3-4902-96e4-f07da736cdf8","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-same-16e73bd48b7e","components":[{"kind":"skill","name":"uap-cline-shared","support":"native"},{"kind":"mcp_server","name":"uap-cline-shared","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-user.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-user.stderr.log new file mode 100644 index 000000000..941f6ba1a --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-user.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Installing uap-cline-same for 1 selected clients... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-user.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-user.stdout.log new file mode 100644 index 000000000..d842f69e0 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-same-user.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-2b627754b9005f5311885c2ccf3ff20d","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-cline-same","version":"1.0.0","source":"direct local source","tree_digest":"sha256:a25f750b59e66da04e2ec62157036f4480ec2e3db156a47fc0ef15a398bd6b73","manifest_digest":"sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"reload the Cline MCP view in VS Code, or start a new Cline CLI process; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-2b627754b9005f5311885c2ccf3ff20d","plugin":"uap-cline-same","version":"1.0.0","source":"direct local source","tree_digest":"sha256:a25f750b59e66da04e2ec62157036f4480ec2e3db156a47fc0ef15a398bd6b73","manifest_digest":"sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738","next_action":"reload the Cline MCP view in VS Code, or start a new Cline CLI process; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"9e60d821-80ad-4f61-917d-e96f563f058a","plan":{"client_id":"cline","scope":"user","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-same-3aa3866d303c","components":[{"kind":"skill","name":"uap-cline-shared","support":"native"},{"kind":"mcp_server","name":"uap-cline-shared","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","agentplugins will install and verify Cline skills and MCP servers automatically; VS Code reloads MCP settings, while Cline CLI reads them on its next process"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"active","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["reload the Cline MCP view in VS Code, or start a new Cline CLI process"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-user.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-user.stderr.log new file mode 100644 index 000000000..0053ea273 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-user.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Installing uap-cline-user-package for 1 selected clients... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-user.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-user.stdout.log new file mode 100644 index 000000000..3d8fb9b50 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/add-user.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"operation_id":"op-65cfab43f8d714f56fe9e396ec497e2e","batch":true,"status":"completed","succeeded":1,"failed":0,"action_required":1,"plugin":"uap-cline-user-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:66b3ec842767aa35ec94f5b8d7e754f830aedffd1fd0f455103e141a4ecd0bca","manifest_digest":"sha256:01200dfdc248fed4da890ae04740f959042077ef89f95534e3076813ae5661b1","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"reload the Cline MCP view in VS Code, or start a new Cline CLI process; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-65cfab43f8d714f56fe9e396ec497e2e","plugin":"uap-cline-user-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:66b3ec842767aa35ec94f5b8d7e754f830aedffd1fd0f455103e141a4ecd0bca","manifest_digest":"sha256:01200dfdc248fed4da890ae04740f959042077ef89f95534e3076813ae5661b1","next_action":"reload the Cline MCP view in VS Code, or start a new Cline CLI process; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"542bcb99-5845-4d07-a4f1-75c50be2723a","plan":{"client_id":"cline","scope":"user","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-user-package-85f605274722","components":[{"kind":"skill","name":"uap-cline-user","support":"native"},{"kind":"mcp_server","name":"uap-cline-user","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","agentplugins will install and verify Cline skills and MCP servers automatically; VS Code reloads MCP settings, while Cline CLI reads them on its next process"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"active","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["reload the Cline MCP view in VS Code, or start a new Cline CLI process"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}],"environment":{"reason":"no_save_requested","saved":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cleanup.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cleanup.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cline-project-evidence.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cline-project-evidence.json new file mode 100644 index 000000000..86cedaabf --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cline-project-evidence.json @@ -0,0 +1,2116 @@ +{ + "artifact_sha256": { + "add-A.stderr.log": "dd6461f8484691e52c746ee11a2ce659a5f7fa22db1009904ecb368d88f5918a", + "add-A.stdout.log": "85b99da05aba6479b16a424b74d63c7c9544989ce92d3288f006bdd307a8db1b", + "add-B.stderr.log": "9e3202447bb884a8f418ec9323b92f5f6a3f5c6fcea070215421e12c5adeae79", + "add-B.stdout.log": "3ec933466631ec9d33385f0f98990f61970882f9c2c105b6d926772ac8a5767e", + "add-same-A.stderr.log": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131", + "add-same-A.stdout.log": "2bc8a862fabf84b588f72499610587a88e54de8ffd48a3e83d4da61ebd1cd6fe", + "add-same-B.stderr.log": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131", + "add-same-B.stdout.log": "56b40aaedd56d8706a1699929a84b60b245e7eb3b3ce87e0980af0e0e6b7d725", + "add-same-user.stderr.log": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131", + "add-same-user.stdout.log": "e10ded4887e3019038f12685562d668728a185d23dbb4853e785ed016d4f7fdf", + "add-user.stderr.log": "4f0096de4eaa9ddeecbcee07c58e9cebfc53a03f6dc0c753ed1b9a53c7e05795", + "add-user.stdout.log": "8a5b94898a35297768f7f1451735019ec9392d452ed5c29a8c2f064d6f86732b", + "cleanup.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "cleanup.stdout.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "cross-A-from-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "cross-A-from-B.stdout.log": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "default-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "default-A.stdout.log": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66", + "default-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "default-B.stdout.log": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66", + "foreign-snapshots.json": "d0fe9852a1fa292f34994740dc691dda32e268d202278c06264a190bd532d626", + "git-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-A.stdout.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-B.stdout.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-user.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-user.stdout.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "initial-A-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "initial-A-skills.stdout.log": "576a276260670749579c2b68e5f6d3bb7cfd0a712e360209df60a8a08efbcb38", + "initial-B-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "initial-B-skills.stdout.log": "bc83d557e031876dc0de38ef132d574152c8ec8f594630708bd690f95de4de2e", + "intact-repair-A.stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "intact-repair-A.stdout.log": "028e7fe66734ff032cc968c4bb1373fb5c6efa774ec3be9c1f87038cac06265f", + "missing-repair-A.stderr.log": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "missing-repair-A.stdout.log": "b0c88a36ab0afff7aeb391f370e2ff40a77407d288a1fde5cfc294441ebf8949", + "override-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "override-A.stdout.log": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "override-B.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "override-B.stdout.log": "a2482086d7db1f2bd8b50fb774872828be42448c48ddba62327707132917ec42", + "prepared-A-config.json": "12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "prepared-A-inspection.json": "10781645be342d10bd404f24b8a2101a8a9f16ab2d99aa1be1c431e2b6122e15", + "prepared-B-config.json": "b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "prepared-B-inspection.json": "02d4efad79f9427d27cb731a28c044e732804fde3f57d335c720e2ed96888550", + "prepared-user-config.json": "0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "remove-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "remove-A.stdout.log": "853b271621cf0a65c4d88c9e9d5d5568ada990f708ad2fb648cd324f6d466cc7", + "removed-A-config.json": "803bd4f4a6410babd14711cae3743bd73ed6f157a8a4758ff13318474f52f437", + "removed-A-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "removed-A-mcp.stdout.log": "a07ddb0e9fa1ab77c2be91175a58a2d716d504ee7abbd9fda465e31c07ac00c4", + "removed-A-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "removed-A-skills.stdout.log": "cc86a473154a1c8c79e0557e9a1f7e048b1621f0ad6a2e03746287c4563f53b1", + "repaired-A-config.json": "732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "repaired-A-inspection.json": "b12d82d94a561924c8bb309e7e9ca7a447784b6d7a2f67fad28bea80702e5bbb", + "repaired-A-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "repaired-A-mcp.stdout.log": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "repaired-A-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "repaired-A-skills.stdout.log": "c3fb38e749e98677be46ec4e19e2132641be44c1a3c9846ae58979e37c7b572f", + "repeat-add-A.stderr.log": "c2908b301e4d5b9f819536bcb66bdc310bf8b94a4d6be576837cd1ceda9df9c4", + "repeat-add-A.stdout.log": "ae537da7b41ca039292dfa5338e53a5af053dfa8acb142fb25ec44aafaedc020", + "restart-default-A.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "restart-default-A.stdout.log": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66", + "same-A-config.json": "f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "same-A-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "same-A-mcp.stdout.log": "13aecb38b3b9e85818774e09b181ec219639dad94858028ecb07309aa73b8b1f", + "same-A-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "same-A-skills.stdout.log": "5deecffa34a751810e18acdd38f3064e798e0ce1563434caf99e8101aafd68e0", + "same-B-config.json": "8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "same-B-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "same-B-mcp.stdout.log": "1097641eefe915af9ce751949357afebe65f88d708718adeacfddc27ac2eddc2", + "same-B-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "same-B-skills.stdout.log": "4fe7a104b69a1a8bc0930fbc61345e1c47c2c0a9c421da6ec713d860c0ccdb9c", + "same-name-winners.json": "073738b182babc90afd69b8bd311a0b69017f0e89c715f32f42539ac1e700006", + "same-user-config.json": "54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "same-user-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "same-user-mcp.stdout.log": "547a8a32ce773235bcb1de5566c2dbf20bb31c07a43bba4d29ad648a98c16704", + "same-user-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "same-user-skills.stdout.log": "706221aa7420cf7ee365961288d190cfcf9749b54eb6a54cfde6a4ab8bb70a71", + "stable-A-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stable-A-mcp.stdout.log": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "stable-A-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stable-A-skills.stdout.log": "576a276260670749579c2b68e5f6d3bb7cfd0a712e360209df60a8a08efbcb38", + "unchanged-update-A.stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "unchanged-update-A.stdout.log": "42de5d239ec8d81724632f94127980b3be7488e9559d0c5c888b4467dc31bbd9", + "update-A.stderr.log": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "update-A.stdout.log": "a449175240578e352adc13111a5fceb4782cd288c0b95473ea3dcf01b4ce8754", + "updated-A-config.json": "732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "updated-A-mcp.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "updated-A-mcp.stdout.log": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "updated-A-skills.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "updated-A-skills.stdout.log": "c3fb38e749e98677be46ec4e19e2132641be44c1a3c9846ae58979e37c7b572f", + "version.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "version.stdout.log": "942a33503b6a87e2f85be82d79fc3ced6c59ebd1b901ba309db8d04da494ffc7" + }, + "cleanup_complete": true, + "cleanup_error": "", + "client": "cline", + "client_version_measured": "3.0.73", + "collection_complete": true, + "commands": [ + { + "argv": [ + "/usr/bin/git", + "-c", + "core.hooksPath=/dev/null", + "init", + "--quiet", + "--template=", + "--initial-branch=main" + ], + "class": "git", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "elapsed_ms": 2, + "error": "", + "exit": 0, + "in_process": false, + "label": "git-user", + "overflow": false, + "stderr_bytes": 0, + "stderr_file": "git-user.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 0, + "stdout_file": "git-user.stdout.log", + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "argv": [ + "/usr/bin/git", + "-c", + "core.hooksPath=/dev/null", + "init", + "--quiet", + "--template=", + "--initial-branch=main" + ], + "class": "git", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 2, + "error": "", + "exit": 0, + "in_process": false, + "label": "git-A", + "overflow": false, + "stderr_bytes": 0, + "stderr_file": "git-A.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 0, + "stdout_file": "git-A.stdout.log", + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "argv": [ + "/usr/bin/git", + "-c", + "core.hooksPath=/dev/null", + "init", + "--quiet", + "--template=", + "--initial-branch=main" + ], + "class": "git", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 2, + "error": "", + "exit": 0, + "in_process": false, + "label": "git-B", + "overflow": false, + "stderr_bytes": 0, + "stderr_file": "git-B.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 0, + "stdout_file": "git-B.stdout.log", + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/user", + "--no-save", + "--target", + "cline", + "--format", + "json" + ], + "class": "public_uap", + "cwd": "", + "elapsed_ms": 24, + "error": "", + "exit": 0, + "in_process": true, + "label": "add-user", + "overflow": false, + "stderr_bytes": 139, + "stderr_file": "add-user.stderr.log", + "stderr_sha256": "4f0096de4eaa9ddeecbcee07c58e9cebfc53a03f6dc0c753ed1b9a53c7e05795", + "stdout_bytes": 2553, + "stdout_file": "add-user.stdout.log", + "stdout_sha256": "8a5b94898a35297768f7f1451735019ec9392d452ed5c29a8c2f064d6f86732b" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/A", + "--no-save", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 49, + "error": "", + "exit": 0, + "in_process": true, + "label": "add-A", + "overflow": false, + "stderr_bytes": 136, + "stderr_file": "add-A.stderr.log", + "stderr_sha256": "dd6461f8484691e52c746ee11a2ce659a5f7fa22db1009904ecb368d88f5918a", + "stdout_bytes": 5476, + "stdout_file": "add-A.stdout.log", + "stdout_sha256": "85b99da05aba6479b16a424b74d63c7c9544989ce92d3288f006bdd307a8db1b" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/B", + "--no-save", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 52, + "error": "", + "exit": 0, + "in_process": true, + "label": "add-B", + "overflow": false, + "stderr_bytes": 136, + "stderr_file": "add-B.stderr.log", + "stderr_sha256": "9e3202447bb884a8f418ec9323b92f5f6a3f5c6fcea070215421e12c5adeae79", + "stdout_bytes": 5476, + "stdout_file": "add-B.stdout.log", + "stdout_sha256": "3ec933466631ec9d33385f0f98990f61970882f9c2c105b6d926772ac8a5767e" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "--version" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "elapsed_ms": 902, + "error": "", + "exit": 0, + "in_process": false, + "label": "version", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "version.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 7, + "stdout_file": "version.stdout.log", + "stdout_sha256": "942a33503b6a87e2f85be82d79fc3ced6c59ebd1b901ba309db8d04da494ffc7", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 878, + "error": "", + "exit": 0, + "in_process": false, + "label": "initial-A-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "initial-A-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1432, + "stdout_file": "initial-A-skills.stdout.log", + "stdout_sha256": "576a276260670749579c2b68e5f6d3bb7cfd0a712e360209df60a8a08efbcb38", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 883, + "error": "", + "exit": 0, + "in_process": false, + "label": "initial-B-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "initial-B-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1432, + "stdout_file": "initial-B-skills.stdout.log", + "stdout_sha256": "bc83d557e031876dc0de38ef132d574152c8ec8f594630708bd690f95de4de2e", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 845, + "error": "", + "exit": 0, + "in_process": false, + "label": "default-A", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "default-A.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 457, + "stdout_file": "default-A.stdout.log", + "stdout_sha256": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 917, + "error": "", + "exit": 0, + "in_process": false, + "label": "default-B", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "default-B.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 457, + "stdout_file": "default-B.stdout.log", + "stdout_sha256": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 848, + "error": "", + "exit": 0, + "in_process": false, + "label": "override-A", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "override-A.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 411, + "stdout_file": "override-A.stdout.log", + "stdout_sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 865, + "error": "", + "exit": 0, + "in_process": false, + "label": "override-B", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "override-B.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 411, + "stdout_file": "override-B.stdout.log", + "stdout_sha256": "a2482086d7db1f2bd8b50fb774872828be42448c48ddba62327707132917ec42", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 1360, + "error": "", + "exit": 0, + "in_process": false, + "label": "cross-A-from-B", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "cross-A-from-B.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 411, + "stdout_file": "cross-A-from-B.stdout.log", + "stdout_sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 928, + "error": "", + "exit": 0, + "in_process": false, + "label": "restart-default-A", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:5382:e8762796cfb5ce850330962ebe8eb9694c84e8fd0433cc8565b13de76d6a086c", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "restart-default-A.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 457, + "stdout_file": "restart-default-A.stdout.log", + "stdout_sha256": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/A", + "--no-save", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 46, + "error": "", + "exit": 0, + "in_process": true, + "label": "repeat-add-A", + "overflow": false, + "stderr_bytes": 120, + "stderr_file": "repeat-add-A.stderr.log", + "stderr_sha256": "c2908b301e4d5b9f819536bcb66bdc310bf8b94a4d6be576837cd1ceda9df9c4", + "stdout_bytes": 3942, + "stdout_file": "repeat-add-A.stdout.log", + "stdout_sha256": "ae537da7b41ca039292dfa5338e53a5af053dfa8acb142fb25ec44aafaedc020" + }, + { + "argv": [ + "update", + "uap-cline-a-package", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 51, + "error": "", + "exit": 0, + "in_process": true, + "label": "unchanged-update-A", + "overflow": false, + "stderr_bytes": 87, + "stderr_file": "unchanged-update-A.stderr.log", + "stderr_sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "stdout_bytes": 5087, + "stdout_file": "unchanged-update-A.stdout.log", + "stdout_sha256": "42de5d239ec8d81724632f94127980b3be7488e9559d0c5c888b4467dc31bbd9" + }, + { + "argv": [ + "repair", + "uap-cline-a-package", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 77, + "error": "", + "exit": 0, + "in_process": true, + "label": "intact-repair-A", + "overflow": false, + "stderr_bytes": 78, + "stderr_file": "intact-repair-A.stderr.log", + "stderr_sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "stdout_bytes": 4902, + "stdout_file": "intact-repair-A.stdout.log", + "stdout_sha256": "028e7fe66734ff032cc968c4bb1373fb5c6efa774ec3be9c1f87038cac06265f" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 957, + "error": "", + "exit": 0, + "in_process": false, + "label": "stable-A-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:6529:b971b7eda184b4a5a5cd808b0593a69bfe48f41cd50eaf68e20b94c310e658f6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:6529:b971b7eda184b4a5a5cd808b0593a69bfe48f41cd50eaf68e20b94c310e658f6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "stable-A-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1432, + "stdout_file": "stable-A-skills.stdout.log", + "stdout_sha256": "576a276260670749579c2b68e5f6d3bb7cfd0a712e360209df60a8a08efbcb38", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 1252, + "error": "", + "exit": 0, + "in_process": false, + "label": "stable-A-mcp", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:6529:b971b7eda184b4a5a5cd808b0593a69bfe48f41cd50eaf68e20b94c310e658f6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:d2e47cdacb08995d61917dd2d2f048e3d4c15f3938b5f2e2966c2cac0c01f4bd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:6529:b971b7eda184b4a5a5cd808b0593a69bfe48f41cd50eaf68e20b94c310e658f6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "stable-A-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 411, + "stdout_file": "stable-A-mcp.stdout.log", + "stdout_sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "update", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/A", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 73, + "error": "", + "exit": 0, + "in_process": true, + "label": "update-A", + "overflow": false, + "stderr_bytes": 87, + "stderr_file": "update-A.stderr.log", + "stderr_sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10", + "stdout_bytes": 5069, + "stdout_file": "update-A.stdout.log", + "stdout_sha256": "a449175240578e352adc13111a5fceb4782cd288c0b95473ea3dcf01b4ce8754" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 905, + "error": "", + "exit": 0, + "in_process": false, + "label": "updated-A-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:7676:13034c5671991805a1cf206f8c5873d4ed2b7be831381a08d8ef9087a054c11a", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:7676:13034c5671991805a1cf206f8c5873d4ed2b7be831381a08d8ef9087a054c11a", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "updated-A-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1432, + "stdout_file": "updated-A-skills.stdout.log", + "stdout_sha256": "c3fb38e749e98677be46ec4e19e2132641be44c1a3c9846ae58979e37c7b572f", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 866, + "error": "", + "exit": 0, + "in_process": false, + "label": "updated-A-mcp", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:7676:13034c5671991805a1cf206f8c5873d4ed2b7be831381a08d8ef9087a054c11a", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:7676:13034c5671991805a1cf206f8c5873d4ed2b7be831381a08d8ef9087a054c11a", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "updated-A-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 411, + "stdout_file": "updated-A-mcp.stdout.log", + "stdout_sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "repair", + "uap-cline-a-package", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 75, + "error": "", + "exit": 0, + "in_process": true, + "label": "missing-repair-A", + "overflow": false, + "stderr_bytes": 78, + "stderr_file": "missing-repair-A.stderr.log", + "stderr_sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f", + "stdout_bytes": 4902, + "stdout_file": "missing-repair-A.stdout.log", + "stdout_sha256": "b0c88a36ab0afff7aeb391f370e2ff40a77407d288a1fde5cfc294441ebf8949" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 871, + "error": "", + "exit": 0, + "in_process": false, + "label": "repaired-A-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8823:caad251910a32779c6b8380d0172dfc653d91df6c00c7464393bfd0e146da8b6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8823:caad251910a32779c6b8380d0172dfc653d91df6c00c7464393bfd0e146da8b6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "repaired-A-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1432, + "stdout_file": "repaired-A-skills.stdout.log", + "stdout_sha256": "c3fb38e749e98677be46ec4e19e2132641be44c1a3c9846ae58979e37c7b572f", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 916, + "error": "", + "exit": 0, + "in_process": false, + "label": "repaired-A-mcp", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8823:caad251910a32779c6b8380d0172dfc653d91df6c00c7464393bfd0e146da8b6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md": "644:144:ae38ac53775d985861d5caaab672f460b6aaff739644d43dd2f3935fbbbcb9a1", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:227:732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8823:caad251910a32779c6b8380d0172dfc653d91df6c00c7464393bfd0e146da8b6", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "repaired-A-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 411, + "stdout_file": "repaired-A-mcp.stdout.log", + "stdout_sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "remove", + "uap-cline-a-package", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 64, + "error": "", + "exit": 0, + "in_process": true, + "label": "remove-A", + "overflow": false, + "stderr_bytes": 0, + "stderr_file": "remove-A.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1700, + "stdout_file": "remove-A.stdout.log", + "stdout_sha256": "853b271621cf0a65c4d88c9e9d5d5568ada990f708ad2fb648cd324f6d466cc7" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 886, + "error": "", + "exit": 0, + "in_process": false, + "label": "removed-A-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:137:803bd4f4a6410babd14711cae3743bd73ed6f157a8a4758ff13318474f52f437", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:2972:0fd473881abb2758f46b6b04fe5a0d1fef5b340e11665a57271d18e262893987", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:137:803bd4f4a6410babd14711cae3743bd73ed6f157a8a4758ff13318474f52f437", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:2972:0fd473881abb2758f46b6b04fe5a0d1fef5b340e11665a57271d18e262893987", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "removed-A-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1070, + "stdout_file": "removed-A-skills.stdout.log", + "stdout_sha256": "cc86a473154a1c8c79e0557e9a1f7e048b1621f0ad6a2e03746287c4563f53b1", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 853, + "error": "", + "exit": 0, + "in_process": false, + "label": "removed-A-mcp", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:137:803bd4f4a6410babd14711cae3743bd73ed6f157a8a4758ff13318474f52f437", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:2972:0fd473881abb2758f46b6b04fe5a0d1fef5b340e11665a57271d18e262893987", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:137:803bd4f4a6410babd14711cae3743bd73ed6f157a8a4758ff13318474f52f437", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:2972:0fd473881abb2758f46b6b04fe5a0d1fef5b340e11665a57271d18e262893987", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + }, + "stderr_bytes": 0, + "stderr_file": "removed-A-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 205, + "stdout_file": "removed-A-mcp.stdout.log", + "stdout_sha256": "a07ddb0e9fa1ab77c2be91175a58a2d716d504ee7abbd9fda465e31c07ac00c4", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/same-user", + "--no-save", + "--target", + "cline", + "--format", + "json" + ], + "class": "public_uap", + "cwd": "", + "elapsed_ms": 27, + "error": "", + "exit": 0, + "in_process": true, + "label": "add-same-user", + "overflow": false, + "stderr_bytes": 131, + "stderr_file": "add-same-user.stderr.log", + "stderr_sha256": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131", + "stdout_bytes": 2533, + "stdout_file": "add-same-user.stdout.log", + "stdout_sha256": "e10ded4887e3019038f12685562d668728a185d23dbb4853e785ed016d4f7fdf" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/same-A", + "--no-save", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 56, + "error": "", + "exit": 0, + "in_process": true, + "label": "add-same-A", + "overflow": false, + "stderr_bytes": 131, + "stderr_file": "add-same-A.stderr.log", + "stderr_sha256": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131", + "stdout_bytes": 5471, + "stdout_file": "add-same-A.stdout.log", + "stdout_sha256": "2bc8a862fabf84b588f72499610587a88e54de8ffd48a3e83d4da61ebd1cd6fe" + }, + { + "argv": [ + "add", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/sources/same-B", + "--no-save", + "--target", + "cline", + "--format", + "json", + "--scope", + "project", + "--project-root", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B" + ], + "class": "public_uap", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 62, + "error": "", + "exit": 0, + "in_process": true, + "label": "add-same-B", + "overflow": false, + "stderr_bytes": 131, + "stderr_file": "add-same-B.stderr.log", + "stderr_sha256": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131", + "stdout_bytes": 5471, + "stdout_file": "add-same-B.stdout.log", + "stdout_sha256": "56b40aaedd56d8706a1699929a84b60b245e7eb3b3ce87e0980af0e0e6b7d725" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "elapsed_ms": 1011, + "error": "", + "exit": 0, + "in_process": false, + "label": "same-user-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "stderr_bytes": 0, + "stderr_file": "same-user-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1119, + "stdout_file": "same-user-skills.stdout.log", + "stdout_sha256": "706221aa7420cf7ee365961288d190cfcf9749b54eb6a54cfde6a4ab8bb70a71", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/user-cwd", + "elapsed_ms": 1278, + "error": "", + "exit": 0, + "in_process": false, + "label": "same-user-mcp", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "stderr_bytes": 0, + "stderr_file": "same-user-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 688, + "stdout_file": "same-user-mcp.stdout.log", + "stdout_sha256": "547a8a32ce773235bcb1de5566c2dbf20bb31c07a43bba4d29ad648a98c16704", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 882, + "error": "", + "exit": 0, + "in_process": false, + "label": "same-A-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "stderr_bytes": 0, + "stderr_file": "same-A-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1462, + "stdout_file": "same-A-skills.stdout.log", + "stdout_sha256": "5deecffa34a751810e18acdd38f3064e798e0ce1563434caf99e8101aafd68e0", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "elapsed_ms": 1372, + "error": "", + "exit": 0, + "in_process": false, + "label": "same-A-mcp", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "stderr_bytes": 0, + "stderr_file": "same-A-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 416, + "stdout_file": "same-A-mcp.stdout.log", + "stdout_sha256": "13aecb38b3b9e85818774e09b181ec219639dad94858028ecb07309aa73b8b1f", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "config", + "skills", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 867, + "error": "", + "exit": 0, + "in_process": false, + "label": "same-B-skills", + "mcp_settings_override": "", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "stderr_bytes": 0, + "stderr_file": "same-B-skills.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 1824, + "stdout_file": "same-B-skills.stdout.log", + "stdout_sha256": "4fe7a104b69a1a8bc0930fbc61345e1c47c2c0a9c421da6ec713d860c0ccdb9c", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "config", + "mcp", + "--json" + ], + "automatic_update_disabled": true, + "class": "native", + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "elapsed_ms": 912, + "error": "", + "exit": 0, + "in_process": false, + "label": "same-B-mcp", + "mcp_settings_override": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json", + "overflow": false, + "owned_after": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "owned_before": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md": "600:120:a0863e687e753049b025e56a98e8cc48e235b0f7494331b0ce94acb732ae65ea", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared/SKILL.md": "644:151:cb89674966c134bd428d3465adc1b3449d559b8ea63b623871802a383bbd3ce2", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json": "600:233:f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/state-v2.json": "600:8313:d8edbc3b8272a433d2977a972cb56ef044a99a8d83fd3f8d4ec7ef277b6f4f04", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared/SKILL.md": "644:151:8bfdf7c9a2b53f40daf3769dae8753c5f05962f3fc34251e9b1def1783354222", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:323:8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:10721:12382bb0166e43070d7e273091909889dbe9f3ae76d9b70111fe22daca74fbf4", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:337:54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md": "644:157:b0a57a1ae2e8cfabd77f455cb8ab3966d1f3a61625f5896aacf44c818e7e5725", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:10710:bfd8fb988817091a1dd2c97bd7bcb23da0840b37868a10e9b7a72da8f0b00f36" + }, + "stderr_bytes": 0, + "stderr_file": "same-B-mcp.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 622, + "stdout_file": "same-B-mcp.stdout.log", + "stdout_sha256": "1097641eefe915af9ce751949357afebe65f88d708718adeacfddc27ac2eddc2", + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home" + }, + { + "argv": [ + "/usr/bin/python3", + "-I", + "-c", + "import os,stat,sys\nroot,dev,ino,uid=sys.argv[1:]; parent,name=os.path.split(root)\np=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW); fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=p)\nidentity=os.fstat(fd)\nif (identity.st_dev,identity.st_ino,identity.st_uid)!=(int(dev),int(ino),int(uid)):raise SystemExit('root authority changed')\ncount=[0]\ndef clear(fd,depth):\n if depth\u003e32:raise RuntimeError('cleanup depth')\n os.fchmod(fd,os.fstat(fd).st_mode|0o700)\n with os.scandir(fd) as entries:\n for entry in entries:\n count[0]+=1\n if count[0]\u003e8192:raise RuntimeError('cleanup entry bound')\n item=os.stat(entry.name,dir_fd=fd,follow_symlinks=False)\n if item.st_uid!=int(uid):raise RuntimeError('foreign cleanup owner')\n if stat.S_ISDIR(item.st_mode):\n child=os.open(entry.name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)\n try:\n if not os.path.samestat(item,os.fstat(child)):raise RuntimeError('directory changed')\n clear(child,depth+1)\n finally:os.close(child)\n os.rmdir(entry.name,dir_fd=fd)\n else:os.unlink(entry.name,dir_fd=fd)\nclear(fd,0)\nif not os.path.samestat(identity,os.stat(name,dir_fd=p,follow_symlinks=False)):raise SystemExit('cleanup root replaced')\nos.close(fd);os.rmdir(name,dir_fd=p);os.close(p)", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539", + "2049", + "9197181", + "1001" + ], + "class": "cleanup", + "cwd": "/", + "elapsed_ms": 26, + "error": "", + "exit": 0, + "in_process": false, + "label": "cleanup", + "overflow": false, + "stderr_bytes": 0, + "stderr_file": "cleanup.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 0, + "stdout_file": "cleanup.stdout.log", + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "finished_utc": "2026-10-25T10:40:46.7999496Z", + "fixture_root": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539", + "native_authentication_qualified": false, + "native_ide_qualified": false, + "native_mcp_initialization_qualified": false, + "native_model_use_qualified": false, + "native_project_activation_qualified": false, + "native_runtime_qualified": false, + "native_session_policy_qualified": false, + "native_skill_execution_qualified": false, + "native_trust_qualified": false, + "network": "not isolated; only source-established read-only config dispatch; no connection-attempt or initialization claim", + "original_gid": 1001, + "original_uid": 1001, + "read_only_unique_marker_discovery_qualified": true, + "same_name_active_isolation_qualified": false, + "same_name_observation_complete": true, + "same_name_owned_bindings": { + "A": { + "client_binding_id": "client_d63877b9b7fde1b5424ca707", + "client_id": "cline", + "scope": "project", + "target_locator": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/uap-cline-same-5b087a63e616", + "physical_artifact_id": "uap-cline-same-5b087a63e616", + "materialization": "materialized", + "activation": "prepared", + "authentication": "not_checked", + "policy": "allowed", + "verification": "installation_verified", + "package_revision": { + "version": "1.0.0", + "tree_digest": "sha256:cda789985a308a53d3b46ced59b40c991181dae2d87e1a09beabb96e124f9a9e", + "manifest_digest": "sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738" + }, + "affected_surfaces": [ + "cline" + ], + "native_profile_root": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A", + "native_objects": [ + { + "object_id": "package:cline:uap-cline-same-5b087a63e616", + "kind": "managed_package_directory", + "logical_name": "uap-cline-same", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/uap-cline-same-5b087a63e616", + "managed_digest": "sha256:31c1a85cee2ddf5db0719b27c8d9874cd25d2d89f7c09b42d4cc0888ebfb1786", + "protection_class": "managed" + }, + { + "object_id": "cline_project_mcp_server:uap-cline-shared", + "kind": "cline_project_mcp_server", + "logical_name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json", + "managed_digest": "sha256:7389e79d31cc8cbacdaa457518c9a16036e4df120024e8cc728fa5ded1f979cf", + "protection_class": "managed" + }, + { + "object_id": "cline_project_skill_directory:uap-cline-shared", + "kind": "cline_project_skill_directory", + "logical_name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-shared", + "source_relative": "skills/uap-cline-shared", + "managed_digest": "sha256:69ca932320328f518bcf25858d0c2b82ffdb53b2006d5b365efca11bcd79e0c6", + "protection_class": "managed" + } + ], + "receipts": [ + { + "operation_id": "op-0ae904764d52735b724a037b4879d329-001", + "operation_group_id": "op-0ae904764d52735b724a037b4879d329", + "sequence": 1, + "mutation_type": "directory_swap", + "client_binding_id": "client_d63877b9b7fde1b5424ca707", + "active_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/uap-cline-same-5b087a63e616", + "staging_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/.agentplugins-staging-248fa7c4da0f6d41", + "backup_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/.agentplugins-backup-248fa7c4da0f6d41", + "after_digest": "sha256:31c1a85cee2ddf5db0719b27c8d9874cd25d2d89f7c09b42d4cc0888ebfb1786", + "phase": "committed" + } + ], + "updated_at": "2026-10-25T10:40:39.600246957Z" + }, + "B": { + "client_binding_id": "client_8650966e18efa0561e6c94a7", + "client_id": "cline", + "scope": "project", + "target_locator": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-same-16e73bd48b7e", + "physical_artifact_id": "uap-cline-same-16e73bd48b7e", + "materialization": "materialized", + "activation": "prepared", + "authentication": "not_checked", + "policy": "allowed", + "verification": "installation_verified", + "package_revision": { + "version": "1.0.0", + "tree_digest": "sha256:a3d76a9360099acad193243758b1c2bad21a861575faa8e4dd2b2c02798d9d33", + "manifest_digest": "sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738" + }, + "affected_surfaces": [ + "cline" + ], + "native_profile_root": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B", + "native_objects": [ + { + "object_id": "package:cline:uap-cline-same-16e73bd48b7e", + "kind": "managed_package_directory", + "logical_name": "uap-cline-same", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-same-16e73bd48b7e", + "managed_digest": "sha256:0dfdcf439b3b98a5918153e64ffe44dd43a626406f2a104e8ee5edd7be2e1b4f", + "protection_class": "managed" + }, + { + "object_id": "cline_project_mcp_server:uap-cline-shared", + "kind": "cline_project_mcp_server", + "logical_name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json", + "managed_digest": "sha256:b73fa5634976bd33d4ac61d1f455557f23c5449e3e1319e165a58c24051a556f", + "protection_class": "managed" + }, + { + "object_id": "cline_project_skill_directory:uap-cline-shared", + "kind": "cline_project_skill_directory", + "logical_name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-shared", + "source_relative": "skills/uap-cline-shared", + "managed_digest": "sha256:e5e96a5be5dcc958edf515509ce3d149fdcfeebf88e26a2d6636fb14487b1c5d", + "protection_class": "managed" + } + ], + "receipts": [ + { + "operation_id": "op-e112bf197757a0975c6e6d0db7c0d33e-001", + "operation_group_id": "op-e112bf197757a0975c6e6d0db7c0d33e", + "sequence": 1, + "mutation_type": "directory_swap", + "client_binding_id": "client_8650966e18efa0561e6c94a7", + "active_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-same-16e73bd48b7e", + "staging_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/.agentplugins-staging-5da2e71ae0df99ec", + "backup_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/.agentplugins-backup-5da2e71ae0df99ec", + "after_digest": "sha256:0dfdcf439b3b98a5918153e64ffe44dd43a626406f2a104e8ee5edd7be2e1b4f", + "phase": "committed" + } + ], + "updated_at": "2026-10-25T10:40:39.72093097Z" + }, + "user": { + "client_binding_id": "client_38e74c1ac69f17f1fd5572af", + "client_id": "cline", + "scope": "user", + "target_locator": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/managed/clients/cline/uap-cline-same-3aa3866d303c", + "physical_artifact_id": "uap-cline-same-3aa3866d303c", + "materialization": "materialized", + "activation": "active", + "authentication": "not_checked", + "policy": "allowed", + "verification": "installation_verified", + "package_revision": { + "version": "1.0.0", + "tree_digest": "sha256:a25f750b59e66da04e2ec62157036f4480ec2e3db156a47fc0ef15a398bd6b73", + "manifest_digest": "sha256:68808cf9ff33c1d477130bb4c0b989c874908a4974950ce927fd2b0f76494738" + }, + "affected_surfaces": [ + "cline" + ], + "native_objects": [ + { + "object_id": "package:cline:uap-cline-same-3aa3866d303c", + "kind": "managed_package_directory", + "logical_name": "uap-cline-same", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/managed/clients/cline/uap-cline-same-3aa3866d303c", + "managed_digest": "sha256:d765acb9af1f05060682530a6621d4bde672cfbaae0f1b779313541890d8867e", + "protection_class": "managed" + }, + { + "object_id": "cline-mcp:uap-cline-shared", + "kind": "cline_global_mcp_server", + "logical_name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json", + "source_relative": ".agentplugins-cline-native.json", + "managed_digest": "sha256:507f8323486066ca6d4406984169ef7a98adf2304919175a6adb1c332a9fc554", + "protection_class": "managed" + }, + { + "object_id": "cline-skill:uap-cline-shared", + "kind": "cline_global_skill_directory", + "logical_name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared", + "source_relative": "skills/uap-cline-shared", + "managed_digest": "sha256:a215df3529cce1e00447ae798ede6467c0e539346c90599dd0e1634bb5716d8f", + "protection_class": "managed" + } + ], + "receipts": [ + { + "operation_id": "op-2b627754b9005f5311885c2ccf3ff20d-001", + "operation_group_id": "op-2b627754b9005f5311885c2ccf3ff20d", + "sequence": 1, + "mutation_type": "directory_swap", + "client_binding_id": "client_38e74c1ac69f17f1fd5572af", + "active_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/managed/clients/cline/uap-cline-same-3aa3866d303c", + "staging_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/managed/clients/cline/.agentplugins-staging-cc3a855a05980dfd", + "backup_path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/managed/clients/cline/.agentplugins-backup-cc3a855a05980dfd", + "after_digest": "sha256:d765acb9af1f05060682530a6621d4bde672cfbaae0f1b779313541890d8867e", + "phase": "committed" + } + ], + "updated_at": "2026-10-25T10:40:39.542093944Z" + } + }, + "schema_version": 1, + "source_commit": "3e175de8cfa433f2e9427a47c4e1d7a8e0da5a77", + "source_composition": "public Cobra and production scoped installer/file transactions; explicit disposable client authority and inert security admission; not automatic IDE detection", + "source_tree": "72bc516716ae105baffb2dc5a7ec7b8d259f7b95", + "stages": { + "foreign_preservation": { + "reason": "original user and B state/native bytes exact across A lifecycle", + "status": "passed" + }, + "missing_repair": { + "reason": "restored installed A-TWO from unchanged selected local source; remote immutable-pin proof remains separate TLS Git fixture", + "status": "passed" + }, + "remove": { + "reason": "owned skill/member absent; foreign A/user/B bytes retained", + "status": "passed" + }, + "same_name_precedence": { + "reason": "actual rows separate from three UAP receipts; independent active versions/session policy unqualified", + "status": "blocked" + }, + "source_install": { + "reason": "public UAP created exact enabled remote config and scoped receipts; explicit inert security admission", + "status": "passed" + }, + "stable_lifecycle": { + "reason": "replay/update unchanged; intact repair truthfully restages with mutation and exact native bytes", + "status": "passed" + }, + "unique_discovery": { + "reason": "populated exact A/B/user skill paths and default/explicit/cross-cwd/restart unedited enabled source files observed", + "status": "passed" + }, + "update": { + "reason": "same-version update changed owned bytes and receipt; listing read-only", + "status": "passed" + } + }, + "task_home": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home", + "version": "3.0.73" +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cline-project-frontend.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cline-project-frontend.json new file mode 100644 index 000000000..218dd8d74 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cline-project-frontend.json @@ -0,0 +1,170 @@ +{ + "schema_version": 1, + "client": "cline", + "version": "3.0.73", + "commands": [ + { + "probe": "frontend-commit", + "argv": [ + "/usr/bin/git", + "rev-parse", + "HEAD" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.003, + "stdout_file": "frontend-commit.stdout.log", + "stderr_file": "frontend-commit.stderr.log", + "stdout_bytes": 41, + "stderr_bytes": 0, + "stdout_sha256": "2d81485c35c9e316e0c4188ac08f5aea59afd93c277e54f6e1a9b72c828a587f", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "label": "frontend-commit", + "class": "frontend" + }, + { + "probe": "frontend-tree", + "argv": [ + "/usr/bin/git", + "rev-parse", + "HEAD^{tree}" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.003, + "stdout_file": "frontend-tree.stdout.log", + "stderr_file": "frontend-tree.stderr.log", + "stdout_bytes": 41, + "stderr_bytes": 0, + "stdout_sha256": "8f8ef0b6db96c890c9f6875f02077295ef45e0f5b64e8acfb7f9b79dfed77a76", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "label": "frontend-tree", + "class": "frontend" + }, + { + "probe": "frontend-clean", + "argv": [ + "/usr/bin/git", + "status", + "--porcelain", + "--untracked-files=normal" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.146, + "stdout_file": "frontend-clean.stdout.log", + "stderr_file": "frontend-clean.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "label": "frontend-clean", + "class": "frontend" + }, + { + "probe": "frontend-go-version", + "argv": [ + "/opt/hostedtoolcache/go/1.26.10/x64/bin/go", + "version" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.006, + "stdout_file": "frontend-go-version.stdout.log", + "stderr_file": "frontend-go-version.stderr.log", + "stdout_bytes": 33, + "stderr_bytes": 0, + "stdout_sha256": "4e0b213cba780f63b02f3ab9001ee61e4101d254e0af1a8205574c19dad0f93b", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "label": "frontend-go-version", + "class": "frontend" + }, + { + "probe": "frontend-build", + "argv": [ + "/opt/hostedtoolcache/go/1.26.10/x64/bin/go", + "test", + "-p=1", + "-c", + "-trimpath", + "-o", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/bin/cline-project.test", + "./cmd/agentplugins" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins/cli", + "error": null, + "exit": 0, + "elapsed_seconds": 52.757, + "stdout_file": "frontend-build.stdout.log", + "stderr_file": "frontend-build.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 1969, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "9eaeddd556e26dd46e884925eb95a5d3080b31f083dfccdaa1d5279c30898d30", + "label": "frontend-build", + "class": "frontend" + }, + { + "probe": "frontend-gate", + "argv": [ + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/bin/cline-project.test", + "-test.run=^TestClineCurrentSourceNativeProjectConfigLifecycle$", + "-test.count=1", + "-test.v", + "-test.timeout=370s" + ], + "cwd": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr", + "error": null, + "exit": 0, + "elapsed_seconds": 25.771, + "stdout_file": "frontend-gate.stdout.log", + "stderr_file": "frontend-gate.stderr.log", + "stdout_bytes": 136, + "stderr_bytes": 0, + "stdout_sha256": "773c799d435b0a283299c80a8b536d9a08f07920b25c812b6eab063ebb27b89e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "label": "frontend-gate", + "class": "frontend" + } + ], + "collection_complete": true, + "read_only_unique_marker_discovery_qualified": true, + "python_runtime": { + "version": "3.12.3 (main, Aug 31 2026, 10:18:26) [GCC 13.3.0]", + "implementation": "cpython", + "executable": "/usr/bin/python3" + }, + "isolation": "fresh task authority and bounded direct children; no OS sandbox or unrelated network claim", + "native_project_activation_qualified": false, + "native_mcp_initialization_qualified": false, + "native_runtime_qualified": false, + "native_model_use_qualified": false, + "native_ide_qualified": false, + "native_trust_qualified": false, + "same_name_active_isolation_qualified": false, + "native_skill_execution_qualified": false, + "native_authentication_qualified": false, + "native_session_policy_qualified": false, + "source_commit": "3e175de8cfa433f2e9427a47c4e1d7a8e0da5a77", + "source_tree": "72bc516716ae105baffb2dc5a7ec7b8d259f7b95", + "acquisition": { + "url": "https://registry.npmjs.org/@cline/cli-linux-x64/-/cli-linux-x64-3.0.73.tgz", + "version": "3.0.73", + "archive_integrity": "sha512-K+T94s0kPHLoMSANOJJ7KHb/etzYXZGQUbarKDKUPoZsvy4+oD1MAqYnZlfbKFM8tpnRQWouFngD9TB3mFqqeA==", + "archive_bytes": 50817241, + "binary_sha256": "1ff8841d17d4d83763134c193d414e4f7fcf2ef56af759e5ebbe96727fc2c8f7", + "binary_bytes": 132511200, + "installer_executed": false, + "archived_assets_materialized": false + }, + "fixture_executable": { + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/bin/cline-project.test", + "bytes": 34566945, + "sha256": "bb6348504817c187d15d5153d59ae440814ec664ec9241bf14d366ecbe425831" + }, + "cleanup_complete": true +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cross-A-from-B.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cross-A-from-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cross-A-from-B.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cross-A-from-B.stdout.log new file mode 100644 index 000000000..88065fd56 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/cross-A-from-B.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"},{"name":"uap-cline-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-A.stdout.log new file mode 100644 index 000000000..da98962cf --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-A.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"},{"name":"uap-cline-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-B.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-B.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-B.stdout.log new file mode 100644 index 000000000..da98962cf --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/default-B.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"},{"name":"uap-cline-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/foreign-snapshots.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/foreign-snapshots.json new file mode 100644 index 000000000..37e805368 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/foreign-snapshots.json @@ -0,0 +1,20 @@ +{ + "B": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md": "600:120:0fc0f5301310ee74febae1265d7ef1d80846ff9904bee928890f6ca4c8081224", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json": "600:227:b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/data/caller-owned/keep.txt": "600:26:b77832994312f8e62d5faf79448a14c52182809ba284f921025a1544d24bb3a5", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-b-package-c803cde6e469/.agentplugins-cline-native.json": "644:159:71fdbb3669d9bbf16fa3a311c0eda36c099cfd4b6c91312d4401d8b6deb86845", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-b-package-c803cde6e469/mcp.json": "644:198:f306a3ad30031caf2c9fc6e0cefa1e21f80212b8c3d171811669d2a25c48798d", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-b-package-c803cde6e469/plugin.json": "644:119:94a8a14e0858dd89a236faae8a801f1b74d268beb12201b53654366683b2be83", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-b-package-c803cde6e469/skills/uap-cline-b/SKILL.md": "644:144:c5545f22a76081666da3b2352f9e8ea7e445cbcb6d1a775799d452b60b8ba902", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/mutation.lock": "600:61:b66facfdfaa43df8c0dcd07e57f95ebc54115ea9d88da814a56a4a9dfde19b70", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/state-v2.json": "600:5382:f93b27a85055dd91105a6143003378c581813bc0bbd383c8205335673e0b4582" + }, + "user": { + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json": "600:238:0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md": "600:123:088204317df3c936614068eee5eb692467a4ee768f61a692ccbef8f931b7126e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md": "644:145:1fdd7bf55a557e2af72904c7305d032c1f69076a586e0f14222824776edea58e", + "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/installer-state/state-v2.json": "600:5399:dbf8643ddf523c14ca33c7d21e354f917070dd61049cb33befcdc84c3dd027ee" + } +} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-build.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-build.stderr.log new file mode 100644 index 000000000..d7a0b480e --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-build.stderr.log @@ -0,0 +1,36 @@ +go: downloading github.com/spf13/cobra v1.10.2 +go: downloading golang.org/x/term v0.34.0 +go: downloading gopkg.in/yaml.v3 v3.0.1 +go: downloading github.com/spf13/pflag v1.0.9 +go: downloading golang.org/x/text v0.40.0 +go: downloading golang.org/x/sys v0.44.0 +go: downloading charm.land/lipgloss/v2 v2.0.1 +go: downloading github.com/charmbracelet/x/ansi v0.11.6 +go: downloading github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd +go: downloading golang.org/x/sync v0.22.0 +go: downloading github.com/dlclark/regexp2 v1.12.0 +go: downloading github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 +go: downloading golang.org/x/mod v0.40.0 +go: downloading github.com/pelletier/go-toml/v2 v2.3.0 +go: downloading charm.land/bubbles/v2 v2.0.0 +go: downloading charm.land/bubbletea/v2 v2.0.2 +go: downloading charm.land/huh/v2 v2.0.3 +go: downloading github.com/charmbracelet/colorprofile v0.4.2 +go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 +go: downloading github.com/muesli/cancelreader v0.2.2 +go: downloading github.com/charmbracelet/x/term v0.2.2 +go: downloading github.com/clipperhouse/displaywidth v0.11.0 +go: downloading github.com/lucasb-eyer/go-colorful v1.3.0 +go: downloading github.com/rivo/uniseg v0.4.7 +go: downloading github.com/creachadair/tomledit v0.0.29 +go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 +go: downloading github.com/mattn/go-runewidth v0.0.20 +go: downloading github.com/catppuccin/go v0.2.0 +go: downloading github.com/charmbracelet/x/exp/ordered v0.1.0 +go: downloading github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 +go: downloading github.com/mitchellh/hashstructure/v2 v2.0.2 +go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e +go: downloading github.com/charmbracelet/x/termios v0.1.1 +go: downloading github.com/charmbracelet/x/windows v0.2.2 +go: downloading github.com/dustin/go-humanize v1.0.1 +go: downloading github.com/atotto/clipboard v0.1.4 diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-build.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-build.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-clean.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-clean.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-clean.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-clean.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-commit.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-commit.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-commit.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-commit.stdout.log new file mode 100644 index 000000000..334ac145a --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-commit.stdout.log @@ -0,0 +1 @@ +3e175de8cfa433f2e9427a47c4e1d7a8e0da5a77 diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-gate.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-gate.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-gate.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-gate.stdout.log new file mode 100644 index 000000000..db35d5598 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-gate.stdout.log @@ -0,0 +1,3 @@ +=== RUN TestClineCurrentSourceNativeProjectConfigLifecycle +--- PASS: TestClineCurrentSourceNativeProjectConfigLifecycle (25.75s) +PASS diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-go-version.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-go-version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-go-version.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-go-version.stdout.log new file mode 100644 index 000000000..a3cd3c880 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-go-version.stdout.log @@ -0,0 +1 @@ +go version go1.26.10 linux/amd64 diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-tree.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-tree.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-tree.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-tree.stdout.log new file mode 100644 index 000000000..36540f389 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/frontend-tree.stdout.log @@ -0,0 +1 @@ +72bc516716ae105baffb2dc5a7ec7b8d259f7b95 diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-A.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-B.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-B.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-B.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-user.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-user.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-user.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/git-user.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-A-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-A-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-A-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-A-skills.stdout.log new file mode 100644 index 000000000..eb0f707d1 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-A-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-a","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-a","description":"A-ONE","instructions":"A-ONE\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-a","description":"A-ONE","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-B-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-B-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-B-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-B-skills.stdout.log new file mode 100644 index 000000000..f0c85bac4 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/initial-B-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-b","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-b","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-b","description":"B-ONE","instructions":"B-ONE\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-b","description":"B-ONE","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/intact-repair-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/intact-repair-A.stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/intact-repair-A.stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/intact-repair-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/intact-repair-A.stdout.log new file mode 100644 index 000000000..5e7374a16 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/intact-repair-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-c945a5a033240c105254a3ee88f4222e","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-cline-a-package","dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-c945a5a033240c105254a3ee88f4222e","plugin":"uap-cline-a-package","version":"1.0.0","source":"local","tree_digest":"sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-a-package-0f8e27b9e447","components":[{"kind":"skill","name":"uap-cline-a","support":"native"},{"kind":"mcp_server","name":"uap-cline-a","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/missing-repair-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/missing-repair-A.stderr.log new file mode 100644 index 000000000..b82b6aebd --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/missing-repair-A.stderr.log @@ -0,0 +1 @@ +Resolving and validating each unique exact installed package revision once... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/missing-repair-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/missing-repair-A.stdout.log new file mode 100644 index 000000000..c5e0e085c --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/missing-repair-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"repair","result":"success","data":{"operation_id":"op-2762684cd8aa43a1703e699b7bef069a","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-cline-a-package","dry_run":false,"targets":[{"target":"cline","status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-2762684cd8aa43a1703e699b7bef069a","plugin":"uap-cline-a-package","version":"1.0.0","source":"local","tree_digest":"sha256:ca4909cb3edebedda2c797087bd292a2c4637764a34c1db1513ce81c0d8f8df0","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-a-package-0f8e27b9e447","components":[{"kind":"skill","name":"uap-cline-a","support":"native"},{"kind":"mcp_server","name":"uap-cline-a","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-A.stdout.log new file mode 100644 index 000000000..88065fd56 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-A.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"},{"name":"uap-cline-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-B.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-B.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-B.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-B.stdout.log new file mode 100644 index 000000000..f91a759ec --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/override-B.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-b","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json"},{"name":"uap-cline-b","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-A-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-A-config.json new file mode 100644 index 000000000..dfbc89272 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-A-config.json @@ -0,0 +1,3 @@ +{"foreignPreference":"retain-a","mcpServers":{"foreign-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-a"}}, + "uap-cline-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/A-ONE"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-A-inspection.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-A-inspection.json new file mode 100644 index 000000000..70366d633 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-A-inspection.json @@ -0,0 +1,40 @@ +{ + "StateRoot": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap", + "Installations": [ + { + "SourceKey": "github::", + "InstallationID": "942c3806-570d-438c-8bc1-5e1cceeb96e2", + "TreeDigest": "sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa", + "Bindings": [ + { + "LifecycleConverged": false, + "ClientID": "cline", + "BindingID": "client_4003f71d041d0b85c58efec9", + "Scope": "project", + "TargetPath": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/uap-cline-a-package-0f8e27b9e447", + "DataRoot": "", + "TreeDigest": "sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa", + "Materialization": "materialized", + "Activation": "prepared", + "Authentication": "not_checked", + "Verification": "installation_verified", + "Revision": "", + "Owner": null, + "ProjectionDigest": "sha256:2eac5d87b4761f76d4c2f048c6e4bb142f7ec87cb61a83188b35db774021d323", + "ProjectionStatus": "" + } + ], + "DataRetained": false, + "DataRoots": null + } + ], + "Recovery": { + "StateDigest": "sha256:cae27516253bc7aa025ea1fe1ca352288b005919ecda664e5cc5d3f410ca2493", + "Required": false, + "Journals": null, + "NativeIntents": null, + "Receipts": null, + "Reason": "" + }, + "EnvironmentReceipts": null +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-B-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-B-config.json new file mode 100644 index 000000000..4b79a29e3 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-B-config.json @@ -0,0 +1,3 @@ +{"foreignPreference":"retain-b","mcpServers":{"foreign-b":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-b"}}, + "uap-cline-b":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/B-ONE"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-B-inspection.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-B-inspection.json new file mode 100644 index 000000000..d3ad9e514 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-B-inspection.json @@ -0,0 +1,40 @@ +{ + "StateRoot": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap", + "Installations": [ + { + "SourceKey": "github::", + "InstallationID": "9442d271-4751-4ba1-9d4a-b3660dbca4aa", + "TreeDigest": "sha256:a9537359413e1f94ab67eb0fcd1f7547a4788e9e4ec27ded5baffb723d3e27d5", + "Bindings": [ + { + "LifecycleConverged": false, + "ClientID": "cline", + "BindingID": "client_88115bd9e9fa8dff6f442190", + "Scope": "project", + "TargetPath": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/managed/clients/cline/uap-cline-b-package-c803cde6e469", + "DataRoot": "", + "TreeDigest": "sha256:a9537359413e1f94ab67eb0fcd1f7547a4788e9e4ec27ded5baffb723d3e27d5", + "Materialization": "materialized", + "Activation": "prepared", + "Authentication": "not_checked", + "Verification": "installation_verified", + "Revision": "", + "Owner": null, + "ProjectionDigest": "sha256:40e5b6c0ec03a3ee5806d69d4f1aa70a1ef39976a3442f2f5708365e755c41eb", + "ProjectionStatus": "" + } + ], + "DataRetained": false, + "DataRoots": null + } + ], + "Recovery": { + "StateDigest": "sha256:a8e64c658228c8cd6f66c7c7c767ec3c90102d961469b667da125fda78ca48d3", + "Required": false, + "Journals": null, + "NativeIntents": null, + "Receipts": null, + "Reason": "" + }, + "EnvironmentReceipts": null +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-user-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-user-config.json new file mode 100644 index 000000000..d672becfb --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/prepared-user-config.json @@ -0,0 +1,3 @@ +{"foreignPreference":"retain-user","mcpServers":{"foreign-user":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-user"}}, + "uap-cline-user":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/USER"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/provenance.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/provenance.json new file mode 100644 index 000000000..d42747eeb --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/provenance.json @@ -0,0 +1,439 @@ +{ + "run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40182752894", + "job_id": 121137092632, + "artifact_id": 12596067396, + "artifact_name": "cline-3.0.73-current-source-enabled-project-config-linux-amd64", + "outer_bytes": 22632, + "outer_sha256": "0537bb6a4e4b551bb06c34ad43ff89446f3a2f418c6c8595212035f3779d7f09", + "inner_bytes": 296350, + "inner_sha256": "2019c2cfe15d26f9b0bbb199da915d66e4d0cc5db1a424d863942d4eba0d71ea", + "source_commit": "3e175de8cfa433f2e9427a47c4e1d7a8e0da5a77", + "source_tree": "72bc516716ae105baffb2dc5a7ec7b8d259f7b95", + "members": { + "add-A.stderr.log": { + "bytes": 136, + "sha256": "dd6461f8484691e52c746ee11a2ce659a5f7fa22db1009904ecb368d88f5918a" + }, + "add-A.stdout.log": { + "bytes": 5476, + "sha256": "85b99da05aba6479b16a424b74d63c7c9544989ce92d3288f006bdd307a8db1b" + }, + "add-B.stderr.log": { + "bytes": 136, + "sha256": "9e3202447bb884a8f418ec9323b92f5f6a3f5c6fcea070215421e12c5adeae79" + }, + "add-B.stdout.log": { + "bytes": 5476, + "sha256": "3ec933466631ec9d33385f0f98990f61970882f9c2c105b6d926772ac8a5767e" + }, + "add-same-A.stderr.log": { + "bytes": 131, + "sha256": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131" + }, + "add-same-A.stdout.log": { + "bytes": 5471, + "sha256": "2bc8a862fabf84b588f72499610587a88e54de8ffd48a3e83d4da61ebd1cd6fe" + }, + "add-same-B.stderr.log": { + "bytes": 131, + "sha256": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131" + }, + "add-same-B.stdout.log": { + "bytes": 5471, + "sha256": "56b40aaedd56d8706a1699929a84b60b245e7eb3b3ce87e0980af0e0e6b7d725" + }, + "add-same-user.stderr.log": { + "bytes": 131, + "sha256": "639b8f8d1bff1da9ad4e2b85c3f80982fa01790e497717798526d37de64cc131" + }, + "add-same-user.stdout.log": { + "bytes": 2533, + "sha256": "e10ded4887e3019038f12685562d668728a185d23dbb4853e785ed016d4f7fdf" + }, + "add-user.stderr.log": { + "bytes": 139, + "sha256": "4f0096de4eaa9ddeecbcee07c58e9cebfc53a03f6dc0c753ed1b9a53c7e05795" + }, + "add-user.stdout.log": { + "bytes": 2553, + "sha256": "8a5b94898a35297768f7f1451735019ec9392d452ed5c29a8c2f064d6f86732b" + }, + "cleanup.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cleanup.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cline-project-evidence.json": { + "bytes": 191230, + "sha256": "4fa02d5f6e3017bb7e78049c1c20168fb6d5a0b99a28a18f973b8bc0ac30b633" + }, + "cline-project-frontend.json": { + "bytes": 6235, + "sha256": "e82113ffd1dc172d55721506d4a6773dcea60f893166d41e7bd3605717643762" + }, + "cross-A-from-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cross-A-from-B.stdout.log": { + "bytes": 411, + "sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61" + }, + "default-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "default-A.stdout.log": { + "bytes": 457, + "sha256": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66" + }, + "default-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "default-B.stdout.log": { + "bytes": 457, + "sha256": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66" + }, + "foreign-snapshots.json": { + "bytes": 3180, + "sha256": "d0fe9852a1fa292f34994740dc691dda32e268d202278c06264a190bd532d626" + }, + "frontend-build.stderr.log": { + "bytes": 1969, + "sha256": "9eaeddd556e26dd46e884925eb95a5d3080b31f083dfccdaa1d5279c30898d30" + }, + "frontend-build.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-clean.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-clean.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-commit.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-commit.stdout.log": { + "bytes": 41, + "sha256": "2d81485c35c9e316e0c4188ac08f5aea59afd93c277e54f6e1a9b72c828a587f" + }, + "frontend-gate.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-gate.stdout.log": { + "bytes": 136, + "sha256": "773c799d435b0a283299c80a8b536d9a08f07920b25c812b6eab063ebb27b89e" + }, + "frontend-go-version.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-go-version.stdout.log": { + "bytes": 33, + "sha256": "4e0b213cba780f63b02f3ab9001ee61e4101d254e0af1a8205574c19dad0f93b" + }, + "frontend-tree.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend-tree.stdout.log": { + "bytes": 41, + "sha256": "8f8ef0b6db96c890c9f6875f02077295ef45e0f5b64e8acfb7f9b79dfed77a76" + }, + "git-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "git-A.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "git-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "git-B.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "git-user.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "git-user.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "initial-A-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "initial-A-skills.stdout.log": { + "bytes": 1432, + "sha256": "576a276260670749579c2b68e5f6d3bb7cfd0a712e360209df60a8a08efbcb38" + }, + "initial-B-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "initial-B-skills.stdout.log": { + "bytes": 1432, + "sha256": "bc83d557e031876dc0de38ef132d574152c8ec8f594630708bd690f95de4de2e" + }, + "intact-repair-A.stderr.log": { + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + "intact-repair-A.stdout.log": { + "bytes": 4902, + "sha256": "028e7fe66734ff032cc968c4bb1373fb5c6efa774ec3be9c1f87038cac06265f" + }, + "missing-repair-A.stderr.log": { + "bytes": 78, + "sha256": "c07f022606d872579b151926f65c51b12e6e35041e3a0cb40c11dad0da08901f" + }, + "missing-repair-A.stdout.log": { + "bytes": 4902, + "sha256": "b0c88a36ab0afff7aeb391f370e2ff40a77407d288a1fde5cfc294441ebf8949" + }, + "override-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "override-A.stdout.log": { + "bytes": 411, + "sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61" + }, + "override-B.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "override-B.stdout.log": { + "bytes": 411, + "sha256": "a2482086d7db1f2bd8b50fb774872828be42448c48ddba62327707132917ec42" + }, + "prepared-A-config.json": { + "bytes": 227, + "sha256": "12c8e2cdce352431e53f69055a115fc0aaa2c6878e0b54fdb6b7d39c34c2a3f5" + }, + "prepared-A-inspection.json": { + "bytes": 1539, + "sha256": "10781645be342d10bd404f24b8a2101a8a9f16ab2d99aa1be1c431e2b6122e15" + }, + "prepared-B-config.json": { + "bytes": 227, + "sha256": "b87ad427a7460711b0ef826e59d2ed197e1897d9ec73325618c12f31d609f3ad" + }, + "prepared-B-inspection.json": { + "bytes": 1539, + "sha256": "02d4efad79f9427d27cb731a28c044e732804fde3f57d335c720e2ed96888550" + }, + "prepared-user-config.json": { + "bytes": 238, + "sha256": "0066b578cd9f4185e501337b65b75c4989565c0ed77582dd52b93d3c4d764717" + }, + "remove-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "remove-A.stdout.log": { + "bytes": 1700, + "sha256": "853b271621cf0a65c4d88c9e9d5d5568ada990f708ad2fb648cd324f6d466cc7" + }, + "removed-A-config.json": { + "bytes": 137, + "sha256": "803bd4f4a6410babd14711cae3743bd73ed6f157a8a4758ff13318474f52f437" + }, + "removed-A-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "removed-A-mcp.stdout.log": { + "bytes": 205, + "sha256": "a07ddb0e9fa1ab77c2be91175a58a2d716d504ee7abbd9fda465e31c07ac00c4" + }, + "removed-A-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "removed-A-skills.stdout.log": { + "bytes": 1070, + "sha256": "cc86a473154a1c8c79e0557e9a1f7e048b1621f0ad6a2e03746287c4563f53b1" + }, + "repaired-A-config.json": { + "bytes": 227, + "sha256": "732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6" + }, + "repaired-A-inspection.json": { + "bytes": 1539, + "sha256": "b12d82d94a561924c8bb309e7e9ca7a447784b6d7a2f67fad28bea80702e5bbb" + }, + "repaired-A-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "repaired-A-mcp.stdout.log": { + "bytes": 411, + "sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61" + }, + "repaired-A-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "repaired-A-skills.stdout.log": { + "bytes": 1432, + "sha256": "c3fb38e749e98677be46ec4e19e2132641be44c1a3c9846ae58979e37c7b572f" + }, + "repeat-add-A.stderr.log": { + "bytes": 120, + "sha256": "c2908b301e4d5b9f819536bcb66bdc310bf8b94a4d6be576837cd1ceda9df9c4" + }, + "repeat-add-A.stdout.log": { + "bytes": 3942, + "sha256": "ae537da7b41ca039292dfa5338e53a5af053dfa8acb142fb25ec44aafaedc020" + }, + "restart-default-A.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "restart-default-A.stdout.log": { + "bytes": 457, + "sha256": "80c1df09a6bd26ea3bd59c9d0931fee8efd30cf42c5fed015bf5d1bc3ec18a66" + }, + "same-A-config.json": { + "bytes": 233, + "sha256": "f2dd419a75f39c4444df6edba1c80b425de2ad7f7431e85106d5e1697297137d" + }, + "same-A-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "same-A-mcp.stdout.log": { + "bytes": 416, + "sha256": "13aecb38b3b9e85818774e09b181ec219639dad94858028ecb07309aa73b8b1f" + }, + "same-A-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "same-A-skills.stdout.log": { + "bytes": 1462, + "sha256": "5deecffa34a751810e18acdd38f3064e798e0ce1563434caf99e8101aafd68e0" + }, + "same-B-config.json": { + "bytes": 323, + "sha256": "8d985e0f1b162d6cf7e3e2750df83039e535e9d1ff9b243f8f9baf699c6754c3" + }, + "same-B-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "same-B-mcp.stdout.log": { + "bytes": 622, + "sha256": "1097641eefe915af9ce751949357afebe65f88d708718adeacfddc27ac2eddc2" + }, + "same-B-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "same-B-skills.stdout.log": { + "bytes": 1824, + "sha256": "4fe7a104b69a1a8bc0930fbc61345e1c47c2c0a9c421da6ec713d860c0ccdb9c" + }, + "same-name-winners.json": { + "bytes": 1496, + "sha256": "073738b182babc90afd69b8bd311a0b69017f0e89c715f32f42539ac1e700006" + }, + "same-user-config.json": { + "bytes": 337, + "sha256": "54dac92b2b2b364e4933f7104093683ba51b418afcf9e6adef6445b0c72339f7" + }, + "same-user-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "same-user-mcp.stdout.log": { + "bytes": 688, + "sha256": "547a8a32ce773235bcb1de5566c2dbf20bb31c07a43bba4d29ad648a98c16704" + }, + "same-user-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "same-user-skills.stdout.log": { + "bytes": 1119, + "sha256": "706221aa7420cf7ee365961288d190cfcf9749b54eb6a54cfde6a4ab8bb70a71" + }, + "stable-A-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stable-A-mcp.stdout.log": { + "bytes": 411, + "sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61" + }, + "stable-A-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stable-A-skills.stdout.log": { + "bytes": 1432, + "sha256": "576a276260670749579c2b68e5f6d3bb7cfd0a712e360209df60a8a08efbcb38" + }, + "unchanged-update-A.stderr.log": { + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + "unchanged-update-A.stdout.log": { + "bytes": 5087, + "sha256": "42de5d239ec8d81724632f94127980b3be7488e9559d0c5c888b4467dc31bbd9" + }, + "update-A.stderr.log": { + "bytes": 87, + "sha256": "e75a15f135db1fcd94fff31932040c95267684d88d3f33f5bbd9b01999940d10" + }, + "update-A.stdout.log": { + "bytes": 5069, + "sha256": "a449175240578e352adc13111a5fceb4782cd288c0b95473ea3dcf01b4ce8754" + }, + "updated-A-config.json": { + "bytes": 227, + "sha256": "732f50fdd9ed5f377bdf7d9bc6461eefd38bae642e0cc78ef769abef92f874c6" + }, + "updated-A-mcp.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "updated-A-mcp.stdout.log": { + "bytes": 411, + "sha256": "eb3132cf157d1eeb03acbafe5993abdea6f355ea7b6108e62312e23698af4f61" + }, + "updated-A-skills.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "updated-A-skills.stdout.log": { + "bytes": 1432, + "sha256": "c3fb38e749e98677be46ec4e19e2132641be44c1a3c9846ae58979e37c7b572f" + }, + "version.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "version.stdout.log": { + "bytes": 7, + "sha256": "942a33503b6a87e2f85be82d79fc3ced6c59ebd1b901ba309db8d04da494ffc7" + } + }, + "qualification": "read-only native marker metadata plus current-source UAP file lifecycle only" +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/remove-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/remove-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/remove-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/remove-A.stdout.log new file mode 100644 index 000000000..d1689abeb --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/remove-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"remove","result":"success","data":{"operation_id":"op-1349e39b7887d311ada6791ceaf1621a","batch":true,"status":"data_retained","succeeded":1,"failed":0,"plugin":"uap-cline-a-package","plugin_data_preserved":true,"data_retained":true,"retained_data":[{"data_receipt_id":"data_e967b2294e6ba09ee194d0c0","physical_backend_id":"uap-cline-a-package-0f8e27b9e447","scope":"project","state":"owned"}],"retained_data_action":"run `agentplugins remove 942c3806-570d-438c-8bc1-5e1cceeb96e2 --purge-data --scope project --project-root /home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A` to delete retained plugin data","dry_run":false,"targets":[{"target":"cline","status":"external_completed","output":{"operation_id":"op-1349e39b7887d311ada6791ceaf1621a","plugin":"uap-cline-a-package","version":"1.0.0","source":"local","tree_digest":"sha256:ca4909cb3edebedda2c797087bd292a2c4637764a34c1db1513ce81c0d8f8df0","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","next_action":"Start a fresh Cline CLI process to observe removal; existing sessions may retain previously selected MCP configuration","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plugin":"uap-cline-a-package","client_id":"cline","requires_confirmation":false,"mutated":true,"deactivation":{"activation":"not_required","artifact_removal_allowed":true,"external_removal_complete":true,"user_actions":["Start a fresh Cline CLI process to observe removal; existing sessions may retain previously selected MCP configuration"]},"affected_surfaces":["cline"],"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-config.json new file mode 100644 index 000000000..e32c90a49 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-config.json @@ -0,0 +1,2 @@ +{"foreignPreference":"retain-a","mcpServers":{"foreign-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-a"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-mcp.stdout.log new file mode 100644 index 000000000..10353c750 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-skills.stdout.log new file mode 100644 index 000000000..bf42f29b6 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/removed-A-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-a","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-config.json new file mode 100644 index 000000000..8ccbb7ba7 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-config.json @@ -0,0 +1,3 @@ +{"foreignPreference":"retain-a","mcpServers":{"foreign-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-a"}}, + "uap-cline-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/A-TWO"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-inspection.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-inspection.json new file mode 100644 index 000000000..bdb22fdb2 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-inspection.json @@ -0,0 +1,40 @@ +{ + "StateRoot": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap", + "Installations": [ + { + "SourceKey": "github::", + "InstallationID": "942c3806-570d-438c-8bc1-5e1cceeb96e2", + "TreeDigest": "sha256:ca4909cb3edebedda2c797087bd292a2c4637764a34c1db1513ce81c0d8f8df0", + "Bindings": [ + { + "LifecycleConverged": false, + "ClientID": "cline", + "BindingID": "client_4003f71d041d0b85c58efec9", + "Scope": "project", + "TargetPath": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/managed/clients/cline/uap-cline-a-package-0f8e27b9e447", + "DataRoot": "", + "TreeDigest": "sha256:ca4909cb3edebedda2c797087bd292a2c4637764a34c1db1513ce81c0d8f8df0", + "Materialization": "materialized", + "Activation": "prepared", + "Authentication": "not_checked", + "Verification": "installation_verified", + "Revision": "", + "Owner": null, + "ProjectionDigest": "sha256:46777957f3cfbf041e4bb99b89a685bcf922df069ac91f3f1e63b3b792bec28b", + "ProjectionStatus": "" + } + ], + "DataRetained": false, + "DataRoots": null + } + ], + "Recovery": { + "StateDigest": "sha256:4a674f3417f4a92a96584e05016b9f0e95c4ece0c316358517b13ee1948191fd", + "Required": false, + "Journals": null, + "NativeIntents": null, + "Receipts": null, + "Reason": "" + }, + "EnvironmentReceipts": null +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-mcp.stdout.log new file mode 100644 index 000000000..88065fd56 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"},{"name":"uap-cline-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-skills.stdout.log new file mode 100644 index 000000000..f6d43247b --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repaired-A-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-a","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-a","description":"A-TWO","instructions":"A-TWO\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-a","description":"A-TWO","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repeat-add-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repeat-add-A.stderr.log new file mode 100644 index 000000000..4342083ba --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repeat-add-A.stderr.log @@ -0,0 +1,2 @@ +Resolving and validating one Agent Plugin package for every selected target... +Applying transactional client package... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repeat-add-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repeat-add-A.stdout.log new file mode 100644 index 000000000..c7d315738 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/repeat-add-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"add","result":"success","data":{"plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","security":{"schema_version":0,"subject":{"tree_digest":"","manifest_digest":""},"scanner":{"id":"","version":""},"policy":{"id":"","version":0,"digest":""},"outcome":"","counts":{"blocking":0,"warnings":0,"total":0},"scanned_files":0,"report_digest":""},"next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-a-package-0f8e27b9e447","components":[{"kind":"skill","name":"uap-cline-a","support":"native"},{"kind":"mcp_server","name":"uap-cline-a","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":false}}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/restart-default-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/restart-default-A.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/restart-default-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/restart-default-A.stdout.log new file mode 100644 index 000000000..da98962cf --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/restart-default-A.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"},{"name":"uap-cline-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-config.json new file mode 100644 index 000000000..6701c8db8 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-config.json @@ -0,0 +1,3 @@ +{"foreignPreference":"retain-a","mcpServers":{"foreign-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-a"}}, + "uap-cline-shared":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/SAME-A"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-mcp.stdout.log new file mode 100644 index 000000000..b03ee9d75 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"},{"name":"uap-cline-shared","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-skills.stdout.log new file mode 100644 index 000000000..8364a4c61 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-A-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-a","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-shared","description":"SAME-USER","instructions":"SAME-USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-shared","description":"SAME-USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-config.json new file mode 100644 index 000000000..abd9ec7fc --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-config.json @@ -0,0 +1,4 @@ +{"foreignPreference":"retain-b","mcpServers":{"foreign-b":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-b"}}, + "uap-cline-b":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/B-ONE"}}, + "uap-cline-shared":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/SAME-B"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-mcp.stdout.log new file mode 100644 index 000000000..eb7d5698a --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-b","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json"},{"name":"uap-cline-b","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json"},{"name":"uap-cline-shared","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-skills.stdout.log new file mode 100644 index 000000000..09ce0b4a0 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-B-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-b","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-b","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/foreign-b/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-b","description":"B-ONE","instructions":"B-ONE\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-b","description":"B-ONE","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/B/.cline/skills/uap-cline-b/SKILL.md"},{"name":"uap-cline-shared","description":"SAME-USER","instructions":"SAME-USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-shared","description":"SAME-USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-name-winners.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-name-winners.json new file mode 100644 index 000000000..f7efd3247 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-name-winners.json @@ -0,0 +1,41 @@ +{ + "same-A": [ + { + "description": "SAME-USER", + "frontmatter": { + "description": "SAME-USER", + "disable-model-invocation": true, + "name": "uap-cline-shared" + }, + "instructions": "SAME-USER\nInert text only. No command, model, hook or server execution.", + "name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md" + } + ], + "same-B": [ + { + "description": "SAME-USER", + "frontmatter": { + "description": "SAME-USER", + "disable-model-invocation": true, + "name": "uap-cline-shared" + }, + "instructions": "SAME-USER\nInert text only. No command, model, hook or server execution.", + "name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md" + } + ], + "same-user": [ + { + "description": "SAME-USER", + "frontmatter": { + "description": "SAME-USER", + "disable-model-invocation": true, + "name": "uap-cline-shared" + }, + "instructions": "SAME-USER\nInert text only. No command, model, hook or server execution.", + "name": "uap-cline-shared", + "path": "/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md" + } + ] +} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-config.json new file mode 100644 index 000000000..8838d47ce --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-config.json @@ -0,0 +1,4 @@ +{"foreignPreference":"retain-user","mcpServers":{"foreign-user":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-user"}}, + "uap-cline-user":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/USER"}}, + "uap-cline-shared":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/SAME-USER"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-mcp.stdout.log new file mode 100644 index 000000000..d924387c5 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"},{"name":"uap-cline-shared","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"},{"name":"uap-cline-user","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-skills.stdout.log new file mode 100644 index 000000000..5038e9383 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/same-user-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-shared","description":"SAME-USER","instructions":"SAME-USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-shared","description":"SAME-USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-shared/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-mcp.stdout.log new file mode 100644 index 000000000..88065fd56 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"},{"name":"uap-cline-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-skills.stdout.log new file mode 100644 index 000000000..eb0f707d1 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/stable-A-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-a","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-a","description":"A-ONE","instructions":"A-ONE\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-a","description":"A-ONE","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/unchanged-update-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/unchanged-update-A.stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/unchanged-update-A.stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/unchanged-update-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/unchanged-update-A.stdout.log new file mode 100644 index 000000000..1dc750267 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/unchanged-update-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-620b268c42d9f3677a04b2da8ea10f93","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa","dry_run":false,"targets":[{"target":"cline","selected":true,"status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-620b268c42d9f3677a04b2da8ea10f93","plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:07bf6cce7eef0891e383ecbdfe1972d3cadb9fc3aaaed3b6d52015abc59c8efa","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-a-package-0f8e27b9e447","components":[{"kind":"skill","name":"uap-cline-a","support":"native"},{"kind":"mcp_server","name":"uap-cline-a","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":false,"no_change":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/update-A.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/update-A.stderr.log new file mode 100644 index 000000000..25557ffd1 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/update-A.stderr.log @@ -0,0 +1 @@ +Resolving and validating one updated Agent Plugin package for every selected target... diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/update-A.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/update-A.stdout.log new file mode 100644 index 000000000..f7a1b9e95 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/update-A.stdout.log @@ -0,0 +1 @@ +{"schema_version":1,"command":"update","result":"success","data":{"operation_id":"op-3af51c63be4b64628742cecdba06ed1d","batch":true,"status":"completed","succeeded":1,"failed":0,"plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:ca4909cb3edebedda2c797087bd292a2c4637764a34c1db1513ce81c0d8f8df0","dry_run":false,"targets":[{"target":"cline","selected":true,"status":"external_completed","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","output":{"operation_id":"op-3af51c63be4b64628742cecdba06ed1d","plugin":"uap-cline-a-package","version":"1.0.0","source":"direct local source","tree_digest":"sha256:ca4909cb3edebedda2c797087bd292a2c4637764a34c1db1513ce81c0d8f8df0","manifest_digest":"sha256:7fe9154a96717dae0c4835ddea1519539bf334c9c808f6d5e7142350e0314279","next_action":"After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source; verify this plugin's authentication requirements before using it","dry_run":false,"result":{"installation_id":"942c3806-570d-438c-8bc1-5e1cceeb96e2","plan":{"client_id":"cline","scope":"project","status":"ready","package_mode":"native","activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"package_validated","physical_artifact_id":"uap-cline-a-package-0f8e27b9e447","components":[{"kind":"skill","name":"uap-cline-a","support":"native"},{"kind":"mcp_server","name":"uap-cline-a","support":"native"}],"user_actions":["verify the plugin's authentication requirements before using it","After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"],"warnings":["authentication_not_catalog_verified"]},"activation":{"activation":"prepared","authentication":"not_checked","policy":"allowed","verification":"installation_verified","user_actions":["After Cline project component files are installed, start Cline from working directory \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\" and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved. For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to \"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json\" and CLINE_NO_AUTO_UPDATE to 1, and use arguments [\"--auto-approve\",\"false\",\"--cwd\",\"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A\"]. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source"]},"requires_confirmation":false,"mutated":true,"group_phase":"external_completed"}}}]}} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-config.json b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-config.json new file mode 100644 index 000000000..8ccbb7ba7 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-config.json @@ -0,0 +1,3 @@ +{"foreignPreference":"retain-a","mcpServers":{"foreign-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/FOREIGN-a"}}, + "uap-cline-a":{"transport":{"type":"streamableHttp","url":"http://127.0.0.1:9/A-TWO"}} +}} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-mcp.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-mcp.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-mcp.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-mcp.stdout.log new file mode 100644 index 000000000..88065fd56 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-mcp.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"},{"name":"uap-cline-a","transportType":"streamableHttp","disabled":false,"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-skills.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-skills.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-skills.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-skills.stdout.log new file mode 100644 index 000000000..f6d43247b --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/updated-A-skills.stdout.log @@ -0,0 +1 @@ +[{"name":"foreign-a","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-a","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/foreign-a/SKILL.md"},{"name":"foreign-user","description":"Foreign fixture","instructions":"Retain exact unrelated inert bytes.","frontmatter":{"name":"foreign-user","description":"Foreign fixture","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/foreign-user/SKILL.md"},{"name":"uap-cline-a","description":"A-TWO","instructions":"A-TWO\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-a","description":"A-TWO","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/A/.cline/skills/uap-cline-a/SKILL.md"},{"name":"uap-cline-user","description":"USER","instructions":"USER\nInert text only. No command, model, hook or server execution.","frontmatter":{"name":"uap-cline-user","description":"USER","disable-model-invocation":true},"path":"/home/runner/work/_temp/uap-cline-project-gate-soqc_vrr/fixtures/uap-cline-project-4114749539/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/version.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/version.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/version.stdout.log new file mode 100644 index 000000000..96e7c9550 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/version.stdout.log @@ -0,0 +1 @@ +3.0.73 diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/README.md b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/README.md new file mode 100644 index 000000000..50d050bfb --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/README.md @@ -0,0 +1,69 @@ +# Cline 3.0.73: genuine hosted configuration observations + +Recorded and independently interpreted on 2026-10-24. This is historical +pre-adapter research, measured at source +`bb8c89bf62542096a0af3aff26f21a16fee4d595`, from +[run 40017958131](https://github.com/777genius/universal-agent-plugins/actions/runs/40017958131) +and [job 120634802143](https://github.com/777genius/universal-agent-plugins/actions/runs/40017958131/job/120634802143). +The successful artifact is `cline-3.0.73-precode-config-linux-amd64`, ID +12532307825. Its ZIP was 9,915 bytes, SHA256 +`cec4b3dd3bf2aa7a8572c7d517f06c3529837d5e01445326d00d4a2d4f27c68c`. +The 29 flat text members are preserved byte-for-byte here; the binary ZIP is +not committed. `provenance.json` records each member's length and SHA256. + +## Verification and source attribution + +The implementing worker and root independently checked all 12 native stdout +and stderr length/hash pairs, exact native version 3.0.73, successful process +results, clean JSON-command stderr and the before/after fixture inventories. +The worker also checked both Git-preparation stream pairs. All 28 recorded +streams match. The inventories are equal; native-created state is empty. +The original summary remains unchanged, including both false qualification +fields. No native process or acquisition was rerun during this interpretation. + +The Cline collector, acquisition pins, dedicated workflow and Grok inventory/ +budget module are byte-identical to comparison checkpoint +`4cba9df5cab26f12fafa4c0e68a119953485e652`. The imported shared capture helper +is **different**: the current reviewed helper enforces separate live stream +caps. The imported native matrix module also differs through added project +suite selection; its hosted admission function was not changed by that diff. +`provenance.json` records every compared blob. These observations belong to the +historical source closure, not the current head's native gate. + +## Observed native behavior + +All fixture registrations are disabled remote loopback markers; skills contain +inert text. Every child used literal `CLINE_NO_AUTO_UPDATE=1`, selected cwd and +`--auto-approve false`. The following table reports actual populated paths; +`` is `/tmp/cline-native-research-3c04fr3x` in the raw output. + +| Probe | Actual native selection | Consequence for the adapter | +| --- | --- | --- | +| `default-a`, `default-b`, `default-a-again` | Only `uap-cline-user` at `/home/.cline/data/settings/cline_mcp_settings.json` | Working directory alone did not select either prepared project MCP file. A later ordinary process returned to the user source. | +| `override-a` | `uap-cline-a` at `/project-a/.uap/clients/cline/mcp.json` | An explicit process override can select A's prepared file. | +| `override-b` | `uap-cline-b` at `/project-b/.uap/clients/cline/mcp.json` | An explicit process override can select B's prepared file. | +| `cross-a-from-b` | A's marker and A's exact file, while cwd is B | MCP source selection follows the override independently of cwd. A launch action must fix both. | +| `skills-a` | A's exact `.cline/skills/uap-cline-a/SKILL.md` and the user skill | Native skill inventory found A plus user and omitted B. | +| `skills-b` | B's exact `.cline/skills/uap-cline-b/SKILL.md` and the user skill | Native skill inventory found B plus user and omitted A. | + +MCP rows contain `name`, `transportType`, `disabled` and `path`. A/user uses +`streamableHttp`; B uses `sse`; all are disabled. Skills contain actual path, +frontmatter/instructions and description. The exact-tag primary implementation +supports those observed config branches: +[config.ts](https://github.com/cline/cline/blob/cli-v3.0.73/apps/cli/src/commands/config.ts). +An empty skills list would not establish discovery because that source catches +some enumeration errors; the positive path-bearing markers here matter. + +## Independent interpretation + +Both `skillshare_scope` and root concluded **GO for a concrete post-observation +project adapter plan only**. The observed component boundary is project skill +files plus explicitly process-selected prepared MCP configuration. It does not +establish a native project plugin registry or ordinary project MCP startup. + +No MCP initialize/tool call, enabled skill execution, IDE behavior, trust, +authentication, model request, hook/JS/TS plugin, ancestor lookup, same-name +user/project precedence or runtime network isolation is proved. Unique markers +were used; do not infer conflict precedence from their coexistence. UAP +installation/receipts/update/repair/remove were not exercised by this collector. +A new current-source hosted fixture remains necessary for those product claims. diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cline-precode-research.json b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cline-precode-research.json new file mode 100644 index 000000000..f8bc27f51 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cline-precode-research.json @@ -0,0 +1,935 @@ +{ + "evidence_kind": "cline-precode-read-only-config-observations", + "version": "3.0.73", + "target": "linux-amd64", + "source_commit": "bb8c89bf62542096a0af3aff26f21a16fee4d595", + "expected_probes": [ + "version", + "help", + "config-help", + "mcp-help", + "default-a", + "default-b", + "override-a", + "override-b", + "cross-a-from-b", + "default-a-again", + "skills-a", + "skills-b" + ], + "probes": [ + { + "probe": "version", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "--version" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.896, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 7, + "stderr_bytes": 0, + "stdout_sha256": "942a33503b6a87e2f85be82d79fc3ced6c59ebd1b901ba309db8d04da494ffc7", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "help", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "--help" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.936, + "stdout_file": "help.stdout.log", + "stderr_file": "help.stderr.log", + "stdout_bytes": 3993, + "stderr_bytes": 0, + "stdout_sha256": "04d834c4ae0cc8ea0bf304351a54a4d534acfb52f92c40fa6e28bd290ddef651", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "config-help", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "config", + "--help" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.938, + "stdout_file": "config-help.stdout.log", + "stderr_file": "config-help.stderr.log", + "stdout_bytes": 186, + "stderr_bytes": 0, + "stdout_sha256": "f251998cd21fa6e94730770601b6081e279a79312f98102d3445a5ef983358cf", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "mcp-help", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "mcp", + "--help" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.874, + "stdout_file": "mcp-help.stdout.log", + "stderr_file": "mcp-help.stderr.log", + "stdout_bytes": 332, + "stderr_bytes": 0, + "stdout_sha256": "104e1a4b531a169d65d77511a9d8b9b1140dd5e33534274c12cd1c7fa7838563", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "default-a", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "config", + "mcp", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.917, + "stdout_file": "default-a.stdout.log", + "stderr_file": "default-a.stderr.log", + "stdout_bytes": 171, + "stderr_bytes": 0, + "stdout_sha256": "8736a40624cd044c742fbe14108eebc9d8e4e163940f826154b124d967dd5e8c", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "default-b", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-b", + "config", + "mcp", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.933, + "stdout_file": "default-b.stdout.log", + "stderr_file": "default-b.stderr.log", + "stdout_bytes": 171, + "stderr_bytes": 0, + "stdout_sha256": "8736a40624cd044c742fbe14108eebc9d8e4e163940f826154b124d967dd5e8c", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "override-a", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "config", + "mcp", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.935, + "stdout_file": "override-a.stdout.log", + "stderr_file": "override-a.stderr.log", + "stdout_bytes": 156, + "stderr_bytes": 0, + "stdout_sha256": "1e24e90f88c97a59125df9dbd80bb881d82c2107ac1eacfbe95bbd3bc2aa42a6", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": "/tmp/cline-native-research-3c04fr3x/project-a/.uap/clients/cline/mcp.json", + "automatic_update_disabled": true + }, + { + "probe": "override-b", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-b", + "config", + "mcp", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.916, + "stdout_file": "override-b.stdout.log", + "stderr_file": "override-b.stderr.log", + "stdout_bytes": 145, + "stderr_bytes": 0, + "stdout_sha256": "36e7b91aa4dd2c2013048df70e6dc0139ce0cd3bd8ff89a39d2c96808ac307d1", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": "/tmp/cline-native-research-3c04fr3x/project-b/.uap/clients/cline/mcp.json", + "automatic_update_disabled": true + }, + { + "probe": "cross-a-from-b", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-b", + "config", + "mcp", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.874, + "stdout_file": "cross-a-from-b.stdout.log", + "stderr_file": "cross-a-from-b.stderr.log", + "stdout_bytes": 156, + "stderr_bytes": 0, + "stdout_sha256": "1e24e90f88c97a59125df9dbd80bb881d82c2107ac1eacfbe95bbd3bc2aa42a6", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": "/tmp/cline-native-research-3c04fr3x/project-a/.uap/clients/cline/mcp.json", + "automatic_update_disabled": true + }, + { + "probe": "default-a-again", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "config", + "mcp", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 1.216, + "stdout_file": "default-a-again.stdout.log", + "stderr_file": "default-a-again.stderr.log", + "stdout_bytes": 171, + "stderr_bytes": 0, + "stdout_sha256": "8736a40624cd044c742fbe14108eebc9d8e4e163940f826154b124d967dd5e8c", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "skills-a", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-a", + "config", + "skills", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.878, + "stdout_file": "skills-a.stdout.log", + "stderr_file": "skills-a.stderr.log", + "stdout_bytes": 665, + "stderr_bytes": 0, + "stdout_sha256": "0125c771a967130ba90d2ac84afb91a860bc7795a5325b14601f2f9122058a7a", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + }, + { + "probe": "skills-b", + "argv": [ + "/tmp/cline-native-research-3c04fr3x/binary/cline", + "--auto-approve", + "false", + "--cwd", + "/tmp/cline-native-research-3c04fr3x/project-b", + "config", + "skills", + "--json" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.935, + "stdout_file": "skills-b.stdout.log", + "stderr_file": "skills-b.stderr.log", + "stdout_bytes": 665, + "stderr_bytes": 0, + "stdout_sha256": "15fa742c1862f6e2be4a25783eda83013d476374c0b50e3519718fd4cb8f5cb5", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "mcp_settings_override": null, + "automatic_update_disabled": true + } + ], + "fixture_preparation": [ + { + "probe": "git-init-project-a", + "argv": [ + "/usr/bin/git", + "init", + "--quiet", + "--template=" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.026, + "stdout_file": "git-init-project-a.stdout.log", + "stderr_file": "git-init-project-a.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "git-init-project-b", + "argv": [ + "/usr/bin/git", + "init", + "--quiet", + "--template=" + ], + "cwd": "/tmp/cline-native-research-3c04fr3x/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.026, + "stdout_file": "git-init-project-b.stdout.log", + "stderr_file": "git-init-project-b.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + } + ], + "inventories": { + "before": { + "home/.cline/data/settings/cline_mcp_settings.json": { + "mode": "0o600", + "size": 194, + "kind": "file", + "sha256": "0b585b9b4f6d5b8c58e428b8f4b2b69faf4b689a87f7ae0a57dc11a65024a413" + }, + "home/.cline/data/settings": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline/skills/uap-cline-user/SKILL.md": { + "mode": "0o600", + "size": 139, + "kind": "file", + "sha256": "e54fa0d557c9d7997bd08fcdb78f2b2717398228587d9727291879bff186ff87" + }, + "home/.cline/skills/uap-cline-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.cline/skills/uap-cline-a/SKILL.md": { + "mode": "0o600", + "size": 136, + "kind": "file", + "sha256": "cd3447d9be0d17125f782396f26f4c879ff1bd3944b3f3e50ee9596db76d1a3d" + }, + "project-a/.cline/skills/uap-cline-a": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.cline/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.uap/clients/cline/mcp.json": { + "mode": "0o600", + "size": 188, + "kind": "file", + "sha256": "2f1fa58450d7cfe843c1e26968f3c32a679c18cde7654411adb85900651a938c" + }, + "project-a/.uap/clients/cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.uap/clients": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.uap": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.cline/skills/uap-cline-b/SKILL.md": { + "mode": "0o600", + "size": 136, + "kind": "file", + "sha256": "9d64880f23d3c59d6ca81b22061b2d49a215e7fa1d40cbed3e5de246a73e8dd2" + }, + "project-b/.cline/skills/uap-cline-b": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.cline/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.uap/clients/cline/mcp.json": { + "mode": "0o600", + "size": 177, + "kind": "file", + "sha256": "bb5bbf05ac99698aedaccd3c37b3ec50cac637ded5e75c6de4c1ce30dcbe8772" + }, + "project-b/.uap/clients/cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.uap/clients": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.uap": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "after": { + "home/.cline/data/settings/cline_mcp_settings.json": { + "mode": "0o600", + "size": 194, + "kind": "file", + "sha256": "0b585b9b4f6d5b8c58e428b8f4b2b69faf4b689a87f7ae0a57dc11a65024a413" + }, + "home/.cline/data/settings": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline/skills/uap-cline-user/SKILL.md": { + "mode": "0o600", + "size": 139, + "kind": "file", + "sha256": "e54fa0d557c9d7997bd08fcdb78f2b2717398228587d9727291879bff186ff87" + }, + "home/.cline/skills/uap-cline-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.cline/skills/uap-cline-a/SKILL.md": { + "mode": "0o600", + "size": 136, + "kind": "file", + "sha256": "cd3447d9be0d17125f782396f26f4c879ff1bd3944b3f3e50ee9596db76d1a3d" + }, + "project-a/.cline/skills/uap-cline-a": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.cline/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.uap/clients/cline/mcp.json": { + "mode": "0o600", + "size": 188, + "kind": "file", + "sha256": "2f1fa58450d7cfe843c1e26968f3c32a679c18cde7654411adb85900651a938c" + }, + "project-a/.uap/clients/cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.uap/clients": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.uap": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.cline/skills/uap-cline-b/SKILL.md": { + "mode": "0o600", + "size": 136, + "kind": "file", + "sha256": "9d64880f23d3c59d6ca81b22061b2d49a215e7fa1d40cbed3e5de246a73e8dd2" + }, + "project-b/.cline/skills/uap-cline-b": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.cline/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.uap/clients/cline/mcp.json": { + "mode": "0o600", + "size": 177, + "kind": "file", + "sha256": "bb5bbf05ac99698aedaccd3c37b3ec50cac637ded5e75c6de4c1ce30dcbe8772" + }, + "project-b/.uap/clients/cline": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.uap/clients": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.uap": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "native_created": {} + }, + "collection_complete": true, + "project_source_qualified": false, + "project_runtime_qualified": false, + "interpretation": "Human review of populated native marker/path/override attribution required", + "unobserved": [ + "UAP project installation/ownership", + "ordinary project MCP startup", + "MCP initialize/tool result", + "enabled skill execution", + "IDE discovery", + "trust/model/account", + "native restart" + ], + "runtime_network_isolation_claimed": false, + "acquisition": { + "url": "https://registry.npmjs.org/@cline/cli-linux-x64/-/cli-linux-x64-3.0.73.tgz", + "version": "3.0.73", + "archive_integrity": "sha512-K+T94s0kPHLoMSANOJJ7KHb/etzYXZGQUbarKDKUPoZsvy4+oD1MAqYnZlfbKFM8tpnRQWouFngD9TB3mFqqeA==", + "archive_bytes": 50817241, + "binary_sha256": "1ff8841d17d4d83763134c193d414e4f7fcf2ef56af759e5ebbe96727fc2c8f7", + "binary_bytes": 132511200, + "installer_executed": false, + "archived_assets_materialized": false + }, + "native_environment": { + "HOME": "/tmp/cline-native-research-3c04fr3x/home", + "USERPROFILE": "/tmp/cline-native-research-3c04fr3x/home", + "XDG_CONFIG_HOME": "/tmp/cline-native-research-3c04fr3x/home/config", + "XDG_CACHE_HOME": "/tmp/cline-native-research-3c04fr3x/home/cache", + "XDG_STATE_HOME": "/tmp/cline-native-research-3c04fr3x/home/state", + "XDG_DATA_HOME": "/tmp/cline-native-research-3c04fr3x/home/data", + "NPM_CONFIG_CACHE": "/tmp/cline-native-research-3c04fr3x/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/cline-native-research-3c04fr3x/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/cline-native-research-3c04fr3x/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/cline-native-research-3c04fr3x/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "TMPDIR": "/tmp/cline-native-research-3c04fr3x/tmp", + "LANG": "C.UTF-8", + "CLINE_NO_AUTO_UPDATE": "1" + } +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/config-help.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/config-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/config-help.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/config-help.stdout.log new file mode 100644 index 000000000..0df3af04f --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/config-help.stdout.log @@ -0,0 +1,8 @@ +Usage: cline config [options] + +Show current configuration + +Options: + --json Output as JSON + --config configuration directory + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cross-a-from-b.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cross-a-from-b.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cross-a-from-b.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cross-a-from-b.stdout.log new file mode 100644 index 000000000..38bb28fc2 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/cross-a-from-b.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-a","transportType":"streamableHttp","disabled":true,"path":"/tmp/cline-native-research-3c04fr3x/project-a/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a-again.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a-again.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a-again.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a-again.stdout.log new file mode 100644 index 000000000..c25329c3b --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a-again.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-user","transportType":"streamableHttp","disabled":true,"path":"/tmp/cline-native-research-3c04fr3x/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a.stdout.log new file mode 100644 index 000000000..c25329c3b --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-a.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-user","transportType":"streamableHttp","disabled":true,"path":"/tmp/cline-native-research-3c04fr3x/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-b.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-b.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-b.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-b.stdout.log new file mode 100644 index 000000000..c25329c3b --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/default-b.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-user","transportType":"streamableHttp","disabled":true,"path":"/tmp/cline-native-research-3c04fr3x/home/.cline/data/settings/cline_mcp_settings.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-a.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-a.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-a.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-a.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-b.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-b.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-b.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/git-init-project-b.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/help.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/help.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/help.stdout.log new file mode 100644 index 000000000..2466e98f4 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/help.stdout.log @@ -0,0 +1,69 @@ +Usage: cline [options] [command] [prompt] + +Cline CLI - AI coding assistant in your terminal + +Arguments: + prompt Your prompt. Default to start in act mode with + auto-approve enabled. + +Options: + -V, --version Output the version number + -p, --plan Run in plan mode + -y, --yolo Auto-approve a limited tool set for unsupervised + runs (benchmarks, CI). DANGEROUS: use only in + sandboxed environments. + --json Output messages as JSON instead of styled text + --auto-approve Set tool auto-approval for all tools (default: + true) + -c, --cwd Working directory + --thinking Set reasoning effort: + none|low|medium|high|xhigh. Bare --thinking uses + medium; omitted uses the level last chosen with + /model, or the provider default. + --compaction Context compaction mode: agentic|basic|off + (default: agentic) + -i, --tui Open the terminal user interface (TUI) for + interactive sessions + --id Resume an existing session by ID + -P, --provider Provider id (default: cline) + -k, --key API key for this run, also saved as the + provider's key + -m, --model Model to use for the session with the selected + provider + -s, --system Override the default system prompt + -z, --zen Start a session that runs in the background hub + --retries Number of maximum consecutive mistakes (retries) + before exiting (default: 3) + -t, --timeout Optional timeout in seconds (default: 0 for no + timeout) + --acp Run in Agent Client Protocol (ACP) mode for + editor integration + --config Configuration directory (default: ~/.cline) + --data-dir Use isolated local state at this directory path + (default: ~/.cline/data) + --hooks-dir Directory path to additional hooks for runtime + hook injection (default: ~/.cline/hooks) + --worktree Auto-create a detached git worktree under + ~/.cline/worktrees/ and run the task there + --update Check for updates and install if available + --kanban Run the kanban app + -v, --verbose Show verbose output + -h, --help display help for command + +Commands: + auth [options] [provider] Authenticate a provider and configure what model + is used + config [options] Show current configuration + plugin Manage Cline Plugins + skill [args...] Manage Cline Skills via the open skills CLI (npx + skills) + connect [options] [channel] Connect to an external channel + mcp Manage MCP servers + doctor Diagnose and fix configuration issues + history|h [options] List session history or manage saved sessions + hook Handle a hook payload from stdin + schedule Manage scheduled tasks + hub Manage the local hub daemon + update [options] Check for updates and install if available + version Show Cline CLI version number + kanban Run the kanban app diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/mcp-help.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/mcp-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/mcp-help.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/mcp-help.stdout.log new file mode 100644 index 000000000..3cff142c0 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/mcp-help.stdout.log @@ -0,0 +1,10 @@ +Usage: cline mcp [options] [command] + +Manage MCP servers + +Options: + -h, --help display help for command + +Commands: + install|add [options] [targetArgs...] Open the MCP add wizard with server fields prefilled + uninstall|remove [options] Uninstall an MCP server by name diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-a.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-a.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-a.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-a.stdout.log new file mode 100644 index 000000000..38bb28fc2 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-a.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-a","transportType":"streamableHttp","disabled":true,"path":"/tmp/cline-native-research-3c04fr3x/project-a/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-b.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-b.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-b.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-b.stdout.log new file mode 100644 index 000000000..9d4ea4067 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/override-b.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-b","transportType":"sse","disabled":true,"path":"/tmp/cline-native-research-3c04fr3x/project-b/.uap/clients/cline/mcp.json"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/provenance.json b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/provenance.json new file mode 100644 index 000000000..ee0574d83 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/provenance.json @@ -0,0 +1,229 @@ +{ + "evidence_kind": "historical-hosted-research-provenance", + "recorded_utc": "2026-10-24", + "repository": "777genius/universal-agent-plugins", + "source_commit": "bb8c89bf62542096a0af3aff26f21a16fee4d595", + "run_id": 40017958131, + "job_id": 120634802143, + "artifact_id": 12532307825, + "artifact_name": "cline-3.0.73-precode-config-linux-amd64", + "artifact_archive_bytes": 9915, + "artifact_archive_sha256": "cec4b3dd3bf2aa7a8572c7d517f06c3529837d5e01445326d00d4a2d4f27c68c", + "run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40017958131", + "job_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40017958131/job/120634802143", + "archive_committed": false, + "verifications": { + "raw_text_members": 29, + "stream_records_size_and_sha256_verified": 28, + "fixture_inventories_equal": true, + "native_created_empty": true, + "original_summary_preserved": true + }, + "comparison_checkpoint": "4cba9df5cab26f12fafa4c0e68a119953485e652", + "source_closure_comparison": [ + { + "path": "scripts/research-cline-native.py", + "measured_source_blob": "43b4dfb73fc89cbd84dc392c52e80ae6bb201bd2", + "comparison_checkpoint_blob": "43b4dfb73fc89cbd84dc392c52e80ae6bb201bd2", + "byte_identical": true + }, + { + "path": "scripts/native_client_research.py", + "measured_source_blob": "7ae48fa7d2b3dc7dcbbea85ccb6181944d4f9f31", + "comparison_checkpoint_blob": "624fcd6d098cbcac5e3ad176e3718b85ca661fcc", + "byte_identical": false + }, + { + "path": "scripts/research-grok-native.py", + "measured_source_blob": "66f64189ae91f7a5795aa5501508db767f8b9fa5", + "comparison_checkpoint_blob": "66f64189ae91f7a5795aa5501508db767f8b9fa5", + "byte_identical": true + }, + { + "path": "scripts/run-native-client-matrix.py", + "measured_source_blob": "6947dc0d105a311d7f19ba851e50c3ac28c9a654", + "comparison_checkpoint_blob": "9d23a2ececdd5346b3f497f80d7aa216c2cc6648", + "byte_identical": false + }, + { + "path": "scripts/native_client_draft.py", + "measured_source_blob": "521e1420d65e0ee2b29ecebfb66ffbe17c13cc50", + "comparison_checkpoint_blob": "521e1420d65e0ee2b29ecebfb66ffbe17c13cc50", + "byte_identical": true + }, + { + "path": ".github/workflows/cline-native-research.yml", + "measured_source_blob": "ec7d58458f1ffc91862cc5b7a953278cf85ca4a2", + "comparison_checkpoint_blob": "ec7d58458f1ffc91862cc5b7a953278cf85ca4a2", + "byte_identical": true + } + ], + "interpretation_reviews": [ + { + "reviewer": "skillshare_scope", + "decision": "GO for concrete post-observation plan only", + "native_executed_by_review": false + }, + { + "reviewer": "root", + "decision": "GO for concrete post-observation plan only", + "native_executed_by_review": false + } + ], + "qualification_boundary": { + "current_head_native_gate": false, + "uap_project_installation": false, + "ordinary_project_mcp_startup": false, + "mcp_initialize_or_tool": false, + "skill_execution": false, + "ide_or_trust_or_model": false, + "runtime_network_isolation": false + }, + "files": [ + { + "name": "cline-precode-research.json", + "bytes": 27900, + "sha256": "3cc5c427a4abe2f1bfe45a36c414e487d70da3d7109dd1dd6541b33488fe3176" + }, + { + "name": "config-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "config-help.stdout.log", + "bytes": 186, + "sha256": "f251998cd21fa6e94730770601b6081e279a79312f98102d3445a5ef983358cf" + }, + { + "name": "cross-a-from-b.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "cross-a-from-b.stdout.log", + "bytes": 156, + "sha256": "1e24e90f88c97a59125df9dbd80bb881d82c2107ac1eacfbe95bbd3bc2aa42a6" + }, + { + "name": "default-a-again.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "default-a-again.stdout.log", + "bytes": 171, + "sha256": "8736a40624cd044c742fbe14108eebc9d8e4e163940f826154b124d967dd5e8c" + }, + { + "name": "default-a.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "default-a.stdout.log", + "bytes": 171, + "sha256": "8736a40624cd044c742fbe14108eebc9d8e4e163940f826154b124d967dd5e8c" + }, + { + "name": "default-b.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "default-b.stdout.log", + "bytes": 171, + "sha256": "8736a40624cd044c742fbe14108eebc9d8e4e163940f826154b124d967dd5e8c" + }, + { + "name": "git-init-project-a.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "git-init-project-a.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "git-init-project-b.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "git-init-project-b.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "help.stdout.log", + "bytes": 3993, + "sha256": "04d834c4ae0cc8ea0bf304351a54a4d534acfb52f92c40fa6e28bd290ddef651" + }, + { + "name": "mcp-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "mcp-help.stdout.log", + "bytes": 332, + "sha256": "104e1a4b531a169d65d77511a9d8b9b1140dd5e33534274c12cd1c7fa7838563" + }, + { + "name": "override-a.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "override-a.stdout.log", + "bytes": 156, + "sha256": "1e24e90f88c97a59125df9dbd80bb881d82c2107ac1eacfbe95bbd3bc2aa42a6" + }, + { + "name": "override-b.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "override-b.stdout.log", + "bytes": 145, + "sha256": "36e7b91aa4dd2c2013048df70e6dc0139ce0cd3bd8ff89a39d2c96808ac307d1" + }, + { + "name": "skills-a.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "skills-a.stdout.log", + "bytes": 665, + "sha256": "0125c771a967130ba90d2ac84afb91a860bc7795a5325b14601f2f9122058a7a" + }, + { + "name": "skills-b.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "skills-b.stdout.log", + "bytes": 665, + "sha256": "15fa742c1862f6e2be4a25783eda83013d476374c0b50e3519718fd4cb8f5cb5" + }, + { + "name": "version.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "version.stdout.log", + "bytes": 7, + "sha256": "942a33503b6a87e2f85be82d79fc3ced6c59ebd1b901ba309db8d04da494ffc7" + } + ] +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/readonly-source-trace.json b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/readonly-source-trace.json new file mode 100644 index 000000000..c1684148c --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/readonly-source-trace.json @@ -0,0 +1,42 @@ +{ + "evidence_kind": "static-source-dispatch-trace-not-runtime-proof", + "recorded_utc": "2026-10-24", + "tag": "cli-v3.0.73", + "annotated_tag_object": "75fc1aaa39db1119dc8b63e4969cf4ff5e27ba46", + "commit": "c718573538d76310f464541617ab665108a6c999", + "tag_and_commit_text_compared_equal": true, + "native_launched": false, + "enabled_configuration_observed": false, + "sources": [ + { + "path": "apps/cli/src/main.ts", + "characters": 39126, + "url": "https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/apps/cli/src/main.ts" + }, + { + "path": "apps/cli/src/commands/config.ts", + "characters": 14875, + "url": "https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/apps/cli/src/commands/config.ts" + }, + { + "path": "apps/cli/src/commands/update.ts", + "characters": 18495, + "url": "https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/apps/cli/src/commands/update.ts" + }, + { + "path": "sdk/packages/shared/src/storage/paths.ts", + "characters": 27126, + "url": "https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/sdk/packages/shared/src/storage/paths.ts" + }, + { + "path": "sdk/packages/core/src/extensions/mcp/config-loader.ts", + "characters": 29733, + "url": "https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/sdk/packages/core/src/extensions/mcp/config-loader.ts" + }, + { + "path": "sdk/packages/core/src/extensions/mcp/remote-proxy.ts", + "characters": 1655, + "url": "https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/sdk/packages/core/src/extensions/mcp/remote-proxy.ts" + } + ] +} diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-a.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-a.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-a.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-a.stdout.log new file mode 100644 index 000000000..7dcf258a3 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-a.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-a","description":"Inert Cline source research marker","instructions":"Text only. No executable, hook, plugin, MCP command or task.","frontmatter":{"name":"uap-cline-a","description":"Inert Cline source research marker"},"path":"/tmp/cline-native-research-3c04fr3x/project-a/.cline/skills/uap-cline-a/SKILL.md"},{"name":"uap-cline-user","description":"Inert Cline source research marker","instructions":"Text only. No executable, hook, plugin, MCP command or task.","frontmatter":{"name":"uap-cline-user","description":"Inert Cline source research marker"},"path":"/tmp/cline-native-research-3c04fr3x/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-b.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-b.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-b.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-b.stdout.log new file mode 100644 index 000000000..e5d1d097c --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/skills-b.stdout.log @@ -0,0 +1 @@ +[{"name":"uap-cline-b","description":"Inert Cline source research marker","instructions":"Text only. No executable, hook, plugin, MCP command or task.","frontmatter":{"name":"uap-cline-b","description":"Inert Cline source research marker"},"path":"/tmp/cline-native-research-3c04fr3x/project-b/.cline/skills/uap-cline-b/SKILL.md"},{"name":"uap-cline-user","description":"Inert Cline source research marker","instructions":"Text only. No executable, hook, plugin, MCP command or task.","frontmatter":{"name":"uap-cline-user","description":"Inert Cline source research marker"},"path":"/tmp/cline-native-research-3c04fr3x/home/.cline/skills/uap-cline-user/SKILL.md"}] \ No newline at end of file diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/version.stderr.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/version.stdout.log b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/version.stdout.log new file mode 100644 index 000000000..96e7c9550 --- /dev/null +++ b/docs/evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/version.stdout.log @@ -0,0 +1 @@ +3.0.73 diff --git a/docs/evidence/project-scope-research/codex-0.164.0-linux-amd64-local-scope/README.md b/docs/evidence/project-scope-research/codex-0.164.0-linux-amd64-local-scope/README.md new file mode 100644 index 000000000..038318ca0 --- /dev/null +++ b/docs/evidence/project-scope-research/codex-0.164.0-linux-amd64-local-scope/README.md @@ -0,0 +1,67 @@ +# Codex 0.164.0 disposable scope observations + +Observed 2026-10-24 during the owner-authorized practical spike. This is a local +disposable-authority experiment, not a hosted native-matrix pass, installer +lifecycle qualification, or release evidence. No UAP production implementation +was changed or executed by this spike. + +The official `@openai/codex@0.164.0` npm package supplied the measured native CLI. +Its native executable SHA-256 was +`b93f6811d3f653f0fb83be4050d857fb9abbb111dfa2ce160563327daff7f306`. +The npm archive integrity was +`sha512-TWj7UGyPRe3Pj/A3ddNAxs1te3a/W/xAd+J0LHrrFTmsSfahGMOLF/vn88D3QCIgAT3B1rm1y/Z3NYE0rx6cZQ==`. +Unused voice resources were removed from the disposable package during host disk +exhaustion; the measured CLI executable was unchanged. Nothing was installed into +a real user profile. One disposable user authority was outside two sibling +projects; both projects reused that same authority. No project masqueraded as +native user configuration. + +`observations.json` retains sanitized native command outputs, selected read-only +RPC responses and raw evidence hashes. Absolute paths describe disposable test +locators only. Each fresh app-server received `initialize`, `initialized` and +read-only `skills/list`, `config/read`, or experimental `plugin/list`. No login, +credential, thread/start, turn/start, model inference, MCP initialization/tool +execution, hook execution, or trust-bypass flag was requested. Ordinary native +trust entries in the disposable user profile distinguished trusted/untrusted +fixtures. This experiment did not establish network isolation or observe all +native background behavior. Do not treat the experimental plugin RPC as a +production-supported UAP dependency. + +## Concrete observations + +- `skills/list` reported both the same-named user skill and exact selected + repository skill, with distinct paths/scopes; it did not merge their bodies or + list the other project's repository skill. +- Trusted A loaded its `.codex/config.toml` model sentinel and project MCP entry. + Untrusted B retained the user sentinel and excluded its project MCP entry; + `config/read` supplied the disabled project-layer explanation. Direct B skills + were still listed. Config trust and skill discovery are separate boundaries. +- Native CLI `plugin add --help` had no project scope option. With only a repo + catalog, native CLI marketplace/plugin list was empty and plugin add could not + find the plugin, including when `-C` explicitly selected the project. +- Experimental app-server inventory recognized repo catalogs and project plugin + enablement. A fresh enabled A loaded its namespaced plugin skill from native + user cache, not directly from the repo plugin path. +- Trusted A and B using identical marketplace/plugin/version with different + inert bodies shared one native cache identity. B's inventory resolved B's + source, but B skill discovery loaded A's cached body. Repetition preserved A. +- Giving B a distinct marketplace identity selected B's body in a distinct native + user-cache path while A retained A's body. +- Explicit native `plugin marketplace add ` then `plugin add ... --json` + succeeded and wrote marketplace/plugin enablement into the disposable **user** + config. Native plugin list from B then showed that user-installed A plugin. + +These observations support keeping UAP's exact project skill/config component +route. They do not qualify MCP operation, native session skill use, user-project +name override, or a project-owned native plugin catalog/cache lifecycle. + +Official documentation checked during the spike: + +- https://learn.chatgpt.com/docs/build-skills +- https://developers.openai.com/plugins/build/plugins +- https://learn.chatgpt.com/docs/app-server + +Current documentation distinguishes repository catalogs, trusted project config, +user plugin cache, and experimental app-server plugin methods. The observed CLI +versus app-server inventory difference and same-identity cache coupling are +version-specific experimental findings, not assumptions about future clients. diff --git a/docs/evidence/project-scope-research/codex-0.164.0-linux-amd64-local-scope/observations.json b/docs/evidence/project-scope-research/codex-0.164.0-linux-amd64-local-scope/observations.json new file mode 100644 index 000000000..dba1ff4f9 --- /dev/null +++ b/docs/evidence/project-scope-research/codex-0.164.0-linux-amd64-local-scope/observations.json @@ -0,0 +1,384 @@ +{ + "runtime": "codex-cli 0.164.0", + "native_sha256": "b93f6811d3f653f0fb83be4050d857fb9abbb111dfa2ce160563327daff7f306", + "network_or_model_requests": "No login, credential, turn/start, thread/start, MCP initialization, hook execution or trust bypass requested", + "authority": "one disposable user authority outside sibling projects A and B; production HOME/CODEX_HOME untouched", + "baseline": { + "A": { + "skills": [ + { + "name": "scope-probe", + "description": "Inert project A scope probe", + "path": "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/project-A/.agents/skills/same-name/SKILL.md", + "scope": "repo", + "enabled": true, + "pluginId": null + }, + { + "name": "scope-probe", + "description": "Inert user scope probe", + "path": "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/authority/user/.agents/skills/same-name/SKILL.md", + "scope": "user", + "enabled": true, + "pluginId": null + } + ], + "errors": [], + "model": "scope-project-A", + "mcp_names": [ + "project_only", + "user_only" + ], + "disabled_layers": [] + }, + "B": { + "skills": [ + { + "name": "scope-probe", + "description": "Inert project B scope probe", + "path": "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/project-B/.agents/skills/same-name/SKILL.md", + "scope": "repo", + "enabled": true, + "pluginId": null + }, + { + "name": "scope-probe", + "description": "Inert user scope probe", + "path": "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/authority/user/.agents/skills/same-name/SKILL.md", + "scope": "user", + "enabled": true, + "pluginId": null + } + ], + "errors": [], + "model": "scope-user", + "mcp_names": [ + "user_only" + ], + "disabled_layers": [ + { + "name": { + "type": "project", + "dotCodexFolder": "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/project-B/.codex" + }, + "reason": "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/project-B is marked as untrusted in the effective configuration. To load project-local config, hooks, and exec policies, update its trust setting. If that setting is managed by your organization, contact your administrator." + } + ] + } + }, + "catalog_checks": [ + { + "file": "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-A-answers.json", + "plugin_skills": [ + { + "name": "scope-catalog:catalog-skill", + "description": "Inert catalog project A", + "scope": "user", + "pluginId": "scope-catalog@scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market/scope-catalog/1.0.0/skills/catalog-skill/SKILL.md", + "enabled": true, + "body": "---\nname: catalog-skill\ndescription: Inert catalog project A\n---\nCatalog source A.\n" + } + ], + "catalogs": [] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-B-answers.json", + "plugin_skills": [], + "catalogs": [] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-A-answers.json", + "plugin_skills": [], + "catalogs": [ + { + "name": "scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/project-A/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market", + "installed": false, + "enabled": true, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-A/plugins/scope-catalog" + } + } + ] + } + ] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-B-answers.json", + "plugin_skills": [], + "catalogs": [ + { + "name": "scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/project-B/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market", + "installed": false, + "enabled": false, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-B/plugins/scope-catalog" + } + } + ] + } + ] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-A-answers.json", + "plugin_skills": [ + { + "name": "scope-catalog:catalog-skill", + "description": "Inert catalog project A", + "scope": "user", + "pluginId": "scope-catalog@scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market/scope-catalog/1.0.0/skills/catalog-skill/SKILL.md", + "enabled": true, + "body": "---\nname: catalog-skill\ndescription: Inert catalog project A\n---\nCatalog source A.\n" + } + ], + "catalogs": [ + { + "name": "scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/project-A/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market", + "installed": true, + "enabled": true, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-A/plugins/scope-catalog" + } + } + ] + } + ] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-B-answers.json", + "plugin_skills": [ + { + "name": "scope-catalog:catalog-skill", + "description": "Inert catalog project B", + "scope": "user", + "pluginId": "scope-catalog@scope-probe-market-B", + "path": "/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market-B/scope-catalog/1.0.0/skills/catalog-skill/SKILL.md", + "enabled": true, + "body": "---\nname: catalog-skill\ndescription: Inert catalog project B\n---\nCatalog source B.\n" + } + ], + "catalogs": [ + { + "name": "scope-probe-market-B", + "path": "/dev/shm/uap-codex-scope-spike/project-B/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market-B", + "installed": false, + "enabled": true, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-B/plugins/scope-catalog" + } + } + ] + } + ] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/same-catalog-A1-answers.json", + "plugin_skills": [ + { + "name": "scope-catalog:catalog-skill", + "description": "Inert catalog project A", + "scope": "user", + "pluginId": "scope-catalog@scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market/scope-catalog/1.0.0/skills/catalog-skill/SKILL.md", + "enabled": true, + "body": "---\nname: catalog-skill\ndescription: Inert catalog project A\n---\nCatalog source A.\n" + } + ], + "catalogs": [ + { + "name": "scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/project-A/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market", + "installed": true, + "enabled": true, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-A/plugins/scope-catalog" + } + } + ] + } + ] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B0-answers.json", + "plugin_skills": [ + { + "name": "scope-catalog:catalog-skill", + "description": "Inert catalog project A", + "scope": "user", + "pluginId": "scope-catalog@scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market/scope-catalog/1.0.0/skills/catalog-skill/SKILL.md", + "enabled": true, + "body": "---\nname: catalog-skill\ndescription: Inert catalog project A\n---\nCatalog source A.\n" + } + ], + "catalogs": [ + { + "name": "scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/project-B/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market", + "installed": true, + "enabled": true, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-B/plugins/scope-catalog" + } + } + ] + } + ] + }, + { + "file": "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B2-answers.json", + "plugin_skills": [ + { + "name": "scope-catalog:catalog-skill", + "description": "Inert catalog project A", + "scope": "user", + "pluginId": "scope-catalog@scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market/scope-catalog/1.0.0/skills/catalog-skill/SKILL.md", + "enabled": true, + "body": "---\nname: catalog-skill\ndescription: Inert catalog project A\n---\nCatalog source A.\n" + } + ], + "catalogs": [ + { + "name": "scope-probe-market", + "path": "/dev/shm/uap-codex-scope-spike/project-B/.agents/plugins/marketplace.json", + "plugins": [ + { + "id": "scope-catalog@scope-probe-market", + "installed": true, + "enabled": true, + "source": { + "type": "local", + "path": "/dev/shm/uap-codex-scope-spike/project-B/plugins/scope-catalog" + } + } + ] + } + ] + } + ], + "explicit-plugin-add-A": { + "argv": [ + "plugin", + "add", + "scope-catalog@scope-probe-market", + "--json" + ], + "cwd": "/dev/shm/uap-codex-scope-spike/project-A", + "exit": 1, + "stdout": "", + "stderr": "Error: plugin `scope-catalog` was not found in marketplace `scope-probe-market`\n" + }, + "configured-marketplace-add": { + "argv": [ + "plugin", + "marketplace", + "add", + "/dev/shm/uap-codex-scope-spike/project-A" + ], + "cwd": "/dev/shm/uap-codex-scope-spike/project-A", + "exit": 0, + "stdout": "Added marketplace `scope-probe-market` from /dev/shm/uap-codex-scope-spike/project-A.\nInstalled marketplace root: /dev/shm/uap-codex-scope-spike/project-A\n", + "stderr": "" + }, + "configured-plugin-add": { + "argv": [ + "plugin", + "add", + "scope-catalog@scope-probe-market", + "--json" + ], + "cwd": "/dev/shm/uap-codex-scope-spike/project-A", + "exit": 0, + "stdout": "{\n \"pluginId\": \"scope-catalog@scope-probe-market\",\n \"name\": \"scope-catalog\",\n \"marketplaceName\": \"scope-probe-market\",\n \"version\": \"1.0.0\",\n \"installedPath\": \"/dev/shm/uap-codex-scope-spike/authority/user/.codex/plugins/cache/scope-probe-market/scope-catalog/1.0.0\",\n \"authPolicy\": \"ON_INSTALL\"\n}\n", + "stderr": "" + }, + "configured-plugin-list-B": { + "argv": [ + "plugin", + "list", + "--json" + ], + "cwd": "/dev/shm/uap-codex-scope-spike/project-B", + "exit": 0, + "stdout": "{\n \"installed\": [\n {\n \"pluginId\": \"scope-catalog@scope-probe-market\",\n \"name\": \"scope-catalog\",\n \"marketplaceName\": \"scope-probe-market\",\n \"version\": \"1.0.0\",\n \"installed\": true,\n \"enabled\": true,\n \"source\": {\n \"source\": \"local\",\n \"path\": \"/dev/shm/uap-codex-scope-spike/project-A/plugins/scope-catalog\"\n },\n \"marketplaceSource\": {\n \"sourceType\": \"local\",\n \"source\": \"/dev/shm/uap-codex-scope-spike/project-A\"\n },\n \"installPolicy\": \"AVAILABLE\",\n \"authPolicy\": \"ON_INSTALL\"\n }\n ],\n \"available\": []\n}\n", + "stderr": "" + }, + "raw_evidence_sha256": { + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/mcp-A.json": "7c7a6486a7f11c8901f28897ae60e63b486a00c71df9cfd469d9b0df431604af", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/baseline-A-answers.json": "d80cade753d40cd29bdf4f74f350ad533e59d20caa79cc99f03e750d83f28121", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/baseline-A-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/baseline-A-rpc.jsonl": "eeb4a6b65ef66962364bb40b8f22ceb3a87fe49bf5ff7b29587ec27d21982e28", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/version.json": "30e5c3110eef5dcf9d48a716d3189d2b12e9fee15e740be72f7f55f681303825", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/plugin-add-help.json": "25e52ce1ff18d1c99fa02ad3d23b6f5031cc60da26eecfe5d86d026ca7b319e1", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/baseline-B-answers.json": "23e4b31e225f602e9eb69a5a3c9a2cb577b29a92ddff286e4baffe0e7856bf9c", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/mcp-B.json": "07c2c921cb6f3b7b6fc21d768cfbfbb9e105074f185a329732995441f9b55d69", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/baseline-B-stderr.log": "400713f68981f38d7db5b06fa6567cc095f3c3c1c782a808d2d1a7eafd5cce6b", + "/workspace/scratch/5e2d9d9f7f5b/codex-scope-spike/logs/baseline-B-rpc.jsonl": "b39feb1001bee2cd204af17b1eb219e182dd4be0bf7b50cca13d6bf5639fd92a", + "/dev/shm/uap-codex-scope-spike/logs/configured-plugin-list-B.json": "1bd543dc7b8c8f7d4701b64866a510a27730f1b938d4fbf8ca535931c43323fd", + "/dev/shm/uap-codex-scope-spike/logs/configured-plugin-add.json": "f1fd0aa665892eff6befbe8ae4bc61a99dfa7456d11aa25dcde74bd499f3bda9", + "/dev/shm/uap-codex-scope-spike/logs/configured-marketplace-add.json": "068f94091ba68dd62b43eae8e8262423fca6c51f8748f4292b8d5d2bb0e0c3f7", + "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-A-answers.json": "ee4e0137073d1cb56032fd449dfcefb2e0c862b287ea082138f1f1b3937b72c9", + "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-A-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-A-rpc.jsonl": "1dccce76707f05072aeba4312429f230cff8297395c45d34edfe65830c8de841", + "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-B-answers.json": "e39f981a86e7c6dcb878ff03779617c50829c6a9c15da1feccbf3fd7ec6425cc", + "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-B-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/qualified-catalog-B-rpc.jsonl": "e9255a9684e45839b962df95be32f65597825777413ef5d1f030495fff063927", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B2-answers.json": "5024a45e05e213065127111677da84d2f5087968eb4d38f69b5a984497acfc8a", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B2-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B2-rpc.jsonl": "04a44dd6a5cc0ed1a93e32dd2d675736d28896125ccee0b881f41cfdebbf87ad", + "/dev/shm/uap-codex-scope-spike/logs/cli-catalog-cd-A.json": "cfa51a907169de67da8ca891e9d99a5e33561bd7406228982357452cef187499", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-A1-answers.json": "91c3b18cb0b7af7980b4a753f54d44f2df8d66f163cdafbfb654ffeb5097f12c", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-A1-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-A1-rpc.jsonl": "4467d7c068f1ff07a044dd38056a585c6120324bd4a2de74fd7d58e448b87cbe", + "/dev/shm/uap-codex-scope-spike/logs/cli-catalog-cd-B.json": "b1ad553fc2868bdb2889c59d90983bc2f2a52010accf7608c6170ab193aa940e", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B0-answers.json": "cbc89e8b106ef9cc73a830791ab5beeaf10c6068058d58b0e2128927adc405ed", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B0-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/same-catalog-B0-rpc.jsonl": "48d9a58db61ca4228aa8f59a51ba843f9f3c96a2b621385630ab91852f1e2d1b", + "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-B-answers.json": "659978f082c369e14bc8a6cca7f8dffe1c508bf8a643a0d7f59b0eac676e09dc", + "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-B-stderr.log": "13c4cad0047cdf9a0790db9642a87b0c8c556a2eb201a29fc35296f2f5b315b1", + "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-B-rpc.jsonl": "5d93457c7b0f6d2797b825244f82bdbdcb21b0eab2b031ede758f29dd8a61d73", + "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-A-answers.json": "d5d9d97dc277c6d65119c0d8e96089904af206754692b1ed41e1c0340382d24f", + "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-A-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/catalog-after-add-A-rpc.jsonl": "ba98f3e89de41201a4cd3e19840e56f813d1ad35c05f305125938b226f1c838c", + "/dev/shm/uap-codex-scope-spike/logs/explicit-plugin-add-A.json": "81227b5b309cc9cdf51f42a888b9854101e1a9f2830969b5ea4e6f890830e199", + "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-B-answers.json": "6f455ed114bdcfafa9ddf8810c00ce9729a4d0b2a2cba24f65f95ccbb7c06541", + "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-B-stderr.log": "b54fd6d720a09f7ee0a5beb4d836f91384f60c24fb84fe34298f8d6898406a3c", + "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-B-rpc.jsonl": "a942bef28a6bba9bcb3c9e4020db1ab5d83b68b52f84ebf372c51db262b8c196", + "/dev/shm/uap-codex-scope-spike/logs/catalog-B.json": "561a865cfe050fba450eb7bb76a9b5bc0b8491f407f69537febdb23cb762d87b", + "/dev/shm/uap-codex-scope-spike/logs/marketplaces-B.json": "dc97aefc230dc870808baee2badb3dee997698198c4fd1ed452b885acba96121", + "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-A-answers.json": "65eda946e4c33b0a41958d33e5ca1e88e7a927587dc30d9dfc5b1fd46dd4df1f", + "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-A-stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/dev/shm/uap-codex-scope-spike/logs/catalog-before-add-A-rpc.jsonl": "34b9709e5ca6bf8a07d33eb562084e4e72436e9322396386be0795fa1d3c31f2", + "/dev/shm/uap-codex-scope-spike/logs/catalog-A.json": "f94728b3bc583052f45442fe18fdf749efef69912ef998ad1a3cf09e1d1d653d", + "/dev/shm/uap-codex-scope-spike/logs/marketplaces-A.json": "22ecb83fbe4e37b5d4af7be2b0d3de2c98f4f5d29e776d152f9888c03d6d4aeb" + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-inspect.stdout.log new file mode 100644 index 000000000..ecdf273dd --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-inspect.stdout.log @@ -0,0 +1,144 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-a/", + "projectTrusted": true, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-a-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-inspect.stdout.log new file mode 100644 index 000000000..1b3ab4c9d --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-inspect.stdout.log @@ -0,0 +1,144 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-b/", + "projectTrusted": true, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/empty-project-b-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-a.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-a.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-a.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-a.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-b.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-b.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-b.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/git-init-project-b.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/grok-precode-research.json b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/grok-precode-research.json new file mode 100644 index 000000000..7da850789 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/grok-precode-research.json @@ -0,0 +1,3588 @@ +{ + "evidence_kind": "grok-precode-project-source-observations", + "version": "1.0.67", + "target": "linux-amd64", + "source_commit": "249a9e5731822b79df3b07d9da0ef9b432258caa", + "expected_probes": [ + "version", + "plugin-list-help", + "inspect-help", + "empty-project-a-list", + "empty-project-a-inspect", + "empty-project-b-list", + "empty-project-b-inspect", + "user-project-a-list", + "user-project-a-inspect", + "user-project-b-list", + "user-project-b-inspect", + "project-a-project-a-list", + "project-a-project-a-inspect", + "project-a-project-b-list", + "project-a-project-b-inspect", + "project-b-project-a-list", + "project-b-project-a-inspect", + "project-b-project-b-list", + "project-b-project-b-inspect" + ], + "probes": [ + { + "probe": "version", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "--version" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.046, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 27, + "stderr_bytes": 0, + "stdout_sha256": "e3180b48f49d248819c0764a6e1010b548d3158f9d738f3c2d161ea03fd37a7e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "plugin-list-help", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--help" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "plugin-list-help.stdout.log", + "stderr_file": "plugin-list-help.stderr.log", + "stdout_bytes": 789, + "stderr_bytes": 0, + "stdout_sha256": "2e4b4117fcc31d706335b5de0035e45b65802ea950e9b98b15f12db0a1a664dd", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "inspect-help", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--help" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "inspect-help.stdout.log", + "stderr_file": "inspect-help.stderr.log", + "stdout_bytes": 722, + "stderr_bytes": 0, + "stdout_sha256": "6c1e788375ab69df4eb8768532ee052154aa8e51f747965fb389129f332755c4", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "empty-project-a-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.147, + "stdout_file": "empty-project-a-list.stdout.log", + "stderr_file": "empty-project-a-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "empty-project-a-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "empty-project-a-inspect.stdout.log", + "stderr_file": "empty-project-a-inspect.stderr.log", + "stdout_bytes": 3238, + "stderr_bytes": 0, + "stdout_sha256": "c46f614771c47a25cc4b7eb9405f99a8f43b9262b4f1bbf8ef2dd9506355ed8e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "empty-project-b-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.147, + "stdout_file": "empty-project-b-list.stdout.log", + "stderr_file": "empty-project-b-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "empty-project-b-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "empty-project-b-inspect.stdout.log", + "stderr_file": "empty-project-b-inspect.stderr.log", + "stdout_bytes": 3238, + "stderr_bytes": 0, + "stdout_sha256": "2248956b4ea135095445ad43b9e8f8854563166423d4a85b6a46c621ddcbd22c", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "user-project-a-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.148, + "stdout_file": "user-project-a-list.stdout.log", + "stderr_file": "user-project-a-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "user-project-a-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "user-project-a-inspect.stdout.log", + "stderr_file": "user-project-a-inspect.stderr.log", + "stdout_bytes": 3561, + "stderr_bytes": 0, + "stdout_sha256": "dbfe41d5c0447144d238f432d7463c4c6a63d831951723e23b872efee405cecc", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "user-project-b-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.148, + "stdout_file": "user-project-b-list.stdout.log", + "stderr_file": "user-project-b-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "user-project-b-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "user-project-b-inspect.stdout.log", + "stderr_file": "user-project-b-inspect.stderr.log", + "stdout_bytes": 3561, + "stderr_bytes": 0, + "stdout_sha256": "a325a2b549c964c4dbff8cf0b841faf41da2a9680e81a949652f77161285b080", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-a-project-a-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.147, + "stdout_file": "project-a-project-a-list.stdout.log", + "stderr_file": "project-a-project-a-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-a-project-a-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "project-a-project-a-inspect.stdout.log", + "stderr_file": "project-a-project-a-inspect.stderr.log", + "stdout_bytes": 3571, + "stderr_bytes": 0, + "stdout_sha256": "5f0e6de8c4726ccf55bfcf1249ea26f49ceff137e747e48dd231fc7fbb9315c0", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-a-project-b-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.147, + "stdout_file": "project-a-project-b-list.stdout.log", + "stderr_file": "project-a-project-b-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-a-project-b-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "project-a-project-b-inspect.stdout.log", + "stderr_file": "project-a-project-b-inspect.stderr.log", + "stdout_bytes": 3561, + "stderr_bytes": 0, + "stdout_sha256": "a325a2b549c964c4dbff8cf0b841faf41da2a9680e81a949652f77161285b080", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-b-project-a-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.148, + "stdout_file": "project-b-project-a-list.stdout.log", + "stderr_file": "project-b-project-a-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-b-project-a-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "project-b-project-a-inspect.stdout.log", + "stderr_file": "project-b-project-a-inspect.stderr.log", + "stdout_bytes": 3571, + "stderr_bytes": 0, + "stdout_sha256": "5f0e6de8c4726ccf55bfcf1249ea26f49ceff137e747e48dd231fc7fbb9315c0", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-b-project-b-list", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "plugin", + "list", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.148, + "stdout_file": "project-b-project-b-list.stdout.log", + "stderr_file": "project-b-project-b-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "project-b-project-b-inspect", + "argv": [ + "/tmp/grok-native-research-h5lgn7tv/binary/grok", + "inspect", + "--json" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.047, + "stdout_file": "project-b-project-b-inspect.stdout.log", + "stderr_file": "project-b-project-b-inspect.stderr.log", + "stdout_bytes": 3571, + "stderr_bytes": 0, + "stdout_sha256": "e4c4676d3ed57092a3954512a630f9877d1ac1a9bef4ccc59ce05998f63dd1ff", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + } + ], + "fixture_preparation": [ + { + "probe": "git-init-project-a", + "argv": [ + "/usr/bin/git", + "init", + "--quiet", + "--template=" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "error": null, + "exit": 0, + "elapsed_seconds": 0.027, + "stdout_file": "git-init-project-a.stdout.log", + "stderr_file": "git-init-project-a.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "git-init-project-b", + "argv": [ + "/usr/bin/git", + "init", + "--quiet", + "--template=" + ], + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "error": null, + "exit": 0, + "elapsed_seconds": 0.026, + "stdout_file": "git-init-project-b.stdout.log", + "stderr_file": "git-init-project-b.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + } + ], + "inventories": [ + { + "stage": "empty", + "before": { + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "after": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 1254, + "kind": "file", + "sha256": "9da23d39988a16c2f1050252dee05daaf0a8bd83eec4249a832f6291c39d0b17" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "native_created": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 1254, + "kind": "file", + "sha256": "9da23d39988a16c2f1050252dee05daaf0a8bd83eec4249a832f6291c39d0b17" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + } + }, + { + "stage": "user", + "before": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 1254, + "kind": "file", + "sha256": "9da23d39988a16c2f1050252dee05daaf0a8bd83eec4249a832f6291c39d0b17" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "ee6fff34592ccf52918e1a2347420666d72d842af0f5be071d4d9cecd02c8917" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user/SKILL.md": { + "mode": "0o644", + "size": 151, + "kind": "file", + "sha256": "a5858ef04c048433c3dd5c25b5f43e1d9b7de6ba1d316e30ecbf86fcb1723ed4" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "after": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 2508, + "kind": "file", + "sha256": "d0d948549c7ad4d5798cf8a28f0892f891ddd136996bc514820af531423820fe" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "ee6fff34592ccf52918e1a2347420666d72d842af0f5be071d4d9cecd02c8917" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user/SKILL.md": { + "mode": "0o644", + "size": 151, + "kind": "file", + "sha256": "a5858ef04c048433c3dd5c25b5f43e1d9b7de6ba1d316e30ecbf86fcb1723ed4" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "native_created": {} + }, + { + "stage": "project-a", + "before": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 2508, + "kind": "file", + "sha256": "d0d948549c7ad4d5798cf8a28f0892f891ddd136996bc514820af531423820fe" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "ee6fff34592ccf52918e1a2347420666d72d842af0f5be071d4d9cecd02c8917" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user/SKILL.md": { + "mode": "0o644", + "size": 151, + "kind": "file", + "sha256": "a5858ef04c048433c3dd5c25b5f43e1d9b7de6ba1d316e30ecbf86fcb1723ed4" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "b8d1e981a5358d2b5b657ffed881676cf7367780579f7585ff2b183e417c3c6d" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a/SKILL.md": { + "mode": "0o644", + "size": 148, + "kind": "file", + "sha256": "5b4579951865cc7305006bb675e0ecc034b7ee8356ea27e7c4c0e0093661f8eb" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "after": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 3762, + "kind": "file", + "sha256": "807466e9eddb96a9593c958d9f410c18f622fa099b5e2c345e0cafe10e0f4d43" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "ee6fff34592ccf52918e1a2347420666d72d842af0f5be071d4d9cecd02c8917" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user/SKILL.md": { + "mode": "0o644", + "size": 151, + "kind": "file", + "sha256": "a5858ef04c048433c3dd5c25b5f43e1d9b7de6ba1d316e30ecbf86fcb1723ed4" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "b8d1e981a5358d2b5b657ffed881676cf7367780579f7585ff2b183e417c3c6d" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a/SKILL.md": { + "mode": "0o644", + "size": 148, + "kind": "file", + "sha256": "5b4579951865cc7305006bb675e0ecc034b7ee8356ea27e7c4c0e0093661f8eb" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "native_created": {} + }, + { + "stage": "project-b", + "before": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 3762, + "kind": "file", + "sha256": "807466e9eddb96a9593c958d9f410c18f622fa099b5e2c345e0cafe10e0f4d43" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "ee6fff34592ccf52918e1a2347420666d72d842af0f5be071d4d9cecd02c8917" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user/SKILL.md": { + "mode": "0o644", + "size": 151, + "kind": "file", + "sha256": "a5858ef04c048433c3dd5c25b5f43e1d9b7de6ba1d316e30ecbf86fcb1723ed4" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "b8d1e981a5358d2b5b657ffed881676cf7367780579f7585ff2b183e417c3c6d" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a/SKILL.md": { + "mode": "0o644", + "size": 148, + "kind": "file", + "sha256": "5b4579951865cc7305006bb675e0ecc034b7ee8356ea27e7c4c0e0093661f8eb" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "737c568e59639e2cf6f87f383143cf74b55fc6a8a3e244f15182f88cf49ec650" + }, + "project-b/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research/skills/uap-research-b/SKILL.md": { + "mode": "0o644", + "size": 148, + "kind": "file", + "sha256": "d3dddab65c4e2f0d6152d4f599d6db2862535d032e017a116a00500c3d09a497" + }, + "project-b/.grok/plugins/uap-project-native-research/skills/uap-research-b": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "after": { + "home/.grok/docs/user-guide/01-getting-started.md": { + "mode": "0o644", + "size": 10495, + "kind": "file", + "sha256": "9c022943cfce0873165bd606a520e216b48a98decb1a974ca6cc6f30f0b32efd" + }, + "home/.grok/docs/user-guide/02-authentication.md": { + "mode": "0o644", + "size": 16079, + "kind": "file", + "sha256": "d5a471051752238b2cf700f02fffc50f9418388072f28df0ebe83867ec2974c5" + }, + "home/.grok/docs/user-guide/03-keyboard-shortcuts.md": { + "mode": "0o644", + "size": 37358, + "kind": "file", + "sha256": "ec298a3dc40b9bc6f937158b9342d42f766482d71547c2a4cf0063a162044004" + }, + "home/.grok/docs/user-guide/04-slash-commands.md": { + "mode": "0o644", + "size": 42812, + "kind": "file", + "sha256": "355e3f2b0e89ed378df155161750e06f6d1735a25747e36acfb462d64c7e5bbe" + }, + "home/.grok/docs/user-guide/05-configuration.md": { + "mode": "0o644", + "size": 57643, + "kind": "file", + "sha256": "8f7fd76981c6dab32fa96a5707da70dcd02af36d83430f85503dae64501ac72c" + }, + "home/.grok/docs/user-guide/06-theming.md": { + "mode": "0o644", + "size": 17523, + "kind": "file", + "sha256": "4aff4bc0d42d927b7c37c0e08dd80365649520fa18baa202e82b65b8b117aa63" + }, + "home/.grok/docs/user-guide/07-mcp-servers.md": { + "mode": "0o644", + "size": 25958, + "kind": "file", + "sha256": "088039bcb670bee17f0acdf7ef679d3d910641912eb667ce0c65678f4e0b7855" + }, + "home/.grok/docs/user-guide/08-skills.md": { + "mode": "0o644", + "size": 13886, + "kind": "file", + "sha256": "4afcd002489982e069e02dbb0d07fb490216035707fab208b36c434fe6852f1c" + }, + "home/.grok/docs/user-guide/09-plugins.md": { + "mode": "0o644", + "size": 28001, + "kind": "file", + "sha256": "867c8c39394ad4b30b8d38d19eee8f1895a06b37b3581f5f7d63e64f85826b5a" + }, + "home/.grok/docs/user-guide/10-hooks.md": { + "mode": "0o644", + "size": 103892, + "kind": "file", + "sha256": "7a0138b61ced3decb5fdc99c9c82cbff694109ab77791fda5bceafddeab7ba5f" + }, + "home/.grok/docs/user-guide/11-custom-models.md": { + "mode": "0o644", + "size": 19362, + "kind": "file", + "sha256": "4a0fe03334e91c1a472cf985d490ca73810e9738a171f5e0a5bf615552a6ca9a" + }, + "home/.grok/docs/user-guide/12-project-rules.md": { + "mode": "0o644", + "size": 10890, + "kind": "file", + "sha256": "5bc157f20c2548362df49d2d704d4f4b2c568db2adc8a437680ab02064537f53" + }, + "home/.grok/docs/user-guide/13-memory.md": { + "mode": "0o644", + "size": 14019, + "kind": "file", + "sha256": "51c764bfcfef4c4f79ad6dc4a270e057a3726cae237109b950083bf7007d7831" + }, + "home/.grok/docs/user-guide/14-headless-mode.md": { + "mode": "0o644", + "size": 54963, + "kind": "file", + "sha256": "b76cedf9d8f6532377b1470590b3404ccbb20b24d5fb0d90042e3d70f92535ef" + }, + "home/.grok/docs/user-guide/15-agent-mode.md": { + "mode": "0o644", + "size": 14507, + "kind": "file", + "sha256": "b4c4b840392a9b85a356b433b729ea02a89bf2742aa74894bbec56ff369c8fed" + }, + "home/.grok/docs/user-guide/16-subagents.md": { + "mode": "0o644", + "size": 23119, + "kind": "file", + "sha256": "6556ef4eab94ff5dc01343feedacf525334b23131d626b38b4cd752eeb6c8c78" + }, + "home/.grok/docs/user-guide/17-sessions.md": { + "mode": "0o644", + "size": 35835, + "kind": "file", + "sha256": "31849a3670d04470ef95df8afc79452f2bbf916c48cc01ac2774daa7edcb5e69" + }, + "home/.grok/docs/user-guide/18-sandbox.md": { + "mode": "0o644", + "size": 27219, + "kind": "file", + "sha256": "82f3ca41b20a4b393cfff26a138d02464590dbff833f1817cc569520a4154181" + }, + "home/.grok/docs/user-guide/19-plan-mode.md": { + "mode": "0o644", + "size": 14010, + "kind": "file", + "sha256": "b8c557491c640891b423c497546fe46a7286819f34f27bb4448195df2fd9b61f" + }, + "home/.grok/docs/user-guide/20-background-tasks.md": { + "mode": "0o644", + "size": 10777, + "kind": "file", + "sha256": "0589ac332ab67724ffe85c9f0caf4e0e4c0b91789a4eeac19aabd561077a7a46" + }, + "home/.grok/docs/user-guide/21-terminal-support.md": { + "mode": "0o644", + "size": 15077, + "kind": "file", + "sha256": "cd8323d383a3e90e2a3a97e618bc2428e4a000e9227c453028d6cbbe984b1023" + }, + "home/.grok/docs/user-guide/22-permissions-and-safety.md": { + "mode": "0o644", + "size": 40609, + "kind": "file", + "sha256": "fdba89c72148c184a606034e82d98e0e5181c1bb34d5cc75bfc67f7ff2a3e192" + }, + "home/.grok/docs/user-guide/23-dashboard.md": { + "mode": "0o644", + "size": 18425, + "kind": "file", + "sha256": "facc441ceef37a4b131e145f8f1ef4e58675e96cf9e9290d500795265179b430" + }, + "home/.grok/docs/user-guide/24-monitoring-usage.md": { + "mode": "0o644", + "size": 22276, + "kind": "file", + "sha256": "331245078267fb8072df695815ce6fa23d184733be62b42a08fc5843f157783f" + }, + "home/.grok/docs/user-guide/25-status-line.md": { + "mode": "0o644", + "size": 18204, + "kind": "file", + "sha256": "d4d5c64ea02163171425176906f52ce36cf0154a6324608fe0179490bb4852c2" + }, + "home/.grok/docs/user-guide/26-config-reference.md": { + "mode": "0o644", + "size": 86122, + "kind": "file", + "sha256": "d68b333b18bd3c160ce58a438dc21d4b6d1271703ca09192ff6c6ae5e790ab66" + }, + "home/.grok/docs/user-guide/27-grok-clone.md": { + "mode": "0o644", + "size": 10752, + "kind": "file", + "sha256": "cb557fe0d0a82cff73daad173c113727e9ed050159e3008a13bddafd699755ce" + }, + "home/.grok/docs/user-guide": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/docs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/logs/unified.jsonl": { + "mode": "0o644", + "size": 5016, + "kind": "file", + "sha256": "28e4d84472ebf8046ad52d85e0b3f01c35b771a4b17b79e0c36e89ff51ed25e9" + }, + "home/.grok/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "ee6fff34592ccf52918e1a2347420666d72d842af0f5be071d4d9cecd02c8917" + }, + "home/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user/SKILL.md": { + "mode": "0o644", + "size": 151, + "kind": "file", + "sha256": "a5858ef04c048433c3dd5c25b5f43e1d9b7de6ba1d316e30ecbf86fcb1723ed4" + }, + "home/.grok/plugins/uap-project-native-research/skills/uap-research-user": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-a/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-a/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "b8d1e981a5358d2b5b657ffed881676cf7367780579f7585ff2b183e417c3c6d" + }, + "project-a/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a/SKILL.md": { + "mode": "0o644", + "size": 148, + "kind": "file", + "sha256": "5b4579951865cc7305006bb675e0ecc034b7ee8356ea27e7c4c0e0093661f8eb" + }, + "project-a/.grok/plugins/uap-project-native-research/skills/uap-research-a": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-a/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/HEAD": { + "mode": "0o644", + "size": 23, + "kind": "file", + "sha256": "f6f2b945f6c411b02ba3da9c7ace88dcf71b6af65ba2e0d89aa82900042b5a10" + }, + "project-b/.git/config": { + "mode": "0o644", + "size": 92, + "kind": "file", + "sha256": "cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe" + }, + "project-b/.git/objects/info": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects/pack": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/objects": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/heads": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs/tags": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git/refs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research/.grok-plugin/plugin.json": { + "mode": "0o644", + "size": 143, + "kind": "file", + "sha256": "737c568e59639e2cf6f87f383143cf74b55fc6a8a3e244f15182f88cf49ec650" + }, + "project-b/.grok/plugins/uap-project-native-research/.grok-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research/skills/uap-research-b/SKILL.md": { + "mode": "0o644", + "size": 148, + "kind": "file", + "sha256": "d3dddab65c4e2f0d6152d4f599d6db2862535d032e017a116a00500c3d09a497" + }, + "project-b/.grok/plugins/uap-project-native-research/skills/uap-research-b": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins/uap-project-native-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "project-b/.grok": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + } + }, + "native_created": {} + } + ], + "collection_complete": true, + "project_source_qualified": false, + "interpretation": "Human review of actual source/version/marker attribution and A/B/user precedence required", + "unobserved": [ + "UAP project installation/ownership", + "disabled/alias/ancestor and Claude-compatible conflicts", + "skill execution", + "MCP initialization/tool result", + "model/account/trust", + "native restart" + ], + "runtime_network_isolation_claimed": false, + "acquisition": { + "url": "https://x.ai/cli/grok-1.0.67-linux-x86_64.gz", + "version": "1.0.67", + "archive_integrity": "sha256:6bbea96cde807c13e4c66d9583d8acdd155e446d93131c2820999cf5523ed5a5", + "integrity_source": "measured official HTTPS versioned bytes; no separate vendor SHA256 published", + "archive_bytes": 76217006, + "binary_sha256": "ce33b1a29e37901f56fe59916cda7a1149e6fd377118f918e90d89f9abb52a51", + "binary_bytes": 194169184, + "installer_executed": false + }, + "native_environment": { + "HOME": "/tmp/grok-native-research-h5lgn7tv/home", + "USERPROFILE": "/tmp/grok-native-research-h5lgn7tv/home", + "XDG_CONFIG_HOME": "/tmp/grok-native-research-h5lgn7tv/home/config", + "XDG_CACHE_HOME": "/tmp/grok-native-research-h5lgn7tv/home/cache", + "XDG_STATE_HOME": "/tmp/grok-native-research-h5lgn7tv/home/state", + "XDG_DATA_HOME": "/tmp/grok-native-research-h5lgn7tv/home/data", + "NPM_CONFIG_CACHE": "/tmp/grok-native-research-h5lgn7tv/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/grok-native-research-h5lgn7tv/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/grok-native-research-h5lgn7tv/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/grok-native-research-h5lgn7tv/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "TMPDIR": "/tmp/grok-native-research-h5lgn7tv/tmp", + "LANG": "C.UTF-8" + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/inspect-help.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/inspect-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/inspect-help.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/inspect-help.stdout.log new file mode 100644 index 000000000..2f621395e --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/inspect-help.stdout.log @@ -0,0 +1,21 @@ +Show the configuration Grok discovers for this directory + +Usage: grok inspect [OPTIONS] + +Options: + --json + Emit machine-readable JSON output + + --debug + Enable debug logging + + --debug-file + Write debug logs to FILE + + -h, --help + Print help (see a summary with '-h') + + --leader-socket + Use a custom leader socket path instead of the default `~/.grok/leader.sock`. + + A local/branch build can thus run an isolated leader without colliding with the default one already running on the machine. Name it `~/.grok/leader-*.sock` to keep `grok leader list/kill` able to find it; any other location works but won't be auto-discovered diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/plugin-list-help.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/plugin-list-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/plugin-list-help.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/plugin-list-help.stdout.log new file mode 100644 index 000000000..76767c3d1 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/plugin-list-help.stdout.log @@ -0,0 +1,24 @@ +List installed plugins + +Usage: grok plugin list [OPTIONS] + +Options: + --json + Emit machine-readable JSON output + + --available + Include available plugins from marketplace sources. Requires --json + + --debug + Enable debug logging + + --debug-file + Write debug logs to FILE + + -h, --help + Print help (see a summary with '-h') + + --leader-socket + Use a custom leader socket path instead of the default `~/.grok/leader.sock`. + + A local/branch build can thus run an isolated leader without colliding with the default one already running on the machine. Name it `~/.grok/leader-*.sock` to keep `grok leader list/kill` able to find it; any other location works but won't be auto-discovered diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-inspect.stdout.log new file mode 100644 index 000000000..6513b999b --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-inspect.stdout.log @@ -0,0 +1,157 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-a/", + "projectTrusted": false, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [ + { + "name": "uap-project-native-research", + "scope": "project", + "path": "/tmp/grok-native-research-h5lgn7tv/project-a/.grok/plugins/uap-project-native-research", + "enabled": false, + "provides": { + "skills": 1, + "agents": 0, + "hooks": false, + "mcpServers": 0 + } + } + ], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-a-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-inspect.stdout.log new file mode 100644 index 000000000..86e686543 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-inspect.stdout.log @@ -0,0 +1,157 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-b/", + "projectTrusted": true, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [ + { + "name": "uap-project-native-research", + "scope": "user", + "path": "/tmp/grok-native-research-h5lgn7tv/home/.grok/plugins/uap-project-native-research", + "enabled": true, + "provides": { + "skills": 1, + "agents": 0, + "hooks": false, + "mcpServers": 0 + } + } + ], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-a-project-b-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-inspect.stdout.log new file mode 100644 index 000000000..6513b999b --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-inspect.stdout.log @@ -0,0 +1,157 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-a/", + "projectTrusted": false, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [ + { + "name": "uap-project-native-research", + "scope": "project", + "path": "/tmp/grok-native-research-h5lgn7tv/project-a/.grok/plugins/uap-project-native-research", + "enabled": false, + "provides": { + "skills": 1, + "agents": 0, + "hooks": false, + "mcpServers": 0 + } + } + ], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-a-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-inspect.stdout.log new file mode 100644 index 000000000..2780b9d59 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-inspect.stdout.log @@ -0,0 +1,157 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-b/", + "projectTrusted": false, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [ + { + "name": "uap-project-native-research", + "scope": "project", + "path": "/tmp/grok-native-research-h5lgn7tv/project-b/.grok/plugins/uap-project-native-research", + "enabled": false, + "provides": { + "skills": 1, + "agents": 0, + "hooks": false, + "mcpServers": 0 + } + } + ], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/project-b-project-b-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/provenance.json b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/provenance.json new file mode 100644 index 000000000..22750ae2b --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/provenance.json @@ -0,0 +1,58 @@ +{ + "run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40014264873", + "run_id": 40014264873, + "job_id": 120622168241, + "artifact_id": 12531617069, + "artifact_bytes": 19676, + "artifact_sha256": "5a90bbc0f72a82385653ef657932351f45b013f05d1b5a25313dd578b230e4c1", + "head_sha": "249a9e5731822b79df3b07d9da0ef9b432258caa", + "recovery_download_verified": true, + "zip_crc_verified": true, + "all_43_members": true, + "all_log_hashes_and_lengths_verified": true, + "file_sha256": { + "empty-project-a-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty-project-a-inspect.stdout.log": "c46f614771c47a25cc4b7eb9405f99a8f43b9262b4f1bbf8ef2dd9506355ed8e", + "empty-project-a-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty-project-a-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "empty-project-b-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty-project-b-inspect.stdout.log": "2248956b4ea135095445ad43b9e8f8854563166423d4a85b6a46c621ddcbd22c", + "empty-project-b-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty-project-b-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "git-init-project-a.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-init-project-a.stdout.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-init-project-b.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "git-init-project-b.stdout.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "grok-precode-research.json": "8f3e0a646ce1bf29cc13d96e95467fec3c0cfdd7e110bb933eee88d7810282ce", + "inspect-help.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "inspect-help.stdout.log": "6c1e788375ab69df4eb8768532ee052154aa8e51f747965fb389129f332755c4", + "plugin-list-help.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "plugin-list-help.stdout.log": "2e4b4117fcc31d706335b5de0035e45b65802ea950e9b98b15f12db0a1a664dd", + "project-a-project-a-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-a-project-a-inspect.stdout.log": "5f0e6de8c4726ccf55bfcf1249ea26f49ceff137e747e48dd231fc7fbb9315c0", + "project-a-project-a-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-a-project-a-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "project-a-project-b-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-a-project-b-inspect.stdout.log": "a325a2b549c964c4dbff8cf0b841faf41da2a9680e81a949652f77161285b080", + "project-a-project-b-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-a-project-b-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "project-b-project-a-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-b-project-a-inspect.stdout.log": "5f0e6de8c4726ccf55bfcf1249ea26f49ceff137e747e48dd231fc7fbb9315c0", + "project-b-project-a-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-b-project-a-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "project-b-project-b-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-b-project-b-inspect.stdout.log": "e4c4676d3ed57092a3954512a630f9877d1ac1a9bef4ccc59ce05998f63dd1ff", + "project-b-project-b-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "project-b-project-b-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "user-project-a-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "user-project-a-inspect.stdout.log": "dbfe41d5c0447144d238f432d7463c4c6a63d831951723e23b872efee405cecc", + "user-project-a-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "user-project-a-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "user-project-b-inspect.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "user-project-b-inspect.stdout.log": "a325a2b549c964c4dbff8cf0b841faf41da2a9680e81a949652f77161285b080", + "user-project-b-list.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "user-project-b-list.stdout.log": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "version.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "version.stdout.log": "e3180b48f49d248819c0764a6e1010b548d3158f9d738f3c2d161ea03fd37a7e" + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-inspect.stdout.log new file mode 100644 index 000000000..5057b6a82 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-inspect.stdout.log @@ -0,0 +1,157 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-a", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-a/", + "projectTrusted": true, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [ + { + "name": "uap-project-native-research", + "scope": "user", + "path": "/tmp/grok-native-research-h5lgn7tv/home/.grok/plugins/uap-project-native-research", + "enabled": true, + "provides": { + "skills": 1, + "agents": 0, + "hooks": false, + "mcpServers": 0 + } + } + ], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-a-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-inspect.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-inspect.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-inspect.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-inspect.stdout.log new file mode 100644 index 000000000..86e686543 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-inspect.stdout.log @@ -0,0 +1,157 @@ +{ + "grokVersion": "1.0.67", + "channel": "unknown", + "cwd": "/tmp/grok-native-research-h5lgn7tv/project-b", + "projectRoot": "/tmp/grok-native-research-h5lgn7tv/project-b/", + "projectTrusted": true, + "projectInstructions": [], + "permissions": { + "sources": [], + "loaded": 0, + "skipped": [], + "mcpServerAllowlist": [], + "mcpLockdownSources": [], + "mcpManagedServersOnly": "off", + "marketplaceAllowlist": [], + "marketplaceLockdownSources": [], + "managedMarketplaces": [], + "managedSettingsPath": "/etc/claude-code/managed-settings.json", + "managedSettingsExists": false, + "managedSettingsActive": false, + "claudeBypassLockAdvisory": false + }, + "loginPolicy": { + "disableApiKeyAuth": null, + "forceLoginTeamUuid": null, + "apiKeyAuthDisabled": false + }, + "hooks": [], + "skills": [], + "agents": [ + { + "name": "general-purpose", + "description": "General purpose agent for multi-step tasks.", + "source": { + "type": "builtin" + } + }, + { + "name": "explore", + "description": "Fast, read-only agent specialized for codebase exploration.", + "source": { + "type": "builtin" + } + }, + { + "name": "plan", + "description": "Software architect for planning implementation strategies.", + "source": { + "type": "builtin" + } + } + ], + "plugins": [ + { + "name": "uap-project-native-research", + "scope": "user", + "path": "/tmp/grok-native-research-h5lgn7tv/home/.grok/plugins/uap-project-native-research", + "enabled": true, + "provides": { + "skills": 1, + "agents": 0, + "hooks": false, + "mcpServers": 0 + } + } + ], + "marketplaces": [], + "mcpServers": [], + "lspServers": [], + "configSources": { + "layers": [] + }, + "experiments": [], + "externalCompat": { + "remoteSettingsLoaded": false, + "cells": [ + { + "vendor": "cursor", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "cursor", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "skills", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "rules", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "agents", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "mcps", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "hooks", + "enabled": true, + "source": "default" + }, + { + "vendor": "claude", + "surface": "sessions", + "enabled": true, + "source": "default" + }, + { + "vendor": "codex", + "surface": "sessions", + "enabled": true, + "source": "default" + } + ] + } +} diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-list.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-list.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/user-project-b-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/version.stderr.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/version.stdout.log b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/version.stdout.log new file mode 100644 index 000000000..7712e69d8 --- /dev/null +++ b/docs/evidence/project-scope-research/grok-1.0.67-linux-amd64-40014264873/version.stdout.log @@ -0,0 +1 @@ +grok 1.0.67 (2fe064390d07) diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/README.md b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/README.md new file mode 100644 index 000000000..de9889116 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/README.md @@ -0,0 +1,39 @@ +# Qoder 1.1.78: genuine bounded inventory refusal + +The first populated scope collector ran at exact source +`e29237fe546d8aebde98adaae59d346142143d2d` on genuine hosted Ubuntu 24.04: +[run 40130660701](https://github.com/777genius/universal-agent-plugins/actions/runs/40130660701), +[job 120981483006](https://github.com/777genius/universal-agent-plugins/actions/runs/40130660701/job/120981483006). +It failed its observation step and successfully uploaded artifact **12575528479**. +The 6,264-byte ZIP SHA256 is +`acf3d04abec66d0fde470e76fb501efd7bf20621c86c72ee15cfeb5719ee9bb8`. +All 15 flat text members are preserved byte-for-byte; the ZIP is not committed. +`provenance.json` records every member length/hash. All four native and three +auxiliary stdout/stderr pairs were verified against their recorded hashes. + +## What actually ran + +The parent measured exact git HEAD, acquired the pinned official 1.1.78 archive +and verified its 182,982,112-byte executable. The child proved original real and +effective UID/GID 1001, empty supplementary groups, a network namespace distinct +from the parent, only loopback, all capability sets zero and no-new-privileges. +The four version/install-help/list-help/uninstall-help commands each exited zero +with empty stderr. These are fresh actual help observations, not installs. + +The first stage inventory then refused `home/.qoder/logs/latest`: a native-created +symlink, mode 0777 and 42-byte target length. The collector intentionally did not +read its target bytes or follow it. No list or install command was reached, so +this artifact contains no populated registry schema, source attribution or +installation outcome. The complete source fixture snapshots and six file pins +were retained; all eight qualification fields remain false and `success:false` +is unchanged. Independent bounded cleanup completed successfully. + +## Consequence + +The collector's strict refusal worked as designed. A concrete reviewed amendment +may record bounded **link metadata only** for this exact native log alias while +keeping every source fixture and all other links/specials strict. This failure +must not be normalized to successful project discovery, and metadata admission +must never read a target path, relax source pinning, change runtime authority, +add native commands or promote qualification. No client or acquisition was rerun +locally during artifact interpretation. diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/cleanup.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/cleanup.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/install-help.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/install-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/install-help.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/install-help.stdout.log new file mode 100644 index 000000000..f7407c261 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/install-help.stdout.log @@ -0,0 +1,16 @@ +Usage: qodercli plugins install|i [options] + +Install a plugin from available marketplaces, a remote HTTPS .zip URL, or a +local directory path. + +Arguments: + plugin Plugin name, plugin@marketplace, remote HTTPS .zip URL, + or local plugin directory path. + +Options: + -s, --scope Installation scope: "user" (global, default), "project" + (shared, committed to VCS), or "local" (personal, not + committed). (choices: "user", "project", "local", + default: "user") + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/isolated-collector.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/isolated-collector.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/isolated-collector.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/isolated-collector.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/list-help.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/list-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/list-help.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/list-help.stdout.log new file mode 100644 index 000000000..9cb9a927e --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/list-help.stdout.log @@ -0,0 +1,12 @@ +Usage: qodercli plugins list [options] + +List installed plugins. + +Options: + --json Output as JSON. (default: false) + --available Include available plugins from marketplaces + (requires --json). (default: false) + -o, --output-format The format of the CLI output. (choices: "text", + "json") + --plugin-dir Additional plugin directories to scan. + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/provenance.json b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/provenance.json new file mode 100644 index 000000000..581862449 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/provenance.json @@ -0,0 +1,91 @@ +{ + "evidence_kind": "genuine-hosted-research-failure-provenance", + "source_commit": "e29237fe546d8aebde98adaae59d346142143d2d", + "run_id": 40130660701, + "job_id": 120981483006, + "artifact_id": 12575528479, + "archive_bytes": 6264, + "archive_sha256": "acf3d04abec66d0fde470e76fb501efd7bf20621c86c72ee15cfeb5719ee9bb8", + "archive_committed": false, + "member_hashes_and_lengths_verified": true, + "native_probe_stream_pairs_verified": 4, + "auxiliary_stream_pairs_verified": 3, + "qualified_project_installation": false, + "files": [ + { + "name": "cleanup.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "cleanup.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "install-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "install-help.stdout.log", + "bytes": 681, + "sha256": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e" + }, + { + "name": "isolated-collector.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "isolated-collector.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "list-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "list-help.stdout.log", + "bytes": 531, + "sha256": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b" + }, + { + "name": "qoder-populated-research.json", + "bytes": 13521, + "sha256": "96c65504aefbb511456bdbd9979924a96e6d9cdc6d9bbe7b5c969cf0889c8be2" + }, + { + "name": "source-checkout.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "source-checkout.stdout.log", + "bytes": 41, + "sha256": "c5fd24e8b59928f6ae0d8a1a793781b6c708a93c023ad73e868a7c32e66d5cd9" + }, + { + "name": "uninstall-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uninstall-help.stdout.log", + "bytes": 606, + "sha256": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9" + }, + { + "name": "version.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "version.stdout.log", + "bytes": 7, + "sha256": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde" + } + ] +} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/qoder-populated-research.json b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/qoder-populated-research.json new file mode 100644 index 000000000..8b110919b --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/qoder-populated-research.json @@ -0,0 +1,364 @@ +{ + "evidence_kind": "qoder-populated-scope-research", + "version": "1.1.78", + "expected_source_commit": "e29237fe546d8aebde98adaae59d346142143d2d", + "success": false, + "probes": [ + { + "probe": "version", + "argv": [ + "/tmp/qoder-populated-eyag1kqi/binary/qoder", + "--version" + ], + "cwd": "/tmp/qoder-populated-eyag1kqi/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.241, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 7, + "stderr_bytes": 0, + "stdout_sha256": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "install-help", + "argv": [ + "/tmp/qoder-populated-eyag1kqi/binary/qoder", + "plugins", + "install", + "--help" + ], + "cwd": "/tmp/qoder-populated-eyag1kqi/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.284, + "stdout_file": "install-help.stdout.log", + "stderr_file": "install-help.stderr.log", + "stdout_bytes": 681, + "stderr_bytes": 0, + "stdout_sha256": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "list-help", + "argv": [ + "/tmp/qoder-populated-eyag1kqi/binary/qoder", + "plugins", + "list", + "--help" + ], + "cwd": "/tmp/qoder-populated-eyag1kqi/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.229, + "stdout_file": "list-help.stdout.log", + "stderr_file": "list-help.stderr.log", + "stdout_bytes": 531, + "stderr_bytes": 0, + "stdout_sha256": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "uninstall-help", + "argv": [ + "/tmp/qoder-populated-eyag1kqi/binary/qoder", + "plugins", + "uninstall", + "--help" + ], + "cwd": "/tmp/qoder-populated-eyag1kqi/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.224, + "stdout_file": "uninstall-help.stdout.log", + "stderr_file": "uninstall-help.stderr.log", + "stdout_bytes": 606, + "stderr_bytes": 0, + "stdout_sha256": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + } + ], + "inventories": {}, + "project_ownership_qualified": false, + "native_activation_qualified": false, + "loaded_skills_qualified": false, + "mcp_qualified": false, + "trust_qualified": false, + "model_qualified": false, + "authentication_qualified": false, + "native_session_restart_qualified": false, + "isolation": { + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "groups": [], + "network_namespace": "net:[4026532312]", + "parent_network_namespace": "net:[4026531833]", + "interfaces": [ + "lo" + ], + "no_new_privs": "1", + "capabilities": { + "CapInh": "0000000000000000", + "CapPrm": "0000000000000000", + "CapEff": "0000000000000000", + "CapBnd": "0000000000000000", + "CapAmb": "0000000000000000" + } + }, + "native_environment": { + "HOME": "/tmp/qoder-populated-eyag1kqi/home", + "USERPROFILE": "/tmp/qoder-populated-eyag1kqi/home", + "XDG_CONFIG_HOME": "/tmp/qoder-populated-eyag1kqi/home/config", + "XDG_CACHE_HOME": "/tmp/qoder-populated-eyag1kqi/home/cache", + "XDG_STATE_HOME": "/tmp/qoder-populated-eyag1kqi/home/state", + "XDG_DATA_HOME": "/tmp/qoder-populated-eyag1kqi/home/data", + "NPM_CONFIG_CACHE": "/tmp/qoder-populated-eyag1kqi/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/qoder-populated-eyag1kqi/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/qoder-populated-eyag1kqi/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/qoder-populated-eyag1kqi/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "TMPDIR": "/tmp/qoder-populated-eyag1kqi/tmp", + "LANG": "C.UTF-8" + }, + "fixture_pins": { + "/tmp/qoder-populated-eyag1kqi/sources/user/.qoder-plugin/plugin.json": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3", + "/tmp/qoder-populated-eyag1kqi/sources/user/skills/marker/SKILL.md": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126", + "/tmp/qoder-populated-eyag1kqi/sources/A/.qoder-plugin/plugin.json": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64", + "/tmp/qoder-populated-eyag1kqi/sources/A/skills/marker/SKILL.md": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7", + "/tmp/qoder-populated-eyag1kqi/sources/B/.qoder-plugin/plugin.json": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef", + "/tmp/qoder-populated-eyag1kqi/sources/B/skills/marker/SKILL.md": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + }, + "source_baseline": { + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "fixed_fixture_bodies": { + "/tmp/qoder-populated-eyag1kqi/sources/user/.qoder-plugin/plugin.json": "{\"name\": \"uap-scope-research\", \"version\": \"0.9.0\", \"description\": \"Inert scoped research fixture\"}\n", + "/tmp/qoder-populated-eyag1kqi/sources/user/skills/marker/SKILL.md": "---\nname: marker\ndescription: Inert USER research marker\n---\nUSER\n", + "/tmp/qoder-populated-eyag1kqi/sources/A/.qoder-plugin/plugin.json": "{\"name\": \"uap-scope-research\", \"version\": \"1.0.0\", \"description\": \"Inert scoped research fixture\"}\n", + "/tmp/qoder-populated-eyag1kqi/sources/A/skills/marker/SKILL.md": "---\nname: marker\ndescription: Inert A research marker\n---\nA\n", + "/tmp/qoder-populated-eyag1kqi/sources/B/.qoder-plugin/plugin.json": "{\"name\": \"uap-scope-research\", \"version\": \"1.1.0\", \"description\": \"Inert scoped research fixture\"}\n", + "/tmp/qoder-populated-eyag1kqi/sources/B/skills/marker/SKILL.md": "---\nname: marker\ndescription: Inert B research marker\n---\nB\n" + }, + "stage": "empty", + "error": "InventoryError: inventory symlink or special file refused", + "partial_inventory": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "refused", + "type": "0o120000" + } + }, + "acquisition": { + "url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.78/qodercli-linux-x64.tar.gz", + "version": "1.1.78", + "archive_integrity": "sha256:5e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea", + "archive_bytes": 78115289, + "binary_sha256": "0da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953", + "binary_bytes": 182982112 + }, + "wrapper": { + "probe": "isolated-collector", + "argv": [ + "/usr/bin/sudo", + "-n", + "/usr/bin/unshare", + "--net", + "--", + "/usr/bin/setpriv", + "--reuid=1001", + "--regid=1001", + "--clear-groups", + "--inh-caps=-all", + "--ambient-caps=-all", + "--bounding-set=-all", + "--no-new-privs", + "/usr/bin/env", + "-i", + "PATH=/usr/bin:/bin", + "LANG=C.UTF-8", + "PYTHONDONTWRITEBYTECODE=1", + "GITHUB_ACTIONS=true", + "RUNNER_ENVIRONMENT=github-hosted", + "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED=1", + "EXPECTED_COMMIT=e29237fe546d8aebde98adaae59d346142143d2d", + "/usr/bin/python3", + "/home/runner/work/universal-agent-plugins/universal-agent-plugins/scripts/research-qoder-populated.py", + "--collect", + "/tmp/qoder-populated-eyag1kqi/request.json" + ], + "cwd": "/tmp/qoder-populated-eyag1kqi", + "error": null, + "exit": 1, + "elapsed_seconds": 1.865, + "stdout_file": "isolated-collector.stdout.log", + "stderr_file": "isolated-collector.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "checkout": { + "probe": "source-checkout", + "argv": [ + "/usr/bin/git", + "rev-parse", + "HEAD" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.003, + "stdout_file": "source-checkout.stdout.log", + "stderr_file": "source-checkout.stderr.log", + "stdout_bytes": 41, + "stderr_bytes": 0, + "stdout_sha256": "c5fd24e8b59928f6ae0d8a1a793781b6c708a93c023ad73e868a7c32e66d5cd9", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "source_commit": "e29237fe546d8aebde98adaae59d346142143d2d", + "observed_source_commit": "e29237fe546d8aebde98adaae59d346142143d2d", + "cleanup": { + "complete": true, + "probe": { + "probe": "cleanup", + "argv": [ + "/usr/bin/python3", + "-c", + "\nimport os,stat,sys\npath,device,inode=sys.argv[1:]\nparent,name=os.path.split(path)\npfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)\nroot=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd)\nidentity=os.fstat(root)\nif (identity.st_dev,identity.st_ino)!=(int(device),int(inode)):raise SystemExit(\"cleanup root identity changed\")\ndef clear(fd):\n for name in os.listdir(fd):\n item=os.stat(name,dir_fd=fd,follow_symlinks=False)\n if stat.S_ISDIR(item.st_mode):\n child=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)\n try:\n if not os.path.samestat(item,os.fstat(child)):raise RuntimeError(\"cleanup directory changed\")\n clear(child)\n finally:os.close(child)\n os.rmdir(name,dir_fd=fd)\n else:os.unlink(name,dir_fd=fd)\nclear(root)\nif not os.path.samestat(identity,os.stat(name,dir_fd=pfd,follow_symlinks=False)):raise SystemExit(\"cleanup root replaced\")\nos.close(root)\nos.rmdir(name,dir_fd=pfd)\nos.close(pfd)\n", + "/tmp/qoder-populated-eyag1kqi", + "66305", + "9204715" + ], + "cwd": "/home/runner/work/_temp/qoder-populated-research", + "error": null, + "exit": 0, + "elapsed_seconds": 0.031, + "stdout_file": "cleanup.stdout.log", + "stderr_file": "cleanup.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } +} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/source-checkout.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/source-checkout.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/source-checkout.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/source-checkout.stdout.log new file mode 100644 index 000000000..331a89c96 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/source-checkout.stdout.log @@ -0,0 +1 @@ +e29237fe546d8aebde98adaae59d346142143d2d diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/uninstall-help.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/uninstall-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/uninstall-help.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/uninstall-help.stdout.log new file mode 100644 index 000000000..4e9297a4d --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/uninstall-help.stdout.log @@ -0,0 +1,15 @@ +Usage: qodercli plugins uninstall|remove [options] + +Uninstall a plugin. + +Arguments: + plugin Plugin name to uninstall. + +Options: + -s, --scope Scope to uninstall from: "user" (global, default), + "project" (shared, committed to VCS), or "local" + (personal, not committed). (choices: "user", "project", + "local", default: "user") + --keep-data Preserve the plugin data directory. (default: false) + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/version.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/version.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/version.stdout.log new file mode 100644 index 000000000..8cf600ff1 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/version.stdout.log @@ -0,0 +1 @@ +1.1.78 diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-install.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-install.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-install.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-install.stdout.log new file mode 100644 index 000000000..c85540fcb --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-install.stdout.log @@ -0,0 +1 @@ +{"ok":true,"pluginId":"uap-scope-research@local","version":"1.0.0","path":"/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0"} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-A-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-A-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-A-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-A-list.stdout.log new file mode 100644 index 000000000..462529406 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-A-list.stdout.log @@ -0,0 +1,51 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-B-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-B-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-B-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-B-list.stdout.log new file mode 100644 index 000000000..462529406 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-project-B-list.stdout.log @@ -0,0 +1,51 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-user-cwd-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-user-cwd-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-user-cwd-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-user-cwd-list.stdout.log new file mode 100644 index 000000000..462529406 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/A-installed-user-cwd-list.stdout.log @@ -0,0 +1,51 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-install.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-install.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-install.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-install.stdout.log new file mode 100644 index 000000000..c0f7aec97 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-install.stdout.log @@ -0,0 +1 @@ +{"ok":true,"pluginId":"uap-scope-research@local","version":"1.1.0","path":"/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0"} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-A-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-A-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-A-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-A-list.stdout.log new file mode 100644 index 000000000..4aa9f7f96 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-A-list.stdout.log @@ -0,0 +1,76 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-B-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-B-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-B-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-B-list.stdout.log new file mode 100644 index 000000000..4aa9f7f96 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-project-B-list.stdout.log @@ -0,0 +1,76 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-user-cwd-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-user-cwd-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-user-cwd-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-user-cwd-list.stdout.log new file mode 100644 index 000000000..4aa9f7f96 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/B-installed-user-cwd-list.stdout.log @@ -0,0 +1,76 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/README.md b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/README.md new file mode 100644 index 000000000..83db30fa3 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/README.md @@ -0,0 +1,59 @@ +# Qoder 1.1.78 populated scoped inventory: genuine hosted observations + +Run [40135008690](https://github.com/777genius/universal-agent-plugins/actions/runs/40135008690), +job120994562933, exact source2b0ef18146c28da9b6a9b1ef86a0fb413d950eee. +Publisher archive and executable pins remain the reviewed1.1.78 pins. This is a +successful finite research collection, not a UAP/native qualification result. + +Artifact12577491506 contains45flat text members,177172uncompressed bytes. The +20745byte GitHub ZIP has SHA256 +`88c9374cf0ddac3eebc30cf10e828a50942e444cccc07036119109c5601ec975`. +[Provenance](provenance.json) preserves every original member length/hash; those +45members are copied here byte-identically. The ZIP CRC, all22capture records +and44stream receipts, native observed_json equality, six fixed source bodies and +all eight complete source baselines were independently verified. The APM and +Skillshare workers independently interpreted the actual raw observations. + +All19native commands exited0 with clean stderr. The wrapper measured original +and effective UID/GID1001, empty groups, a distinct network namespace with only +loopback, zero capability sets and NNP. Cleanup completed. The only admitted +native log alias remained metadata-only with target_followed=false. + +## What was actually observed + +The fixtures use one disposable original HOME and three separate ordinary +working directories. A/B are **plain directories without Git repositories**. +Same portable manifest name `uap-scope-research` has user version0.9.0, +A version1.0.0 and B version1.1.0, with distinct inert skill marker descriptions. + +At each empty/user/A/B stage the three `plugins list --json` results are +byte-identical across user/A/B working directories. Row counts advance0→1→2→3. +The final list has three records with the same `id` and `source` +`uap-scope-research@local`: user scope plus two project-scope rows with exact +projectPath A and B. Every installPath points into the same original user's +`.qoder/plugins/cache/local/uap-scope-research/` tree. The captured +inventory's cache file hashes match all six authored source pins. + +The original user installed_plugins_v2.json grows310→612→914bytes while user +settings.json retains the same hash after the project adds. All eight inventories +show A/B with no child entries. This proves the behavior of these exact non-Git +fixtures. It does **not** prove that native project installation never writes +project settings in a Git repository or that the documented VCS contract is +false. The native registry file's body was not captured; its structure must not +be invented from its length/hash. + +The native list therefore supplies scoped registration attribution through +projectPath. It is a global inventory rather than a cwd-filtered list, and its +resource metadata does not establish actual loaded-skill selection. Different +versions were used: same-name/same-version cache coupling and update/uninstall +behavior remain unobserved. No native uninstall was executed. + +## Boundaries retained + +All eight qualification fields in the original summary remainfalse: project +ownership, native activation, loaded skills, MCP, trust, model, authentication +and session restart. No model task, server, login, trust grant, actual runtime +selection or UAP lifecycle was exercised. The native CLI created fresh machine/ +installation identity metadata and logs in the disposable authority; no existing +user credentials were copied, and no authentication success follows from those +file creations. The earlier log-alias failure remains preserved separately. diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/cleanup.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/cleanup.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-A-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-A-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-A-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-A-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-A-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-B-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-B-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-B-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-B-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-project-B-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-user-cwd-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-user-cwd-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-user-cwd-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-user-cwd-list.stdout.log new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/empty-user-cwd-list.stdout.log @@ -0,0 +1 @@ +[] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/install-help.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/install-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/install-help.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/install-help.stdout.log new file mode 100644 index 000000000..f7407c261 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/install-help.stdout.log @@ -0,0 +1,16 @@ +Usage: qodercli plugins install|i [options] + +Install a plugin from available marketplaces, a remote HTTPS .zip URL, or a +local directory path. + +Arguments: + plugin Plugin name, plugin@marketplace, remote HTTPS .zip URL, + or local plugin directory path. + +Options: + -s, --scope Installation scope: "user" (global, default), "project" + (shared, committed to VCS), or "local" (personal, not + committed). (choices: "user", "project", "local", + default: "user") + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/isolated-collector.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/isolated-collector.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/isolated-collector.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/isolated-collector.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/list-help.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/list-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/list-help.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/list-help.stdout.log new file mode 100644 index 000000000..9cb9a927e --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/list-help.stdout.log @@ -0,0 +1,12 @@ +Usage: qodercli plugins list [options] + +List installed plugins. + +Options: + --json Output as JSON. (default: false) + --available Include available plugins from marketplaces + (requires --json). (default: false) + -o, --output-format The format of the CLI output. (choices: "text", + "json") + --plugin-dir Additional plugin directories to scan. + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/provenance.json b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/provenance.json new file mode 100644 index 000000000..115b15283 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/provenance.json @@ -0,0 +1,236 @@ +{ + "run_id": 40135008690, + "job_id": 120994562933, + "artifact_id": 12577491506, + "source_commit": "2b0ef18146c28da9b6a9b1ef86a0fb413d950eee", + "artifact_zip_bytes": 20745, + "artifact_zip_sha256": "88c9374cf0ddac3eebc30cf10e828a50942e444cccc07036119109c5601ec975", + "members": [ + { + "name": "A-installed-install.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "A-installed-install.stdout.log", + "bytes": 164, + "sha256": "57f14dbb71c4ea336113cd4c7b5b828455e2978a166fe2ed2251bffe8a0e923f" + }, + { + "name": "A-installed-project-A-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "A-installed-project-A-list.stdout.log", + "bytes": 1383, + "sha256": "d3a12745ed090574358c657bf98f0fbf9a8fbba07c3486a7435d99f4dd30d0b1" + }, + { + "name": "A-installed-project-B-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "A-installed-project-B-list.stdout.log", + "bytes": 1383, + "sha256": "d3a12745ed090574358c657bf98f0fbf9a8fbba07c3486a7435d99f4dd30d0b1" + }, + { + "name": "A-installed-user-cwd-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "A-installed-user-cwd-list.stdout.log", + "bytes": 1383, + "sha256": "d3a12745ed090574358c657bf98f0fbf9a8fbba07c3486a7435d99f4dd30d0b1" + }, + { + "name": "B-installed-install.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "B-installed-install.stdout.log", + "bytes": 164, + "sha256": "d702b21a249daad6c9fa897dcaa07914a3ea3c6b70bc57145cc49fc4963aa3ac" + }, + { + "name": "B-installed-project-A-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "B-installed-project-A-list.stdout.log", + "bytes": 2104, + "sha256": "446f9d8525d200e95d425af0f689220803cbefa3545af314becf029b7e7c602e" + }, + { + "name": "B-installed-project-B-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "B-installed-project-B-list.stdout.log", + "bytes": 2104, + "sha256": "446f9d8525d200e95d425af0f689220803cbefa3545af314becf029b7e7c602e" + }, + { + "name": "B-installed-user-cwd-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "B-installed-user-cwd-list.stdout.log", + "bytes": 2104, + "sha256": "446f9d8525d200e95d425af0f689220803cbefa3545af314becf029b7e7c602e" + }, + { + "name": "cleanup.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "cleanup.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "empty-project-A-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "empty-project-A-list.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "empty-project-B-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "empty-project-B-list.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "empty-user-cwd-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "empty-user-cwd-list.stdout.log", + "bytes": 3, + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "name": "install-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "install-help.stdout.log", + "bytes": 681, + "sha256": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e" + }, + { + "name": "isolated-collector.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "isolated-collector.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "list-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "list-help.stdout.log", + "bytes": 531, + "sha256": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b" + }, + { + "name": "qoder-populated-research.json", + "bytes": 162358, + "sha256": "9370fdc369e144f6b21ba9e49a376fdcd930b9032d6d3f7465db69cf637fa114" + }, + { + "name": "source-checkout.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "source-checkout.stdout.log", + "bytes": 41, + "sha256": "4291bd51263f1b9d3595e402a286ce3f37d93df2592669a78f1a168afe6300de" + }, + { + "name": "uninstall-help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "uninstall-help.stdout.log", + "bytes": 606, + "sha256": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9" + }, + { + "name": "user-installed-install.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "user-installed-install.stdout.log", + "bytes": 164, + "sha256": "3e59512de5812417e02837d98f003aca1bc1e6d8ed5551507f43bad75610d03d" + }, + { + "name": "user-installed-project-A-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "user-installed-project-A-list.stdout.log", + "bytes": 662, + "sha256": "09c3e318bcb0cc98840d7a9cf74bb834d203379604b1263194b928f0fd8b54b4" + }, + { + "name": "user-installed-project-B-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "user-installed-project-B-list.stdout.log", + "bytes": 662, + "sha256": "09c3e318bcb0cc98840d7a9cf74bb834d203379604b1263194b928f0fd8b54b4" + }, + { + "name": "user-installed-user-cwd-list.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "user-installed-user-cwd-list.stdout.log", + "bytes": 662, + "sha256": "09c3e318bcb0cc98840d7a9cf74bb834d203379604b1263194b928f0fd8b54b4" + }, + { + "name": "version.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "version.stdout.log", + "bytes": 7, + "sha256": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde" + } + ], + "verification": "ZIP length/hash/CRC, all45 exact raw members, all22 capture stream receipts and observed_json equality, six body pins and full source baselines across eight inventories verified; all qualification fields remain false" +} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/qoder-populated-research.json b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/qoder-populated-research.json new file mode 100644 index 000000000..a3c898ec1 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/qoder-populated-research.json @@ -0,0 +1,4927 @@ +{ + "evidence_kind": "qoder-populated-scope-research", + "version": "1.1.78", + "expected_source_commit": "2b0ef18146c28da9b6a9b1ef86a0fb413d950eee", + "success": true, + "probes": [ + { + "probe": "version", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "--version" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.333, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 7, + "stderr_bytes": 0, + "stdout_sha256": "c4686cb8ea686d3220f88295494aaebff51a91ac90d64a6b5eb48b27d6ce6fde", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "install-help", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "install", + "--help" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.335, + "stdout_file": "install-help.stdout.log", + "stderr_file": "install-help.stderr.log", + "stdout_bytes": 681, + "stderr_bytes": 0, + "stdout_sha256": "b2a99711c310da0df75a1918b42fe82870bcfc198221213e46bd48e79c0ec96e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "list-help", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--help" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.335, + "stdout_file": "list-help.stdout.log", + "stderr_file": "list-help.stderr.log", + "stdout_bytes": 531, + "stderr_bytes": 0, + "stdout_sha256": "9d29ee8027c2862585f860bea7e53bc5253654137ee88fd6ce8df49ef31e167b", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "uninstall-help", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "uninstall", + "--help" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.307, + "stdout_file": "uninstall-help.stdout.log", + "stderr_file": "uninstall-help.stderr.log", + "stdout_bytes": 606, + "stderr_bytes": 0, + "stdout_sha256": "8d43b1ec885561af42110e20bf202ad858b0ad7787c533ac1d13105acfcc3ca9", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true + }, + { + "probe": "empty-user-cwd-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.387, + "stdout_file": "empty-user-cwd-list.stdout.log", + "stderr_file": "empty-user-cwd-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [] + }, + { + "probe": "empty-project-A-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-A", + "error": null, + "exit": 0, + "elapsed_seconds": 0.378, + "stdout_file": "empty-project-A-list.stdout.log", + "stderr_file": "empty-project-A-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [] + }, + { + "probe": "empty-project-B-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-B", + "error": null, + "exit": 0, + "elapsed_seconds": 0.374, + "stdout_file": "empty-project-B-list.stdout.log", + "stderr_file": "empty-project-B-list.stderr.log", + "stdout_bytes": 3, + "stderr_bytes": 0, + "stdout_sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [] + }, + { + "probe": "user-installed-install", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "install", + "/tmp/qoder-populated-w490rb05/sources/user", + "--scope", + "user", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.378, + "stdout_file": "user-installed-install.stdout.log", + "stderr_file": "user-installed-install.stderr.log", + "stdout_bytes": 164, + "stderr_bytes": 0, + "stdout_sha256": "3e59512de5812417e02837d98f003aca1bc1e6d8ed5551507f43bad75610d03d", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": { + "ok": true, + "pluginId": "uap-scope-research@local", + "version": "0.9.0", + "path": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0" + } + }, + { + "probe": "user-installed-user-cwd-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.387, + "stdout_file": "user-installed-user-cwd-list.stdout.log", + "stderr_file": "user-installed-user-cwd-list.stderr.log", + "stdout_bytes": 662, + "stderr_bytes": 0, + "stdout_sha256": "09c3e318bcb0cc98840d7a9cf74bb834d203379604b1263194b928f0fd8b54b4", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "user-installed-project-A-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-A", + "error": null, + "exit": 0, + "elapsed_seconds": 0.38, + "stdout_file": "user-installed-project-A-list.stdout.log", + "stderr_file": "user-installed-project-A-list.stderr.log", + "stdout_bytes": 662, + "stderr_bytes": 0, + "stdout_sha256": "09c3e318bcb0cc98840d7a9cf74bb834d203379604b1263194b928f0fd8b54b4", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "user-installed-project-B-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-B", + "error": null, + "exit": 0, + "elapsed_seconds": 0.382, + "stdout_file": "user-installed-project-B-list.stdout.log", + "stderr_file": "user-installed-project-B-list.stderr.log", + "stdout_bytes": 662, + "stderr_bytes": 0, + "stdout_sha256": "09c3e318bcb0cc98840d7a9cf74bb834d203379604b1263194b928f0fd8b54b4", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "A-installed-install", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "install", + "/tmp/qoder-populated-w490rb05/sources/A", + "--scope", + "project", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-A", + "error": null, + "exit": 0, + "elapsed_seconds": 0.385, + "stdout_file": "A-installed-install.stdout.log", + "stderr_file": "A-installed-install.stderr.log", + "stdout_bytes": 164, + "stderr_bytes": 0, + "stdout_sha256": "57f14dbb71c4ea336113cd4c7b5b828455e2978a166fe2ed2251bffe8a0e923f", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": { + "ok": true, + "pluginId": "uap-scope-research@local", + "version": "1.0.0", + "path": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0" + } + }, + { + "probe": "A-installed-user-cwd-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.366, + "stdout_file": "A-installed-user-cwd-list.stdout.log", + "stderr_file": "A-installed-user-cwd-list.stderr.log", + "stdout_bytes": 1383, + "stderr_bytes": 0, + "stdout_sha256": "d3a12745ed090574358c657bf98f0fbf9a8fbba07c3486a7435d99f4dd30d0b1", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "A-installed-project-A-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-A", + "error": null, + "exit": 0, + "elapsed_seconds": 0.375, + "stdout_file": "A-installed-project-A-list.stdout.log", + "stderr_file": "A-installed-project-A-list.stderr.log", + "stdout_bytes": 1383, + "stderr_bytes": 0, + "stdout_sha256": "d3a12745ed090574358c657bf98f0fbf9a8fbba07c3486a7435d99f4dd30d0b1", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "A-installed-project-B-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-B", + "error": null, + "exit": 0, + "elapsed_seconds": 0.378, + "stdout_file": "A-installed-project-B-list.stdout.log", + "stderr_file": "A-installed-project-B-list.stderr.log", + "stdout_bytes": 1383, + "stderr_bytes": 0, + "stdout_sha256": "d3a12745ed090574358c657bf98f0fbf9a8fbba07c3486a7435d99f4dd30d0b1", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "B-installed-install", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "install", + "/tmp/qoder-populated-w490rb05/sources/B", + "--scope", + "project", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-B", + "error": null, + "exit": 0, + "elapsed_seconds": 0.391, + "stdout_file": "B-installed-install.stdout.log", + "stderr_file": "B-installed-install.stderr.log", + "stdout_bytes": 164, + "stderr_bytes": 0, + "stdout_sha256": "d702b21a249daad6c9fa897dcaa07914a3ea3c6b70bc57145cc49fc4963aa3ac", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": { + "ok": true, + "pluginId": "uap-scope-research@local", + "version": "1.1.0", + "path": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0" + } + }, + { + "probe": "B-installed-user-cwd-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/user-cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.383, + "stdout_file": "B-installed-user-cwd-list.stdout.log", + "stderr_file": "B-installed-user-cwd-list.stderr.log", + "stdout_bytes": 2104, + "stderr_bytes": 0, + "stdout_sha256": "446f9d8525d200e95d425af0f689220803cbefa3545af314becf029b7e7c602e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "B-installed-project-A-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-A", + "error": null, + "exit": 0, + "elapsed_seconds": 0.386, + "stdout_file": "B-installed-project-A-list.stdout.log", + "stderr_file": "B-installed-project-A-list.stderr.log", + "stdout_bytes": 2104, + "stderr_bytes": 0, + "stdout_sha256": "446f9d8525d200e95d425af0f689220803cbefa3545af314becf029b7e7c602e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + }, + { + "probe": "B-installed-project-B-list", + "argv": [ + "/tmp/qoder-populated-w490rb05/binary/qoder", + "plugins", + "list", + "--json" + ], + "cwd": "/tmp/qoder-populated-w490rb05/project-B", + "error": null, + "exit": 0, + "elapsed_seconds": 0.366, + "stdout_file": "B-installed-project-B-list.stdout.log", + "stderr_file": "B-installed-project-B-list.stderr.log", + "stdout_bytes": 2104, + "stderr_bytes": 0, + "stdout_sha256": "446f9d8525d200e95d425af0f689220803cbefa3545af314becf029b7e7c602e", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "accepted": true, + "observed_json": [ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } + ] + } + ], + "inventories": { + "empty-before": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "a72cd66e0614876fde43a491383edfdcb262771b1fdc4f59b3773836b6b6990f", + "target_hex": "72756e732f323032362d31302d32345432322d30382d30342d3537345a2d616a736b6e362d7032343536", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "empty-after": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "8dda02961aaf40fed4fd3118297ae245b44d04be41a23f58981f12df75a7f8f4", + "target_hex": "72756e732f323032362d31302d32345432322d30382d30362d3431395a2d617269616c712d7032353032", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "user-installed-before": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "8dda02961aaf40fed4fd3118297ae245b44d04be41a23f58981f12df75a7f8f4", + "target_hex": "72756e732f323032362d31302d32345432322d30382d30362d3431395a2d617269616c712d7032353032", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "user-installed-after": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "f3f960f3e0288ca5060f14a23b440acebe01dbad2d5a957c43d74a46af9bd268", + "target_hex": "72756e732f323032362d31302d32345432322d30382d30392d3039385a2d7262656436672d7032353632", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/manifest.json": { + "mode": "0o644", + "size": 677, + "kind": "file", + "sha256": "de153e7a835758e98385298e8697bb98143f5620359b7a0b3a00860cccc5d58f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/qodercli.log": { + "mode": "0o644", + "size": 10076, + "kind": "file", + "sha256": "708a9719dac391615ba95ddaa3585fd40eca4b86fdcb04fe3a73d565eb814f34" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "04c6451092c7d6be3f3d7ebd20a4e09de7a7e5e53b5a260642623bde0bd14152" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "2c41d31d3a9e24402304f152cf60fc9a9f604b8230c17795ef4ad4e9198e352b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "e1773a23a3a951161c84e2743f8367130501b94460d06f0aa8380600e06a334c" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "ee854966283cb8b6d2059ebe4a347a205c352339ea74b8ae4c1a629fa6662e5e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "daac07ec01259286107d03e547b4b3926da945bd7ca2c357823c4d4791453019" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "5567acfe57bc46890067c820c6a92072c8ec6cef5e5235212896cdac7f396e16" + }, + "home/.qoder/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "home/.qoder/plugins/installed_plugins_v2.json": { + "mode": "0o644", + "size": 310, + "kind": "file", + "sha256": "cbb5d70c46e9005470b35a1e82c827024facb79f343d334f94afabf329f05ec4" + }, + "home/.qoder/settings.json": { + "mode": "0o600", + "size": 66, + "kind": "file", + "sha256": "a81ec84331eeeab50ace0da9ce7bb344fb89b64f1ab00b10b46d6ead2554fc99" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "A-installed-before": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "f3f960f3e0288ca5060f14a23b440acebe01dbad2d5a957c43d74a46af9bd268", + "target_hex": "72756e732f323032362d31302d32345432322d30382d30392d3039385a2d7262656436672d7032353632", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/manifest.json": { + "mode": "0o644", + "size": 677, + "kind": "file", + "sha256": "de153e7a835758e98385298e8697bb98143f5620359b7a0b3a00860cccc5d58f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/qodercli.log": { + "mode": "0o644", + "size": 10076, + "kind": "file", + "sha256": "708a9719dac391615ba95ddaa3585fd40eca4b86fdcb04fe3a73d565eb814f34" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "04c6451092c7d6be3f3d7ebd20a4e09de7a7e5e53b5a260642623bde0bd14152" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "2c41d31d3a9e24402304f152cf60fc9a9f604b8230c17795ef4ad4e9198e352b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "e1773a23a3a951161c84e2743f8367130501b94460d06f0aa8380600e06a334c" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "ee854966283cb8b6d2059ebe4a347a205c352339ea74b8ae4c1a629fa6662e5e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "daac07ec01259286107d03e547b4b3926da945bd7ca2c357823c4d4791453019" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "5567acfe57bc46890067c820c6a92072c8ec6cef5e5235212896cdac7f396e16" + }, + "home/.qoder/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "home/.qoder/plugins/installed_plugins_v2.json": { + "mode": "0o644", + "size": 310, + "kind": "file", + "sha256": "cbb5d70c46e9005470b35a1e82c827024facb79f343d334f94afabf329f05ec4" + }, + "home/.qoder/settings.json": { + "mode": "0o600", + "size": 66, + "kind": "file", + "sha256": "a81ec84331eeeab50ace0da9ce7bb344fb89b64f1ab00b10b46d6ead2554fc99" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "A-installed-after": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "eb96af0546f71e00a0089e8f0dda88df8e9e56d18300460f8eabb9ed2e2e8bd7", + "target_hex": "72756e732f323032362d31302d32345432322d30382d31312d3736315a2d61696e3269612d7032363236", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/manifest.json": { + "mode": "0o644", + "size": 677, + "kind": "file", + "sha256": "de153e7a835758e98385298e8697bb98143f5620359b7a0b3a00860cccc5d58f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/qodercli.log": { + "mode": "0o644", + "size": 10076, + "kind": "file", + "sha256": "708a9719dac391615ba95ddaa3585fd40eca4b86fdcb04fe3a73d565eb814f34" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "04c6451092c7d6be3f3d7ebd20a4e09de7a7e5e53b5a260642623bde0bd14152" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "2c41d31d3a9e24402304f152cf60fc9a9f604b8230c17795ef4ad4e9198e352b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "e1773a23a3a951161c84e2743f8367130501b94460d06f0aa8380600e06a334c" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "ee854966283cb8b6d2059ebe4a347a205c352339ea74b8ae4c1a629fa6662e5e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "daac07ec01259286107d03e547b4b3926da945bd7ca2c357823c4d4791453019" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "5567acfe57bc46890067c820c6a92072c8ec6cef5e5235212896cdac7f396e16" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577/manifest.json": { + "mode": "0o644", + "size": 679, + "kind": "file", + "sha256": "c00d7ff8acf37b37bf5e9756be2b57b7c771336e96fc5e4307ce45d800d45db0" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577/qodercli.log": { + "mode": "0o644", + "size": 10082, + "kind": "file", + "sha256": "feef5986694336cdc8e66a32a1edc806666d3f4acce2e4d5e510d74adea1ebd1" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "11da7c063bbd57c096a8e34f7c4bfa3027b4a6686b59e3a876932164afa4ab0d" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "721bd3dab11fe57df05bb28bdeb18fe10f66fe96b9b3b6f9880aca07f583eb36" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "5a792da01315eed566962017cf880d3107d9d8e1f965a85d4398f9a9e238247a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "5914e7f2e646dffdb42d567f27f246023c99588996f6d997069beb8be07dd1d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "3743b4adf2b10af7e60fd8e02b74332ea9ecd72c39c68f7cc3d35673471f5d8e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "c2add02414f013500203cbc6911550237c225825940332586b96eb7e6bcb8a8a" + }, + "home/.qoder/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "home/.qoder/plugins/installed_plugins_v2.json": { + "mode": "0o644", + "size": 612, + "kind": "file", + "sha256": "ef3d92ad9f830071bd6cb6ac414e55684ce65fc0ce8ebac255fc0063dd851cdd" + }, + "home/.qoder/settings.json": { + "mode": "0o600", + "size": 66, + "kind": "file", + "sha256": "a81ec84331eeeab50ace0da9ce7bb344fb89b64f1ab00b10b46d6ead2554fc99" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "B-installed-before": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "eb96af0546f71e00a0089e8f0dda88df8e9e56d18300460f8eabb9ed2e2e8bd7", + "target_hex": "72756e732f323032362d31302d32345432322d30382d31312d3736315a2d61696e3269612d7032363236", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/manifest.json": { + "mode": "0o644", + "size": 677, + "kind": "file", + "sha256": "de153e7a835758e98385298e8697bb98143f5620359b7a0b3a00860cccc5d58f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/qodercli.log": { + "mode": "0o644", + "size": 10076, + "kind": "file", + "sha256": "708a9719dac391615ba95ddaa3585fd40eca4b86fdcb04fe3a73d565eb814f34" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "04c6451092c7d6be3f3d7ebd20a4e09de7a7e5e53b5a260642623bde0bd14152" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "2c41d31d3a9e24402304f152cf60fc9a9f604b8230c17795ef4ad4e9198e352b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "e1773a23a3a951161c84e2743f8367130501b94460d06f0aa8380600e06a334c" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "ee854966283cb8b6d2059ebe4a347a205c352339ea74b8ae4c1a629fa6662e5e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "daac07ec01259286107d03e547b4b3926da945bd7ca2c357823c4d4791453019" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "5567acfe57bc46890067c820c6a92072c8ec6cef5e5235212896cdac7f396e16" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577/manifest.json": { + "mode": "0o644", + "size": 679, + "kind": "file", + "sha256": "c00d7ff8acf37b37bf5e9756be2b57b7c771336e96fc5e4307ce45d800d45db0" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577/qodercli.log": { + "mode": "0o644", + "size": 10082, + "kind": "file", + "sha256": "feef5986694336cdc8e66a32a1edc806666d3f4acce2e4d5e510d74adea1ebd1" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "11da7c063bbd57c096a8e34f7c4bfa3027b4a6686b59e3a876932164afa4ab0d" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "721bd3dab11fe57df05bb28bdeb18fe10f66fe96b9b3b6f9880aca07f583eb36" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "5a792da01315eed566962017cf880d3107d9d8e1f965a85d4398f9a9e238247a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "5914e7f2e646dffdb42d567f27f246023c99588996f6d997069beb8be07dd1d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "3743b4adf2b10af7e60fd8e02b74332ea9ecd72c39c68f7cc3d35673471f5d8e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "c2add02414f013500203cbc6911550237c225825940332586b96eb7e6bcb8a8a" + }, + "home/.qoder/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "home/.qoder/plugins/installed_plugins_v2.json": { + "mode": "0o644", + "size": 612, + "kind": "file", + "sha256": "ef3d92ad9f830071bd6cb6ac414e55684ce65fc0ce8ebac255fc0063dd851cdd" + }, + "home/.qoder/settings.json": { + "mode": "0o600", + "size": 66, + "kind": "file", + "sha256": "a81ec84331eeeab50ace0da9ce7bb344fb89b64f1ab00b10b46d6ead2554fc99" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "B-installed-after": { + "home": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "home/.qoder": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/.auth/.credential-transaction": { + "mode": "0o600", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/.qoder/.auth/machine_id": { + "mode": "0o600", + "size": 36, + "kind": "file", + "sha256": "c56817ea2f4339ab288d2dd7e10c2aef149d3d0cf21fd309fb9e14bd6200d726" + }, + "home/.qoder/installation_id": { + "mode": "0o644", + "size": 36, + "kind": "file", + "sha256": "897bac458a8d1241ae35d8faa2204888a85f6c1f722ed7c48819c71a56e21dd8" + }, + "home/.qoder/logs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/latest": { + "mode": "0o777", + "size": 42, + "kind": "symlink_metadata_only", + "type": "0o120000", + "target_bytes": 42, + "target_sha256": "637555a7632cb94edc53936648b1e51f817eb7840632a69480c47bb46a42c027", + "target_hex": "72756e732f323032362d31302d32345432322d30382d31342d3435365a2d3569337069702d7032363839", + "target_followed": false + }, + "home/.qoder/logs/runs": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/manifest.json": { + "mode": "0o644", + "size": 573, + "kind": "file", + "sha256": "c4985ae0a1f07b780bc10fb62db6eaa1be1609b2b4bffd2e00f1e018b427ee98" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-02-801Z-1f0cr6-p2412/qodercli.log": { + "mode": "0o644", + "size": 4273, + "kind": "file", + "sha256": "39fc2a2d0149b656f40d59c855471fa65fb368ce812ff9abcb719721cfec6bbf" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/manifest.json": { + "mode": "0o644", + "size": 600, + "kind": "file", + "sha256": "f3ab07c3621e3b90c24c30462a9dc570793bd31e0ce110fbaebf1acc8b05af8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-391Z-zgrsgp-p2427/qodercli.log": { + "mode": "0o644", + "size": 4270, + "kind": "file", + "sha256": "1cba9a3a4cbe7259df5ad36b4cf54b41db34d96e8f9a99a11df032d5f659823f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "987e3e665d83633e7910692cdc73082390fdea967e7a706cd4e63bd289b7d8fa" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-03-984Z-wf1zyu-p2441/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "7ea94caf5affbc387ff337a8e33388b24e39c9109d441061e8c43628c8b8469b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/manifest.json": { + "mode": "0o644", + "size": 602, + "kind": "file", + "sha256": "56b8037b4e406efade1667b59f66e0259fd46e08a2bb29ae88a9c73f9d1eed8f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-04-574Z-ajskn6-p2456/qodercli.log": { + "mode": "0o644", + "size": 4268, + "kind": "file", + "sha256": "dfa566dbe6ce204a5c16b05681179d899d724c3f74bd5dcc5a2abb8cde71ccad" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "9334e50500734ba70e2827229c715941ed8382dafd05b8a9baabd0e92de216d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-141Z-5hu2q9-p2470/qodercli.log": { + "mode": "0o644", + "size": 9758, + "kind": "file", + "sha256": "9e4aff6f34e6531750d28032142344822e6e8dfaab1e88f69e7ddbe1afd5c031" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "57717726d63c818b317ef34ae4aa213dcc97fa1b89bd196139681f51b2e15f70" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-05-784Z-mymuet-p2488/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "53c32cd1fd0189b2b6f46dfb7730cf689dce639f570e22122a1536583f81270e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "9f37bb99ec640515ccb225c67309c98863970279fca8496370d30da182caeed5" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-06-419Z-arialq-p2502/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c1de9782ab8f07c0a48dd931b3561a326e2418dec3ee90d900104fc42c2a3433" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/manifest.json": { + "mode": "0o644", + "size": 677, + "kind": "file", + "sha256": "de153e7a835758e98385298e8697bb98143f5620359b7a0b3a00860cccc5d58f" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-182Z-zhe18s-p2516/qodercli.log": { + "mode": "0o644", + "size": 10076, + "kind": "file", + "sha256": "708a9719dac391615ba95ddaa3585fd40eca4b86fdcb04fe3a73d565eb814f34" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "04c6451092c7d6be3f3d7ebd20a4e09de7a7e5e53b5a260642623bde0bd14152" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-07-817Z-f051yw-p2532/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "2c41d31d3a9e24402304f152cf60fc9a9f604b8230c17795ef4ad4e9198e352b" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "e1773a23a3a951161c84e2743f8367130501b94460d06f0aa8380600e06a334c" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-08-460Z-5brizv-p2547/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "ee854966283cb8b6d2059ebe4a347a205c352339ea74b8ae4c1a629fa6662e5e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "daac07ec01259286107d03e547b4b3926da945bd7ca2c357823c4d4791453019" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-098Z-rbed6g-p2562/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "5567acfe57bc46890067c820c6a92072c8ec6cef5e5235212896cdac7f396e16" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577/manifest.json": { + "mode": "0o644", + "size": 679, + "kind": "file", + "sha256": "c00d7ff8acf37b37bf5e9756be2b57b7c771336e96fc5e4307ce45d800d45db0" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-09-867Z-za1ydy-p2577/qodercli.log": { + "mode": "0o644", + "size": 10082, + "kind": "file", + "sha256": "feef5986694336cdc8e66a32a1edc806666d3f4acce2e4d5e510d74adea1ebd1" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "11da7c063bbd57c096a8e34f7c4bfa3027b4a6686b59e3a876932164afa4ab0d" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-10-507Z-p74onh-p2593/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "721bd3dab11fe57df05bb28bdeb18fe10f66fe96b9b3b6f9880aca07f583eb36" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "5a792da01315eed566962017cf880d3107d9d8e1f965a85d4398f9a9e238247a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-130Z-qbb6q5-p2610/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "5914e7f2e646dffdb42d567f27f246023c99588996f6d997069beb8be07dd1d8" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "3743b4adf2b10af7e60fd8e02b74332ea9ecd72c39c68f7cc3d35673471f5d8e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-11-761Z-ain2ia-p2626/qodercli.log": { + "mode": "0o644", + "size": 9412, + "kind": "file", + "sha256": "c2add02414f013500203cbc6911550237c225825940332586b96eb7e6bcb8a8a" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-12-528Z-rki4q7-p2642": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-12-528Z-rki4q7-p2642/manifest.json": { + "mode": "0o644", + "size": 679, + "kind": "file", + "sha256": "1fa68e860fbe14a8de02b89b144a5765eace1401bd9b01f77c2dc476142bff6e" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-12-528Z-rki4q7-p2642/qodercli.log": { + "mode": "0o644", + "size": 10081, + "kind": "file", + "sha256": "1ce0f4e9d448c1a58c99a21579c0ab5acb03a773a64f1d39523bf57041800321" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-13-176Z-2o12xh-p2657": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-13-176Z-2o12xh-p2657/manifest.json": { + "mode": "0o644", + "size": 597, + "kind": "file", + "sha256": "bb6888c0580e2ca120cac89b9eb73958bfae1dba498b38b38f93b0597a6e0fe0" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-13-176Z-2o12xh-p2657/qodercli.log": { + "mode": "0o644", + "size": 9413, + "kind": "file", + "sha256": "ad466d9eda1e99f1c60f42d2cdda81dd460d1531249898ea59fc1d77f607df52" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-13-814Z-yq75wj-p2673": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-13-814Z-yq75wj-p2673/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "267daf6da72edb1c91d16792e294a80df72626a9c214ac451c6784e015ecfe22" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-13-814Z-yq75wj-p2673/qodercli.log": { + "mode": "0o644", + "size": 9414, + "kind": "file", + "sha256": "a7c83404d6d35ee9493442e7429d1513091c5616fd9464b5d3ab4fe32da2e589" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-14-456Z-5i3pip-p2689": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-14-456Z-5i3pip-p2689/manifest.json": { + "mode": "0o644", + "size": 599, + "kind": "file", + "sha256": "f4c583e14f3b0373eef44b18f01bb46f69a3b0ca4ca4fb582e67a61cb569afe2" + }, + "home/.qoder/logs/runs/2026-10-24T22-08-14-456Z-5i3pip-p2689/qodercli.log": { + "mode": "0o644", + "size": 9415, + "kind": "file", + "sha256": "c60e69e01bae27cf5c08bbe0a3bb8b56bdac8f6bf51d372a5c339bd8b9b8c039" + }, + "home/.qoder/plugins": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/0.9.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.0.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.1.0": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.1.0/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.1.0/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.1.0/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.1.0/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/plugins/cache/local/uap-scope-research/1.1.0/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + }, + "home/.qoder/plugins/installed_plugins_v2.json": { + "mode": "0o644", + "size": 914, + "kind": "file", + "sha256": "c4f91e4ef73efaa2e2b3e06ebbf1b9c5b5ca35343a2db742caf77e9141feedf7" + }, + "home/.qoder/settings.json": { + "mode": "0o600", + "size": 66, + "kind": "file", + "sha256": "a81ec84331eeeab50ace0da9ce7bb344fb89b64f1ab00b10b46d6ead2554fc99" + }, + "home/.qoder/tmp": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/.qoder/tmp/telemetry/active-runs": { + "mode": "0o700", + "size": 4096, + "kind": "directory" + }, + "home/cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/config/git/ignore": { + "mode": "0o644", + "size": 72, + "kind": "file", + "sha256": "3088d2496f7d98d3a18cb4b7568ff2373cfc2bbe73756611dbb8d1441a99e436" + }, + "home/data": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/git-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-cache": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "home/npm-global-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/npm-user-config": { + "mode": "0o644", + "size": 0, + "kind": "file", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "home/state": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "user-cwd": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "project-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + } + }, + "project_ownership_qualified": false, + "native_activation_qualified": false, + "loaded_skills_qualified": false, + "mcp_qualified": false, + "trust_qualified": false, + "model_qualified": false, + "authentication_qualified": false, + "native_session_restart_qualified": false, + "isolation": { + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "groups": [], + "network_namespace": "net:[4026532216]", + "parent_network_namespace": "net:[4026531833]", + "interfaces": [ + "lo" + ], + "no_new_privs": "1", + "capabilities": { + "CapInh": "0000000000000000", + "CapPrm": "0000000000000000", + "CapEff": "0000000000000000", + "CapBnd": "0000000000000000", + "CapAmb": "0000000000000000" + } + }, + "native_environment": { + "HOME": "/tmp/qoder-populated-w490rb05/home", + "USERPROFILE": "/tmp/qoder-populated-w490rb05/home", + "XDG_CONFIG_HOME": "/tmp/qoder-populated-w490rb05/home/config", + "XDG_CACHE_HOME": "/tmp/qoder-populated-w490rb05/home/cache", + "XDG_STATE_HOME": "/tmp/qoder-populated-w490rb05/home/state", + "XDG_DATA_HOME": "/tmp/qoder-populated-w490rb05/home/data", + "NPM_CONFIG_CACHE": "/tmp/qoder-populated-w490rb05/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/qoder-populated-w490rb05/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/qoder-populated-w490rb05/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/qoder-populated-w490rb05/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "TMPDIR": "/tmp/qoder-populated-w490rb05/tmp", + "LANG": "C.UTF-8" + }, + "fixture_pins": { + "/tmp/qoder-populated-w490rb05/sources/user/.qoder-plugin/plugin.json": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3", + "/tmp/qoder-populated-w490rb05/sources/user/skills/marker/SKILL.md": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126", + "/tmp/qoder-populated-w490rb05/sources/A/.qoder-plugin/plugin.json": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64", + "/tmp/qoder-populated-w490rb05/sources/A/skills/marker/SKILL.md": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7", + "/tmp/qoder-populated-w490rb05/sources/B/.qoder-plugin/plugin.json": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef", + "/tmp/qoder-populated-w490rb05/sources/B/skills/marker/SKILL.md": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + }, + "source_baseline": { + "source-user": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-user/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "9ad640227db1b182b244402d0cd110ff1233fa7bd6e3c64b3367c54f88863fe3" + }, + "source-user/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-user/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 66, + "kind": "file", + "sha256": "8a8e9dfd726588615d9873ed1415613eb8189830682ab71a5689f42b09fb5126" + }, + "source-A": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-A/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "4304965902e15c82d65312f81d1b7bd7ea6e7ec3a57eadcbdd4e23b89ec48e64" + }, + "source-A/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-A/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "7dca4d363bf4b90e4295be2528d598c62c9eef8b6c1ee89851c8d5a888adbdd7" + }, + "source-B": { + "kind": "directory", + "mode": "0o755", + "size": 4096 + }, + "source-B/.qoder-plugin": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/.qoder-plugin/plugin.json": { + "mode": "0o644", + "size": 99, + "kind": "file", + "sha256": "181ad24d7795939792bc864a46aa73f66c22e4df1b53c3918f9436b0ae7c18ef" + }, + "source-B/skills": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker": { + "mode": "0o755", + "size": 4096, + "kind": "directory" + }, + "source-B/skills/marker/SKILL.md": { + "mode": "0o644", + "size": 60, + "kind": "file", + "sha256": "e3889929ded7e2721213d8f93b7582046abf71efbeda908854e1566abb5fabe8" + } + }, + "fixed_fixture_bodies": { + "/tmp/qoder-populated-w490rb05/sources/user/.qoder-plugin/plugin.json": "{\"name\": \"uap-scope-research\", \"version\": \"0.9.0\", \"description\": \"Inert scoped research fixture\"}\n", + "/tmp/qoder-populated-w490rb05/sources/user/skills/marker/SKILL.md": "---\nname: marker\ndescription: Inert USER research marker\n---\nUSER\n", + "/tmp/qoder-populated-w490rb05/sources/A/.qoder-plugin/plugin.json": "{\"name\": \"uap-scope-research\", \"version\": \"1.0.0\", \"description\": \"Inert scoped research fixture\"}\n", + "/tmp/qoder-populated-w490rb05/sources/A/skills/marker/SKILL.md": "---\nname: marker\ndescription: Inert A research marker\n---\nA\n", + "/tmp/qoder-populated-w490rb05/sources/B/.qoder-plugin/plugin.json": "{\"name\": \"uap-scope-research\", \"version\": \"1.1.0\", \"description\": \"Inert scoped research fixture\"}\n", + "/tmp/qoder-populated-w490rb05/sources/B/skills/marker/SKILL.md": "---\nname: marker\ndescription: Inert B research marker\n---\nB\n" + }, + "stage": "B-installed", + "acquisition": { + "url": "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.78/qodercli-linux-x64.tar.gz", + "version": "1.1.78", + "archive_integrity": "sha256:5e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea", + "archive_bytes": 78115289, + "binary_sha256": "0da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953", + "binary_bytes": 182982112 + }, + "wrapper": { + "probe": "isolated-collector", + "argv": [ + "/usr/bin/sudo", + "-n", + "/usr/bin/unshare", + "--net", + "--", + "/usr/bin/setpriv", + "--reuid=1001", + "--regid=1001", + "--clear-groups", + "--inh-caps=-all", + "--ambient-caps=-all", + "--bounding-set=-all", + "--no-new-privs", + "/usr/bin/env", + "-i", + "PATH=/usr/bin:/bin", + "LANG=C.UTF-8", + "PYTHONDONTWRITEBYTECODE=1", + "GITHUB_ACTIONS=true", + "RUNNER_ENVIRONMENT=github-hosted", + "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED=1", + "EXPECTED_COMMIT=2b0ef18146c28da9b6a9b1ef86a0fb413d950eee", + "/usr/bin/python3", + "/home/runner/work/universal-agent-plugins/universal-agent-plugins/scripts/research-qoder-populated.py", + "--collect", + "/tmp/qoder-populated-w490rb05/request.json" + ], + "cwd": "/tmp/qoder-populated-w490rb05", + "error": null, + "exit": 0, + "elapsed_seconds": 12.559, + "stdout_file": "isolated-collector.stdout.log", + "stderr_file": "isolated-collector.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "checkout": { + "probe": "source-checkout", + "argv": [ + "/usr/bin/git", + "rev-parse", + "HEAD" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.003, + "stdout_file": "source-checkout.stdout.log", + "stderr_file": "source-checkout.stderr.log", + "stdout_bytes": 41, + "stderr_bytes": 0, + "stdout_sha256": "4291bd51263f1b9d3595e402a286ce3f37d93df2592669a78f1a168afe6300de", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "source_commit": "2b0ef18146c28da9b6a9b1ef86a0fb413d950eee", + "observed_source_commit": "2b0ef18146c28da9b6a9b1ef86a0fb413d950eee", + "cleanup": { + "complete": true, + "probe": { + "probe": "cleanup", + "argv": [ + "/usr/bin/python3", + "-c", + "\nimport os,stat,sys\npath,device,inode=sys.argv[1:]\nparent,name=os.path.split(path)\npfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)\nroot=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd)\nidentity=os.fstat(root)\nif (identity.st_dev,identity.st_ino)!=(int(device),int(inode)):raise SystemExit(\"cleanup root identity changed\")\ndef clear(fd):\n for name in os.listdir(fd):\n item=os.stat(name,dir_fd=fd,follow_symlinks=False)\n if stat.S_ISDIR(item.st_mode):\n child=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)\n try:\n if not os.path.samestat(item,os.fstat(child)):raise RuntimeError(\"cleanup directory changed\")\n clear(child)\n finally:os.close(child)\n os.rmdir(name,dir_fd=fd)\n else:os.unlink(name,dir_fd=fd)\nclear(root)\nif not os.path.samestat(identity,os.stat(name,dir_fd=pfd,follow_symlinks=False)):raise SystemExit(\"cleanup root replaced\")\nos.close(root)\nos.rmdir(name,dir_fd=pfd)\nos.close(pfd)\n", + "/tmp/qoder-populated-w490rb05", + "2049", + "9181606" + ], + "cwd": "/home/runner/work/_temp/qoder-populated-research", + "error": null, + "exit": 0, + "elapsed_seconds": 0.037, + "stdout_file": "cleanup.stdout.log", + "stderr_file": "cleanup.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } +} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/source-checkout.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/source-checkout.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/source-checkout.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/source-checkout.stdout.log new file mode 100644 index 000000000..cea801ab1 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/source-checkout.stdout.log @@ -0,0 +1 @@ +2b0ef18146c28da9b6a9b1ef86a0fb413d950eee diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/uninstall-help.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/uninstall-help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/uninstall-help.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/uninstall-help.stdout.log new file mode 100644 index 000000000..4e9297a4d --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/uninstall-help.stdout.log @@ -0,0 +1,15 @@ +Usage: qodercli plugins uninstall|remove [options] + +Uninstall a plugin. + +Arguments: + plugin Plugin name to uninstall. + +Options: + -s, --scope Scope to uninstall from: "user" (global, default), + "project" (shared, committed to VCS), or "local" + (personal, not committed). (choices: "user", "project", + "local", default: "user") + --keep-data Preserve the plugin data directory. (default: false) + --json Output as JSON. (default: false) + -h, --help display help for command diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-install.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-install.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-install.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-install.stdout.log new file mode 100644 index 000000000..354651256 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-install.stdout.log @@ -0,0 +1 @@ +{"ok":true,"pluginId":"uap-scope-research@local","version":"0.9.0","path":"/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0"} diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-A-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-A-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-A-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-A-list.stdout.log new file mode 100644 index 000000000..4ee2323d8 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-A-list.stdout.log @@ -0,0 +1,26 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-B-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-B-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-B-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-B-list.stdout.log new file mode 100644 index 000000000..4ee2323d8 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-project-B-list.stdout.log @@ -0,0 +1,26 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-user-cwd-list.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-user-cwd-list.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-user-cwd-list.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-user-cwd-list.stdout.log new file mode 100644 index 000000000..4ee2323d8 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/user-installed-user-cwd-list.stdout.log @@ -0,0 +1,26 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/version.stderr.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/version.stdout.log b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/version.stdout.log new file mode 100644 index 000000000..8cf600ff1 --- /dev/null +++ b/docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/version.stdout.log @@ -0,0 +1 @@ +1.1.78 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/README.md b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/README.md new file mode 100644 index 000000000..311344083 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/README.md @@ -0,0 +1,53 @@ +# VS Code 1.143.1: genuine shipped CLI help/version + +Measured at reviewed source `0a34182aa7f424a52a9ff9a49177d88dbf2fb711` in +[run 40132182435](https://github.com/777genius/universal-agent-plugins/actions/runs/40132182435), +[job 120986058838](https://github.com/777genius/universal-agent-plugins/actions/runs/40132182435/job/120986058838), +on actual GitHub-hosted Ubuntu 24.04. The workflow completed successfully. +Artifact 12576470352, `vscode-1.143.1-precode-publisher-linux-amd64`, had a +5,805-byte outer ZIP, SHA256 +`5f9f68cf934db20059327f77788ee4a33baaee65b47a82ab3f6478d1667fe651`. +Its sole member is the admitted 24,354-byte `research.zip`, SHA256 +`9bfb6dca7280a56b11d8066cadbe310aa22d49ad481a0171e5aca24738937698`. +All seven flat raw text members are preserved byte-for-byte here. The ZIPs stay +in the task runtime; `provenance.json` records all text lengths and hashes. + +## What was verified + +The implementing worker and root independently verified both archive byte +counts/hashes and CRC, all seven member bytes and all six stdout/stderr stream +size/hash pairs. Both +native commands exited zero with empty stderr. The exact version stdout is +1.143.1, publisher commit `e646868227c3e3a9e3f24277c8c7220be42c0be7`, x64. +Help was 8,469 bytes and version 53 bytes. The successful same-process proofs +record real/effective UID/GID 1001, no supplementary groups, only loopback, a +network namespace distinct from the parent, zero capability sets and +no-new-privileges. Each probe had a distinct fresh home/cwd/profile. The shipped +ELF ran its shipped cli.js with ELECTRON_RUN_AS_NODE=1; no editor/workbench or +extension host was launched. Cleanup completed in 0.25 seconds with empty streams. + +The collector recorded complete publisher archive integrity before parsing: +350,844,741 bytes with the reviewed publisher SHA256. It consumed 4,262 TAR +members and 1,034,475,520 expanded bytes; regular payload was 1,031,068,984 bytes. +No symbolic links were present/omitted. Required ELF/CLI/product file hashes and +identities are in the original summary, checked by the collector before/after +commands. Interpretation did not reacquire or rerun those deleted native assets. + +## Observed command boundary and conclusion + +The native help explicitly describes `--add-mcp` as a **user-profile** writer. +It exposes process diagnostics, extension/profile controls and chat invocation; +it does not provide a read-only workspace MCP/skill inventory command. None of +those additional commands was run. A project adapter cannot be based on this +user writer or on help/version alone. + +The implemented help/version pre-code observation gate is complete. Both independent +interpretations are GO for a new concrete workspace observation/design plan only. Original +`native_project_qualified:false` is unchanged. Workspace skill/MCP attribution, +UAP ownership/maintenance, effective Local harness behavior, trust, extension +host/UI, model/authentication and restart remain unobserved. + +Primary pinned workbench/API source may guide the next plan, but only a genuinely +shipped-host observation can populate those additional evidence stages. No local +vendor process, installer, extension, MCP server or account operation was executed +while downloading or interpreting this artifact. diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/cleanup.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/cleanup.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/help.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/help.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/help.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/help.stdout.log new file mode 100644 index 000000000..0c7ae2e2d --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/help.stdout.log @@ -0,0 +1,133 @@ +Visual Studio Code 1.143.1 + +Usage: code [options] [paths...] + +To read from stdin, append '-' (e.g. 'ps aux | grep code | code -') + +Options + -d --diff Compare two files with each + other. + -m --merge Perform a three-way merge by + providing paths for two modified + versions of a file, the common + origin of both modified versions + and the output file to save merge + results. + -a --add Add folder(s) to the last active + window. + --remove Remove folder(s) from the last + active window. + -g --goto Open a file at the path on the + specified line and character + position. + -n --new-window Force to open a new window. + -r --reuse-window Force to open a file or folder in + an already opened window. + --agents Opens the agents window. If a + folder is provided, opens a new + session with that folder + selected. + -w --wait Wait for the files to be closed + before returning. + --locale The locale to use (e.g. en-US or + zh-TW). + --user-data-dir Specifies the directory that user + data is kept in. Can be used to + open multiple distinct instances + of Code. + --profile Opens the provided folder or + workspace with the given profile + and associates the profile with + the workspace. If the profile does + not exist, a new empty one is + created. + -h --help Print usage. + +Extensions Management + --extensions-dir Set the root path for extensions. + --list-extensions List the installed extensions. + --show-versions Show versions of installed extensions, + when using --list-extensions. + --category Filters installed extensions by provided + category, when using --list-extensions. + --install-extension Installs or updates an extension. The + argument is either an extension id or a + path to a VSIX. The identifier of an + extension is '${publisher}.${name}'. Use + '--force' argument to update to latest + version. To install a specific version + provide '@${version}'. For example: + 'vscode.csharp@1.2.3'. + --pre-release Installs the pre-release version of the + extension, when using + --install-extension + --uninstall-extension Uninstalls an extension. + --update-extensions Update the installed extensions. + --enable-proposed-api Enables proposed API features for + extensions. Can receive one or more + extension IDs to enable individually. + +Model Context Protocol + --add-mcp Adds a Model Context Protocol server definition to the user + profile. Accepts JSON input in the form + '{"name":"server-name","command":...}' + +Troubleshooting + -v --version Print version. + --verbose Print verbose output (implies + --wait). + --log Log level to use. Default is 'info'. + Allowed values are 'critical', + 'error', 'warn', 'info', 'debug', + 'trace', 'off'. You can also + configure the log level of an + extension by passing extension id and + log level in the following format: + '${publisher}.${name}:${logLevel}'. + For example: 'vscode.csharp:trace'. + Can receive one or more such + entries. + -s --status Print process usage and diagnostics + information. + --prof-startup Run CPU profiler during startup. + --disable-extensions Disable all installed extensions. + This option is not persisted and is + effective only when the command opens + a new window. + --disable-extension Disable the provided extension. This + option is not persisted and is + effective only when the command opens + a new window. + --sync Turn sync on or off. + --inspect-extensions Allow debugging and profiling of + extensions. Check the developer tools + for the connection URI. + --inspect-brk-extensions Allow debugging and profiling of + extensions with the extension host + being paused after start. Check the + developer tools for the connection + URI. + --disable-lcd-text Disable LCD font rendering. + --disable-gpu Disable GPU hardware acceleration. + --disable-chromium-sandbox Use this option only when there is + requirement to launch the application + as sudo user on Linux or when running + as an elevated user in an applocker + environment on Windows. + --locate-shell-integration-path Print the path to a terminal shell + integration script. Allowed values + are 'bash', 'pwsh', 'zsh' or 'fish'. + --telemetry Shows all telemetry events which VS + code collects. + --transient Run with temporary data and extension + directories, as if launched for the + first time. + +Subcommands + chat Pass in a prompt to run in a chat session in the current working + directory. + serve-web Run a server that displays the editor UI in browsers. + agent Start and interact with AI agent hosts. + tunnel Make the current machine accessible from vscode.dev or other + machines through a secure tunnel. + diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/provenance.json b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/provenance.json new file mode 100644 index 000000000..d9701813c --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/provenance.json @@ -0,0 +1,62 @@ +{ + "evidence_kind": "genuine-hosted-publisher-help-version-provenance", + "source_commit": "0a34182aa7f424a52a9ff9a49177d88dbf2fb711", + "run_id": 40132182435, + "job_id": 120986058838, + "artifact_id": 12576470352, + "outer_zip_bytes": 5805, + "outer_zip_sha256": "5f9f68cf934db20059327f77788ee4a33baaee65b47a82ab3f6478d1667fe651", + "research_zip_bytes": 24354, + "research_zip_sha256": "9bfb6dca7280a56b11d8066cadbe310aa22d49ad481a0171e5aca24738937698", + "archives_committed": false, + "all_six_process_streams_verified": true, + "all_seven_raw_text_members_preserved": true, + "original_false_native_project_qualification_preserved": true, + "independent_interpretation": [ + { + "reviewer": "skillshare_scope", + "decision": "GO for workspace observation plan only; no workspace/native project qualification" + }, + { + "reviewer": "root", + "decision": "GO for workspace observation plan only; independently verified both ZIPs/CRC/seven members/six streams/exact version/isolation/cleanup" + } + ], + "files": [ + { + "name": "help.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "cleanup.stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "version.stdout.log", + "bytes": 53, + "sha256": "04fb3de70a59cd94e0df25660c2910080564c3fcd24d68c66044e7226b8f5b02" + }, + { + "name": "help.stdout.log", + "bytes": 8469, + "sha256": "1ab85e694c0e95a4752fec5d0777db7bad49baecf1706fdab794245d40093f03" + }, + { + "name": "version.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "cleanup.stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "vscode-precode-research.json", + "bytes": 15018, + "sha256": "2c33e0e5a2b6034a0ca67eca3ad02d6264910ca8e6996597486283f152916e05" + } + ] +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/version.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/version.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/version.stdout.log new file mode 100644 index 000000000..02482ade4 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/version.stdout.log @@ -0,0 +1,3 @@ +1.143.1 +e646868227c3e3a9e3f24277c8c7220be42c0be7 +x64 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/vscode-precode-research.json b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/vscode-precode-research.json new file mode 100644 index 000000000..4e29301b4 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/vscode-precode-research.json @@ -0,0 +1,243 @@ +{ + "evidence_kind": "vscode-pinned-help-version-research", + "version": "1.143.1", + "publisher_commit": "e646868227c3e3a9e3f24277c8c7220be42c0be7", + "source_commit": "0a34182aa7f424a52a9ff9a49177d88dbf2fb711", + "collection_complete": true, + "native_project_qualified": false, + "interpretation": "Independent publisher observation review required; no workspace contract inferred", + "probes": [ + { + "probe": "version", + "argv": [ + "/usr/bin/sudo", + "-n", + "/usr/bin/unshare", + "--net", + "/usr/bin/setpriv", + "--reuid=1001", + "--regid=1001", + "--clear-groups", + "--bounding-set=-all", + "--inh-caps=-all", + "--ambient-caps=-all", + "--no-new-privs", + "/usr/bin/env", + "-i", + "/usr/bin/python3", + "-c", + "\nimport json,os,socket,sys\nparent,uid,gid,output,env,argv=sys.argv[1:]\nobserved={\"network_namespace\":os.stat(\"/proc/self/ns/net\").st_ino,\"uid\":os.getuid(),\"gid\":os.getgid(),\"euid\":os.geteuid(),\"egid\":os.getegid(),\"groups\":os.getgroups(),\"interfaces\":[name for _,name in socket.if_nameindex()]}\nif observed[\"network_namespace\"]==int(parent) or observed[\"uid\"]!=int(uid) or observed[\"gid\"]!=int(gid) or observed[\"uid\"]==0 or observed[\"gid\"]==0 or observed[\"euid\"]!=int(uid) or observed[\"egid\"]!=int(gid) or observed[\"groups\"] or observed[\"interfaces\"]!=[\"lo\"]:\n raise SystemExit(\"isolated native process admission failed\")\nwith open(\"/proc/self/status\") as source:\n status=dict(line.rstrip().split(\":\",1) for line in source if \":\" in line)\nif any(int(status[key].strip(),16) for key in (\"CapInh\",\"CapPrm\",\"CapEff\",\"CapBnd\",\"CapAmb\")) or status[\"NoNewPrivs\"].strip()!=\"1\":\n raise SystemExit(\"native privilege drop failed\")\nobserved[\"capabilities_zero\"]=True\nobserved[\"no_new_privileges\"]=True\nwith open(output,\"x\") as stream:json.dump(observed,stream,sort_keys=True)\nos.execve(json.loads(argv)[0],json.loads(argv),json.loads(env))\n", + "4026531833", + "1001", + "1001", + "/tmp/vscode-native-research-33aorwdd/version/isolation.json", + "{\"HOME\": \"/tmp/vscode-native-research-33aorwdd/version/home\", \"USERPROFILE\": \"/tmp/vscode-native-research-33aorwdd/version/home\", \"XDG_CONFIG_HOME\": \"/tmp/vscode-native-research-33aorwdd/version/home/config\", \"XDG_CACHE_HOME\": \"/tmp/vscode-native-research-33aorwdd/version/home/cache\", \"XDG_STATE_HOME\": \"/tmp/vscode-native-research-33aorwdd/version/home/state\", \"XDG_DATA_HOME\": \"/tmp/vscode-native-research-33aorwdd/version/home/data\", \"NPM_CONFIG_CACHE\": \"/tmp/vscode-native-research-33aorwdd/version/home/npm-cache\", \"NPM_CONFIG_USERCONFIG\": \"/tmp/vscode-native-research-33aorwdd/version/home/npm-user-config\", \"NPM_CONFIG_GLOBALCONFIG\": \"/tmp/vscode-native-research-33aorwdd/version/home/npm-global-config\", \"GIT_CONFIG_GLOBAL\": \"/tmp/vscode-native-research-33aorwdd/version/home/git-config\", \"GIT_CONFIG_NOSYSTEM\": \"1\", \"GIT_TERMINAL_PROMPT\": \"0\", \"PATH\": \"/usr/bin:/bin\", \"LANG\": \"C.UTF-8\", \"TMPDIR\": \"/tmp/vscode-native-research-33aorwdd/version/tmp\", \"VSCODE_AGENT_FOLDER\": \"/tmp/vscode-native-research-33aorwdd/version/user-data\", \"ELECTRON_RUN_AS_NODE\": \"1\"}", + "[\"/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/code\", \"/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/resources/app/out/cli.js\", \"--version\", \"--user-data-dir\", \"/tmp/vscode-native-research-33aorwdd/version/user-data\", \"--extensions-dir\", \"/tmp/vscode-native-research-33aorwdd/version/extensions\"]" + ], + "cwd": "/tmp/vscode-native-research-33aorwdd/version/cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.685, + "stdout_file": "version.stdout.log", + "stderr_file": "version.stderr.log", + "stdout_bytes": 53, + "stderr_bytes": 0, + "stdout_sha256": "04fb3de70a59cd94e0df25660c2910080564c3fcd24d68c66044e7226b8f5b02", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "native_argv": [ + "/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/code", + "/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/resources/app/out/cli.js", + "--version", + "--user-data-dir", + "/tmp/vscode-native-research-33aorwdd/version/user-data", + "--extensions-dir", + "/tmp/vscode-native-research-33aorwdd/version/extensions" + ], + "native_environment": { + "HOME": "/tmp/vscode-native-research-33aorwdd/version/home", + "USERPROFILE": "/tmp/vscode-native-research-33aorwdd/version/home", + "XDG_CONFIG_HOME": "/tmp/vscode-native-research-33aorwdd/version/home/config", + "XDG_CACHE_HOME": "/tmp/vscode-native-research-33aorwdd/version/home/cache", + "XDG_STATE_HOME": "/tmp/vscode-native-research-33aorwdd/version/home/state", + "XDG_DATA_HOME": "/tmp/vscode-native-research-33aorwdd/version/home/data", + "NPM_CONFIG_CACHE": "/tmp/vscode-native-research-33aorwdd/version/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/vscode-native-research-33aorwdd/version/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/vscode-native-research-33aorwdd/version/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/vscode-native-research-33aorwdd/version/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "LANG": "C.UTF-8", + "TMPDIR": "/tmp/vscode-native-research-33aorwdd/version/tmp", + "VSCODE_AGENT_FOLDER": "/tmp/vscode-native-research-33aorwdd/version/user-data", + "ELECTRON_RUN_AS_NODE": "1" + }, + "native_cwd": "/tmp/vscode-native-research-33aorwdd/version/cwd", + "accepted": true, + "isolation": { + "capabilities_zero": true, + "egid": 1001, + "euid": 1001, + "gid": 1001, + "groups": [], + "interfaces": [ + "lo" + ], + "network_namespace": 4026532218, + "no_new_privileges": true, + "uid": 1001 + } + }, + { + "probe": "help", + "argv": [ + "/usr/bin/sudo", + "-n", + "/usr/bin/unshare", + "--net", + "/usr/bin/setpriv", + "--reuid=1001", + "--regid=1001", + "--clear-groups", + "--bounding-set=-all", + "--inh-caps=-all", + "--ambient-caps=-all", + "--no-new-privs", + "/usr/bin/env", + "-i", + "/usr/bin/python3", + "-c", + "\nimport json,os,socket,sys\nparent,uid,gid,output,env,argv=sys.argv[1:]\nobserved={\"network_namespace\":os.stat(\"/proc/self/ns/net\").st_ino,\"uid\":os.getuid(),\"gid\":os.getgid(),\"euid\":os.geteuid(),\"egid\":os.getegid(),\"groups\":os.getgroups(),\"interfaces\":[name for _,name in socket.if_nameindex()]}\nif observed[\"network_namespace\"]==int(parent) or observed[\"uid\"]!=int(uid) or observed[\"gid\"]!=int(gid) or observed[\"uid\"]==0 or observed[\"gid\"]==0 or observed[\"euid\"]!=int(uid) or observed[\"egid\"]!=int(gid) or observed[\"groups\"] or observed[\"interfaces\"]!=[\"lo\"]:\n raise SystemExit(\"isolated native process admission failed\")\nwith open(\"/proc/self/status\") as source:\n status=dict(line.rstrip().split(\":\",1) for line in source if \":\" in line)\nif any(int(status[key].strip(),16) for key in (\"CapInh\",\"CapPrm\",\"CapEff\",\"CapBnd\",\"CapAmb\")) or status[\"NoNewPrivs\"].strip()!=\"1\":\n raise SystemExit(\"native privilege drop failed\")\nobserved[\"capabilities_zero\"]=True\nobserved[\"no_new_privileges\"]=True\nwith open(output,\"x\") as stream:json.dump(observed,stream,sort_keys=True)\nos.execve(json.loads(argv)[0],json.loads(argv),json.loads(env))\n", + "4026531833", + "1001", + "1001", + "/tmp/vscode-native-research-33aorwdd/help/isolation.json", + "{\"HOME\": \"/tmp/vscode-native-research-33aorwdd/help/home\", \"USERPROFILE\": \"/tmp/vscode-native-research-33aorwdd/help/home\", \"XDG_CONFIG_HOME\": \"/tmp/vscode-native-research-33aorwdd/help/home/config\", \"XDG_CACHE_HOME\": \"/tmp/vscode-native-research-33aorwdd/help/home/cache\", \"XDG_STATE_HOME\": \"/tmp/vscode-native-research-33aorwdd/help/home/state\", \"XDG_DATA_HOME\": \"/tmp/vscode-native-research-33aorwdd/help/home/data\", \"NPM_CONFIG_CACHE\": \"/tmp/vscode-native-research-33aorwdd/help/home/npm-cache\", \"NPM_CONFIG_USERCONFIG\": \"/tmp/vscode-native-research-33aorwdd/help/home/npm-user-config\", \"NPM_CONFIG_GLOBALCONFIG\": \"/tmp/vscode-native-research-33aorwdd/help/home/npm-global-config\", \"GIT_CONFIG_GLOBAL\": \"/tmp/vscode-native-research-33aorwdd/help/home/git-config\", \"GIT_CONFIG_NOSYSTEM\": \"1\", \"GIT_TERMINAL_PROMPT\": \"0\", \"PATH\": \"/usr/bin:/bin\", \"LANG\": \"C.UTF-8\", \"TMPDIR\": \"/tmp/vscode-native-research-33aorwdd/help/tmp\", \"VSCODE_AGENT_FOLDER\": \"/tmp/vscode-native-research-33aorwdd/help/user-data\", \"ELECTRON_RUN_AS_NODE\": \"1\"}", + "[\"/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/code\", \"/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/resources/app/out/cli.js\", \"--help\", \"--user-data-dir\", \"/tmp/vscode-native-research-33aorwdd/help/user-data\", \"--extensions-dir\", \"/tmp/vscode-native-research-33aorwdd/help/extensions\"]" + ], + "cwd": "/tmp/vscode-native-research-33aorwdd/help/cwd", + "error": null, + "exit": 0, + "elapsed_seconds": 0.119, + "stdout_file": "help.stdout.log", + "stderr_file": "help.stderr.log", + "stdout_bytes": 8469, + "stderr_bytes": 0, + "stdout_sha256": "1ab85e694c0e95a4752fec5d0777db7bad49baecf1706fdab794245d40093f03", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "native_argv": [ + "/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/code", + "/tmp/vscode-native-research-33aorwdd/publisher/VSCode-linux-x64/resources/app/out/cli.js", + "--help", + "--user-data-dir", + "/tmp/vscode-native-research-33aorwdd/help/user-data", + "--extensions-dir", + "/tmp/vscode-native-research-33aorwdd/help/extensions" + ], + "native_environment": { + "HOME": "/tmp/vscode-native-research-33aorwdd/help/home", + "USERPROFILE": "/tmp/vscode-native-research-33aorwdd/help/home", + "XDG_CONFIG_HOME": "/tmp/vscode-native-research-33aorwdd/help/home/config", + "XDG_CACHE_HOME": "/tmp/vscode-native-research-33aorwdd/help/home/cache", + "XDG_STATE_HOME": "/tmp/vscode-native-research-33aorwdd/help/home/state", + "XDG_DATA_HOME": "/tmp/vscode-native-research-33aorwdd/help/home/data", + "NPM_CONFIG_CACHE": "/tmp/vscode-native-research-33aorwdd/help/home/npm-cache", + "NPM_CONFIG_USERCONFIG": "/tmp/vscode-native-research-33aorwdd/help/home/npm-user-config", + "NPM_CONFIG_GLOBALCONFIG": "/tmp/vscode-native-research-33aorwdd/help/home/npm-global-config", + "GIT_CONFIG_GLOBAL": "/tmp/vscode-native-research-33aorwdd/help/home/git-config", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "PATH": "/usr/bin:/bin", + "LANG": "C.UTF-8", + "TMPDIR": "/tmp/vscode-native-research-33aorwdd/help/tmp", + "VSCODE_AGENT_FOLDER": "/tmp/vscode-native-research-33aorwdd/help/user-data", + "ELECTRON_RUN_AS_NODE": "1" + }, + "native_cwd": "/tmp/vscode-native-research-33aorwdd/help/cwd", + "accepted": true, + "isolation": { + "capabilities_zero": true, + "egid": 1001, + "euid": 1001, + "gid": 1001, + "groups": [], + "interfaces": [ + "lo" + ], + "network_namespace": 4026532218, + "no_new_privileges": true, + "uid": 1001 + } + } + ], + "unobserved": [ + "workspace skills/MCP", + "UAP installation ownership", + "trust", + "extension host/UI", + "model/auth", + "native restart" + ], + "archive": { + "url": "https://vscode.download.prss.microsoft.com/dbazure/download/stable/e646868227c3e3a9e3f24277c8c7220be42c0be7/code-stable-x64-1792691793.tar.gz", + "bytes": 350844741, + "sha256": "48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be" + }, + "materialization": { + "members": 4262, + "expanded_bytes": 1034475520, + "regular_bytes": 1031068984, + "omitted_symlinks": 0, + "omitted_symlink_metadata_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "required_files": { + "code": { + "sha256": "cf8508cc7ddde30cfccc70caeda877a9a99119953c4476dcb30a3eb1a2c415e9", + "bytes": 223685880, + "identity": [ + 2049, + 8942516 + ] + }, + "resources/app/out/cli.js": { + "sha256": "bd393ad6f0d28ba0aa7cd79cc685617f5187096f092009e315a4f098349f7976", + "bytes": 275169, + "identity": [ + 2049, + 8944347 + ] + }, + "resources/app/product.json": { + "sha256": "a141a1de8ed44415c0c527b1882362e6abc25e674be052d67c09da71a27fbd7e", + "bytes": 68251, + "identity": [ + 2049, + 8942528 + ] + } + }, + "cleanup": { + "complete": true, + "probe": { + "probe": "cleanup", + "argv": [ + "/usr/bin/python3", + "-c", + "\nimport os,stat,sys\npath,device,inode=sys.argv[1:]\nparent,name=os.path.split(path)\npfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)\nroot=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd)\nidentity=os.fstat(root)\nif (identity.st_dev,identity.st_ino)!=(int(device),int(inode)):raise SystemExit(\"cleanup root identity changed\")\ndef clear(fd):\n for name in os.listdir(fd):\n item=os.stat(name,dir_fd=fd,follow_symlinks=False)\n if stat.S_ISDIR(item.st_mode):\n child=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)\n try:\n if not os.path.samestat(item,os.fstat(child)):raise RuntimeError(\"cleanup directory changed\")\n clear(child)\n finally:os.close(child)\n os.rmdir(name,dir_fd=fd)\n else:os.unlink(name,dir_fd=fd)\nclear(root)\nif not os.path.samestat(identity,os.stat(name,dir_fd=pfd,follow_symlinks=False)):raise SystemExit(\"cleanup root replaced\")\nos.close(root)\nos.rmdir(name,dir_fd=pfd)\nos.close(pfd)\n", + "/tmp/vscode-native-research-33aorwdd", + "2049", + "8942514" + ], + "cwd": "/home/runner/work/_temp/vscode-native-research", + "error": null, + "exit": 0, + "elapsed_seconds": 0.25, + "stdout_file": "cleanup.stdout.log", + "stderr_file": "cleanup.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-restored/archive-first-member-range.json b/docs/evidence/project-scope-research/vscode-1.143.1-restored/archive-first-member-range.json new file mode 100644 index 000000000..7c85d03ed --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-restored/archive-first-member-range.json @@ -0,0 +1,26 @@ +{ + "status": 206, + "headers": { + "content-type": "application/octet-stream", + "server": "envoy", + "accept-ranges": "bytes", + "cache-control": "public, max-age=259200", + "x-ms-apiversion": "Distribute 1.2", + "x-ms-region": "prod-eus2-z1", + "content-disposition": "attachment; filename=code-stable-x64-1792691793.tar.gz; filename*=UTF-8''code-stable-x64-1792691793.tar.gz", + "last-modified": "Thu, 22 Oct 2026 18:32:15 GMT", + "etag": "\"0x89DDA818DCC766545E841388A568EF4D04643809A395350E04B857B617F06C41\"", + "date": "Sat, 24 Oct 2026 08:22:51 GMT", + "content-range": "bytes 0-65535/350844741", + "content-length": "65536", + "x-osid": "2", + "x-cid": "2", + "x-ccc": "US", + "x-envoy-upstream-service-time": "225", + "connection": "close" + }, + "read_bytes": 65536, + "first_tar_name": "VSCode-linux-x64/", + "first_tar_type": "5", + "partial_only": true +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-restored/archive-head-metadata.json b/docs/evidence/project-scope-research/vscode-1.143.1-restored/archive-head-metadata.json new file mode 100644 index 000000000..2fdfe972e --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-restored/archive-head-metadata.json @@ -0,0 +1,21 @@ +{ + "status": 200, + "headers": { + "content-length": "350844741", + "content-type": "application/octet-stream", + "server": "envoy", + "accept-ranges": "bytes", + "cache-control": "public, max-age=259200", + "etag": "\"0x89DDA818DCC766545E841388A568EF4D04643809A395350E04B857B617F06C41\"", + "last-modified": "Thu, 22 Oct 2026 18:32:15 GMT", + "x-ms-apiversion": "Distribute 1.2", + "x-ms-region": "prod-eus2-z1", + "content-disposition": "attachment; filename=code-stable-x64-1792691793.tar.gz; filename*=UTF-8''code-stable-x64-1792691793.tar.gz", + "date": "Sat, 24 Oct 2026 08:22:50 GMT", + "x-osid": "2", + "x-cid": "2", + "x-ccc": "US", + "x-envoy-upstream-service-time": "230", + "connection": "close" + } +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-restored/stable-update-metadata.json b/docs/evidence/project-scope-research/vscode-1.143.1-restored/stable-update-metadata.json new file mode 100644 index 000000000..d94da25f5 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-restored/stable-update-metadata.json @@ -0,0 +1 @@ +{"url":"https://vscode.download.prss.microsoft.com/dbazure/download/stable/e646868227c3e3a9e3f24277c8c7220be42c0be7/code-stable-x64-1792691793.tar.gz","name":"1.143.1","version":"e646868227c3e3a9e3f24277c8c7220be42c0be7","productVersion":"1.143.1","hash":"ab4ee4bba476f05ef69cd369174bb2eac2df98f3","timestamp":1792690529437,"sha256hash":"48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be","supportsFastUpdate":true,"notes":"e646868227c3e3a9e3f24277c8c7220be42c0be7"} \ No newline at end of file diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/A.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/A.merged.log new file mode 100644 index 000000000..60ded04e1 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/A.merged.log @@ -0,0 +1,2 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5300,"network_namespace":4026532326,"parent_network_namespace":4026531833,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} +[5300:1025/104944.207946:FATAL:sandbox/linux/suid/client/setuid_sandbox_host.cc:166] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /tmp/uap-vscode-sealed-2209129318/publisher/VSCode-linux-x64/chrome-sandbox is owned by root and has mode 4755. diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/README.md b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/README.md new file mode 100644 index 000000000..3e786b48c --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/README.md @@ -0,0 +1,56 @@ +# VS Code 1.143.1 untrusted workspace: blocked before metadata + +Genuine hosted observation attempted on 2026-10-25 at source +`37fc30df2cb0b8499c6d32520e8b32d82d7fa9da`, pinned publisher commit +`e646868227c3e3a9e3f24277c8c7220be42c0be7`, in +[run 40182639794](https://github.com/777genius/universal-agent-plugins/actions/runs/40182639794), +[job 121136740348](https://github.com/777genius/universal-agent-plugins/actions/runs/40182639794/job/121136740348). +The inert contracts passed. The actual collector exited 2; **no workspace +component contract or product implementation is admitted by this artifact**. + +Provider artifact `vscode-1.143.1-untrusted-workspace-linux-amd64`, ID +`12596073531`, was 5,185 bytes, SHA256 +`21bad1e1d409fe33476cc2b9ec96d64aaafc38c0609d7262aec8617eb897d977`. +Its sole member is `research.zip`; all 20 inner text members are preserved +byte-for-byte here. `provenance.json` records both ZIP identities and every +raw member length/hash. The worker verified the outer digest, unique flat +non-symlink member closure, 14 separate command streams, two merged native +diagnostic streams and exact frontend/controller source agreement. Binary ZIPs +are not committed, and original failed summaries are unchanged. + +## Concrete native blocker + +`A.merged.log` records the publisher's Chromium fatal error: its SUID sandbox +helper is present but is not configured as a root-owned mode-4755 executable. +The publisher aborts rather than run without sandboxing. The approved collector +intentionally permits execution only of the ordinary nonprivileged editor and +ripgrep ELF files; it does not enable the privileged sandbox helper. The reviewed +plan explicitly forbids granting setuid privileges, disabling the sandbox or +changing kernel/AppArmor policy as a fallback. No such fallback was attempted. + +The A verifier emitted real UID/EUID/GID/EGID 1001, empty groups, zero capability +sets, no-new-privileges and a network namespace distinct from its recorded +parent, with only loopback. Xvfb also emitted its own isolation proof. These +pre-exec facts do not prove successful workbench startup or successful teardown. +Both child records report `tree_reaped:false`, `planned_stop:false`, and failed +strict cleanup; display asset cleanup remains unproved. The controller's +`observations` is `null`: there is no A metadata result and the B/restart/removal +table was not completed. All qualification flags and collection-complete values +remain false. + +## Separate cleanup defect + +The root-owned sealed-tree remover returned successfully, but global display +assets were deliberately retained because their process-tree cleanup was not +proved. Separately, frontend cleanup of its own input tree failed with +`PermissionError` on `mkall.sh` under the Go build/module cache. That cache can +contain ordinary owner-controlled read-only directories; the current held-root +remover does not restore owner write permission before descending. A narrow +final-cleanup correction requires its own source review and inert regression; +it cannot repair or qualify the failed native workspace observation. + +The artifact is negative native evidence. A newly reviewed safe execution +design and fresh positive component observations remain necessary before the +conditional VS Code adapter plan permits product code. Help/version success, +static schema support, VM disposal or local filesystem fixtures cannot replace +that gate. No native client was rerun while interpreting this artifact. diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/Xvfb.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/Xvfb.merged.log new file mode 100644 index 000000000..f16389ce4 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/Xvfb.merged.log @@ -0,0 +1,20 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5296,"network_namespace":4026532216,"parent_network_namespace":4026531833,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} +The XKEYBOARD keymap compiler (xkbcomp) reports: +> Warning: Could not resolve keysym XF86CameraAccessEnable +> Warning: Could not resolve keysym XF86CameraAccessDisable +> Warning: Could not resolve keysym XF86CameraAccessToggle +> Warning: Could not resolve keysym XF86NextElement +> Warning: Could not resolve keysym XF86PreviousElement +> Warning: Could not resolve keysym XF86AutopilotEngageToggle +> Warning: Could not resolve keysym XF86MarkWaypoint +> Warning: Could not resolve keysym XF86Sos +> Warning: Could not resolve keysym XF86NavChart +> Warning: Could not resolve keysym XF86FishingChart +> Warning: Could not resolve keysym XF86SingleRangeRadar +> Warning: Could not resolve keysym XF86DualRangeRadar +> Warning: Could not resolve keysym XF86RadarOverlay +> Warning: Could not resolve keysym XF86TraditionalSonar +> Warning: Could not resolve keysym XF86ClearvuSonar +> Warning: Could not resolve keysym XF86SidevuSonar +> Warning: Could not resolve keysym XF86NavInfo +Errors from xkbcomp are not fatal to the X server diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/cleanup.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/cleanup.stderr.log new file mode 100644 index 000000000..fb24cb35a --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/cleanup.stderr.log @@ -0,0 +1,8 @@ +Traceback (most recent call last): + File "", line 20, in + File "", line 16, in clear + File "", line 16, in clear + File "", line 16, in clear + [Previous line repeated 5 more times] + File "", line 19, in clear +PermissionError: [Errno 13] Permission denied: 'mkall.sh' diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/cleanup.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/controller.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/controller.stderr.log new file mode 100644 index 000000000..5a85e55ae --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/controller.stderr.log @@ -0,0 +1,8 @@ +context deadline exceeded +context deadline exceeded +duplex process left live descendants that required forced cleanup +signal: trace/breakpoint trap (core dumped) +duplex process required forced supervised cleanup after its exit grace +signal: killed +display tree/asset cleanup unproved; no global unlink authorized +display tree/asset cleanup unproved; no global unlink authorized diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/controller.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/controller.stdout.log new file mode 100644 index 000000000..b5908f5d1 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/controller.stdout.log @@ -0,0 +1,5 @@ +{ + "cleanup_complete": false, + "collection_complete": false, + "error": "context deadline exceeded\ncontext deadline exceeded\nduplex process left live descendants that required forced cleanup\nsignal: trace/breakpoint trap (core dumped)\nduplex process required forced supervised cleanup after its exit grace\nsignal: killed\ndisplay tree/asset cleanup unproved; no global unlink authorized\ndisplay tree/asset cleanup unproved; no global unlink authorized" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/go-version.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/go-version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/go-version.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/go-version.stdout.log new file mode 100644 index 000000000..a3cd3c880 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/go-version.stdout.log @@ -0,0 +1 @@ +go version go1.26.10 linux/amd64 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/helper-build.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/helper-build.stderr.log new file mode 100644 index 000000000..bb677a9bd --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/helper-build.stderr.log @@ -0,0 +1 @@ +go: downloading golang.org/x/sys v0.44.0 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/helper-build.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/helper-build.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/provenance.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/provenance.json new file mode 100644 index 000000000..cccd488ad --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/provenance.json @@ -0,0 +1,94 @@ +{ + "run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40182639794", + "job_id": 121136740348, + "artifact_id": 12596073531, + "artifact_name": "vscode-1.143.1-untrusted-workspace-linux-amd64", + "outer_bytes": 5185, + "outer_sha256": "21bad1e1d409fe33476cc2b9ec96d64aaafc38c0609d7262aec8617eb897d977", + "inner_bytes": 17759, + "inner_sha256": "17c3dfca8efaceffdb6ef807c791df351c419604fb3c7949aaf7653fc644d73d", + "source_commit": "37fc30df2cb0b8499c6d32520e8b32d82d7fa9da", + "members": { + "A.merged.log": { + "bytes": 586, + "sha256": "9bd9099649a4052af09250d67430c4a8a23da15b3028651d1ef0833422a295b1" + }, + "Xvfb.merged.log": { + "bytes": 1408, + "sha256": "e94cc6e16b6c3a1ca246c9ac86d70cb1586bf9a160bc43f2bfa5f73edccd8e37" + }, + "cleanup.stderr.log": { + "bytes": 321, + "sha256": "2e35fe8e97094b539670868e17bd25952a18f237b05480eb9a6183224b7fcae9" + }, + "cleanup.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "controller.stderr.log": { + "bytes": 378, + "sha256": "8b91960103eb79ff1b9f3d4f6778da29f5bf462dfa76b7ca811d2b47d9cba2fc" + }, + "controller.stdout.log": { + "bytes": 463, + "sha256": "f0fb78e2076656f499b64dfb3f617444145d0da221a6463af44488af972bb2da" + }, + "go-version.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "go-version.stdout.log": { + "bytes": 33, + "sha256": "4e0b213cba780f63b02f3ab9001ee61e4101d254e0af1a8205574c19dad0f93b" + }, + "helper-build.stderr.log": { + "bytes": 41, + "sha256": "b683257fca25cabaa9ba8f1ca2a8006ae11a5853fa2424b28a8f8f38d274d569" + }, + "helper-build.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "root-cleanup.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "root-cleanup.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "source-checkout.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "source-checkout.stdout.log": { + "bytes": 41, + "sha256": "eab43b276d82cd631bbd75b1b88caa82f13221b472bae7373181cf63d81c71e4" + }, + "system-executables.json": { + "bytes": 364, + "sha256": "c2e82bf6e074463af4160df7b0383de8529abcfe3251257407978065f62959e2" + }, + "vscode-workspace-controller.json": { + "bytes": 4832, + "sha256": "d93d546da26cf3bec0c67b140a8870b3c262d66a6449959171a349994c287df6" + }, + "vscode-workspace-research.json": { + "bytes": 6446, + "sha256": "50b60f5f6c385f93c539c03b0c41f4b84250ec8d3ff7d7f2bed33ad1f13dbac5" + }, + "xauth.json": { + "bytes": 382, + "sha256": "6a21b49a557d0a5e8fdc5572ffc28db19568be321ddf057c98586b378d83a05f" + }, + "xauth.stderr.log": { + "bytes": 86, + "sha256": "e8cde161b289053605fdc42da3a04309a93f43ca03b8e7fcd80a9604d8e35f6b" + }, + "xauth.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + "qualification": "failed observation; no project component contract admitted" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/root-cleanup.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/root-cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/root-cleanup.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/root-cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/source-checkout.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/source-checkout.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/source-checkout.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/source-checkout.stdout.log new file mode 100644 index 000000000..cdbb934b2 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/source-checkout.stdout.log @@ -0,0 +1 @@ +37fc30df2cb0b8499c6d32520e8b32d82d7fa9da diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/system-executables.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/system-executables.json new file mode 100644 index 000000000..eee7d7149 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/system-executables.json @@ -0,0 +1,12 @@ +{ + "/usr/bin/Xvfb": { + "bytes": 2064864, + "sha256": "6b27845931670c8c9b3ce7423696a452ce3cab1c7f316ce5f7ea7f1297fa2f84", + "version": "not inferred from binary identity" + }, + "/usr/bin/xauth": { + "bytes": 56280, + "sha256": "ffcb5f569bf9fce94cbf37f5d61790d463508c6122511a5f4c6e280d3633086e", + "version": "not inferred from binary identity" + } +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/vscode-workspace-controller.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/vscode-workspace-controller.json new file mode 100644 index 000000000..3169bb525 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/vscode-workspace-controller.json @@ -0,0 +1,112 @@ +{ + "children": [ + { + "label": "A", + "argv": [ + "/tmp/uap-vscode-sealed-2209129318/publisher/VSCode-linux-x64/code", + "--new-window", + "--disable-crash-reporter", + "--user-data-dir", + "/tmp/uap-vscode-sealed-2209129318/probes/A/profile", + "--extensions-dir", + "/tmp/uap-vscode-sealed-2209129318/probes/A/extensions", + "--extensionDevelopmentPath", + "/tmp/uap-vscode-sealed-2209129318/observer", + "--extensionTestsPath", + "/tmp/uap-vscode-sealed-2209129318/observer/runner.cjs", + "--enable-proposed-api", + "uap-research.workspace-observer", + "/tmp/uap-vscode-sealed-2209129318/projects/A" + ], + "cwd": "/tmp/uap-vscode-sealed-2209129318/projects/A", + "isolation": { + "pid": 5300, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532326, + "parent_network_namespace": 4026531833, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "A.merged.log", + "merged_bytes": 586, + "merged_sha256": "9bd9099649a4052af09250d67430c4a8a23da15b3028651d1ef0833422a295b1", + "merged_overflow": false, + "planned_stop": false, + "tree_reaped": false, + "error": "context deadline exceeded\ncontext deadline exceeded\nduplex process left live descendants that required forced cleanup\nsignal: trace/breakpoint trap (core dumped)" + }, + { + "label": "Xvfb", + "argv": [ + "/usr/bin/Xvfb", + ":93", + "-screen", + "0", + "1024x768x24", + "-nolisten", + "tcp", + "-auth", + "/tmp/uap-vscode-sealed-2209129318/auth/authority" + ], + "cwd": "/tmp/uap-vscode-sealed-2209129318", + "isolation": { + "pid": 5296, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532216, + "parent_network_namespace": 4026531833, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "Xvfb.merged.log", + "merged_bytes": 1408, + "merged_sha256": "e94cc6e16b6c3a1ca246c9ac86d70cb1586bf9a160bc43f2bfa5f73edccd8e37", + "merged_overflow": false, + "planned_stop": false, + "tree_reaped": false, + "error": "context deadline exceeded\ncontext deadline exceeded\nduplex process left live descendants that required forced cleanup\nsignal: trace/breakpoint trap (core dumped)\nduplex process required forced supervised cleanup after its exit grace\nsignal: killed" + } + ], + "cleanup": { + "complete": false, + "error": "display tree/asset cleanup unproved; no global unlink authorized", + "stderr_bytes": 0, + "stderr_file": "root-cleanup.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 0, + "stdout_file": "root-cleanup.stdout.log", + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "collection_complete": false, + "diagnostics": "verifier-prefixed merged streams; planned teardown may truncate trailing diagnostics; API result is separate", + "error": "context deadline exceeded\ncontext deadline exceeded\nduplex process left live descendants that required forced cleanup\nsignal: trace/breakpoint trap (core dumped)\nduplex process required forced supervised cleanup after its exit grace\nsignal: killed\ndisplay tree/asset cleanup unproved; no global unlink authorized", + "evidence_kind": "vscode-pinned-untrusted-workspace-research", + "executable_hashes": { + "code": "cf8508cc7ddde30cfccc70caeda877a9a99119953c4476dcb30a3eb1a2c415e9", + "resources/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/linux-x64/rg": "193906679498de4d939345b937fa24e0e69a03c244bd70c859f5e41232713f21", + "resources/app/out/cli.js": "bd393ad6f0d28ba0aa7cd79cc685617f5187096f092009e315a4f098349f7976", + "resources/app/product.json": "a141a1de8ed44415c0c527b1882362e6abc25e674be052d67c09da71a27fbd7e" + }, + "native_mcp_initialization_qualified": false, + "native_project_qualified": false, + "native_runtime_qualified": false, + "observations": null, + "publisher_commit": "e646868227c3e3a9e3f24277c8c7220be42c0be7", + "source_commit": "37fc30df2cb0b8499c6d32520e8b32d82d7fa9da", + "version": "1.143.1" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/vscode-workspace-research.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/vscode-workspace-research.json new file mode 100644 index 000000000..e6fec89e2 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/vscode-workspace-research.json @@ -0,0 +1,158 @@ +{ + "evidence_kind": "vscode-untrusted-workspace-metadata-research", + "source_commit": "37fc30df2cb0b8499c6d32520e8b32d82d7fa9da", + "version": "1.143.1", + "publisher_commit": "e646868227c3e3a9e3f24277c8c7220be42c0be7", + "collection_complete": false, + "native_project_qualified": false, + "native_mcp_initialization_qualified": false, + "native_runtime_qualified": false, + "checkout": { + "probe": "source-checkout", + "argv": [ + "/usr/bin/git", + "rev-parse", + "HEAD" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.003, + "stdout_file": "source-checkout.stdout.log", + "stderr_file": "source-checkout.stderr.log", + "stdout_bytes": 41, + "stderr_bytes": 0, + "stdout_sha256": "eab43b276d82cd631bbd75b1b88caa82f13221b472bae7373181cf63d81c71e4", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "archive": { + "url": "https://vscode.download.prss.microsoft.com/dbazure/download/stable/e646868227c3e3a9e3f24277c8c7220be42c0be7/code-stable-x64-1792691793.tar.gz", + "bytes": 350844741, + "sha256": "48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be" + }, + "materialization": { + "members": 4262, + "expanded_bytes": 1034475520, + "regular_bytes": 1031068984, + "omitted_symlinks": 0, + "omitted_symlink_metadata_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "publisher_files": { + "code": { + "sha256": "cf8508cc7ddde30cfccc70caeda877a9a99119953c4476dcb30a3eb1a2c415e9", + "bytes": 223685880, + "identity": [ + 2049, + 9216988 + ] + }, + "resources/app/out/cli.js": { + "sha256": "bd393ad6f0d28ba0aa7cd79cc685617f5187096f092009e315a4f098349f7976", + "bytes": 275169, + "identity": [ + 2049, + 9219812 + ] + }, + "resources/app/product.json": { + "sha256": "a141a1de8ed44415c0c527b1882362e6abc25e674be052d67c09da71a27fbd7e", + "bytes": 68251, + "identity": [ + 2049, + 9217007 + ] + } + }, + "helper": { + "version_probe": { + "probe": "go-version", + "argv": [ + "/opt/hostedtoolcache/go/1.26.10/x64/bin/go", + "version" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.005, + "stdout_file": "go-version.stdout.log", + "stderr_file": "go-version.stderr.log", + "stdout_bytes": 33, + "stderr_bytes": 0, + "stdout_sha256": "4e0b213cba780f63b02f3ab9001ee61e4101d254e0af1a8205574c19dad0f93b", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "build": { + "probe": "helper-build", + "argv": [ + "/opt/hostedtoolcache/go/1.26.10/x64/bin/go", + "build", + "-trimpath", + "-p=1", + "-ldflags", + "-X main.buildCommit=37fc30df2cb0b8499c6d32520e8b32d82d7fa9da", + "-o", + "/tmp/vscode-workspace-input-9ql8h61e/controller", + "./install/integrationctl/research/vscodeworkspace" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 11.967, + "stdout_file": "helper-build.stdout.log", + "stderr_file": "helper-build.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 41, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "b683257fca25cabaa9ba8f1ca2a8006ae11a5853fa2424b28a8f8f38d274d569" + }, + "bytes": 4980186, + "sha256": "cdfa7504de0afbb28857d4506ea1cb3eb760877e9be6c677f4cc274f746296e6" + }, + "controller": { + "argv": [ + "/usr/bin/sudo", + "-n", + "--preserve-env=GITHUB_ACTIONS,RUNNER_ENVIRONMENT,AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED,EXPECTED_COMMIT", + "/tmp/vscode-workspace-input-9ql8h61e/controller", + "--input-root", + "/tmp/vscode-workspace-input-9ql8h61e" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": "fixed controller returned failure", + "controller_reaped": true, + "exit": 1, + "stdout_file": "controller.stdout.log", + "stdout_bytes": 463, + "stdout_sha256": "f0fb78e2076656f499b64dfb3f617444145d0da221a6463af44488af972bb2da", + "stderr_file": "controller.stderr.log", + "stderr_bytes": 378, + "stderr_sha256": "8b91960103eb79ff1b9f3d4f6778da29f5bf462dfa76b7ca811d2b47d9cba2fc" + }, + "error": "ValueError: fixed root controller failed; retain bounded evidence", + "frontend_cleanup": { + "complete": false, + "probe": { + "probe": "cleanup", + "argv": [ + "/usr/bin/python3", + "-I", + "-c", + "\nimport os,stat,sys\npath,device,inode=sys.argv[1:]\nparent,name=os.path.split(path)\npfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)\nroot=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd)\nidentity=os.fstat(root)\nif (identity.st_dev,identity.st_ino)!=(int(device),int(inode)):raise SystemExit(\"cleanup root identity changed\")\ndef clear(fd):\n for name in os.listdir(fd):\n item=os.stat(name,dir_fd=fd,follow_symlinks=False)\n if stat.S_ISDIR(item.st_mode):\n child=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)\n try:\n if not os.path.samestat(item,os.fstat(child)):raise RuntimeError(\"cleanup directory changed\")\n clear(child)\n finally:os.close(child)\n os.rmdir(name,dir_fd=fd)\n else:os.unlink(name,dir_fd=fd)\nclear(root)\nif not os.path.samestat(identity,os.stat(name,dir_fd=pfd,follow_symlinks=False)):raise SystemExit(\"cleanup root replaced\")\nos.close(root)\nos.rmdir(name,dir_fd=pfd)\nos.close(pfd)\n", + "/tmp/vscode-workspace-input-9ql8h61e", + "2049", + "9216975" + ], + "cwd": "/", + "error": null, + "exit": 1, + "elapsed_seconds": 0.425, + "stdout_file": "cleanup.stdout.log", + "stderr_file": "cleanup.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 321, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "2e35fe8e97094b539670868e17bd25952a18f237b05480eb9a6183224b7fcae9" + } + }, + "elapsed_seconds": 66.255588783 +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.json new file mode 100644 index 000000000..1a3d6867d --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.json @@ -0,0 +1,15 @@ +{ + "argv": [ + "/usr/bin/xauth", + "-f", + "/tmp/uap-vscode-sealed-2209129318/auth/authority", + "source", + "-" + ], + "error": "", + "redacted": true, + "stderr_bytes": 86, + "stderr_sha256": "e8cde161b289053605fdc42da3a04309a93f43ca03b8e7fcd80a9604d8e35f6b", + "stdout_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.stderr.log new file mode 100644 index 000000000..c526a3f6c --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.stderr.log @@ -0,0 +1 @@ +/usr/bin/xauth: file /tmp/uap-vscode-sealed-2209129318/auth/authority does not exist diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40182639794/xauth.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-after-removal.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-after-removal.merged.log new file mode 100644 index 000000000..c9a20f4f4 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-after-removal.merged.log @@ -0,0 +1,501 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5874,"network_namespace":4026532313,"parent_network_namespace":4026531840,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} +Unable to create argv.json configuration file in /tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json, falling back to defaults (Error: EACCES: permission denied, mkdir '/tmp/uap-vscode-sealed-3399636247/home/.vscode') +[5874:1025/112040.840976:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5874:1025/112040.849880:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[5874:1025/112040.851808:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5874:1025/112040.852194:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[main 2026-10-25T11:20:40.954Z] [node-compile-cache] main: packaged cache inactive (status: disabled, configured: /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node-compile-cache/main, active: none) +[main 2026-10-25T11:20:40.991Z] Error: Unable to retrieve mac address (unexpected format) + at F_ (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:4889) + at G5 (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5213) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5132 + at async ZD (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100630) + at async nM (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100975) + at async Promise.all (index 0) + at async Xs.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:13169) + at async fP.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:192:55975) +[main 2026-10-25T11:20:41.000Z] StorageMainService: creating application shared storage +[5874:1025/112041.088685:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[main 2026-10-25T11:20:41.088Z] [shared storage] Creating shared storage database at ':memory:' (wasCreated: true) +[main 2026-10-25T11:20:41.089Z] [shared storage] Initializing fallback application storage (path: in-memory) +[main 2026-10-25T11:20:41.106Z] gn [Error]: Unable to read file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json' (Error: Unable to resolve nonexistent file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json') + at ea.restoreReadError (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10637) + at ea.doReadFileStream (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10420) + at async ea.doReadFile (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:9670) + at async Xs.updateCrashReporterEnablement (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:30775) { + fileOperationResult: 1, + options: { preferUnbuffered: true } +} +[5942:1025/112041.108312:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:285] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +[main 2026-10-25T11:20:41.128Z] [shared storage] Fallback application storage initialized with 3 items +Via 'product.json#extensionEnabledApiProposals' extension 'github.vscode-pull-request-github' wants API proposal 'agentsWindowActivation' but that proposal DOES NOT EXIST. Likely, the proposal has been finalized (check 'vscode.d.ts') or was abandoned. +[main 2026-10-25T11:20:42.455Z] update#disable - updates are disabled by user preference +[main 2026-10-25T11:20:42.456Z] update#setState disabled +[RemoteAgentHost] Reconciling: desired=[], current=[] +[5874:1025/112042.467005:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[AgentHost:renderer] Acquiring MessagePort to agent host... +[main 2026-10-25T11:20:42.498Z] AgentHostProcessManager: agent host started +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey=undefined conversationKey=undefined modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[LM] Failed to request chat control data Offline +[AgentHost:renderer] MessagePort acquired, creating client... +Started local extension host with pid 6025. +Started initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/extensions +[main 2026-10-25T11:20:43.309Z] [AgentHost:stderr] (node:5969) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities. +(Use `exe --trace-deprecation ...` to show where the warning was created) + +[RemoteAgentHostProtocol] Sending initialize: address=local clientId=3f802e7e-c45f-476d-9983-3000b3f87724 requestId=1 +[main 2026-10-25T11:20:43.384Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Initialized: address=local clientId=3f802e7e-c45f-476d-9983-3000b3f87724 durationMs=920 +[AgentHost:renderer] Protocol connection established; clientId=3f802e7e-c45f-476d-9983-3000b3f87724 +[main 2026-10-25T11:20:43.500Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Completed initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/extensions +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +Loading development extension at /tmp/uap-vscode-sealed-3399636247/observer +Extension 'github.copilot-chat' appears in product.json but enables LESS API proposals than the extension wants. +package.json (LOSES): agentSessionsWorkspace +speech +agentsWindowConfiguration +chatDebug +chatHooks +envIsConnectionMetered +extensionsAny +newSymbolNamesProvider +interactive +codeActionAI +activeComment +commentReveal +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +documentFiltersExclusive +embeddings +findTextInFiles +findTextInFiles2 +languageModelToolSupportsModel +findFiles2 +textSearchProvider +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +chatParticipantAdditions +defaultChatParticipant +contribSourceControlInputBoxMenu +authLearnMore +authIssuers +testObserver +aiTextSearchProvider +chatParticipantPrivate +chatProvider +contribDebugCreateConfiguration +chatReferenceDiagnostic +textSearchProvider2 +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelPricing +languageModelRateLimit +inlineCompletionsAdditions +chatStatusItem +chatInputNotification +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +languageModelThinkingPart +chatSessionsProvider +devDeviceId +contribEditorContentMenu +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +environmentPower +terminalTitle +toolInvocationApproveCombination +chatSessionCustomizationProvider +product.json (WINS): agentSessionsWorkspace +agentsWindowConfiguration +interactive +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +chatParticipantAdditions +defaultChatParticipant +embeddings +chatProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +codeActionAI +findTextInFiles +findTextInFiles2 +textSearchProvider +textSearchProvider2 +activeComment +commentReveal +contribSourceControlInputBoxMenu +contribCommentEditorActionsMenu +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +extensionAffinity +newSymbolNamesProvider +findFiles2 +chatReferenceDiagnostic +extensionsAny +authLearnMore +testObserver +aiTextSearchProvider +documentFiltersExclusive +chatParticipantPrivate +contribDebugCreateConfiguration +inlineCompletionsAdditions +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelThinkingPart +languageModelToolSupportsModel +chatStatusItem +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +chatSessionsProvider +chatSessionCustomizationProvider +devDeviceId +contribEditorContentMenu +tokenInformation +toolInvocationApproveCombination +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +chatHooks +chatDebug +envIsConnectionMetered +environmentPower +terminalTitle +languageModelPricing +chatInputNotification +speech +DELTA: authIssuers +languageModelRateLimit +Proceeding with EXTRA proposals (authIssuers, languageModelRateLimit) because extension is in development mode. Still, this EXTENSION WILL BE BROKEN unless product.json is updated. +Failed to resolve MCP server 'uap-user' from gallery: Offline: Offline +Unknown channel: agentHostClientByokLm +Unknown channel: agentHostClientProxy +Offline: Offline: Offline + at sRt.queryRawGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:56135) + at async sRt.queryGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:51373) + at async sRt.getExtensionsUsingQueryApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:43611) + at async sRt.getExtensionsUsingResourceApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:44980) + at async sRt.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:42545) + at async ise.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:22515) + at async ise.onDidChangeRunningExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:19518) +[main 2026-10-25T11:20:44.376Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Unknown channel: agentHostClientProxy +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[main 2026-10-25T11:20:44.493Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:20:44.493Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:20:44.705Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[RemoteAgentHostProtocol] Request 2 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[RemoteAgentHost] Reconciling: desired=[], current=[] +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +Settings Sync: Account status changed from uninitialized to unavailable +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:20:45.164Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Request 3 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:20:45.705Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:20:45.705Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/profile/User/settings.json.vsctmp'): Error: Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/profile/User/settings.json.vsctmp') + at owe.writeFile (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5419:92390) + at async vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1707:22639 +Offline: Offline: Offline + at Xrr (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5425:54431) + at async bqt.logAndRequest (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:635:10540) + at async kmt.doFetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7655) + at async kmt.fetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7563) +[main 2026-10-25T11:20:46.164Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:20:46.164Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZWZmNjAyNmQtODc4MC00ZDNkLTgwZGYtOGVmZjFiNTYxNmU3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-after-removal.observation.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-after-removal.observation.json new file mode 100644 index 000000000..79227c6e8 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-after-removal.observation.json @@ -0,0 +1 @@ +{"schema_version":1,"collected":true,"version":"1.143.1","app_root":"/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app","app_host":"desktop","observer":{"id":"uap-research.workspace-observer","mode":3,"proposals":["mcpServerDefinitions","chatPromptFiles"]},"folder":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A","authority":""},"trusted":false,"policy":{"skills_enabled":true,"mcp_access":"all","mcp_autostart":"never"},"mcp":[{"label":"uap-user","version":"dca08194154f5a28c2d70ef3da3dacc7adb610117a25421177e49d4b110b106d","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-USER","scheme":"http","path":"/UAP-USER","authority":"127.0.0.1:9"},"headers":{}}],"skills":[{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-shared/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-shared/SKILL.md","authority":""},"name":"uap-shared","source":"user","disableModelInvocation":true,"description":"USER-SHARED","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","authority":""},"name":"uap-user","source":"user","disableModelInvocation":true,"description":"USER","userInvocable":true}],"native_project_qualified":false,"native_mcp_initialization_qualified":false,"native_runtime_qualified":false} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-restart.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-restart.merged.log new file mode 100644 index 000000000..ec8d7adcb --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-restart.merged.log @@ -0,0 +1,506 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5658,"network_namespace":4026532313,"parent_network_namespace":4026531840,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} +Unable to revert mtime: /usr/share/fonts/truetype/lato +Unable to revert mtime: /usr/share/fonts/truetype/liberation +Unable to revert mtime: /usr/share/fonts/truetype/noto +Unable to revert mtime: /usr/share/fonts/truetype/lato +Unable to revert mtime: /usr/share/fonts/truetype/liberation +Unable to revert mtime: /usr/share/fonts/truetype/noto +Unable to create argv.json configuration file in /tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json, falling back to defaults (Error: EACCES: permission denied, mkdir '/tmp/uap-vscode-sealed-3399636247/home/.vscode') +[5658:1025/112021.497975:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5658:1025/112021.507825:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[5658:1025/112021.508830:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5658:1025/112021.511192:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[main 2026-10-25T11:20:21.618Z] [node-compile-cache] main: packaged cache inactive (status: disabled, configured: /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node-compile-cache/main, active: none) +[main 2026-10-25T11:20:21.650Z] Error: Unable to retrieve mac address (unexpected format) + at F_ (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:4889) + at G5 (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5213) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5132 + at async ZD (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100630) + at async nM (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100975) + at async Promise.all (index 0) + at async Xs.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:13169) + at async fP.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:192:55975) +[main 2026-10-25T11:20:21.658Z] StorageMainService: creating application shared storage +[5658:1025/112021.743643:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[main 2026-10-25T11:20:21.743Z] [shared storage] Creating shared storage database at ':memory:' (wasCreated: true) +[main 2026-10-25T11:20:21.744Z] [shared storage] Initializing fallback application storage (path: in-memory) +[5730:1025/112021.759716:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:285] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +[main 2026-10-25T11:20:21.777Z] gn [Error]: Unable to read file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json' (Error: Unable to resolve nonexistent file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json') + at ea.restoreReadError (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10637) + at ea.doReadFileStream (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10420) + at async ea.doReadFile (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:9670) + at async Xs.updateCrashReporterEnablement (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:30775) { + fileOperationResult: 1, + options: { preferUnbuffered: true } +} +[main 2026-10-25T11:20:21.782Z] [shared storage] Fallback application storage initialized with 3 items +Via 'product.json#extensionEnabledApiProposals' extension 'github.vscode-pull-request-github' wants API proposal 'agentsWindowActivation' but that proposal DOES NOT EXIST. Likely, the proposal has been finalized (check 'vscode.d.ts') or was abandoned. +[main 2026-10-25T11:20:23.112Z] update#disable - updates are disabled by user preference +[main 2026-10-25T11:20:23.112Z] update#setState disabled +[RemoteAgentHost] Reconciling: desired=[], current=[] +[5658:1025/112023.124118:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[AgentHost:renderer] Acquiring MessagePort to agent host... +[main 2026-10-25T11:20:23.152Z] AgentHostProcessManager: agent host started +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey=undefined conversationKey=undefined modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[LM] Failed to request chat control data Offline +[AgentHost:renderer] MessagePort acquired, creating client... +Started local extension host with pid 5812. +Started initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/A-restart/extensions +[main 2026-10-25T11:20:23.981Z] [AgentHost:stderr] (node:5756) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities. +(Use `exe --trace-deprecation ...` to show where the warning was created) + +[RemoteAgentHostProtocol] Sending initialize: address=local clientId=b7b4b0b9-035e-4ba7-8b66-8d158051a43d requestId=1 +[RemoteAgentHostProtocol] Initialized: address=local clientId=b7b4b0b9-035e-4ba7-8b66-8d158051a43d durationMs=844 +[AgentHost:renderer] Protocol connection established; clientId=b7b4b0b9-035e-4ba7-8b66-8d158051a43d +[main 2026-10-25T11:20:24.038Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[main 2026-10-25T11:20:24.156Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false +Completed initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/A-restart/extensions +Loading development extension at /tmp/uap-vscode-sealed-3399636247/observer +Extension 'github.copilot-chat' appears in product.json but enables LESS API proposals than the extension wants. +package.json (LOSES): agentSessionsWorkspace +speech +agentsWindowConfiguration +chatDebug +chatHooks +envIsConnectionMetered +extensionsAny +newSymbolNamesProvider +interactive +codeActionAI +activeComment +commentReveal +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +documentFiltersExclusive +embeddings +findTextInFiles +findTextInFiles2 +languageModelToolSupportsModel +findFiles2 +textSearchProvider +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +chatParticipantAdditions +defaultChatParticipant +contribSourceControlInputBoxMenu +authLearnMore +authIssuers +testObserver +aiTextSearchProvider +chatParticipantPrivate +chatProvider +contribDebugCreateConfiguration +chatReferenceDiagnostic +textSearchProvider2 +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelPricing +languageModelRateLimit +inlineCompletionsAdditions +chatStatusItem +chatInputNotification +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +languageModelThinkingPart +chatSessionsProvider +devDeviceId +contribEditorContentMenu +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +environmentPower +terminalTitle +toolInvocationApproveCombination +chatSessionCustomizationProvider +product.json (WINS): agentSessionsWorkspace +agentsWindowConfiguration +interactive +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +chatParticipantAdditions +defaultChatParticipant +embeddings +chatProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +codeActionAI +findTextInFiles +findTextInFiles2 +textSearchProvider +textSearchProvider2 +activeComment +commentReveal +contribSourceControlInputBoxMenu +contribCommentEditorActionsMenu +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +extensionAffinity +newSymbolNamesProvider +findFiles2 +chatReferenceDiagnostic +extensionsAny +authLearnMore +testObserver +aiTextSearchProvider +documentFiltersExclusive +chatParticipantPrivate +contribDebugCreateConfiguration +inlineCompletionsAdditions +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelThinkingPart +languageModelToolSupportsModel +chatStatusItem +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +chatSessionsProvider +chatSessionCustomizationProvider +devDeviceId +contribEditorContentMenu +tokenInformation +toolInvocationApproveCombination +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +chatHooks +chatDebug +envIsConnectionMetered +environmentPower +terminalTitle +languageModelPricing +chatInputNotification +speech +DELTA: authIssuers +languageModelRateLimit +Proceeding with EXTRA proposals (authIssuers, languageModelRateLimit) because extension is in development mode. Still, this EXTENSION WILL BE BROKEN unless product.json is updated. +Failed to resolve MCP server 'uap-user' from gallery: Offline: Offline +Failed to resolve MCP server 'uap-a' from gallery: Offline: Offline +Offline: Offline: Offline + at sRt.queryRawGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:56135) + at async sRt.queryGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:51373) + at async sRt.getExtensionsUsingQueryApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:43611) + at async sRt.getExtensionsUsingResourceApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:44980) + at async sRt.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:42545) + at async ise.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:22515) + at async ise.onDidChangeRunningExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:19518) +Unknown channel: agentHostClientByokLm +Unknown channel: agentHostClientProxy +[main 2026-10-25T11:20:25.035Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Unknown channel: agentHostClientProxy +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[main 2026-10-25T11:20:25.139Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:20:25.140Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:20:25.375Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Request 2 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[RemoteAgentHost] Reconciling: desired=[], current=[] +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +Settings Sync: Account status changed from uninitialized to unavailable +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[main 2026-10-25T11:20:25.832Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Request 3 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/A-restart/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/A-restart/profile/User/settings.json.vsctmp'): Error: Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/A-restart/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/A-restart/profile/User/settings.json.vsctmp') + at owe.writeFile (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5419:92390) + at async vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1707:22639 +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:20:26.376Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Offline: Offline: Offline + at Xrr (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5425:54431) + at async bqt.logAndRequest (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:635:10540) + at async kmt.doFetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7655) + at async kmt.fetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7563) +[main 2026-10-25T11:20:26.832Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:20:26.832Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ODk4ZjAyYzYtMTYzYi00YjQ0LTkzYjMtZDEzY2JkNTFlYmI3" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-restart.observation.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-restart.observation.json new file mode 100644 index 000000000..2a4d2b8d2 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A-restart.observation.json @@ -0,0 +1 @@ +{"schema_version":1,"collected":true,"version":"1.143.1","app_root":"/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app","app_host":"desktop","observer":{"id":"uap-research.workspace-observer","mode":3,"proposals":["mcpServerDefinitions","chatPromptFiles"]},"folder":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A","authority":""},"trusted":false,"policy":{"skills_enabled":true,"mcp_access":"all","mcp_autostart":"never"},"mcp":[{"label":"uap-a","version":"88bdd8c76d4f82a3e611b33ae0c0b9171de6d939c54730f93ccd46aba7693f8b","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-A","scheme":"http","path":"/UAP-A","authority":"127.0.0.1:9"},"headers":{}},{"label":"uap-user","version":"dca08194154f5a28c2d70ef3da3dacc7adb610117a25421177e49d4b110b106d","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-USER","scheme":"http","path":"/UAP-USER","authority":"127.0.0.1:9"},"headers":{}}],"skills":[{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-a/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-a/SKILL.md","authority":""},"name":"uap-a","source":"local","disableModelInvocation":true,"description":"A","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-shared/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-shared/SKILL.md","authority":""},"name":"uap-shared","source":"local","disableModelInvocation":true,"description":"A-SHARED","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","authority":""},"name":"uap-user","source":"user","disableModelInvocation":true,"description":"USER","userInvocable":true}],"native_project_qualified":false,"native_mcp_initialization_qualified":false,"native_runtime_qualified":false} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A.merged.log new file mode 100644 index 000000000..e374ed44f --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A.merged.log @@ -0,0 +1,510 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5213,"network_namespace":4026532313,"parent_network_namespace":4026531840,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} +Unable to revert mtime: /usr/share/fonts +Unable to revert mtime: /usr/share/fonts/truetype +Unable to revert mtime: /usr/share/fonts/truetype/dejavu +Unable to revert mtime: /usr/share/fonts +Unable to revert mtime: /usr/share/fonts/truetype +Unable to revert mtime: /usr/share/fonts/truetype/dejavu +Unable to revert mtime: /usr/share/fonts/truetype/lato +Unable to create argv.json configuration file in /tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json, falling back to defaults (Error: EACCES: permission denied, mkdir '/tmp/uap-vscode-sealed-3399636247/home/.vscode') +Unable to revert mtime: /usr/share/fonts/truetype/liberation +Unable to revert mtime: /usr/share/fonts/truetype/noto +[5213:1025/111942.839017:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5213:1025/111942.852238:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[5213:1025/111942.853809:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5213:1025/111942.854879:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[main 2026-10-25T11:19:42.948Z] [node-compile-cache] main: packaged cache inactive (status: disabled, configured: /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node-compile-cache/main, active: none) +[main 2026-10-25T11:19:42.985Z] Error: Unable to retrieve mac address (unexpected format) + at F_ (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:4889) + at G5 (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5213) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5132 + at async ZD (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100630) + at async nM (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100975) + at async Promise.all (index 0) + at async Xs.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:13169) + at async fP.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:192:55975) +[main 2026-10-25T11:19:42.994Z] StorageMainService: creating application shared storage +[5213:1025/111943.110894:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[main 2026-10-25T11:19:43.111Z] [shared storage] Creating shared storage database at ':memory:' (wasCreated: true) +[main 2026-10-25T11:19:43.111Z] [shared storage] Initializing fallback application storage (path: in-memory) +[main 2026-10-25T11:19:43.190Z] gn [Error]: Unable to read file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json' (Error: Unable to resolve nonexistent file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json') + at ea.restoreReadError (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10637) + at ea.doReadFileStream (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10420) + at async ea.doReadFile (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:9670) + at async Xs.updateCrashReporterEnablement (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:30775) { + fileOperationResult: 1, + options: { preferUnbuffered: true } +} +[main 2026-10-25T11:19:43.210Z] [shared storage] Fallback application storage initialized with 3 items +Via 'product.json#extensionEnabledApiProposals' extension 'github.vscode-pull-request-github' wants API proposal 'agentsWindowActivation' but that proposal DOES NOT EXIST. Likely, the proposal has been finalized (check 'vscode.d.ts') or was abandoned. +[main 2026-10-25T11:19:44.513Z] update#disable - updates are disabled by user preference +[main 2026-10-25T11:19:44.513Z] update#setState disabled +[RemoteAgentHost] Reconciling: desired=[], current=[] +[5213:1025/111944.524910:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[AgentHost:renderer] Acquiring MessagePort to agent host... +[main 2026-10-25T11:19:44.551Z] AgentHostProcessManager: agent host started +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey=undefined conversationKey=undefined modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[LM] Failed to request chat control data Offline +[AgentHost:renderer] MessagePort acquired, creating client... +Started local extension host with pid 5372. +Started initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/A/extensions +[main 2026-10-25T11:19:45.350Z] [AgentHost:stderr] (node:5314) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities. +(Use `exe --trace-deprecation ...` to show where the warning was created) + +[main 2026-10-25T11:19:45.416Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Sending initialize: address=local clientId=84346e91-2e10-4f49-8b28-3272b776acc7 requestId=1 +[RemoteAgentHostProtocol] Initialized: address=local clientId=84346e91-2e10-4f49-8b28-3272b776acc7 durationMs=926 +[AgentHost:renderer] Protocol connection established; clientId=84346e91-2e10-4f49-8b28-3272b776acc7 +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false +[main 2026-10-25T11:19:45.570Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +Failed to resolve MCP server 'uap-a' from gallery: Offline: Offline +Failed to resolve MCP server 'uap-a' from gallery: Offline: Offline +Failed to resolve MCP server 'uap-user' from gallery: Offline: Offline +Completed initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/A/extensions +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[main 2026-10-25T11:19:45.867Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[RemoteAgentHostProtocol] Request 2 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[RemoteAgentHost] Reconciling: desired=[], current=[] +Loading development extension at /tmp/uap-vscode-sealed-3399636247/observer +Extension 'github.copilot-chat' appears in product.json but enables LESS API proposals than the extension wants. +package.json (LOSES): agentSessionsWorkspace +speech +agentsWindowConfiguration +chatDebug +chatHooks +envIsConnectionMetered +extensionsAny +newSymbolNamesProvider +interactive +codeActionAI +activeComment +commentReveal +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +documentFiltersExclusive +embeddings +findTextInFiles +findTextInFiles2 +languageModelToolSupportsModel +findFiles2 +textSearchProvider +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +chatParticipantAdditions +defaultChatParticipant +contribSourceControlInputBoxMenu +authLearnMore +authIssuers +testObserver +aiTextSearchProvider +chatParticipantPrivate +chatProvider +contribDebugCreateConfiguration +chatReferenceDiagnostic +textSearchProvider2 +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelPricing +languageModelRateLimit +inlineCompletionsAdditions +chatStatusItem +chatInputNotification +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +languageModelThinkingPart +chatSessionsProvider +devDeviceId +contribEditorContentMenu +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +environmentPower +terminalTitle +toolInvocationApproveCombination +chatSessionCustomizationProvider +product.json (WINS): agentSessionsWorkspace +agentsWindowConfiguration +interactive +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +chatParticipantAdditions +defaultChatParticipant +embeddings +chatProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +codeActionAI +findTextInFiles +findTextInFiles2 +textSearchProvider +textSearchProvider2 +activeComment +commentReveal +contribSourceControlInputBoxMenu +contribCommentEditorActionsMenu +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +extensionAffinity +newSymbolNamesProvider +findFiles2 +chatReferenceDiagnostic +extensionsAny +authLearnMore +testObserver +aiTextSearchProvider +documentFiltersExclusive +chatParticipantPrivate +contribDebugCreateConfiguration +inlineCompletionsAdditions +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelThinkingPart +languageModelToolSupportsModel +chatStatusItem +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +chatSessionsProvider +chatSessionCustomizationProvider +devDeviceId +contribEditorContentMenu +tokenInformation +toolInvocationApproveCombination +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +chatHooks +chatDebug +envIsConnectionMetered +environmentPower +terminalTitle +languageModelPricing +chatInputNotification +speech +DELTA: authIssuers +languageModelRateLimit +Proceeding with EXTRA proposals (authIssuers, languageModelRateLimit) because extension is in development mode. Still, this EXTENSION WILL BE BROKEN unless product.json is updated. +Unknown channel: agentHostClientByokLm +Unknown channel: agentHostClientProxy +Offline: Offline: Offline + at sRt.queryRawGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:56135) + at async sRt.queryGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:51373) + at async sRt.getExtensionsUsingQueryApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:43611) + at async sRt.getExtensionsUsingResourceApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:44980) + at async sRt.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:42545) + at async ise.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:22515) + at async ise.onDidChangeRunningExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:19518) +[main 2026-10-25T11:19:46.385Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[main 2026-10-25T11:19:46.412Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Request 3 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +Unknown channel: agentHostClientProxy +[main 2026-10-25T11:19:46.553Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:19:46.553Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[CloudSandboxApi] No 'github' session with scopes [read:user, user:email, repo, workflow] +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:19:46.868Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +Settings Sync: Account status changed from uninitialized to unavailable +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:19:47.387Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + +[main 2026-10-25T11:19:47.387Z] [AgentHost:stderr] stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/A/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/A/profile/User/settings.json.vsctmp'): Error: Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/A/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/A/profile/User/settings.json.vsctmp') + at owe.writeFile (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5419:92390) + at async vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1707:22639 +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +Offline: Offline: Offline + at Xrr (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5425:54431) + at async bqt.logAndRequest (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:635:10540) + at async kmt.doFetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7655) + at async kmt.fetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7563) +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/ZjJmOWJkYTUtYjk2Ny00YTY0LTk2ZTQtM2NlYjY3N2EwZjkw" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A.observation.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A.observation.json new file mode 100644 index 000000000..2a4d2b8d2 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/A.observation.json @@ -0,0 +1 @@ +{"schema_version":1,"collected":true,"version":"1.143.1","app_root":"/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app","app_host":"desktop","observer":{"id":"uap-research.workspace-observer","mode":3,"proposals":["mcpServerDefinitions","chatPromptFiles"]},"folder":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A","authority":""},"trusted":false,"policy":{"skills_enabled":true,"mcp_access":"all","mcp_autostart":"never"},"mcp":[{"label":"uap-a","version":"88bdd8c76d4f82a3e611b33ae0c0b9171de6d939c54730f93ccd46aba7693f8b","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-A","scheme":"http","path":"/UAP-A","authority":"127.0.0.1:9"},"headers":{}},{"label":"uap-user","version":"dca08194154f5a28c2d70ef3da3dacc7adb610117a25421177e49d4b110b106d","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-USER","scheme":"http","path":"/UAP-USER","authority":"127.0.0.1:9"},"headers":{}}],"skills":[{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-a/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-a/SKILL.md","authority":""},"name":"uap-a","source":"local","disableModelInvocation":true,"description":"A","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-shared/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/A/.github/skills/uap-shared/SKILL.md","authority":""},"name":"uap-shared","source":"local","disableModelInvocation":true,"description":"A-SHARED","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","authority":""},"name":"uap-user","source":"user","disableModelInvocation":true,"description":"USER","userInvocable":true}],"native_project_qualified":false,"native_mcp_initialization_qualified":false,"native_runtime_qualified":false} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/B.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/B.merged.log new file mode 100644 index 000000000..c7e719f15 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/B.merged.log @@ -0,0 +1,496 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5444,"network_namespace":4026532313,"parent_network_namespace":4026531840,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} +Unable to revert mtime: /usr/share/fonts/truetype/lato +Unable to revert mtime: /usr/share/fonts/truetype/liberation +Unable to revert mtime: /usr/share/fonts/truetype/noto +Unable to create argv.json configuration file in /tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json, falling back to defaults (Error: EACCES: permission denied, mkdir '/tmp/uap-vscode-sealed-3399636247/home/.vscode') +[5444:1025/112002.287165:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5444:1025/112002.297649:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[5444:1025/112002.297729:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[5444:1025/112002.300909:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[main 2026-10-25T11:20:02.407Z] [node-compile-cache] main: packaged cache inactive (status: disabled, configured: /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node-compile-cache/main, active: none) +[main 2026-10-25T11:20:02.440Z] Error: Unable to retrieve mac address (unexpected format) + at F_ (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:4889) + at G5 (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5213) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:58:5132 + at async ZD (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100630) + at async nM (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:103:100975) + at async Promise.all (index 0) + at async Xs.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:13169) + at async fP.startup (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:192:55975) +[main 2026-10-25T11:20:02.448Z] StorageMainService: creating application shared storage +[5444:1025/112002.533034:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[main 2026-10-25T11:20:02.533Z] [shared storage] Creating shared storage database at ':memory:' (wasCreated: true) +[main 2026-10-25T11:20:02.533Z] [shared storage] Initializing fallback application storage (path: in-memory) +[main 2026-10-25T11:20:02.546Z] gn [Error]: Unable to read file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json' (Error: Unable to resolve nonexistent file '/tmp/uap-vscode-sealed-3399636247/home/.vscode/argv.json') + at ea.restoreReadError (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10637) + at ea.doReadFileStream (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:10420) + at async ea.doReadFile (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:183:9670) + at async Xs.updateCrashReporterEnablement (file:///tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/mainImpl.js:161:30775) { + fileOperationResult: 1, + options: { preferUnbuffered: true } +} +[5511:1025/112002.549040:ERROR:gpu/ipc/client/command_buffer_proxy_impl.cc:285] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +[main 2026-10-25T11:20:02.568Z] [shared storage] Fallback application storage initialized with 3 items +Via 'product.json#extensionEnabledApiProposals' extension 'github.vscode-pull-request-github' wants API proposal 'agentsWindowActivation' but that proposal DOES NOT EXIST. Likely, the proposal has been finalized (check 'vscode.d.ts') or was abandoned. +[main 2026-10-25T11:20:03.862Z] update#disable - updates are disabled by user preference +[main 2026-10-25T11:20:03.862Z] update#setState disabled +[RemoteAgentHost] Reconciling: desired=[], current=[] +[5444:1025/112003.873231:ERROR:dbus/bus.cc:406] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[AgentHost:renderer] Acquiring MessagePort to agent host... +[main 2026-10-25T11:20:03.902Z] AgentHostProcessManager: agent host started +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey=undefined conversationKey=undefined modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[LM] Failed to request chat control data Offline +[AgentHost:renderer] MessagePort acquired, creating client... +Started local extension host with pid 5597. +Started initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/B/extensions +[main 2026-10-25T11:20:04.702Z] [AgentHost:stderr] (node:5537) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities. +(Use `exe --trace-deprecation ...` to show where the warning was created) + +[main 2026-10-25T11:20:04.736Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Sending initialize: address=local clientId=6aa20de3-79c7-4ee6-b863-91e3b4e2acca requestId=1 +[RemoteAgentHostProtocol] Initialized: address=local clientId=6aa20de3-79c7-4ee6-b863-91e3b4e2acca durationMs=883 +[AgentHost:renderer] Protocol connection established; clientId=6aa20de3-79c7-4ee6-b863-91e3b4e2acca +[main 2026-10-25T11:20:04.878Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Completed initializing default profile extensions in extensions installation folder. file:///tmp/uap-vscode-sealed-3399636247/probes/B/extensions +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false +Failed to resolve MCP server 'uap-b' from gallery: Offline: Offline +Loading development extension at /tmp/uap-vscode-sealed-3399636247/observer +Extension 'github.copilot-chat' appears in product.json but enables LESS API proposals than the extension wants. +package.json (LOSES): agentSessionsWorkspace +speech +agentsWindowConfiguration +chatDebug +chatHooks +envIsConnectionMetered +extensionsAny +newSymbolNamesProvider +interactive +codeActionAI +activeComment +commentReveal +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +documentFiltersExclusive +embeddings +findTextInFiles +findTextInFiles2 +languageModelToolSupportsModel +findFiles2 +textSearchProvider +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +chatParticipantAdditions +defaultChatParticipant +contribSourceControlInputBoxMenu +authLearnMore +authIssuers +testObserver +aiTextSearchProvider +chatParticipantPrivate +chatProvider +contribDebugCreateConfiguration +chatReferenceDiagnostic +textSearchProvider2 +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelPricing +languageModelRateLimit +inlineCompletionsAdditions +chatStatusItem +chatInputNotification +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +languageModelThinkingPart +chatSessionsProvider +devDeviceId +contribEditorContentMenu +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +environmentPower +terminalTitle +toolInvocationApproveCombination +chatSessionCustomizationProvider +product.json (WINS): agentSessionsWorkspace +agentsWindowConfiguration +interactive +terminalDataWriteEvent +terminalExecuteCommandEvent +terminalSelection +terminalQuickFixProvider +chatParticipantAdditions +defaultChatParticipant +embeddings +chatProvider +mappedEditsProvider +aiRelatedInformation +aiSettingsSearch +codeActionAI +findTextInFiles +findTextInFiles2 +textSearchProvider +textSearchProvider2 +activeComment +commentReveal +contribSourceControlInputBoxMenu +contribCommentEditorActionsMenu +contribCommentThreadAdditionalMenu +contribCommentsViewThreadMenus +contribChatEditorInlineGutterMenu +extensionAffinity +newSymbolNamesProvider +findFiles2 +chatReferenceDiagnostic +extensionsAny +authLearnMore +testObserver +aiTextSearchProvider +documentFiltersExclusive +chatParticipantPrivate +contribDebugCreateConfiguration +inlineCompletionsAdditions +chatReferenceBinaryData +languageModelSystem +languageModelCapabilities +languageModelThinkingPart +languageModelToolSupportsModel +chatStatusItem +taskProblemMatcherStatus +contribLanguageModelToolSets +textDocumentChangeReason +resolvers +taskExecutionTerminal +dataChannels +chatSessionsProvider +chatSessionCustomizationProvider +devDeviceId +contribEditorContentMenu +tokenInformation +toolInvocationApproveCombination +chatPromptFiles +mcpServerDefinitions +tabInputMultiDiff +workspaceTrust +chatHooks +chatDebug +envIsConnectionMetered +environmentPower +terminalTitle +languageModelPricing +chatInputNotification +speech +DELTA: authIssuers +languageModelRateLimit +Proceeding with EXTRA proposals (authIssuers, languageModelRateLimit) because extension is in development mode. Still, this EXTENSION WILL BE BROKEN unless product.json is updated. +Failed to resolve MCP server 'uap-user' from gallery: Offline: Offline +Failed to resolve MCP server 'uap-b' from gallery: Offline: Offline +Offline: Offline: Offline + at sRt.queryRawGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:56135) + at async sRt.queryGalleryExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:51373) + at async sRt.getExtensionsUsingQueryApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:43611) + at async sRt.getExtensionsUsingResourceApi (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:44980) + at async sRt.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1746:42545) + at async ise.getExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:22515) + at async ise.onDidChangeRunningExtensions (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:3815:19518) +Unknown channel: agentHostClientByokLm +Unknown channel: agentHostClientProxy +[main 2026-10-25T11:20:05.731Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Unknown channel: agentHostClientProxy +[main 2026-10-25T11:20:05.871Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + +[main 2026-10-25T11:20:05.871Z] [AgentHost:stderr] stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[main 2026-10-25T11:20:06.016Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[RemoteAgentHostProtocol] Request 2 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[RemoteAgentHost] Reconciling: desired=[], current=[] +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +Settings Sync: Account status changed from uninitialized to unavailable +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[AgentHost] No signed-in session resolved for resource: https://api.github.com +[main 2026-10-25T11:20:06.522Z] [AgentHost:stderr] Unhandled Promise Rejection: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[RemoteAgentHostProtocol] Request 3 failed: { + code: -32603, + message: 'Error: Cannot call write after a stream was destroyed\n' + + ' at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/connection.js:1089:44\n' + + ' at process.processTicksAndRejections (node:internal/process/task_queues:104:5)' +} +[AgentHostSessionList] Failed to refresh sessions { code: -32603 } +[AgentHost] No signed-in session resolved for resource: https://api.github.com/repos +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[main 2026-10-25T11:20:07.017Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 +Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/B/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/B/profile/User/settings.json.vsctmp'): Error: Unable to write file 'vscode-userdata:/tmp/uap-vscode-sealed-3399636247/probes/B/profile/User/settings.json' (NoPermissions (FileSystemError): Error: EACCES: permission denied, open '/tmp/uap-vscode-sealed-3399636247/probes/B/profile/User/settings.json.vsctmp') + at owe.writeFile (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5419:92390) + at async vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:1707:22639 +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +Offline: Offline: Offline + at Xrr (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5425:54431) + at async bqt.logAndRequest (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:635:10540) + at async kmt.doFetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7655) + at async kmt.fetchAlerts (vscode-file://vscode-app/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/out/vs/workbench/workbench.desktop.main.js:5918:7563) +[main 2026-10-25T11:20:07.523Z] [AgentHost:stderr] rejected promise not handled within 1 second: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed +stack trace: Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 + +[main 2026-10-25T11:20:07.523Z] [AgentHost:stderr] Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed + at _write (node:internal/streams/writable:493:11) + at Writable.write (node:internal/streams/writable:512:10) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:88:29 + at new Promise () + at WritableStreamWrapper.write (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/node/ril.js:78:16) + at TeardownResilientStreamMessageWriter.doWrite (/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:99:33) + at /tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app/node_modules.asar/vscode-jsonrpc/lib/common/messageWriter.js:90:29 { + code: 'ERR_STREAM_DESTROYED' +} + +[ChatModelSelection] event=no-model-at-toolbar-build surface="workbench" sessionKey="local" conversationKey="vscode-chat-session://local/NzdkMjkyZWEtNmQzZi00ZTIzLThmYzktMDUxNmJkZGFlOGEx" modelTarget=undefined storageKey="chat.currentLanguageModel.panel" widgetViewKind="view" +[AccountPolicyGate] apply: state=inactive, reason=undefined, isRestricted=false diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/B.observation.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/B.observation.json new file mode 100644 index 000000000..d4f2dc81e --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/B.observation.json @@ -0,0 +1 @@ +{"schema_version":1,"collected":true,"version":"1.143.1","app_root":"/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/resources/app","app_host":"desktop","observer":{"id":"uap-research.workspace-observer","mode":3,"proposals":["mcpServerDefinitions","chatPromptFiles"]},"folder":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/B","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/B","authority":""},"trusted":false,"policy":{"skills_enabled":true,"mcp_access":"all","mcp_autostart":"never"},"mcp":[{"label":"uap-b","version":"f2016e30b9139c847fb9ef53199870e40a498074d95e29877bf674771775eac9","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-B","scheme":"http","path":"/UAP-B","authority":"127.0.0.1:9"},"headers":{}},{"label":"uap-user","version":"dca08194154f5a28c2d70ef3da3dacc7adb610117a25421177e49d4b110b106d","kind":"http","uri":{"value":"http://127.0.0.1:9/UAP-USER","scheme":"http","path":"/UAP-USER","authority":"127.0.0.1:9"},"headers":{}}],"skills":[{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/B/.github/skills/uap-b/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/B/.github/skills/uap-b/SKILL.md","authority":""},"name":"uap-b","source":"local","disableModelInvocation":true,"description":"B","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/projects/B/.github/skills/uap-shared/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/projects/B/.github/skills/uap-shared/SKILL.md","authority":""},"name":"uap-shared","source":"local","disableModelInvocation":true,"description":"B-SHARED","userInvocable":true},{"uri":{"value":"file:///tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","scheme":"file","path":"/tmp/uap-vscode-sealed-3399636247/home/.copilot/skills/uap-user/SKILL.md","authority":""},"name":"uap-user","source":"user","disableModelInvocation":true,"description":"USER","userInvocable":true}],"native_project_qualified":false,"native_mcp_initialization_qualified":false,"native_runtime_qualified":false} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/README.md b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/README.md new file mode 100644 index 000000000..4f7e42028 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/README.md @@ -0,0 +1,85 @@ +# VS Code 1.143.1: untrusted workspace metadata observation + +Genuine hosted collection succeeded on 2026-10-25 at source +`f48d685133105e3ec7922acf811ee30a95830ef8`, tree +`11593c9f96d45f17fc671b53f90299927160fa33`, in +[run 40188310034](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034), +[job 121153707553](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034/job/121153707553). +The exact job's official setup log reports Ubuntu 22.04.5, image +`ubuntu-22.04`, version `20261012.322.1`; this is observed image identity, +not an inference from the workflow label or the latest image inventory. + +Provider artifact `vscode-1.143.1-untrusted-workspace-linux-amd64`, ID +`12597047500`, is 22,575 bytes, SHA256 +`4621b804126db48b64930d66a308f2906c047acaeb0cf3fffb58bfd442407b07`. +The sole outer member is `research.zip`. All 27 raw inner text members are +preserved byte-for-byte here. `provenance.json` records both ZIP identities, +exact source/image identities and every member length/hash. Binary ZIPs are +not committed. No vendor code was executed during artifact intake. + +## Verified collection boundary + +The worker and ROOT independently verified the provider digest, exact bounded +flat unique member closure, all 14 separate command streams and five merged +native diagnostic streams, source/publisher agreement and the four observation +results. Both frontend and root cleanup completed. All five native child +records report planned stop, proven tree reaping, no overflow and no error. + +Each same-process prelude emitted UID/EUID/GID/EGID 1001, empty groups, zero +capability sets, no-new-privileges, a network namespace distinct from its recorded +parent and only loopback. The unchanged publisher invocation contains no +sandbox-disabling or trust-disabling flags. The exact publisher remains +1.143.1 / `e646868227c3e3a9e3f24277c8c7220be42c0be7`; archive and required +ELF/CLI/product digests match the pinned records. This successful observation +does not retrospectively prove the exact cause of the earlier Ubuntu 24.04 +sandbox failure, whose original negative evidence remains preserved separately. + +The observer ran in test extension mode 3 with exactly the two reviewed proposed +APIs, `mcpServerDefinitions` and `chatPromptFiles`. This is a bounded research +extension on the shipped workbench, not a stable public CLI inventory API or a +production dependency. Every probe selected exactly one local folder and +reported `trusted:false`, `skills_enabled:true`, MCP access `all`, and restrictive +MCP autostart `never`. No trust was granted or policy weakened. + +## Actual metadata and its attribution + +The base directory below is the exact sealed task root in the raw files. +Every raw native row is retained; names were not collapsed before attribution. + +| Probe | Parsed skills | Configured MCP metadata | +| --- | --- | --- | +| A | Exact A `.github/skills/uap-a/SKILL.md`, A's same-name `uap-shared` with `A-SHARED`, and personal `home/.copilot/skills/uap-user/SKILL.md`. Sources are respectively `local`, `local`, `user`. | `uap-a` at `http://127.0.0.1:9/UAP-A` and the distinct user URL; no B marker. | +| B | Exact B `.github/skills/uap-b/SKILL.md`, B's `uap-shared` with `B-SHARED`, and the same personal user marker. | B marker plus the user marker; no A marker. | +| A restart | Observation JSON is byte-identical to A, including actual folder, trust, policy, skill paths and MCP definitions. | A and user markers return in a fresh workbench process. | +| A after declared fixture removal | Only personal `uap-user` and personal `uap-shared` with `USER-SHARED`; A's removed skill paths are absent. | Only the user marker remains; A's removed registration is absent. | + +Skill attribution uses actual API-emitted file URIs and source values. The MCP +API emits definitions and marker URLs but no configuration-file origin, so its +source attribution is the explicit inference from unique sealed fixture URLs. +Do not relabel it as an emitted `.vscode/mcp.json` path. The same-name project +skill wins in the A/B table and personal fallback appears after removal; this +does not establish independent simultaneously active same-name versions. + +The controller authored these inert fixtures and removed only its declared A +fixture files. No UAP project adapter, ownership receipt, package transaction or +native MCP handshake was exercised by this research collector. Actual configured +HTTP metadata supports the `.vscode/mcp.json` component path/schema gate; SSE +spelling has its separate immutable primary-source schema evidence, not a tested +SSE connection. All skills disable model invocation. + +## Interpretation and next implementation gate + +ROOT independently gave GO for the reviewed component paths and the conditional +Prepared adapter plan after inspecting the original ZIP, all 19 stream receipts, +five isolation/teardown records, four actual metadata results and both cleanup +records. This permits implementing the separate project skills/MCP backend and +testing its owned file lifecycle. It does not qualify native project activation. + +Every original `native_project_qualified`, `native_mcp_initialization_qualified` +and `native_runtime_qualified` field remains false. Connection attempts were not +instrumented. MCP startup/operation, model or skill execution, trust acceptance, +authentication, active sessions, remote/multi-root projects and effective Local +versus Copilot selection remain unqualified. The adapter must retain +Prepared + installed-file verification and require the user's normal client +trust/session decisions. Final assembled-source installer tests and review +remain necessary; later revisions are not implicitly covered by this observation. diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/Xvfb.merged.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/Xvfb.merged.log new file mode 100644 index 000000000..7d555004e --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/Xvfb.merged.log @@ -0,0 +1 @@ +UAP_VSCODE_ISOLATION_V1 {"pid":5211,"network_namespace":4026532250,"parent_network_namespace":4026531840,"uid":1001,"gid":1001,"euid":1001,"egid":1001,"groups":[],"interfaces":["lo"],"capabilities_zero":true,"no_new_privileges":true} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/cleanup.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/cleanup.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/controller.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/controller.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/controller.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/controller.stdout.log new file mode 100644 index 000000000..cabdc4d84 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/controller.stdout.log @@ -0,0 +1,5 @@ +{ + "cleanup_complete": true, + "collection_complete": true, + "error": "" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/go-version.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/go-version.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/go-version.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/go-version.stdout.log new file mode 100644 index 000000000..a3cd3c880 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/go-version.stdout.log @@ -0,0 +1 @@ +go version go1.26.10 linux/amd64 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/helper-build.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/helper-build.stderr.log new file mode 100644 index 000000000..bb677a9bd --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/helper-build.stderr.log @@ -0,0 +1 @@ +go: downloading golang.org/x/sys v0.44.0 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/helper-build.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/helper-build.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/provenance.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/provenance.json new file mode 100644 index 000000000..c60556933 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/provenance.json @@ -0,0 +1,129 @@ +{ + "run_url": "https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034", + "job_id": 121153707553, + "artifact_id": 12597047500, + "artifact_name": "vscode-1.143.1-untrusted-workspace-linux-amd64", + "outer_bytes": 22575, + "outer_sha256": "4621b804126db48b64930d66a308f2906c047acaeb0cf3fffb58bfd442407b07", + "inner_bytes": 190409, + "inner_sha256": "71bfef813d28b3106f498756d0a09d9ad7953471bd627434cd660bb68c84ed14", + "source_commit": "f48d685133105e3ec7922acf811ee30a95830ef8", + "source_tree": "11593c9f96d45f17fc671b53f90299927160fa33", + "runner_image": { + "os": "Ubuntu 22.04.5", + "label": "ubuntu-22.04", + "image_version": "20261012.322.1", + "source": "official setup log for exact job121153707553" + }, + "members": { + "A-after-removal.merged.log": { + "bytes": 40046, + "sha256": "f0caa906539e3bf8bcdb3617bea17f66a8a0a5e7a29857c8578ad9f395e49de6" + }, + "A-after-removal.observation.json": { + "bytes": 1545, + "sha256": "701e5b96c0103c73c6ec5958bce763ffcaea267659b313d6168f85891ebb40e2" + }, + "A-restart.merged.log": { + "bytes": 40083, + "sha256": "a6cbdaf98ef60dfe02d0785eb8054b4c869760ecfa3dc79fff02fd8ea3118184" + }, + "A-restart.observation.json": { + "bytes": 2094, + "sha256": "0f9ce1980b2ccc1ca4441ae573c3783c34d59e1ed96aaedaa3226fcaed48e486" + }, + "A.merged.log": { + "bytes": 40157, + "sha256": "cc332ca5291b81798e8268b7e7b0d27163f4fb861964941706c9b1447d680874" + }, + "A.observation.json": { + "bytes": 2094, + "sha256": "0f9ce1980b2ccc1ca4441ae573c3783c34d59e1ed96aaedaa3226fcaed48e486" + }, + "B.merged.log": { + "bytes": 39276, + "sha256": "cad5061dad944b7c7131d79cdaa5c74b968d19db95591dd36ea5b989f5b3a252" + }, + "B.observation.json": { + "bytes": 2094, + "sha256": "f7f8e6a57896dde5827e82e9ddb9042caf2e1a7fa0e64401a1a83536943462dd" + }, + "Xvfb.merged.log": { + "bytes": 234, + "sha256": "4b57b7e0057675bc004f922ca23c5c382e6dbdf3be9595f410bb754862ab53a3" + }, + "cleanup.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cleanup.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "controller.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "controller.stdout.log": { + "bytes": 77, + "sha256": "9aaefc2134a5aaccd766fdbb75f8690691999e93460cad68a1bb20cf5011a9ad" + }, + "go-version.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "go-version.stdout.log": { + "bytes": 33, + "sha256": "4e0b213cba780f63b02f3ab9001ee61e4101d254e0af1a8205574c19dad0f93b" + }, + "helper-build.stderr.log": { + "bytes": 41, + "sha256": "b683257fca25cabaa9ba8f1ca2a8006ae11a5853fa2424b28a8f8f38d274d569" + }, + "helper-build.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "root-cleanup.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "root-cleanup.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "source-checkout.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "source-checkout.stdout.log": { + "bytes": 41, + "sha256": "2b6adf439ffe5900a952eae6fd446826ab16c07bbf1f534cb5bbb9451f1671d8" + }, + "system-executables.json": { + "bytes": 364, + "sha256": "3899f3683c1fa4be587584f6c6afaaad428967c88af2d4422eb58cd6ddf553b4" + }, + "vscode-workspace-controller.json": { + "bytes": 10500, + "sha256": "edf0204b1a3ad33998a07523859190cf2ea45a4748345fd0479c431e8f957a23" + }, + "vscode-workspace-research.json": { + "bytes": 8048, + "sha256": "f882c62132e442d43d5adf381e9cadea8701d73d5af49e13891d70693a683813" + }, + "xauth.json": { + "bytes": 382, + "sha256": "71a204f50eedd472f0fa5ec5f939e0d4d139909d5573782bd44721eabe4100c1" + }, + "xauth.stderr.log": { + "bytes": 86, + "sha256": "a67c95fb878a3b6b67e8bb4314214581f92fcb7ba526a728229486e32a7ed9d3" + }, + "xauth.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + "qualification": "untrusted workbench metadata paths and safe collection only; product adapter implementation gate, no runtime qualification" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/root-cleanup.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/root-cleanup.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/root-cleanup.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/root-cleanup.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/source-checkout.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/source-checkout.stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/source-checkout.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/source-checkout.stdout.log new file mode 100644 index 000000000..0d80f9437 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/source-checkout.stdout.log @@ -0,0 +1 @@ +f48d685133105e3ec7922acf811ee30a95830ef8 diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/system-executables.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/system-executables.json new file mode 100644 index 000000000..1189c6cc6 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/system-executables.json @@ -0,0 +1,12 @@ +{ + "/usr/bin/Xvfb": { + "bytes": 2073056, + "sha256": "cf71d78eab8321f7c568271053af449f966a75422140117ea1f244bf72a7e81e", + "version": "not inferred from binary identity" + }, + "/usr/bin/xauth": { + "bytes": 56280, + "sha256": "f914cf066df79d535d12721ac117c555ba43efe9a575804d8cc296f5ef23a44b", + "version": "not inferred from binary identity" + } +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/vscode-workspace-controller.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/vscode-workspace-controller.json new file mode 100644 index 000000000..7a38655f7 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/vscode-workspace-controller.json @@ -0,0 +1,294 @@ +{ + "children": [ + { + "label": "A", + "argv": [ + "/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/code", + "--new-window", + "--disable-crash-reporter", + "--user-data-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/A/profile", + "--extensions-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/A/extensions", + "--extensionDevelopmentPath", + "/tmp/uap-vscode-sealed-3399636247/observer", + "--extensionTestsPath", + "/tmp/uap-vscode-sealed-3399636247/observer/runner.cjs", + "--enable-proposed-api", + "uap-research.workspace-observer", + "/tmp/uap-vscode-sealed-3399636247/projects/A" + ], + "cwd": "/tmp/uap-vscode-sealed-3399636247/projects/A", + "isolation": { + "pid": 5213, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532313, + "parent_network_namespace": 4026531840, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "A.merged.log", + "merged_bytes": 40157, + "merged_sha256": "cc332ca5291b81798e8268b7e7b0d27163f4fb861964941706c9b1447d680874", + "merged_overflow": false, + "planned_stop": true, + "tree_reaped": true, + "error": "" + }, + { + "label": "B", + "argv": [ + "/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/code", + "--new-window", + "--disable-crash-reporter", + "--user-data-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/B/profile", + "--extensions-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/B/extensions", + "--extensionDevelopmentPath", + "/tmp/uap-vscode-sealed-3399636247/observer", + "--extensionTestsPath", + "/tmp/uap-vscode-sealed-3399636247/observer/runner.cjs", + "--enable-proposed-api", + "uap-research.workspace-observer", + "/tmp/uap-vscode-sealed-3399636247/projects/B" + ], + "cwd": "/tmp/uap-vscode-sealed-3399636247/projects/B", + "isolation": { + "pid": 5444, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532313, + "parent_network_namespace": 4026531840, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "B.merged.log", + "merged_bytes": 39276, + "merged_sha256": "cad5061dad944b7c7131d79cdaa5c74b968d19db95591dd36ea5b989f5b3a252", + "merged_overflow": false, + "planned_stop": true, + "tree_reaped": true, + "error": "" + }, + { + "label": "A-restart", + "argv": [ + "/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/code", + "--new-window", + "--disable-crash-reporter", + "--user-data-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/A-restart/profile", + "--extensions-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/A-restart/extensions", + "--extensionDevelopmentPath", + "/tmp/uap-vscode-sealed-3399636247/observer", + "--extensionTestsPath", + "/tmp/uap-vscode-sealed-3399636247/observer/runner.cjs", + "--enable-proposed-api", + "uap-research.workspace-observer", + "/tmp/uap-vscode-sealed-3399636247/projects/A" + ], + "cwd": "/tmp/uap-vscode-sealed-3399636247/projects/A", + "isolation": { + "pid": 5658, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532313, + "parent_network_namespace": 4026531840, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "A-restart.merged.log", + "merged_bytes": 40083, + "merged_sha256": "a6cbdaf98ef60dfe02d0785eb8054b4c869760ecfa3dc79fff02fd8ea3118184", + "merged_overflow": false, + "planned_stop": true, + "tree_reaped": true, + "error": "" + }, + { + "label": "A-after-removal", + "argv": [ + "/tmp/uap-vscode-sealed-3399636247/publisher/VSCode-linux-x64/code", + "--new-window", + "--disable-crash-reporter", + "--user-data-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/profile", + "--extensions-dir", + "/tmp/uap-vscode-sealed-3399636247/probes/A-after-removal/extensions", + "--extensionDevelopmentPath", + "/tmp/uap-vscode-sealed-3399636247/observer", + "--extensionTestsPath", + "/tmp/uap-vscode-sealed-3399636247/observer/runner.cjs", + "--enable-proposed-api", + "uap-research.workspace-observer", + "/tmp/uap-vscode-sealed-3399636247/projects/A" + ], + "cwd": "/tmp/uap-vscode-sealed-3399636247/projects/A", + "isolation": { + "pid": 5874, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532313, + "parent_network_namespace": 4026531840, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "A-after-removal.merged.log", + "merged_bytes": 40046, + "merged_sha256": "f0caa906539e3bf8bcdb3617bea17f66a8a0a5e7a29857c8578ad9f395e49de6", + "merged_overflow": false, + "planned_stop": true, + "tree_reaped": true, + "error": "" + }, + { + "label": "Xvfb", + "argv": [ + "/usr/bin/Xvfb", + ":93", + "-screen", + "0", + "1024x768x24", + "-nolisten", + "tcp", + "-auth", + "/tmp/uap-vscode-sealed-3399636247/auth/authority" + ], + "cwd": "/tmp/uap-vscode-sealed-3399636247", + "isolation": { + "pid": 5211, + "uid": 1001, + "gid": 1001, + "euid": 1001, + "egid": 1001, + "network_namespace": 4026532250, + "parent_network_namespace": 4026531840, + "groups": [], + "interfaces": [ + "lo" + ], + "capabilities_zero": true, + "no_new_privileges": true + }, + "diagnostics": "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail; task-cookie bytes redacted", + "merged_file": "Xvfb.merged.log", + "merged_bytes": 234, + "merged_sha256": "4b57b7e0057675bc004f922ca23c5c382e6dbdf3be9595f410bb754862ab53a3", + "merged_overflow": false, + "planned_stop": true, + "tree_reaped": true, + "error": "" + } + ], + "cleanup": { + "complete": true, + "error": "", + "stderr_bytes": 0, + "stderr_file": "root-cleanup.stderr.log", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stdout_bytes": 0, + "stdout_file": "root-cleanup.stdout.log", + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "collection_complete": true, + "diagnostics": "verifier-prefixed merged streams; planned teardown may truncate trailing diagnostics; API result is separate", + "error": "", + "evidence_kind": "vscode-pinned-untrusted-workspace-research", + "executable_hashes": { + "code": "cf8508cc7ddde30cfccc70caeda877a9a99119953c4476dcb30a3eb1a2c415e9", + "resources/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/linux-x64/rg": "193906679498de4d939345b937fa24e0e69a03c244bd70c859f5e41232713f21", + "resources/app/out/cli.js": "bd393ad6f0d28ba0aa7cd79cc685617f5187096f092009e315a4f098349f7976", + "resources/app/product.json": "a141a1de8ed44415c0c527b1882362e6abc25e674be052d67c09da71a27fbd7e" + }, + "native_mcp_initialization_qualified": false, + "native_project_qualified": false, + "native_runtime_qualified": false, + "observations": [ + { + "attribution": { + "connection_attempts": "not instrumented", + "mcp": "passed", + "mcp_source": "unique sealed marker URL inference; API provides no config-file origin", + "native_project_qualified": false, + "same_name": "all native rows retained; no simultaneous activation claim", + "skills": "passed" + }, + "error": "", + "metadata_collected": true, + "probe": "A" + }, + { + "attribution": { + "connection_attempts": "not instrumented", + "mcp": "passed", + "mcp_source": "unique sealed marker URL inference; API provides no config-file origin", + "native_project_qualified": false, + "same_name": "all native rows retained; no simultaneous activation claim", + "skills": "passed" + }, + "error": "", + "metadata_collected": true, + "probe": "B" + }, + { + "attribution": { + "connection_attempts": "not instrumented", + "mcp": "passed", + "mcp_source": "unique sealed marker URL inference; API provides no config-file origin", + "native_project_qualified": false, + "same_name": "all native rows retained; no simultaneous activation claim", + "skills": "passed" + }, + "error": "", + "metadata_collected": true, + "probe": "A-restart" + }, + { + "attribution": { + "connection_attempts": "not instrumented", + "mcp": "passed", + "mcp_source": "unique sealed marker URL inference; API provides no config-file origin", + "native_project_qualified": false, + "same_name": "all native rows retained; no simultaneous activation claim", + "skills": "passed" + }, + "error": "", + "metadata_collected": true, + "probe": "A-after-removal" + } + ], + "publisher_commit": "e646868227c3e3a9e3f24277c8c7220be42c0be7", + "source_commit": "f48d685133105e3ec7922acf811ee30a95830ef8", + "version": "1.143.1" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/vscode-workspace-research.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/vscode-workspace-research.json new file mode 100644 index 000000000..fe04c7285 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/vscode-workspace-research.json @@ -0,0 +1,158 @@ +{ + "evidence_kind": "vscode-untrusted-workspace-metadata-research", + "source_commit": "f48d685133105e3ec7922acf811ee30a95830ef8", + "version": "1.143.1", + "publisher_commit": "e646868227c3e3a9e3f24277c8c7220be42c0be7", + "collection_complete": true, + "native_project_qualified": false, + "native_mcp_initialization_qualified": false, + "native_runtime_qualified": false, + "checkout": { + "probe": "source-checkout", + "argv": [ + "/usr/bin/git", + "rev-parse", + "HEAD" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.003, + "stdout_file": "source-checkout.stdout.log", + "stderr_file": "source-checkout.stderr.log", + "stdout_bytes": 41, + "stderr_bytes": 0, + "stdout_sha256": "2b6adf439ffe5900a952eae6fd446826ab16c07bbf1f534cb5bbb9451f1671d8", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "archive": { + "url": "https://vscode.download.prss.microsoft.com/dbazure/download/stable/e646868227c3e3a9e3f24277c8c7220be42c0be7/code-stable-x64-1792691793.tar.gz", + "bytes": 350844741, + "sha256": "48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be" + }, + "materialization": { + "members": 4262, + "expanded_bytes": 1034475520, + "regular_bytes": 1031068984, + "omitted_symlinks": 0, + "omitted_symlink_metadata_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "publisher_files": { + "code": { + "sha256": "cf8508cc7ddde30cfccc70caeda877a9a99119953c4476dcb30a3eb1a2c415e9", + "bytes": 223685880, + "identity": [ + 2049, + 9096913 + ] + }, + "resources/app/out/cli.js": { + "sha256": "bd393ad6f0d28ba0aa7cd79cc685617f5187096f092009e315a4f098349f7976", + "bytes": 275169, + "identity": [ + 2049, + 9099737 + ] + }, + "resources/app/product.json": { + "sha256": "a141a1de8ed44415c0c527b1882362e6abc25e674be052d67c09da71a27fbd7e", + "bytes": 68251, + "identity": [ + 2049, + 9096932 + ] + } + }, + "helper": { + "version_probe": { + "probe": "go-version", + "argv": [ + "/opt/hostedtoolcache/go/1.26.10/x64/bin/go", + "version" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 0.006, + "stdout_file": "go-version.stdout.log", + "stderr_file": "go-version.stderr.log", + "stdout_bytes": 33, + "stderr_bytes": 0, + "stdout_sha256": "4e0b213cba780f63b02f3ab9001ee61e4101d254e0af1a8205574c19dad0f93b", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "build": { + "probe": "helper-build", + "argv": [ + "/opt/hostedtoolcache/go/1.26.10/x64/bin/go", + "build", + "-trimpath", + "-p=1", + "-ldflags", + "-X main.buildCommit=f48d685133105e3ec7922acf811ee30a95830ef8", + "-o", + "/tmp/vscode-workspace-input-fytis4qj/controller", + "./install/integrationctl/research/vscodeworkspace" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "exit": 0, + "elapsed_seconds": 12.521, + "stdout_file": "helper-build.stdout.log", + "stderr_file": "helper-build.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 41, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "b683257fca25cabaa9ba8f1ca2a8006ae11a5853fa2424b28a8f8f38d274d569" + }, + "bytes": 4996105, + "sha256": "30b2cba5e6c2a533416d5f311581ad477b682b3c977b6f41db09084c42748ac8" + }, + "controller": { + "argv": [ + "/usr/bin/sudo", + "-n", + "--preserve-env=GITHUB_ACTIONS,RUNNER_ENVIRONMENT,AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED,EXPECTED_COMMIT", + "/tmp/vscode-workspace-input-fytis4qj/controller", + "--input-root", + "/tmp/vscode-workspace-input-fytis4qj" + ], + "cwd": "/home/runner/work/universal-agent-plugins/universal-agent-plugins", + "error": null, + "controller_reaped": true, + "exit": 0, + "stdout_file": "controller.stdout.log", + "stdout_bytes": 77, + "stdout_sha256": "9aaefc2134a5aaccd766fdbb75f8690691999e93460cad68a1bb20cf5011a9ad", + "stderr_file": "controller.stderr.log", + "stderr_bytes": 0, + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "frontend_cleanup": { + "complete": true, + "probe": { + "probe": "cleanup", + "argv": [ + "/usr/bin/python3", + "-I", + "-c", + "\nimport os,stat,sys\npath,device,inode,owner=sys.argv[1:]\ndevice,inode,owner=int(device),int(inode),int(owner)\nparent,name=os.path.split(path)\nif parent!=\"/tmp\" or not name.startswith(\"vscode-workspace-input-\") or not name or owner<=0 or os.getuid()!=owner or os.geteuid()!=owner:\n raise SystemExit(\"frontend cleanup authority refused\")\ndef same(a,b):\n return (a.st_dev,a.st_ino,a.st_uid,stat.S_IFMT(a.st_mode))==(b.st_dev,b.st_ino,b.st_uid,stat.S_IFMT(b.st_mode))\ndef owned_directory(info):\n if not stat.S_ISDIR(info.st_mode) or info.st_uid!=owner or info.st_dev!=device:\n raise RuntimeError(\"frontend cleanup directory authority differs\")\nparent_fd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)\nroot=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent_fd)\nidentity=os.fstat(root)\nif (identity.st_dev,identity.st_ino,identity.st_uid)!=(device,inode,owner) or not same(identity,os.stat(name,dir_fd=parent_fd,follow_symlinks=False)):\n raise SystemExit(\"frontend cleanup root changed\")\ndef clear(fd,expected):\n current=os.fstat(fd)\n owned_directory(current)\n if not same(current,expected):raise RuntimeError(\"frontend cleanup opened directory changed\")\n os.fchmod(fd,(current.st_mode&0o777)|0o700)\n for entry in os.listdir(fd):\n item=os.stat(entry,dir_fd=fd,follow_symlinks=False)\n if item.st_uid!=owner or item.st_dev!=device:raise RuntimeError(\"frontend cleanup leaf authority differs\")\n if stat.S_ISDIR(item.st_mode):\n child=os.open(entry,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)\n try:\n opened=os.fstat(child)\n if not same(item,opened) or not same(opened,os.stat(entry,dir_fd=fd,follow_symlinks=False)):\n raise RuntimeError(\"frontend cleanup directory replaced\")\n clear(child,opened)\n if not same(opened,os.stat(entry,dir_fd=fd,follow_symlinks=False)):\n raise RuntimeError(\"frontend cleanup directory replaced before removal\")\n finally:os.close(child)\n os.rmdir(entry,dir_fd=fd)\n else:\n if not (stat.S_ISREG(item.st_mode) or stat.S_ISLNK(item.st_mode)):\n raise RuntimeError(\"frontend cleanup unsupported leaf\")\n if not same(item,os.stat(entry,dir_fd=fd,follow_symlinks=False)):\n raise RuntimeError(\"frontend cleanup leaf replaced\")\n os.unlink(entry,dir_fd=fd)\nclear(root,identity)\nif not same(identity,os.stat(name,dir_fd=parent_fd,follow_symlinks=False)):\n raise SystemExit(\"frontend cleanup root replaced before removal\")\nos.close(root)\nos.rmdir(name,dir_fd=parent_fd)\nos.close(parent_fd)\n", + "/tmp/vscode-workspace-input-fytis4qj", + "2049", + "9096901", + "1001" + ], + "cwd": "/", + "error": null, + "exit": 0, + "elapsed_seconds": 0.718, + "stdout_file": "cleanup.stdout.log", + "stderr_file": "cleanup.stderr.log", + "stdout_bytes": 0, + "stderr_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + "elapsed_seconds": 104.287642312 +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.json b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.json new file mode 100644 index 000000000..1e4ccd28b --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.json @@ -0,0 +1,15 @@ +{ + "argv": [ + "/usr/bin/xauth", + "-f", + "/tmp/uap-vscode-sealed-3399636247/auth/authority", + "source", + "-" + ], + "error": "", + "redacted": true, + "stderr_bytes": 86, + "stderr_sha256": "a67c95fb878a3b6b67e8bb4314214581f92fcb7ba526a728229486e32a7ed9d3", + "stdout_bytes": 0, + "stdout_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.stderr.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.stderr.log new file mode 100644 index 000000000..2626cb535 --- /dev/null +++ b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.stderr.log @@ -0,0 +1 @@ +/usr/bin/xauth: file /tmp/uap-vscode-sealed-3399636247/auth/authority does not exist diff --git a/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.stdout.log b/docs/evidence/project-scope-research/vscode-1.143.1-untrusted-workspace-linux-amd64-40188310034/xauth.stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/docs/plans/project-install-e2e/acquisition-fixture-failure-diagnostics.md b/docs/plans/project-install-e2e/acquisition-fixture-failure-diagnostics.md new file mode 100644 index 000000000..a98df75d0 --- /dev/null +++ b/docs/plans/project-install-e2e/acquisition-fixture-failure-diagnostics.md @@ -0,0 +1,55 @@ +# Preserve fixed diagnostics at the two observed acquisition fixture seams + +At source `1a19373e024ba4c2ac0748fda1306ce0ffd95429`, Required job +`121162735459` fails `TestSkillsBatchRejectsStaleAndUnresolvedBeforeEffects` +at `skills_batch_test.go:134` with the fixed selection preparation error. +Coverage job `121162735210` fails +`TestNativeHTTPSGitPublicAndPrivateCredentialHelper/false` at +`git_transport_test.go:131`: native ref resolution succeeds, then immutable +acquisition fails with its intentionally sanitized message. The nearby TLS +server errors do not establish this failure's cause. These are different +fixtures from the earlier instrumented dependency paths. + +## Exact change + +- In `newSavedJourney`, attach the existing `dependencyGitDiagnosticRunner` + around the existing `sourceacquisition.OSRunner` before `skillJourney` + copies its Acquirer. Keep its existing fixed command phase/class/status, + four-failure cap and exact context/command/result/error delegation. On a + failing test only, emit one fixed-label summary of delegated failure count. + Zero observed delegated failures remains distinct from a transport error; + neither case authorizes guessing the selection refusal's cause. +- Give each existing native HTTPS subtest a test-only Runner decorator. It + delegates its original context and complete Command to OSRunner exactly + once, returns the original bytes and error, and logs at most four failures. + The outer operation label is only `resolve` or `acquire`, assigned at the + existing call boundaries; this new decorator never reads command arguments. + Error classification reuses the established closed policy: context, + unsupported ordinary containment, stdout limit, forced cleanup, recognized + ordinary-supervisor stage, distinctive positive Git exit/status, or unknown. + No error text, streams, credentials, paths, environment or arbitrary types + are formatted or read for classification. Opaque native TLS policy errors + remain unknown. No production changes are included. +- Add inert contracts for exact delegation/context/body/error identity, the + four-failure cap, silent success, fixed operation/class/status, generic + process exit refusal and an Error-panic positive Git exit fixture. Preserve + every existing fixture assertion and transport/credential configuration. +- Revise the existing finite diagnostic workflow on its exact temporary + diagnostic branch. Keep the Required Linux selector, Go/tool pins, read-only + permissions and 15-minute job budget. Run exactly the newly failed Skills + test without coverage and exactly the native HTTPS test with the observed + atomic coverage mode/profile, each once with `-count=1 -timeout=5m`. + Do not repeat the two earlier passed diagnostic tests, add retries or use + a repeated full lane to obtain one label. Full Required/Coverage keep the + decorators for subsequent actual failures. + +## Validation and qualification + +Run only the inert privacy/delegation/classification contracts and compile +the affected real fixture entry points. Use configured affected-package lint +with CI's `origin/main` merge base and the unfiltered size/architecture gates. +Review the exact focused workflow command selection. Both independent plan +rounds and frozen source review precede code publication. One genuine hosted +diagnostic run may expose a fixed origin; a passing run does not establish +the historical cause. No timing, command, TLS, environment, containment, +credential, retry or success/failure semantics change. diff --git a/docs/plans/project-install-e2e/chatgpt-code-review-r1.md b/docs/plans/project-install-e2e/chatgpt-code-review-r1.md new file mode 100644 index 000000000..ad561ac70 --- /dev/null +++ b/docs/plans/project-install-e2e/chatgpt-code-review-r1.md @@ -0,0 +1,66 @@ +# ChatGPT project boundary independent implementation review R1 + +Reviewer: codex_claude, 2026-10-23. Reviewed frozen leaf +`74608d3b4340815d9603bed7030411b15818be2e` in +`work/uap-project-chatgpt`. Reviewer did not author this implementation. + +## Decision: GO + +No blocker found in the explicit unsupported project boundary. The leaf adds no +native project capability, filesystem installation layout, fake profile, +remote-registration attestation or project preparation artifact. It preserves +the existing user personal/manual preparation contract. + +## Production composition and no-effects assessment + +- Production scope selection uses the selected existing project and core's + namespace guard. Explicit ChatGPT and mixed target requests are refused by + the shared project capability gate before detector, acquisition, scanning or + lifecycle dispatch. No ProjectTargetLayout, NativeRegistryLayout or project + catalog declaration is added for ChatGPT. +- The composed fixtures rebuild the real production App through ForScope and + reapply only detector/acquirer/security spies. They exercise saved and + imperative add, no-save, personal app IDs, completion flags, positional + install, update, repair, remove, mixed targets and invocation-cwd default. + The error remains `chatgpt does not support project scope`; success output, + host-boundary calls and filesystem changes are rejected by assertions. +- The project dependency preflight reads and validates declared manifest + targets before the environment lock can create `.uap`. This is an early + refusal check only. The authoritative manifest is still reread and validated + after locking, recovery admission remains inside that lock, and existing + manifest-change checks still govern publication. User scope returns before + this new project-only preflight. +- Saved lock/update/sync/environment-install fixtures require no lock, + `.uap`, user-state namespace or acquired source. Existing manifest and + user/project-A/project-B byte-and-mode inventories are preserved. Adjacent + transfer validation also validates target eligibility before publication; + no alternative import/install route is added by this leaf. +- Direct ChatGPT activation/preflight rejects either project scope or a + nonempty project root, including inconsistent legacy labels and positive + attestation/personal preparation flags. Deactivation refuses the same + unsupported context even with external-uninstall attestation. The provider + invokes preflight before its generic attestation shortcut. +- Existing user activation still returns manual or personal-prepared outcomes + under its existing requirements. It does not become installed verification + or remotely observed authentication. Existing user external-uninstall + behavior is unchanged. + +## Independent verification + +Ran with Go 1.26.10, GOMAXPROCS=3, `-p=2 -count=1 -timeout=3m`: + +```sh +go test ./install/integrationctl/agentplugins/clients/chatgpt \ + ./cli/internal/agentpluginscli ./cli/cmd/agentplugins \ + -run '^Test(ProjectChatGPT|ChatGPT)' +``` + +Results: client PASS **0.009s**, internal CLI PASS **0.074s**, production CLI +PASS **0.522s**. `git diff --check` on the frozen leaf: PASS. Reviewed the +changed production functions and surrounding scope, saved-target, locked +manifest, sync and transfer validation paths. + +These are inert production-composition and refusal contracts. No ChatGPT +remote API, native client, MCP server, model, app registration or account/ +workspace mutation was executed. There is no native project proof to infer +from an unsupported leaf. Independent R2 and applicable CI remain separate. diff --git a/docs/plans/project-install-e2e/chatgpt-code-review-r2.md b/docs/plans/project-install-e2e/chatgpt-code-review-r2.md new file mode 100644 index 000000000..c37b52a4a --- /dev/null +++ b/docs/plans/project-install-e2e/chatgpt-code-review-r2.md @@ -0,0 +1,57 @@ +# ChatGPT project boundary independent implementation review R2 + +Reviewer: cursor_clients, 2026-10-23. Reviewed frozen checkpoint +`74608d3b4340815d9603bed7030411b15818be2e` in `work/uap-project-chatgpt`. +Read the approved ChatGPT client plan, repository governing instructions and +capability-preservation contract, implementation diff, production CLI scope and +saved dependency entrypoints, direct adapter lifecycle, and added fixtures. + +## Decision: GO for the explicit unsupported project boundary + +No correctness blocker found. This is approval of the approved negative leaf: +it does not qualify a ChatGPT project filesystem installation, remote app +connection or account/workspace operation. + +## Reviewed boundaries + +- ChatGPT does not advertise ScopeProject, ProjectTargetLayout or + NativeRegistryLayout. The implementation adds no fake project native root, + user-profile substitution or artifact-only bypass. Existing user mapping + authorization and manual/prepared lifecycle remain intact. +- Production CLI project target admission rejects explicit ChatGPT and mixed + target selections before detector, acquirer, security evaluator or mutation. + Imperative/saved add, positional install, personal-registration inputs, + activation/authentication attestation, exact-ID update/repair/remove and + invocation-directory root defaults retain the stable unsupported diagnostic. +- Saved lock/update now perform a pure manifest-target preflight before the + environment lock can create `.uap`. The operation still rereads and validates + the authoritative manifest under lock, checks pending recovery and compares + manifest bytes before publishing. The helper extraction preserves existing + installer-nil and recovery-required behavior. Sync/environment install have + the same target refusal, with project guidance taking precedence over the + user-only manual-preparation suggestion. +- The transfer preparation path also validates source agreement targets before + confirmation, lock/publication or install. Project scope does not gain an + unsupported import/install escape through that existing generic route. +- Direct activation preflight refuses either project scope or a ProjectRoot + even with inconsistent user labels, personal preparation or attestation. + Deactivation similarly refuses unsupported project completion despite an + external-uninstall assertion. The provider calls preflight before its generic + attestation shortcut, and direct adapter activation calls the same guard. +- Added E2E contracts assert zero host/acquisition/security calls, exact + user/A/B byte-and-mode preservation, and absence of mutable namespace, lock + and user-state creation. Existing intent is preserved. Public guidance keeps + unsupported project behavior and bounded user preparation distinct. + +## Independent verification + +With the existing Go 1.26.10 toolchain, GOMAXPROCS=3 and `-p=2 -count=1`: + +- Complete `clients/chatgpt` suite: PASS, 0.013s. +- Production composed CLI `./cmd/agentplugins -run '^TestProjectChatGPT'`: + PASS, 0.442s. + +These execute the negative project E2E and direct user/project adapter +contracts. No native client, MCP command, model call, remote API, trust, +registration, authentication or workspace publication was run. The passing +fixtures establish refusal/isolation, not remote product capability. diff --git a/docs/plans/project-install-e2e/chatgpt-implementation-evidence.md b/docs/plans/project-install-e2e/chatgpt-implementation-evidence.md new file mode 100644 index 000000000..15eb1324a --- /dev/null +++ b/docs/plans/project-install-e2e/chatgpt-implementation-evidence.md @@ -0,0 +1,55 @@ +# ChatGPT project boundary implementation evidence + +The all-client plan's two review rounds approved an explicit unsupported +ChatGPT project leaf. This implementation adds no native project capability, +artifact-only preparation route, project layout, native registry or remote API. +It retains the existing user personal/manual preparation path. + +## Implemented boundary + +- Production composed CLI tests exercise saved/imperative additions, personal + app registration input, activation/authentication attestation, positional + install, mixed Gemini/ChatGPT targets, exact-ID update/repair/removal and the + invocation-directory project default. Each returns the stable unsupported + reason before detector, acquirer or security evaluator calls. +- Full user/project-A/project-B byte and mode inventories remain unchanged. + The tests require no project `.uap`, lock or user state namespace to be + created. Existing user/project markers remain intact. +- Saved project declarations containing ChatGPT are refused by deps lock, + deps update, deps sync and environment install. The negative fixture exposed + lock/update creating `.uap/environment.lock` before target validation; a pure + project declaration preflight now occurs before that lock. The existing + locked manifest reread, validation and publication checks remain in place. +- Project target validation reports project scope unsupported before manual + user-only guidance, so it cannot suggest `--no-save` as a project bypass. +- Direct ChatGPT activation/preflight rejects project scope or a project + TargetContext even when personal-preparation/attestation flags are supplied. + Direct deactivation refuses unsupported project completion even with an + external-uninstall attestation. This protects adapter reuse in addition to + the shared provider/CLI scope gate. +- Capability tests retain user scope and assert absence of project scope, + ProjectTargetLayout and NativeRegistryLayout. Direct user manual/prepared + outcomes remain bounded and never become verified remote installations. + +## Verification + +Go1.26.10 focused test execution (`-p=2 -count=1 -timeout=3m`) passed production +CLI project ChatGPT contracts **0.576s**, client contracts including existing +exact manual removal **0.009s**, and existing Context7 guided/personal/manual +preparation/lifecycle regressions **0.920s**. Before the correction, the saved +lock/update cases reproduced namespace creation and sync/install exposed +misleading user-only guidance. The final tests retain those precise no-effect +and diagnostic assertions. + +After the concrete locked recovery-inspection helper extraction, a final +focused run also included existing staged snapshots, changed-lock rejection +and all-client native recovery: production CLI **0.491s**, client **0.009s**, +and internal CLI **3.653s**, all passing. The extraction retains the prior +installer-nil and recovery-required behavior inside the environment lock. + +Exact golangci-lint2.13.2 changed-code and whole-file size/architecture gates +on the affected CLI and ChatGPT client packages: **0 issues** each. No lint +baseline or original assertion was relaxed. +Independent implementation reviews and applicable cross-platform CI follow the +frozen checkpoint. These tests execute no ChatGPT remote API, native client, +MCP server, model, registration or workspace publication. diff --git a/docs/plans/project-install-e2e/claude-code-review-r1.md b/docs/plans/project-install-e2e/claude-code-review-r1.md new file mode 100644 index 000000000..b7baf1ce8 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-code-review-r1.md @@ -0,0 +1,96 @@ +# Claude project implementation independent review R1 + +Reviewer: core_clients, 2026-10-23. Reviewed initial adapter `3505f0ab`, +source-only native gate `c47d3393`, scoped SDK/data/restart contracts `40f108c4`, +and frozen corrective `32dd485e789b57c7825c6c4fb2128263901be78f` in +`work/uap-project-claude`. The original two client-plan GO results remain +separate. No production or test file was edited by this reviewer. + +## Decision: GO at 32dd485e + +One exact native identity finding is independently verified closed. No +remaining implementation blocker was found in the scoped directory/listing +lane or the authored source-only native gate. This is an implementation review, +not a completed hosted native project qualification. + +## R1-1: lexical native paths could conceal a different physical project + +At `40f108c4`, project listing accepted filepath.Clean(installPath) equal to the +expected managed path. A native spelling such as skills/alias/../artifact can +lexically normalize to project A's expected path while physically resolving to +project B/artifact if alias points at B/sub. The optional projectPath check also +normalized its input instead of demanding exact selected spelling. + +Closed in `32dd485e`. Project entries now require literal installPath equality +to the validated expected path and literal optional projectPath equality to +the selected project. The user-only parser behavior is unchanged. Regressions +include nonclean install/project paths and a real symlink traversal fixture +that proves physical resolution differs despite lexical normalization. The +authored native project fixture also rejects nonclean discovered install paths. +The correction performs no filesystem mutation or trust action. + +## Reviewed scope and native evidence boundaries + +- Validated original user/project TargetContext selects exact project + `.claude` authority and `.claude/skills/` delivery. + Layout, plan precondition, projection, native registry listing, activation + and deactivation revalidate that selected authority. Package staging remains + beside the watched skills root, preventing precommit staging from appearing + installed. No user-profile surrogate or package-derived project scope exists. +- Every project listing/removal probe uses fixed selected project cwd and the + original user config root in CLAUDE_CONFIG_DIR. The bounded environment and + existing timeout/runner supervision remain intact. The user-only list helper + remains user-scoped. Project operations do not invoke user uninstall or write + native settings/trust/account state. +- The existing package projector, standard loader, rendered command skills, + managed stdio launcher, directory transaction, immutable package receipts and + persistent data contract are reused. Core's declared project CLI lifecycle + matches the real directory/listing lane; it does not borrow an invented + direct-config engine. +- Activated/installed status requires an actual exact enabled project + name@skills-dir listing at the managed path. Wrong scope/path, disabled, + duplicate, malformed, load-error and mismatching optional-project rows do + not establish installation. Missing or failed observations do not become + success. Project attestation is rejected unconditionally before the generic + provider shortcut, including prepared component selections and direct reuse. +- Deactivation requires exact selected-project listing before core removes the + owned directory. Recognized absent evidence cannot silently remove an + allegedly active binding. No global plugin or whole shared settings document + is treated as owned. +- Public SDK fixture journeys exercise user/A/B install, unchanged update, + exact supplied-pin repair after drift despite a moved source, selected update, + status and exact removal. Distinct stdio data receipts retain marker bytes. + Postcommit listing failure leaves incomplete/nonactive ownership; a fresh + installer repairs the recorded pin and reobserves exact identity. Fixture + runners are explicitly not native client evidence. +- The native gate uses the genuine pinned artifact and actual UAP project + operations, fresh plugin list/details, command-byte checks and project-B + absence checks. Its separate opt-in reaches the unchanged disposable guard + before client launch/acquisition. Exact required-test accounting rejects skips + or omitted tests; released/draft/prepared inputs cannot select project proof. + Claude/Codex selector rows must be unioned on integration as documented. + +## Independent execution + +Go 1.26.10, GOMAXPROCS=3, `-p=2`, bounded RAM toolchain/cache at final SHA: + +- Project listing/client contracts: **PASS 0.007s**. +- Composed production provider project contracts: **PASS 0.042s**. +- Public installer user/project-A/project-B lifecycle/data/restart contract: + **PASS 0.177s**. +- Inert native discovered-skill parser contract: **PASS 0.008s**. +- Native runner Python safety/evidence suite: **56 tests PASS 0.455s**. + +An initial SDK run used this reviewer's overly restricted PATH without the +already installed Node runtime; stdio readiness was withheld and the test's +required data assertion correctly failed. Retaining the installed Node path +resolved that harness issue without source, policy or assertion changes. +The existing full client permission-denial suite requires unprivileged execution +as documented by the author; this review used the focused project contracts. + +No native Claude process, configured MCP command, model, account mutation, +login, auto-trust or hosted qualification ran in this review. Local official +binary probes previously aborted with Bun134 and remain crash evidence. The +authored native gate and historical user native results do not qualify new +project discovery, skill execution, MCP initialization/tool use, native restart +or release readiness until genuine hosted results are collected. diff --git a/docs/plans/project-install-e2e/claude-code-review-r2.md b/docs/plans/project-install-e2e/claude-code-review-r2.md new file mode 100644 index 000000000..e1dc48f3c --- /dev/null +++ b/docs/plans/project-install-e2e/claude-code-review-r2.md @@ -0,0 +1,92 @@ +# Claude project adapter independent implementation review R2 + +Reviewer: cursor_clients, 2026-10-23. Reviewed the final adapter and source-only +native gate stack through `40f108c43b7dcbd8d5e57c39799072ece048fbc3`, plus the +R1 corrective frozen checkpoint `32dd485e789b57c7825c6c4fb2128263901be78f`, in +`work/uap-project-claude`. Read repository governing/capability-preservation +instructions, the approved two-GO Claude client plan, production adapter, +public installer and provider fixtures, native gate and matrix selection. + +## Decision: GO at corrected checkpoint 32dd485e + +No remaining implementation blocker found. The original frozen adapter's +project listing path normalization weakness was already identified by the +independent R1 reviewer. This R2 independently checked its final correction: +project `installPath` and optional `projectPath` require literal equality to +the validated selected paths; nonclean traversal spellings cannot manufacture +identity. Legacy user-only listing normalization remains unchanged. The +actual symlink-plus-parent traversal regression demonstrates that the rejected +spelling can resolve into another directory even when filepath.Clean makes it +look equal to the desired managed path. + +## Reviewed boundaries + +- Native project layout is exactly `.claude/skills/`. + TargetContext, original detected user ConfigRoot, `.claude` authority, + target anchor/root and active child layout are revalidated. Staging remains + beside the watched skills directory. Core supplies transactional package + ownership, project state/data and physical root authority. +- All project registry, activation and removal listing commands preserve the + original `CLAUDE_CONFIG_DIR` and set Dir to immutable selected ProjectRoot. + They do not substitute project configuration for a user profile, depend on + ambient cwd, invoke marketplace installation or perform user uninstall. + Existing bounded environment and supervised process timeout remain in use. +- Scoped native listing requires the exact expected `name@skills-dir`, exact + selected project scope/path, enabled status, no duplicate identity, no load + errors and consistent optional projectPath. Wrong user/path identities are + collisions, malformed/unrecognized observations remain unknown, and exact + negative evidence cannot be upgraded by attestation. +- Provider activation preflight runs before generic attestation shortcuts, + and direct adapter activation invokes the same guard. Project attestation + is refused even when selected components would otherwise take a prepared + path. Recognized exact native plugin listing may establish installed plugin + activation under the existing lifecycle; it does not establish MCP + initialization, skill execution or model/task completion. +- Project removal uses the same scope/cwd/config roots and verifies exact + managed native identity before core removes the owned directory. It never + treats a shared settings file or another scope as owned. User behavior and + the existing user-only listing entrypoint retain their prior contract. +- Production loader/planner/renderer/stager/provider contracts cover rendered + commands, wrong/disabled/unknown listings, copied/changed scope/root/user + authority, attestation refusal and user/project-B isolation. +- Public SDK tests install all three scopes with separate stdio data locators, + retain data through update/repair/removal, repair from exact supplied pinned + bytes after source advance, and preserve user/B binding and package bytes. + A native listing failure injected after commit produces an incomplete, + non-active owned binding; a fresh installer instance repairs the selected + pin and reobserves exact project identity. These are production lifecycle + fixture contracts with an inert runner/launcher, not native Claude proof. +- The new source-only native test runs production UAP project operations and + genuine pinned read-only plugin list/details processes at project cwd, + checks project-B absence, exact rendered commands and foreign hashes, and + emits separate project evidence. The unchanged disposable guard runs first. + There is no session/model/trust/MCP initialization/login request. Required + test accounting refuses skips/omissions, and released/draft/prepared inputs + refuse this source-only selection. + +## Independent verification + +Using Go 1.26.10, GOMAXPROCS=3, `-p=2 -count=1`, independently ran +`./clients/claude ./providers ./installer -run '^TestProjectClaude|^TestProjectListing'` +at the clean corrected checkpoint: + +- Scoped client listing contracts: PASS, 0.008s. +- Composed project provider contracts: PASS, 0.118s. +- Public installer three-scope/data/restart lifecycle: PASS, 0.243s. +- `python scripts/test_native_client_matrix.py`: PASS, 17 tests. + +These are reviewer execution results. No native Claude process, package +payload, MCP command, model request, login or trust action was executed. +Existing complete client permission-denial tests were not rerun as task root; +the author's separately recorded unprivileged full-suite run is not relabeled +as reviewer execution. + +## Integration and native qualification boundary + +The shared native matrix delta must retain the union of independently authored +Codex and Claude project selections/workflow conditions on integration. The +author evidence explicitly records that requirement. Native fixture authorship, +parser/compile checks, historical user runtime evidence and local Bun exit134 +probe failures do not qualify the new project runtime. Genuine hosted project +listing/discovery, MCP/skill execution and fresh-session task evidence remain +separate gates. This review makes no release-readiness claim. diff --git a/docs/plans/project-install-e2e/claude-completion-code-reviews.md b/docs/plans/project-install-e2e/claude-completion-code-reviews.md new file mode 100644 index 000000000..63c67d4ff --- /dev/null +++ b/docs/plans/project-install-e2e/claude-completion-code-reviews.md @@ -0,0 +1,76 @@ +# Claude project completion: independent code reviews + +Date: 2026-10-24. Decision: **two independent CODE GO reviews**. +This records review of current source and fixtures, not release qualification or +successful execution of the hosted native lifecycle. + +## Reviewed identity and scope + +Both reviewers inspected the complete Claude completion branch through local +commit `f50111a056837452287bfc3799eb8d4e7d47ee40`, including the adapter observation +and lifecycle checkpoint published as `b9aaab16bc77867feb6e85d6704ce6146ac344f6` +and the SDK/shared-usecase checkpoint published as +`530b1cebafd1cbcdae5adc7f2b16c9734da84f20`. The final checkpoint was published +with the identical Git tree as `0462c69c98c14d965f7e46cfb44038694fe3466b` +(tree `b673e6468d33faeafa949b022199288115231e28`). + +The reviews cover strict project listing classification, native namespace +admission, independent owned delivery verification, initial receipt-free add, +Prepared activation and replay, lifecycle deactivation, transient repair actions, +fixture coverage, and the untrusted Prepared native harness. Existing user +runtime gates remain in force. + +## Review 1: root integrator + +Reviewer: root integrator, agent `/root`. Final decision: **CODE GO**. + +The reviewer traced the classifier and Prepared ownership separation, including +initial collisions; verified that project probes preserve native scope and that +inactive removal remains subject to the owned transaction kernel; and checked +that the stable Prepared result helper uses verified receipt equality and fresh +reobservation. Repair guidance remains transient. Every new native evidence stage +and the raw-list parser was reviewed: owned files do not certify discovery, the +harness grants no trust and performs no model request or MCP initialization, and +user gates are unchanged. + +Independently reported focused verification: + +| Package or contract | Result | +| --- | --- | +| Providers | PASS, 0.165 s | +| Installer | PASS, 0.540 s | +| Repository native evidence contracts | PASS, 0.021 s | + +## Review 2: independent APM reviewer + +Reviewer: independent APM reviewer, agent `/root/apm_vercel_scope`. +Final decision: **CODE GO**. + +The reviewer inspected the full `b07c3a6e` through `f50111a` branch, including +strict JSON uniqueness, mixed listing rows, provider ownership, generic resume +and repair. A clear registry finding admits only the scope namespace; prepared +scan and receipt/digest checks remain independent. Blocked results do not set +`NativeDiscoveryReconciled`. Former Active bindings refuse removal when fresh +native evidence is contradictory. Stable Prepared classification verifies the +owned digest, native objects and lifecycle without skipping reobservation; +status remains host-free and repair guidance remains transient. + +The harness keeps owned-file checks separate from raw native suppression in +projects A/B. Native discovery remains blocked, MCP/model operation remains +unevaluated, and user gates are unchanged. + +Independently reported focused verification: + +| Package or contract | Result | +| --- | --- | +| Claude client | PASS, 0.020 s | +| Providers | PASS, 0.213 s | +| Installer | PASS, 0.646 s | +| Repository native evidence contracts | PASS, 0.013 s | +| Python native matrix selection | 18 PASS, 0.390 s | + +Neither independent reviewer executed a vendor binary for these code-review +checks. The separately preserved genuine CLI spike and the hosted Prepared +lifecycle harness have distinct evidentiary roles. No hosted pass, trusted +precedence observation, MCP startup, authentication or release approval is +inferred from these CODE GO decisions. diff --git a/docs/plans/project-install-e2e/claude-failure-diagnostic-code-review-r1.md b/docs/plans/project-install-e2e/claude-failure-diagnostic-code-review-r1.md new file mode 100644 index 000000000..05e9043c2 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-failure-diagnostic-code-review-r1.md @@ -0,0 +1,49 @@ +# Claude bounded failure-listing diagnostics — independent code R1 + +Reviewed frozen clean `0b85d4a293beee1be05733cfc75dd4fd9afd260a` in +`work/uap-project-claude`, after the plan and both GO reports were committed at +`c53e030e`. **GO** for the diagnostic-only checkpoint. Reviewer did not author +the implementation and executed no Claude/native process or acquisition. + +The production parser, Claude adapter, opt-in and real disposable-host guard +are unchanged. The four-file diff adds test-only capture/metadata/contracts, +first-error fixture wiring and truthful evidence. Successful installer runs +take the original path. The fixture's latch permits only the first returned +installer error to trigger the fixed two A/B listing observations. It uses the +already verified genuine client and fresh explicit original profile environment, +then retains the original result/error and existing foreign-byte check. The +lifecycle marks a stage failed before running its installer, and the original +installer stdout/stderr are already recorded before this additional capture; +diagnostics do not mark it passed or retry it. + +Each capture has its own ten-second context, empty stdin/no shell, separate +stdout/stderr writers capped live at 1MiB, cancellation on overflow and 250ms +WaitDelay for inherited pipes. Writer state is mutex-protected, never appends +beyond its cap, and returns an independent snapshot. Direct-child kill/pipe +bounds are described accurately without a descendant-sandbox claim. Exit -1 +for failure to start, real process exit when available, context deadline, +per-stream overflow, elapsed time and bounded error remain explicit. + +The finite helper copies the fixture environment per child, uses only +`plugin list --json` from exact A/B cwd, and preserves separate capped raw logs +and byte/SHA256 metadata. No unknown listing is parsed into ownership, active +status or a success result; every diagnostic record and aggregate qualification +remain false. Fixed artifact paths are inside the existing evidence root, whose +ordinary writer hashes the files for upload. No mutation, guessed plugin ID, +trust/account/model/MCP action, new acquisition or format relaxation appears. + +Independent inert verification: +`go test -p=2 ./repotests -run '^TestClaudeProjectDiagnostic' -count=1` passed +both contracts in **0.009s**, without opting in to a native test. They cover +exact boundary/overflow cancellation, bounded later writes and snapshot copies, +fixed two-call selection, independent env/streams, failure metadata/hashes and +bounded error text. `git diff --check` passed. Guard and production adapter bytes +are unchanged across the reviewed diff. The author's exact lint pass remains +separate recorded evidence. + +No blocking implementation issue found. Root may publish/replay this exact +diagnostic checkpoint only after the other independent implementation GO. +Actual raw hosted output and both reviewers' interpretation are still required +before a separate evidence-based production decoder amendment. This GO does not +qualify the failed project lane, native discovery/runtime, trust, another OS, +all-client E2E or a release. diff --git a/docs/plans/project-install-e2e/claude-failure-diagnostic-code-review-r2.md b/docs/plans/project-install-e2e/claude-failure-diagnostic-code-review-r2.md new file mode 100644 index 000000000..20a269b5e --- /dev/null +++ b/docs/plans/project-install-e2e/claude-failure-diagnostic-code-review-r2.md @@ -0,0 +1,13 @@ +# Claude first project failure diagnostics — independent code R2 + +**GO** at frozen clean `0b85d4a293beee1be05733cfc75dd4fd9afd260a` in `work/uap-project-claude`, reviewed by cursor_clients on 2026-10-23. The diff from `c53e030e` is four files: the test-only capture primitive, two inert contracts, the fixture's first-error hook, and the truthful evidence note. Both independent plan GO reports precede source. No production parser, adapter, native success assertion, acquisition, opt-in or hosted guard changes. + +The hook runs only when the existing installer runner returns an error and diagnosticAttempted was false. It sets the flag before capture, then uses the same verified client path, exact existing A/B roots and original explicit fixture environment. Its fixed loop produces exactly two fresh `plugin list --json` calls. Successful installer paths launch neither. The original result/error are returned unchanged after diagnostics and the existing foreign-byte check; the installer stdout/stderr had already been recorded by commandsRunInstaller. No retry, details with a guessed ID, native mutation, trust, reload, model or MCP operation is introduced. + +Each real capture creates an independent10second context and a direct exec.CommandContext with no shell, empty stdin, copied environment and WaitDelay250ms. Separate mutex-protected writers append at most1MiB each while pipe copying runs. A write beyond the remaining limit stores only the bounded prefix, marks overflow, cancels the child and returns a short-write error. Exact-boundary writes are permitted and the next nonempty write triggers cancellation. Repeated writes after overflow cannot grow the buffer. Snapshot clones cannot modify captured data. The direct-child deadline plus WaitDelay bound inherited pipe closure; this is not OS/process-group isolation, and the evidence note correctly avoids such a claim. + +Capture records process exit or -1, bounded512byte error, independent deadline and per-stream limit flags, elapsed duration, separate raw log filenames/lengths/SHA256 and exact argv/cwd. Unknown or malformed JSON is retained raw rather than parsed into installed/active evidence. Each record has qualification:false; the wrapper metadata keeps project_qualified:false and identifies the original installer label/error. New logs/metadata are inside the ordinary evidence root, so existing writeEvidence hashes/uploads them. Supplementary capture failure does not convert the original failed operation into success. Existing fatal IO/foreign-byte checks continue failing the test if evidence cannot be safely recorded or foreign state drifts. + +Independent verification: both inert diagnostic tests passed in0.012s. They exercise exact-cap acceptance, overflow cancellation and bounded subsequent writes, cloned snapshots, fixed two-call argv/cwd/environment selection, copied environment, raw separate streams, exit/deadline/limit/hash metadata and bounded errors. git diff --check passed. Static comparison confirms the actual native opt-in and disposable-host guard remain before fixture creation, and all existing production parser/qualifier paths are untouched. No Claude binary, installer, MCP, account or model process was run by this review. + +No blocking finding remains. Root can publish/replay this diagnostic-only checkpoint under the existing native guards. The actual failing project artifact remains failed/pending; a successful diagnostic artifact must be independently interpreted before any separately planned parser/scope correction. This GO does not establish project discovery/runtime, trust, other-platform, all-client or release qualification. diff --git a/docs/plans/project-install-e2e/claude-failure-diagnostic-plan-review-r1.md b/docs/plans/project-install-e2e/claude-failure-diagnostic-plan-review-r1.md new file mode 100644 index 000000000..bf1496811 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-failure-diagnostic-plan-review-r1.md @@ -0,0 +1,39 @@ +# Claude failure-listing diagnostic plan — independent R1 + +Reviewed documentation-only frozen `423d912ff56c033cbae455786b6176a64394cfac` +in `work/uap-project-claude` on 2026-10-23. **GO** for this test-only diagnostic +supplement, after both independent plan GO reports are committed. No Claude +process, acquisition or native operation was executed during review. + +Independently traced the existing project fixture. Its installer run can return +an error before the later skills/listedPath probes, so the existing failure +artifact can lack the raw native project listing needed to assess a decoder +refusal. The unchanged real disposable-host guard precedes fixture creation. +The fixture already owns the verified client path, original explicit profile +environment and existing A/B project directories, and its evidence writer hashes +captured JSON/log files. The plan uses those existing authorities rather than +guessing an install ID, root, profile or native shape. + +The proposed first-error-only capture is at most two fixed read-only +`plugin list --json` calls, A then B, with the same genuine verified binary and +fresh profile. Successful installer paths incur no diagnostic calls. No retry, +mutation, guessed plugin details, trust/account/model/MCP action, parser change, +new acquisition or guard alteration is introduced. The original installer error +and failed lifecycle stage remain failed; diagnostic failure is recorded +separately. The foreign-byte check still runs after collection. + +The stream design is appropriately bounded while running: distinct 1MiB writers +for stdout/stderr, cancellation on overflow, independent ten-second contexts, +closed stdin, no shell and at-most-250ms WaitDelay for inherited pipes. Preserve +the available capped raw bytes even for errors, malformed output or overflow, +plus bounded exact argv/cwd, exit/error/deadline/limit/elapsed and hash/count +metadata. This does not promise OS isolation or general descendant sandboxing. +Inert tests must exercise exact caps, cancellation, separate-stream accounting, +first-failure call selection and bounded metadata without executing Claude or +spoofing hosted admission. + +No blocking plan issue found. Both implementation reviews still precede root's +exact hosted replay, and both reviewers must interpret the resulting genuine +listing before any production decoder/scope amendment. Neither collected JSON +nor passing existing user suites may relabel the failed project lane, runtime, +trust, another platform, all-client E2E or release qualification. diff --git a/docs/plans/project-install-e2e/claude-failure-diagnostic-plan-review-r2.md b/docs/plans/project-install-e2e/claude-failure-diagnostic-plan-review-r2.md new file mode 100644 index 000000000..607bf374f --- /dev/null +++ b/docs/plans/project-install-e2e/claude-failure-diagnostic-plan-review-r2.md @@ -0,0 +1,13 @@ +# Claude first project failure diagnostics — independent plan R2 + +**GO** for frozen docs-only `423d912ff56c033cbae455786b6176a64394cfac` in `work/uap-project-claude`, reviewed by cursor_clients on 2026-10-23. No source changed and no Claude/native process was executed. This approves only the bounded failure-observability supplement; both plan GO reports precede implementation, then both code reviews must precede root's replay. + +Independently inventoried the retained genuine artifact ZIP. The failing project prefix `uap-claude-native-evidence-58929984/` contains version, add installer stdout/stderr and project evidence JSON, with no raw A/B native list logs. Traced the current fixture: `run` returns the installer result only after its foreign-byte check, while ordinary `skills`/`listedPath` observations happen later and cannot be reached after add fails. The plan addresses missing evidence rather than assuming a populated schema from an unrecognized-list error. + +The proposed hook is narrow: only the first installer error, at most two fixed `plugin list --json` calls from exact already-existing A/B cwd, same verified client and fresh explicit original CLAUDE_CONFIG_DIR environment. Successful installer paths do nothing extra. There is no add retry, guessed plugin ID/details call, mutation, reload, new acquisition, trust/account/model/MCP execution, parser change or success relaxation. Both existing opt-in and the real disposable-host guard remain before fixture/acquisition/process creation. + +The independent10second context per command, fixed1MiB-per-stream capped writers, cancellation on overflow, DEVNULL/no shell and WaitDelay<=250ms are appropriate for this read-only diagnostic. The code review must confirm the cap is enforced while writing, exact-limit/overflow behavior is recorded without unbounded accumulation, and stdout/stderr are preserved separately even on error/timeout/unknown output. Metadata must retain fixed argv/cwd, bounded errors, elapsed/deadline/limit/exit and hashes/lengths. The primitive does not promise general descendant isolation, as the plan states. + +The original installer failure/result/stage remains the failure; diagnostic failure cannot suppress it or qualify discovery. The foreign-byte check remains after diagnostics, and existing writeEvidence includes the new logs/metadata hashes. Inert stream-boundary/cancellation/metadata/finite-selection tests plus existing native guard/safety checks are sufficient. No broad rebuild or local Claude execution is needed. + +The next actual hosted artifact must be independently interpreted before a separately reviewed evidence-based parser/scope correction. Existing user-suite success and diagnostic collection cannot establish project, trust, MCP runtime, other-platform, all-client or release qualification. No blocking plan finding remains. diff --git a/docs/plans/project-install-e2e/claude-failure-listing-diagnostic-plan.md b/docs/plans/project-install-e2e/claude-failure-listing-diagnostic-plan.md new file mode 100644 index 000000000..cd4c55369 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-failure-listing-diagnostic-plan.md @@ -0,0 +1,72 @@ +# Claude project native failure — bounded diagnostic capture + +Pre-code supplement, 2026-10-23. The original twice-reviewed Claude project +adapter/native gate remains in force. This changes only missing failure +observability in the opt-in source native fixture, not its success criteria +or the production adapter/parser. Obtain two independent plan GO reviews before +source changes, then two independent implementation reviews before root replay. + +## Actual failure and missing fact + +Genuine disposable hosted macOS arm64 run40011624952, job120613172265, artifact +12528049546 executed Claude Code2.1.263 with measured binary SHA256 +`ef5d2909c8af49f31ab6d5487e90316777bc2fac170adfe8160716caa8aaf4f9`. +Artifact ZIP SHA256 is +`c45098c244379dd4e0b62f2dfb7e235b415959428e19cd235cf083652fb5caf7`. +User command/plugin/runtime suites passed; the project test actually failed +at add with `the Claude Code plugin list output is not recognized`. Its project +prefix `uap-claude-native-evidence-58929984/` contains installer failure/version +but no raw native project listing, because add aborts before the later probe. +No project list shape or correct decoder change can be inferred from this +error alone. Keep the current fail-closed parser and actual failed stage. + +## Narrow test-only correction + +In the existing Claude project native fixture, when its installer runner first +returns an error, collect at most two fresh read-only native commands before +returning the same original failure: `plugin list --json`, once from its exact +project A cwd and once from its already-existing project B cwd. Use the same +verified genuine client path and explicit fixture environment/original +CLAUDE_CONFIG_DIR as ordinary native probes. No installer retry, native mutation, +reload, plugin details with a guessed ID, trust/account/model/MCP invocation, +new acquisition or hosted guard change is allowed. Successful installer paths +run no additional diagnostic commands. Both existing opt-in and unchanged real +disposable-host guard remain before fixture/acquisition/process creation. + +Capture stdout and stderr separately with a fixed1MiB cap per stream while the +process runs, an independent10s context deadline per command, DEVNULL/empty +stdin, no shell and a short exec.WaitDelay (at most250ms) for inherited pipe +closure. Each capped writer cancels the child promptly on overflow and records +that overflow; do not use CombinedOutput or truncate unbounded output afterward. +Use fixed labels/argv and only known fixture roots. Record exact argv/cwd, +exit/error/deadline/limit/elapsed and byte/hash metadata with bounded error text; +preserve separate raw capped stdout/stderr logs under the existing evidence +root even on nonzero exit, timeout, malformed/unknown JSON or overflow. Do not +parse this diagnostic into installed/active evidence or suppress its error. +The existing context/WaitDelay primitive bounds this read-only diagnostic; +it does not claim OS isolation or general descendant/process sandboxing. + +Run the existing foreign-byte check after diagnostics and preserve the original +installer failure/stage as failed. Additional diagnostic failure is separately +recorded, never a reason to mark the original lifecycle successful. Existing +writeEvidence hashes the captured logs/metadata in the ordinary uploaded +artifact. No secrets/account state exist in this fresh explicit fixture. + +## Meaningful inert verification and next gate + +Add focused tests for the capped stream primitive's exact boundary, overflow +cancellation and preservation without unbounded accumulation, separate streams, +bounded metadata/errors and finite diagnostic argv/cwd selection. Tests may +inject inert capture functions or use an inert test process; they may not launch +Claude, spoof hosted admission or weaken native success assertions. Retain +existing native opt-in/guard safety tests, syntax/format/diff checks and bounded +relevant repotests. No broad test rebuild is required while task storage is +constrained. + +Commit the source checkpoint with both independent implementation GO reports; +root publishes/replays the exact hosted native suite. Independently inspect +that genuine artifact before proposing any production parser/scope change. +If a decoder correction is warranted, freeze a separate narrow evidence-based +plan and obtain both plan GO reviews before coding it. Neither successful +diagnostic collection nor existing user-suite success qualifies the failing +project lane, trust, MCP/runtime, another OS, all clients or release completion. diff --git a/docs/plans/project-install-e2e/claude-grouped-prepared-update-plan.md b/docs/plans/project-install-e2e/claude-grouped-prepared-update-plan.md new file mode 100644 index 000000000..ad9e4da67 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-grouped-prepared-update-plan.md @@ -0,0 +1,104 @@ +# Claude grouped Prepared update correction plan + +## Scope and frozen evidence + +This plan concerns result classification for an unchanged, verified project delivery that remains Prepared after a fresh native observation. It does not change native discovery, activation completeness, ownership admission, transaction authority, or trust policy. No production source has been changed for this plan. + +The genuine current-source run [40131383052](https://github.com/777genius/universal-agent-plugins/actions/runs/40131383052) checked out commit `75cece1488cdab0674b4817ea7a30ea95f7aad06`, tree `c302aa9bc97127b89504c9e6de049abfcf5ef580`, and executed pinned Claude Code 2.1.263 on darwin-arm64 and windows-amd64. Both jobs passed the existing user commands, installation, and runtime lifecycle tests. Both failed `TestAgentpluginsClaudeNativeProjectPreparedLifecycle` at the unchanged update assertion: + +``` +unchanged prepared update: mutated = true, want false +``` + +Each raw update result contains `mutated:true` together with `no_change:true`; each repeated add result contains `mutated:false,no_change:true`. Both outcomes remain Prepared + installation_verified with the trust blocker. The fixture aborted before checking its post-update state hash, so this artifact alone does not prove the state remained byte-identical after update. Later lifecycle stages were not evaluated. Project discovery remains blocked; MCP initialization and model use were not evaluated. + +Raw project-stage files, original test transcripts, and independently checked ZIP/member hashes are in: + +`docs/evidence/project-scope-native/claude-2.1.263-prepared-update-failure-40131383052/` + +Darwin artifact 12575658957: 382183 bytes, SHA256 `0359fc9c1dc6bf694a3fa1c89bbe585636a21c799eba5b0ea576d0bc6369ede4`. +Windows artifact 12575786665: 396353 bytes, SHA256 `a944c0b0f27fe96c2eb32700830e6ec11b04ec3048cec3def9201f8c9479f60b`. +Both ZIP archives and every original project artifact hash were checked after connector download. No vendor process was executed locally. + +The newer source run 40135056328 at `4a1634f4906453eac0b87eddbcc54246226dcfdd` was queued when inspected. No run was dispatched or retried. + +## Exact cause and boundaries + +The ordinary repeated-add path in `usecase/lifecycle.go` calls `verifiedPreparedObservationUnchanged`. That helper already requires project scope; non-native-effect lifecycle; an available actual verifier; materialized exact-path/digest delivery; saved and fresh Prepared/Installed states with equal authentication and policy; no activation/authentication attestation; no native effect or newly returned native objects; and exact recorded delivery native objects. + +The grouped update path in `usecase/group_activate.go` derives and verifies the existing delivery through `activeNativeDelivery`, then reobserves it through the ordinary activator. Claude is intentionally a CLI-backed lifecycle whose Prepared registration is incomplete. `nonNativeRegistrationComplete` therefore returns false. After successful unchanged Prepared observation, `activateOneGroupTarget` unconditionally clears its local `target.noChange` whenever `nativeComplete` is false. `assignActivationOutcome` derives `Mutated=true` from that cleared flag, although its preflight `NoChange=true` flag survives if no lifecycle field changed. + +This diagnosis does not rely on treating complete=false as an error or promoting it to complete=true. The root's separately reviewed default prepared-file observer work addresses early ownership admission and does not correct this grouped result branch. + +## Proposed minimal implementation + +1. Return the actual delivery used for activation from private `activateGroupDelivery` to its sole caller, alongside the unchanged outcome/nativeComplete/error results. This keeps the boolean's native completeness meaning intact. +2. After fresh activation succeeds, preserve a preflight no-change target only when its existing managed binding and the returned actual delivery/outcome pass the same `verifiedPreparedObservationUnchanged` helper used by repeated add. Do not infer this from Prepared alone. Keep ordinary noncomplete-effect classification unchanged for every request that does not pass this predicate. +3. Keep persisted lifecycle/receipt change detection independent. Any real lifecycle change still makes the result mutated. An actual package transaction still starts from target.noChange=false and cannot become unchanged through this exception. Native committed/uncertain effects, newly returned native objects, policy/authentication drift, attestation, missing digest, or changed owned bytes cannot pass the predicate. +4. Synchronize a target's final NoChange flag with the existing invalidation branch so an effect-bearing successful incomplete activation cannot emit mutated=true/no_change=true. OR each final target Mutated flag into GroupResult.Mutated, because a native effect can occur while recorded receipts and lifecycle fields remain identical. GroupResult has no NoChange field: retain its existing wire schema; require the target NoChange=false on actual mutation. Preserve existing projection-failure no-change semantics: a failure before effects can retain its original no-change classification with the recorded lifecycle and both mutation flags false. +5. Do not change `activeNativeDelivery`, `nonNativeRegistrationComplete`, ownership, identity, recovery eligibility, native attempt creation, transaction staging/commit, or read-only versus acting adapter selection. Do not change the strict native harness assertion or its existing required tests. + +No new generic framework, public API, adapter-specific shared branch, or production injection hook is proposed. + +## Regression acceptance and finite validation + +- Add a focused inert usecase group regression with a real materialized project binding and a fresh successful Prepared observation: repeated grouped update reports group and target Mutated=false, target NoChange=true; exact state bytes, receipts, and owned tree remain unchanged; verifier is called again; activation remains Prepared/Installed. +- Cover the actual saved CLI single-target batch update route used by the native harness with an inert Claude runner returning the exact known suppression sentinel. Check repeated add and update, exact state hash/owned bytes/user+A/B preservation, fresh native-list calls, and no discovery promotion. This is an inert regression, not replacement native evidence. +- Include changed source bytes while remaining Prepared: update must mutate real owned bytes and report Mutated=true, NoChange=false. Include a successful incomplete outcome carrying NativeEffectCommitted while the persisted receipts and lifecycle facts remain identical: BOTH GroupResult.Mutated and Targets[].Mutated must be true, and Targets[].NoChange must be false. Include changed lifecycle/policy and explicit authentication/activation attestation negatives so the exception cannot mask real changes; the existing authentication-preservation rules remain intact. The shared predicate must still require saved/fresh normalized authentication and policy equality. Preserve existing pre-activation projection-failure test and strict user-scope no-change tests. +- Run only new focused group/CLI tests and existing relevant idempotent/projection-failure/Claude Prepared tests with shared Go1.26.10, -p=1. No broad suite, vendor process, authentication, trust change, or local hosted guard bypass. +- Freeze implementation for independent root and peer code review after both independent R1 and R2 plan reviews. Publish only after both code GOs. +- Reuse the next existing exact-source hosted native run if its source includes the reviewed correction, or let the reviewed normal PR workflow run on publication. Do not blindly dispatch or retry the already known failing run. Retain original failures and require the unmodified Prepared lifecycle gate to pass before declaring this issue closed. + +## Remaining unrelated bootstrap investigation + +The Linux bootstrap cancellation failure is being traced read-only. No timeout/assertion has been relaxed and no production fix is proposed by this Claude plan. + +## Independent plan review record + +R1 reviewed exact `6876e8f3` before production edits: + +- Root: architecture GO after plan and grouped-branch trace; require final group/target mutation aggregation, retain actual wire fields, and keep projection failure classification. +- apm_vercel_scope: architecture GO after tracing the helper, grouped activation, CLI route, and native evidence; require same-recorded-facts NativeEffectCommitted negative at both group and target levels. + +The R2 proposal incorporates both requirements above. No production source, test selector, strict native assertion, or native completeness semantics have changed. + +## SDK group mutation propagation amendment — R2 + +Root's real public SDK Cline+Codex two-target repair regression exposed a separate inherited reporting omission while the default observer work was being tested. In installer/group_apply.go, finishGroup receives a core GroupResult whose real managed directory restoration has Mutated=true, but never transfers that flag to installer.Result. Its inherited Prepared/Installed bindings continue to have hostHandoffPending, so the SDK honestly retains OutcomeIncomplete with the existing reason committed binding is not activated; it incorrectly reports Mutated=false even after the owned files were restored. This amendment changes only mutation reporting, not that Prepared handoff contract. + +Proposed bounded correction after both independent R1 architecture GOs; R2 review is required before editing SDK code: + +1. At the start of finishGroup, accumulate result.Mutated OR got.Mutated before state reload, activation-error, pending-handoff, or normal-success return branches. This preserves effects already recorded by host reconciliation and actual core mutation even if later inspection fails. Do not infer native activation or mutation from Prepared alone. +2. Keep OutcomeIncomplete, its native handoff reason, activation/verifier facts, recovery decisions, and all hostHandoffPending behavior unchanged. There is no new automatic completion of Prepared bindings and no attestation/discovery promotion. Gate only finishGroup's final unchanged classification on !result.Mutated, so a stale no-change preview cannot override an actual observed mutation. Keep its existing pending/native handoff checks. Do not alter the unrelated early unchanged Apply branch. +3. Focused SDK tests must pin: actual managed repair with Prepared/Installed remains Incomplete but Mutated=true; unchanged read-only core result does not manufacture mutation; existing input result.Mutated=true is not cleared; committed effects remain visible on activation/pending/reload-error exits even if those exits keep their existing Outcome classification. Include the stale prepared.plan.NoChange=true plus actual core/prior host mutation case: SDK must not claim NoChange or OutcomeUnchanged after a real observed effect. A truly read-only result with a no-change preview still returns unchanged. Reuse the root's composed real two-target repair regression when integrated and add a narrow installer finishGroup unit table for the reporting paths as needed. The earlier core group classification negatives remain mandatory independently. + +No SDK production code was edited for this amendment. Independent review and a frozen code checkpoint are required before publication. + +SDK amendment R1 exact a30408aa received independent architecture GOs from Root and apm_vercel_scope. Root required verified mutation preservation even when state reload/Outcome classification fails; APM required that finishGroup's final unchanged branch cannot contradict an actual core/prior host effect. R2 incorporates those requirements without changing the early applyGroup branch or Prepared handoff semantics. + +Core grouped Prepared R2 exact cd99183b independently received PLAN GO from Root and apm_vercel_scope before source edits. SDK reporting amendment R2 exact ad6478e8 independently received PLAN GO from both reviewers before its two production-line changes. Original failing native assertions remain unchanged. + +## Local implementation checkpoint validation + +The approved correction now reuses the exact activation delivery in grouped classification, retains nativeComplete=false for blocked registration, synchronizes target NoChange after invalidation, and accumulates final target mutation into the group. The SDK amendment accumulates the core/prior mutation before every finishGroup exit and gates only that function's final unchanged result. The existing Prepared handoff remains Incomplete with its original reason when pending. The existing delivery equality predicate was extracted unchanged into a small private helper to satisfy the repository complexity gate; no condition was removed or widened. + +Focused inert Go1.26.10 tests, sequential with -p=1: + +- CLI project regression plus existing user idempotent update tests: PASS0.226s. Actual project namespace composition, exact suppression fixture, repeated add/update with fresh listing and exact state/owned bytes, changed real skill bytes remaining Prepared, and original user-native/user-state/A/B foreign preservation. +- Usecase grouped Prepared table plus existing single-resume authentication, Kimi unchanged and pre-activation projection-failure regressions: PASS0.231s. The same-recorded-facts committed effect writes an actual inert native marker while receipts/lifecycle/state bytes remain exact; both group/target mutation flags must report it. Policy/authentication/attested outcome negatives are covered. +- SDK reporting table, existing group attempt/cancellation classification and project Claude Prepared lifecycle: PASS2.148s. Pending handoff/error/read-failure effects remain visible; stale no-change previews cannot override actual mutation; existing handoff facts remain Prepared/Installed. + +These tests execute no vendor, native plugin, MCP server, authentication or model process. The unchanged genuine hosted Prepared lifecycle assertion still needs a successful exact corrected-source run before closure. Bootstrap remains a separately approved plan with no production edit in this checkpoint. + +### Frozen code review correction + +Root's independent review of2cbfecc8 identified a known-effect reporting edge when nativeComplete=true: an adapter that unexpectedly reports NativeEffectCommitted during VerifyOnly must still report the actual effect, even with identical saved facts. Final target mutation therefore includes NativeEffectCommitted independently of projection completeness; the same final flag clears target NoChange and accumulates into group mutation. This preserves effect truth without promoting discovery or changing adapter authority. A new inert Active/Installed binding case proves complete=true/VerifyOnly=true, real marker write, unchanged state/receipts, and both mutation flags true. Focused six-case group table plus existing Kimi/projection-failure tests PASS0.299s. No test assertion or native gate was weakened. + +## Independent frozen-source code review receipts + +Both reviewers assessed exact source commit `20920de589227ffd34ece978ac52add365ceac4b` (tree `39a42178feabf4a9589085f8c67f9bd4501c477c`) after the earlier `2cbfecc8` correction. This record changes documentation only; the reviewed core/SDK production source remains identical. + +- `/root`: CODE GO. Reviewed all three production files, CLI/core/SDK regressions and final committed-effect delta. Confirmed exact actual delivery reuse and unchanged strict Prepared predicate; nativeComplete/native attempts/ownership/Prepared handoff remain separate. Final known committed effect is reported independently of complete/VerifyOnly, clears target NoChange and aggregates group mutation. SDK mutation propagation/final unchanged guard remain narrow. Reviewed the owner's focused passing test outputs; no independent vendor run was claimed. +- `/root/apm_vercel_scope`: CODE GO. Independently read the complete production, CLI, core and SDK source and the final delta. Confirmed complete=true committed-effect marker case pins group/target mutation and target NoChange; Prepared predicate, actual delivery, native completeness and uncertainty fences remain intact. Confirmed SDK entry OR and final mutation guard. No new build or vendor process was run by this reviewer. + +Publication may carry the exact reviewed production source plus this receipt and already reviewed plans/evidence. Genuine exact corrected-source hosted Prepared lifecycle qualification remains required; neither code review nor the inert local tests certifies native project loading, MCP initialization, or model use. diff --git a/docs/plans/project-install-e2e/claude-implementation-evidence.md b/docs/plans/project-install-e2e/claude-implementation-evidence.md new file mode 100644 index 000000000..103d59677 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-implementation-evidence.md @@ -0,0 +1,208 @@ +# Claude project skills-directory plugin evidence + +Pre-code checkpoint, 2026-10-23. The all-client plan and both independent plan +review/fix rounds select one native skills-directory plugin at the selected +project's `.claude/skills/`. This extends the existing +projection/listing lifecycle; marketplace installation is not an alternate +fallback. Existing user behavior and `CLAUDE_CONFIG_DIR` authority stay intact. + +## Pinned evidence inspected before code + +- Official Linux x64 package `@anthropic-ai/claude-code-linux-x64` **2.1.263**: + https://registry.npmjs.org/@anthropic-ai/claude-code-linux-x64/-/claude-code-linux-x64-2.1.263.tgz +- Pinned archive integrity: + `sha512-0IrvpLd/0FP0acQw59T4Cvx/r4nwAXKBrW0WyhIXymzYWurPCLztB+Icu9MkeewAUI+p3PTXsSfmilv/n6XlAQ==`. +- Acquired binary SHA256 + `26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba`. +- Local read-only version, plugin-help and plugin-list-help probes in the + restricted disposable namespace all aborted with Bun exit **134**. Their + logs are crash evidence, not successful CLI help or native project proof. + No private host `/proc`, guard bypass, login, trust or model call was used. +- Existing committed hosted user evidence is indexed by + `docs/evidence/client-compatibility-linux-amd64-0.1.53.json`: exact pinned + version, plugin-list observations and user lifecycle/runtime records. That + historical user evidence does not qualify this new project lane. +- Current official plugin CLI and installation documentation were opened before + implementation: + https://code.claude.com/docs/en/plugins/cli-reference and + https://code.claude.com/docs/en/plugins/install . The CLI reference documents + skills-directory list entries with id, scope, enabled and installPath, and + project/user skills-directory sources. This is the inspected listing schema; + a fixture runner is not claimed as native CLI help or execution. + +## Implementation boundaries + +The adapter will validate core's TargetContext, select exact `.claude` native +authority and `.claude/skills` watched layout, retain the original detected user +configuration environment, and run every project listing at the immutable +selected project cwd. Staging remains outside the watched skills directory. +Existing package projection, managed stdio launcher, command rendering, +transaction, receipt and ownership machinery are reused. + +Listing accepts only the expected project `name@skills-dir` identity, enabled +at the exact managed path; another scope/path, disabled or unknown listing +cannot establish installation. Project activation attestation cannot override +recognized negative or unavailable native discovery evidence. Removal validates +the same selected scope/root and exact native identity before the shared owned +directory removal. It never invokes a user uninstall or edits native settings. + +Contract/fixture evidence and new genuine hosted project native observations +will be recorded separately. The local Bun failure remains a pending native +probe boundary; no model task, account mutation, auto-trust, scanner upload or +credential creation is authorized to remove that boundary. + +## Initial implementation checkpoint + +The adapter now selects the exact project skills-directory plugin, validates +original user config/project/native root authority and fixed project cwd, and +retains the user-only listing entry point. Project listing additionally refuses +load errors and a mismatching optional projectPath. Exact enabled native listing +can establish installed activation; MCP initialization, skill execution and model +use remain separate observations. Project attestation is refused before generic +provider shortcuts, and project removal probes the same scope/cwd/config roots. + +Production loader/planner/rendering/staging/provider contracts passed (0.074s) +with an inert command runner. Public installer contracts passed (0.251s) across +user/project-A/project-B install, unchanged update, exact supplied-pin repair +after drift, moving local source update, status and exact directory removal. +The public SDK requires its host to supply exact pinned bytes for repair; it +does not silently follow moved source. User/B state and native package bytes +remain unchanged by the selected project journey. These are fixture contracts, +not native-client evidence. + +Full domain/planner packages passed (0.027s/0.044s). Existing Claude client +permission fixtures intentionally require an unprivileged process: under task +root, mode-bit denial checks fail because root can read those directories. The +compiled full client suite passed as uid 65534 without source/test changes or +skips. Existing user `TestClaude` provider contracts passed (0.166s). + +The first exact-code lint found only capitalization/import-shadow issues, which +were corrected. Full-file size/architecture lint passed on the initial adapter +and provider scope without suppressions. The expanded public-installer lint +checkpoint encountered a task-host cache write ENOSPC; its final rerun and both +independent implementation reviews remain to be recorded. The actual native +project fixture gate is still pending; local help/version crash evidence and +historical hosted user evidence must not be relabeled as project discovery. + +## Authored source-only native project gate + +`TestAgentpluginsClaudeNativeProjectPluginLifecycle` adds a separate source-only +opt-in behind the unchanged disposable-host guard. It uses genuine pinned +`plugin list --json` and `plugin details` commands at selected project cwd after +actual UAP add, unchanged update, command-rule update, repair and removal. The +listing must be project-scoped, enabled and directly under selected +`.claude/skills`; fresh project-B processes must not see the fixture plugin. +Installed rendered command bytes are checked and foreign user/cache/A/B files +retain their hashes. No session, model request, trust edit, MCP initialization, +login or credential operation is added. A dedicated +`claude-project-plugin-evidence.json` records the separate native facts and +unobserved runtime boundaries. + +The native matrix's explicit `--project-components` source-only selection also +requires this test for Claude. Released, draft, prepared and prepare-only inputs +refuse that selection; required-test accounting rejects missing/skipped tests. +The Python matrix contracts pass (17 tests). The authored Go native fixture has +not run: its first compile attempt was blocked before package setup by the full +task-host Go cache. This remains an authored gate, not native discovery evidence. +On integration, the shared selector/workflow delta must retain the union of the +independently authored Codex and Claude project rows and source conditions. + +## Final author verification after bounded resource recovery + +The completed Codex research binary intermediate was removed only after its +preserved binary SHA256 was reverified; all pinned provenance and actual help +outputs remain intact, and hosted jobs acquire independent official artifacts. +That recovered bounded memory-backed cache headroom without broad cache pruning +or deleting unrelated work. The guarded Claude native fixture now compiles; +the selected inert `TestClaudeDiscoveredSkills` parser contract passes (0.008s). +No native project client was executed by that compile/parser run. + +The public SDK fixture now includes stdio projection with an inert trusted +launcher and explicit persistent-data references. User/A/B each receive separate +data roots; update, exact-pin repair and removal retain all marker bytes and +original data authority. Its postcommit fault seam makes native listing +unavailable only after the package/binding commit: the outcome stays incomplete +and its owned binding does not manufacture active status. A fresh public +installer instance then repairs the recorded project pin and reobserves exact +project identity. The complete scoped SDK journey passes (0.171s). + +The final full Claude client suite, including strict project scope/path/load +errors, passes as uid 65534 with a short isolated temporary root; this preserves +both permission-denial and Unix-socket path-limit fixture behavior. Exact +changed-code lint and full-file size/architecture gates pass for the adapter, +domain, providers and installer packages, and both gates pass for the root native +fixtures. No new suppression rules were introduced. Genuine hosted project +listing/discovery results and both independent final adapter reviews remain +separate pending gates. + +## Independent R1 correction: exact native listing spelling + +R1 identified that cleaning a native-reported project path before comparison +could admit `alias/../artifact` even when resolving a symlink would reach a +different project's artifact. Project listing now requires literal installPath +equality and exact optional projectPath spelling; legacy user parsing retains +its prior normalization contract. Regressions cover both nonclean fields and a +real symlink traversal whose lexical clean path names A but physical resolution +reaches B. The authored native gate also refuses nonclean reported paths. +Focused client/provider/public-SDK contracts pass (0.007s/0.061s/0.253s), and +changed-code plus full-file client lint gates report zero issues. + +## Final independent implementation review closure + +The final adapter checkpoint `32dd485e789b57c7825c6c4fb2128263901be78f` +received independent R1 and R2 **GO** decisions. Both reviewers independently +rechecked the exact native path correction and the composed client, provider +and public SDK contracts; their reports are committed beside this evidence. +The R1 reviewer also ran the native runner's complete Python safety/evidence +suite (56 PASS), while R2 reran the selected native matrix contracts (17 PASS). +These implementation reviews close the earlier pending adapter-review gate. +Genuine hosted project observations remain pending and must be reported apart +from fixture execution, authored gate code and historical user evidence. + +## Genuine project failure and diagnostic-only checkpoint + +The actual macOS arm64 Claude2.1.263 hosted run40011624952 failed project add +with unrecognized native plugin-list output; user suites passed independently. +Artifact12528049546 ZIP SHA256 is +`c45098c244379dd4e0b62f2dfb7e235b415959428e19cd235cf083652fb5caf7`. +The project prefix has installer failure/version records but no raw list, +because add failed before its later read-only probe. Project qualification is +therefore failed/pending, not implied by prior fixture or user-suite success. + +The supplemental diagnostic plan423d912f and both independent plan GO reports +were committed before source atc53e030e. This test-only change captures at most +two fixed A/B `plugin list --json` commands after the first installer error, +with the same verified binary and fresh explicit original profile. Each has +live1MiB-per-stream caps/cancellation,10s context deadline, empty stdin, no shell +and250ms WaitDelay; raw separate streams plus bounded exit/error/hash metadata +remain artifact evidence. The original installer error and failed stage remain +unchanged. Production parser, client success criteria, opt-in/hosted guard, +acquisition and successful lifecycle paths are unchanged. No diagnostic output +can become an installed/active result. + +Two inert diagnostic contracts PASS0.007s without a Claude process; exact +changed-code lint reports zero issues after routine naming/preallocation fixes. +Genuine hosted replay and independent raw-output interpretation are required +before proposing any parser correction. The direct-child timeout/pipe bounds +are not OS isolation or general descendant sandboxing, and no model/trust/MCP +initialization or project success is claimed. + + +## Completion continuation: blocked discovery is prepared + +The independently reviewed completion plan keeps the same skills-directory +backend and original manifest name. The Linux pinned spike is preserved under +[the 2026-10-24 spike evidence](../../evidence/project-scope-native/claude-2.1.263-linux-amd64-spike-2026-10-24/README.md). +The current implementation recognizes the native suppression sentinel without +using it as ownership proof. Selected-project filesystem namespace and receipt +verification remain independent. Suppressed/disabled/user-shadowed owned installs +stay Prepared+Installed; malformed listings remain failures. Exact active project +listing is the only discovery confirmation, and no MCP workload follows from it. + +The source-only project gate is explicitly +`TestAgentpluginsClaudeNativeProjectPreparedLifecycle`; it exercises production +add/replay/update/intact repair/missing-directory repair/remove and separate fresh +native A/B listing. Its stage validator requires native skill/command discovery +to remain blocked and MCP/model use unevaluated. It cannot replace existing user +native runtime gates or certify trusted project discovery. No new hosted result +is claimed by the implementation or inert contract tests. diff --git a/docs/plans/project-install-e2e/claude-project-list-parser-refactor-plan.md b/docs/plans/project-install-e2e/claude-project-list-parser-refactor-plan.md new file mode 100644 index 000000000..5f77c6680 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-project-list-parser-refactor-plan.md @@ -0,0 +1,65 @@ +# Claude project native-list parser extraction plan + +## R1: concrete failure and bounded scope + +The genuine [architecture job121008405952 in run40139625683](https://github.com/777genius/universal-agent-plugins/actions/runs/40139625683/job/121008405952) checked merge34c85ffb (428bdf5d49e63b78bc4783df6b766c87c8e678ca into09b7650d34eb1a7876fabfd26fa2be89afe15cdf). Its raw log reports all three existing limits for clients/claude/project_observation.go: observeClaudeProjectList has72 statements (limit40), cognitive complexity57 (limit25), cyclomatic complexity43 (limit20). The complete decoded job log was fetched read-only; no local vendor process ran. The current function still has the exact reported shape. + +This is an adapter-local behavior-preserving extraction. Keep the existing private entry point, return type, state enum, native strings and every admission/refusal predicate. Do not edit lifecycle, ownership, filesystem observation, trust, recovery, generic JSON decoding, user listing, commands, harness selectors, workflows, linter configuration or budgets. Existing successful hosted source428 is evidence for the pre-extraction behavior; it is not evidence that a later extraction has already executed natively. + +## Proposed extraction + +Keep all helpers private to the Claude adapter and specific to this exact JSON format. A small projectListScan accumulator will contain the existing result, selected/other/suppressed/foreign booleans and seen-identity map; a projectListQuery value will contain only name/active/project/user, so long repeated string parameter lists are avoided. Neither type is a public abstraction or a new generic parsing framework. + +1. Extract query validation with the exact current size/name/absolute-clean-path/direct-parent predicates. Extract bounded unique whole-JSON decoding into a helper that retains UseNumber, shared.DecodeUniqueJSONValue, the trailing-token EOF requirement and the top-level array assertion. +2. Keep the entry loop in observeClaudeProjectList; reject non-object rows. Delegate each object to the scan's record method. Preserve id string/nonempty validation first, exact sentinel handling before ordinary metadata, the duplicate sentinel refusal and untrusted-note assignment. Ordinary-row validation remains before filtering an unrelated id, so unrelated malformed records still invalidate the whole listing. +3. Extract ordinary row validation (scope/enabled/path types, exact absolute path, known scope and validProjectListMetadata) into a private typed row helper. Preserve id+NUL+scope+NUL+path duplicate identity detection for every ordinary row before the same-name filter. Do not reject unrelated known local/managed rows merely because same-name skills-dir candidates permit only user/project origins. +4. Extract matching-candidate processing into its own method. Keep user direct-parent and one-user-winner rules; project direct-parent, exact active path and one-selected rules; selected-row nonempty errors/notes refusal; enabled/disabled classification; and same-name local/managed refusal. A foreign same-name project row remains foreign even if disabled and is not treated as absent. +5. Extract final aggregation with the exact existing order: selected plus any suppression/user/foreign is unknown; otherwise foreign wins, then suppression (retaining otherSource), then same-name user, otherwise the selected/absent result. Return a fresh zero observation on every refusal, rather than leaking partially accumulated untrusted/otherSource flags. Preserve the behavior of valid unknown suppression notes: suppressed without manufactured trust evidence. + +No helper should itself exceed current statement/complexity limits. Split only at these existing validation/state boundaries; do not move predicates into opaque generic expression combinators or introduce linter suppressions. The existing helper metadata and suppression validation functions need no semantic change. + +## Verification and exact acceptance + +Retain all existing TestProjectObservationSeparatesBlockedDiscoveryFromIdentity, TestProjectSuppressionRejectsMalformedNearMisses, TestProjectListingRequiresExactScopePathAndNoLoadErrors and TestProjectListingRejectsSymlinkTraversalSpelling assertions. Add one bounded adapter-level full-observation table for current mixed-row boundaries that currently assert only state: suppression+user in both orders preserves untrusted/otherSource; foreign+suppression/user permutations preserve the exact current foreign result and flags; selected combined with any competing source always returns the completely zero observation; duplicate identities and two different same-name user paths refuse; malformed unrelated rows and unrelated known local/managed rows retain their current behavior. These examples pin public observer truth without testing private helper layout. + +Run the complete small Claude adapter package inert test suite with -p=1 after coordinating the shared build slot. Then run pinned golangci-lint2.13.2 only over ./clients/claude with concurrency1, GOMEMLIMIT512MiB and a90s timeout, using the exact committed pre-extraction revision as --new-from-rev. Verify the whole modified source remains within the existing file-length guard. Do not run broad module lint or native commands locally. + +Acceptance is identical adapter outcomes for the existing and added boundary tables plus no changed-source lint findings. There is no new positive native-discovery claim. Genuine native runtime checks may run through the normal hosted PR gate after publication; any later result must identify its exact source and preserve the same Prepared/blocked limits. Obtain two independent plan rounds from Root and APM before production edits, then freeze the source and obtain both code reviews before publication. + +## Risks retained + +Extraction can accidentally defer validation until after the unrelated-id filter, silently accept duplicate unrelated rows, merge selected with a competing source, clear foreign flags, treat suppression as ownership or leak partial observations after invalid input. The named boundaries and full-result tests explicitly target these risks. No observer identity, persisted state, file authority, recovery eligibility or trust is changed by this plan. + +## R2: explicit preserved ordering and full-result facts + +Both Root and APM independently gave R1 PLAN GO on document SHA256ef752f5627ce59107730f21b6d77850bde395e8a8d8468c29b569fd8dd020088 after reading the complete current classifier. Their clarifications are binding for this extraction: + +- The exact suppression discriminator is handled before the ordinary row decoder. Ordinary metadata validation must happen before any notes/errors `[]any` access and before skipping an unrelated id. Keep the same boundedProjectStrings validation and projectSuppression schema; selected-row typed access therefore relies on the same prior validation, not an unchecked assertion introduced by a helper. +- Final foreign aggregation retains any previously recorded untrusted flag but does not set otherSource, even if a same-name user row was seen. Suppressed aggregation retains both the exact note-derived untrusted flag and the same-name user-derived otherSource. OtherSource-only is true only for the user-winning or suppressed+user results. This asymmetry is the existing return value and will not be normalized. +- Every rejection returns projectListObservation{}, including after a valid suppression or selected row. The accumulator's partially recorded flags must never escape a refusal path. Mixed-row table comparisons will compare the complete struct, not only state, in both row orders. + +The added table will explicitly include known and opaque suppression+user; foreign+known suppression; foreign+user; foreign+suppression+user; selected+suppression/user/foreign; duplicate unrelated identities; two different same-name user paths; malformed unrelated metadata; and valid unrelated local/managed rows. Each case's expected value comes from the existing contract described above. Existing path/scope/sentinel/duplicate-key/trailing-input tests remain unchanged. No new native command, trust, file or state behavior is requested. + +The exact pre-extraction architecture log has also been preserved locally at /dev/shm/uap-project-runtime-5e2d9d9f7f5b/claude-project-parser-121008405952.log (75319bytes, SHA2565df5e916efd4868771a390b6c6841bf38f10947b09b39b506366af0e1dc0cabf). Its three reported metrics remain the acceptance problem; no suppression or budget adjustment is permitted. Production edits remain blocked until Root and APM independently approve this frozen R2. + +## R2 independent approval receipts + +- **Root (`/root`) — final PLAN GO** on frozen R2 document SHA2561bafb19627ad09ffc884bfe778a8f1228c7fc85d078558e8d969ae2ed4d9ce31 after reading the complete current function and both plan rounds. Typed metadata validation, foreign/suppressed flags and completely zero refusal ordering are explicit. Authorized only the mechanical adapter-local extraction and reviewed regression/lint checks. +- **APM (`/root/apm_vercel_scope`) — final independent PLAN GO** on the same R2 document SHA256. Read the full R2 and source; confirmed metadata validation before typed slices/unrelated filtering, sentinel-first/all-row duplicates, foreign untrusted-without-otherSource asymmetry, suppressed/user flags and completely zero refusal. Full-observation permutations, unchanged boundary regressions and narrow lint satisfy the behavior-preserving scope. No source edits, build or vendor execution formed part of this plan review. + +Both approvals preceded production edits. Source still requires two independent frozen code reviews before publication. + +## Frozen implementation and focused validation + +The adapter now separates query admission, unique whole-array decoding, ordinary metadata validation, one-row accumulation, matching-candidate classification and final aggregation. Every original predicate remains at the same semantic boundary. New full-struct mixed-row cases exercise rotations/reversals (all permutations for two/three rows), including foreign's retained untrusted/no-otherSource asymmetry and zero flags after any rejection. No lifecycle/user-parser/trust/recovery/ownership/selector source changed. + +Using Go1.26.10, the exact new full-observation table first passed0.017s against original parser source1bc5b898d8085e707fb405ed0c8f84ca2454157f via a read-only Go source overlay. The final complete current Claude adapter package passed0.024s. Its first ordinary root run hit two pre-existing mode000 permission expectations because this workspace is uid0 with DAC bypass; rerunning only the test subprocess under `setpriv --bounding-set=-dac_override,-dac_read_search` exercised those assertions successfully. No tests were skipped or relaxed and no file permissions/product code were changed to accommodate the environment. + +Pinned golangci-lint2.13.2 over only ./clients/claude with concurrency1, GOMEMLIMIT512MiB/GOGC50/90s and --new-from-rev=1bc5b898d8085e707fb405ed0c8f84ca2454157f passed0issues. One initial inert test-loop intrange finding was corrected by ranging over the same slice indices; the final whole-package test and lint runs include that correction. The modified source is below the unchanged500-line guard. Raw focused logs remain at /dev/shm/uap-project-runtime-5e2d9d9f7f5b/claude-parser-{before-tests,final-tests,final-lint}.log. No native, MCP, model, trust, auth or broad lint execution occurred. Two independent frozen-source code approvals remain required before publication. + +## Final independent frozen-source review receipts + +- **Root (`/root`) — CODE GO** on exact f1f2f2fbd6dadb444c8c58782041237992aa2256, tree2413d92786df2badfdb29d44a7a197660754a620. Read the complete diff/current parser and all18 mixed-row cases. Confirmed query size/path validation, unique JSON/EOF, metadata before filtering/typed access, all-row duplicate identity, exact sentinel, same-name origins, zero selected conflicts and foreign flag asymmetry are identical. Accepted old-parser-overlay versus current complete-result evidence. No new IO, trust or native proof is introduced. +- **APM (`/root/apm_vercel_scope`) — final independent CODE GO** on the same exact f1f2f2fb source. Read the complete production diff,18-case permutation/full-struct tests and plan receipts. Confirmed sentinel-first handling, ordinary metadata before typed notes/errors and unrelated filtering, all-row identities, candidate rules, final priority and zero rejection. No unrelated metadata helpers, lifecycle or user parser changed. Accepted the old-source overlay and DAC-restricted whole-package validation as accurately separate from native evidence. This final review was source-only; no new test or vendor execution was claimed. + +Both frozen-source approvals were received before publication. The subsequent documentation checkpoint preserves those receipts and the separate genuine hosted Linux bootstrap subset; it does not modify the reviewed parser bytes. diff --git a/docs/plans/project-install-e2e/claude-trusted-profile-initialization.md b/docs/plans/project-install-e2e/claude-trusted-profile-initialization.md new file mode 100644 index 000000000..3271ce855 --- /dev/null +++ b/docs/plans/project-install-e2e/claude-trusted-profile-initialization.md @@ -0,0 +1,90 @@ +# Claude trusted project fixture: initialize before the byte baseline + +## Observed failure and primary evidence + +The native run at source `d6809d5438654ccdb47bd8d241b96b44e91515c8` +(`40187422616`) fails the new trusted fixture's whole `.claude.json` hash +guard on Windows (`121151065174`) and macOS (`121151065203`). The original +four native tests pass. The preserved macOS public-add result is already +Active + Installed; the Windows result is Prepared with explicit native +project suppression. Neither artifact contains the post-native profile, so +the actual changed fields are not yet established by those runs. + +The original Windows artifact is 416,557 bytes with SHA-256 +`dc4bcfa000362add4dbff64cfc7f1b252a8d7fa8ce261ab453097c2763dbabec`; +the macOS artifact is 402,554 bytes with SHA-256 +`c0a0f68ff1f68c30e58a78022135abee5a2e98be087af6306bfcd2476fa75e8a`. +Both exact outer archives were independently checked before inspection. + +Static inspection uses the official pinned npm package +`@anthropic-ai/claude-code-win32-x64@2.1.263`, without executing it locally. +The archive's SHA-512 is +`P1LnudAWj2ptyE0IZzCZKAe7nU7LxqlkcLdBfp8FsHlH5dp75Og4JJ6TClvfjanHn3shYW4CYN6oI1vSqZlkHw==`; +its native executable has SHA-256 +`0b35df94c1307004f07b738390bfef8dfca5e9af29aaf6517f305bf086b95b03`, +matching the matrix pin and measured native artifact. Its embedded source +shows: + +- `h$` normalizes paths and replaces Windows backslashes with forward slashes; + `G1e`, `vde` and parent trust lookup use these normalized project keys. + The failing Windows fixture authored native backslash keys instead. +- Global configuration lives at `CLAUDE_CONFIG_DIR/.claude.json` in the + production suffix. The fixture selected the correct original profile. +- `init` invokes `HCn` and `SCe`. `HCn` writes missing `firstStartTime` and + `firstStartVersion`; `SCe` writes a missing generated `machineID`. Both + return without an update once their fields exist. Native configuration + merging preserves the authored `projects` entries. + +These pinned write paths support a finite initialization experiment. They do +not prove which fields changed in either existing hosted failure, or that all +future native commands are byte stable. The corrected test must measure both. + +## Frozen change + +Only the separate trusted Claude fixture and narrow inert helper contracts +change. Production adapters, the existing untrusted fixture, containment, +environment, pins, timeouts and native command arguments remain unchanged. + +1. Author both exact canonical A/B project trust entries using + `filepath.ToSlash`, in the same original disposable `CLAUDE_CONFIG_DIR`. + Retain the authored raw profile and its hash as evidence. Do not add this + file to the whole-byte foreign map until native initialization finishes. + All existing user, A/B, settings, disabled foreign skill and cache byte + sentinels are guarded throughout initialization. +2. Before any public UAP operation, execute exactly one bounded genuine + `plugin list --json` in A and one in B through the existing capture helper. + Save raw streams and command receipts. Require a unique whole JSON array, + no managed fixture candidate and no suppression sentinel; require all + existing foreign hashes and both exact authored trust entries after each + command. A malformed/changed trust entry fails. No retries or new flags. +3. Read the resulting disposable profile as one ordinary nonsymlink regular + file with a fixed 1 MiB limit. Save the exact post-initialization bytes and + authored/post-initialization hashes, plus bounded top-level changed-key + names, as evidence. Never rewrite or normalize the native-produced file. + Add its current hash to the foreign map exactly once, before public UAP. +4. Execute one fixed second A/B empty-list control using the same strict + validators. The whole profile baseline and every existing foreign hash + must now remain byte exact. This is a finite stability assertion, not a + retry-until-stable loop. If native behavior remains unstable, fail before + claiming or attempting public installation. +5. Keep the existing public add, exact Active identity, genuine A/B listing, + details/skill bytes and remove assertions. Every subsequent operation and + observation retains the complete profile byte guard. Never restamp the + baseline after an installer or later native command. Preserve failure + evidence and the existing absence, suppression and scope checks. + +No original user profile is copied or exempted. Only the new test's authored +disposable profile undergoes native initialization before its single byte +baseline; this does not authorize production trust writes or change the +untrusted-project admission contract. + +## Validation and review + +Use inert contracts for exact authored trust-key/type/value admission, +malformed/duplicate/trailing JSON and wrong native empty-list identities. +Compile the existing opt-in native suites without executing vendor code. +Run configured changed-source lint and diff checks. Two independent plan +rounds and frozen code review precede publication. The next genuine hosted +native matrix must establish actual Windows/macOS initialization deltas, +stable second controls and complete public add/remove behavior; source-only +checks cannot qualify native discovery. diff --git a/docs/plans/project-install-e2e/client-plans-review-r1.md b/docs/plans/project-install-e2e/client-plans-review-r1.md new file mode 100644 index 000000000..6730c4480 --- /dev/null +++ b/docs/plans/project-install-e2e/client-plans-review-r1.md @@ -0,0 +1,82 @@ +# Client project plans — independent R1 review + +Reviewer: source lane. Reviewed all 14 sections of `client-plans.md` at the revised implementation base `3ea24e3014ddf6679ca86729c56f1069112b0d3a`, relevant existing adapters, the accepted core TargetContext/layout/deactivation contract, and the linked first-party docs. No native client was executed and no code was changed. + +## Verdict + +The proposed client-specific paths and the separation of native registration, component configuration, prepared sessions and remote/manual installation are substantially correct. Changes below must be applied before R2. Most are small capability/output/verification clarifications; do not turn them into new generic frameworks or second fallback implementations. + +## Shared required clarifications + +**C1 — choose one pinned lane before adapter coding.** Codex native marketplace versus component fallback, Grok plugin versus direct MCP fallback, and Devin version layouts are currently alternatives. Each PR must record the actual selected official artifact/version and one primary implementation before code. Unsupported/unobserved versions return prepared/unsupported results, preserving user behavior; do not implement two speculative lifecycle systems to make the plan look universally complete. The decision can be an initial read-only pinned native probe recorded in that client plan. This is still consistent with a project capability that supports multiple explicitly verified versions later. + +**C2 — effective configuration, not file presence, decides activation.** Each lane must identify higher-precedence project/user/admin/session sources for the exact configured keys and preserve them. If a managed project entry is shadowed, disabled or not loaded by the selected harness, status remains configured/prepared/blocked with a precise reason; neither static file verification nor `--activation-complete` may manufacture durable automatic discovery. The fixture matrix must cover the meaningful specific precedence for each client, not a generic arbitrary foreign file. Direct component adapters must reuse existing rendered command-skill projection and component support decisions when applicable rather than copying only raw `skills/` and dropping the current command behavior. + +**C3 — keep no native evidence distinct from unsupported product completion.** When artifact/account/UI evidence is unavailable, preserve the bounded fixture/config result and explicitly report missing native proof. Do not mark a real-client E2E complete from a fake executable or a generic MCP harness. Pin/record the package and exact help/list schema used for adapter decisions; no model task, trust write, source upload or credential creation is required to obtain a truthful partial observation. + +## Per-client ledger and findings + +| Client | R1 result | Required changes / exact review focus | +|---|---|---| +| Claude | GO after shared C2/C3 clarification | The documented `name@skills-dir` project listing and manifest-version contract supports this narrow approach. Preserve admin allow/block policy and scope-specific disabled entries. No extra client-specific architecture finding. | +| Codex | Fix C1 | Freeze native repo-marketplace **or** component implementation after pinned actual CLI inspection. The changelog's repo source does not prove that the currently installed CLI isolates installed/enable identities. If native proof fails, conservative component/prepared behavior is acceptable; do not add a speculative global-cache management path. | +| Cursor | GO after shared C2/C3 | Project `.cursor/skills` + `.cursor/mcp.json` component path is a bounded choice. Keep UI installed plugins separate; selected official editor/agent evidence must identify actual project components. No guessed project plugin registry or extra Stop/hook claim. | +| Gemini | GO after shared C2/C3 | Existing plan already tests `.agents/skills` precedence. Make the higher-precedence same-name alias produce shadowed/inactive observation rather than successful managed activation; preserve disabled workspace settings without `--consent` autoenable. No additional architecture finding. | +| OpenCode | Fix C2 concretely | Add actual effective-config observation (e.g. pinned `opencode debug config`) and the high-precedence inline/managed/custom-directory override case. Current official config docs distinguish standard project precedence from inline/admin overrides; writing root `opencode.json[c]` alone cannot prove the selected MCP is effective. Preserve JSON/JSONC selection and rooted ancestor search. | +| Copilot CLI | Add format-preserving project MCP contract | Official per-repository MCP accepts both `{mcpServers:{...}}` and a bare server map; same-directory `.mcp.json` wins and closer ancestor files override. Declare support for both existing formats through one codec or fail safely before mutation for a bare map; never silently wrap a bare document and lose foreign servers. Remove project entries by exact transactional key/path, not `copilot mcp remove`, which docs say edits user config. Existing nested override/trust boundaries are correct. | +| Kiro | GO after shared C2/C3 | Keep IDE/CLI surface qualification separate. Agent overrides/include requirements must appear in readiness output; no agent permission/config rewrites. Existing workspace path, same-name/agent-override and trust tests are appropriate. No extra architecture finding. | +| Grok Build | Fix C1 and C2 concretely | Choose project plugin directory as primary only after pinned list/source observation. Test cross-read Claude-compatible plugin/skill same-name sources too: official Grok docs read Claude conventions in addition to `.grok`, so A/B/user `.grok` alone does not exhaust native collisions. Do not implement a second direct-TOML fallback speculatively; return a precise unsupported/prepared MCP result until separately qualified. | +| Qoder | Add retained-native-data command contract | All project mutating native commands need `--scope project`, including replacement uninstall and remove. Use documented `--keep-data` on project uninstall/reinstall whenever data retention is promised. Existing user adapter uninstalls without it; keep unrelated user behavior untouched and explicitly qualify project native/UAP data markers. Listing has no documented scope flag; inspect returned scope/source from fixed cwd rather than inventing `list --scope`. | +| Cline | Make session preparation non-durable and safe launch explicit | `CLINE_MCP_SETTINGS_PATH` selects a process configuration; the ordinary IDE/global path is not project automatic discovery. Prepared MCP remains prepared after a one-session attestation/restart and must display launch instructions again. Do not copy global credentials. The current CLI defaults a prompt/act invocation to autoapproval, so proof must use inert documented subcommands or a launch action that explicitly disables autoapproval; generic `cline --cwd A` cannot be the unattended MCP proof. | +| Windsurf / Devin | Fix C1 and C2 concretely | Record the actual selected harness/version and canonical version source for the v3000.3/Local3.6 transition; the currently opened canonical MCP page documents dedicated files but does not itself state those version numbers. Test `.devin/mcp_config.local.json` shadowing the project file, and never invoke an unscoped `devin mcp remove` that defaults to local while intending project removal. Unknown/legacy Cascade remains bounded, no editor-install inference. | +| Kimi Code | GO after current-doc correction below | Current Kimi Code supports native project `.kimi-code/mcp.json` and `.kimi-code/skills/`; preserve trust, exact owned config and project-over-user precedence. Nearest `.git` skill root mismatch requires a gate. Legacy `.kimi`/`--mcp-config-file` instructions are superseded and must not reach code. See current-doc re-verification below. | +| ChatGPT | Fix saved-admission/preparation distinction | Core excludes remote-only ChatGPT from dependency install admission. Do not add native `ScopeProject` or fake NativeRegistryLayout merely to satisfy the shared project gate. Keep supported project preparation on the existing explicit/manual path (normally `--no-save`) and return unsupported for saved dependency sync/install. The common add/sync/update proof matrix must mark these operations unsupported/preparation-only, not claim a full project lifecycle. No personal/workspace account mutation. | +| VS Code | GO after shared C2/C3 and explicit single-folder limit | Local-harness `.vscode/mcp.json` `servers` is supported compatibility behavior; portable `.mcp.json` is separate experimental behavior. State that first project qualification is a single selected filesystem folder, or explicitly reject multi-root/remote workspace contexts until their authority is qualified. A `code --version`/Copilot listing/fake host must not qualify Local UI discovery. Preserve frozen hooks and unrelated user behavior. | + +## Concrete source seams + +Source lane introduces typed URL Git and archive pins but no new project paths in manifest/lock/source identity. Client project tests can use local source fixtures for most cases; one qualified project scenario must later exercise a generic remote/archive lock through the selected App to catch accidental user-runtime reconstruction. No transport-by-14-client cross product is needed. + +Same-tree shared store entries can carry another source's historical metadata. All client requests must consume provenance rebuilt by source core from the selected validated lock; never derive native identity/repair coordinates from first-writer store metadata or the physical archive wrapper. This is already a fixed source-plan R1 invariant. + +## Primary references opened during this review + +* Claude plugin command/list/skills-dir contract: https://code.claude.com/docs/en/plugins/cli-reference +* Codex repo plugin changelog: https://developers.openai.com/codex/changelog (currently redirects to https://learn.chatgpt.com/docs/changelog) +* Cursor native/project UI distinction: https://cursor.com/docs/plugins +* Gemini skill precedence: https://geminicli.com/docs/cli/skills/ +* OpenCode config merge/precedence and `debug config`: https://opencode.ai/docs/config/ +* Copilot native repo formats, ancestor precedence and user-only `mcp remove`: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers +* Kiro workspace MCP: https://kiro.dev/docs/mcp/configuration/ +* Grok `.grok` and Claude-compatible discovery: https://docs.x.ai/build/features/skills-plugins-marketplaces +* Qoder scope/list/uninstall `--keep-data`: https://docs.qoder.com/cli/plugins +* Cline project paths and current CLI autoapproval defaults: https://docs.cline.bot/getting-started/config and https://docs.cline.bot/cli/cli-reference +* Devin dedicated project/local MCP config and default local scope: https://docs.devin.ai/cli/extensibility/mcp/configuration +* Kimi nearest Git root and overriding extra skill flag: https://moonshotai.github.io/kimi-cli/en/customization/skills.html ; session MCP: https://moonshotai.github.io/kimi-cli/en/customization/mcp.html +* ChatGPT account/workspace plugin boundary: https://help.openai.com/en/articles/20001256-plugins-in-chatgpt +* VS Code Local versus portable MCP destination: https://code.visualstudio.com/docs/agent-customization/mcp-servers + +All source facts above are paraphrased narrowly; no native runtime proof is implied by documentation. Author applies fixes to each client section and ledger, reviewer verifies the revision, then project_core performs independent R2. Root owns implementation authorization. + +## Applied-fix verification + +Verified the author's stable revised `client-plans.md` on 2026-10-23. All shared C1–C3 and per-client findings above are explicitly addressed: one Codex component/Grok plugin/current Devin lane; effective configuration and rendered command skills; Copilot formats/exact removal; Qoder scoped retained-data commands; Cline explicit autoapproval-off action and inert proof; Kimi root/session gates; ChatGPT preparation outside saved admission; single-folder Local VS Code qualification. **R1 GO for all 14 client plans.** Their actual pinned native artifact/help observation remains the stated pre-coding qualification gate. Independent R2 and root authorization remain required. + + +## Kimi current-documentation correction and R1 re-verification + +The archived `kimi-cli` documentation used during initial R1 describes a different product generation. It must not define the current adapter. Re-opened the current official `kimi-code` MCP, skills, plugins and command reference and verified the author's stable corrected section 12, matrix and common lifecycle exceptions. + +* Current native project MCP uses `.kimi-code/mcp.json`; user MCP uses `~/.kimi-code/mcp.json`/KIMI_CODE_HOME. Project definitions override same-name user entries, but folder trust is required; headless untrusted runs do not load them. Do not grant trust or use KIMI_CODE_TRUST_WORKSPACE to force evidence. +* Project skills use `.kimi-code/skills` plus `.agents/skills`, based on nearest `.git` ancestor. Reject/report a selected-root mismatch; --skills-dir replaces auto-discovery and is not a silent fix. +* Plugin installation remains user-only; project component delivery does not install a project plugin or mutate user plugins/managed. Existing UAP `.kimi-code` user-root handling does not need an unrelated legacy migration. +* Deletion verifies the exact owned project config entry and fresh-session absence. Existing sessions retain removed entries/tools with a removal notice, so absence from that old list is not the success gate. +* Legacy --mcp-config-file/session-preparation language is removed for current Kimi Code; Cline alone retains that session-selected MCP exception. No archived no-argument launcher, model task, --auto/--yolo/prompt automatic approval or trust override supplies native proof. + +**Kimi R1 re-verification GO.** This supersedes earlier Kimi session-preparation review wording. Independent R2 and the parent code-authorization gate remain required. + +Current primary references: https://moonshotai.github.io/kimi-code/en/customization/mcp ; https://moonshotai.github.io/kimi-code/en/customization/skills ; https://moonshotai.github.io/kimi-code/en/customization/plugins ; https://moonshotai.github.io/kimi-code/en/reference/kimi-command . + +## Scoped lifecycle supplement verification + +Verified the final new shared/client clauses after core R2: one core-owned scoped effective-policy helper covers the existing provider native-kernel gate, lifecycle/attempt persistence, projection/readiness, recovery/repair/remove, physical backend/sibling ownership, validation and data receipts. Its project policy has no fallback to user CLI traits, and user behavior remains unchanged. The Copilot/VS Code independent MCP backend test plus unresolved native-attempt crash test cover the concrete risks. Cline's existing ActivationPreflighter runs before the composed generic attestation branch, so its session-only MCP cannot be falsely promoted by a flag. ChatGPT now truthfully rejects all project requests because no accepted preparation artifact route exists. VS Code uses a new project-only adapter/constructor, leaving New/NewLocal and user saved admission unchanged. **R1 supplement GO; no additional findings.** diff --git a/docs/plans/project-install-e2e/client-plans-review-r2.md b/docs/plans/project-install-e2e/client-plans-review-r2.md new file mode 100644 index 000000000..4cb9bf2d0 --- /dev/null +++ b/docs/plans/project-install-e2e/client-plans-review-r2.md @@ -0,0 +1,176 @@ +# Client project plans — independent R2 review + +Reviewer: project-core lane. Reviewed the post-R1 client plan against +`3ea24e3014ddf6679ca86729c56f1069112b0d3a`, the accepted project core contract, +current registry composition, generic activation/attestation dispatch, selected +delivery traits, native-attempt recovery, and backend ownership selection. +Opened current first-party Kimi Code, Cline CLI and VS Code documentation. +No native runtime proof or implementation occurred. + +Verdict: **narrow shared integration fixes required**. Most individual path and +client-lane choices are GO. These findings address existing code that runs +before/around an adapter; a project branch inside an adapter alone cannot fix +them. Coordinate the small shared changes with core rather than introducing a +new plugin engine or universal client framework. + +## R2-1 — Cline session preparation must reach the adapter under attestation + +`providers.Activator.Activate` currently accepts `ActivationComplete` before +dispatch whenever `AutomaticallyActivates` is false, immediately returning +active/installation-verified. Cline's existing native-config adapter has no +automatic CLI activation, so the proposed adapter-only "remain prepared after +attestation" policy would never run. This also affects effective-config +shadowing observations in any project lane that uses this generic shortcut. + +Use the existing `ActivationPreflighter`, which the composed provider invokes +before the shortcut: Cline rejects `ActivationComplete` for selected project +session MCP, retaining the prepared state and explicit launch action. Cover +direct adapter reuse too. Project lanes with known shadowed/disabled evidence +likewise refuse positive attestation through their preflight rather than let +generic completion override that negative. Preserve existing user attestation. +No new generic policy interface is needed. Do not invent a persistent session +registration or set an automatic-activation predicate merely to dodge the gate. + +Required regression: Cline project MCP is prepared; repeat saved add with +`--activation-complete` refuses with a precise session-only explanation; status +and fresh sync/restart still report prepared and the launch action. A known +shadowed/disabled project configuration must likewise +remain blocked instead of becoming active from the shortcut. + +## R2-2 — project component delivery needs its actual lifecycle/recovery policy + +Codex, Cursor, Copilot and VS Code retain user CLI-registry traits while their +proposed project lane directly edits native component configuration. Existing +generic policy reads `SelectedDelivery.EffectiveTraits`; zero SelectedDelivery +falls back to user ClientTraits. `nativeLifecycleClient` uses these facts to +write an attempt marker **before** native mutation. A new project branch can +therefore write project settings without the native-config attempt/recovery +guard if it only changes paths. Existing selected modes are closed records for +specific VS Code Local profile and Cursor user Stop contracts; they are not +generic project component authority. + +Record one small scope-aware effective-policy route shared with core, selected +from the actual project delivery and persisted scope. Direct project component +lanes must use native-config attempt/ownership/recovery behavior; real project +CLI plugin lanes retain their actual CLI policy. Preserve all existing user +traits. Do not relabel a workspace as an existing user-profile selected mode, +fake ConfigRoot, globally change the client's traits, or add one custom engine +per adapter. The exact helper/field choice can stay internal, but identify its +owner and consumers before client implementation. + +Required contract: for one CLI-user/native-project adapter, interrupt after a +native project edit and before acknowledgement. A retry must observe the +recorded unresolved attempt and recover/refuse safely; it must not blindly +rewrite project state or fall into user plugin listing/uninstall. + +## R2-3 — VS Code composition, saved admission and physical sibling ownership + +The production registry contains `vscode.New()`. Its legacy user behavior shares +the Copilot CLI backend. `vscode.NewLocal` is a different, explicitly configured +profile-registration contract with profile settings/qualified tuple and +`chat.plugins.paths` ownership; it does not mean `.vscode/mcp.json` workspace +component delivery. Also, `SavedDependenciesDeferred` currently rejects VS Code +before checking the declared scope. Merely adding ScopeProject and project +methods will either remain unreachable or accidentally alter frozen user +qualification. + +Specify the production composition path for one explicit local-folder Local +harness project adapter. It may be a project branch in the current registered +adapter, preserving the default user branch; do not globally replace it with +NewLocal or mutate the singleton registry. Make saved qualification scoped: +the newly qualified project lane can pass while user dependency qualification +remains deferred. This is the already authorized narrow owner exception. + +The current zero-mode `SharesBackend`/sibling lookup also assumes Copilot and +VS Code share one physical install. Project Copilot `.mcp.json` and VS Code +`.vscode/mcp.json` are distinct registries. Fence project physical ownership by +the actual selected layout/context so project operations cannot adopt/remove a +sibling's registration merely because the user backend family matches. Preserve +user backend sharing. Actual overlapping skill paths still use exact existing +collision/ownership rules; do not auto-adopt them. + +Required tests: production CLI composition admits project VS Code, keeps its +user saved target deferred, and avoids Copilot global commands. In one project, +create distinct Copilot and VS Code project MCP entries; maintenance/removal of +one preserves the other's entry and receipt. No extra native host is qualified +by this fixture. + +## R2-4 — ChatGPT artifact preparation has no executable project-scope route yet + +The plan correctly forbids native ScopeProject, fake NativeRegistryLayout and +saved dependency admission for remote ChatGPT. It simultaneously proposes +`--scope project --no-save` preparation under `.uap/`. The accepted common scope +gate and planner reject that request before acquisition/layout. `--no-save` +alone does not bypass the native scope contract, and routing it through user +scope with a fabricated project profile would violate core invariants. + +Choose a concrete bounded result before coding. The smallest option is to +return explicit unsupported for every native `--scope project` ChatGPT install, +retain the existing user/manual preparation route unchanged, and describe any +project-local artifact preparation as deferred. If project-local artifacts are +required now, name one reviewed artifact-only entry path outside native saved +admission, using existing preparation implementation, without inventing a +native binding or second engine. Coordinate that one exception with core; +do not leave an unreachable promised branch in the plan. + +Required test: saved and unsupported native project requests make zero +acquisition/remote/native effects, while any explicitly supported artifact-only +path has a real entry point and cannot mutate account/workspace state. + +## Current Kimi correction accepted + +Independently opened current first-party Kimi Code MCP and skills docs. They +support project `.kimi-code/mcp.json` and `.kimi-code/skills/`, with project MCP +overriding same-name user entries and workspace trust controlling activation. +The revised current-component lane replaces the archived CLI session-file +assumption appropriately. Keep the pinned official-artifact gate, nearest-Git +skill-root check, exact fresh-session removal observation and no auto-trust. +Project SSE or other newly documented transport support must be explicitly +qualified; current docs alone must not silently widen user capability policy. + +Primary references: + +* https://moonshotai.github.io/kimi-code/en/customization/mcp +* https://moonshotai.github.io/kimi-code/en/customization/skills +* https://docs.cline.bot/cli/cli-reference +* https://code.visualstudio.com/docs/agent-customization/mcp-servers + +## Per-client R2 ledger + +| Client | R2 decision | Concrete integration focus | +| --- | --- | --- | +| Claude | GO | Exact project skills-dir identity/cwd; separate user profile; scope-aware removal. | +| Codex | GO after R2-2 | One component lane; actual native-config attempts/recovery; no global marketplace action. | +| Cursor | GO after R2-1/R2-2 | Component policy and truthful blocked/manual state reach adapter rather than generic attestation. | +| Gemini | GO after shared R2-1 | Existing native-config lifecycle is suitable; shadowing/disabled observations cannot be attested away. | +| OpenCode | GO after shared R2-1 | Exact project layout and effective-config observation; use existing native transitions. | +| Copilot CLI | GO after R2-2/R2-3 | Native component recovery plus distinct project registry ownership; preserve bare/nested document format. | +| Kiro | GO | Workspace/agent overrides and prepared scope are explicit; qualify IDE/CLI separately. | +| Grok Build | GO | One observed project-directory plugin lane; Claude-compatible collisions and no global trust command. | +| Qoder | GO | Explicit project install/uninstall, keep-data and exact scoped source/version listing. | +| Cline | GO after R2-1 | Session MCP remains prepared under completion attestation and restart. | +| Windsurf / Devin | GO after shared R2-1 | One pinned newer harness; actual effective dedicated project config and shadowing. | +| Kimi Code | GO after independent current-doc R1 refresh and shared R2-1 | Current native components accepted; no archived CLI or automatic trust. | +| ChatGPT | Fix R2-4 | Choose reachable artifact-only preparation or explicit project unsupported result. | +| VS Code | Fix R2-2/R2-3 | Explicit production project composition, scoped saved qualification and independent physical ownership. | + +No style-only changes requested. Author updates shared/client sections and the +ledger; reviewer verifies the fixes. Parent owns implementation authorization +and later code/native/release qualification gates. + +## Applied-fix verification — 2026-10-23 + +Verified the revised client plan contains: existing activation preflight guards +before generic completion (Cline session-only and known negative project +evidence); one core-owned declarative scoped lifecycle/backend policy and its +attempt/recovery/group/data consumers; explicit VS Code project-only product +composition and qualification while preserving user/NewLocal behavior; distinct +project Copilot/VS Code physical ownership; and explicit unsupported native +ChatGPT project requests for every intent including `--no-save`. Current Kimi +Code project components were independently checked against current official +docs, and the source reviewer refreshed its R1. + +**All 14 client plans: R2 GO.** All R2 findings are closed at the plan level. +Actual pinned-artifact/help observation remains each client's stated pre-coding +gate, followed by code review and truthful native/configuration E2E. This review +does not assert implementation or runtime completion. diff --git a/docs/plans/project-install-e2e/client-plans.md b/docs/plans/project-install-e2e/client-plans.md new file mode 100644 index 000000000..1aceb3ec5 --- /dev/null +++ b/docs/plans/project-install-e2e/client-plans.md @@ -0,0 +1,636 @@ +# Project installation: separate plans for all 14 supported clients + +Status: **both independent plan review/fix rounds GO; implementation is +not authorized by this document alone**. Prepared 2026-10-23 against source +`2bf6c3d703640ffbecc14346996709b4aec36ddc`; parent refreshed the implementation +base to `3ea24e3014ddf6679ca86729c56f1069112b0d3a`. Recheck every touched source +against that base before coding. Every client is a separate PR stacked on the +project core PR, with incremental reviewed commits. No native-client proof was +executed while preparing these plans. PATH inventory found only `code`; that +alone is not client identification or compatibility evidence. + +## Current delivery supersedes the original classification + +The table and individual proposals below are the original reviewed plans, not +an assertion of today's delivered capabilities. Subsequent pinned observations +changed three decisions: Qoder now exposes an explicit Prepared skills-only +manual import view with a separate native global cache; Grok project preparation +withholds MCP; VS Code requires its shipped-workbench observation and a separately +reviewed adapter. Preserve those historical proposals when interpreting reviews. +Use [current project support](../../PROJECT_CLIENT_SUPPORT.md) and the later +client evidence/amendments for the implemented contract. + +## Governing decisions and shared contract + +Read repository AGENTS, capability preservation, and active owner decisions. +The latest explicit owner request covers all supported agents; parent records +the narrow exception for **VS Code project-scope installation** while retaining +the unrelated VS Code freeze. All workers/reviewers use the owner-selected +GPT-6.1 high/xhigh. Preserve user behavior, existing useful source/tests, one +standard authoring engine, and historical release evidence. + +Use core's validated `TargetContext` (scope, project root, original user config +root), `ProjectTargetLayout`, project-aware native registry selection, and +context propagation through activation **and deactivation**. Never encode a +project as a fake user profile, change HOME for product behavior, use an +unrelated editor executable as a CLI, or infer project authority from a package +path. Client stacks add project scope to domain capabilities only when the +implemented capability is truthful. Package support is separate from native +discovery and runtime verification. + +Each stack reuses the existing projector, exact native-config kernel, +transaction/receipt, immutable package and security gates. UAP mutable state, +package data, and prepared packages are project-local under core's `.uap/`; +host-discovered skills/configuration use the documented paths below. Native +object receipts own precise entries/directories, never entire shared config +files. Refuse foreign names, disabled entries, unsafe aliases and drift rather +than overwrite them. Direct JSON/JSONC/TOML edits preserve foreign data and +comments through a suitable existing codec; add only a narrowly needed codec. + +For every adapter, observe the effective selected configuration and meaningful +higher-priority admin/user/project/session sources. A shadowed, disabled or +unloaded entry stays configured/prepared/blocked with a specific reason; +neither a file receipt nor operator attestation manufactures durable native +discovery. Reuse existing rendered command-skill projection and supported +component selection, including command behavior, rather than copying raw +`skills/` alone. Record pinned official artifact/version and inspected help/list +schema before each adapter implementation. Where a version/harness is not +observed, return prepared/unsupported instead of implementing speculative +fallback lifecycle systems. Initial read-only native probes do not authorize +model tasks, account mutation, auto-trust, credential creation or scanner upload. +For any known shadowed/disabled project result, use the existing activation +preflight before generic attestation to refuse `ActivationComplete` as a bypass; +preserve user attestation behavior. Composed-provider regression coverage must +prove that a positive flag cannot override this recognized negative evidence. + +### Core coordination required for project lifecycle policy + +Core owns one narrow scoped effective-policy helper before client implementation. +Current `SelectedDelivery.EffectiveTraits` and zero-mode backend sharing default +to each client's user traits. Codex/Cursor/Copilot/VS Code user CLI-registry +traits therefore cannot govern project direct-config delivery. Add declarative +project policy to the existing client definitions (qualified client stacks fill +their rows) and a scoped helper selecting it from validated target context and +the existing persisted binding scope. User scope retains the exact existing +selected-delivery/default traits. Project scope has no fallback to user policy +or an old TEST user-profile selected mode. + +Direct project component lanes select native-config lifecycle/attempt/ownership +and recovery policy; actual CLI plugin lanes retain their real CLI policy. +The helper must govern `providers` native-kernel admission, usecase +`nativeLifecycleClient`, pre-mutation `NativeActivationAttempt`, native +projection/readiness, repair/remove/recovery, and installer binding/profile +validation. It also governs physical backend selection, group collision/adoption, +and data receipts. Core updates these shared consumers once; each client does +not introduce a separate engine or policy branch. Operational policy/root data +does not enter portable manifest/lock/source identity or public path output. + +For project Copilot and VS Code, declare separate physical backend/registry +identity because root `.mcp.json` and `.vscode/mcp.json` are distinct. Scoped +backend/sibling helpers must fence adoption/removal by actual context/layout; +zero-mode `SharesBackend` must not borrow the user's Copilot/VS Code alias. +Preserve current user backend sharing. Real overlapping skill paths retain +exact collision/ownership checks and cannot be silently adopted. + +Required core contracts: interrupt a CLI-user/native-config-project lane after +its native edit and before acknowledgment; the persisted unresolved attempt +must force safe recovery/refusal rather than blind rewrite or user CLI fallback. +In one project install distinct Copilot and VS Code MCP entries, then maintain +or remove one while preserving the other's entries and receipts; repeat a user +case to prove existing shared-backend behavior unchanged. Production project +composition, scope-aware saved admission, and configured/attestation boundaries +must be tested through the real provider path, not only direct adapter calls. + +## Delivery classification + +| Client | Project skills | Project MCP | Preferred first implementation | +| --- | --- | --- | --- | +| Claude | Native skills-directory plugin | Bundled native plugin MCP | Existing package layout under project `.claude/skills/`, exact project listing | +| Codex | `.agents/skills/` | `.codex/config.toml` | Project component projection; repo marketplace remains a separate deferred qualification | +| Cursor | `.cursor/skills/` | `.cursor/mcp.json` | Native project component projection; UI plugin installation remains separate/manual | +| Gemini | `.gemini/skills/` | `.gemini/settings.json` | Native project component projection; no project extension-registry claim | +| OpenCode | `.opencode/skills/` | Root `opencode.json[c]` | Native project component projection with existing transactional ownership | +| Copilot CLI | `.github/skills/` | Root `.mcp.json` or `.github/mcp.json` | Native project component projection, not global plugin install | +| Kiro | `.kiro/skills/` | `.kiro/settings/mcp.json` | Native project component projection, IDE/CLI version gated | +| Grok Build | Project plugin skills | Bundled project plugin MCP | One pinned project plugin-directory lane; unobserved support stays prepared/unsupported | +| Qoder CLI | Native project plugin | Native project plugin | Native `plugins install --scope project` and scoped removal | +| Cline | `.cline/skills/` | Explicit session-selected file only | Native project skills; MCP prepared with documented `CLINE_MCP_SETTINGS_PATH` launch action | +| Windsurf | `.devin/skills/` | Current Devin Local dedicated project MCP config | One pinned newer Devin Local lane; older/unknown/Cascade project MCP unsupported | +| Kimi Code | `.kimi-code/skills/` | `.kimi-code/mcp.json` | Current native project components; legacy kimi-cli and project plugin registry excluded | +| ChatGPT | No native project install | No native project install | Explicit pre-acquisition unsupported for project requests; existing user manual preparation preserved | +| VS Code | Supported workspace skill paths | `.vscode/mcp.json` for Local harness | Project Local-harness components; keep Copilot CLI and experimental portable config distinct | + +## Common validation required in every client PR + +**Contract/fixture coverage:** use the public installer and production CLI +composition with disposable user profile and projects A/B. Exercise add, +unchanged sync, moving-ref update, exact-pin repair after drift, status, +interrupted operation/recovery, and remove. Assert that the selected native +paths, cwd, scope flags, receipts and data refer to A; B and user files remain +byte-identical. Verify repeat removal, foreign same-name/disabled entries, +malformed configuration, symlink escape, changed ambient cwd/home, copied +mutable state, and unsupported/mixed component selections. A failed native +operation must retain uncertain/pending ownership until exact verification or +the existing explicit completion contract resolves it. Fixtures do not prove +that a real client discovers or executes a component. + +**Native evidence:** acquire a pinned official binary/package in the existing +disposable-host test policy; record source URL, version, artifact integrity, +binary digest, platform, command/cwd/config scope, and sanitized observations. +Use a local harmless marker MCP fixture and a distinctive skill with recorded +server handshake/tool result and real client discovery. Prove fresh restart +behavior, A/B/user precedence, targeted update/repair/removal and no residual +managed registration. Do not invoke model tasks, grant trust, add credentials, +use YOLO/AFK or upload source to scanners to make a test pass. If native +inspection needs login/UI or an unavailable official artifact, report that +boundary explicitly: configuration/fixture proof can pass while runtime proof +is pending. Native installed listing, native discovery, MCP initialization, +tool invocation, authentication and release qualification are separate facts. + +The current native matrix provisions only Claude/Codex/OpenCode. Extend its +pin/evidence plumbing only for a client actually being tested; use separate +manual UI evidence for editor-only behavior. A third-party imitation binary, +shell script fixture or locally authored parser never counts as that client. + +The common saved lifecycle applies only to admitted saved dependencies. +ChatGPT is excluded: all project requests, including `--no-save`, fail before +acquisition with its precise unsupported reason; existing user preparation stays +unchanged. Cline +session MCP preparation remains prepared across restart and any one-session +attestation; display the explicit launch action again. Native project skills +can be installed independently. Do not persist a session observation as a +durable host registration. One qualified project source-seam test later consumes +a generic Git/archive lock through the selected App; the 14 clients do not need +a transport-by-client cross product. + +## 1. Claude Code — native project skills-directory plugin + +**Evidence/choice.** Official plugin list documents skills-directory plugins +with `id=name@skills-dir`, `scope=user|project`, `enabled`, and `installPath`. +This matches existing UAP's user layout and avoids a new marketplace/cache +lifecycle. Prefer `/.claude/skills/` with existing projected +plugin/MCP layout; use project cwd for all native list/probes. Official +marketplace install `--scope project` is an alternative only if this directory +contract is unavailable in the pinned client, and must be separately qualified. +Sources: [CLI reference](https://code.claude.com/docs/en/plugins/cli-reference), +[install scopes](https://code.claude.com/docs/en/plugins/install). + +**Changes.** `clients/claude/{plan,project,identity,list,probe,lifecycle}.go` and +dedicated tests: add project layout/registry selection, preserve staging outside +the watched `skills` directory, parameterize exact listing scope without +weakening user-only callers, and pass immutable project cwd/context. Keep user +`CLAUDE_CONFIG_DIR` semantics; never set it to the project. Scope-aware +deactivation verifies exact project identity before owned-directory removal. + +**Proof/gates.** Real `claude plugin list --json` must find project identity at +the exact path, reject same-name user/path collisions, and stop listing it after +removal. A harmless skill and MCP runtime fixture prove discovery separately. +Project trust/security policy remains a user action; admin allow/block and +scope-specific disabled entries remain authoritative. Unknown listing versions +stay manual/unverified. Test worktree and nested cwd behavior, because plugin +marketplace relative paths and skills-directory discovery can differ. + +## 2. Codex — native project components + +**Evidence/choice.** Choose one first implementation: native project skills in +`.agents/skills/` and MCP in trusted-project `.codex/config.toml`. Official +changelog also documents repo marketplaces, but cache/install scope isolation +is a separate qualification and will not be implemented speculatively here. +Sources: [skills](https://developers.openai.com/codex/build-skills), +[MCP](https://developers.openai.com/codex/mcp), +[project config](https://developers.openai.com/codex/config-file/config-advanced). + +**Changes.** `clients/codex/{plan,project,identity,lifecycle,lifecycle_deactivate, +command,profile,list}.go` only as needed plus scoped tests. Keep current user +CLI marketplace path intact. Inspect pinned native project config/help first; +write exact project MCP TOML entries and skill objects transactionally with +foreign members/comments preserved. All CLI commands preserve selected user +`CODEX_HOME` and set `Dir=ProjectRoot`. Project operations do not invoke user-only +`plugin add`, marketplace remove or global-cache cleanup. + +**Limits.** Unknown/unobserved versions stay prepared/unsupported for components +not proved; no second plugin lifecycle fallback. Exact TOML member names and +skill collision behavior must come from pinned native help/source/probe, not +guessed from removed documentation routes. Repo/user/admin skills with the same +name may coexist in selectors; do not claim the managed one won by filename. +No auto-trust writes. + +**Proof.** Native A/B/user different versions, restart skill discovery, effective +MCP config and scoped entry removal prove component isolation. This makes no +installed-plugin isolation claim. Fixture config/command contracts remain +separate from genuine `codex` proof. + +## 3. Cursor — project components, plugin UI remains separate + +**Evidence/choice.** Official docs support `.cursor/mcp.json` and CLI/editor +shared MCP configuration. Plugin UI offers project/user scope but documents no +stable unattended project installer. Project skill discovery uses +`.cursor/skills/`; verify with the selected actual editor/agent version. +Sources: [MCP](https://cursor.com/docs/mcp), +[CLI MCP](https://cursor.com/docs/cli/mcp), +[plugins](https://cursor.com/docs/plugins). + +**Changes.** `clients/cursor/{plan,profile,project,identity,lifecycle}.go`, README +and new dedicated project tests. Retain existing manual editor package contract +for user scope. Implement owned project skills and exact `.cursor/mcp.json` +members, with existing managed stdio launcher and project data. Use project +registry authority and a matching codec; never guess a `.cursor/plugins/local` +project registry, use editor `--user-data-dir` as HOME, or invoke ambiguous +`agent` as a detected CLI. Component configuration can be installed while +native activation remains manual until observed. + +**Proof.** Fixture E2E checks merge/no-op/update/remove, foreign disabled entries, +user hooks/config unchanged, and editor/agent-only detection. Real pinned +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 +are `.gemini/skills/` or `.agents/skills/`. Extensions use a separate installed +extension lifecycle, so project install projects components instead of inventing +a project extension registry. Sources: +[configuration](https://geminicli.com/docs/reference/configuration/), +[skills](https://geminicli.com/docs/cli/skills/), +[extension reference](https://geminicli.com/docs/extensions/reference/). + +**Changes.** `clients/gemini/{plan,project,identity,native_apply,native_mcp, +native_skills,native_objects,profile}.go` and dedicated project tests. Resolve +actual project `.gemini` root from context; preserve detected user root and +GEMINI_CLI_HOME as user-profile authority. Reuse exact settings MCP codec, +skill ownership, rollback and no-op receipts. Keep package projection under +`.uap/` distinct from the native project settings/skills tree. + +**Proof/limits.** Native `gemini mcp list` and skills list from A plus fresh +restart must observe selected project components; B and user remain unchanged. +Test alias precedence when `.agents/skills` has a foreign same-name skill; +report the managed skill shadowed/inactive rather than successfully activated. +Preserve disabled workspace settings without consent/autoenable. Also test +malformed/commented settings, conflicting user server and trust gating. A +settings-file receipt proves configuration, not initialized MCP/tools or +authentication. No extension install/global profile redirection is needed. + +**Implemented checkpoint (2026-10-23).** `65b8f17a` qualifies the native +project component lane with independent implementation +[R1](gemini-code-review-r1.md) and [R2](gemini-code-review-r2.md) GO. Project +configuration success remains prepared with installed exact objects. Native +session discovery, workspace trust, system policy and MCP initialization remain +unobserved; activation attestation cannot upgrade that status. Bounded +user/workspace restriction and alias warnings preserve policy and foreign +files. [Pinned evidence and verification](gemini-implementation-evidence.md) +separate actual version/help research, production composition contracts and +remaining hosted native runtime qualification. + +## 5. OpenCode — project config at repository root + +**Evidence/choice.** Native project MCP uses `/opencode.json` or +`opencode.jsonc`; skills use `.opencode/skills/`. These roots are different, +so a project cannot be represented by substituting one fake config root into +the user adapter. Sources: [config](https://opencode.ai/docs/config/), +[skills](https://opencode.ai/docs/skills/), +[MCP](https://opencode.ai/docs/mcp-servers/). + +**Changes.** `clients/opencode/{plan,native*,identity,profile,project}.go` only +as required. Add an internal resolved native layout containing exact config +paths and skill directory, selected from context. Feed that layout through +existing codec/namespace/transition/recovery logic. Preserve JSON-vs-JSONC +selection rules, comments, exact owned MCP keys, tool exclusions and existing +user v1/v2 behavior. Do not widen decoder precedence or transplant state. + +**Proof.** Extend genuine native matrix using pinned current OpenCode artifact: +real config read/MCP list/skill discovery, local marker tool, A/B/user versions, +restart and removal. Contract tests inject crashes across settings+skill +transition, check exact repair/no-op/foreign edits and preserved plugin/tool +namespaces. Project config/native roots must remain stable when operation cwd +changes. Observe actual effective config through pinned `opencode debug config` +or its documented equivalent; cover higher-priority inline/managed/custom +directory overrides and rooted ancestor search. A shadowed managed entry remains +configured/prepared, never active solely because root config exists. +Source/package extension work remains separate from native JS plugin +APIs, which this task does not expand. + +## 6. GitHub Copilot CLI — repository components + +**Evidence/choice.** Official per-repository MCP docs load root `.mcp.json` and +`.github/mcp.json`, with `.mcp.json` precedence in the same directory; project +files require folder trust. Skills use `.github/skills/`. Copilot's installed +plugin command is not a documented project-scope installer. Sources: +[CLI MCP](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers), +[plugin reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference). + +**Changes.** `clients/copilot/{plan,project,identity,lifecycle}.go` and scoped +tests. Project branch must bypass shared global Copilot plugin activation and +deactivation. Select the existing effective repository MCP file conservatively +(root `.mcp.json` if present; otherwise `.github/mcp.json`, preserving foreign +members), manage exact owned servers and skills, and run inspection from fixed +project cwd. Existing project documents can be nested `mcpServers` or bare +server maps: support both through one format-preserving codec or reject a bare +map before effects; never wrap it and lose foreign servers. Remove exact +transactional project keys, never user-config `copilot mcp remove`. +Keep `.vscode/mcp.json` distinct; CLI does not read its `servers` +shape. Refuse ambiguous nested overriding configs rather than verify the +wrong effective server. + +**Proof/limits.** Genuine pinned Copilot CLI listing/config inspection plus +local MCP handshake and skill discovery after explicit trust. Do not set +GITHUB_COPILOT_PROMPT_MODE_WORKSPACE_MCP to bypass trust. Native auth-required +inspection remains pending when account is absent. Fixtures assert no global +plugin install/uninstall, no user config writes and correct precedence. + +## 7. Kiro — workspace components with explicit surface support + +**Evidence/choice.** Official workspace paths are `.kiro/settings/mcp.json` and +`.kiro/skills/`. Agent `mcpServers` overrides workspace MCP; custom agents may +need `includeMcpJson`. Sources: [MCP config](https://kiro.dev/docs/mcp/configuration/), +[configuration scopes](https://kiro.dev/docs/configuration/), +[agent configuration](https://kiro.dev/docs/custom-agents/configuration-reference/). + +**Changes.** `clients/kiro/{plan,project,identity,activate,lifecycle,native*}.go` +as applicable, and project tests. Preserve IDE-vs-CLI detection. Use exact +project root paths via layout/context for MCP+skills and retain existing +transaction/capacity/rollback checks. Remove current user-only prepared-scope +assumption only in the project branch. Do not edit active custom-agent tool +permissions/include flags to force loading; report that surface requirement. + +**Proof/limits.** Native Kiro CLI/IDE discovery at the pinned version must show +workspace MCP and skills; test same-name user server plus agent override, +workspace trust, restart, project remove and disabled config. A CLI check +cannot qualify the IDE, mobile, web or Powers install. An unavailable official +artifact/account yields explicitly bounded configuration/fixture evidence. + +## 8. Grok Build — native project plugin directory + +**Evidence/choice.** Official docs discover `./.grok/plugins/`, separate user +plugins, and project `.grok/config.toml`; `grok inspect` reports loaded +configuration. Prefer project plugin directory delivery if current real +`plugin list` proves its exact project source. Sources: +[plugins](https://docs.x.ai/build/features/skills-plugins-marketplaces), +[settings](https://docs.x.ai/build/settings), +[MCP scope](https://docs.x.ai/build/features/mcp-servers). + +**Changes.** `clients/grok/{plan,project,registry,lifecycle}.go` and dedicated +tests. Place package under `.grok/plugins/`, preserve the current +projector, add scoped registry/list classification and fixed project cwd. +Project install/remove must not execute current global `plugin install --trust` +or global uninstall. Freeze one project-directory lane after pinned native +observation; if bundled MCP is not proved, leave it prepared/unsupported with +its exact reason. Do not implement a second direct-TOML fallback speculatively. +Never auto-run `/hooks-trust`. + +**Proof.** Official pinned Grok artifact, actual project plugin list/inspect, +skill discovery and harmless MCP initialization. Verify user same-name plugin, +disabled project plugin, nested config precedence, A/B versions and removal. +Include Claude-compatible project/user plugin and skill same-name discovery, +which Grok reads in addition to `.grok`; distinguish shadowed/disabled results. +No claim about remote Grok Bot, arbitrary `grok`-named binaries, or hook trust +follows from local project installation. + +## 9. Qoder CLI — native project registry lifecycle + +**Evidence/choice.** Official `qoder plugins install PATH --scope project` +writes project settings; `uninstall NAME --scope project` is documented. +Local-scope settings are a distinct optional UX, not the same as project. +Source: [Qoder plugins](https://docs.qoder.com/cli/plugins). + +**Changes.** `clients/qoder/{plan,project,registry,lifecycle}.go` and dedicated +tests. Keep user paths/commands unchanged. Project package source stays under +`.uap/`; run install/list/uninstall from immutable project cwd with explicit +project scope on every mutating command, including replacement uninstall and +final remove. Add documented `--keep-data` whenever retaining native plugin +data, and prove separate UAP/native data markers survive replacement. Listing +has no assumed scope flag: inspect actual scope/source returned from fixed cwd. +Match qualified plugin ID, source, +effective enabled state, scope and version; refuse foreign same-name entries. +Audit scoped listing schema against pinned CLI rather than assume adding a +flag to the existing user parser suffices. Capture exact project settings/native +authority for recovery and removal. + +**Proof/limits.** Genuine current Qoder CLI must confirm project settings, +scope/source/version, skills and MCP after restart, with A/B and user at +different versions. No-version output remains manual/unverified under existing +policy. Failed reinstall cannot succeed from a stale listing. Tests cover +disabled registration, same-version changed bytes, partial install/uninstall +and unrelated cwd. No direct raw settings edit unless native commands cannot +express an already documented operation. + +## 10. Cline — native skills, explicit session MCP preparation + +**Evidence/choice.** Official config lists project `.cline/skills/`, hooks, +agents and plugins; MCP settings remain global under +`~/.cline/data/settings/cline_mcp_settings.json`. CLI documents +`CLINE_MCP_SETTINGS_PATH`. This is an explicit process-selected MCP file, not +automatic project discovery. Sources: +[configuration](https://docs.cline.bot/getting-started/config), +[CLI reference](https://docs.cline.bot/cli/cli-reference). + +**Changes.** `clients/cline/{plan,project,identity,lifecycle,native*}.go` plus +project tests. Use `.cline/skills/` for native project skills. Prepare +owned MCP config under `.uap/clients/cline/` and a reviewable launch action +fixing cwd and CLINE_MCP_SETTINGS_PATH to that file. Keep mixed MCP install +prepared/manual even after a one-session observation; ordinary restart again +requires the explicit launch action. Do not redirect CLINE_DIR or HOME, +copy global secrets into a project, or claim ordinary Cline startup loads the +file. Cline JS/TS plugins differ from portable MCP plugins and are outside this +initial projection. + +Guard attestation at the real composed provider boundary. Implement Cline's +existing `ActivationPreflighter` hook to reject `ActivationComplete` whenever +project MCP is session-selected; `providers.Activator.Activate` invokes that +hook before its generic manual-attestation branch. Reuse the same validation +for direct adapter activation. An adapter's eventual prepared return alone +does not prevent generic attestation from bypassing it. No new policy framework +or generic client-ID switch is needed. Real composed-provider tests cover this +flag during add/repair, restarted sessions and persisted binding reload, proving +that no active/installation-verified record is fabricated. + +**Proof.** Actual pinned Cline CLI with documented environment override must +load marker MCP through inert documented MCP subcommands; a generic prompt/act +invocation defaults to autoapproval. Any displayed session launch action must +include documented `--auto-approve false` and fixed cwd. Do not run an unattended +prompt/act invocation as MCP proof. Normal startup must not be called +project-MCP activation. +Prove project skill discovery independently. Verify user global settings +unchanged, explicit override behavior, project removal and UI-vs-CLI limitation. + +## 11. Windsurf / Devin Desktop — harness- and version-aware project path + +**Evidence/choice.** Devin Local is distinct from legacy Cascade. Canonical +Devin MCP/configuration docs identify v3000.3/Local3.6 moving entries to dedicated project +`.devin/mcp_config.json` and local `.devin/mcp_config.local.json`; older main +config entries auto-migrate. Cascade retains a global `mcp_config.json`, so it +cannot be advertised as project MCP. Sources: +[canonical MCP config](https://docs.devin.ai/cli/extensibility/mcp/configuration), +[Desktop Local harness](https://docs.devin.ai/desktop/devin-local), +[versioned file locations](https://docs.devin.ai/cli/reference/configuration/config-file), +[skills](https://docs.devin.ai/cli/extensibility/skills/overview). + +**Changes.** `clients/windsurf/{detect,plan,project,identity,lifecycle,native*}.go` +plus dedicated tests. Add explicit detected/selected harness capability and +version, keeping legacy Cascade user mutation behavior. Freeze the first lane +to one pinned official newer Devin Local artifact, with `.devin/skills/` and +dedicated `.devin/mcp_config.json`. Older, unknown or unobserved +harness/version stays prepared/manual or rejects MCP with a precise reason; +never infer Devin Local merely from an editor install. Avoid triggering native +migration by writing obsolete project config on new clients. + +**Proof.** Genuine official Devin CLI can qualify shared harness configuration; +Desktop UI evidence separately qualifies editor discovery. Test selected newer +layout and explicit older/unknown rejection, no duplicate active MCP source, +`.devin/mcp_config.local.json` shadowing, same-name Cascade/user server, +A/B isolation, restart and removal. Plugin install remains user/account-level; +component projection avoids changing personal cloud plugin manifests. +Remove only exact transactional project entries; never invoke unscoped +`devin mcp remove` while intending project removal, because its default is local. + +## 12. Kimi Code — current native project components + +**Evidence/choice.** The former `MoonshotAI/kimi-cli` was archived in September +2026; its `.kimi` paths and `--mcp-config-file` instructions must not define the +current client. The official migration link leads to `MoonshotAI/kimi-code`. +Current docs explicitly support project `.kimi-code/mcp.json` and +`.kimi-code/skills/` (plus `.agents/skills/`); user data defaults to +`~/.kimi-code`, matching UAP's existing detected root. Plugin installation +remains user-only. Sources: +[current MCP](https://moonshotai.github.io/kimi-code/en/customization/mcp), +[current skills](https://moonshotai.github.io/kimi-code/en/customization/skills), +[current plugins](https://moonshotai.github.io/kimi-code/en/customization/plugins), +[migration](https://moonshotai.github.io/kimi-code/en/guides/migration). + +**Changes.** `clients/kimi/{adapter,project,identity,lifecycle,registry}.go` and +project tests. Choose one first lane: current pinned Kimi Code project component +projection, exact owned MCP members in `.kimi-code/mcp.json` plus skills under +`.kimi-code/skills/`, using the existing no-follow/native-config transactions. +Do not install a project plugin in the user `plugins/managed` registry, rewrite +KIMI_CODE_HOME, or apply legacy CLI flags. Preserve disabled foreign MCP and +user config. Observe `enabled`, effective project-over-user precedence, trust, +component naming, and actual native startup before promoting readiness. + +**Proof/limits.** Pin the current official Kimi Code artifact and help/schema; +do not install or launch archived `kimi-cli` (its final no-argument entrypoint +can download a replacement installer). Native project MCP requires independently +established folder trust. Never set KIMI_CODE_TRUST_WORKSPACE or use `--auto`/ +`--yolo` to force proof; untrusted/headless discovery stays configured/blocked. +Real fresh-session MCP/skill discovery, A/B/user collision, update/repair/remove, +no-session residual registration, and retained data are required. Existing open +sessions retain removed MCP entries and visible tools that fail with a removal +notice; new sessions omit the tools. Removal verifies exact project entry +deletion and fresh-session absence, never absence from an already-open session. +Skills resolve at the nearest `.git` ancestor; +if that differs from selected ProjectRoot, reject automatic skill activation +before acquisition/effects or report the precise prepared/unsupported state. +`--skills-dir` replaces ordinary discovery and must not be inserted silently. + +## 13. ChatGPT — explicit unsupported project installation + +**Evidence/choice.** ChatGPT plugins use personal/workspace installation and +authorized app connections. Local Work/Codex folder access is not a native +ChatGPT filesystem plugin scope. The accepted core gate has no artifact-only +project-preparation route. Choose explicit unsupported project installation +instead of adding a fake native layout or user context. Sources: +[plugins](https://help.openai.com/en/articles/20001256-plugins-in-chatgpt), +[Work and Codex](https://help.openai.com/en/articles/20001275-chatgpt-work-and-codex). + +**Changes.** Dedicated ChatGPT project capability/CLI tests and precise +unsupported guidance, plus `clients/chatgpt/{plan,identity,lifecycle}.go` only +where a defensive diagnostic is needed. Reject `--scope project` before source +acquisition or effects for both saved dependencies and imperative `--no-save`. +Do not add native `ScopeProject`, `ProjectTargetLayout`, `NativeRegistryLayout` +or a bypass of the shared project gate. Keep existing user-scoped validated +personal/manual preparation behavior unchanged; a future artifact-only project +preparation path needs its own reviewed contract and is not this PR. + +**Proof/limits.** E2E proves project requests fail with a stable unsupported +reason, make zero acquisition calls, and do not create `.uap/`, intent, lock, +state, native or remote mutations. Verify existing user preparation still +works under its prior constraints and lifecycle. No generic MCP harness, Codex +CLI result or operator attestation qualifies a ChatGPT app connection, +authentication, workspace publication or project installation. This client PR +reports the product limitation rather than a false completed project lifecycle. + +## 14. VS Code — narrowly reopened project Local-harness components + +**Evidence/choice.** Current VS Code docs retain `.vscode/mcp.json` with top-level +`servers` for the Local harness. The newer Copilot harness portable root +`.mcp.json` is experimental/gated and is a separate contract. Docs label the +older guided-flow destination deprecated but preserve compatibility. +Source: [MCP destinations](https://code.visualstudio.com/docs/agent-customization/mcp-servers). + +**Changes.** Add a project-only VS Code component adapter/constructor in new +`clients/vscode/project_*.go` files, reusing neutral rendering/native-config +helpers only where safe. The project App composition registers that adapter +for the VS Code ID; user composition keeps `vscode.New()` unchanged. Existing +`NewLocal` is an explicitly TEST-qualified profile plugin-registration route, +not a product workspace MCP adapter, and must not be relabeled as one. Project +composition/registry tests must prove actual provider dispatch reaches this +new adapter instead of the default Copilot plugin route. Update the shared +scope-aware saved admission only for the new qualified project adapter: +`ScopeProject` plus both required project hooks qualifies it; legacy +`SavedDependenciesDeferred`/user saved admission stays unchanged. Generic code +uses the existing scope/hook declarations rather than a VS Code ID switch. +Select actual Local harness explicitly; use owned +`.vscode/mcp.json` entries and documented workspace skills. Do not invoke +Copilot global plugin install/uninstall for this path, silently enable an +experimental setting, or reinterpret user profile as project. Maintain exact +scope/harness authority across recovery/remove. +First qualification is one selected local filesystem folder. Reject multi-root +and remote workspace contexts until their authority is separately qualified. + +**Proof/limits.** Real pinned VS Code extension-host/UI discovery must load +workspace MCP and skill after trust and restart; `code --version` or Copilot +CLI listing is insufficient. Headless extension-host fixture proves protocol +only when it is not the actual shipped host. Test `.vscode` server shape, +foreign inputs/secrets/disabled entries, no new trust grants, A/B/user isolation +and remove. Native VS Code hooks and other frozen implementation remain out of +scope. + +## Review ledger + +R1 by sources_lead: findings at `client-plans-review-r1.md`; author applied the +shared effective-config/command-skill/evidence rules and each client-specific +fix on 2026-10-23. Codex now has one component lane, Grok one project-plugin +lane, and Devin one newer dedicated-MCP lane; no speculative dual lifecycle. +Pinned native artifact/help observation remains a pre-coding gate in each lane. +A later migration-link check corrected Kimi to current native project components; +its old kimi-cli sources were archived. sources_lead independently verified the +current Kimi correction R1 GO. +Copilot preserves bare/nested formats and removes exact project keys; Qoder +retains native data with scoped `--keep-data`; Cline remains session-prepared; current Kimi Code uses native project components; +ChatGPT rejects all project requests before acquisition; VS Code first scope is one local +folder. sources_lead verified R1 fixes; project_core independently verified all +R2 integration fixes and granted all-14 R2 GO. + +R2 integration fixes applied: Cline's existing preflight refuses session MCP +completion before generic attestation; core owns scoped effective lifecycle and +physical backend policy; VS Code gets explicit project registry composition and +project-only saved qualification; ChatGPT's unreachable artifact-only proposal +is replaced by explicit unsupported. R2 fixes independently verified GO in +`client-plans-review-r2.md`; root assigns implementation only after gates. + +| Client | Round 1 (sources_lead) | Fixes | Round 2 (project_core) | Implementation | +| --- | --- | --- | --- | --- | +| claude | GO | Verified | GO | Root assignment required | +| codex | GO | Verified | GO | Root assignment required | +| cursor | GO | Verified | GO | Root assignment required | +| gemini | GO | Verified | GO | Implemented at65b8f17a; independent implementation R1/R2 GO; native discovery pending | +| opencode | GO | Verified | GO | Implemented locally; implementation R1/R2 pending; public CLI/native discovery pending | +| copilot | GO | Verified | GO | Implementation 2 GO; native project evidence pending | +| kiro | GO | Verified | GO | Root assignment required | +| grok | GO | Verified | GO | Root assignment required | +| qoder | GO | Verified | GO | Root assignment required | +| cline | GO | Verified | GO | Root assignment required | +| windsurf | GO | Verified | GO | Root assignment required | +| kimi | GO (current docs) | Verified | GO | Authored project components; final implementation R1/R2 pending; native project discovery pending | +| chatgpt | GO | Verified | GO | Explicit project refusal implemented; independent implementation review pending | +| vscode | GO | Verified | GO | Root assignment required | diff --git a/docs/plans/project-install-e2e/cline-component-code-reviews.md b/docs/plans/project-install-e2e/cline-component-code-reviews.md new file mode 100644 index 000000000..b8c407794 --- /dev/null +++ b/docs/plans/project-install-e2e/cline-component-code-reviews.md @@ -0,0 +1,44 @@ +# Cline component implementation: independent code review receipts + +2026-10-24. Initial frozen source +`50f23db0613333fbe3294f3738729260d281e9e0`, based on both approved plan rounds. +These are leaf-source reviews; the independently reproduced generic default-SDK +ownership-observer gap remains unqualified until root's separately reviewed fix. + +## Claude reviewer + +Independent `claude_codex_scope` issued **GO** for the Cline leaf after reading +all 17 changed files, its plan and existing user helpers. It verified exact +project/original-profile/nonoverlap/managed-artifact authority, distinct project +object kinds, no ambient user helper redirect, supported component selection, +complete final delivery before shared mutation, foreign member preservation, +Prepared outcomes, early and direct attestation refusal, whole-source structured +session guidance and owned removal without a client. User dispatch/policy remain. +It independently ran the full Cline client suite (0.010s), new provider contracts +(0.149s) and public installer Cline regression (0.543s), all passing. No vendor or +MCP ran. It suggested strengthening the saved CLI user-state snapshot. + +## APM reviewer and narrow correction + +Independent `apm_vercel_scope` traced the same frozen source and reported no +semantic leaf blocker beyond the acknowledged shared observer integration. It +independently passed new providers (0.078s) and the public early-attestation test +(0.424s). Its concrete format finding was the shared import mistakenly grouped +with standard context in project.go. The narrow revision regroups that import +and adds exact original-user state-v2.json preservation to the actual saved +user+A+B CLI journey, alongside existing user native/B snapshots. +The revised saved journey passed in 9.988s; this is actual TLS Git/file/state +proof with a runner refusing every native/MCP command, not native qualification. + +## Additional current-source verification + +Affected Go vet completed successfully. Windows amd64 Cline adapter and composed +provider test cross-compilation completed successfully, without execution. +The full small Cline/domain suites passed 0.009s/0.041s. These close the checks +marked pending at the initial freeze, apart from full relevant golangci-lint, +which is not installed locally. They do not replace hosted or native gates. + +The narrow revised source still needs final review before the complete unpublished +stack is published. No current-source Cline vendor lifecycle or MCP initialization +claim is made. The default observer-off collision gap is root-owned and must be +integrated and independently verified before claiming full project admission. diff --git a/docs/plans/project-install-e2e/cline-component-implementation-evidence.md b/docs/plans/project-install-e2e/cline-component-implementation-evidence.md new file mode 100644 index 000000000..42496abc9 --- /dev/null +++ b/docs/plans/project-install-e2e/cline-component-implementation-evidence.md @@ -0,0 +1,85 @@ +# Cline project components: implementation checkpoint + +2026-10-24. The exact post-observation plan at remote `ae07f5e7` obtained both +independent R2 GOs, recorded before product code at `0a34182a`. This implements +the project-only component lane and the one coordinated shared correction. +No native collector code, vendor execution or runtime qualification is added. + +## Source boundary + +Project selection includes either project scope or a nonempty project root; +then both scope fields, original profile, exact managed package/namespace and +native component paths are validated. The new project kinds bind `.cline/skills` +and the fixed `.uap/clients/cline/mcp.json`, bypassing ambient user path helpers. +Original-profile overlap and aliases are refused. Staging must be the exact +ordinary contained child of its selected target; final delivery must be populated +and match all selected components. Existing user lifecycle and projections remain. + +The adapter reuses Cline's existing neutral server/data contract and kernel codec, +shared ProjectComponents receipts and combined ProjectComponentMutation. It +preserves unrelated JSON members and returns Prepared after actual local file +verification. Early and direct composed preflight reject activation attestation. +The sole shared lifecycle edit forwards input.ActivationComplete through the +existing early preflight request. Recorded removal requires exact project +identity, never a client executable or global uninstall. Suggested session +actions use JSON-quoted structured values, fix cwd/arguments/environment and +explain that the explicit file replaces the whole MCP source for that process. + +## Actual verification + +- Existing Cline/domain targeted contracts initially PASS, 0.017/0.005 seconds. +- New real loader/planner/stager/kernel provider Cline contracts initially PASS, + 0.087 seconds. They cover ambient path poisoning, exact file ownership, + unrelated disabled remote members, verify-only, repeated remove, malformed + scope/client/profile/artifact/delivery, foreign/tampered native skill refusal + and stdio data expansion. Independent skill/MCP selections were added later. +- Focused current Cline/attestation/preflight selection PASS: Cline 0.012s, + providers 4.744s, public installer 38.281s, usecase 0.346s. Domain had no matching + tests in this selection; it is not counted as a new domain test pass. +- Public installer first-add/replay/update/repair/reopen attestation refusal + independently PASS0.857s before the combined selection: exact saved state + unchanged, zero native calls, stable unchanged binding and host-free Inspect. +- Actual saved public CLI TLS Git user+A+B journey PASS22.777s. It covers locked + sync, denied replay with exact state/native bytes unchanged, moving-source + pinned managed-package repair, missing native-skill repair, selected update, + host-free status, exact owned removal and user/B preservation. Its runner + refuses every client/MCP command; this is file/source-contract evidence only. +- A first CLI fixture incorrectly expected repair to overwrite an externally + edited native skill. Shared ownership correctly refused; the test now exercises + damaged managed-package and missing-component repair, retaining a separate + tamper-refusal regression. One subsequent retry timed out at initial loopback + Git ref resolution before product work; the final unchanged transport/assertion + run passed. Neither failure is counted as native evidence. +- Go formatting and diff checks pass. Affected Go vet and Windows compile-only + checks are pending at this source freeze. Full relevant golangci-lint is not + installed locally; standard vet does not replace either repository lint gate. + +## Known generic observer composition gap + +A separate actual disposable SDK diagnostic with its observer flag left at the +public default false produced incomplete/Mutated=true and persisted state size +0→4080 bytes on a foreign native skill collision. The foreign bytes remained +unchanged and native calls were zero. The SDK composes the richer observer only +when EnableNativeObserver && Runner!=nil; the fallback checks the managed package, +and shared component requireAbsent detects the foreign target after its delivery. +The raw diagnostic source/result is retained in the task runtime and reported to +root for a separately reviewed composition fix. It is not committed as a passing +acceptance test or represented as a pre-delivery collision pass. The actual SDK/ +CLI acceptance fixtures explicitly compose observation with a runner that refuses +every command. This gap remains pending; do not weaken collision assertions or +silently broaden shared observation in this client-owned checkpoint. + +Two independent frozen code GOs, relevant lint gates and the newly reviewed +current-source native evidence lane remain outstanding. Historical Cline source +attribution and static read-only dispatch proof do not close those product gates. + +## Later shared composition resolution + +The generic observer gap above is a historical finding at this client checkpoint. +Shared commit `b74589e814dccb957cc668a959ad73ce893bc1e0` adds mandatory runner-free +project component ownership independently of `EnableNativeObserver`. Both the +CLI and default SDK now use the exact local ownership path before publication, +while Cline remains Prepared. The integration adds meaningful foreign-name, +changed-byte, prepare-boundary, replay, repair and fresh-instance regressions; +no vendor discovery or runtime qualification is inferred. The current contract +is documented in [Cline project installation](../../CLINE_PROJECT_INSTALLATION.md). diff --git a/docs/plans/project-install-e2e/cline-hosted-research-code-review-r1.md b/docs/plans/project-install-e2e/cline-hosted-research-code-review-r1.md new file mode 100644 index 000000000..38b5481bc --- /dev/null +++ b/docs/plans/project-install-e2e/cline-hosted-research-code-review-r1.md @@ -0,0 +1,15 @@ +# Cline hosted pre-code collector — independent implementation R1 + +**GO** at frozen `b4946a86d51e47f1d9326a8ff4431ee90c95e5a1` in `work/uap-project-cline`, reviewer codex_claude, 2026-10-23. Reviewed four-file diff from `5e240cd3`; the original amended plan and both independent GO reports were committed before runner source. This is research plumbing, not a Cline adapter or genuine runtime result. + +The actual unchanged imported require_hosted guard runs before output roots, acquisition or native execution. The exact official npm version/URL/SHA512 precedes bounded gzip/tar inspection, exact regular binary size/SHA256 precedes executable creation, and only package/bin/cline is materialized. Paths/count/type/case-duplicate/traversal limits reject unsafe members; other package assets remain unextracted. The shared process utility, Grok Budget/inventory and native matrix are byte-identical to reviewed 514cca44. + +The crucial tagged-source startup-updater finding is correctly closed in code: runtime_environment supplies literal CLINE_NO_AUTO_UPDATE=1 and child_environment rejects its omission/override, including every help/version/config child. No ambient tokens, proxies, CLINE_DIR/DATA_DIR, config override or account authority are inherited. Only fixed known A/B session-MCP files can be explicitly selected. The finite12 Cline commands use DEVNULL/no shell and literal --auto-approve false plus selected --cwd. The documented config mcp/skills read-only branches do not request a default session, task, installer, update, trust, account, MCP connection, payload or enable/disable mutation. + +Fixtures are regular nonexecutable skill text and disabled loopback port9 remote MCP markers. Original baseline files are checked after every native probe; complete bounded no-follow inventories record additional native-created files separately. Shared live file/output/deadline/process-group cleanup bounds and the hard8min collector timer are retained, with10min outer upload margin. Failures cannot become complete collection: exact version token, strict JSON arrays/object rows, duplicate-key/nonconstant rejection, clean JSON stderr, expected sequence, exits and limits are checked. Unknown populated identity remains raw evidence and both project qualification fields stay false. + +The workflow uses pinned checkout/upload actions, exact PR-head checkout, persist-credentials:false, contents:read, genuine Ubuntu24.04 and explicit hosted opt-in only for the collection step. Always-upload preserves admitted failures. Runtime network isolation is explicitly unclaimed; no network-capable task or plugin payload is requested. + +Independent checks PASS: all23 inert research tests1.117s, unchanged54 native safety tests0.539s, git diff --check. Tests do not acquire or execute Cline, patch/spoof the real hosted guard or fabricate native proof. The genuine prior Bun abort134 remains the local observation. Actual hosted version/help/config marker/path/override output still requires independent interpretation before production adapter work; ordinary project MCP startup, trust, enabled skill/MCP execution and restart remain unqualified. + +No blocker found within the reviewed plan's boundaries. diff --git a/docs/plans/project-install-e2e/cline-hosted-research-code-review-r2.md b/docs/plans/project-install-e2e/cline-hosted-research-code-review-r2.md new file mode 100644 index 000000000..e27d818a1 --- /dev/null +++ b/docs/plans/project-install-e2e/cline-hosted-research-code-review-r2.md @@ -0,0 +1,28 @@ +# Cline hosted read-only research collector — independent code R2 + +## Decision + +**GO** at frozen clean `b4946a86d51e47f1d9326a8ff4431ee90c95e5a1` in `work/uap-project-cline`, reviewed by cursor_clients on 2026-10-23. The diff from `5e240cd3` contains only the separate collector, its inert tests, dedicated workflow and truthful evidence note. Both pre-code plan GO reports precede source. No Cline binary or native child was executed by this review; the real guard refusal test intentionally stops before acquisition/output creation. + +## Source and safety boundaries + +The real imported `require_hosted('linux-amd64')` runs before requested output/probe roots, acquisition and processes. Guard rejection returns2 with bounded stderr and creates no requested parent. No native guard, existing collector or shared helper is changed in the reviewed diff. The dedicated Ubuntu24.04 workflow retains exact PR/head checkout, pinned actions, contents-read, disabled credential persistence, an explicit disposable-host opt-in and a10minute deadline. Failed collection still uploads logs through always(). It does not add Cline to a production runtime qualification gate. + +Acquisition uses the exact official @cline/cli-linux-x643.0.73 URL, vendor npm SHA512, measured binary SHA256 and exact132,511,200byte binary size. Compressed64MiB, expanded384MiB, binary256MiB and256member caps are enforced while reading/parsing. Member path/type/casefold duplicate validation excludes absolute/traversal/backslash/link/special entries. Only exact package/bin/cline bytes are materialized; package assets/scripts and vendor installers are never extracted or run. Archive and binary verification precede executable output. + +Every native observation receives an explicit fresh profile environment, PATH=/usr/bin:/bin, selected cwd, literal --auto-approve false and DEVNULL from the unchanged shared process helper. CLINE_NO_AUTO_UPDATE=1 is mandatory in the base and rechecked by child_environment; an ambient MCP override or absent/changed updater suppression fails before native invocation. Only a known disposable project override can be added. Re-read the captured exact-tag main/config/update source independently: startup calls autoUpdateOnStartup before parsing, but its literal environment guard returns before update logic. config mcp reads registrations into name/transportType/disabled/path records without starting the tools/MCP connection path; config skills enumerates instruction records and stops its service. The collector never invokes config tools, prompt/session, npx skills, wizard, MCP add/remove, trust or auth commands. + +The actual finite sequence matches the plan: four version/help, six MCP default/override/cross-cwd observations and two skills observations. Fixtures are six regular non-executable files containing plain skill text or disabled remote loopback registrations; they have no command, OAuth, hook, dependency or executable payload. Only fresh project git init with an empty template is launched before collection. Baseline authored file maps are checked after every native observation. Native-created files are separately inventoried rather than treated as ownership or proof of unchanged whole-profile state. + +The reviewed Grok hard8minute budget includes acquisition, extraction, commands and descriptor-relative no-follow inventories. Each child receives min(30seconds, remaining budget), live1MiB stdout/stderr limits and process-group/descendant cleanup. The existing bounded inventory preserves4096entry/depth32/file1MiB/aggregate64MiB and metadata limits, no-follow traversal, regular type/identity/stability checks and hardlink refusal. The budget context cancels its timer before the bounded failure summary; a deadline cannot silently become successful collection. + +Strict JSON parsing rejects duplicate keys, nonfinite constants, trailing data, invalid UTF-8 and nonobject rows; nonzero exits, deadlines/output failures or stderr on JSON cannot become accepted probes. An empty observed list may be a successfully collected negative result; it does not qualify project discovery. Every expected attempted probe must be present and accepted for collection_complete. Both project_source_qualified and project_runtime_qualified remain false, with explicit unobserved installation, ordinary MCP startup, skill execution, runtime, trust/account and restart boundaries. Native runtime network isolation is explicitly unclaimed, matching the approved read-only plan. + +## Independent verification + +- The8 Cline-specific inert tests passed in0.078s, including actual unchanged guard rejection with zero output effects, exact pin/member checks, mandatory updater suppression, finite argv/cwd/override allowlist, baseline refusal and strict result classification. +- All23 research tests passed, preserving the existing Grok/Qoder contracts. +- All54 existing native safety tests passed in0.411s. +- git diff --check passed; the four-file diff leaves the real matrix guard, shared process/read helper and Grok inventory/budget unchanged. + +No blocking implementation finding remains. This GO permits root's separate research publication/dispatch, not a production Cline project adapter or native success claim. The earlier task-host abort134 remains genuine evidence; only an admitted hosted artifact and independent marker/path/override interpretation can close the pre-code observation gap. diff --git a/docs/plans/project-install-e2e/cline-hosted-research-evidence.md b/docs/plans/project-install-e2e/cline-hosted-research-evidence.md new file mode 100644 index 000000000..f86109f7a --- /dev/null +++ b/docs/plans/project-install-e2e/cline-hosted-research-evidence.md @@ -0,0 +1,70 @@ +# Cline hosted research collector — source checkpoint + +2026-10-23. This checkpoint implements only the separately reviewed +`cline-hosted-research-plan.md`. Both final independent plan GO reports were +committed in `99013fbc` before collector source. The merge at `5e240cd3` reuses +the reviewed Grok/Qoder utilities from `514cca44`; the native matrix guard, +shared process/read utility and either existing collector remain byte-identical +to that reviewed checkpoint. + +## Implemented boundary + +The separate Ubuntu 24.04 workflow checks out the exact PR head with persisted +credentials disabled, contents-read permission, pinned actions and a ten-minute +deadline. The collector calls the actual hosted admission guard before any +requested output path, acquisition or native process. Only the admitted job +can acquire the exact pinned official Cline 3.0.73 archive, verify its registry +SHA512 and the exact binary size/SHA256, and write the one fixed executable. +Archive members are inspected under byte/count/path/type bounds; no asset, +installer or package script is materialized or run. + +The eight-minute total budget, bounded no-follow inventory and unchanged +DEVNULL/no-shell/process-group/file-limit helper apply throughout. Every Cline +process gets a fresh explicit profile environment and literal +`CLINE_NO_AUTO_UPDATE=1`, including help/version. The fixed twelve probes use +only help/version or read-only config MCP/skills inspection, literal +`--auto-approve false`, selected cwd and known disposable overrides. Fixtures +contain only plain skill text and disabled remote loopback registrations. +Original fixture bytes must survive each probe, and native-created state is +reported separately. Failed, missing, ambiguous or over-limit output cannot +become complete collection evidence. + +## Local verification + +- All 23 inert research tests passed in 1.108s: eight Cline tests plus the + existing eight Grok and seven Qoder tests. The Cline suite exercises finite + argv/cwd/override selection, mandatory updater suppression, pin/member/output + admission, text-only fixtures, changed-baseline refusal, strict result + classification and the actual zero-output hosted-guard refusal. +- All 54 existing native-client safety tests passed in 0.439s without changing + their source or the actual native guard. +- Python compilation, workflow YAML parsing and `git diff --check` passed. +- No Cline binary, installer, session, trust/auth action, model, MCP connection + or payload was executed during this verification. Mocked process calls in + the collector tests are expressly inert and do not qualify native behavior. + +## Evidence still required + +The earlier genuine local Cline attempts reached Bun 1.4.2 and aborted 134; +their provenance and failure logs remain the record. No successful local +grammar, runtime schema or project discovery is claimed. A genuine admitted +hosted run and independent interpretation of its populated marker/path and +process-override output are still required before the Cline project adapter. +The collector always leaves `project_source_qualified` and +`project_runtime_qualified` false. Even successful collection would not prove +ordinary project MCP startup, UAP project installation/ownership, trust, +enabled skill execution, MCP initialization/tool output or native restart. + +Both independent implementation reviews are now **GO** for source checkpoint +`b4946a86d51e47f1d9326a8ff4431ee90c95e5a1`; their complete reports are committed +beside this evidence note. R1 independently passed 23 research and 54 unchanged +native safety tests; R2 independently passed the same suites and rechecked the +exact tagged updater/config dispatch. Neither reviewer executed or acquired +Cline. Root may publish this separate stacked research checkpoint to collect +the missing actual hosted observations. Publication and all remote merges +remain root-owned; the production adapter and genuine native outcomes remain +pending the artifact interpretation above. + +## Historical hosted observation recovered on 2026-10-24 + +A genuine successful earlier run at exact source `bb8c89bf62542096a0af3aff26f21a16fee4d595` was recovered and independently interpreted by the implementing worker and root. The raw text artifact, verified provenance and precise source-closure differences are preserved in [the hosted observation record](../../evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/README.md). Positive exact skill paths and explicit MCP override attribution close the missing pre-code observation for a [concrete post-observation adapter plan](cline-post-observation-plan.md). This does not replace a current-source UAP native gate, establish automatic project MCP startup or alter the original artifact's false qualification flags. diff --git a/docs/plans/project-install-e2e/cline-hosted-research-plan-review-r1.md b/docs/plans/project-install-e2e/cline-hosted-research-plan-review-r1.md new file mode 100644 index 000000000..16e2bc1fa --- /dev/null +++ b/docs/plans/project-install-e2e/cline-hosted-research-plan-review-r1.md @@ -0,0 +1,65 @@ +# Cline hosted pre-code research plan independent review R1 + +Reviewer: codex_claude, 2026-10-23. Reviewed the initial a4cf690f plan and the +frozen corrective `8b99e838f8a11645b2ffc2254f41b555f6253ad8` in +work/uap-project-cline. No runner or adapter code exists in this supplement. + +## Decision: GO at corrected 8b99e838 + +The automatic-updater finding below is closed before source. The plan approves +only bounded genuine read-only configuration/skill observations in the actual +disposable hosted environment, never ordinary project MCP startup or runtime +qualification. Original independently approved client plans remain in force. + +## R1 finding: startup updater must be disabled explicitly + +Independently fetched official cli-v3.0.73 tagged main.ts, commands/config.ts, +MCP config-loader.ts and commands/update.ts through the read-only GitHub +connector after public raw-page retrieval failed. runCli calls +`autoUpdateOnStartup()` before argument parsing. That function may check a new +version and arrange a deferred detached shell updater even for short commands; +it explicitly returns before that work when CLINE_NO_AUTO_UPDATE equals1. +Fresh profile and an explicit ordinary environment alone cannot guarantee the +plan's no-updater boundary. + +Closed in8b99e838: every Cline process must receive literal +CLINE_NO_AUTO_UPDATE=1 from the runner's explicit environment, including +version/help and both config branches. Tests must reject omission, removal or +an observation-specific override. The plan records the exact tagged source +basis and no longer relies on incidental installation classification to avoid +an updater. No updater/installer execution is approved. + +## Other reviewed boundaries + +- Exact official npm version, archive SHA512 and measured binary count/hash are + bound before execution. Compressed/expanded bytes, tar members, portable names + and types are bounded; only the exact regular package/bin/cline is written + to a fixed executable path. Other assets are metadata, never extracted or + executed. Earlier genuine task-host abort134 logs remain failure evidence. +- Actual unchanged require_hosted runs before output/temp/acquisition/process + effects. Guard refusal creates no output parent/summary. Hosted read-only + permissions, exact source checkout and credential-free literal environments + preserve account boundaries; tests cannot forge or patch hosted admission. +- Reusing the reviewed read/process, total-budget and descriptor-relative + inventory helpers avoids another native guard. Eight-minute collection plus + ten-minute job, live child file caps/deadlines/group cleanup, bounded no-follow + inventories and post-admission failure summaries are explicit requirements. +- Text-only skills and disabled remote loopback-port9 markers contain no stdio, + hooks, executable/plugin agent or credentials. Native command selection is + finite: version/help, explicit config mcp JSON and config skills JSON. Fixed + no-auto-approval and cwd arguments prevent accidental implicit task intent. + Tagged config code has explicit inspection branches before the default task; + config tools/interactive/default session paths remain excluded. +- Explicit MCP-override cases observe process-selected configuration, including + a later default process and B with A's selected file; they cannot prove + automatic workspace discovery. Populated JSON is recorded before interpreting + fields; exact version/strict JSON and missing/error/overflow/deadline failures + are explicit, while source/runtime qualification remain false pending human + review. No model, MCP connection/tool, trust, native registry mutation or + main/release operation is approved. + +No additional Cline native process ran during this plan review. The reviewer's +earlier assigned version/help probes produced separately retained Bun failures; +those failures and static tagged source are not relabeled as genuine grammar, +project discovery or runtime success. Commit both independent GO reports before +adding source, then obtain the required independent implementation reviews. diff --git a/docs/plans/project-install-e2e/cline-hosted-research-plan-review-r2.md b/docs/plans/project-install-e2e/cline-hosted-research-plan-review-r2.md new file mode 100644 index 000000000..de279c18f --- /dev/null +++ b/docs/plans/project-install-e2e/cline-hosted-research-plan-review-r2.md @@ -0,0 +1,61 @@ +# Cline hosted research supplement independent plan review R2 + +Reviewer: cursor_clients, 2026-10-23. Final pre-code plan +`8b99e838f8a11645b2ffc2254f41b555f6253ad8` in work/uap-project-cline. +Read the complete supplement and relevant exact-tag source snapshots under +research/cline-3.0.73-source: CLI main/config/update, storage paths and MCP loader. +The original client plan and its two GO rounds are not reopened. + +## Decision: GO for the bounded read-only research plan + +No remaining plan blocker found after the R1 updater correction. This approves +research plumbing, not Cline project MCP activation or adapter implementation. + +- Exact npm3.0.73 archive, registry SHA512, recorded binary length/hash, actual + compressed/expanded/member/path/type limits precede materializing a single + fixed executable. Other ordinary archive assets are inspected as metadata + only. No npm, installer, updater or archived script is executed. +- The real unchanged imported hosted guard precedes directories, acquisition, + summary and native launch. Fresh HOME/XDG/npm/Git/profile mapping plus a literal + environment excludes ambient account/config/proxy/model/credential authority. + Linux hosted workflow source/permissions/checkout and artifact boundaries + match the already reviewed Grok/Qoder research pattern. +- Independently confirmed the exact tagged runCli invokes autoUpdateOnStartup + before parsing, and update.ts returns immediately for literal + CLINE_NO_AUTO_UPDATE=1. Making that immutable child environment member + mandatory for every version/help/config invocation closes the real updater + side effect; tests must prove it cannot be omitted or overridden. A fresh + profile alone was correctly not treated as sufficient. +- Exact tagged config mcp dispatch reads registrations and formats source path, + transport and disabled status; it does not create an agent session or connect + MCP. Config skills lists instruction records and returns. The allowlist avoids + config tools, agent/task prompts, native install/mutation, auth/trust and MCP + connection/tool commands. Explicit auto-approve false and selected cwd are + retained. A failed/unknown short command cannot be counted as runtime proof. +- Test-owned fixtures are only skill text and disabled inert loopback remote + entries. Independent local Git roots bound ancestor discovery. The six MCP + observations distinguish ordinary A/B from explicit process-only A/B overrides, + cross-project override selection and a later process without an override. + No persistent native registry or account setting is written to force selection. +- The independent eight-minute budget, min(30s, remaining) deadlines, DEVNULL + stdin, inherited hard output limits and process-group cleanup are reused + without weakening reviewed utilities. Bounded descriptor-relative no-follow + inventories, hard-link refusal and before/after fixture baseline checks remain + in force, including entry/depth/file/aggregate/metadata limits and failure on + volatility or overflow. The ten-minute outer workflow leaves summary/upload + margin. Runtime egress isolation is explicitly unclaimed. +- Twelve expected probes are individually accounted for with strict complete + JSON/version/help validation and honest failed/missing/signal/deadline/overflow + classification. The summary keeps project_source_qualified and + project_runtime_qualified false pending genuine output interpretation. + Static source, explicit override or a green JSON collector cannot establish + ordinary automatic project MCP startup, enabled skill execution, IDE behavior, + initialization/tool results or native restart. + +Commit both plan/fix GO reports before runner code. Implementation review must +check the immutable updater control, guard ordering, exact command/environment +and shared budget/inventory reuse, then run meaningful inert tests plus the +real zero-effects guard refusal. No native Cline process was executed by this +review. Genuine uploaded evidence must precede the project adapter, which +retains the original prepared/manual session-selected MCP boundary and +attestation refusal. diff --git a/docs/plans/project-install-e2e/cline-hosted-research-plan.md b/docs/plans/project-install-e2e/cline-hosted-research-plan.md new file mode 100644 index 000000000..12144c8ea --- /dev/null +++ b/docs/plans/project-install-e2e/cline-hosted-research-plan.md @@ -0,0 +1,158 @@ +# Cline pinned hosted research before project adapter implementation + +2026-10-23. The original Cline project plan and both independent GO reviews in +`client-plans.md` remain in force. This supplement obtains genuine read-only +grammar/config-source observations after the task-host binary failed before +help. No Cline adapter or research-runner source has been changed in this leaf. + +## Research and missing evidence + +The official npm package `@cline/cli-linux-x64` **3.0.73** is pinned to +`https://registry.npmjs.org/@cline/cli-linux-x64/-/cli-linux-x64-3.0.73.tgz`. +The captured registry SHA512 integrity is +`sha512-K+T94s0kPHLoMSANOJJ7KHb/etzYXZGQUbarKDKUPoZsvy4+oD1MAqYnZlfbKFM8tpnRQWouFngD9TB3mFqqeA==`. +Archive length is 50,817,241 bytes. Its regular `package/bin/cline` member is +132,511,200 bytes, SHA256 +`1ff8841d17d4d83763134c193d414e4f7fcf2ef56af759e5ebbe96727fc2c8f7`. +The genuine version/help attempts each started Bun 1.4.2 and aborted 134 in a +fresh networkless namespace with empty HOME/project and empty /proc. Neither +successful grammar nor a runtime schema is claimed. Provenance, member list and +failure logs remain under `tools/native-probes/cline-3.0.73` in the task area. + +Current primary references were reread: + +- https://docs.cline.bot/getting-started/config +- https://docs.cline.bot/cli/cli-reference +- https://docs.cline.bot/mcp/mcp-overview +- https://docs.cline.bot/customization/skills + +Bounded official source at Git tag `cli-v3.0.73` was read without executing it: +`sdk/packages/shared/src/storage/paths.ts`, +`sdk/packages/core/src/extensions/mcp/config-loader.ts`, and +`apps/cli/src/{main.ts,commands/config.ts,commands/update.ts}`. Static source explicitly selects +`CLINE_MCP_SETTINGS_PATH` over the global data/settings path and accepts nested +transport registrations as well as legacy flat entries. The config subcommand +has explicit `mcp` and `skills` JSON inspection branches and returns before the +default agent session. These are static observations, not substituted genuine +runtime output. Ordinary project startup must not be described as project MCP +discovery. The documentation requires explicit auto-approval control; no prompt +or task execution is part of this research. + +Independent R1 identified a startup side effect in this exact tagged source: +`runCli()` invokes `autoUpdateOnStartup()` before parsing even short commands, +and the updater can defer a detached shell installer until process exit. +The tagged `commands/update.ts` explicitly returns early when +`CLINE_NO_AUTO_UPDATE=1`. Every Cline child environment must set this literal +value; a fresh profile alone cannot establish the no-updater boundary. Source: +https://github.com/cline/cline/blob/cli-v3.0.73/apps/cli/src/commands/update.ts + +## Planned files, ancestry and real admission + +After two independent GO reviews of this supplement, merge the independently +reviewed Grok/Qoder research utility checkpoint and add only +`scripts/research-cline-native.py`, its inert tests, and +`.github/workflows/cline-native-research.yml`. Reuse the already reviewed +`native_client_research` bounded read/process helper and Grok's `Budget` and +descriptor-relative bounded `inventory` via explicit adjacent module loading; +do not modify those helpers, either existing runner or the native matrix guard. +The Cline script calls the real imported `require_hosted('linux-amd64')` before +output/temp/probe directories, acquisition or native execution. Guard rejection +prints only a bounded diagnostic, returns nonzero and creates no requested +output parent or summary. No spoofed hosted environment or monkeypatched guard +may be used in local verification. + +The separate workflow uses only GitHub-hosted Ubuntu 24.04, contents read, +pinned checkout/upload actions, exact PR-head checkout and +persist-credentials:false. A fixed explicit disposable-host opt-in is allowed +only there. Native children receive no workflow tokens, secrets, proxy, model +keys or inherited account/settings authority. The ten-minute job has an +independent eight-minute collector budget and always uploads bounded research +outputs after admitted failures. + +## Exact bounded acquisition + +Download only the exact HTTPS npm archive, with a request timeout no larger +than 90 seconds/remaining collector budget, actual compressed reads at most +64MiB, and the exact registry SHA512 before decompression. Bound gzip-expanded +tar bytes to 384MiB. Inspect at most 256 tar members, with bounded portable +relative names under `package/`, no duplicate/case-colliding names, links, +special files, traversal or absolute paths. Directory and ordinary regular +asset entries are permitted as metadata and never extracted. Read only the +single exact regular `package/bin/cline`, size at most 256MiB, require its exact +recorded byte count and SHA256, and write it to one fixed private executable +path mode 0700. Do not run npm, a package installer, postinstall script, the +vendor updater or any archived asset. No general extraction API is used. + +## Fresh fixtures and finite read-only probes + +Create separate binary/home/tmp/project-A/project-B roots. Use the existing +fresh profile_environment mapping plus literal PATH=/usr/bin:/bin, TMPDIR, +LANG and **CLINE_NO_AUTO_UPDATE=1** for every Cline invocation. This explicit +vendor no-update control is required before any version/help/config probe; +tests must assert it cannot be omitted or overridden. Default native environments contain no CLINE_DIR, CLINE_DATA_DIR, custom +config/hook/provider options or ambient product variables. Only the explicit +MCP-override observations add CLINE_MCP_SETTINGS_PATH, pointing to a known +test-owned A/B file inside this same disposable fixture. + +Prepare only inert text fixtures: unique user, project-A and project-B +`SKILL.md` markers; the user global settings file and separate project +`.uap/clients/cline/mcp.json` files contain unique **disabled** remote markers +with inert loopback port-9 URLs, no credentials or OAuth. Use the canonical +nested `transport` schema selected by the future existing-codec lane. No +stdio command, executable, hook, JavaScript/TypeScript plugin, agent, prompt, +trust or account fixture is created. Each project gets local system +`/usr/bin/git init --quiet --template=` only, with the fresh Git environment and +no network or remote. No native settings/registry mutation command is allowed. + +Run exactly four version/help probes: `--version`, `--help`, `config --help` +and `mcp --help`. Then run `config mcp --json` in six fresh processes: default +A; default B; A with A's explicit override; B with B's override; B with A's +override; and default A again. This observes explicit process selection and +its absence on a later ordinary process. Run `config skills --json` once from +each A/B cwd. These twelve Cline probes use fixed argv and selected cwd; every +invocation includes literal `--auto-approve false` and `--cwd `. +No prompt, plan/act task, interactive wizard, `config tools`, native install, +enable/disable/remove, auth, trust, hub/update or MCP connection/tool command +is permitted. Fixed config inspection branches were checked in tagged source; +unknown/nonzero output fails rather than falling through to a model proof. + +Each child has DEVNULL stdin, no shell, at most min(30 seconds, remaining budget), +child-enforced 1MiB stdout/stderr caps and the unchanged process-group cleanup. +Acquisition/read/inventory loops and scheduling check the total collector +budget; signal expiry fails collection, retaining post-admission summary. +The bounded no-follow inventories use the existing 4096-entry/depth-32, +1MiB-per-file/64MiB-total and bounded metadata limits, reject hard-linked regular +content, and record symlink/special metadata without opening/following it. +Compare the complete fixture text/config baseline and record native-created +state separately. Overflow, unavailable/volatile inventory or changed baseline +is failure, not partial completeness. Runtime network isolation is unclaimed. + +Record every expected probe, exact argv/cwd/explicit override, exit/signal, +elapsed time, stdout/stderr counts and hashes plus separate bounded logs. +Version must have exact 3.0.73 token boundaries, help must be nonempty, and each +config output must be complete strict JSON with clean stderr. Reject duplicate +keys, non-JSON constants, truncation, nonzero/signal/deadline/overflow, missing +or skipped probes. Capture populated output before interpreting its native +fields; JSON collection alone is not schema qualification. Summary fields +`project_source_qualified:false` and `project_runtime_qualified:false` remain +false until independent human review of actual marker/path/override attribution. +The explicit override does not establish automatic project MCP startup, an MCP +handshake/tool result, enabled skill execution, IDE discovery or native restart. + +## Verification and implementation gate + +Commit this plan and two independent review/fix GO reports before runner code. +Inert tests cover acquisition pins/caps/member admission, exact finite argv/cwd +and explicit environments including the mandatory literal no-update control, +credential/override boundaries, baseline accounting, +strict result/failure classification and the real zero-effects guard refusal. +Reuse existing live output/deadline/descendant tests, total timer and bounded +inventory tests without native execution or guard changes. Python syntax, +workflow YAML and relevant research suites must pass. Obtain two independent +implementation reviews before root publishes the separate stacked research PR. + +Review genuine uploaded evidence before writing the Cline project adapter. +If version/help/config selection cannot be observed, the missing fact remains +blocked and no success is invented. The adapter must still follow the original +prepared/manual session-selected MCP boundary and provider attestation refusal; +the research workflow cannot turn that into ordinary native project activation. diff --git a/docs/plans/project-install-e2e/cline-post-observation-plan-reviews.md b/docs/plans/project-install-e2e/cline-post-observation-plan-reviews.md new file mode 100644 index 000000000..4145330e5 --- /dev/null +++ b/docs/plans/project-install-e2e/cline-post-observation-plan-reviews.md @@ -0,0 +1,69 @@ +# Cline post-observation plan: independent review receipts + +## R1, 2026-10-24 — NO-GO and adopted corrections + +Reviewed exact local `370b7365ce365130f9b0a4f75ec2dc9ecc8e559a`, published as +`4461dfa9445fc9b8731a77214295fc8215c9d3bf` with identical tree. This is an +actual review record, not a substitute for review of the revised plan. + +Independent critic `apm_vercel_scope` issued **NO-GO** on two concrete source/ +plan mismatches, and root agreed both required revision before product code: + +1. `usecase.preflightActivation` constructs an early ActivationRequest without + `input.ActivationComplete`. The adapter's later provider guard cannot meet + the promised no-state-change boundary if core already recorded an attempt. + Adopt the one-field shared forwarding correction with actual public-core + first-add/replay/update/repair/reopen state/native-effects regressions. A + direct providers-only test cannot qualify that earlier boundary. Root + explicitly assigned this coordinated shared delta to the Cline group. +2. Current `nativeconfig.Server`/Cline codec cannot emit the disabled field, so + an actual UAP-produced file cannot satisfy the plan's disabled-marker native + fixture promise unchanged. Adopt an explicitly reviewed read-only config + observation contract for actual unedited UAP output instead of inventing a + production option for testing or post-editing receipt-owned bytes. The + revised plan traces exact immutable Cline dispatch/resolver/transport/update + source, requires the fixed non-session command, and honestly records + `disabled:false`. It makes no MCP connection/initialization claim. + +The critic also required user guidance that the explicit MCP file selects the +whole source; user registrations are not merged/copied into that session. This +is added. The architecture otherwise received GO: separate original profile and +project namespace, exact fixed prepared path bypassing ambient user helpers, +distinct receipt kinds, shared combined mutation/recovery, Prepared lifecycle, +local verification versus discovery, no CLI requirement for removal, and +separate unique-marker versus same-name precedence stages. The reviewer +independently verified all 29 preserved member lengths/hashes and the original +false qualification flags. No native/source acquisition was performed by that +reviewer. The implementing worker completed the six-file immutable source +reread for the revised plan; its review is a separate R2 gate. + +## R2, 2026-10-24 — both independent GO + +Frozen local `97303665b3d796fc758d66dad4c323c74d5b234d`, published as +`ae07f5e7db13514ac9f317d4bb3757c7d828e600`, identical tree +`8d6ec0058f8deaad1578604cd7137c1e2746e115`. + +Root independently issued **GO** after reading all six immutable Cline source +files. It confirmed config's numeric exit precedes the session path, MCP's +exact-path resolver and print-only branch, parser/map versus the separately +unused registerServer loop, data-only transport normalization, explicit override +precedence and the literal updater suppression. This admits the proposed +read-only registration lane, not initialization/IDE/model behavior. Root also +accepted either-scope/nonempty-root dispatch, early-empty versus final-populated +delivery validation, the whole-selected-source warning and public early-flag +regressions as closing R1. + +Independent reviewer `apm_vercel_scope` also issued **GO** on the same frozen +source. It independently fetched all six immutable primary files, checked the +recorded character counts and traced the same no-initialization dispatch, +resolver and data-normalization boundary. It accepted the narrow shared flag +forwarding with public regressions, actual unedited enabled-configuration +observation without fabricated disabled fields, whole-file selection warning, +malformed-root guard and early/final delivery distinction. No native process, +vendor acquisition or product edit was performed by either plan review. + +Both final R2 GOs are now recorded before product code. They authorize the +planned Cline implementation and the sole coordinated shared flag-forwarding +correction. Two independent frozen code reviews remain required before any +new current-source native fixture publication; source trace alone is not +measured enabled behavior or MCP initialization proof. diff --git a/docs/plans/project-install-e2e/cline-post-observation-plan.md b/docs/plans/project-install-e2e/cline-post-observation-plan.md new file mode 100644 index 000000000..e1c462376 --- /dev/null +++ b/docs/plans/project-install-e2e/cline-post-observation-plan.md @@ -0,0 +1,256 @@ +# Cline project components: concrete post-observation plan + +2026-10-24. **Plan only; no project adapter code is authorized by this document +until two independent review rounds are complete.** The owner-authorized +all-client program and original Cline choice in `client-plans.md` remain the +scope. The preserved historical hosted observations are +[recorded with raw output and provenance](../../evidence/project-scope-research/cline-3.0.73-linux-amd64-40017958131/README.md). +Root and the implementing worker independently interpreted them as sufficient +for this concrete plan, not current-head production/native qualification. + +## Product boundary and evidence + +Pinned Cline CLI 3.0.73 on hosted Linux discovered project `.cline/skills` +markers alongside user skills. `config mcp --json` selected the user settings +file in both project working directories; only explicit +`CLINE_MCP_SETTINGS_PATH` selected a prepared project file. Cross-cwd selection +followed that override, and the next ordinary process returned to user settings. +No same-name precedence, enabled execution, IDE, trust or MCP initialization +was observed. Exact source and native pins, complete raw streams, original false +qualification fields and source-closure differences are preserved in evidence. + +Implement owned project **skill directories plus explicit-session MCP +preparation**. All project activation results remain `Prepared`; file receipts +may establish `installation_verified` after exact local verification. That +value means installed component files, never native session discovery or MCP +startup. Initial plans are package-valid; broken/foreign objects fail normally. +Even skills-only results remain conservatively Prepared because production +verification is host-free and no native config enumeration is performed. + +| Case | Local delivery | Lifecycle and guidance | +| --- | --- | --- | +| Skill only | Install exact owned skill directories | Prepared; start Cline from the selected project and inspect skills. | +| MCP only or skills + MCP | Install project skills as selected; merge only owned MCP members in the fixed prepared file | Prepared; explicit launch action selects the exact file and cwd. A later ordinary process is not covered. | +| `ActivationComplete` in any project add/resume/update/repair path | Refuse before effects, including composed-provider generic attestation | No attested Active/Installed record. Actual verified file outcomes remain allowed. | +| Foreign/tampered expected target or changed recorded identity | Refuse before mutation | Preserve foreign/project/user bytes and the prior binding. | +| Remove recorded project binding | Verify exact recorded ownership; remove owned skill directories/MCP members only | No native uninstall. Preserve user configuration and unrelated members. Warn that existing sessions can retain state. | + +The displayed launch action is data for review, not a command UAP executes. +It uses explicit `CLINE_MCP_SETTINGS_PATH=`, +`CLINE_NO_AUTO_UPDATE=1`, `cline --auto-approve false --cwd `. +Provide correct POSIX and PowerShell quoting or structured instructions for +both platforms; no guessed shell execution, wrapper scripts, changed HOME, +CLINE_DIR, CLINE_DATA_DIR or copied user secrets. Mark CLI-only selection and +IDE behavior separately. The explicit MCP file selects the **whole** session +source: original user MCP registrations are not merged or copied into that +session, and UAP must not manufacture a user/project overlay. No prompt/act/ +model/MCP-tool command is requested. + +## Architecture and exact authority + +Use existing `TargetContext`, scoped composition, immutable package store, +receipts, transaction/recovery journal and original-App namespace guards. Add +Cline's explicit project policy as `LifecycleNativeConfig` only with this +adapter. Preserve the entire existing user implementation and user traits. +No generic installer, new settings framework, SDK schema or client-ID branch +in usecase orchestration is needed. One narrowly reviewed shared correction is +required: existing `usecase.preflightActivation` must forward +`input.ActivationComplete` into its existing `domain.ActivationRequest` before +apply/group-plan/repair effects. The current helper omits that flag; the later +provider preflight alone can refuse only after an attempt was recorded. This +mechanical shared edit and regression are owned by the Cline implementation +group, coordinated with root; no other shared lifecycle behavior is changed. +Native policy belongs to `clients/cline`; +shared filesystem ownership remains `ProjectComponents`/ +`ProjectComponentMutation` and the existing `nativeconfig.CodecCline`. + +Add narrow `clients/cline/{scope,project_components,project_lifecycle, +project_identity,project_validation}.go` files and dispatch at the existing +plan/project/identity/activation/removal entry points when either scope field +selects project **or TargetContext.ProjectRoot is nonempty**. New code must +validate both scope fields agree on project; mismatches fail, including both +labels=user with a project root. Direct adapter entry cannot fall through to +legacy user helpers in that malformed case. + +Canonical project paths are: + +- Registry namespace: the selected absolute project root (not `.cline` as a + fabricated user profile), bound separately from original user `ConfigRoot`. +- Managed package: `/.uap/managed/clients/cline/`. +- Skills: `/.cline/skills/`. +- Prepared MCP file: `/.uap/clients/cline/mcp.json`. +- Distinct project object kinds: `cline_project_skill_directory` and + `cline_project_mcp_server`, with `kind:name` IDs and exact digest receipts. + +One validator derives those paths from `TargetContext`, validates the selected +client, scope, original user-root identity, physical artifact and containment, +and is reused by planning, projection, observation, activation and removal. +`ProfileBindingValidator` must reject copied user/project kinds, foreign root, +changed artifact/target and out-of-project native objects before applying any +state-loaded operation. Project code must never call ambient-sensitive +`ClineMCPSettingsPath`: hostile `CLINE_MCP_SETTINGS_PATH` and `CLINE_DATA_DIR` +values cannot redirect it. The existing user path behavior stays intact. + +`ProjectTargetLayout` supplies the managed path; `NativeRegistryLayout` supplies +the exact project namespace and no native registry executable. Plan refinement +supports only existing portable skill/MCP components, preserves unsupported +commands/agents/hooks as unsupported, and returns truthful Prepared guidance. +No JS/TS plugin, dependency execution, new hook, global alias or native registry +is synthesized. Unknown native config transport/settings schema fails closed +through the current Cline codec; do not broaden it from a guessed native schema. + +Projection reuses `ClineNeutralServer` and the existing stdio data contract/ +placeholder handling. Project-specific object creation supplies the exact +layout path directly, never the ambient-sensitive user path helper. Use +`ProjectComponents.SkillObject`/`MCPObject` and the current kernel codec for +canonical hashes. Reuse `ProjectComponentMutation` for ownership-first skill +swap + MCP member commit, rollback and recovery. It preserves unrelated JSON +members and can represent skill-only/MCP-only/zero-owned removals without +inventing a second combined transaction implementation. + +`RegistryInspector` and `PreparedRegistryInspector` verify only exact scoped +file authority. Owned and unchanged is Expected; a missing owned object is +repairable under the existing checked path; unowned same-name targets collide; +malformed, symlinked, hardlinked, changed-digest or mixed identity is refused. +A discovered native user row never authorizes a project object. Original user +settings are not inspected or copied to infer effective Cline policy. + +`ActivationPreflighter` rejects project `ActivationComplete` before the +provider's generic attestation branch and is called again by direct activation. +Early preflight is deliberately valid before staging: an empty Delivery is +permitted, while plan/target/flags and any supplied delivery identity are checked. +Final activation requires the populated exact Delivery before mutation and +repeats those checks. Both validate supported components, previous/desired +object kinds and exact file paths; a partial delivery cannot authorize effects. Activation uses shared native-config +completion for actual component mutation/verification, then keeps Prepared and +clears attestation. Auth flags cannot complete project native activation. +A per-session read-only observation never changes this result to Active. +`VerifierAvailable` means exact local file verification only. `Engine.Inspect` +remains host-free and reports saved Prepared values plus general next action. +Fresh lifecycle operations return current reviewable launch guidance. + +Removal uses only the recorded validated project layout and receipts. It must +work without Cline or an accepted current version present, must not run native +uninstall or user lifecycle helpers, and must preserve every unrelated member, +user/A/B state and sibling directory. Symlink/hardlink substitutions and changed +owned files fail with prior state intact. Recovery uses the shared ownership +journal, including interruption between skill delivery and MCP commit. + +## Incremental implementation and inert verification + +After both plan rounds GO, first add the project policy/layout/validators and +projection, with table tests for exact identities, zero-value policy, unchanged +user behavior and hostile ambient overrides. Then add lifecycle/registry hooks +and composed-provider tests. Do not edit shared helpers unless a concrete +unrepresentable case is found and separately reviewed with all existing users. + +Exercise public composed SDK/CLI operations on disposable directories: same +plugin at original user + A + B; skill-only/MCP-only/mixed; foreign skills and +MCP collisions; exact-repeat add/update; invocation-change update; intact and +missing-object repair; removal and persisted reopen. Snapshot original user, +other project, unrelated JSON and source package bytes before/after every +operation. Test wrong scope/client/artifact/root, copied/tampered binding, +symlink and hardlink roots, file-mode/JSON corruption, runtime/stdio data-path +bindings, and failed MCP commit rollback. Reuse shared real interruption tests; +add a Cline integration case only if its prepared path introduces an uncovered +boundary. No test may mock a native success and label it native evidence. + +Real composed `providers.Activator` tests must reject `ActivationComplete`; +public core/SDK/CLI regressions must additionally exercise first add, replay, +update, repair and reloaded bindings. Compare selected persisted state/native +object bytes and native runner counters before/after rejection to prove the +shared early flag-forwarding prevents activation-attempt state writes and native +commands. Operational lock metadata and temporary acquisition/cache activity +are reported separately, not called immutable state. The preflight-only provider +fixture cannot substitute for these public boundary tests. Also cover mismatched +scope fields/direct adapter entry, +AuthComplete, mixed unsupported components, and no native runner calls during +host-free status. Prepared repeated outcomes should be stable under the shared +native-config idempotence contract; no global LifecycleConverged change. + +Run affected Cline/domain/provider/installer/SDK/CLI tests, unchanged user Cline +regressions, meaningful shared component/kernel recovery tests, go vet and +format/diff checks; perform Windows compile checks for the dedicated tests. +Report environment/tool limitations honestly. Obtain two independent code GOs +on the frozen product/fixture source before any hosted native publication. + +## Current-source hosted acceptance and remaining gates + +Keep the existing twelve-probe historical collector unchanged. After adapter +and fixture code reviews, add a dedicated pinned Cline project evidence lane +using its existing exact acquisition and no-updater controls, real hosted guard, +exact checkout, contents-read workflow, clean child env, finite commands, +independent stream/total budgets, bounded cleanup and separately admitted +bounded upload envelope. Review this collector code before it can run. + +The new fixture may use public UAP operations to mutate **its own disposable** +project/user files. Native Cline commands are strictly `--version` and +`config {skills,mcp} --json`, always `--auto-approve false --cwd ` +plus mandatory no-update. Fixture MCP registrations are loopback remote markers +in the **actual unedited +file emitted by UAP**. The current Cline codec has no disabled field; the fixture +must not pretend those entries are disabled, add a production option solely for +testing, post-edit receipt-owned bytes, or inspect a disabled copy as product +proof. This is a separately reviewed **read-only registration observation** +contract: exact pinned source loads/parses registrations and prints metadata +without constructing/registering/connecting an MCP manager. Enabled configuration +is distinct from an enabled connection. No stdio executable, MCP initialize, +prompt, trust, auth, model, hook or plugin code is launched. No hosted environment +is spoofed locally. The six-source exact-tag trace below is a precondition of +this contract, and its limitations require explicit independent plan review. + +Use separate source and native stages. Public UAP add/replay/update/repair/remove +must return the truthful Prepared lifecycle and exact project file receipts; +record raw CLI JSON and state/file hashes. Native listing after delivery must +show selected A/B unique skill marker paths, with the other's marker absent; +default MCP must still select only the user fixture, explicit A/B overrides +must select exact corresponding prepared files, cross-cwd selection must follow +the explicit override, and ordinary restart must return to the user file. +After removal, the exact owned skill path and explicit-file MCP member must be +absent while unrelated/user/B markers survive. Capture both stdout/stderr and +path-bearing rows. Missing, malformed, nonzero, truncated, ambiguous or changed +fixture output fails its stage and preserves raw diagnostics. Do not turn +empty-array collection into successful discovery. + +Separately exercise same-plugin user+A+B UAP ownership with identical portable +names, preserving bytes; if native same-name rows collapse, record the actual +winner and mark same-name active isolation blocked/unqualified. Do not infer +three active installations from three separate receipts. The positively proved +unique-marker discovery stage and this precedence stage remain distinct. + +Native MCP initialization/tool output, IDE behavior, enabled skill execution, +trust/managed/session effective policy and model/auth remain explicitly +not_evaluated. Session-selection configuration is the supported product boundary +and remains Prepared even if its read-only config list succeeds. This lane +qualifies only the pinned Linux CLI behavior/current source actually measured; +other platforms and versions need their own observations. Final per-client +matrix must name these limits. A failed native stage is not repaired by relaxing +assertions, changing pins or substituting the historical artifact. + +## R1 corrections: exact pinned read-only source trace + +Both R1 critics required the concrete shared flag forwarding and a representable +MCP evidence contract. The adopted route preserves the existing codec. +`cli-v3.0.73` resolves via annotated tag object +`75fc1aaa39db1119dc8b63e4969cf4ff5e27ba46` to source commit +`c718573538d76310f464541617ab665108a6c999`; all six files below were reread +from that immutable commit and compared equal to the exact-tag text. This is +source-level no-initialization evidence plus the prior disabled-configuration +observations, not a claim that enabled ordinary startup was already measured. +The artifact binary remains pinned by its own recorded size/digest. + +| Exact source at `c7185735` | Traced behavior needed by this plan | +| --- | --- | +| [main.ts](https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/apps/cli/src/main.ts) | Config dispatch lazily builds the config command, parses the explicit target and returns when its numeric exit code is set, before the default agent/session path. Startup updater occurs earlier and therefore needs its separate literal suppression. | +| [config.ts](https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/apps/cli/src/commands/config.ts) | Exact `mcp` target calls `runMcpConfigCommand`: choose path, test existence, resolve registrations, map name/transport/disabled/path and print. Post-action forwards the exit code. It does not take `config tools`, targetless interactive configuration or session branches. | +| [config-loader.ts](https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/sdk/packages/core/src/extensions/mcp/config-loader.ts) | Resolution synchronously reads and validates the file, maps registrations and normalizes transport. The separate `registerMcpServersFromSettingsFile(manager, ...)` loop is not called by this observation path. | +| [remote-proxy.ts](https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/sdk/packages/core/src/extensions/mcp/remote-proxy.ts) | Normalization only transforms transport data; remote markers are returned as data. Even legacy proxy recognition parses a URL without spawning or connecting. The fixture supplies no stdio/proxy entry. | +| [paths.ts](https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/sdk/packages/shared/src/storage/paths.ts) | Explicit CLINE_MCP_SETTINGS_PATH wins over the user data/settings default; no project working-directory merge is performed by that resolver. | +| [update.ts](https://github.com/cline/cline/blob/c718573538d76310f464541617ab665108a6c999/apps/cli/src/commands/update.ts) | `autoUpdateOnStartup` returns immediately on literal CLINE_NO_AUTO_UPDATE=1. Every fixture child must receive it, including version/help. | + +The new hosted lane records the emitted `disabled:false` rows honestly, exact +source paths and preserved file receipts. If this pinned dispatch/source changes +or the fixed target cannot be established, refuse the new observation lane; do +not assume another version is read-only. The fresh native collection remains +required, and a successful registration list cannot qualify initialization/tool +use, trust, IDE forwarding or ordinary restart with the project override absent. diff --git a/docs/plans/project-install-e2e/cline-recovery-status.md b/docs/plans/project-install-e2e/cline-recovery-status.md new file mode 100644 index 000000000..c4db5be86 --- /dev/null +++ b/docs/plans/project-install-e2e/cline-recovery-status.md @@ -0,0 +1,144 @@ +# Cline current-source gate: reconstructed draft, not native evidence + +The executor reset removed the original uncommitted gate. This reconstruction +uses retained tool-command history and the twice-approved current-source Cline +R2 plan in cline-post-observation-plan.md. It is not claimed byte-identical to the +lost draft and inherits no frozen code GO or runtime qualification. + +## Restored implementation + +Five Linux files under cli/cmd/agentplugins implement the real public Cobra and +production scoped installer/file lifecycle, the native metadata parser and finite +read-only table, exact owned-file snapshots, and inert contracts. The detector +is explicit disposable fixture authority, not fake editor storage or automatic +IDE detection. The production kernels/stager/transitions are retained and the +UAP runner forbids all native calls. Inert security admission is explicit. + +The source lifecycle is user/A/B add, repeat add/unchanged update, intact repair, +changed A update, missing delivered skill repair, fresh ordinary SDK Inspect, +removal and same-plugin user/A/B ownership. Local repair restores the unchanged +selected local bytes. The separate saved TLS Git fixture covers immutable remote +pin replay; this gate makes no mutable-local-source pin claim. Intact repair +reports its real managed-package restage as mutated=true while native bytes stay +equal. Repeat add/update require mutated=false and exact state/native bytes. + +The frontend builds the exact clean current checkout with Go1.26.10, reuses the +unchanged official pinned Cline3.0.73 acquisition, and admits only version and +config skills/mcp JSON. Every native call carries literal --auto-approve false, +explicit --cwd, one native HOME outside A/B, and CLINE_NO_AUTO_UPDATE=1. Only +explicit probes set CLINE_MCP_SETTINGS_PATH to an exact UAP-emitted enabled +file. There is no receipt-owned postedit, disabled shadow copy, stdio payload, +MCP initialize, prompt, auth, trust, model, IDE or skill execution. + +The closed table is3Git+12in-process public UAP+23native+1cleanup receipts, plus +6frontend children. Every stream is capped live at128KiB;39+6receipts plus14fixed +JSON extras and2summaries fit128files/32MiB. Unique skill rows must identify exact +selected user/A/B paths and reject all sibling-root paths. MCP rows must report +the exact selected file, enabled registration (disabled must be false), and +remote transport. Same-name actual winner rows are retained separately from +three exact UAP bindings. Every same-name inventory rejects all sibling-root +rows, including differently named foreign skills; active-version/session +isolation remains blocked. + +The already-loaded forked publisher has4swork+1snonblocking reap. Every +noncomplete publication quarantines the exact original upload directory except +a naturally exited/reaped closed normal-failure exit2, which contains only the +writer’s false failure envelope. Exceptions, signals, deadlines and unproved +reaps withhold even an already-created positive ZIP. The upload directory is +not recreated; the +known writer cannot recreate a missing upload parent. The pure closed Cline +validator authenticates all stream hashes, Go artifact membership, exact source +commit/tree agreement and stage/qualification boundaries. Runtime/MCP/model/ +IDE/trust/auth/skill/session/independent-active-version flags all remain false. +These diagnostic/direct-child controls are not an OS sandbox or unrelated +network-isolation claim. + +## Shared publisher inheritance and reviewed workflow promotion + +The restored platform input from APM was exact Git blob +325e56f7426f3017934f14913d53bd980b7e66dd (SHA256 + a188771a45ffdb95ab23f1046d6011fb71e89230c00aa98e95069d365ab2a567). +The old full0ec approval remains withdrawn over OpenCode repeat-add progress; +Windows/archive source equivalence does not repair that independent table. +This draft layers only a closed keyword client=cline branch, preserving default +OpenCode calls/validation, full path/held identity domains and final rechecks. +Cline upload-parent identity is also checked before publication. + +The unchanged reviewed workflow is promoted to +.github/workflows/cline-project-native.yml only after both independent fresh +source approvals below. The promotion has its own exact delta review before +root publication and genuine hosted collection. Other native/release matrices +stay unchanged. Source approval and workflow promotion do not qualify native +discovery or runtime behavior. + +## Fresh checks and remaining work + +- Reconstructed full TestClineGate public source/inert suite: PASS4.338s, with no + vendor opt-in or vendor/MCP command. This composes real file transactions and + fresh SDK inspection; it does not fabricate successful native rows. +- Frontend/pure-envelope/actual ZIP/preloaded real-fork contracts:25PASS0.378s. + ZIP positives deliberately carry false synthetic native claims. Stuck-child, + unproved-reap, late-import, sibling, duplicate, parent-replacement and foreign + artifact negatives retain exact refusal. The new cases use actual preloaded + forks for an exception after a written ZIP, a reaped deadline after a positive + write barrier, and an unreaped late writer after the upload path is withheld. + Only a naturally reaped closed normal-failure exit2 retains its false envelope; + an exit2 first observed after the work deadline remains unqualified. +- Existing default OpenCode envelope tests:5PASS. Additional inherited platform + tests and the exact OpenCode repeat measurement remain APM-owned. +- Final configured changed-source cli/cmd/agentplugins Go lint:0 issues + (concurrency1,512MiB heap target,90s cap). Fresh frozen root/Skillshare code + reviews are recorded below. + Genuine hosted native evidence and independent interpretation remain pending. + +No local vendor binary was downloaded or executed, no hosted guard was spoofed, +no model/MCP endpoint was invoked and no existing user profile was modified. + +## Corrected fresh review boundary + +Both independent full reviewers withheld approval of b8736db1 for two concrete +issues: a reaped unsuccessful publisher could leave an unqualified positive ZIP, +and same-name observations did not pass all sibling roots to the existing +strict parser. The corrected child records closed normal-failure admission +separately from exceptions/signals/deadlines, quarantines every other incomplete +publication through exact held-directory authority, and never recreates the +canonical upload parent. Same-name user/A/B reads now pass A+B/B/A forbidden +roots respectively; a focused table rejects differently named sibling rows for +every one of those calls while retaining the original selected-row behavior. + +No approval or runtime result from the lost draft is inherited. The source +reviews below authorize the unchanged workflow promotion for a fresh hosted +collection, subject to the separate promotion review and root publication. + +## Fresh independent exact source approvals + +Reviewed source:374d427d5cb0a705905a731a1080ff7150e266ec, +tree1e20df5bfc2aa72a58b30034cb3ba27912e44ff4. + +- ROOT:CODE GO after the complete b873 review and full five-file correction, + tracing publication/main and sameName callers. Confirmed exceptions, signals, + deadlines and unproved cleanup withhold the canonical upload namespace; only + naturally reaped closed normal-failure exit2 retains its false envelope. + Confirmed A+B/B/A roots reach the all-row parser. Source-only independent + review; local test results above belong to the implementation owner. +- Skillshare:independent final CODE GO after the full b873 source and complete + five-file child, including actual-fork exceptional/deadline/unreaped tests + and all-row sameName forbidden-root table. Source-only; no test or vendor + execution during that review. + +The workflow file content is byte-identical to the disabled draft in374d427d. +No genuine hosted result is claimed by either approval or by this promotion. + +## Genuine current-source observation received + +Run `40182752894` passed on source +`3e175de8cfa433f2e9427a47c4e1d7a8e0da5a77`, tree +`72bc516716ae105baffb2dc5a7ec7b8d259f7b95`. Its +[raw current-source evidence and interpretation](../../evidence/project-scope-research/cline-3.0.73-current-source-linux-amd64-40182752894/README.md) +preserve all 106 text members, source/ZIP identities and 90 stream hashes. +Unique selected project skills and exact enabled process-MCP source metadata +were observed through the public owned lifecycle. Same-name inventory selected +the user skill in user/A/B; that precedence stage remains blocked for independent +active versions. All runtime/trust/model/IDE/session flags remain false. This +new checkpoint does not rewrite the historical pre-adapter collector or qualify +later untested source revisions. diff --git a/docs/plans/project-install-e2e/codex-authority-recovery-plan.md b/docs/plans/project-install-e2e/codex-authority-recovery-plan.md new file mode 100644 index 000000000..5522451f4 --- /dev/null +++ b/docs/plans/project-install-e2e/codex-authority-recovery-plan.md @@ -0,0 +1,91 @@ +# Codex exact project authority recovery — fresh amendment C2 + +The reset erased finald8daad5a including exact authority corrections and public +lifecycle fixtures. C1 is now independently reviewed with current tests at +5708e861 and reports8c4e1cec. C2 restores the known separate obligations against +that concrete parser seam. Historical GOs/native passes are context, never proof +for reconstructed bytes. Two fresh independent plan GOs precede C2 code. + +## Exact authority before effects + +Every project candidate includes either project scope or any nonempty project +root; inconsistent user/project facts must not fall into user CLI behavior. At +Codex project boundaries require literal ClientCodex for detected client, plan, +and populated delivery; matching explicit ScopeProject on plan and context; a +validated canonical target context retaining the exact original user ConfigRoot; +nonempty exact native project root; clean portable nonempty physical artifact; +and literal deterministic managed target/active paths under selected project's +.uap/managed/clients/codex/. Reject omitted, rebound, cleaned aliases, +foreign client/profile/project/artifact and malformed paths before reads/writes +or native runner effects. Root and native object ownership rules stay strict. + +Early activation preflight legitimately occurs before staging. Accept only an +entirely zero/unstaged delivery there, after validating exact plan/context; once +any delivery fact is present require full exact delivery client, active path and +owned base. Actual Activate always requires populated exact delivery. Validate +ActivationComplete refusal before any operational effect. Readiness remains +prepared with independently observed installed native configuration, never +automatically active/native discovery/trust or attested activation. + +Prepared inspection must construct complete client/scope/artifact/native-root/ +active-locator binding facts rather than relying on missing-field tolerance. +A real managed project binding requires literal client/scope/artifact/root and +target locator equality against the validated plan; validate native objects as +before. Confirmed project removal similarly requires exact recorded client, +artifact, selected original profile/project/native root and deterministic managed +artifact path before mutation, independent of current executable/version. +No current user-scope behavior or legacy marketplace evidence is broadened. + +## Physical stage and current stdio reuse + +Before ProjectOpenAIMCP writes anything, validate project staging as an existing +clean absolute physical nonalias directory strictly inside the selected project +and its managed target area, separate from active/native source/watch locations. +Validate all existing ancestors using no-follow containment; do not silently +canonicalize caller input. Refuse project root, original user profile, project B, +external source, active-path alias, symlink and nonclean spelling with whole-tree +inventories unchanged. This is adapter-local, not a generic pathpolicy rewrite. + +Read-only active reconstruction requires root exactly equal to plan.ActivePath +and strict plan authority. For each selected stdio server require the current +selected data locator to be nonempty clean absolute physical project-contained +input, then exact projected PLUGIN_ROOT==plan.ActivePath and +PLUGIN_DATA==current data locator. Omitted/relative/rebound data or either stale +package/data projection returns errors wrapping existing +domain.ErrProjectionRefreshRequired, with zero writes; installer can reproject +from the exact pinned package. Remote-only and skill-only reuse remains valid +without an artificial data requirement. Keep all authentication-reference and +transport validation. Never execute the inert MCP command. + +## Fresh composed proof and boundaries + +Direct adapter and real provider tests exercise all omitted/mismatched identity, +scope/context, artifact/layout, populated-delivery, prepared-binding and recorded +removal facts; assert no runner invocation and unchanged user/A/B/source/package +inventories on refusal. Positive exact project delivery must still install/ +verify/update/remove supported skill/command and stdio/remote MCP components. +Preserve malformed/collision/foreign-edited receipt and user contract tests. + +Reconstruct a real public Engine/SDK A/B journey with original user profile, +stdio persistent-data binding, unchanged sync, restart/cold pinned repair despite +moving source, explicit update and confirmed removal retaining owned data. +Reconstruct public production CLI journeys over actual disposable TLS Git and +HTTPS ZIP transports: saved manifest/lock/state, source pins and cold repair, +selected native config/skills, user/B foreign policy preservation, update/status/ +remove with retained data. Reuse existing project fixture infrastructure without +substituting fake parser/installer/provider results; host detector/security and +network endpoints may be inert controlled boundaries. Test fixture seed roots +are physically canonical before composition, retaining intentional alias +negative tests; no production normalization or platform skips. + +Run complete preserved Codex client/provider packages, affected public Engine/ +SDK and CLI journeys, unchanged architecture closure and both pinned changed +code/full-file size/arch lint. No vendor/model/auth/trust or MCP execution. +Freeze for two fresh independent code reviews, resolve findings, commit reports +and send root checkpoints immediately. C2 fixture proof does not relabel old +macOS/Windows native evidence or replace required current assembled/native gates. + +The existing project configuration readers have a known post-read size check; +this amendment does not authorize an unrelated kernel/reader redesign. Any new +concrete bounded-reader defect requiring source changes receives its own narrow +reviewed amendment rather than broadening this recovery silently. diff --git a/docs/plans/project-install-e2e/codex-code-review-r1.md b/docs/plans/project-install-e2e/codex-code-review-r1.md new file mode 100644 index 000000000..366b0b74e --- /dev/null +++ b/docs/plans/project-install-e2e/codex-code-review-r1.md @@ -0,0 +1,103 @@ +# Codex project implementation independent review R1 + +Reviewer: core_clients, 2026-10-23. Reviewed frozen `048c3455` including the +`fda36d23` TOML codec, `6387ac85` adapter and source-native discovery addition. +This review is independent of the codec-only R1 and the author implementation. + +## Decision + +**R1 GO at `4f4a320bc58c5c45e23a75d56c43bc0c2b3da1a6`.** No remaining +implementation blocker was found in scope authority, ownership, mutation or +native evidence boundaries. The R2 selector correction and R1 follow-up below +are independently inspected closed. Hosted native discovery is still pending; +this review supplies no runtime GO. + +## Reviewed behavior + +- Project admission uses the declarative native-config scoped policy instead + of Codex user CLI-registry traits. The adapter requires both plan and target + context to name project scope, retains the original detected user profile, + and checks the explicit project registry root and no-follow native children. + Project layout does not derive authority from a prepared package path. +- The production planner/stager/projector/activator path emits exact owned + `.agents/skills` directories and `.codex/config.toml` MCP members. Existing + rendered commands, OpenAI MCP projection, managed stdio package/data/cwd + materialization and authentication references are reused. Active reprojection + validates the bound stdio locators without editing immutable package bytes. +- Skill and MCP mutation consumes the reviewed shared component transaction + and native ownership kernel. Per-object IDs, kinds, logical names, paths and + digests fence update/remove, foreign disabled entries remain collisions, and + missing exact owned skills can be repaired. Project operations do not run + Codex user marketplace registration/removal or mutate its user cache. +- The TOML parser refuses malformed/duplicate documents. Retained syntax edits + are re-parsed and compared against complete requested MCP entries and all + non-MCP semantic settings before publication. Existing JSON/JSONC lifecycle + remains on the same kernel. SSE is refused rather than silently converted. +- Project file success is always prepared activation plus installed + verification. The adapter's preflight unconditionally rejects project + activation attestation, including through the composed provider shortcut. + Visible user/project restrictions are diagnostic observations rather than a + claim to reconstruct native trust, profile, managed or session precedence. +- The new native gate is restricted to current-source Codex. It uses exact + required-test accounting, rejects released/draft/prepared evidence inputs, + retains hosted disposal and artifact integrity gates, and fails skipped or + missing required tests. No guard is spoofed or removed. +- The native fixture uses fresh genuine pinned app-server processes and the + documented `initialize`/`skills/list` route only. Its parser requires exact + selected-project repository paths, enabled entries, no plugin identity, + correct cwd, no discovery errors and no duplicates. Update, deleted-skill + repair and removal observe fresh discovery; project B must not see A's + fixture skills and recorded foreign user/cache/project files retain bytes. + Exact rendered command bytes are checked. No thread/model request, trust, + credentials, MCP initialization or effective-policy proof is manufactured. + +## Independent execution + +Go 1.26.10, GOMAXPROCS=3, `-p=2`, RAM toolchain/cache: + +- Focused nativeconfig Codex contracts: PASS 0.011s. +- Codex client contracts (including existing user Codex cases): PASS 0.033s. +- Production provider Codex contracts: PASS 0.242s. +- Domain compatibility and planner goldens: PASS 0.005s / 0.020s. +- Exact new response-parser and disposable-boundary contracts: PASS 0.008s. +- Native runner Python safety/evidence suite: 56 tests, PASS 0.495s. + +The first broader user tests correctly rejected this harness's case-duplicate +ambient proxy entries. A clean process environment resolved those failures; +production validation was unchanged. An overbroad repotest name selection also +reached existing legacy CLI build cases whose two uncached dependencies could +not be downloaded without inherited network configuration. Those unrelated +legacy cases are not claimed as passing; the exact new parser/safety contracts +were rerun successfully. No native client was executed on the task host. + +## Selector correction and R1 closure + +Independently compared the pinned native `codex-rs/config/src/skills_config.rs` +with `9d128aa2`. Ordered matching, trimmed names, canonical SKILL.md document +identity, ignored both-selector rules, and user/session-only selector authority +are reflected by the bounded user-file warning helper. Project rules no longer +manufacture a native disable observation; session effective policy is explicit +unknown. + +One R1 follow-up was confirmed: native SkillsConfig decoding is all-or-nothing +per layer. A missing required enabled value or nonabsolute typed path +invalidates the section before selector interpretation, including a relative +path on an otherwise ignored both-selector entry. The initial correction +skipped only the invalid rule and could falsely retain a disable warning from +another rule. Closed in `b39a4e9d` and final `4f4a320b`: these typed-invalid +sections now suppress all bounded selector warnings. The focused composed +regressions retain the exact invalid-section cases. + +Independently reran all `^TestProjectCodex` production provider contracts at the +final frozen SHA: **PASS 0.283s**. The code remains conservatively prepared, +never accepts project activation attestation, and never interprets a partial +settings observation as native effective policy. No production file was edited +by this reviewer and no native client was executed. + +## Remaining gates + +Retain both full independent implementation GO results on the final SHA. Run +hosted current-source native +discovery and cross-platform CI. Complete the saved production CLI composed +project journey after integration. Project MCP initialization, native effective +trust/policy, task completion and release qualification remain unqualified. diff --git a/docs/plans/project-install-e2e/codex-code-review-r2.md b/docs/plans/project-install-e2e/codex-code-review-r2.md new file mode 100644 index 000000000..740e43235 --- /dev/null +++ b/docs/plans/project-install-e2e/codex-code-review-r2.md @@ -0,0 +1,111 @@ +# Codex project adapter independent implementation review R2 + +Reviewer: cursor_clients, 2026-10-23. Reviewed the final adapter checkpoint +`6387ac8509a76b84cd42ef19bcf53ebcefa91dcb` and source-only native discovery gate +`048c34555543534bfc38eecf0a58cb64ff0d5ac5` in `work/uap-project-codex`. +The earlier codec-only review is separate and is not counted as full-adapter +approval. Scope includes production planner/provider composition, selected +project roots, exact native ownership, activation/deactivation, policy-warning +truthfulness, and the new hosted native-test selection and evidence boundary. + +## Initial decision: NO GO pending configured-warning correction + +The adapter's bounded skill restriction warnings do not match pinned Codex +`rust-v0.153.4` semantics. `project_status.go` currently inspects skill rules +from both project and user files and treats any matching disabled selector as +effective. Native `codex-rs/config/src/skills_config.rs` accepts skill rules only +from User and SessionFlags layers and processes ordered selectors, allowing +later matching enables to override earlier disables. Rules with both a name +and a path are ignored. Consequently the adapter can incorrectly report that a +preserved project selector disables a skill, or report a disable that a later +user rule reverses. The author independently confirmed the exact-tag source +and is correcting the narrow configured-warning claim. No project trust, +profile, ancestor, managed or session-policy evaluator should be added. + +## Reviewed boundaries without another blocker + +- Project context binds the selected root to its original detected user + ConfigRoot; native authority is the project root, with independent contained + `.agents/skills` and `.codex/config.toml` paths. Scope/root/user mismatches + refuse before native effects. Prepared packages and persistent data remain + under the project's `.uap` namespace. `CODEX_HOME` remains the original user + profile; project operations do not invoke marketplace/cache mutation. +- Project skills and rendered command skills use the shared exact directory + transaction; MCP uses CodecCodex and the existing nativeconfig kernel. + Object identity, selected path and staged digest validation cover updates, + missing-same-pin repair, drift, unowned collisions and exact removal. Foreign + MCP fields, TOML comments/settings, skills and user cache remain outside + ownership. Unsupported SSE is withheld rather than rewritten as HTTP. +- Active reprojection reads already staged values and verifies the bound + package/data locators without changing digest-covered package bytes. Auth + references retain the existing nonsecret OpenAI projection contract. +- Preflight executes before generic activation-attestation shortcuts and also + on direct adapter activation. File configuration remains `prepared` plus + installed verification with ActivationAttested false. Native trust, + selection/aliases, effective managed/session policy and MCP initialization + remain unobserved. Exact native-file verification is not a native session + discovery claim. +- The source matrix adds the exact anchored project-test name to existing + required Codex tests. Released/draft/prepared evidence inputs refuse the new + selection. Existing immutable client pins, fresh environment and disposable + hosted guard remain intact; skipped or omitted required tests cannot pass. + The workflow is still limited to its existing configured platforms and + triggers; writing this test is not evidence of a successful hosted run. +- The new skills-only native suite uses fresh app-server `skills/list`, exact + repo scope/path/enabled checks, expected rendered command bytes and project-B + absence through add/update/missing-skill repair/removal. It has no thread, + model, trust mutation or configured MCP initialization request. Evidence + explicitly separates those unobserved stages from project discovery. + +## Independent verification + +With Go 1.26.10, GOMAXPROCS=3 and bounded existing caches: + +- Complete `clients/codex`: PASS, 0.140s. +- Complete `adapters/nativeconfig`: PASS, 3.624s. +- Complete `providers`: PASS, 1.888s. +- Complete `planner`: PASS, 0.023s; complete `domain`: PASS, 0.025s. +- `repotests -run 'TestCodexProjectDiscoveredSkills|TestNativeDisposableBoundary|TestNativePlatformEnvironmentIsolation' -count=1`: + PASS, 0.014s. Only inert validators/environment contracts were selected. +- `python scripts/test_native_client_matrix.py`: PASS, 17 tests. + +The task host carries duplicate uppercase/lowercase proxy environment names. +An initial pattern that also selected existing user Codex provider tests +reproduced their existing strict environment refusal. Excluding only lowercase +`http_proxy`, `https_proxy`, `all_proxy`, `no_proxy` for the full inert package +run gave the passing results above; no production/test/golden change was made +for that host condition. No native Codex process, package payload, model call, +MCP command, login or trust action ran during this review. + +## Fix verification and final decision: GO + +Independently rechecked correction +`9d128aa206fd9560a2c3d4d32602e4cf3b17ab59` against the exact-tag +`codex-rs/config/src/skills_config.rs` source snapshot. Only the visible user +file now contributes skill selectors. Ordered matching name/document selectors +honor later enables, names are trimmed, both/no selector combinations are +ignored and existing document aliases are canonicalized. Messages identify +the user settings and leave session/effective enablement unobserved. Project +selectors no longer manufacture a disabled-policy claim. This is bounded +observation, without a second native settings/precedence engine. + +Independently ran all composed `providers -run '^TestProjectCodex' -count=1` +contracts at the fixed checkpoint: PASS, 0.244s. The new cases exercise +disabled canonical document/name, later name/path enable, ignored ambiguous +selectors, directory versus document path, ignored project selector and a +canonical document alias. Existing scope/rollback/attestation contracts remain +selected. No native process was launched. + +The sole implementation blocker is closed. GO for the configured project +component implementation and authored source-only discovery gate. Hosted +runtime qualification remains separate: this review does not establish +native skills discovery, MCP initialization, effective trust/policy, model +use, real task completion or release readiness. + +Final frozen-checkpoint follow-up: reviewed +`4f4a320bc58c5c45e23a75d56c43bc0c2b3da1a6`, including the native all-or-nothing +SkillsConfig section decoding guard. A missing required enabled value or any +nonabsolute typed selector path suppresses warnings from the whole section, +matching the native layer decoder. Independent complete `^TestProjectCodex` +provider selection PASS, 0.298s, including nine selector scenarios plus a +canonical document alias. Final decision remains GO at this checkpoint. diff --git a/docs/plans/project-install-e2e/codex-codec-review-r1.md b/docs/plans/project-install-e2e/codex-codec-review-r1.md new file mode 100644 index 000000000..5a79efe7c --- /dev/null +++ b/docs/plans/project-install-e2e/codex-codec-review-r1.md @@ -0,0 +1,51 @@ +# Codex TOML codec independent implementation review R1 + +Reviewer: client_plans, 2026-10-23. +Reviewed committed codec-only diff `fda36d23755318b5cb95c858e1f38430924294ad` +against its parent in the Codex child worktree. Uncommitted adapter changes were +excluded from review. This is not full Codex adapter or native runtime approval. + +## Decision: GO for the narrow codec checkpoint + +No correctness blocker found in the committed diff. The new codec keeps the +existing ownership/locking/conditional-write kernel and supplies a TOML parser, +retained syntax tree and checked render route. Existing JSON/JSONC codecs keep +their parsers and collection names; Codex-only auth options are rejected by +other codecs instead of silently ignored. + +## Reviewed boundaries + +- TOML is parsed semantically before editing, so duplicate keys/table conflicts + and malformed syntax are refused without mutation. The MCP collection must + be a table. Only MCP values enter the neutral JSON mutation document; unrelated + TOML date/time and other native types remain in their original AST. +- The native edit scanner gathers matching complete key-prefix entries before + removal. It removes the owned server's nested table mappings as well as its + heading, while sibling prefixes remain separate. Inline, dotted, parent-table + and child-table collection forms have concrete lifecycle coverage. +- Each render re-parses the resulting TOML, compares all non-MCP semantic + settings, and compares every desired/unchanged MCP member against the + requested neutral document before committing. An unchanged semantic + selection returns the original bytes rather than reformatting the document. +- MCP whole-entry digest ownership includes all native entry fields, including + foreign `enabled=false`. An unreceipted existing member is a collision; + adding a foreign field makes update/remove fail exact ownership. The generic + kernel's desired-receipt checks, batch paths/codec checks, no-follow reads, + writer locks, conditional replacement and verification still apply. +- Native projection maps headers to `http_headers`, retains explicit nonsecret + Codex authentication references, and rejects unsupported SSE rather than + degrading its transport. Stdio values remain command/args/env/cwd. Auth and + OpenCode-specific options cannot silently cross codec boundaries. +- The new TOML editing dependency is limited to native syntax preservation; + this does not add another installation or settings engine. + +## Independent verification + +Ran the complete `adapters/nativeconfig` package on the committed codec scope, +including all new `TestProjectCodexTOML*` fixtures and the existing ownership, +CAS, locking, malformed/duplicate parsing, rollback and other-codec regressions. +Result: PASS, `nativeconfig` 3.680s (Go 1.26.10). No +native Codex process, model call, MCP execution, login or trust action was run. + +Full adapter scope qualification, project/user/A/B isolation and conservative +readiness remain subject to the separate Codex implementation reviews. diff --git a/docs/plans/project-install-e2e/codex-implementation-evidence.md b/docs/plans/project-install-e2e/codex-implementation-evidence.md new file mode 100644 index 000000000..9737056c9 --- /dev/null +++ b/docs/plans/project-install-e2e/codex-implementation-evidence.md @@ -0,0 +1,180 @@ +# Codex project component implementation evidence + +Pre-code evidence recorded 2026-10-23. This implements the reviewed single +component lane from `client-plans.md`: project skills and command skills in +`.agents/skills/`, plus exact MCP entries in `.codex/config.toml`. Existing user +marketplace/plugin behavior remains separate and unchanged. + +## Official pinned inputs + +- Codex CLI **0.153.4**, official asset: + https://github.com/openai/codex/releases/download/rust-v0.153.4/codex-x86_64-unknown-linux-musl.tar.gz +- Asset SHA256 `f479424eca092484dc40d87ae28c44f4cc40234a60045d6131e493800d814a30`, + 98,582,872 bytes. Binary SHA256 + `56ef98ab4032d317ab26e9b5e5a175650717351edb16ed9cde0cb6d1734d62da`, + 258,659,424 bytes. +- Parent independently ran this binary's version, top-level help, MCP help and + `mcp list --help` in a networkless namespace with a fresh home. These prove + version/help contracts only, without credentials, trust or model requests. +- Exact-tag native schema: + https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/core/config.schema.json + SHA256 `692da7699367f6f4fbbd46c0021278c1311440bcebf0bcb9b836690c05e56196`, 200,401 bytes. +- Exact-tag config-layer source: + https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/config/src/loader/README.md + and `loader/mod.rs`. Skill source is under `codex-rs/ext/skills/src/loader/`. + +## Confirmed native contract + +The pinned schema defines `mcp_servers.` with native `command`, `args`, +`env`, `cwd` for stdio and `url`, `http_headers` for HTTP; it supports `enabled`. +Skills have `[[skills.config]]` disable selectors by absolute `path` or `name`. +The current official docs describe repository `.agents/skills` discovery from +cwd up to repo root and allow same-name skills from separate roots to coexist. +Managed ownership of a file does not establish which skill a session selects. + +The exact config loader applies project layers only after native trust admission; +user profile, user base, system, cloud-managed, session and legacy managed layers +remain separate. Project config walks from the detected root toward cwd. The +implementation does not change `CODEX_HOME`, add trust or duplicate that settings +engine. An MCP entry may inherit/merge user or higher-priority policy fields. + +Current official pages were opened before implementation: + +- https://developers.openai.com/codex/config-file/config-basic +- https://developers.openai.com/codex/config-file/config-advanced +- https://developers.openai.com/codex/mcp +- https://developers.openai.com/codex/build-skills + +## Implementation boundary + +- Use core's validated TargetContext and scoped native-config lifecycle policy; + native root is the selected project, with `.agents/skills` and `.codex/config.toml` + validated independently under it. Original detected user profile stays intact. +- Add the narrowly required `CodecCodex` TOML format to the existing nativeconfig + kernel. Reuse exact receipts, writer locks, no-follow file IO, conditional + replacement, postconditions and rollback. Preserve foreign TOML members and + comments. TOML decoding must reject duplicate/malformed documents before edits. +- Reuse existing loader, rendered command skills, immutable staging and native + skill transaction semantics. Exact project objects govern update, repair, + registry collision checks and removal; project operations never run global + marketplace registration/removal or clean a user plugin cache. +- File-only success remains prepared with installed exact objects. Bounded + configured disabled/shadowing warnings do not claim complete effective-policy + evaluation. Generic activation attestation cannot manufacture project native + discovery. Unknown versions or unavailable native observation remain explicit. +- Composed fixture journeys and genuine pinned process observations are recorded + separately. Hosted native guard remains unchanged; no local model task, trust + mutation, credential creation or package payload execution is authorized by + this evidence record. + + +## Initial implementation checkpoint + +The scoped adapter now selects the exact project root, `.agents/skills` and +`.codex/config.toml` while retaining the original detected user profile. It +consumes the reviewed shared ProjectComponents transaction with a distinct +Codex codec and explicit skill/MCP ownership kinds. The existing OpenAI MCP +projector materializes stdio package/data/cwd values and nonsecret auth +references once; active reprojection verifies the bound data locator without +rewriting digest-covered package bytes. Project operations do not invoke a +user marketplace command or change `CODEX_HOME`. + +Native file success remains prepared plus installed. User/project disabled +selectors and MCP entries are preserved and reported; system/managed/session +precedence and native trust remain unobserved. Existing explicit-only command +withholding policy is retained, while accepted commands use the existing skill +renderer. No global user trait is enabled to manufacture project support. + +Author contract checks on the initial adapter checkpoint: full Codex **PASS +0.161s**, full domain **PASS 0.029s**, full planner **PASS 0.033s**; composed +providers `^TestProjectCodex` **PASS 0.170s**. Those provider contracts include +standard loading/planning/staging, rendered commands, remote and managed stdio +MCP, auth references, exact data-bound active reprojection, add/repeat/missing-pin +repair/update/removal, user/cache/project-B isolation, malformed TOML, foreign +disabled collision, writer failure rollback, context/root refusal and generic +attestation rejection. They execute no package payload, native Codex process, +configured MCP command or model request. Independent full-adapter reviews and +hosted native discovery qualification remain separate gates. + +## Source-only native discovery gate + +The source matrix now accepts an explicit `--project-components` selection for +Codex. It retains the existing disposable-host checks, immutable native artifact +pins, fresh environment, exact required-test accounting and refusal of any skip. +The option is rejected for released, draft and prepared installer inputs; those +historical evidence contracts remain unchanged. The source Codex jobs select +both existing user tests and the project component lifecycle test. + +The new test runs production UAP `add --scope project`, `update`, `repair` and +`remove`. Fresh pinned Codex app-server processes call only `skills/list` and +must return the exact enabled repository skill paths under the selected +`.agents/skills`, with exact rendered command bytes. A command-rule update must +withdraw the old command skill, a deleted owned directory must disappear from +native discovery before repair and return afterwards, and removal must leave no +fixture skill. Separate project-B discovery and exact foreign user/cache/A/B +byte checks fence the scope. UAP outcomes must remain prepared plus installed; +this test does not change the product's activation admission. + +The exact-tag repository-root implementation independently resolves +`.agents/skills` from cwd to the detected project boundary: +https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/ext/skills/src/host_roots.rs +The native app-server contract is also exercised upstream at: +https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/app-server/tests/suite/v2/skills_list.rs +The fixture supplies distinct existing project roots and native `.git` markers; +it neither writes trust nor changes `CODEX_HOME` to a project path. + +The discovery gate has been authored but has not yet supplied a hosted runtime +result at this checkpoint. No native process was launched on the task host. +Project MCP initialization, effective trust/policy, model use and real task +completion remain unqualified. The dedicated +`codex-project-components-evidence.json` records those limits and stage outcomes +separately from existing released lifecycle evidence. + +## Independent review correction: visible skill selectors + +Exact-tag native source at +https://github.com/openai/codex/blob/rust-v0.153.4/codex-rs/config/src/skills_config.rs +(blob `cc944d817c1a65c8424e069b5a7cd466646f84bf`) establishes that skill +enablement selectors come from user/session layers, not project layers. Path +selectors identify the canonical skill document; names are trimmed, entries +with both selectors are ignored, and later matching rules may reenable a skill. +The adapter's bounded warnings now honor these semantics for the observed user +file only, including canonical document aliases. They do not interpret project +skill selectors as native restrictions or infer a native session's final policy. +Composed provider regressions cover both override orders, ignored project/both +selectors, document-versus-directory identity and canonical aliases. The whole +project provider selection passed after this correction (0.228s); exact changed +code and full-file size/architecture lint retain the existing gates without new +suppression rules. + +## Actual hosted project discovery result + +The source-only macOS arm64 run now supplies a genuine **PASS** for +`TestAgentpluginsCodexNativeProjectComponentsLifecycle` (2.25s) with pinned Codex +0.153.4. The exact source was remote commit +`0d1319f947c7f4403691b3a60b1193baa0074ee9`, tree +`4b8935ef80c3834f7145dd01289772a48379ba3f`. The runner selected and passed this +test plus both existing user suites, with no skipped required test. + +The [preserved project artifact and independent interpretation](../../evidence/project-scope-native/codex-0.153.4-darwin-arm64-40011489662/README.md) +record fresh exact project-A/project-B native skill discovery after add, command +update, an absent owned skill, repair and removal. All 28 declared project +sub-artifact hashes were verified against the downloaded ZIP; the ZIP digest, +official client pin, installer digest and run/job links are recorded there. + +The client explicitly warned that untrusted project configuration was disabled +while skills remained discoverable. No trust was added. The product therefore +still reports prepared plus installed, and this result does not qualify project +MCP initialization, model use, authentication, another OS or a release. The raw removal result also +exposed a missing project context in its retained-data follow-up command; a +separate reviewed correction preserves this original artifact unchanged. + +The same pinned source tree subsequently passed the actual Windows amd64 +project-skills subtest in **3.94s**. All three required Codex tests passed with +no skips. Its [preserved Windows artifact](../../evidence/project-scope-native/codex-0.153.4-windows-amd64-40011489662/README.md) +contains 28 verified original project sub-artifacts and independent checks of +all nine native replies, including exact A/B paths and the owned command swap, +missing-directory observation, repair and removal. The native Windows replies +did not list the separately seeded user `.agents` marker; user-byte preservation +passed, but native Windows user-marker discovery/precedence remains unqualified. +Trust, project MCP, model/authentication and release limits remain unchanged. diff --git a/docs/plans/project-install-e2e/codex-parser-recovery-evidence.md b/docs/plans/project-install-e2e/codex-parser-recovery-evidence.md new file mode 100644 index 000000000..e3c675f1b --- /dev/null +++ b/docs/plans/project-install-e2e/codex-parser-recovery-evidence.md @@ -0,0 +1,83 @@ +# Codex concrete parser reconstruction checkpoint + +Both fresh plan GOs at5500e56f were committed atc50956de before source edits. +This first recovery slice reconstructs the neutral parser seam and concrete +retained TOML backend. Historicald8daad5a tests are not current proof. + +Nativeconfig now declares only CodexDocument/parser and configured-copy seam. +No concrete TOML production import remains. Missing parser refuses before +FileIO/locks, input bytes cross as clones, literal/typed-nil documents refuse +before methods, neutral entries and output bytes are copied. DesiredReceipt +remains pure/parser-free. Concrete codextoml retains TOML AST/foreign semantics, +original no-op bytes, complete nested-entry removal and checked reparse/render. +JSON UseNumber plus exact signed-integer conversion prevents >2^53 rounding. + +The previous TOML lifecycle/refusal tests were moved to the concrete consumer, +not discarded. Their four table forms, foreign semantic/comment preservation, +exact no-op, malformed/collision/edited ownership, nested removal and unsupported +SSE assertions remain. New seam and concrete tests exercise missing parser +before effects, injected I/O/locks, adversarial input mutation, nil/typed-nil +cleanup, raw/output isolation and9007199254740993 preservation. + +All four Codex boundaries explicitly configure the backend on the existing +kernel copy: component mutation, registry/prepared inspection, object verification, +and native-settings observation. User CLI/profile behavior is unchanged. The +kernel still owns validated paths/receipts/CAS/rollback/locks and other codecs. +Guard snapshots/platform sweep and dependency manifests are unchanged. + +At this initial source freeze only formatting/whitespace checks have passed. +Fresh architecture/full codec/preserved Codex/provider tests, lint and two +independent implementation reviews remain pending and must close before root +publishes/integrates this correction. No vendor/native, model, trust or MCP +process ran during source reconstruction. Exact Codex authority/public SDK/CLI +recovery is a separate required reviewed slice, not completed by this extraction. + +## Fresh first execution and concrete fixture correction + +At initial sourcefff4b87f, unchanged TestContractLayerDependencyClosure actually +passed0.021s and complete nativeconfig passed4.013s. The moved backend test +initially failed compilation because mechanical type qualification also +qualified struct field labels/request selectors; both reviewers independently +reported that same concrete blocker. Only labels/selectors were corrected to +their original spelling, retaining qualified RHS types and all assertions. + +The initial full preserved Codex client attempt also failed on duplicate ambient +uppercase/lowercase HTTP_PROXY/HTTPS_PROXY/NO_PROXY variable names. The production +validator was not changed. A new run removed only the conflicting lowercase +ambient entries for that test process, preserving explicit fixture validation. + +After the test-only qualification correction, complete concrete codextoml tests +passed0.053s, full preserved Codex client passed0.181s, and all ProjectCodex +provider contracts passed0.733s. These are new workspace executions with the +verified Go1.26.10 toolchain/caches and one coordinated GOMAXPROCS2/-p2 process. +Failed compile/environment attempts are not passes. Lint and final independent +review closures remain pending; no native/client/model/MCP/trust execution. + +## Fresh lint closure and narrow helper correction + +Pinned golangci-lint2.13.2 initially reported three new concrete-backend import +grouping issues and gocritic's pointer-to-map parameter warning. Those are +retained failures. The helper now returns its decoded map explicitly; its +UseNumber, trailing-input refusal and recursive exact conversion are unchanged. +Only the caller signature and import groups changed. Complete concrete-backend +tests passed again0.073s after this correction. + +The initial lint also emitted cache-write warnings because its default cache was +under read-only /root. Subsequent actual gates explicitly used the writable +workspace GOLANGCI_LINT_CACHE. The exact changed-code gate fromc50956de over +nativeconfig/codextoml/Codex/providers passed0issues. The separate full-file +revive/funlen/gocyclo/gocognit/dupl/depguard gate over those same packages also +passed0issues. No lint baseline, architecture snapshot or dependency policy was +changed. Fresh test results above remain attributed to their actual source +checkpoints; no vendor/native/model/trust/MCP process executed. Two final fresh +independent code-review closures are now requested on this corrected freeze. + +## Two fresh final implementation reviews + +Both independent reviewers issued final GO at5708e8616bf6aea740e293c5d5929d845c884683 +after inspecting the exact narrow corrections and source-attributed actual gate +results. Their fresh reports are committed beside this evidence. This final +checkpoint adds documentation only; root may publish/integrate the approved +architecture extraction and rerun the unchanged assembled contract closure. +C2 authority, public lifecycle and genuine current-source native gates remain +required, and no prior native pass is relabeled. diff --git a/docs/plans/project-install-e2e/codex-parser-recovery-plan.md b/docs/plans/project-install-e2e/codex-parser-recovery-plan.md new file mode 100644 index 000000000..f3ecc9068 --- /dev/null +++ b/docs/plans/project-install-e2e/codex-parser-recovery-plan.md @@ -0,0 +1,76 @@ +# Codex concrete TOML parser recovery — fresh pre-code plan + +The reset erased unpublished finald8daad5a/source296acbc2 and their objects. +Published base0d1319f947c7f4403691b3a60b1193baa0074ee9 still contains concrete +tomledit/go-toml imports inside nativeconfig, which the unchanged contract +dependency-closure guard correctly rejects. Restore the previously reviewed +concrete backend extraction through two fresh independent plan GOs before code, +then fresh meaningful tests/lint and two fresh code GOs. Historical GOs/tests +are context, never evidence for reconstructed bytes. + +## Exact first slice: architecture without losing behavior + +Move retained TOML syntax parsing/editing into concrete adapters/codextoml, +outside the clients/domain/ports contract's nativeconfig closure. Preserve +tomledit/go-toml dependencies for that real consumer and useful legacy tests; +do not remove capability, widen guard snapshots/allowlists/platform sweeps or +change go.mod just to turn the guard green. Keep TOML-specific server projection +and pure desired-receipt/digest handling in the neutral kernel where appropriate. + +Expose only a small nativeconfig CodexDocument interface and parser function: +copy-returned MCP entry map as json.RawMessage plus render-from-current-entry +map. The concrete backend retains original bytes/TOML AST/foreign semantic +settings; nativeconfig owns hujson neutral edits, whole-entry receipt/digest, +FileIO, locks, original snapshots, CAS/rollback and committed cleanup. + +A per-Kernel WithCodexDocumentParser method returns a configured copy retaining +the exact injected FileIO/lock acquirer. No global registration, implicit default +parser or nativeconfig import of the concrete package. CodecCodex Inspect/Apply +refuse a missing parser before reads/locks/effects; ordinary codecs are unchanged. +Pure DesiredReceipt remains parser-free and effect-free. Reject literal/typed +nil documents before native methods, release acquired locks and perform no write. +Clone original input bytes before crossing the parser seam and clone raw entry/ +no-op output bytes so injected parsers cannot mutate retained snapshots. + +The concrete backend preserves existing malformed/duplicate TOML refusals, +inline/dotted/parent/child collection forms, exact original no-op bytes, foreign +settings/comments, complete owned nested-entry removal and all requested native +MCP values. Reparse rendered TOML and compare foreign semantics and desired MCP +entries before returning bytes. Neutral canonical JSON keeps UseNumber; conversion +back to TOML retains exact signed integer values, including above2^53, rather +than silently introducing float rounding. No serializer rewrite for other codecs. + +Supply the concrete parser explicitly on configured copies at all Codex project +mutation, registry/prepared inspection, object verification and native-settings +observation boundaries. Do not replace injected kernels with New() or alter user +Codex CLI/profile/trust/auth/model behavior. Update affected direct fixtures to +configure the concrete backend explicitly; retain every old TOML lifecycle and +refusal assertion at the concrete consumer plus new seam/codec regressions. + +## Fresh proof and incremental publication + +First verify the unchanged exact TestContractLayerDependencyClosure on its actual +internal/archtest package, including its unchanged platform sweep. Run complete +nativeconfig, concrete codextoml and preserved Codex client packages, affected +ProjectCodex provider/public lifecycle contracts, and pinned changed-code plus +full-file size/architecture lint. Meaningful seam tests cover missing parser +before FileIO/locks, injected I/O/lock preservation, adversarial byte mutation, +literal/typed nil with cleanup, raw-entry/no-op copy isolation and large integers. +No vendor/native/model/MCP/trust process executes during local checks. + +Freeze this first source slice for two fresh independent code reviews, commit +reports and send each clean checkpoint immediately to root for feature-branch +publication. Root may integrate this approved extraction to remove the inherited +architecture blocker and rerun the unchanged guard. This is not all-client or +current-native qualification. + +## Retained separate recovery obligations + +The lost final Codex correction also tightened exact client/scope/original-user/ +artifact/managed-layout/direct-delivery/binding/removal authority, physical +non-alias staging before projection, and exact current stdio data/package reuse. +Its public SDK A/B/pin-repair/restart/data and composed TLS Git/ZIP journeys must +be reconstructed through a separate reviewed authority recovery slice after this +architecture slice. Do not silently drop these known obligations or claim that +earlier native passes qualify changed source. Preserve the existing genuine +macOS/Windows evidence at its original source/run until a current-source replay. diff --git a/docs/plans/project-install-e2e/codex-parser-recovery-review-r1.md b/docs/plans/project-install-e2e/codex-parser-recovery-review-r1.md new file mode 100644 index 000000000..638e64a8c --- /dev/null +++ b/docs/plans/project-install-e2e/codex-parser-recovery-review-r1.md @@ -0,0 +1,33 @@ +# Fresh Codex parser recovery plan R1 + +Reviewer finish_sources_vscode_cline, independent of author. **GO** at +5500e56f01f99fa28c9db619b4263b5b8e803dfa before reconstructed source. +Full plan read: concrete TOML AST/edit backend outside neutral contract closure, +no guard/dependency/platform weakening, per-Kernel copy injection preserving +FileIO/locks, missing parser before effects, typed-nil/byte/raw/no-op isolation, +UseNumber/exact large integers and whole-entry/foreign semantics retained. +Explicit four Codex composition boundaries and complete consumer/seam/unchanged +architecture tests are meaningful. Known exact authority recovery remains a +separate required slice; no earlier native pass is relabeled. No blocker, +source edit, Go or native process by this review. Fresh code reviews required. + +## Fresh final reconstructed implementation closure + +Reviewer: finish_sources_vscode_cline. Fresh independent final code GO at +5708e8616bf6aea740e293c5d5929d845c884683. + +The full production extraction review atfff4 stands. Exactfff..5708 closes the +moved-test field/selector qualification blocker without losing assertions. The +explicit decoded-map return retains UseNumber, trailing refusal and recursive +exact-integer conversion; remaining source edits are import grouping. Missing +parser/typed-nil/copy boundaries, preserved I/O/locks and four explicit concrete +composition boundaries remain sound. + +Actual unchanged closure PASS0.021s/full nativeconfig4.013s atfff4; corrected +full backend0.053s/Codex0.181s/ProjectCodex providers0.733s at478e; final helper +backend0.073s and both pinned lint gates0issues close current verification. +Initial compile/proxy/cache/lint failures are honestly retained. No guard, +snapshot, platform sweep or dependency policy was widened. No duplicate reviewer +Go/native execution. This GO qualifies architecture extraction only; exact C2 +authority, public SDK/CLI and current native/assembled qualification remain +separate required gates. diff --git a/docs/plans/project-install-e2e/codex-parser-recovery-review-r2.md b/docs/plans/project-install-e2e/codex-parser-recovery-review-r2.md new file mode 100644 index 000000000..015d94130 --- /dev/null +++ b/docs/plans/project-install-e2e/codex-parser-recovery-review-r2.md @@ -0,0 +1,33 @@ +# Fresh Codex parser recovery plan R2 + +Reviewer finish_grok_integration, independent of author. **GO** at +5500e56f01f99fa28c9db619b4263b5b8e803dfa before reconstructed source. +Full plan preserves TOML behavior/dependencies outside guarded closure, unchanged +guard/platform snapshot, explicit configured-copy seam retaining injected I/O/ +locks, missing parser before effects, nil/typed-nil/byte/raw/no-op isolation, +parser-free desired receipts, AST/foreign/whole-entry semantics and exact large +signed integers. No implicit/global backend or lost capability. Authority/SDK/ +CLI restoration remains explicitly separate. Actual architecture test package, +meaningful seam/backend/Codex/provider/lint gates and two fresh code GOs remain +required. No blocking finding, source edit, Go or native execution by reviewer. + +## Fresh final reconstructed implementation closure + +Reviewer: finish_grok_integration. Fresh independent final code GO at +5708e8616bf6aea740e293c5d5929d845c884683. + +The full production extraction review atfff4 stands. The concrete moved-test +namespace blocker is fixed with all old assertions retained. Explicit mapreturn +does not change exact decoding/refusal semantics. Configured kernel copies +preserve injected I/O/locks; missing parser and literal/typed-nil guards, input/ +raw/no-op copy isolation and pure DesiredReceipt remain coherent. All four +Codex composition boundaries supply the concrete backend explicitly. + +Actual unchanged closure PASS0.021s/full nativeconfig4.013s atfff4; corrected +full backend0.053s/Codex0.181s/ProjectCodex providers0.733s at478e; final helper +backend0.073s and both pinned lint gates0issues close current verification. +Initial compile/proxy/cache/lint failures are honestly retained. No guard, +snapshot, platform sweep or dependency policy was widened. No duplicate reviewer +Go/native execution. This GO qualifies architecture extraction only; exact C2 +authority, public SDK/CLI and current native/assembled qualification remain +separate required gates. diff --git a/docs/plans/project-install-e2e/codex-prepared-replay-result.md b/docs/plans/project-install-e2e/codex-prepared-replay-result.md new file mode 100644 index 000000000..89d6b7b3c --- /dev/null +++ b/docs/plans/project-install-e2e/codex-prepared-replay-result.md @@ -0,0 +1,51 @@ +# Codex Prepared native configuration: unchanged replay result + +2026-10-25. At source `d6809d5438654ccdb47bd8d241b96b44e91515c8`, tree +`7ba2bf288af06d6be36f65c708e913688a26567a`, native run `40187422616`, Windows +job `121151065083`, fails both project suites on repeat add. Public JSON reports +`mutated:false`, Prepared and installation verified, but omits `no_change`. +The exact artifact is `12596773186`, 133,705 bytes, SHA-256 +`4d2dbe5f4eda4b84e6ed0a0af11586da419fec0dc312f36720c788910de16da4`. +Its complete runner membership/digests match that source. Initial trusted direct +MCP assertions passed with zero captured Responses requests; replay and removal +qualification remain incomplete. Neither native assertion will be weakened. + +## Cause and bounded correction + +`Service.resume` currently classifies unchanged Active registrations or unchanged +Prepared observations for clients without native lifecycle. Codex deliberately +retains Prepared after exact project native configuration verification, so its +successful verify-only replay falls outside both classes. + +Extend the existing Prepared-observation classifier with a separate branch only +for project `EffectiveTraitsForScope(...).LifecycleKind == LifecycleNativeConfig`. +Keep its current nonnative Prepared branch unchanged. The additional branch must +require the exact same Prepared/Installed lifecycle, authentication and policy; +materialized binding; exact unchanged delivery and proven owned receipt set; +explicit `NativeEffectUnchanged`; exact outcome/client/delivery native receipt +equality; no unresolved native attempt/intent; and no input completion or output +activation/authentication attestation. Retain exact project scope/identity and +available verifier requirements. Unknown, missing, committed or uncertain effect, +changed/missing/foreign ownership, or any contradictory outcome cannot qualify. + +Carry the existing `activeNativeDelivery` completion proof into this classifier +from its two existing callers. The native-config branch requires that proof and +a nonempty receipt set; the nonnative branch ignores it and retains its guards. + +This reports the completed file verification as an unchanged operation. It does +not make the lifecycle converged, grant trust, promote Prepared, or qualify MCP, +authentication or model behavior. Every repeat retains the existing fresh +verify-only activation path and ownership observation. Change no native fixture, +CLI envelope adapter, registration decision, transaction or activation call. + +## Verification and publication + +Two independent plan rounds and frozen implementation reviews precede publication. +Strengthen the existing composed Codex project installer fixture to assert +`NoChange` on repeat add and unchanged update with exact state/native file bytes, +preserved user/B files and Prepared binding. Add focused inert classifier +contracts for exact unchanged ownership and mismatched delivery/outcome, committed +effect, unresolved attempt/intent, wrong scope/lifecycle and attestation refusal. +Run affected installer/usecase tests and configured changed-source lint. The +unchanged genuine macOS/Windows native suites at the next assembled exact head +remain the qualification gate; the historical partial stage is not full E2E. diff --git a/docs/plans/project-install-e2e/completion-plan-reviews.md b/docs/plans/project-install-e2e/completion-plan-reviews.md new file mode 100644 index 000000000..cb83ac3e3 --- /dev/null +++ b/docs/plans/project-install-e2e/completion-plan-reviews.md @@ -0,0 +1,84 @@ +# Completion plan independent review record + +## Round 1 + +Reviewed plan: commit `a97b101089df755318164422410d3373ec12d87e`, before new +production implementation. Both independent GPT-6.1 xhigh critics inspected the +plan and relevant current source. No production changes or test execution were +performed by the critics during review. + +### Reviewer 1 — /root/skillshare_scope + +Decision: **NO-GO until Claude pre-delivery admission is explicit**. + +The plan defined blocked activation after owned delivery but did not define how +an initial untrusted installation passed registry admission before any receipt +existed. Suppression must not grant ownership of an existing foreign directory. +The required admission table distinguishes absent destination, unowned destination, +owned/digest-verified maintenance, disabled exact records, user winners, same-project +foreign identity, and malformed/contradictory native output. + +Additional requested clarifications: + +- Check the original scoped installer namespace before opening the environment + lock and again after acquiring it; a replacement project must not receive a + lock file. Direct picker/PrepareSkills entrypoints also enforce scope checks. +- Include actual process death between skill swap and MCP commit/acknowledgment, + then reopen and prove unresolved-attempt fencing through production composition. +- Final all-client matrix separates implementation, fixture lifecycle, native + discovery, MCP initialization, restrictions and exact tested version/platform. + +Architecture assessment: GO for shared orchestration and narrow native adapters; +no generic copier, duplicate per-client installer or Codex parser rewrite. + +### Reviewer 2 — /root/apm_vercel_scope + +Decision: **NO-GO until three bounded clarifications are incorporated**. + +1. Define first-add namespace admission independently from native activation. + Project-only RegistryClear is acceptable for known blocked observations only + with existing prepared namespace and receipt/digest validation. Assert + NativeDiscoveryReconciled stays false. Keep generic identity/recovery unchanged. +2. Preserve host-free ordinary status. It reports saved activation/verification + and generic guidance; precise fresh blockers belong to observed lifecycle + operations. Add runner-count regression; no implicit schema/live-status change. +3. Define genuine native untrusted Prepared lifecycle separately from positive + discovery. Exercise add/update/repair/status/remove and foreign preservation; + retain raw suppression and ownership evidence. Filesystem reads cannot mark + discovery or MCP passed. Preserve user native runtime requirements. + +Other clarifications: keep the direct Codex component route, record MCP attempted/ +passed/blocked/not evaluated, retain stable Prepared replay without globally +changing LifecycleConverged, use canonical Windsurf identity, cover every supported +client including explicit ChatGPT refusal, and keep OpenCode versioned schemas +separate. + +### R1 disposition + +The revised completion plan adds the pre-delivery table, strict mixed-row ordering, +project-local namespace mapping, host-free saved-status rule, transient repair +and repeated-operation constraints, explicit untrusted prepared evidence contract, +original-App namespace lock checks, process-death case and evidence-granular canonical +client matrix. No generic core observation schema or ownership relaxation was added. +Both reviewers must inspect the revised plan in Round 2 before production code. + +## Round 2 + +Both independent critics reviewed exact revised commit +`7c18c08d892227c14961030aed95101184e5a8e0` and relevant source seams. + +- **/root/skillshare_scope — GO.** The first-add admission table closes R1's + blocker. RegistryClear does not reconcile native discovery; prepared scan, + unmanaged-path and owned-digest gates remain intact. Original namespace lock + checks, host-free status, Prepared repetition, process death and evidence + boundaries are adequate. No remaining blocking plan finding. +- **/root/apm_vercel_scope — GO.** All three blocking clarifications are resolved. + Adapter-local observation avoids unnecessary generic schema/recovery changes. + Shared Skills direct APIs, stable Prepared replay, Codex component choice, + canonical client coverage and exact-head integration gates are adequate. + +Neither reviewer modified production or ran tests during R2. These decisions +approve implementation under this plan, not unimplemented adapters, native runtime, +merge readiness or release. Evidence-dependent Qoder/Cline/VS Code concrete designs +still require their own post-spike review rounds. Independent code review and +final exact-source CI remain required. diff --git a/docs/plans/project-install-e2e/completion-plan.md b/docs/plans/project-install-e2e/completion-plan.md new file mode 100644 index 000000000..431ef0539 --- /dev/null +++ b/docs/plans/project-install-e2e/completion-plan.md @@ -0,0 +1,254 @@ +# Project installation completion plan + +Owner continuation: 2026-10-24. The owner authorizes practical spikes, independent +critique and fixes, parallel implementation for all supported clients, independent +code review, incremental commits/pushes, and merge after verification. Publishing +a release is not part of this continuation. + +Status: **two independent R2 GOs on revision 7c18c08d; implementation authorized under the staged evidence and review gates below**. +Baseline assembly: `0cf52a6067b03bc36d3bb1f45ac2c6356f95ae52`. +Fresh main: `4555aa53` (includes source transports and the standalone Skills UI). + +Read AGENTS.md, PROJECT_INSTALL_OWNER_SCOPE.md and +AUTHORING_CAPABILITY_PRESERVATION.md. Existing source, contracts, native evidence, +merged source and released availability remain separate claims. Use GPT-6.1 +high/xhigh workers and critics. Preserve the narrow VS Code exception and useful +legacy source/tests. The client plans and previous reviews remain background; +this document governs the new completion changes. + +## Observed spikes and design choice + +1. Pinned Claude Code 2.1.263 emits an untrusted-project sentinel from + `plugin list --json`: `(suppressed)@skills-dir`, scope project, enabled false, + empty installPath, version unknown and explanatory notes. Exit status is zero. + Existing strict parsing rejects the sentinel. Nested cwd does not scan the + parent whole plugin. Native uninstall refuses skills-directory plugins and + directs directory removal. Project disable writes project settings and can + affect a user-origin row in that project. No trust was granted or model task + requested. Raw observations must be preserved with the implementation. +2. Official Codex 0.164.0 exposes project skills and trusted/untrusted config via + app-server read-only calls. A same-name user skill and project skill coexist + in its list. Repo marketplace inventory is available through app-server, but + the CLI plugin add/list path does not provide equivalent project scope. + Two trusted disposable projects using the same marketplace/plugin/version + can load the first project's body from shared user plugin cache. Therefore + retain project component projection; do not replace it with native catalog + installation or rename portable publisher identity to hide cache coupling. +3. A disposable Vercel Skills local-lock probe confirmed relative project source + portability and content hashing; a corrupt lock reads as empty. Adopt the + portable intent concept, but preserve UAP's explicit corrupt-authority refusal. + APM and Skillshare support separating source intent, native target scope and + exact owned entries. Do not adopt silent global fallback, process cwd mutation, + unverified target overwrite, or generic full-file ownership. + +These observations establish the tested boundary, not authentication, trust +approval, model execution or support on untested versions/platforms. + +## Architecture and invariants + +Keep one dependency/environment orchestration layer. Reuse the existing resolver, +manifest/lock formats, immutable source store, security validation, transaction +engine, scoped receipts, namespace guard and recovery. Keep client-specific native +paths, codecs, commands, precedence and activation in the client adapters. +`ProjectComponentMutation` shares exact file/entry ownership where contracts match; +do not rewrite specialized proven transactions solely for visual uniformity. +The existing neutral Codex TOML port and reviewed concrete adapter stay intact. + +- Default scope remains user. Project scope is explicit; omitted project-root + selects the invocation directory, with canonical existing-root validation. +- Resolve immutable TargetContext before source acquisition. A native client user + profile remains user authority; production never changes HOME/CODEX_HOME to + simulate project installation or uses package path as scope authority. +- Intent and source pins are portable. Project receipts, locks, journals and data + under .uap are local authority, bound to selected directory identity. Shared + immutable package bytes do not imply shared installed ownership. +- Every read, preview, prepared action, apply, update, repair, remove and recover + uses the same scope, environment ID, project root and original user profile. +- Preserve foreign data, comments, disabled entries and changed managed objects. + Refuse ambiguous ownership, symlink escape, replaced roots and copied receipts. +- Installed files, prepared configuration, observed native discovery and successful + MCP operation are distinct. Attestation cannot override negative native evidence. + Unknown native output remains unknown; do not weaken parsing to achieve green CI. +- No source acquisition or mutable project state for an unsupported explicit target. + No unsupported-to-user fallback. No credential, account, trust or policy mutation + in production or in a probe of an actual user profile. + +## A. Reconcile the existing work + +Create an isolated continuation branch. Merge fresh main, the current project-core +head and reviewed client leaves, retaining both source/Skills additions and project +scope behavior. Review conflicts semantically; retain existing tests and claims. +Include Claude diagnostics and Codex recovery notes; integrate the reconstructed +Grok implementation and reviewed research collectors. Do not merge unrelated open +PRs. Record incorporated source SHAs and any genuinely new integration decisions. + +Run the existing core suite before/after integration. The first local baseline +attempt hit host ENOSPC before compilation; that is not a code test failure. +Build dependencies and temporary products can use an isolated temporary filesystem. +Never delete another task's files to make room. Remote CI remains authoritative for +macOS and Windows behavior. + +## B. Standalone Skills and UI scope completion + +Compose the selected App at Skills command entry before Git/HTTPS acquisition, +including website Skills. Use the existing prepared batch and dependency installer; +do not introduce another install engine. Carry app.scope()/environmentID() through +Coordinator Resolve/Observe/PrepareSync/ApplySync and adapter options. Preflight +explicit target scope and project namespace before reading source or taking locks. + +Picker target capabilities and displayed destination must use selected project +layout, not detected user ConfigRoot. An unavailable project target remains disabled +with a precise reason. Terminal and browser use the same preparation/apply API. +Stale-plan checks bind namespace, selected scope, exact inputs, target paths and +client authority. The original scoped App/installer namespace is checked before opening the +project environment lock, and again after acquiring it before cache, intent or +native writes. Rechecking only a disposable preview is insufficient. Rejected +replacement roots must not gain a lock file. Direct StartSkillsPicker/PrepareSkills +entrypoints enforce the same selected-scope/target/namespace admission as CLI entry. +No process-global cwd. + +Tests: public CLI Git and website Skills; prepared browser callback route; dry-run +has no durable mutation; rejected target acquires no source; user/A/B isolation; +changed root/receipt/client authority between prepare and apply; locked replay, +update, missing same-pin repair, removal and retained data. Use existing fixtures, +with one representative source-by-client seam rather than a full Cartesian suite. + +## C. Claude native observation and owned inactive lifecycle + +Keep the projected skills-directory backend and original manifest name. Introduce +project-only typed observation: exact enabled, exact disabled, known suppression, +conflicting winner/shadowed, absent, unknown. Preserve strict user parser behavior. +Parse rows order-independently. Recognize the observed sentinel narrowly, with +exact id/scope/false enabled/empty path/unknown version and bounded nonempty notes. +Contradictory fields or wrong types remain unknown; unknown suppression reasons do +not become a trust claim. The sentinel establishes no package identity. + +### Pre-delivery admission is separate from native activation + +The project-only RegistryInspector maps known suppression, a disabled exact row +and an out-of-project competing winner to RegistryClear **only for native namespace +ownership**. It does not claim the target is absent or native discovery reconciled. +Existing InspectPreparedRegistry scans the selected project namespace, and generic +receipt/digest/path/namespace validation must independently gate every mutation. +No generic RegistryFinding, combinedIdentityState or recovery change is planned. + +| First-install or maintenance situation | Admission | +| --- | --- | +| Known suppression; exact destination absent; project namespace and prepared scan clear | Admit prepared first install; suppression supplies no package identity or ownership | +| Known suppression; exact destination or same-name project artifact exists without matching ownership | Collision/refusal; no adoption | +| Known suppression; matching owned binding and validated directory/digest | Admit the existing owned maintenance path; keep native result prepared | +| Exact disabled project row without matching UAP ownership | Collision/refusal through prepared scan; disabled does not mean absent | +| Same-name user winner; exact project target absent and project scan clear | Admit a prepared shadowed project copy; preserve the user row and settings | +| Same-name user winner; matching owned project target | Permit validated owned maintenance/removal; no native-discovery reconciliation | +| Same-name foreign path inside selected project | Collision, even if native listing is suppressed or disabled | +| Native record claims another project or contradicts selected context | Indeterminate/refusal, never selected-project authority | +| Unknown/malformed listing, target drift, symlink escape or replaced namespace | Existing uncertainty/refusal/recovery path; no adoption or activation | + +Decode all rows strictly before selecting a result. Duplicate sentinels, duplicate +expected identities, wrong types and contradictory selected-project rows yield +Unknown. A sentinel plus an exact selected-project record is contradictory and +cannot promote activation. A sentinel plus a user winner is valid blocked evidence; +suppression and shadowing may both be reported. Unrelated valid rows do not change +selected identity. Two matching user/project candidates are ambiguous unless a +future pinned native schema explicitly establishes a winner. A foreign same-name +selected-project path always refuses ownership independently of native ordering. + +Filesystem receipt/namespace/path/digest verification independently establishes +owned delivery. After exact owned delivery, known suppressed/disabled/shadowed +native state is Prepared+Installed with actionable next step, never Active. +Only an exact enabled selected-project record confirms native discovery; no MCP +operation claim follows. Unknown or malformed output retains honest uncertainty +and pending recovery as the existing transaction contract requires. + +Removal consumes exact owned directory authority even when inactive, suppressed, +shadowed or absent; no native plugin uninstall, force-disable, user settings edit +or trust mutation. Preserve active foreign path and symlink protections. Repair +must not acquire ownership of a same-name foreign target. Tests model user and +project candidates together rather than scanning only the selected fixture root. +Exercise the initial untrusted public add as well as owned update/repair/remove, +unowned exact targets under suppression, malformed mixed rows and both user/project +precedence orders. Assert NativeDiscoveryReconciled remains false for recognized +blocked observations. Preserve operator activation flag rejection and user behavior. + +### Saved status, repeated operations and native evidence + +Engine.Inspect remains host-free: plain status reports saved Prepared/Installed +and generic next action, not a freshly observed trust/shadowing reason. Precise +blocker text belongs to fresh activation/update/repair outcomes. Runner-count regressions must prove ordinary inspect/status launches no native process. No new persisted +blocker field or hidden native status command is included. If an intact repair +reconstructs its result from a binding, preserve fresh transient guidance narrowly. +Do not globally declare Prepared converged. Verify repeated blocked add/sync and +repair retain exact bytes and honest mutation/no-change classification. + +Inactive owned removal uses the ordinary verified transactional deletion path; +the adapter only admits it. A previously Active binding with missing/contradictory +native evidence retains its conservative refusal until a supported confirmed +operation records the blocked state. Do not infer new ownership from old activation. + +The genuine untrusted native project fixture must exercise prepared add, repeated +install, update, missing-directory repair and removal using its public owned binding +path. It must verify the native suppression boundary and unchanged user/A/B files. +It must **not** mark native_skill_discovery or native_mcp_initialization as passed +using filesystem reads. Those stages remain blocked/not evaluated. Keep existing +user native runtime requirements unchanged. Trusted project discovery is a distinct +qualification with actual observed output, not a rewritten label or a trust grant +performed to make this gate pass. The harness/evidence validator must encode both +contracts explicitly; a successful prepared journey does not certify loaded plugins. + +## D. Parallel client lanes + +| Lane | Work and acceptance boundary | +| --- | --- | +| Codex | Preserve .agents/skills + .codex/config.toml route; latest observed native regression and exact existing pinned matrix. Record selected config and MCP startup separately as attempted/passed/blocked/not evaluated; use a local harmless server only through a permitted native observation surface. No catalog-cache ownership claim. | +| Cursor, Gemini, Copilot, Kiro, Kimi, Windsurf / Devin (`windsurf`) | Reconcile implemented adapters; fix only demonstrated shared/client defects; verify complete prepared lifecycle and exact version/harness admission. Preserve specific trust/manual/session guidance. | +| OpenCode | Reuse existing root config and project components; add genuine project discovery/configuration/lifecycle to existing pinned native harness. Do not mix native v1 and v2 schemas. | +| Grok | Integrate existing prepared skills-only leaf; independent final reviews, lint/platform lifecycle checks. MCP remains explicitly unsupported unless a new separately reviewed spike establishes it. | +| Qoder | Execute already twice-reviewed populated user/A/B research at pinned1.1.78, inspect output independently; then review a concrete adapter plan before coding. Use real scoped install/list/uninstall semantics, exact scope/source/path/version/enablement identity and retained-data behavior. Empty list evidence is insufficient. | +| Cline | Execute reviewed pinned3.0.73 collector, independently interpret effective config/skills output. Then review and implement native project skills plus explicit session-MCP preparation at .uap/clients/cline. Show launch action; ordinary startup and operator attestation cannot make session config durably active. | +| VS Code | Implement and review already twice-reviewed pinned1.143.1 help/version collector; obtain publisher evidence, then review a narrow workspace skills/.vscode/mcp.json observation and adapter plan. Preserve user backend; project backend independent of Copilot. No unrelated VS Code work. | +| ChatGPT | Preserve explicit pre-acquisition project unsupported, including --no-save. A native feature absent from ChatGPT must not be invented. | + +Phase gates are real: a successful help/version collector does not approve an +unobserved parser or establish native activation. Where the native client cannot +provide a capability, expose the supported prepared/unsupported result precisely. +A client still lacking a required observed installation contract is an outstanding +completion item, not silently counted as fully implemented. + +## E. Review, integration and merge + +1. Obtain two independent plan-review/fix rounds for this completion plan. For each + evidence-dependent lane, obtain two rounds on the concrete post-spike design + before product code. Preserve findings, resolution and reviewed source identities. +2. Implement shared/CLI, Claude, and remaining native/component groups in isolated + branches/worktrees concurrently. Commit and push small coherent increments. + Do not force-push over other work. Integrate reviewed changes with original + authorship/history and preserve per-client PR traceability. +3. Two independent implementation reviews examine scope leakage, ownership, + precedence, interruption/recovery and native evidence interpretation. Fix all + blocking findings; re-review changed logic, not just the earlier revision. +4. Run meaningful exact-head gates: core, lint, vet, generated contracts, + cross-platform builds, three-platform project contracts, affected terminal + flows, source/Skills lifecycle, all-client composed admission/maintenance and + pinned native matrix. Extend evidence selection without dropping required cases. +5. Test supported user+A+B outcomes through production composition: add, unchanged + replay, explicit update, exact-pin repair, status, interruption/recover/remove; + retain foreign artifacts and plugin data. Verify cross-client shared skill paths + and separate Copilot/VS Code project MCP authority. Include one actual process- + death/reopen case after a skill-directory swap and before MCP commit/acknowledgment: + unresolved native attempt must fence blind retry/adoption and preserve the bytes + needed for recovery or honest refusal. Reuse existing attempt/journal machinery. + Distinguish fixtures from native sessions and native configuration from MCP + handshake/tool execution. +6. Update user docs, help/capability matrix and truthful source availability with + exact remaining native boundaries. Per client, record implementation contract, + fixture lifecycle, observed native discovery, observed MCP initialization, + trust/account/session restrictions and exact tested version/platform. A missing + installation contract remains an outstanding item. Cover every canonical + SupportedClientIDs() entry, including ChatGPT unsupported-before-acquisition. + Do not relabel historic binaries as supporting + a feature merged later. Record actual commands/results and source SHA. +7. Refresh main, integrate any relevant movement, rerun affected gates on the final + assembled SHA, then merge the reviewed PR using expected-head protection. Do not + bypass failing required checks, unresolved reviews or missing implementation + evidence. Close/supersede included feature PRs truthfully. Report merged SHA, + verification results and native/product limitations. diff --git a/docs/plans/project-install-e2e/completion-process-coverage-directory.md b/docs/plans/project-install-e2e/completion-process-coverage-directory.md new file mode 100644 index 000000000..22c0c2f84 --- /dev/null +++ b/docs/plans/project-install-e2e/completion-process-coverage-directory.md @@ -0,0 +1,76 @@ +# Keep completion stderr exact under Go coverage instrumentation + +Core run `40201578593`, job `121193687884`, at source +`6a1d53f4b69f6761dd62039bd36404dad7119a04` reports +`TestReleaseCompletionProcessStderr` case zero with a successful exit and +54 stderr bytes in its informational instrumented pass. The fixture's source +blob is `9a45d418515bbba326a48e313cd6f247d4d96e2b`, unchanged from main. + +The child re-executes only its own Go test executable. Its explicit environment +omits `GOCOVERDIR`. The pinned local Go 1.26.10 producer, +`src/internal/coverage/cfile/emit.go:133`, writes a fixed 54-byte warning when +that variable is empty in an instrumented executable. The log reports the byte +count rather than the raw stderr; deterministic instrumented red/green checks +will verify the fixture correction. No vendor executable, account, credential +or native containment prerequisite is involved in this own-test child. + +## Exact correction + +Allocate one separate `t.TempDir()` beside the existing private fixture home. +Set `GOCOVERDIR` to that coverage directory in the existing explicit child-env +allowlist. Do not inherit the caller's coverage directory or whole environment, +reuse the profile home for coverage files, filter stderr, change any completion +argument/protocol, change child dispatch, or relax the zero-stderr, token/debug +file and output assertions. Uninstrumented execution keeps the same assertions. + +The dedicated Coverage workflow currently excludes this test, with a comment +covering it and the unrelated version-probe fixture; this exclusion appeared in +commit `d50723354067e0bdada0e59b0db54dcbb7e190e8`. After the instrumented fix is +verified, remove only the completion exclusion from its CLI coverage command. +Move/narrow the comment to the remaining version-probe exclusions and preserve +those exclusions, instrumentation, package selections, timeouts and all other +workflow settings. The corrected completion test must run in that coverage gate. + +## Review and bounded validation + +Root and the second reviewer perform two independent plan rounds before edits, +then independently review the frozen source/workflow diff. Coordinate the one +Go build slot and disk headroom before running the unchanged exact test with +`-cover -count=1`: it must reproduce the instrumented failure without source +edits. Then make only the planned test/env and workflow edits. Run the same +instrumented test, the exact test with atomic coverage, and the uninstrumented +test; require every completion case to pass with the original assertions. +Run affected-source configured lint, YAML parsing and exact workflow-diff checks. +Do not add a mirror test, disable coverage, change Core's informational status, +execute a vendor client, bypass a native gate, or repeat broad suites locally. +Publish the narrow reviewed fix promptly; final normal CI remains required. + +## Compatibility fixture follow-up at 2c64dc8c + +Removing the old name-based CLI coverage exclusion also exposes the retained +compatibility package's test with the same name. Exact candidate +`2c64dc8ceb98687a2f9640d93a1078524c25b0e5`, Coverage run `40208387011`, +job `121214150623`, reports `release_compat_test.go:424`: protocol `__complete`, +case zero, successful exit and 54 stderr bytes. Repository-wide Go-source search +finds exactly two definitions and two explicit completion child environments. +The agentplugins copy is corrected; `cli/cmd/plugin-kit-ai/release_compat_test.go` +blob `eccc6f44c4691ae7c8b87da07cd725193af39379` still omits `GOCOVERDIR`. + +Apply the same two-line test-only correction to that compatibility copy: one +separate sibling `t.TempDir()` and one child-only `GOCOVERDIR` allowlist entry. +Preserve `commands.App.Execute`/`newReleaseRoot`, all 40 compatibility protocol +cases and every stdout, zero-stderr, token and debug-file assertion. Do not +change production or compatibility semantics, share a parent coverage/profile +directory, inherit environment, filter output, change the already corrected +agentplugins copy or reintroduce any workflow exclusion. Preserve the useful +compatibility tests under the authoring capability-preservation contract. + +Two independent plan rounds and frozen source reviews precede publication. +The fresh exact-source CI failure supplies the unchanged compatibility red; +no duplicate local red is needed. After the reserved Go slot is released, run +one bounded `-p=1 -count=1 -covermode=atomic` command selecting +`^TestReleaseCompletionProcessStderr$` in **both** `./cli/cmd/agentplugins` and +`./cli/cmd/plugin-kit-ai`. Both now-unexcluded paths must pass their unchanged +assertions. Run configured affected-package lint and verify that the frozen +implementation contains only the two planned compatibility-test lines. No +workflow change, extra fixture, vendor process or broad local suite is needed. diff --git a/docs/plans/project-install-e2e/context-followup-integration-evidence.md b/docs/plans/project-install-e2e/context-followup-integration-evidence.md new file mode 100644 index 000000000..2ebe513db --- /dev/null +++ b/docs/plans/project-install-e2e/context-followup-integration-evidence.md @@ -0,0 +1,32 @@ +# Reviewed project context follow-up integration + +2026-10-23. Integration commit `042452234027d3656dd00e807bafd692549153a1` +contains two clean, ordinary Git merges with no conflict resolution or novel +production edits: + +- Retained-data guidance final `852e3e0b1339c03e0fb00a876dc21439b497075d`, + reviewed source `33004e06cf5d65f112efad0455cc793bc0a56f39`. Both plan and + implementation rounds are committed. Project follow-up commands retain the + validated canonical project selection; user wording and actual purge + admission remain unchanged. +- Shared host context final `8fec136f9dfa4e290e67f71b2f46b28362ccb335`, reviewed + source `64c7ee6d48084da6247a200eb3e77e8e6eb0dc29`. Both plan and implementation + rounds are committed. The selected detected version travels in an ephemeral + request field, and an absent legacy profile is admitted only for an already + validated project request. User and strict sibling-profile checks remain. + +After the clean merges, the public CLI retained-data regression passed in +**1.993s**, including all three JSON/human/saved routes, captured shell argument +boundaries and replay from an unrelated cwd. Exact user/project-B/cache bytes +remain unchanged while only project A's retained data is purged. Existing +POSIX presentation scope is preserved; no additional Windows shell claim is +made by that test. + +The two project-only absent-profile/user-admission tests passed in **0.012s**. +The separately selected `TestProjectPreparePreservesSelectedHostVersion` +passed in **0.010s**. Go1.26.10, bounded `-p=2` execution and fresh disposable +test roots were used. No native agent was launched by these checks. + +This checkpoint supplies shared reviewed context for the remaining client PRs. +It does not enable the pending Kimi/Devin adapters, replace their independent +code reviews or qualify native discovery, authentication, MCP or a release. diff --git a/docs/plans/project-install-e2e/copilot-code-review-r1.md b/docs/plans/project-install-e2e/copilot-code-review-r1.md new file mode 100644 index 000000000..041b6fea6 --- /dev/null +++ b/docs/plans/project-install-e2e/copilot-code-review-r1.md @@ -0,0 +1,99 @@ +# Copilot project implementation independent review R1 + +Reviewer: codex_claude, 2026-10-23. Reviewed cumulative adapter +`5103b9d0..9542b52d0b109acda2883752dfc0748548db2011` and corrective frozen +checkpoints `9129c03f4e932edc52acae860e18e1ec13e0be9b` and +`b98f0c57983433e5c7f2cde76255f94f4da1a079` in work/uap-project-copilot. +The reviewer did not author implementation or edit the author's worktree. + +## Decision: GO at b98f0c57 + +Two selected-package consistency findings are independently verified closed. +No remaining implementation blocker found in the reviewed configured project +component lane. This is not genuine Copilot native discovery, trust, session, +MCP initialization/tool use or release qualification. + +## Findings and closure + +R1-1: original copilotPlanRoot validated project/native/user context but did +not require ActivePath to be the selected physical artifact's exact package. +The shared component helper intentionally validates only project containment; +it cannot infer Copilot's client/artifact namespace. An existing different +package child within the same project therefore needed adapter admission. +Closed in `9129c03f`: validate the physical leaf and literal +.uap/managed/clients/copilot/ path plus containment wherever a resolved +plan is consumed. The early planner precondition remains context-only until +physical target resolution. New regressions cover changed in-project package +and artifact, composed/direct activation, projection and active reuse, with +complete unchanged inventories on refusal. + +R1-2: generic provider activation already required Delivery.ActivePath equal to +Plan.ActivePath, but direct Copilot adapter activation did not repeat that +consistency requirement. Closed in `b98f0c57`: project preflight refuses a nonempty +delivery mismatch while allowing legitimate empty pre-staging delivery; direct +project Activate requires literal equality, including refusal of empty delivery. +An existing other child and empty-delivery regression covers direct activation +and whole-project/user/B unchanged inventories. + +## Reviewed production boundaries + +- Project TargetContext retains the original detected user ConfigRoot and + selects exact project authority. All scope/root mismatches refuse; packages, + state and data remain under selected .uap. Scoped domain policy uses the + existing native-config effect/recovery lifecycle and a distinct Copilot + project backend; user marketplace/global CLI and shared user backend behavior + remain intact. No project operation invokes global plugin or user MCP CLI. +- Exact root .mcp.json is selected when present, otherwise .github/mcp.json; + .github/skills holds native skills. Path containment/alias checks precede + effects. Recorded exact receipt paths fence later effective-config selection + changes instead of silently moving ownership. Existing bare maps, malformed, + duplicate/ambiguous or foreign disabled members refuse without mutation. +- The narrow CodecCopilot reuses the existing kernel, emitting explicit + stdio/http/sse tags and tools:["*"]. Exact native member receipts and shared + component skill swaps/conditional writes own only their selected entries. + Foreign settings/members/skill directories are retained. Unknown/duplicate, + stale and copied binding kinds/paths/artifacts cannot be adopted or removed. + No new configuration transaction engine is introduced. +- Stdio delivery uses the existing trusted launcher with absolute selected + package/data/cwd. Read-only active reprojection recomputes the full selected + source contract against immutable bytes; empty, relative or changed selected + data returns ErrProjectionRefreshRequired without replacing old locators. +- Preflight rejects ActivationComplete before the generic shortcut, including + direct adapter reuse and prepared components. Installed-file success remains + prepared plus installed, without attestation. Trust, native selection, + plugin/session/admin precedence, disabled policy and initialization are + explicitly unobserved. + +Independently reread current primary configuration reference: +https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers +It supports the selected repository paths/precedence, explicit transports and +all-tools shape, and distinguishes trusted native loading from file delivery. +The adapter conservatively refuses bare-map mutation rather than silently +changing its native meaning. + +## Public composition and independent execution + +The saved production CLI fixture uses real loopback TLS Git acquisition, +standard loading/storage/planning/staging, installer/state/native transactions, +A/B install, locked no-op sync, read-only views, installed-pin repair after +source advance, pin update with retained binding/data authority, interrupted +bounded directory recovery, owned remove and repeated absent removal. It +preserves user/B configuration/state, foreign disabled entries and skills, +and persistent data. Detection/security seams are inert; no real native client +or configured server executes. The provider stdio fixture independently checks +the trusted managed launcher and exact selected data receipt. + +Go 1.26.10, GOMAXPROCS=3, -p=2/-count=1/-timeout=3m: + +- Initial full selected `^Test(ProjectCopilot|Copilot)` checkpoint: native + kernel codec **PASS 0.003s**, client **PASS 0.005s**, scoped domain + **PASS 0.003s**, providers **PASS 0.069s**, public saved CLI **PASS 5.652s**. +- Final corrected b98f0c57, `^TestProjectCopilot`: composed providers including + both new no-effect refusal regressions **PASS 0.079s**; complete public saved + CLI journey **PASS 5.378s**. No further optional broad compilation was needed. + +Pinned help/skill-list observations recorded by the author are separate from +this review and do not become new project MCP or runtime evidence. No Copilot +binary, launcher, configured MCP process, account/model/trust operation or +hosted native suite ran in this review. Genuine approved-host project +observations and the unchanged disposable guard remain independent gates. diff --git a/docs/plans/project-install-e2e/copilot-code-review-r2.md b/docs/plans/project-install-e2e/copilot-code-review-r2.md new file mode 100644 index 000000000..1f2c0b8fa --- /dev/null +++ b/docs/plans/project-install-e2e/copilot-code-review-r2.md @@ -0,0 +1,120 @@ +# Copilot CLI project adapter independent code review R2 + +Reviewer: cursor_clients, 2026-10-23. Frozen implementation: +`9542b52d0b109acda2883752dfc0748548db2011` in work/uap-project-copilot; +reviewed the complete client delta from `5103`, narrow native codec, scoped +domain declaration, composed fixtures and public TLS Git lifecycle. Read the +pre-code pin/help evidence and the original two-GO client plan. Rechecked the +current official GitHub MCP/skills documentation linked in that evidence. + +## Decision: GO for the conservative project component adapter + +No code blocker found. The project lane has a distinct exact filesystem +contract and preserves the user marketplace/CLI and unsupported command policy. +Configured files remain prepared with installed-file verification. This review +does not qualify native project discovery, MCP initialization or release. + +### Native format and effective path + +- Copilot's narrow codec maps portable stdio and streamable HTTP/SSE to explicit + native stdio/http/sse entries, with documented tools:["*"]. It delegates + existing field validation and placeholder handling to the current kernel, + refuses unqualified remote transports and native cwd, and leaves the other + codecs unchanged. The tools member selects available tools; this adapter + does not add trust, authentication or permission grants. +- A regular selected-root .mcp.json takes precedence. Otherwise the adapter + selects .github/mcp.json. Root/child aliases refuse through the existing + no-follow containment checks. The .github/skills directory is the sole new + skill lane, with no .vscode config or user registry writes. +- Existing nonempty bare maps refuse before activation instead of being + silently wrapped into a different native meaning. Strict malformed, + duplicate and wrong-shaped documents remain failures. Empty objects can be + initialized. Exact native receipt paths fence a changed selected config; + staged fallback receipts cannot silently become root-config authority. +- Current official MCP documentation confirms repository paths, same-directory + root precedence, nearer-cwd precedence, nested/bare support and the folder + trust boundary. The implementation deliberately supports a narrower safe + write contract and does not infer effective descendant/session/plugin/admin + selection from successful file delivery. + +### Scope, ownership, lifecycle and selected data + +- Original detected user config remains in TargetContext. Project registry is + the selected project root, while managed package/state/data stay under .uap. + Scoped backend identity is Copilot rather than the shared user editor family; + existing user sharing is retained. VS Code implementation is untouched. +- Binding inspection validates project scope, selected artifact/package/native + root and exact native object identities/paths/digests. Unknown ownership is + not filtered into permission to mutate. The only excluded receipt kind is + the core's managed_package_directory. Foreign skills and disabled same-name + entries are collisions; stale managed bytes cannot be overwritten/removed. +- The adapter reuses the reviewed shared skill transaction and ownership-aware + kernel batch. New server values are checked against staged exact receipts; + writer failure rolls back skills, and committed cleanup retains receipts. + Update, missing owned components, dropped MCP and repeated removal preserve + foreign objects and other project/user state. +- Stdio uses the existing trusted managed launcher. Read-only active reuse + recomputes the complete authored stdio contract against the selected package + and data locator, including environment, launcher command/args and cwd. Full + equality with immutable stored projection is required. Missing, relative or + different selected data refuses with projection-refresh-required; inspection + never repairs the mismatch by changing digest-covered package bytes. +- Project attestation is rejected by preflight before automatic/manual fallback. + Project inspection/activation/removal do not invoke a global Copilot command. + Successful native file verification is downgraded to prepared and remains + un-attested. Genuine pre-code skill listing and absent untrusted workspace MCP + evidence are kept separate from the new configuration fixtures. + +### Production composition and evidence limits + +The public saved CLI fixture retains production scoped composition, solver, +store, planner, installer and native filesystem transactions over real loopback +TLS Git. It covers A/B add, exact locked no-op, installed-pin repair despite a +moving source, bounded pending directory-journal recovery and repeated recovery, +moving update, retained source/binding/data authority, conservative saved status, +exact removal and absent repeat without effects. Complete user/B inventories +remain unchanged. Its detector/security seams are explicit, and trusted helper +bytes are copied without execution. The journal fixture is not relabeled as a +genuine native process restart. No export/ZIP/native-runtime claim is inferred +from this particular fixture. + +## Independent bounded verification + +- Project providers: **PASS 0.182s**; narrow Copilot native codec: **PASS 0.010s**; + scoped domain policy: **PASS 0.003s**. +- Full existing Copilot client suite: **PASS 0.006s**. +- Public saved production CLI TLS Git lifecycle: + TestProjectCopilotComposedSavedLifecycle — **PASS 5.529s**. +- Complete delta whitespace check and clean frozen worktree verified. Read the + shared component request/commit path to confirm selected receipt enforcement, + no-write missing removal and rollback semantics are actually composed. + +No native Copilot client, launcher, MCP, model, trust or account process was +executed by this review. Genuine hosted native and cross-platform qualification +remain pending; this GO supports publishing the reviewed project client draft. + +## Final R1 correction refresh: GO at `9129c03f4e932edc52acae860e18e1ec13e0be9b` + +Reviewed the narrow final correction in scope.go/project.go and its composed +regression. The full project plan now requires a valid selected artifact ID and +literal `.uap/managed/clients/copilot/` ActivePath with no-follow project +containment. The earlier planner precondition checks only scope/root because +generic target resolution has not populated ActivePath at that point. Direct +active projection validates the full plan before data/server reads. + +The new regression rebinds an existing in-project package or changes the +physical artifact ID, then requires refusal through composed/direct activation, +direct projection and direct active reuse, with the complete native/package/ +user/B inventory unchanged. Independently executed that exact regression: +**PASS 0.036s**. This closes the R1 exact-artifact gap and preserves this R2 GO. +Other changes in the final leaf are previously reviewed core/source/CI ancestry; +this refresh introduces no broader native/runtime qualification claim. + +Final adjacent direct-delivery closure: **GO** at +`b98f0c57983433e5c7f2cde76255f94f4da1a079`. Preflight accepts an absent delivery +only during the real pre-staging phase and rejects any supplied different +ActivePath. Direct activation requires literal Delivery.ActivePath equality +with the already validated exact project artifact. The regression covers an +existing other child and empty delivery, and preserves the complete base +inventory on refusal. Independently ran both exact-artifact/direct-delivery +regressions: **PASS 0.033s**. All earlier R2 conclusions remain in force. diff --git a/docs/plans/project-install-e2e/copilot-feature-integration-reviews.md b/docs/plans/project-install-e2e/copilot-feature-integration-reviews.md new file mode 100644 index 000000000..2f5979a69 --- /dev/null +++ b/docs/plans/project-install-e2e/copilot-feature-integration-reviews.md @@ -0,0 +1,21 @@ +# Copilot feature integration reviews + +Reviewed source: `857b0019bfac27a30ec6a50f7a82dc812b70ed1c`, merging the published Copilot leaf `88a239a8147fd9fb877036ed6d9d23415ecfa428` with feature head `b5737650da6cade23d73b63aa9e3cd4e9329f4f8`. The exact source tree is also published at `a347c322b1661757472aeae4f8729fef5ca55cb3`. This integration adds the already reviewed Gemini, ChatGPT and Cursor ancestry to the Copilot stack; it does not add another client implementation. + +## Independent R1 — GO + +Reviewer: `finish_sources_vscode_cline`, GPT-6.1 Sol xhigh, 2026-10-24. The sole combined conflict is the adjacent Cursor/Copilot domain declaration. Both retain their own `withProjectScope(..., LifecycleNativeConfig)` wrapper. Copilot retains `withoutCommands`; both original user CLI traits and the adjacent VS Code declaration remain unchanged. The resolved pair is identical to the pair already reviewed in assembly `a9dd4402`. + +Independent Git byte comparisons found no differences in the Copilot client directory or native Copilot codec against `88a239a8`, and no differences in the Cursor directory against `b5737650`. No new client declaration or generic policy change was introduced. This review made no source edit and ran no Go or native process. + +## Independent R2 — GO + +Reviewer: `finish_codex_claude_qoder`, GPT-6.1 Sol xhigh, 2026-10-24. An independent `git merge-tree` comparison of the two parents found exactly one conflicting file, `domain/clients.go`. Every other blob equals Git's automatic merge result, including the documentation and compatibility golden unions. The manual resolution preserves both project wrappers, Copilot's command exclusion, and the original user traits without another semantic resolution. + +Independent byte comparisons found the complete Copilot client directory and native configuration directory unchanged from `88a239a8`, and the complete Cursor client directory unchanged from `b5737650`. This review ran no Go or native process. + +## Fresh verification and remaining gates + +Root independently ran the complete domain package at this exact merge using verified Go 1.26.10: `go test -p=2 ./install/integrationctl/agentplugins/domain -count=1 -timeout=90s` — PASS, **0.039s**. Whitespace validation passed. The earlier combined public project CLI result applies to the reviewed assembled implementation, not to a newly claimed native run. + +Shared canonical-fixture corrections, authoring documentation provenance, final assembled cross-platform CI, and the explicitly unqualified native discovery/runtime observations remain separate gates. These integration approvals authorize merging the reviewed leaf into the project feature only; they do not qualify main or a release. diff --git a/docs/plans/project-install-e2e/copilot-implementation-evidence.md b/docs/plans/project-install-e2e/copilot-implementation-evidence.md new file mode 100644 index 000000000..e210735f1 --- /dev/null +++ b/docs/plans/project-install-e2e/copilot-implementation-evidence.md @@ -0,0 +1,201 @@ +# Copilot CLI project component pre-code evidence + +Recorded 2026-10-23. The original 14-client plan has two independent GO rounds; +this narrows its already approved Copilot component lane with pinned artifact +and observed help/list contracts. No Copilot product implementation has been +changed at this checkpoint. + +## Official pinned artifact + +- Release: **1.0.103**, published 2026-10-22, + https://github.com/github/copilot-cli/releases/tag/v1.0.103 +- Official archive: + https://github.com/github/copilot-cli/releases/download/v1.0.103/copilot-linux-x64.tar.gz +- GitHub release API asset digest and independently measured archive SHA256: + `9aeb4430e9ac9523546db07b227c7aba179a4710a55f9ac29ad143b0f9e1b325`. + Exact archive length: **129,018,878 bytes**. +- The archive contains one regular `copilot` executable, **198,445,888 bytes**. + Independently measured executable SHA256: + `f19b3554a6b62605db3a26d5255ebe2a3ee39fcd6a357edb18301ea5037f6f0d`. + +Version/help/list probes used the managed network-disabled filesystem sandbox +with fresh HOME, XDG roots and project cwd, an explicit executable and minimal +environment without inherited credentials, proxy, account or product settings. +An attempted explicit network namespace was refused by the kernel before any +client launch; no namespace isolation result is claimed. The successful managed +sandbox probes neither enabled a native-suite opt-in nor bypassed its hosted +guard. These are pre-code observations, not hosted native qualification. + +## Observed native contracts + +`--version` exited zero and reported `GitHub Copilot CLI 1.0.103.`. Top-level, +`mcp`, `mcp list`, `skill` and `skill list` help all exited zero. The pinned CLI +documents machine-readable listing via `mcp list --json` and `skill list --json`. +MCP commands name user, workspace and installed-plugin sources; skill commands +name project, personal, plugin and custom roots. + +In a fresh real Git repository, `skill list --json` returned the inert +`.github/skills/uap-precode-observer/SKILL.md` marker with its name, description, +`source: project`, exact **directory** path and `enabled: true`. Other native +builtin rows remained distinct. This observation does not execute the skill. + +With a distinct user HTTP config marker and root `.mcp.json` marker, +`mcp list --json` returned a `mcpServers` object containing the exact user +marker with `source: user` and `enabled: true`; the untrusted workspace marker +was absent. Both URLs were inert loopback port 9 and outbound network syscalls +were blocked by the managed sandbox. No trust, account, model task, configured +server invocation, permission grant or tool execution was requested. The absent +workspace marker is consistent with the documented trust boundary; it supplies +no project MCP discovery or initialization proof. + +## Official configuration and authority references + +- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers + documents repository `.mcp.json` and `.github/mcp.json`, rootward Git-boundary + discovery, nearest-directory and same-directory `.mcp.json` precedence, nested + or bare server maps, trust requirements and exclusion of `.vscode/mcp.json`. +- https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference + documents additional/session config and plugin precedence over workspace + definitions. Current official search retrieval supplied this reference; + direct page retrieval timed out, so no independent full-page snapshot is + claimed. +- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills + documents project `.github/skills`, `.agents/skills`, `.claude/skills` and + separate personal paths plus explicit enable/disable and discovery commands. + +## Implementation direction within the approved plan + +Use the validated original user/project context and reviewed shared exact +component transaction. Project mutable/package/data paths stay in `.uap`; +discovered skills use `.github/skills`. Select root `.mcp.json` when present, +otherwise `.github/mcp.json`, while fencing aliases and any changed effective +selection against recorded exact ownership. Reject preexisting bare maps before +effects rather than silently wrap them and lose foreign server meaning. + +Add the narrow Copilot transport codec to the existing kernel: native stdio +command/args/env and explicit HTTP/SSE tags. Reuse the existing managed stdio +launcher to establish cwd and package/data locators. Retain user global +marketplace behavior unchanged; project add/remove must never run global plugin +or user MCP mutation commands. Existing unsupported portable command policy +remains in force. + +Successful exact configuration stays **prepared plus installed**. Fresh native +project discovery, folder trust, plugin/session/admin precedence, disabled +selection, MCP initialization and tool use remain separate observations. +Project activation attestation cannot supply those observations. Copilot and +VS Code have distinct project physical backend identities and native config +paths; shared user backend behavior remains unchanged. + +## Worktree checkpoint + +`feat/project-copilot-e2e` at the pre-code merge `e3d0f475` contains integration +`d9f3f757` plus reviewed helper ancestry `c1b523b9`, with no other new client +adapter. Root publication alignment and final core corrections remain separate. +No real Copilot project install, native runtime certification, hosted runner, +release, remote publication or user account operation was performed. + +## Implemented project component contract + +The project branch now uses the existing scoped planner, managed artifact/data +paths, exact native ownership kernel and reviewed component transaction. It +selects a regular root `.mcp.json` when present and otherwise +`.github/mcp.json`; owned receipts fence subsequent selection changes. Existing +bare maps and malformed/ambiguous objects fail before native effects. Foreign +servers (including disabled entries), settings and skill directories remain +outside ownership. Copilot's narrow codec emits explicit `stdio`/`http`/`sse` +and native `tools:["*"]`, preserving exact member receipts rather than replacing +whole foreign documents. + +Selected stdio uses the existing managed launcher and explicit package/data/cwd +binding. Read-only active projection recomputes the selected source contract +against the current package and selected data locator; a missing, relative or +different data locator refuses with `ErrProjectionRefreshRequired`, with no +immutable bytes rewritten. Project registry inspection does not launch the +global Copilot CLI. Prepared inspection catches exact native collisions even +without native execution, and stale recorded scope/artifact/locator/config +selection fails closed. + +All project activation attestations are rejected before automatic/manual +fallback. Exact successful configuration remains prepared plus installed and +not attested. Native project discovery, folder trust, plugin/session/admin +precedence, disabled selection, MCP initialization and tool use stay unobserved. +The user marketplace/global CLI and unsupported portable command policy remain +unchanged. The project physical backend is `copilot`, distinct from editor +project delivery; user backend sharing remains intact. + +## Executed local verification + +Production loader/planner/stager/provider/kernel contracts cover both effective +MCP paths; foreign settings and user/B isolation; exact install, verify, update, +missing-skill repair, dropped MCP and repeated removal; bare/malformed maps, +disabled same-name collision, skill collision, changed selection after staging, +native-writer failure with rollback, context/alias refusal, stale ownership, +attestation refusal and selected-data immutability. Focused provider suite: +**PASS 0.078s**. Copilot client **PASS 0.008s**, native kernel selection **PASS +0.006s**, domain policy **PASS 0.005s**, compatibility goldens **PASS 0.016s**. +The only four golden differences add Copilot's project scope. + +`TestProjectCopilotComposedSavedLifecycle` uses the public CLI and production +ForScope/solver/store/planner/installer/native-file transactions with real TLS +Git acquisition: saved A/B add, locked no-op ownership stability, exact old-pin +repair after moving source, actual pending directory-journal recovery and +repeat recovery, moving update retaining binding/data authority, conservative +saved status, exact remove and absent repeat without effects. The complete user +Copilot profile and project B stay byte-identical. **PASS 6.620s**. Host detection +and package-security assessment are inert seams. The trusted test executable is +copied as managed launcher bytes and never executed; no fake Copilot process or +native proof is involved. Pending-journal recovery is a bounded production +journal fixture, not an observed native-process crash/restart. + +Exact changed-code lint and whole-file size/architecture gates for the component +packages passed with **0 issues**; public CLI changed-code lint also passed with +**0 issues**. The full existing native-config kernel suite then passed in +**3.337s**, the full Copilot client suite passed in **0.009s**, and the public +CLI whole-file size/architecture gate passed with **0 issues**. Two independent implementation reviews are pending at this +checkpoint. Genuine pinned pre-code observations above remain distinct from +the new production project contract. No native-suite guard, hosted environment, +authentication, trust, vendor model, release or publication operation changed. + +## Independent R1 correction: exact physical artifact + +R1 found that initial project admission checked project containment but did not +require `ActivePath` to be the selected Copilot physical artifact. The corrected +plan admission validates the artifact leaf and literal +`.uap/managed/clients/copilot/` path before projection, +activation, inspection and active reuse. The generic precondition still checks +scope/root before the planner resolves its physical artifact; subsequent +refinement and lifecycle use the complete exact validation. + +New regressions change either the package to another existing in-project child +or only the selected artifact ID. Composed activation, direct activation, +direct projection and read-only reuse all refuse with identical complete +filesystem inventories. Focused providers **PASS 0.073s**, client **PASS +0.006s**, domain **PASS 0.006s**, compatibility **PASS 0.014s**. The independent +reviewers must close the correction before final publication/merge approval. +Public saved lifecycle recheck **PASS 5.614s**; exact changed-code and full-file +size/architecture correction lint gates both **0 issues**. + +R1 additionally required the direct adapter to enforce delivered-path equality, +which the composed provider already checks. Project preflight now rejects any +nonempty delivery path different from the selected plan; pre-staging empty +delivery remains admissible. Direct activation requires exact nonempty equality. +The new delivery-only mismatch regression verifies the existing foreign child +and empty direct delivery both refuse without any base inventory changes. +Full project provider suite **PASS 0.072s** at this follow-up. + +## Final independent implementation reviews + +Both independent implementation reviewers closed **GO** at +`b98f0c57983433e5c7f2cde76255f94f4da1a079`. R1 independently reran final project +providers **PASS 0.079s** and complete public saved CLI **PASS 5.378s**; R2 +independently reran exact artifact/direct delivery refusal contracts **PASS +0.033s**, retaining earlier full project/provider/codec/domain/client/public +saved CLI checks. The reports are committed beside this evidence. No blocking +implementation finding remains. + +This is a reviewed production configuration/ownership checkpoint. Genuine +Copilot project native discovery, trust, MCP initialization/tool use and restart +qualification remain pending; neither source tests nor the pinned pre-code +untrusted listing is promoted to those claims. The extracted vendor executable +was hash-checked against the recorded binary pin and reclaimed to restore task +disk headroom; its original archive, provenance and all research logs remain. diff --git a/docs/plans/project-install-e2e/cursor-code-review-r1.md b/docs/plans/project-install-e2e/cursor-code-review-r1.md new file mode 100644 index 000000000..bfd68b564 --- /dev/null +++ b/docs/plans/project-install-e2e/cursor-code-review-r1.md @@ -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 `/.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. diff --git a/docs/plans/project-install-e2e/cursor-code-review-r2.md b/docs/plans/project-install-e2e/cursor-code-review-r2.md new file mode 100644 index 000000000..a9206941e --- /dev/null +++ b/docs/plans/project-install-e2e/cursor-code-review-r2.md @@ -0,0 +1,85 @@ +# Cursor project implementation independent review R2 + +Reviewer: core_clients, 2026-10-23. Reviewed adapter `524b808b`, composed +public CLI lifecycle/transfer `37bbb5d7`, and corrective frozen +`f55dc4628a8ce1b671b80e82d07dd0d66011eb15`. The reviewed shared helper and +original two client-plan GO results are separate records. No production file +was edited by this reviewer. + +## Decision: GO at f55dc462 + +One selected-data-authority finding is independently verified closed. No +remaining implementation blocker was found in the configured project component +lane or its composed source lifecycle. This does not supply native Cursor, +MCP runtime, editor or release qualification. + +## R2-1: active reprojection ignored selected data authority + +At `37bbb5d7`, ProjectActiveNative ignored its final dataPath parameter. Core's +activeNativeDelivery validates and supplies the selected persisted data receipt, +but the adapter returned receipts derived only from the old immutable mcp.json. +A different selected data locator could therefore silently retain the prior +launcher/environment locator instead of refusing the inconsistent projection. + +Closed in `f55dc462`. The project-only read-only verifier now requires selected +stdio data to be absolute and nonempty, exact launcher/package/data environment +bindings, no native cwd field, and the exact managed launcher protocol prefix +for mode, package, data, resolved cwd, anchor and separator. It recomputes the +cwd prefix from the current standard envelope and supplied selected locators. +Inconsistent authority returns ErrProjectionRefreshRequired without rewriting +digest-covered package bytes. Remote components and the preserved user lane +retain their prior behavior. The existing positive stdio contract now supplies +its actual data locator; wrong and empty selected-data regressions require +refusal and an unchanged immutable package inventory. + +## Scope, ownership and lifecycle assessment + +- Exact validated project context selects `.cursor` while the original user + config root remains distinct. Planning, active projection, activation, + binding validation and removal fence the scoped registry and package path. + Mutable state/data/prepared packages stay under the selected `.uap`. +- Declarative project policy uses the existing native-config lifecycle rather + than borrowing user CLI-registry policy. User manual package preparation, + detection and existing attestation behavior remain intact. +- Skills and existing rendered command skills use exact directory receipts; + MCP members use the existing transactional ownership kernel. The adapter + removes only the managed-package receipt from native component inputs and + leaves unknown kinds visible for closed refusal. Foreign/disabled same-name + objects collide; removal never owns an entire shared config document. +- Managed stdio uses the existing launcher and explicit package/data/cwd + protocol. The new active check validates those bindings read-only. Source + directories and immutable package bytes are never executed by the proof. +- Successful file configuration remains prepared plus installed verification. + Project preflight rejects activation attestation before provider shortcuts, + including prepared component selections and direct adapter reuse. Discovery, + aliases, enablement, policy, approvals and MCP initialization stay unobserved. +- The composed Git/ZIP journey retains production scope composition, solver, + locks, immutable store, planner, stager, installer and native transactions. + Only detection and security-assessment boundaries are inert. It exercises + saved A/B add, byte-stable locked sync, read commands without effects, + missing-native-skill repair, moving pin update, exact portable export, + cold-cache import/install and exact removal. Foreign user config/hooks/skills + and project-B native/state inventories remain exact while A changes. +- Remote-only composed fixtures avoid launcher or configured-command execution. + Separate provider tests cover stdio/data, rendered commands, no-op/update, + withdrawn MCP, repeated removal, malformed JSON, foreign collisions, writer + failure rollback, scope/context mismatch and contained native aliases. + +## Independent final execution + +Go 1.26.10, GOMAXPROCS=3, `-p=2`, bounded restored RAM toolchain/cache: + +- Complete Cursor client package: **PASS 0.088s**. +- Final `^TestProjectCursor` production provider contracts: **PASS 0.124s**. +- Final public `TestCursorProjectComposedSourceLifecycle` real TLS Git/ZIP + lifecycle and cold transfer: **PASS 7.667s**. + +No Cursor process, model request, configured MCP command, package payload, +trust/approval/login change or hosted native proof was executed. Author's +corrective lint gates and the other independent review remain applicable to +the final checkpoint. Actual native discovery, fresh native restarts, editor +behavior, MCP handshake/tool invocation and release qualification remain pending. +# Hosted selector closure — 2026-10-23 + +Independent R2 rechecked isolated `32c33c6f` against its parent: the only test-code change renames `TestCursorProjectComposedSourceLifecycle` to `TestProjectCursorComposedSourceLifecycle`. The entire Git/ZIP lifecycle body and assertions are unchanged. The existing cross-platform project-contract selector `Test(TargetContext|Project|PrepareRejectsCrossEnvironmentContext)` now includes the composed test through its `TestProject` prefix. The accompanying evidence note correctly leaves pending hosted/native runs pending. **GO for this symbol-only selection correction.** No additional compilation was needed to establish this exact rename preservation; previously reviewed composed behavior retains its prior evidence. + diff --git a/docs/plans/project-install-e2e/cursor-component-helper-amendment.md b/docs/plans/project-install-e2e/cursor-component-helper-amendment.md new file mode 100644 index 000000000..4fe688cb8 --- /dev/null +++ b/docs/plans/project-install-e2e/cursor-component-helper-amendment.md @@ -0,0 +1,146 @@ +# Cursor native component helper amendment + +## Scope + +Cursor project components are already approved in both client plan reviews. The +existing user Cursor manual package preparation stays unchanged. This amendment +adds only new `clients/shared/project_components*.go` helpers consumed by Cursor and the concurrently reviewed Codex project adapter; +no existing Gemini/Cline/OpenCode implementation or shared nativeconfig codec is +changed. There is no client discovery, settings engine, native executable or +new source engine. The helper uses existing `nativeconfig.Kernel` and +the caller-selected existing/independently reviewed codec for native MCP. Cursor +selects `CodecMCPServers` (command/args/env/cwd or url/headers); Codex selects its +separately reviewed native TOML codec. Future clients require their own reviewed +qualification before reuse. + +## Exact inputs and preflight + +A small explicit layout selects project boundary, skills root, exact native +config Paths, codec, and skill/MCP ownership kinds. Cursor supplies selected +ProjectRoot, `.cursor/skills`, `.cursor/mcp.json` and distinct project kinds. +Codex supplies selected ProjectRoot, `.agents/skills`, `.codex/config.toml`, +its independent codec and distinct project kinds. Every supplied filesystem +root/config is an exact no-follow strict child of the selected project boundary. Adapter validates matching ScopeProject/TargetContext, +original user ConfigRoot, exact planned NativeRegistryRoot before any helper. +Project artifacts remain exact `.uap/managed/clients/cursor/`; +user ConfigRoot is never substituted. Shared helper requires clean absolute +project boundary and child roots/config paths, exact owned native paths, leaf names, no-follow containment, nonempty +digests, unique object IDs/names and only its declared kinds; unknown prior or +desired ownership fails closed. Project binding validator checks these paths +plus exact recorded native root; core independently validates selected target, +namespace, scope and project-state identity. + +Adapters retain projection and server-dialect responsibility. Cursor derives +skill digests from already-rendered staged skills and MCP receipts from its +already-resolved staged `mcp.json`; Codex projects its own native codec. Each +adapter reads its immutable package with strict unique-key JSON/no-follow +containment, preserving existing managed stdio launcher, package/data/cwd +binding. The shared mutation accepts prepared native servers keyed by logical +name and exact desired ownership receipts, then compares kernel DesiredReceipt +before mutation. It does not load a descriptor, expand placeholders, implement +portable/native transport mapping, or execute code. Active reprojection reads +digest-covered files without writing the package. Caller native-root/scope +validation remains separate from helper's boundary containment validation. + +## One mutation and rollback + +Before creating a transaction: require injected kernel file IO, validate all +prior+desired identities/paths, verify existing prior directory digest/exact +whole-entry MCP receipt (allow missing for repair/remove), and reject a newly +requested object that already exists without prior ownership. Build all MCP +requests and Desired receipts from caller-prepared native servers; receipt/path/digest must +match staged desired ownership before native changes. Same-name foreign and +disabled entries collide; never enable, adopt or remove them. + +When skills change, create a private same-filesystem transaction under selected +explicit skills root; stage desired copies and verify digests before native writes. +Move previous owned skills to private backups with exclusive rename, reverify +isolated backup digest, then exclusively install desired directories. Identical +prior/desired skill digests may stay in place after verification. Recheck +installed desired skills and cancellation before the one kernel `ApplyBatch` +for MCP. Kernel retains its existing lock, exact-entry preconditions, desired +receipt check, atomic replacement, rollback and postwrite verification. + +If skill or MCP operation fails before confirmed config commit, remove only +newly installed skill directories that still match their digest, restore exact +backups without overwriting a foreign destination, and report native effect +unchanged only when the observed rollback plus kernel failure supports it. +Unknown/partially committed MCP errors remain uncertain even if skills restore; +core retains its existing pre-mutation attempt/pending ownership/recovery path. +If rollback cannot remove an externally changed install or restore a backup, +return a recovery error naming the private transaction and retain it. Never +remove that only recoverable copy. Cancellation before mutations is unchanged; +after mutation begins it follows the same observed rollback rules. + +On successful ApplyBatch, or its existing CommittedCleanupError, native commit +is authoritative: keep installed skills, drop private backups, return committed +outcome; lock cleanup degradation uses existing lifecycle warning. If private +transaction cleanup itself fails, report committed cleanup degradation rather +than rollback committed config. No persistent application data directory is +removed. Empty transaction/skills parents may remain; foreign file bytes/modes, +user/project-B configs, hooks and skills are untouched. No copying of opaque +native state, HOME redirection or session/global MCP fallback. + +## Contract validation and evidence + +Tests through actual Cursor provider/loader/planner/stager, plus Codex consumer +contract fixtures for its distinct root/codec: project skill+MCP +add, repeat verification/noop, changed skill/MCP update, removed component, +exact-pin ownership repair, repeat remove, user/A/B byte+mode inventory, +foreign disabled collision/malformed config and symlink/path/root refusal. +Inject kernel failure after skill installation to prove exact restoration; +change an installed skill at failure boundary to prove preservation+retained +recovery. Exercise staged-receipt tamper and config-only/no-skill requests. +Runtime outcome is always project prepared+installed; generic attestation is +refused in preflight and direct adapter reuse. Tests are inert contract evidence, +not real Cursor discovery or MCP execution. Current official artifact/help +research and hosted native qualification are recorded separately. + +## R1 applied clarification + +A same-ID/same-digest skill no-op requires the destination to be observed +present as an exact real directory with the expected digest. `allowMissing` +verification alone never qualifies a no-op: a missing same-pin skill is staged +and recreated. Dedicated repair proof removes the owned skill without changing +its desired receipt, then proves restoration. All existing symlink ancestors of +skills/config are refused even when their target stays inside the selected +project; tests cover contained aliases as well as external escapes. + +Frozen public helper signatures: + +- `ProjectComponents{ProjectRoot, SkillsRoot string; ConfigPaths nativeconfig.Paths; Codec nativeconfig.Codec; SkillKind, MCPKind string}` +- `(ProjectComponents).ValidateObjects([]domain.NativeObjectOwnership) error` +- `(ProjectComponents).SkillObject(name, sourceRelative, stagingRoot string) (domain.NativeObjectOwnership, error)` +- `(ProjectComponents).MCPObject(name string, server nativeconfig.Server, resolvedPath string) (domain.NativeObjectOwnership, error)` +- `(ProjectComponents).Verify(objects []domain.NativeObjectOwnership, allowMissing bool, kernel nativeconfig.Kernel) error` +- `(ProjectComponents).Inspect(components []domain.ComponentDecision, managed *domain.ClientBinding, kernel nativeconfig.Kernel) (clients.RegistryFinding, error)` +- `ProjectComponentMutation{Layout ProjectComponents; ActivePath string; Previous, Desired []domain.NativeObjectOwnership; Servers map[string]nativeconfig.Server; Kernel nativeconfig.Kernel}` +- `(ProjectComponentMutation).Apply(ctx context.Context) error` + +`MCPObject.resolvedPath` is the explicit selected config path, constrained to +one of the layout's declared config paths. Native object IDs are exactly +`kind + ":" + name`. Prepared server values are the adapter's already-resolved +native codec inputs; helper does not expand placeholders a second time. + +## R2 applied clarification + +`ActivePath` and projection `stagingRoot` must be explicit clean absolute +no-follow strict children of the selected project boundary. Skill +`sourceRelative` is a clean portable child directory with no empty, dot, +traversal, absolute, drive, backslash or control segment; source containment +rejects any symlink ancestor. Before any skill/config effect, the prepared +Servers key set must equal desired MCP logical names exactly; both missing and +extra entries fail. Contract tests tamper source and server-map inputs before +write and assert native user/A/B inventory remains unchanged. + +Private transaction cleanup never recursively removes an unchecked recovery +path. Record its physical directory identity at creation and exact staged and +backup skill digests. Before deletion, reject any replaced transaction root, +unknown entry, symlink or changed backup/staged directory. Cleanup removes only +still-identified owned material, preserving changed or unknown recovery copies. +A postcommit cleanup refusal returns CommittedCleanupError and retains the +transaction with an actionable recovery location; committed native skills/config +are never rolled back because cleanup degraded. A focused fixture changes a +private backup at the successful MCP boundary, then proves committed native +objects remain installed, the modified backup is retained and warning semantics +are preserved. diff --git a/docs/plans/project-install-e2e/cursor-component-helper-review-r1.md b/docs/plans/project-install-e2e/cursor-component-helper-review-r1.md new file mode 100644 index 000000000..dbfa36884 --- /dev/null +++ b/docs/plans/project-install-e2e/cursor-component-helper-review-r1.md @@ -0,0 +1,64 @@ +# Project component helper amendment independent review R1 + +Reviewer: sources_lead, 2026-10-23. Read-only review of +`cursor-component-helper-amendment.md` before helper implementation. Scope: +Cursor and Codex project component consumers, existing nativeconfig Kernel, +no existing Gemini/Cline/OpenCode behavior refactor. + +## Initial decision + +The minimal helper boundary is accepted. Final R1 GO follows two narrow +contract clarifications below; no additional engine or client generalization is +requested. + +1. **P2, missing skill repair versus no-op.** The optimization for equal prior + and desired digests must require an actually present, exactly verified skill + directory. `Verify(allowMissing=true)` succeeds for an absent old directory, + so equal receipts alone cannot authorize skipping its reconstruction. Add + an explicit missing-same-pin repair contract using the production helper. +2. **No-follow clarification.** The declared no-follow roots and containment + must reject ancestor aliases even when they resolve inside the selected + project. Include that contained-alias regression alongside external escape; + a same-project alias must not become native ownership authority merely + because it passes a containment check. + +## Accepted contracts + +- Explicit project boundary, distinct skills root, exact config paths/codec and + declared ownership kinds; adapter retains validated scope, original user root, + selected registry and native dialect/projection responsibility. +- No helper payload loader or descriptor, portable transport mapper, discovery, + trust/settings engine, source acquisition or native executable dispatch. +- Caller-prepared native servers and exact staged desired receipts are checked + before mutation. Existing Kernel handles exact entries, locks, conditional + replacement, postconditions and rollback; native servers are never executed. +- Skills stage/backup/swap under a private same-filesystem transaction, exact + backup verification, post-install verification, and cancellation before MCP + commit. Foreign drift prevents deletion/overwriting, and failed restoration + retains the only recovery copy. +- Successful config commit or CommittedCleanupError keeps installed skills. + Unknown MCP commit state stays uncertain; observed safe failures may remain + unchanged. Private cleanup failure becomes committed cleanup degradation. +- New Codex and Cursor consumers share this boundary without altering existing + client implementations. Each retains separate prepared readiness and exact + native layout, including Codex's separately reviewed narrow TOML codec. + +The proposed API is accepted: ProjectComponents layout with ValidateObjects, +SkillObject, MCPObject, Verify and Inspect; ProjectComponentMutation with active +package, previous/desired ownership, prepared server map, injected Kernel and +Apply(ctx). Object IDs are kind+":"+name; the supplied MCP receipt path is +limited to the layout's exact selected JSON/JSONC candidate. No extra helper +surface is required for Codex. + + +## Fix verification + +Re-read the amended contract's R1 clarification and frozen public signatures. +Both observations are closed: missing same-pin skills are recreated and only +observed exact present directories qualify for no-op; contained as well as +external ancestor aliases are explicitly refused and covered by contracts. + +**R1 GO.** No plan blocker remains. Independent R2 and parent coding +authorization remain separate; this reviewer has not implemented the shared +helper. Codex will consume the frozen API and retain its independent codec, +projection and scope/readiness responsibilities. diff --git a/docs/plans/project-install-e2e/cursor-component-helper-review-r2.md b/docs/plans/project-install-e2e/cursor-component-helper-review-r2.md new file mode 100644 index 000000000..48a74e25a --- /dev/null +++ b/docs/plans/project-install-e2e/cursor-component-helper-review-r2.md @@ -0,0 +1,74 @@ +# Project component helper amendment independent review R2 + +Reviewer: project_core, 2026-10-23. Read-only amendment review before helper +implementation. Reviewed the complete amended contract, R1 findings/fix ledger, +existing nativeconfig committed-cleanup and shared native-effect semantics. + +## Decision + +**R2 GO after two narrow contract clarifications, independently verified in the +final amendment.** The explicit shared boundary is justified by the separately +reviewed Cursor and Codex project consumers. No new source loader, settings or +discovery engine, client dispatch table or durable recovery framework is needed. +Parent coding authorization remains separate from this plan review. + +## R2-1 — source inputs and prepared server maps must be closed before effects + +The frozen public mutation includes ActivePath, projected source-relative +directories, stagingRoot and a prepared server map. Their relationship must be +explicit rather than rely only on destination containment or receipt comparison. +Requested clean absolute no-follow project-child source roots, portable strict +child source-relative paths, and exact equality between the prepared server-name +set and desired MCP ownership names. Extra map entries must not authorize an +unowned write; missing entries must not become an accidental removal. + +**Closed.** The final R2 clarification explicitly states these preconditions, +rejects aliases/traversal/absolute/drive/backslash/control inputs and adds +prewrite source/server-map tamper fixtures with unchanged user/A/B inventories. +Adapter native dialect/projection remains outside the helper, and desired +receipt equality is still checked before any mutation. + +## R2-2 — cleanup may not discard changed recovery material + +A private directory name and an earlier backup digest are insufficient deletion +authority after another writer changes a recovery copy. Requested physical +transaction identity and exact known child/digest revalidation before private +cleanup; unknown, replaced, aliased or modified copies must remain available. +After successful MCP commit, such refusal must be committed cleanup degradation, +not trigger rollback of already committed native configuration. + +**Closed.** The final contract records physical transaction identity and exact +staged/backup digests; it rejects unknown entries, replaced roots, symlinks and +changed copies before deletion. Postcommit refusal retains an actionable +recovery path under CommittedCleanupError. The planned failure fixture changes +a backup at the successful MCP boundary and verifies committed objects remain +installed while the changed recovery copy and warning remain available. + +## Accepted lifecycle and ownership boundaries + +- Layout explicitly names selected project, skills root, exact config paths, + codec and distinct ownership kinds. Original user ConfigRoot and planned + native registry are validated by each adapter; shared helper containment and + core engine/state scope/namespace checks are independent layers. +- All previous/desired objects have exact kind+name identity, paths and digests. + Unknown kinds, duplicates, foreign same-name objects and disabled/conflicting + entries fail closed. Missing same-pin skills are reconstructed; equal receipt + digests alone never authorize a no-op. +- Skill moves/copies remain on one filesystem, staged and isolated backups are + verified, publication is exclusive and installed skills are rechecked before + a single existing kernel ApplyBatch. An MCP precondition race rolls skills + back only through exact ownership checks. +- Observed complete rollback can report unchanged. Unknown/partial config + commit remains uncertain even if skills restore; failed restoration preserves + recovery copies. Successful commit and CommittedCleanupError both retain the + desired skills; private cleanup failures cannot undo committed MCP state. +- Cancellation keeps its existing before-effect/after-effect distinction. + No persistent plugin data, foreign files, unrelated native settings or user + and project-B state is consumed by helper cleanup. +- Existing Gemini/Cline/OpenCode and user Cursor/Codex behavior is outside the + new files. Future consumers need their own reviewed qualification. Project + prepared/installed state does not claim native trust/discovery or execute MCP. + +No code was implemented or edited during this review. The required contract +tests remain implementation gates, including rollback, postcommit cleanup +degradation, exact missing repair, aliases and source/server-map tampering. diff --git a/docs/plans/project-install-e2e/cursor-implementation-evidence.md b/docs/plans/project-install-e2e/cursor-implementation-evidence.md new file mode 100644 index 000000000..34b2c4d1d --- /dev/null +++ b/docs/plans/project-install-e2e/cursor-implementation-evidence.md @@ -0,0 +1,174 @@ +# Cursor project component evidence + +## Pre-implementation evidence, 2026-10-23 + +The reviewed implementation selects project components. Official documentation +confirms `.cursor/mcp.json` for project MCP and `.cursor/skills/` for project +skills. User MCP remains `~/.cursor/mcp.json`; native discovery also considers +other skill roots, including `.agents/skills` and compatibility directories. +The MCP CLI documentation exposes interactive listing, connection status and +project/global configuration source. Enablement and tool approval remain native +client decisions. No enable, approve, trust, login or model command is invoked +by this adapter. + +Sources checked before coding: + +- +- +- +- + +The plugin UI documents project and user install scopes, but this implementation +does not infer a stable unattended plugin-install interface from that UI. It +retains the existing user editor package preparation path and uses exact owned +project skill directories and MCP members for project requests. It does not +write user configuration or hooks, redirect HOME, or detect an ambiguous `agent` +binary as the editor. + +## Evidence boundary + +Pinned official CLI artifact/help research is recorded below. +Production provider/loader/planner/stager contract tests will be recorded below; +they do not prove that a real editor or Cursor Agent discovers, enables or +executes these components. Project configuration success remains prepared with +installed verification, and activation attestation cannot upgrade that claim. +Native session discovery, policy, approvals, aliases and MCP initialization +remain unobserved until the existing hosted native-client workflow qualifies +that exact client/version/platform. + +## Pinned official CLI research + +Parent provisioned Cursor Agent `2026.10.22-27dcf5f` directly from the official +installer's download metadata, without executing the installer: + +- URL: +- Measured archive SHA256: `85296944e4600a83c4b764f578d1502bf9c416db200d33a593e8f3ecabfdc294`. +- The installer did not provide an independently authenticated digest; this is + a measured acquisition pin, not a vendor signature claim. +- Actual version, top-level help and MCP help exited zero in a networkless + disposable namespace with fresh HOME/project and no user mounts. These are + read-only research probes, not native component discovery qualification. +- MCP help explicitly names `.cursor/mcp.json` and `~/.cursor/mcp.json` and + exposes list/list-tools/enable/disable/login. No enable/login was invoked. +- Current top-level help also exposes `plugin`, `--plugin-dir`, and `--workspace`. + No assertion that Cursor Agent lacks plugin CLI support is made. This stack + retains the reviewed component lane; plugin installation is a separate native + delivery contract requiring its own scoped ownership qualification. + +The project component lane does not add ambiguous `agent` executable detection +or claim that agent-only detection selects the editor. Existing editor and +Cursor Agent detection surfaces remain distinct. + + +Follow-up actual plugin help exposed only the `marketplace` child. Requests for +install/list/uninstall `--help` printed the same ancestor help and did not +establish those lifecycle verbs or a project scope grammar. This component +implementation therefore does not claim that CLI plugin install/remove was +qualified by those probes. + +## Implementation and contract verification + +The adapter qualifies the catalog's Cursor project scope using native file +configuration lifecycle traits while preserving user manual preparation. +ProjectTargetRoot selects managed artifacts under project `.uap`; NativeRegistry +selects exact `.cursor`. It validates original detected user root and matching +scope/target/native root throughout plan, inspect, activate, verify and remove. +Project binding validation accepts only the selected managed artifact and exact +native ownership paths. The shared artifact receipt is handled by core; the +component helper receives only native ownership, retaining unknown kinds for +closed refusal. + +The new shared component helper has two independent plan reviews and two +independent implementation reviews in this directory. It uses the existing +nativeconfig Kernel, exact receipts, verified directory swaps and rollback; +there is no second settings/discovery engine. The project stdio projection +uses the existing trusted launcher and binds absolute package/data/cwd inputs. +User package projection and legacy attestation behavior retain their existing +contract. Project attestation is refused before generic provider shortcuts. + +Author verification includes complete Cursor/shared/domain package tests +(PASS 0.189s/0.078s/0.038s), project provider/planner contracts and unchanged +user activation goldens (PASS 0.359s/0.014s). The task host carried duplicated +uppercase/lowercase proxy environment names; the pre-existing Codex user +validator rejects those even when discarded. Removing only duplicated lowercase +proxy names for this inert golden check restored its expected existing behavior; +no production or golden change was made for that unrelated host condition. +Composed Cursor tests load standard plugin.json, render a nonrestricted command +skill, stage production projections and drive production providers. They cover +remote and stdio MCP, read-only active reprojection, repeat/noop/update, +missing-same-pin repair, dropped MCP, repeat removal and persistent data, +foreign disabled entries/hooks/skills, malformed config, lock failure rollback, +user/project-A/project-B byte and mode inventories, scope/root/copied-context +mismatch, contained native aliases and direct/provider attestation refusal. +The injected launcher bytes are inert and never executed. + +Both exact changed-code and full-file size/architecture lint gates pass without +new suppressions. The compatibility golden delta changes only the newly +qualified Cursor Scopes field from `[user]` to `[user, project]` in four reports. +Actual native session discovery, MCP handshake/tool result and fresh restart +qualification remain pending the hosted native-client matrix; none of these +fixture or help probes is presented as that evidence. + +## Composed source lifecycle checkpoint + +The Cursor stack was aligned with reviewed integration `d9f3f757` in local +merge `12177d41`. Dedicated Cursor CLI tests reuse that integration's real +loopback TLS Git and ZIP acquisition transports and its CLI/state inspection +helpers. The scoped CLI composer, dependency solver/manifest/lock, immutable +store, installer, planner, stager and native configuration writer are production +implementations. Only host detection and package security assessment are inert +fixture boundaries. Remote MCP entries avoid executing configured commands or +copying the test executable as a managed launcher. + +`TestProjectCursorComposedSourceLifecycle` covers both Git and ZIP sources: +saved project add in A and B, exact native skill/MCP paths, locked no-op sync, +list/info/doctor/outdated/dependency status/tree reads without native/state +changes, missing native skill repair, pin-advancing update that retains binding +authority, portable export and cold-cache import/install into a new project, +and exact removal. User Cursor configuration/hooks/skills and project B retain +their byte/mode inventories and recorded state while only A changes. The cold +restore retains the exact exported agreement and portable source/tree identity. +Project bindings stay prepared plus installed verification throughout. This +remote-only composed fixture does not allocate persistent stdio data; the +separate stdio provider contract remains the data/launcher evidence. + +Final author verification for the composed checkpoint: + +- Dedicated CLI Git/ZIP lifecycle: PASS, 5.865s. +- Existing project Cursor provider contracts after integration: PASS, 0.183s. +- Exact changed-code and full-file size/complexity/architecture lint gates: + 0 issues, without new suppressions. +- Git whitespace check: clean. + +Independent final adapter R1 is GO in `cursor-code-review-r1.md`; final R2 and +review of the added composed journey remain separate gates. These tests do not +launch Cursor, execute a package payload or configured MCP command, write +trust/approval/login state, or issue a model request. Genuine Cursor discovery, +effective enablement and policy, MCP initialization/tool use, editor behavior, +fresh native restarts and release qualification remain unobserved. + +## Independent R2 correction: selected data authority + +The final adapter R2 found that read-only active reprojection ignored its +selected DataReceipt locator and could reuse old embedded stdio bindings. +Project active reprojection now refuses a different or absent selected data +locator with the existing projection-refresh-required error. It verifies the +managed launcher path, reserved package/data environment, and protocol +package/data/cwd/anchor arguments against the selected active package and data +receipt before producing native ownership. It uses the existing read-only cwd +resolver and never changes digest-covered bytes to hide a mismatch. User +projection and remote-only project entries retain their previous behavior. + +The composed stdio provider regression supplies the correct bound locator for +the positive read-only case, then requires wrong/empty data locators to fail +with unchanged immutable package bytes. Full project Cursor provider selection +passes at this correction: **PASS 0.180s**. The independent R2 reviewer must +recheck this correction before closing the adapter review. + +## Cross-platform source journey selection + +The composed Git/ZIP source lifecycle test now starts with `TestProjectCursor` +so the existing project-contracts selector includes it on Linux, macOS and +Windows. Only the test name changes; its assertions and inert source/runtime +boundaries remain identical. This corrects hosted test selection and does not +claim that any pending native or cross-platform run has passed. diff --git a/docs/plans/project-install-e2e/devin-native-comments-amendment.md b/docs/plans/project-install-e2e/devin-native-comments-amendment.md new file mode 100644 index 000000000..1333fd1bd --- /dev/null +++ b/docs/plans/project-install-e2e/devin-native-comments-amendment.md @@ -0,0 +1,73 @@ +# Devin native configuration comments — pre-code review correction + +2026-10-23. This is a narrow supplemental amendment to the twice-reviewed client +plan and frozen adapter132b8c38. Independent R1 found that strict JSON currently +rejects normal commented native Devin configuration. Automatic approval review +rejected the initial corrective source command because this shared parsing +amendment had not received two independent plan/fix GO reports. The command did +not execute; the source remains frozen and clean. Commit this plan and both +independent GO reports before implementing the correction. + +## Evidence and defect + +The genuine3000.11.205 distribution's pinned official +reference/configuration/config-file.mdx has an explicit JSON with Comments +section supporting line, inline and block comments. It also declares the +dedicated .devin/mcp_config.json and higher mcp_config.local.json native files; +the matching MCP documentation shows this native configuration. References: +https://docs.devin.ai/cli/reference/configuration/config-file and +https://docs.devin.ai/cli/extensibility/mcp/configuration. This is native file +grammar evidence, not native project execution or trailing-comma support. + +The new adapter's readDevinProjectDocument passes native main/local policy +bytes into the strict portable JSON decoder, so an unrelated native comment +blocks installation. The new CodecDevin also currently takes native target +mcp_config.json through strict parseDocument(false), rejecting commented target +files before an otherwise exact owned patch. Legacy CodecWindsurf must retain +its current grammar and user behavior. + +## Narrow implementation + +Reuse the existing comments-only native AST mechanics already used for Gemini: +extract its private syntax admission into parseCommentsOnlyDocument(body,label) +without changing Gemini's accepted/rejected grammar or existing error text. +CodecGemini delegates with its existing label; only the new CodecDevin opts in +with a Devin label. Every other codec, especially legacy CodecWindsurf, keeps +its existing branch. Preserve the original AST and commentsOnly render flag, +existing trailing-comma rejection/cleanup, duplicate-key checks, native name/ +path validation, no-follow IO, writer locking, entry digests and ownership, +transaction effects/rollback and receipt rules. Do not add a second writer or +change mutation/recovery semantics. + +Add one small read-only nativeconfig.StandardizeCommentedJSON helper using that +same comments-only syntax admission followed by existing hujson.Standardize. +The adapter's native main/local policy inspection calls it before the existing +strict unique-key object decoder. Original files remain unmodified and exact +bytes are retained; the normalized bytes are only ephemeral inspection input. +Duplicate keys and undocumented trailing commas refuse, rather than accepting +all JSONC grammar accidentally. Do not relax portable plugin/MCP/manifest JSON, +change a general shared strict decoder, infer trust/policy, migrate old config, +or rewrite local overrides. + +## Meaningful verification + +Add a current-Devin native codec regression with line/block comments and exact +foreign disabled entry/policy trivia. Add/remove only the owned member and +verify foreign comments/member bytes survive. Refuse duplicate keys and trailing +commas through both the target codec and the read-only normalizer. Assert legacy +CodecWindsurf still rejects the same commented document; run affected existing +Gemini comments/grammar tests to prove its behavior and error contract remain. + +At the composed provider boundary, accept unrelated commented config.json, +config.local.json and a foreign-name mcp_config.local.json while preserving each +file byte-for-byte. Refuse commented legacy main mcpServers and same-name local +shadowing with native/user/B inventories unchanged. Retain the exact scope, +artifact, receipt/data and attestation regressions, plus visible-write rollback +and committed-cleanup cases. Existing project CLI/SDK evidence remains separate +from genuine native discovery. Run bounded affected codec/provider suites, +changed/full-file lint and diff checks; freeze the correction for independent +R1/R2 implementation refresh before parent publication. + +No native executable, trust/model/account/MCP process, installer retry, new +acquisition, hosted guard change, user capability expansion, main/release merge +or native runtime qualification is authorized by this syntax correction. diff --git a/docs/plans/project-install-e2e/devin-selected-facts-amendment.md b/docs/plans/project-install-e2e/devin-selected-facts-amendment.md new file mode 100644 index 000000000..f6618c5f6 --- /dev/null +++ b/docs/plans/project-install-e2e/devin-selected-facts-amendment.md @@ -0,0 +1,45 @@ +# Selected Devin facts are not sibling facts + +Pre-code narrow follow-up, 2026-10-23. The two reviewed host-evidence/absent-profile +contracts and original client plan remain in force. No implementation of this +specific correction exists at this checkpoint. + +The new real TLS Git/ZIP public CLI regression succeeds on first saved Devin +project install with no legacy Cascade profile, then fails during saved repair: +`target facts are unavailable or conflict with owned state: ConfigRoot must be +an explicit absolute clean path for windsurf`. The source is +`dependencyInstaller.populateDependencyTargets`: it appends every binding, +including the selected request client, into KnownTargets. The SDK field contract +says KnownTargets is for installed sibling bindings not selected by the request. +Its existing stricter sibling root validation is not the right place to repeat +primary request admission. + +Implement only a selected-client exclusion in that CLI collector: +`if binding.ClientID == request.ClientID { continue }`, before looking up and +appending the sibling fact. Keep the existing knownTargetIndex validation, +BindingID/executable/root requirements and all unselected sibling facts unchanged. +Do not introduce an empty-root exception or version field for sibling facts. +The primary selected Request already carries its fresh observed executable/ +version and optional original root under the reviewed project-only admission. + +Independent tracing of compatibilityChecks shows that selected single-target +bindings already use req.ClientConfigRoot/ClientExecutable directly; only +binding.ClientID != req.ClientID consults KnownTargets and checks its BindingID. +The selected target's prepared DetectedClient replaces its observational +reconstruction in compatibilityBindingCheck, retaining the fresh version. +The selected binding remains bound by core installation/binding/environment +receipts, namespace, target/profile/native object and immutable package guards. +No duplicate selected KnownTargets value supplies additional authority there. + +Verification: retain the failing current-only no-profile real TLS saved +install/locked-sync/repair/update/cold-import/remove journey until it passes; +add an affected-builder assertion that selected facts are omitted while exact +unselected sibling facts remain. Existing knownTargetIndex user/sibling missing/ +invalid root and binding identity tests stay unchanged and must pass. No native +process, new public schema, group expansion, detection order, user capability, +profile fallback or version-dependent cleanup is added. + +Obtain both independent plan GO reports before applying this correction. Freeze +it with the final Devin adapter for both independent code reviews; root owns +publication. This remains prepared file-delivery evidence, not native discovery, +trust, MCP runtime, Desktop harness or release qualification. diff --git a/docs/plans/project-install-e2e/devin-shared-host-evidence-checkpoint.md b/docs/plans/project-install-e2e/devin-shared-host-evidence-checkpoint.md new file mode 100644 index 000000000..53ba387f1 --- /dev/null +++ b/docs/plans/project-install-e2e/devin-shared-host-evidence-checkpoint.md @@ -0,0 +1,27 @@ +# Shared host evidence implementation checkpoint + +The two Devin amendment plan GO reports were committed at f3a4149d before this +source change. This checkpoint carries the optional operational ClientVersion +from both existing CLI installer request builders into the SDK reconstructed +selected client. It adds no process and changes no detection ordering, sibling +selection, portable schema, group target or user capability. Diagnostic JSON +cannot write/read the operational field. + +Generic SDK reconstruction permits an absent original user profile only for a +configured project engine; public Prepare validates exact engine/request scope +and declared project admission first. Nonempty roots retain clean absolute +validation. Existing profile resolvers still run: the focused regression proves +Gemini's stricter empty-profile rejection remains in force. User scope still +requires its original profile. + +Focused public SDK version/frozen-request/diagnostic and generic profile +regressions passed (0.014s). Changed-code lint and full-file complexity/architecture +lint both reported zero issues for installer and CLI packages. The full Devin +composed project lane, both CLI paths, current-only absent legacy profile, +version refusals and lifecycle tests belong to the following adapter checkpoint. + +The no-save facade route remains restricted to its existing qualified clients; +Windsurf is not added to that route. Its direct no-save usecase already retains +the selected DetectedClient. The shared builder receives selected.Version when +its existing qualified clients use it; Devin saved sync reaches the other +builder. No route or qualification is expanded to make the field observable. diff --git a/docs/plans/project-install-e2e/devin-version-evidence-amendment.md b/docs/plans/project-install-e2e/devin-version-evidence-amendment.md new file mode 100644 index 000000000..0a20b97e4 --- /dev/null +++ b/docs/plans/project-install-e2e/devin-version-evidence-amendment.md @@ -0,0 +1,82 @@ +# Narrow host-version evidence propagation for Devin project gating + +Pre-code amendment, 2026-10-23, cursor_clients. This supplements the existing +Windsurf/Devin client plan's requirement for an explicit newer harness/version; +it does not introduce another native probe or installation engine. + +## Observed composition gap + +The existing explicit-target detector can return version evidence. CLI saved +sync and no-save facade currently pass only client ID/config/executable into +installer.Request. Engine.detectedClient reconstructs a domain client without +Version. Thus a correct version-gated project adapter cannot distinguish the +qualified current Devin artifact through the public installer, even when the +host already observed its version. The public SDK's Discover intentionally +never executes a found file, and that read-only contract must remain intact. + +## Proposed narrow change + +1. Add an optional operational `ClientVersion string` to installer.Request, + excluded from diagnostic JSON. It is host-observed metadata paired with + the already explicit ClientExecutable; it is never package-authored or + read from portable plugin/dependency manifests. Copy it into the reconstructed + domain.DetectedClient.Version. Request's immutable prepared snapshot already + copies strings; no mutable evidence object, process runner or cache is added. +2. Pass selected.Version from the same selected DetectedClient alongside its + executable in the existing no-save facade and saved dependency request + builders. Preserve all existing version-probe behavior and environments; + do not run a new subprocess from an adapter or public SDK Discover/Prepare. + SDK hosts can supply their own already observed evidence; absent evidence + stays unknown and cannot qualify this project lane. +3. Keep group/sibling/version/public capability behavior unchanged. The current + SDK group boundary is Claude/Codex only, so no speculative group expansion + or new version fields for unused group targets are needed. Selected Devin + install/update/repair requests receive fresh request-scoped evidence; exact + owned removal remains version-independent and never launches native cleanup. +4. Within the Windsurf adapter, admit only the selected primary executable whose + basename is devin/devin.exe and normalized exact known3000.11.205 evidence. + Current detected user ConfigRoot stays intact as the original authority. + Keep existing detection ordering/channel selection unchanged. If a legacy + Windsurf primary wins a coexistence environment, reject the automatic + project lane with a precise explanation; do not infer current Devin merely + from an editor directory or an unrelated devin surface. An embedding SDK + can explicitly select its observed Devin executable/version pair. +5. This metadata permits only the reviewed configuration-file lane. It is not + binary provenance, native discovery, trust, model/account authentication, + plugin/session precedence or MCP runtime evidence. Preserve prepared+installed + outcomes and project attestation refusal. Actual pinned native qualification + must still record independently acquired binary hashes and genuine outputs. + +## Project-only SDK admission alignment + +Further static tracing found that a current-only Devin installation correctly +selects no legacy Cascade user ConfigRoot. Installer.detectedClient currently +rejects an empty user profile even for a validated project engine, although +NewTargetContext already permits that absence. Do not invent a profile or +change legacy detection ordering/user mutation to get around this mismatch. + +Allow an empty ClientConfigRoot only when the engine/request is already in +validated project scope. A nonempty root still must be clean and absolute; +user scope keeps the existing required-root contract. Adapters continue checking +exact original user-context equality (both empty is legitimate), declared +project admission, real selected project/native root, artifact and receipts. +Project writers must never substitute the absent user root as native authority. +This changes no native path, profile resolver, authority token or fallback. + +Add composed public SDK/CLI coverage for the selected current Devin with an +absent original legacy profile, plus user-scope empty-root refusal and nonempty +invalid project-root refusal. Existing scoped clients' stricter adapter policies +remain in force. Exact removal still uses recorded project authority, without +requiring a running/version-qualified current harness. + +## Verification and publication + +Two independent plan/fix GO reports must precede implementation. Add meaningful +composed tests proving exact known Devin evidence reaches both public CLI request +paths and the public SDK, while missing/older/other-harness evidence refuses +before native effects. Prove a source manifest cannot inject the host field. +Retain existing user/other-client behavior and public SDK Discover's no-execution +contract. Test current dedicated files, legacy migration refusal, local shadowing, +A/B/user isolation, exact ownership, immutable selected-data validation, lifecycle +and conservative status under the original client plan. Freeze a separate client +commit for two independent implementation reviews; root owns publication. diff --git a/docs/plans/project-install-e2e/executor-recovery-evidence.md b/docs/plans/project-install-e2e/executor-recovery-evidence.md new file mode 100644 index 000000000..e57442cb2 --- /dev/null +++ b/docs/plans/project-install-e2e/executor-recovery-evidence.md @@ -0,0 +1,74 @@ +# Executor recovery: reviewed root and Claude source + +The temporary executor was replaced before the last local completion commits +were published. Published PR499 remained at46b4678ea66d7804311f98270b70ebd3cceb928a. +This checkpoint distinguishes exact source recovery from newly reconstructed code. +It is not final all-client qualification or release evidence. + +## Reconstructed shared source + +Frozen recovered commit4e83f9a29d1ad01beec02a1762f5e3e7b71185fc has +tree78a4b7fae6c18415d60d1b9aca20f0fe5cee6035, parent46b4678. +The recovered R2 removal plan has exact SHA256 +6a0a2de5771aadeec396518593e994c5fa084883371deeaf443bb7b2999c98a8, +matching the independently reviewed plan before the reset. + +The source restores the positive project-only grouped-removal admission, +runner-free early ownership observation at six existing removal fences, +independent observation packets, failure-result/picker predicate extractions, +meaningful single/group mutation regressions, and mechanical lint corrections. +It preserves original user guards, exact identity checks, saved state/journal +comparison, and honest uncertainty for changes after the last observation. +The grouped lifecycle fixture reopens its engine before grouped cleanup. + +Fresh independent source reviews at the exact recovered commit: + +- /root/skillshare_scope: CODE GO. Read all changed production and fixture source; + confirmed strict Managed/ReceiptReconciled/digest/positive-Absent observation, + false discovery flags, independent packets, unchanged historical guard and + error precedence, and foreign/absent/replay/change-after-Prepare regressions. +- /root/apm_vercel_scope: CODE GO. Independently reviewed the same frozen source. + Confirmed all six fences, the explicit supported-project exception, unchanged + late-attempt uncertainty, exact pure extractions and mechanical lint corrections. + +Neither critic claimed duplicate test runs or native execution. + +## Fresh checks on the restored shared source + +Using verified Go1.26.10 and golangci-lint2.13.2: + +- Installer focused removal/group/handoff/result lifecycle regressions: + PASS18.765s; focused Grok selected-source collision regression: PASS0.056s. +- Changed-source lint against46b4678 for installer, Cline and Grok:0 issues. +- Whole installer revive/funlen/gocyclo/gocognit/dupl/depguard:0 issues. +- Changed-source public/internal CLI lint against46b4678:0 issues. +- Authored diff whitespace check:PASS. + +The focused test selector was: +Test(ProjectRemoval|ProjectGroupRemoval|ProjectPreparedGroupPublicLifecycleAndHandoff| +ProjectHandoff|ProjectFileLifecycleWithoutLiveObserver|FinishGroupFailure| +ProjectGrokSelectedSkillNameCollidesAcrossNativeSources). +These Linux results do not qualify actual macOS/Windows execution. + +## Exact Claude source recovery + +Recovered commit7bf45c74b19ca2054bedfb23937d5d84923f8d45, parent46b4678, +has exact tree003ef4979bdb76035008ecadb470998e1f260450. It matches the +previously independently reviewed six-file Claude lint correction tree. +Root and APM had approved that source; no new behavior is inferred from the +recreated commit identity. Integration is a disjoint cherry-pick. + +This changes the project-list fixture result to a named struct, lowercases +error prefixes, fixes capacity/copy naming, and removes only three newly +unreachable private historical fixture helpers. Existing native gates remain. +The earlier exact source checks (Claude package0.036s, prepared contract0.031s, +affected lint0) remain historical attributed checks, not new executor runs. + +## Remaining program gates + +Current hosted46b4678 evidence still has eight expected planner golden deltas, +static lint corrections, observed platform fixture defects, and the separate +OpenCode/Windows native harness corrections. Qoder, Cline current-source native +evidence, and VS Code workspace observation/adapter completion remain distinct +lanes. Final assembled source must pass the required checks and independent +review before merge. This checkpoint does not authorize a release. diff --git a/docs/plans/project-install-e2e/gemini-code-review-r1.md b/docs/plans/project-install-e2e/gemini-code-review-r1.md new file mode 100644 index 000000000..a5b678547 --- /dev/null +++ b/docs/plans/project-install-e2e/gemini-code-review-r1.md @@ -0,0 +1,37 @@ +# Gemini project implementation independent review R1 + +Reviewer: project_core, 2026-10-23. Initial read-only checkpoint `d76f29ae..a96b4aff07dedfb99297d3fab564b1c778d6968b` in the Gemini child worktree. Review scope is the Gemini adapter and the owner-authorized precise CodecGemini comment parsing delta. Shared core was reviewed separately. + +## Decision + +**R1 GO at final frozen `65b8f17a951e80f06f9ed9c2677dd16a3ccd3d0a`.** Scoped root selection, conservative project readiness, exact native ownership and the precise Gemini comment codec changes have no confirmed blocker. Initial checkpoint observations below are retained as review history. Native Gemini session discovery and cohesive source/project CLI execution remain separate qualification/integration gates. + +## Initial checkpoint assessment + +**No confirmed blocker in the scoped native paths or codec delta. Final GO is pending the author's catalog qualification, conservative project readiness, disabled/shadowed settings, alias/preflight and composed stager tests.** The initial checkpoint alone is not yet project admission/product proof. + +- Project native settings/skills root is explicitly `/.gemini`. Detected `Client.ConfigRoot` remains the original user profile. Project/root mismatch and changed original user context are rejected before native operations. +- Native mutation, verification, removal and registry inspection select the same explicit root; removal is supplied the recorded actual native registry and TargetContext. Exact existing ownership codecs and native skill transactions are reused. +- The managed artifact layout stays under the selected project, with physical profile canonicalization and no-follow containment reused. User home variables are not replaced and host trust is not granted. +- `CodecGemini` settings.json accepts comments while preserving the existing AST and exact entry receipts. Duplicate keys, malformed input and trailing commas remain errors. Other strict JSON codecs stay strict. +- Independently inspected hujson's parser representation: a final child Value.AfterExtra is cleared for a closing delimiter without a trailing comma and remains present when a trailing comma exists; the new sentinel check therefore does distinguish this format boundary while retaining comments. +- Remaining direct parseDocument calls in transition*.go are guarded closed OpenCode dialect transitions. They do not admit CodecGemini, so no unrelated transition broadening is needed. + +## Final review checklist + +Review the final scoped policy/catalog and readiness delta, run the full Gemini and nativeconfig packages independently on the frozen tree, and verify user behavior plus scoped isolation/disabled/foreign-edit/collision/alias/attestation boundaries. A cohesive composed source/project lifecycle follows integration; adapter fixture proof must not be labeled native Gemini process certification. + +## Final frozen checkpoint verification + +Reviewed the exact final tree read-only, including the real core merge, Gemini catalog qualification and project policy, all moved composed contracts in the external providers test package, and the final `document.go` / `gemini_document.go` last-member deletion correction. + +- The catalog qualifies only Gemini project scope through its declared native-config lifecycle. It does not select a test constructor or alter the user lifecycle traits. +- Project filesystem success consistently remains ActivationPrepared plus VerificationInstalled, without activation attestation. Neither repeated automatic reconciliation, verify-only, a restart, ambient trust variables nor `--activation-complete` can claim native session discovery. The existing ActivationPreflighter rejects the shortcut before mutation and direct adapter reuse invokes the same guard. +- Preflight reads user/project settings through the exact comment-aware native kernel, preserves disabled skills and MCP restrictions, and reports possible `.agents/skills` shadowing. Workspace trust, system policy and MCP initialization remain explicitly unobserved; there is no new trust/settings engine. +- The composed loader/planner/renderer/stager/activator contract renders portable commands as Gemini skills and installs/removes the exact skill/command/MCP objects under the project root. Foreign entries/comments, user profile and project B remain unchanged. Malformed settings, object collisions, writer-lock failure, scope/root mismatch and changed original user context fail closed. +- The final codec fix removes only trailing commas created by deleting a last managed member, moves surviving foreign trivia to the enclosing delimiter, and revalidates the comment-only document. Native-invalid input trailing commas still fail; unrelated strict JSON codecs remain unchanged. The foreign-trivia regression exercises the problematic preceding member directly. +- Architecture was preserved by moving composed journey tests to `providers/project_gemini_*_test.go`, package providers_test; no upward-import suppression was added. + +Independent final execution (`-count=1 -p=2 -timeout=2m`): full Gemini **PASS 0.046s**, full nativeconfig **PASS 3.730s**, full domain **PASS 0.034s**, and providers `^TestProjectGemini` **PASS 0.061s**. The authored CI lint evidence records both exact changed-code and full-file size/architecture gates at zero issues. Shared core CI now discovers TestProject contracts in nativeconfig, providers and all client packages on Linux, macOS and Windows. + +No confirmed review finding remains open. These are inert fixture/contract checks, not hosted native Gemini product certification. diff --git a/docs/plans/project-install-e2e/gemini-code-review-r2.md b/docs/plans/project-install-e2e/gemini-code-review-r2.md new file mode 100644 index 000000000..1faa37583 --- /dev/null +++ b/docs/plans/project-install-e2e/gemini-code-review-r2.md @@ -0,0 +1,32 @@ +# Gemini project implementation independent review R2 + +Reviewer: sources_lead, 2026-10-23. Reviewed exact frozen commit `65b8f17a951e80f06f9ed9c2677dd16a3ccd3d0a` read-only, cumulatively against the reviewed project core. No Gemini worker files were changed. + +## Decision + +**R2 GO.** No confirmed implementation blocker remains in the Gemini project adapter, bounded settings-policy warnings or precise native-config comment parser. This qualifies the implemented file and lifecycle contracts; native Gemini session discovery, workspace trust, system policy and MCP initialization remain explicitly unobserved. Combined source/project CLI qualification follows integration. + +## Independent review findings + +- Project selection consistently uses the explicit TargetContext root for planning, registry inspection, native object preparation, activation, verification and removal. Original detected user configuration remains separate. Changed user authority, user registry substitution and managed-root escape fail closed through existing contextual physical-path checks. +- The domain catalog exposes the project native-config component lane without altering user lifecycle traits. The production loader/planner/command renderer/stager/activator composition is exercised by external provider tests; no test-only adapter composition is required. +- Project file success is consistently `ActivationPrepared` and `VerificationInstalled` with `ActivationAttested=false`. Repeated automatic reconciliation and verify-only do not promote readiness. The shared ActivationPreflighter runs before generic activation attestation, and direct adapter reuse also rejects that shortcut before mutation. An ambient trust flag is not accepted as discovery evidence. +- Preserved user/workspace restrictions and a same-name `.agents/skills` alias generate bounded warnings. These checks do not write trust or policy and do not claim to reconstruct Gemini's complete settings engine. System policy and MCP initialization remain explicitly unobserved. +- Native entry receipts and exact skill ownership remain the mutation authority. Project A add/repeat/verify/remove preserves user configuration, project B, foreign skills, foreign MCP entries, settings and comments. Foreign same-name collisions, malformed settings and writer-lock failure leave native state unchanged; removal is repeatable and exact. +- CodecGemini accepts the pinned native format's comments without admitting duplicate keys or input trailing commas. The deletion correction removes only renderer-created trailing commas and preserves surviving foreign trivia. Strict JSON codecs retain their prior parser. Existing OpenCode-only transitions are unaffected. +- Reviewed pinned v0.65.0 release evidence, configuration/help boundaries and the documented original user-root handling. The implementation makes no live-session, model, authentication or native trust claim from these contract tests. + +## Independent execution + +Executed against the exact clean frozen tree with `-count=1 -p=2`: + +| Scope | Result | +| --- | --- | +| Full clients/gemini package | PASS, 0.028s | +| Full adapters/nativeconfig package | PASS, 3.570s | +| Full domain package | PASS, 0.023s | +| Providers `^TestProjectGemini` composition contracts | PASS, 0.072s | + +The composed tests cover rendered command skills, MCP and skills, prepared status, attestation rejection, original user context, other-project isolation, restrictions/alias preservation, exact removal, malformed input, collision and rollback. No package payload, model request or MCP server was executed. The hosted native runtime guard remains unchanged. + +No review correction was requested in this rotation. diff --git a/docs/plans/project-install-e2e/gemini-implementation-evidence.md b/docs/plans/project-install-e2e/gemini-implementation-evidence.md new file mode 100644 index 000000000..6a2a38f48 --- /dev/null +++ b/docs/plans/project-install-e2e/gemini-implementation-evidence.md @@ -0,0 +1,88 @@ +# Gemini project component implementation evidence + +Date: 2026-10-23. Implemented frozen code: `65b8f17a`. The selected lane installs +native project skills and MCP configuration; it does not invent a project +extension registry. Independent implementation [R1](gemini-code-review-r1.md) +and [R2](gemini-code-review-r2.md) both grant GO for the bounded file and +lifecycle contracts. + +## Pre-code official artifact and help research + +Official [Gemini CLI v0.65.0 release bundle](https://github.com/google-gemini/gemini-cli/releases/download/v0.65.0/gemini-cli-bundle.zip): +21,215,960 bytes; SHA256 +`bb5e7e879608bd9b3fe261930d3886fa56aacbf471da1655f2978eb35b705a73`. +The artifact checksum was verified before implementation. Actual read-only +version, skills help and MCP help probes ran with no network, no user mounts +and fresh isolated home/project directories. The version reports `0.65.0`; +skills help exposes list, enable, disable, install and uninstall; MCP help +exposes add, remove, list, enable and disable. These observations establish the +pinned help/schema research boundary, not project session discovery. + +Primary documentation: +[configuration](https://geminicli.com/docs/reference/configuration/), +[skills](https://geminicli.com/docs/cli/skills/), and +[trusted folders](https://geminicli.com/docs/cli/trusted-folders/). + +Read-only inspection of the exact released JavaScript bundle establishes: + +- `chunk-AZOL6UFL.js`: project settings are `.gemini/settings.json`; trust gates + workspace merging between user and system settings (`mergeSettings`, around + line16338). The pinned schema defaults folder trust to true, resolving stale + text in the separate trusted-folders page. +- The same chunk defines `mcpServers` named-entry shallow merge around + line12455; `mcp.allowed`/`mcp.excluded` around line13987; skills enabled and + union-merged disabled names around line14692; admin MCP/skills enabled fields + around lines15098/15148. Admin sections use replace merging. +- Native settings parse JSON after stripping comments. Comments are accepted; + input trailing commas remain invalid. +- `chunk-DPTJ5QZL.js`: original user configuration and trust storage stay under + native user authority. Workspace `.agents/skills` is a higher-priority alias + at the workspace tier; neither HOME nor GEMINI_CLI_HOME is rebound to the + project by this implementation. + +Offsets identify this immutable bundle and are not claims about future native +versions. No package payload, MCP server or model request was executed during +these probes. + +## Implemented contract and limits + +Planning, inspection, activation, verification and removal use the actual +selected project `.gemini` root while retaining detected user ConfigRoot. +Managed artifacts remain separately under project `.uap`. Exact MCP receipts, +skill directory ownership, rollback and no-follow/canonical path checks are +reused. Comment-aware Gemini writes preserve foreign entries/trivia; duplicate +keys and trailing commas remain invalid, and other strict JSON codecs retain +their previous parser. + +Every configuration-only project success is `ActivationPrepared` plus +`VerificationInstalled`, with no activation attestation. Repeat automatic +reconciliation and read-only verification do not claim native discovery. The +existing preflight port rejects `--activation-complete` before the generic +shortcut, including direct adapter reuse. An ambient trust bypass flag is not +discovery evidence. Trust and policy files are never changed. + +Warnings inspect configured user/workspace restrictions and potential +same-name `.agents/skills` alias paths. They do not recreate Gemini's full +settings engine. Session discovery, workspace trust, system policy and MCP +initialization remain explicitly unobserved. The existing user-scope lifecycle +stays unchanged. + +## Verification + +Production loader, planner, portable command renderer, stager and activator +contracts deliver ordinary skills, command skills and MCP entries into project +A; original user and project B byte/mode inventories remain unchanged. Tests +cover repeated activation/verification, exact repeatable remove, preserved +restrictions/alias, malformed settings, foreign same-name collision, and an MCP +writer failure after skill installation with complete skill rollback. + +Author's exact Go1.26.10 checks: composed project contracts PASS0.063s; +native codec project contracts PASS0.008s; full Gemini/nativeconfig/domain +PASS0.028s/3.790s/0.040s. Exact golangci-lint2.13.2 changed-code and whole-file +size/complexity/architecture gates both report zero issues. Both independent +reviewers separately ran the frozen packages and composed tests successfully. + +These are inert transport-independent file/composition contracts. Real Gemini +project skills/MCP listing from A plus a fresh session, and the cohesive +source/project CLI scenario, remain separate hosted qualification/integration +gates. The existing hosted-only native runtime guard remains unchanged. diff --git a/docs/plans/project-install-e2e/git-fixture-structured-failure-plan.md b/docs/plans/project-install-e2e/git-fixture-structured-failure-plan.md new file mode 100644 index 000000000..2b2e0e48c --- /dev/null +++ b/docs/plans/project-install-e2e/git-fixture-structured-failure-plan.md @@ -0,0 +1,99 @@ +# Immutable Git fixtures: preserve closed failure categories + +2026-10-25. This diagnostic amendment keeps command execution and failure +decisions unchanged. ROOT owns the ordinary-supervisor carrier and focused +workflow; CI/SDK owns the Git-exit carrier and fixture classification. + +## Observed failure and information boundary + +At `d6809d5438654ccdb47bd8d241b96b44e91515c8`, Coverage run `40187422585`, +job `121151062112`, fails `TestDependencyEnvironmentCLIEndToEnd` at +`deps_e2e_test.go:433`, during `deps update demo`. The existing diagnostic +proves only `phase=fetch class=other_git_or_process`. The earlier integration's +different graph fixture failed at the same public acquisition boundary. + +The four typed classes currently available cannot distinguish a real Git exit +from process supervision failure. The Linux child serializes its own process +failure into `linuxOrdinarySupervisorResult.Error`; its receiver uses +`errors.New`, losing structure. A clean nonzero Git exit is also currently +formatted into an opaque error by `sourceacquisition.OSRunner`. + +Do not inspect these error strings to recover a category: they may contain +arguments or streams. Existing logger tests deliberately panic if `Error()` is +called. The local preflight's missing locked-thread observation is a separate +observed host condition, not an explanation for the hosted fetch failure. + +## A. Origin-owned ordinary-supervisor category + +- Add one optional closed stage to the existing private supervisor result: + request decoding, subreaper setup, ordinary preflight, command start, or + descendant cleanup. Assign it only at those existing failure branches. +- On receipt of a nonempty existing Error, preserve its exact message and + failure status. A private typed carrier retains only a recognized stage; + unknown or missing stages remain failing and unclassified. Do not infer a + stage from Error text or allow a category to replace the failure. +- A nonempty stage with an empty Error is contradictory and must fail status + admission; it cannot establish successful execution or cleanup. +- Expose `process.OrdinarySupervisorFailureClass(error) string`, which uses + structural matching and returns a fixed known label or the empty string. + It must never call `Error()` or inspect command/output/environment text. + The helper builds on every GOOS: only the Linux supervisor produces these + carriers, and unrelated errors on any platform return the empty class. +- Preserve existing child exit handling, forced-cleanup causality, stable + handles, status/EOF/trailing-data checks, limits, deadlines and fail-closed + admission. Existing error strings and unwrap/Is behavior remain unchanged + except the additive structural category. +- Add inert producer/receiver/category contracts for all stages, legacy missing + stage, unknown stage, contradictory success and exact message preservation. + Existing process safety tests remain intact. + +## B. Positive Git exit category + +- In only OSRunner's two existing clean nonzero-result branches, wrap the exact + existing formatted error in a private carrier with + `GitTransportExitCode() int`. Its Error delegates to the original formatted + error; Unwrap retains that original error. Do not change the process-error, + context, output-limit, success, environment or command branches. +- The distinctive method prevents a generic supervisor `exec.ExitError` from + being mistaken for a positively observed clean Git exit. Keep the original + integer exit code without imposing a new admission range or decision. +- Extend the existing test-only fixture classifiers using structural checks: + retain current context/unsupported/stdout classes, recognize the process + helper's fixed supervisor class, then recognize the distinctive Git-exit + interface through `errors.As`. Emit only a fixed Git-exit label and integer + status; no formatted error or streams. Preserve existing diagnostic caps, + delegation count, command/context and exact returned bytes/error identity. +- Inert contracts cover code and unwrap preservation, native/nonnative message + equivalence, and logger classification of an exit carrier whose Error panics. + Existing secret/argv/output traps, unknown class and silence tests remain. + +## Finite hosted observation and verification + +After two independent plan rounds and frozen code reviews, add one narrow +current-source workflow using the same Linux runner selection and Go 1.26.10 +as Required. Its sole test invocation selects exactly +`TestDependencyEnvironmentCLIEndToEnd` and +`TestInstallDependencyGraphLifecycle`, with `-count=1` and a bounded test/job +deadline. Keep ordinary process containment and all existing assertions. +No retries, repeat-until-pass loop, alternate process runner, credentials or +additional output/environment authority is permitted. The exact workflow delta +is reviewed with the implementation before one genuine hosted observation. + +Run the existing relevant process and source-acquisition contracts, both inert +diagnostic suites and configured changed-source lint. Preserve exact head/tree, +job and finite output from the focused run. A passing attempt does not identify +the historical cause; a failing attempt can establish only the observed fixed +category and integer status. Any concrete behavior correction requires its own +reviewed evidence. Full final Required/Coverage/Core/native gates remain. + +## R2 amendment: preserve the existing forced-cleanup failure category + +`runExplicit` also fails when `termination.forcedMembers` is true, even if the +supervisor's Error is empty. ROOT will add `process.ErrOrdinaryForcedCleanup` +with the exact current text, `process left live descendants that required forced +cleanup`, and substitute it only for the constant `fmt.Errorf` in that existing +ordinary-runner branch. Preserve the surrounding joins, branch, status and timing; +leave the duplex runner unchanged. An inert contract verifies exact text and +`errors.Is` identity. CI/SDK adds an `errors.Is` fixture classification returning +the fixed `ordinary_forced_cleanup` label. Existing context/output precedence, +four-record caps, original delegate results and all no-Error-read traps remain. diff --git a/docs/plans/project-install-e2e/grok-hosted-research-code-review-r1.md b/docs/plans/project-install-e2e/grok-hosted-research-code-review-r1.md new file mode 100644 index 000000000..322a9eb84 --- /dev/null +++ b/docs/plans/project-install-e2e/grok-hosted-research-code-review-r1.md @@ -0,0 +1,15 @@ +# Grok hosted source research — independent implementation R1 + +Reviewed frozen `3107bced573c4f665d2ce7c440cfb176e4b0a0cf` in `work/uap-project-grok` on 2026-10-23 after both pre-code plan GO reports. Reviewer did not author this runner. **GO for publication of the bounded hosted research checkpoint.** No Grok/Qoder/native client was acquired or executed by this review. + +The actual adjacent matrix guard remains unchanged and is called with `linux-amd64` before output/temp directories, acquisition or native execution. Its direct ordinary-environment refusal has zero requested output-parent effects. The new workflow uses Ubuntu 24.04, pinned actions, exact PR-head checkout, disabled persisted credentials, contents read, an explicit hosted opt-in, a ten-minute deadline and always-uploaded research artifacts. The native child environment is a fresh explicit HOME/XDG/npm/Git mapping plus literal system PATH/TMPDIR/LANG; ambient model/account/repository tokens and proxies are not forwarded. + +The exact official versioned Grok URL and independently measured compressed/binary pins are checked with actual compressed/expanded byte bounds before materializing a fixed executable. The summary correctly labels measured HTTPS integrity rather than claiming a separate vendor digest. The hard eight-minute interval timer and monotonic budget checks cover blocked acquisition, process scheduling, bounded reads and inventory work; each process has at most the remaining budget/30 seconds, closed stdin, live inherited per-file output caps and process-group cleanup. Timeout, overflow, nonzero exit, bad version, malformed/duplicate/non-JSON values or missing probe accounting cannot produce complete collection. + +The finite 19 native probes are exactly version/list-help/inspect-help plus list/inspect at both fixed A/B cwd across empty/user/A/B fixture stages. Local Git initialization uses fixed `/usr/bin/git`, no template hooks and the fresh environment. Fixtures contain only nonexecutable manifest/skill text, no hook, executable, MCP, trust/auth or account data, and refuse an existing plugin or aliased parent. No install/uninstall/enable/disable/prompt/model/native registry mutation command is present. + +Inventories use descriptor-relative no-follow opens/stat, directory/file identity and volatility checks, bounded entry count/depth/path/link metadata, per-file/aggregate actual byte bounds, and reject multiply linked regular files. Symlinks and special files are recorded as metadata without following/opening them. Fixture regular-file baselines are compared after each native stage; additional native-created entries remain observations. The summary explicitly keeps `project_source_qualified:false` and requires human interpretation of actual populated source/version/marker attribution. It makes no native UAP project ownership, MCP/tool, disabled/alias/ancestor conflict, trust/model, restart or network-isolation qualification claim. + +The Qoder change is a mechanical helper extraction plus a workflow path trigger for the shared helper. Independently compared ASTs for `bounded_read`, `child_limits` and `run_probe` against the reviewed Qoder parent: all **identical**, including defaults and cleanup behavior. Qoder pins, allowlist, validators, guard/environment and main control flow remain unchanged. + +Independent bounded verification: `python3 -m unittest discover -s scripts -p 'test_*native_research.py'` passed **15 tests in 1.044s** (8 Grok, 7 Qoder), covering the real guard refusal, total timer, no-follow/special/hardlink behavior, inventory limits and baseline changes, fixture contents/aliases, pin-before-parse ordering, exact finite allowlist/fresh cwd/environment, strict result classification and existing live child output/descendant cleanup. No blocking finding remains. Genuine uploaded outputs still require evidence interpretation before any Grok mutation adapter proceeds. diff --git a/docs/plans/project-install-e2e/grok-hosted-research-code-review-r2.md b/docs/plans/project-install-e2e/grok-hosted-research-code-review-r2.md new file mode 100644 index 000000000..ae2ad5233 --- /dev/null +++ b/docs/plans/project-install-e2e/grok-hosted-research-code-review-r2.md @@ -0,0 +1,75 @@ +# Grok hosted research implementation independent review R2 + +Reviewer: cursor_clients, 2026-10-23. Frozen source +`3107bced573c4f665d2ce7c440cfb176e4b0a0cf` in work/uap-project-grok. +Reviewed complete research runner, shared utility move, inert tests, both +workflow changes and evidence against final pre-code plan `9314feef` and both +plan GO records. No production Grok adapter changes are included. + +## Decision: GO for bounded hosted research + +No implementation blocker found. This approves publishing the research leaf +for genuine read-only populated-source observations. Actual source/version/ +marker attribution and precedence interpretation remain a separate review; +the code always keeps project_source_qualified:false. + +- The actual unchanged imported require_hosted guard precedes output creation, + acquisition and launch. Real refusal is nonzero, bounded, and creates no + requested output parent. Hosted workflow is ubuntu-24.04, contents:read, + exact source checkout without persisted credentials, explicit opt-in, + ten-minute outer timeout and always-upload evidence. No guard is spoofed. +- Exact official versioned archive bytes are bounded during reads and checked + against the measured pin before gzip parsing. Expanded bytes and exact binary + hash are checked before one fixed mode0700 executable is written. Provenance + honestly states these are measured official HTTPS bytes, not an independent + vendor-published digest. No installation script is executed. +- The fresh profile helper plus literal PATH/TMPDIR/LANG is the complete native + environment. Two independent empty local Git repositories prevent discovery + from crossing their project roots; fixed /usr/bin/git init has an empty + template, DEVNULL stdin, the same environment and bounded process helper. + No network/source/remote/auth/hook Git operation is requested. +- Only plain manifest and unique skill text candidates are created under the + exact existing projector's user/A/B directory layout. They declare no hooks, + commands, agents, MCP servers, executables, trust or account settings. Existing + fixture names and aliased parents refuse. Native argv is confined to version, + help, plugin list --json and inspect --json at the selected fresh cwd. +- Nineteen expected observations are ordered and explicitly accounted for. + Nonzero exit, deadline, overflow, malformed/duplicate/nonstandard JSON, + stderr on JSON observations, wrong version or missing probe cannot become + complete collection. Every completed record retains arguments, cwd, byte + counts, hashes, elapsed time, exit/signal and errors. Budget exhaustion yields + incomplete failed collection rather than skipped-success evidence. +- The independent eight-minute main-thread interval timer covers acquisition, + fixture preparation, observations and inventories, with monotonic loop/read + checks and per-process min(30s, remaining budget). Process cleanup has a + bounded wait; the budget context restores timer/signal state before the + post-admission summary is written, leaving margin before the outer job limit. +- Inventory is descriptor-relative and no-follow. Entry/depth/name/path limits, + regular-file size and aggregate actual read limits are enforced while walking + and reading. Regular opens recheck type, identity/link count and before/after + metadata; multiply-linked files refuse. Directory descriptors are validated + before/after recursion. Symlink targets and special-file metadata are bounded + and recorded without following links or opening special files. Baseline + fixture/profile text must remain unchanged; partial/volatile inventory fails. +- Qoder's bounded_read, child_limits and run_probe bodies are AST-identical to + the previously reviewed functions. Qoder pins, exact argv, real guard order, + environment and output acceptance remain intact; its workflow only adds the + shared helper to path routing. The original matrix runner is unchanged. + Child output caps remain hard RLIMIT_FSIZE plus live checks, with process-group + cleanup even when the direct child exits. No post-exit truncation substitutes + for the live output boundary. + +## Independent bounded verification + +- Grok inert contracts: **8 tests PASS** (0.103s), including actual timer + interruption/restoration, no-follow symlink/FIFO and hard-link handling, + entry/depth/file/aggregate/metadata caps, fixed observation accounting and + real guard refusal before effects. +- Existing Qoder inert research contracts: **7 tests PASS**. +- Existing native-client safety/evidence suites: **54 tests PASS**. +- Independently compared moved helper ASTs and verified no matrix-runner delta. + +No native Grok or Qoder binary was executed by this review. Runtime egress +isolation, real project installation/ownership, disabled/ancestor/compatibility +conflicts, skill execution, MCP tool results, trust/account/model and native +restart remain unobserved. A green collection workflow cannot qualify them. diff --git a/docs/plans/project-install-e2e/grok-hosted-research-evidence.md b/docs/plans/project-install-e2e/grok-hosted-research-evidence.md new file mode 100644 index 000000000..4a66f08ab --- /dev/null +++ b/docs/plans/project-install-e2e/grok-hosted-research-evidence.md @@ -0,0 +1,72 @@ +# Grok hosted pre-code source observation checkpoint + +The supplemental research plan at `9314feef` received independent R1/R2 GO; +both reports were committed in `7066ac28` before research source edits. The +original approved single-directory Grok client plan remains unchanged. No Grok +project adapter code is part of this checkpoint. + +The source-only research runner imports the unchanged existing hosted guard +and calls it before output/probe directories, acquisition or execution. It +pins measured exact official 1.0.67 archive/binary bytes, distinguishes those +hashes from a separately published vendor digest, and performs no installation +script. A fresh credential-free profile, two new Git roots and inert text-only +plugin/skill candidates supply the user/A/B observation stages. Git initializes +only those new directories with empty templates; native Grok arguments are +restricted to the nineteen approved version/help/list/inspect probes. + +A hard eight-minute main-thread collector timer covers acquisition, preparation, +all processes and inventory work, with monotonic checks and each process's +remaining-budget deadline. Its dedicated exception reaches group cleanup and +post-admission failed-summary handling. The timer is disabled before writing +that bounded summary; the ten-minute workflow leaves cleanup/upload margin. + +Inventory walks use descriptor-relative no-follow opens/stats with entry, +depth, metadata, per-file and aggregate actual-read bounds. Regular file type, +identity and metadata are rechecked. Symlinks and special files receive metadata +without traversal/opening; multiply linked content is refused. Fixture-authored +configuration/text bytes and modes remain baseline requirements, while native +scratch additions receive separate complete inventories. Overflow, volatility +or unavailable baseline fails collection rather than supplying partial proof. + +The already reviewed Qoder bounded-read and process-output/deadline/group +helpers are moved mechanically into a small shared research module. Their +function ASTs are identical to the reviewed parent checkpoint. Qoder retains +its exact pins, native command allowlist, real guard, explicit environment and +result validation; its workflow also watches the shared helper. The existing +native matrix/guards/tests remain unchanged. + +## Author verification + +- Grok inert safety/contracts: **8 tests PASS 0.101s**. These exercise hard total + deadline/reader admission, complete inventory caps/no-follow symlink/FIFO + behavior, hardlink refusal, immutable text fixture/baseline refusal, fixed + command/cwd/environment/stage accounting, strict JSON/version failure rules, + pin-before-parser and genuine guard refusal with no output directory. +- Existing Qoder inert safety/contracts after helper extraction: **7 tests + PASS 0.913s**, including live child output caps, DEVNULL stdin and actual + inert descendant process-group termination after a deadline. +- Unchanged existing native safety selection: **54 tests PASS 0.413s**. +- Extracted helper AST equality, Python syntax, both hosted workflow YAML/routes + and whitespace checks: PASS. + +The injected subprocess observations are clearly inert test data and never +native proof. No actual Grok list/inspect, native installation, trust, account, +model or MCP process ran on the task host. This checkpoint has not run on the +approved hosted workflow. The resulting summary keeps project_source_qualified +false even when all outputs are collected; a human must inspect genuine exact +source/version/marker attribution and A/B/user precedence before approving the +project directory lane. Actual UAP lifecycle, disabled/alias/ancestor and Claude +conflicts, skill execution, MCP initialization/tool result and native restart +remain separate stronger native gates. This research does not complete all +fourteen client E2E requirements or authorize main/release publication. + +## Independent implementation review closure + +Both independent reviewers returned **GO** at frozen source checkpoint +`3107bced573c4f665d2ce7c440cfb176e4b0a0cf`. R1 independently ran all fifteen +Grok/Qoder inert contracts (PASS1.044s); R2 reran Grok8/Qoder7 (0.103s/0.915s) +and the unchanged54-test native safety selection (0.431s). Both independently +confirmed the extracted helper ASTs are unchanged and reviewed live bounds, +real guard admission, fixed read-only commands and the pending native +interpretation boundary. Their committed reports qualify the research plumbing; +genuine hosted outputs and exact project-source interpretation remain pending. diff --git a/docs/plans/project-install-e2e/grok-hosted-research-plan-review-r1.md b/docs/plans/project-install-e2e/grok-hosted-research-plan-review-r1.md new file mode 100644 index 000000000..7568457ec --- /dev/null +++ b/docs/plans/project-install-e2e/grok-hosted-research-plan-review-r1.md @@ -0,0 +1,13 @@ +# Grok hosted project-source research plan — independent R1 + +Reviewed frozen `9314feef8056812465552f3f76ebd4e6a19b4617` in `work/uap-project-grok` on 2026-10-23. **GO for this bounded research implementation.** The original Grok adapter plan remains in force; no Grok runner or adapter implementation was present for this review. + +The plan obtains the missing genuine project-source observation through the unchanged real disposable GitHub-hosted Linux guard, called before directories, acquisition and native execution. Exact official versioned acquisition is bounded and checked against separately labelled measured compressed/binary hashes; it does not imply a vendor-published digest. Checkout, workflow permissions and fresh child environment preserve the reviewed credential boundary. + +The finite allowlist uses only version/help/list/inspect, explicit A/B cwd, closed stdin, bounded output/deadlines and process-group cleanup. A hard eight-minute collector budget, loop/scheduling checks and at most five-second child cleanup leave time under the ten-minute workflow limit for truthful failure summary and artifact upload. Exhausting the budget fails collection rather than promoting partial observations. + +Only inert text skill/plugin fixtures are created in separate fresh user/A/B roots, with local system Git init marking project discovery boundaries. No payload, hook, MCP, trust, auth/model, enable/disable or native registry mutation command is authorized. The proposed complete inventories are bounded by count, depth, individual/aggregate read sizes and metadata/link text, use no-follow/lstat and handle identity/volatility checks, reject multiply linked regular content, and record special/symlink metadata without opening/following it. Overflow or unavailable/volatile baselines cannot become complete proof. + +The mechanical reuse of already reviewed bounded read/process helpers keeps Qoder pins, allowlist, environment, guard and validators unchanged and requires both inert suites to pass. Guard refusal remains tested without spoofed hosted flags. The exact populated JSON/source spelling will be captured, not guessed from the old parser, and the summary remains `project_source_qualified:false` until independent human evidence interpretation. Missing attribution keeps the adapter gated; configuration/discovery/ownership/runtime/restart remain separate claims. + +No blocking plan correction remains after the explicit total deadline and complete bounded inventory clarifications. Commit both plan GO reports before source; then implementation review and genuine hosted evidence interpretation remain required. This review did not execute Grok or any native client. diff --git a/docs/plans/project-install-e2e/grok-hosted-research-plan-review-r2.md b/docs/plans/project-install-e2e/grok-hosted-research-plan-review-r2.md new file mode 100644 index 000000000..904920059 --- /dev/null +++ b/docs/plans/project-install-e2e/grok-hosted-research-plan-review-r2.md @@ -0,0 +1,54 @@ +# Grok hosted research supplement independent plan review R2 + +Reviewer: cursor_clients, 2026-10-23. Reviewed pre-code plan +`2b49b18710066820ea88ab439d219710830f6bd1` in work/uap-project-grok. +The original client plan and its two GO reviews are not reopened. + +## Initial decision: HOLD for two bounded-collection clarifications + +1. The nineteen native observations at thirty seconds each, plus the ninety + second acquisition budget, exceed the ten-minute outer workflow timeout. + Specify an independent total collector deadline/budget with time left for + failure-summary creation and artifact upload, or increase the outer timeout + with an explicit bounded margin. A hard job cancellation cannot itself + guarantee the promised post-admission summary/upload. +2. The new complete native-created file inventory needs explicit resource and + filesystem limits: maximum entry count, per-file bytes and aggregate bytes; + no-follow inspection; symlink/special metadata recorded without following + links or reading special files; overflow is failed collection. Otherwise + this additional evidence path could bypass the reviewed acquisition and + stdout/stderr bounds through unbounded reads. + +Both findings were sent to the author before implementation. The remaining +plan is appropriately bounded in authority: unchanged real hosted guard before +effects, exact measured official archive/binary pins honestly distinguished +from a vendor digest, fresh credential-free profile and independent project +Git boundaries, text-only plugin/skill candidates, list/inspect observations +without native registry mutation, shared Qoder helper preservation, strict +failure accounting, and project_source_qualified:false until genuine populated +outputs are independently interpreted. No native source identity, discovery, +precedence or stronger project runtime proof is inferred from valid JSON. + +## Final decision: GO at `9314feef8056812465552f3f76ebd4e6a19b4617` + +Both initial findings are closed in the frozen plan before code. The collector +now has an independent hard eight-minute wall deadline covering acquisition, +fixtures, processes and inventories, backed by a Linux interval timer and +monotonic checks. Probe deadlines use the smaller remaining budget. Cleanup +is bounded, and the timer is disabled for a bounded failure summary, leaving +margin before the ten-minute outer timeout/upload. Budget exhaustion is a +truthful incomplete collection failure, rather than an obligation to schedule +all nineteen probes beyond the budget. + +Combined inventory admission now limits entries, depth, individual file and +aggregate content bytes. It uses no-follow inspection and regular-file opens, +type/identity and before/after metadata rechecks, bounded symlink/special +metadata without following or reading special files, and refuses multiply +linked regular content. Overflow, volatility or missing baseline fails the +collection and cannot become partial success. The implementation review must +verify these bounds remain enforced during walks/reads and failure paths. + +This GO approves only the supplemental research plumbing described above. +Commit both independent plan/fix GO reports before implementation. Genuine +populated native output interpretation and the original project's stronger +E2E requirements remain subsequent work. diff --git a/docs/plans/project-install-e2e/grok-hosted-research-plan.md b/docs/plans/project-install-e2e/grok-hosted-research-plan.md new file mode 100644 index 000000000..56d21730c --- /dev/null +++ b/docs/plans/project-install-e2e/grok-hosted-research-plan.md @@ -0,0 +1,157 @@ +# Grok pinned hosted project-source research before adapter code + +2026-10-23, codex_claude. The existing Grok project client plan and its two +independent plan GO results remain in force. This supplement addresses only +the plan's prerequisite to observe exact native project-directory identity +before freezing that delivery lane. No Grok adapter code has been added. + +## Existing genuine evidence and missing fact + +Official stable metadata pins **Grok Build 1.0.67**. The official versioned URL +https://x.ai/cli/grok-1.0.67-linux-x86_64.gz produced 76,217,006 compressed bytes, +SHA256 `6bbea96cde807c13e4c66d9583d8acdd155e446d93131c2820999cf5523ed5a5`, +and a 194,169,184-byte binary, +SHA256 `ce33b1a29e37901f56fe59916cda7a1149e6fd377118f918e90d89f9abb52a51`. +These are measured hashes of bytes from the official HTTPS versioned endpoint; +the captured vendor installer does not publish a separate SHA256. The installer +was read as metadata and never executed. + +All genuine task-host version/plugin-help/plugin-list-help/inspect-help/MCP-help +probes succeeded in a networkless empty-profile namespace with empty /proc; +version output was `grok 1.0.67 (2fe064390d07)`. Actual list/inspect/discovery has +not run on this host. All logs/provenance are retained separately; the verified +reproducible binary copy was reclaimed after checking its recorded hash. + +Official sources reread: +- https://docs.x.ai/build/features/skills-plugins-marketplaces +- https://docs.x.ai/build/features/mcp-servers + +The docs identify `.grok/plugins`, separate user plugins and Claude-compatible +roots. Successful help confirms read-only list/inspect --json grammar. It does +not establish populated record spelling, exact project source, effective +precedence, shadowing/disablement, skill execution or bundled MCP behavior. +Those facts must not be guessed from the old user parser. + +## Planned source and reuse + +The separate Grok leaf starts from the reviewed Qoder research checkpoint +`febd633d`, so it can reuse that reviewed research implementation while keeping +client PRs separate and stacked. After two independent GO reviews of this +supplement, add `scripts/research-grok-native.py`, its inert tests and +`.github/workflows/grok-native-research.yml`. Mechanically move only Qoder's +already reviewed bounded-read and child output/deadline/process-group helpers +to small `scripts/native_client_research.py`; both research runners import the +same implementation. Preserve the existing Qoder CLI allowlist, pins, guard +ordering, environment, result validation and workflow. This is research utility +sharing, not another installer engine, native guard or project adapter. + +Both runners continue importing the actual adjacent native matrix and call its +unchanged require_hosted('linux-amd64') before output/probe paths, acquisition +or execution. Grok's workflow uses only disposable GitHub-hosted ubuntu-24.04, +contents:read, exact source checkout with persist-credentials:false, explicit +native disposable opt-in and no secrets. It runs inert research tests first, +then captures research; always() uploads bounded evidence even on failure. + +## Exact acquisition and runtime authority + +Download only the exact Grok URL above with a 90s request timeout and actual +96MiB compressed cap. Check its measured exact compressed hash before gzip +parsing, then bound expanded bytes to 256MiB and require the exact binary hash +before writing one fixed 0700 executable. Never run the vendor installer. + +Create a fresh temporary parent with separate binary/home/tmp and project-A, +project-B roots. Initialize each project as an independent empty Git repository +using a fixed system Git executable and explicit fresh-profile +environment, so ancestor discovery stops at its selected project. The Git +operation is only local `git init --quiet` inside these two new roots; it has +no network, source acquisition, remote, hook or authentication action. + +Use the existing profile_environment helper, plus literal PATH=/usr/bin:/bin, +TMPDIR and LANG. No ambient HOME/profile, XDG state, workflow token, proxy, +credentials, keys, native trust or permission variables reach Grok/Git. +Native processes receive DEVNULL stdin, explicit selected project cwd, +30s deadlines, child-enforced 1MiB stdout/stderr file caps and process-group +cleanup, exactly as in the reviewed Qoder helper. The collector has an independent hard 8min total wall-clock budget covering +acquisition, fixture preparation, all processes and inventory work. A Linux +main-thread interval timer raises a dedicated collector-deadline exception; +monotonic checks also stop scheduling work when the budget is exhausted. +Each process receives at most min(30s, remaining collector budget), and every +read/inventory loop checks the collector deadline. On expiry, process-group +cleanup has at most its existing 5s wait, then the admitted run writes a failed +summary. Disable the timer before writing that bounded summary. The outer job +has a 10min timeout, leaving nearly 2min for cleanup/summary/artifact upload; +worst-case individual probe timeouts may exhaust the collector and must fail +truthfully rather than require all nineteen probes to run beyond that budget. +Runtime network isolation is explicitly unclaimed. + +## Inert text fixtures and finite read-only observations + +Fixtures contain only `.grok-plugin/plugin.json` with name/version/skills +metadata and `skills//SKILL.md` text. No hooks, agents, commands, +executables, MCP declarations, trust settings or account files exist. The fixture +name is a reserved unique research name in the otherwise fresh profile. Native +install, uninstall, enable/disable, global --trust, /hooks-trust, login/model +and MCP commands are absent. Creating these test-owned directories is fixture +data preparation, not a native plugin-registry mutation. + +First record Grok --version, plugin list --help and inspect --help. Then record +fresh `grok plugin list --json` and `grok inspect --json` processes at each cwd +for these four stages: + +1. Empty user/project-A/project-B roots. +2. Text-only user plugin version 0.9.0, with both projects otherwise empty. +3. Add project-A plugin with the same declared name, version 1.0.0 and distinct + skill marker; project-B still has only the user discovery candidate. +4. Add project-B same-name plugin version 2.0.0 and a third marker; project-A + remains unchanged. + +Use the exact directory layout from the existing projector as the candidate +being investigated. Capture actual output rather than inventing native fields. +After fixture preparation, snapshot the complete fixture configuration/text +files and compare them after the read-only processes; no native-created scratch +or registry files may silently replace those baseline files. Bound each full +combined profile/project inventory to 4096 entries, 1MiB per regular file and +64MiB aggregate actual content read, with depth 32. Walk with lstat/no-follow +semantics and open regular files with no-follow admission; recheck type/identity +and before/after metadata. Record symlink target text and special-file metadata +without following symlinks, opening special files or traversing aliased +directories. Bound link text and metadata strings too. Refuse multiply linked +regular content rather than read an unproved external alias. Any overflow, +volatility or unavailable baseline makes collection fail; no partial inventory +becomes complete proof. Record native-created files separately with their +bounded hashes/metadata. Preserve A/B/user +native inventory differences as observations, never inferred ownership. + +Every expected probe must complete with exit 0, nonempty help/exact 1.0.67 version +token where applicable, and strict complete JSON for list/inspect. Signals, +timeouts, overflow, error, missing/skipped probes or malformed JSON fail +collection; preserve per-probe stdout/stderr hashes, sizes and exit plus the +post-admission failure summary. A guard rejection prints a bounded diagnostic +and nonzero exit with zero output/probe/acquisition/execution effects. + +The summary explicitly keeps `project_source_qualified:false` until a human +reviews the genuine populated outputs. Valid JSON collection alone cannot +establish exact source identity or discovery, even if the workflow is green. +If candidates are absent or the schema cannot attribute the selected project, +the adapter stays gated. Root/reviewers must inspect actual source spelling, +version/marker attribution and A/B/user precedence before approving the existing +single directory lane. Disabled/alias/ancestor and Claude-compatible conflicts, +MCP initialization, tool result, skill execution, native restart and actual UAP +project operations remain separate stronger native E2E gates. No direct TOML +fallback is added. + +## Pre-publication verification + +Commit this plan and both independent review/fix GO reports before source. +Keep the original matrix runner/require_hosted unchanged. Inert tests cover +Grok exact acquisition pins/caps, pure fixture boundaries, fixed argv/cwd, +credential-free environment, complete observation accounting and failure +classification; real-guard refusal creates no outputs. Reuse the existing +Qoder live output-limit/descendant deadline tests to verify the mechanical +shared helper move without executing a native client or spoofing hosted flags. +Python syntax, workflow YAML and both inert suites must pass. Obtain independent +implementation review before root publishes the stacked research PR. + +Hosted output interpretation is a subsequent evidence step. This plan/code, +inert tests and genuine help do not constitute Grok project installation or +runtime proof, and they do not make the all 14 client E2E task complete. diff --git a/docs/plans/project-install-e2e/grok-project-preparation-plan-reviews.md b/docs/plans/project-install-e2e/grok-project-preparation-plan-reviews.md new file mode 100644 index 000000000..ea1da7e25 --- /dev/null +++ b/docs/plans/project-install-e2e/grok-project-preparation-plan-reviews.md @@ -0,0 +1,26 @@ +# Recovered Grok preparation contract — fresh pre-code reviews + +Fresh docs-only target8c986fe16950e0cae5fc79110d0cd32a541d7bd8. Both reviewers +read the complete recovered plan and retained evidence boundary before restored +adapter source. Prior lost-runtime passes and code GOs remain historical only. + +## R1 — finish_sources_vscode_cline + +GO. Exact prior prepared-skills-only/hardened source-scan, staging/context and +attestation contract is preserved. Actual reverified artifact remains source-only +with false native qualification. Fresh reconstructed bytes require current tests, +Windows compile-only evidence, relevant lint and two implementation GOs. + +## R2 — finish_codex_claude_qoder + +GO. Exact scope/original ordinary profile/root/artifact/nonempty-root guards, +no project MCP/global trust, strict fixed-cwd1.0.67 inspect and observed single +trailing projectRoot separator only, finite typed anchored no-follow/nonblocking +source scans, physical staging validation before writes and prepared-always +activation agree with the original approved hardened contract. Direct/composed +attestation refusal and core immutable ownership remain. Genuine source outputs +are narrower than runtime qualification; the unchanged Codex dependency guard +stays an assembly integration condition. Fresh tests/compile/lint/2codeGOs required. + +Neither reviewer edited implementation or executed a compiler/native client. +No blocking plan finding remains. This record precedes restored source edits. diff --git a/docs/plans/project-install-e2e/grok-project-preparation-plan.md b/docs/plans/project-install-e2e/grok-project-preparation-plan.md new file mode 100644 index 000000000..f8f587ce6 --- /dev/null +++ b/docs/plans/project-install-e2e/grok-project-preparation-plan.md @@ -0,0 +1,99 @@ +# Grok project plugin preparation — recovered approved contract + +2026-10-24. Runtime recovery reconstructs the previously approved prepared-only +Grok amendment and hardened implementation contract from retained conversation. +The unpublished old source/objects are lost. Historical plan GOs at d36577d3, +committed6ce5897c, and final code GOs through889ee469 are history, not proof of +these reconstructed bytes. Obtain two fresh independent plan GOs before restored +adapter code, then fresh independent code reviews and actual verification. + +## Genuine source evidence and chosen lane + +Original run40014264873/job120622168241 at published249a9e5731822b79df3b07d9da0ef9b432258caa +produced artifact12531617069, exactly19,676ZIPbytes, SHA256 +5a90bbc0f72a82385653ef657932351f45b013f05d1b5a25313dd578b230e4c1. +After the reset the exact original ZIP was downloaded again, CRC/43members and +all42stdout/stderr lengths+hashes rechecked. All19Grok1.0.67 probes exited0 with +empty stderr. Retain unmodified summary project_source_qualified=false. + +Actual inspect attributes the exact ordinary user .grok/plugins/name directory; +A/B project directories shadow that same-name user source only in their own cwd. +Project entries have scope=project, exact path, enabled=false, projectTrusted=false. +All eight plugin lists and loaded skills arrays are empty. Inspect omits plugin +version/skill markers. This admits only directory preparation, never loaded skill, +version, native-active/installed, trust, MCP or post-UAP lifecycle proof. + +## Restored implementation boundary + +Declare only Grok project scope with LifecyclePrepared; preserve its existing user +CLI registry lifecycle/source. Target exactly /.grok/plugins/ +with current .grok-plugin/plugin.json projection, immutable store, security/digest, +transaction, lock and receipt services. Preserve original detected user ConfigRoot +and executable; never repoint HOME/GROK_HOME/profile to a project. Refuse custom +GROK_HOME, nonordinary .grok user roots, user/project registry overlap, inconsistent +client/scope/target/root/artifact/active locator before mutable work. A nonempty +ProjectRoot with user labels must enter project validation and fail before global +user lifecycle. Persisted project bindings require matching profile/artifact/path. + +Prepare supported skills only; project bundled MCP decisions become unsupported +with the explicit pinned-observation limitation, and MCP-only packages unsupported. +Mixed packages must write no MCP document/manifest member. Unsupported other +components remain unsupported. No direct TOML fallback, global install/uninstall, +trust command, model/authentication change, skill or MCP execution is added. + +Use project-only strictly decoded inspect --json at fixed selected project cwd, +existing15s/1MiB command caps. Require grokVersion=1.0.67, literal exact cwd, bool +projectTrusted, plugin/skills arrays and exact name/scope/path/enabled types. +For projectRoot only accept exact canonical selected root or exactly one trailing +native separator (the actual observed representation). No broad emitted-path +cleaning, aliases, traversal or repeated separators. Duplicate/folded/malformed/ +trailing/unknown-version contracts are indeterminate, never absence proof. +Legacy global plugin list remains unchanged and is never used for project absence. + +Finite user/project/ancestor .grok/.claude scans check same declared plugin names +and selected direct skill names. Permit only ordinary plain manifests; reject +unknown marketplace/cache layouts, aliases/special files/ambiguous identities or +incomplete/volatile scans. Bounds remain64ancestors,128entries per directory, +64KiB per manifest and1MiB aggregate actual reads. Unix opens walk absolute path +components via anchored Openat with O_NOFOLLOW/O_NONBLOCK and directory-only +intermediate hops; Windows handles require disk/nonreparse/exact directory-file +type. Validate fd/path identity, size/mtime around bounded reads. A matching +existing artifact counts as expected only with a matching project receipt; +immutable hash reconciliation stays in the existing core transaction engine. + +Stage beside the watched plugin root at its .grok anchor (including deterministic +discard when only TargetRoot exists). Project projection requires absolute clean +exact-parent/prefix staging plus physical no-alias contained-child checks BEFORE +any write. Preserve active user layout and legacy lifecycle semantics; extraction +of the unchanged user activation body only keeps the scoped dispatcher bounded. + +Project AutomaticallyActivates/VerifierAvailable are false. Early planning +preflight admits not-yet-staged delivery only; any supplied locator must match. +Actual activation requires exact populated delivery and read-only owned source +checks, always returns prepared/package-valid even enabled=true, and refuses +ActivationComplete before generic attestation and on direct calls. Scoped removal +validates exact project/physical artifact/ownership before allowing the existing +transaction engine to remove only its directory; no CLI uninstall and no +external-uninstalled bypass. Review project/plugin and make trust decisions in +Grok; UAP does not do so. Native discovery/runtime remains separately unqualified. + +## Fresh proof and publication + +Restore meaningful strict-schema, scope/profile/artifact, foreign/ancestor/Claude, +finite-bound and real pre-open FIFO/symlink swap regressions; verify typed Unix and +Windows compile-only paths. Stage-positive and user/B/outside alias/nonclean tests +must prove no foreign writes. Restore actual composed TLS-Git UAP A/B installation, +locked sync, pinned repair after source moves, selected update/status/remove and +unchanged original-user/B inventories, with inert native inspect boundary only. +Run narrowly affected client/domain/provider/CLI suites and both lint gates using +fresh workspace toolchain/caches; do not count prior lost-runtime passes. + +Published assembly634457a2 still has inherited Codex nativeconfig TOML imports that +fail unchanged contract dependency closure. Root integrates the separately reviewed +Codex parser extraction; do not widen guard snapshots to hide that dependency. +Shared deps-status false attestation guidance is separate reviewed work. + +Checkpoint approved plans/evidence and small source increments immediately through +the root-supplied exact-tree expected-head publisher to feat/project-grok-e2e. +Do not merge a PR/main/release. No fixture or research command changes the real +hosted guard, native trust or stronger all-client/native/release gates. diff --git a/docs/plans/project-install-e2e/grok-restored-implementation-evidence.md b/docs/plans/project-install-e2e/grok-restored-implementation-evidence.md new file mode 100644 index 000000000..8c1ed1b58 --- /dev/null +++ b/docs/plans/project-install-e2e/grok-restored-implementation-evidence.md @@ -0,0 +1,113 @@ +# Grok restored implementation: current-byte evidence + +Fresh two independent pre-code plan GOs are committed5a4be3f9 after the recovered +actual native source artifact8c986fe1. Lost historical source and test passes are +not current proof. Research proves source attribution only: plugin list and loaded +skills remained empty; project_source_qualified remains false. + +The prepared-only restored adapter source begins8df76685. Current source adds +strict exact context/profile/artifact and before-write physical staging admission, +scoped fixed-cwd1.0.67 inspect, bounded nonfollowing/nonblocking scanner handles, +64ancestor/128entry/64KiB-per-file/1MiB-aggregate refusal, immutable core ownership +and direct/provider ActivationComplete denial. User lifecycle is retained. Mixed +MCP is explicitly unsupported, no MCP projection or native activation is claimed. + +Fresh first full existing Grok/domain run at8df76685 PASS15.420s/0.098s. Meaningful +new project tests at the initial regression source passed0.122s. Later current +client/provider focused tests passed0.234s/0.236s after the missing StatusReady +assignment was corrected. The earlier composed provider check exposed that real +source bug (manual_activation_required despite a prepared skill plan); it was not +counted as a pass. Read-only owned source evidence now additionally requires the +actual exact prepared manifest and matching scoped receipt. + +The public CLI fixture's first attempt overrode only the legacy service runner, +while the real installer engine still had its actual process runner. Its inert +nonexistent executable failed before any native client could execute. The fixture +now injects the same inspect-only boundary into both actual public engine and +legacy lifecycle compositions. A subsequent test incorrectly expected persistent +MCP data from a skills-only projection; it now asserts no fabricated MCP receipt. +The remaining CLI attestation assertion is under investigation; current lint, +Windows compile and two fresh code approvals are pending. No pending gate is a +pass, and no native client, skill, model, trust, auth or MCP process executed. + +## Current composed fixture correction closure + +The attestation experiment confirmed refusal without any native .grok byte, +manifest or portable lock change and without active/installed promotion. Existing +core behavior does record a failed activation attempt in state and refresh its +operational lock files after one read-only source observation. The initial new +fixture incorrectly demanded that whole selected-project operational state be +byte-identical and that no read-only inspect occur. It now checks exact native +bytes and conservative persisted ownership instead, retaining full original-user +and B inventory checks. An explicit ordinary add retries the failed attempt before +the independent pinned-repair journey; repair itself does not activate a failed +binding. This is a test contract correction, not a generic core semantics change. +Actual full public Grok TLS Git A/B saved add/locked-sync/persisted attestation +refusal/retry/moving-source pinned repair/update/status/remove PASS5.299s. Its +client process boundary accepts only inspect --json at the selected cwd; no native +client or MCP runs and no native loaded-skill qualification is claimed. + + +## Current completion branch verification, 2026-10-24 + +The restored Grok leaf is integrated with current main and the shared project +assembly in the Qoder/Grok completion branch. At code tree +`15dcc674b81ab5bbd73e7d46de36f99c8e605fe3` (published head +`e29237fe546d8aebde98adaae59d346142143d2d`), fresh focused validation passes: + +| Gate | Current result | +| --- | --- | +| Full `clients/grok` contracts | PASS, 15.206 s | +| Full domain contracts | PASS, 0.036 s | +| Grok composed provider contract | PASS, 0.018 s | +| Public CLI TLS Git saved Grok lifecycle | PASS, 4.051 s | +| Windows amd64 Grok test cross-compilation | PASS, compile only | +| `go vet` Grok client, providers and public CLI | PASS | +| `gofmt -l` Grok client, provider/CLI fixtures | No changes | + +The public lifecycle uses real TLS Git transport and production saved CLI, +solver, staging, immutable source pin, state and ownership machinery. Its client +boundary is inert and permits only exact `inspect --json` at the selected cwd. +It covers user/A/B preservation, locked sync, denied activation attestation, +explicit retry, pinned repair after the remote source advances, update, saved +Prepared status and exact owned removal. No Grok, MCP, skill or model process +runs in those tests. Windows execution is not implied by cross-compilation. + +The test subprocess alone removes case-duplicate lowercase proxy variables and +uses the checksum-verified local Go 1.26.10 runtime with short temporary paths. +No test assertions or security checks are disabled. The default user profile +semantics remain unchanged. The native source-attribution artifact remains the +prior actual evidence; project_source_qualified is still false. + +Full relevant golangci-lint and two fresh independent implementation reviews +remain pending at this checkpoint. Standard Go vet/format checks do not replace +the repository's size/architecture and changed-lines lint gates. The collector +or unrelated hosted suite cannot close the prepared adapter's remaining gates. + + +## Independent completion implementation reviews + +Frozen product source at published `e29237fe546d8aebde98adaae59d346142143d2d` +(tree `15dcc674b81ab5bbd73e7d46de36f99c8e605fe3`) received two independent +current-byte **GO** decisions for the prepared-only Grok contract: + +- **R1, skillshare_scope:** traced project scope/root/profile identity, exact + staging/layout, anchored typed no-follow/nonblocking foreign-source walks, + pinned bounded inspect and project lifecycle dispatch. Verified no MCP + projection, global trust, native uninstall or attested activation. Independently + reran full Grok client, domain/provider scope and the real TLS Git public saved + A/B lifecycle with inert client transport: PASS (15.273/0.009/0.217/5.756 s). +- **R2, root:** independently read project context/target/root identity, pinned + inspect, finite collision scanning, POSIX and Windows handle implementations, + all staging/lifecycle dispatch and recorded removal authority. Independent + focused project client/provider and public TLS Git lifecycle contracts PASS + (0.043/0.022/3.953 s). + +These approvals cover Prepared/package-valid ownership, not native activation. +The scanner deliberately collides with a same-name user source; native source +attribution does not establish unrestricted same-name active coexistence. This +limitation must remain explicit in the final client matrix. Native MCP startup, +skill execution, model/session use and actual Windows execution remain unproved. +The two fresh code-review gate is closed. Full relevant golangci-lint remains a +separate pending exact-head hosted gate; no local linter was downloaded because +shared runtime capacity is bounded. Go vet and formatting are not substitutes. diff --git a/docs/plans/project-install-e2e/hosted-fixture-portability-evidence.md b/docs/plans/project-install-e2e/hosted-fixture-portability-evidence.md new file mode 100644 index 000000000..8731fe6c8 --- /dev/null +++ b/docs/plans/project-install-e2e/hosted-fixture-portability-evidence.md @@ -0,0 +1,58 @@ +# Hosted fixture portability correction evidence + +Actual46b4678 hosted Core fast run40145167029 exposed fixture setup defects in +macOS job121024834276 and Windows job121024834278; terminal jobs121024833733 +and121024833729 repeated Claude/Grok fixture failures. Production canonical path, +strict native parsing and bounded filesystem discovery guards remain unchanged. + +## Plans and independent code review + +APM and Skillshare independently gave R1 architecture GOs on +b24201c4d33e6720d6212180863a122168f35c8d. Their R2 GOs covered fdd3f5e: +six finite fixture files, exact changed JSON field, literal traversal negatives, +129 physically distinct ASCII entries and the specific unchanged scanner limit, +one platform-absolute inert executable, and an open-handle Windows alias recheck +with independent SameFile proof and canonical positives before and after. + +Both independently gave CODE GO on28ec7ba7d8ba5d73fe4e8e853cf47987cd7bb599, +treecba91337b34675fc241dda3245b8538eec25c95b, after reading all six changed +files and the Windows anchored opener. No production source, parser, sharing +flags, budget, platform skip, workflow selector or assertion was weakened. +The existing before-leaf deterministic ancestor replacement regression remains. + +Root and APM independently gave CODE GO on the separate Claude three-file +canonical seed correction1a93c8d481b9009902f3985a70270b76cb40b33d. +The positive existing project/other/SDK/CLI fixture roots are resolved before +constructing authority. The authored alias/../ traversal and physical escape +proof remain literal, and exact foreign/user/B and result assertions remain. + +## Observed checks + +At exact fixture source28ec7ba7, verified Go1.26.10: + +- Codex provider project contracts:PASS0.720s. +- Kimi/Kiro public SDK stdio/data/repair/restart/removal contracts:PASS1.082s. +- Grok strict native shape and finite collision/budget contracts:PASS0.151s. +- golangci-lint2.13.2 changed-source gate from fdd3f5e for provider/installer/Grok + and public CLI packages:0 issues. +- Authored whitespace check:PASS. +- Windows amd64 Grok test binary compiled successfully,7,588,864bytes, + SHA2564c454aea3923d26ab63da25924b18e7e306d2e43e163810d76f596597f0145a3. + It was not executed here; this is compile evidence only. + +The exact public TLS Git Grok lifecycle was attempted once and failed before +source acquisition with 'initialize private Git transport workspace failed'. +This executor lacks /proc/self/task//children. The existing actual +unsupported-preflight/no-launch regression was then run, rather than modifying +the runner: TestOrdinaryActualUnsupportedPreflightCannotLaunch PASS0.011s. +The local TLS Git journey is not counted as passed. Actual hosted Linux/macOS/ +Windows lifecycle results remain required; there is no local skip or bypass. + +At exact Claude fixture source1a93c8d, its author observed client traversal +PASS0.041s, two public SDK lifecycle casesPASS1.879s, and CLI grouped update +PASS0.546s. Independent critics performed static reviews, not duplicate runs. + +These results qualify the named fixture revisions. The later complete assembly, +including Qoder and remaining client work, requires its own integration checks. +No native vendor, model, MCP server, authentication or trust operation was +performed by these fixture tests. diff --git a/docs/plans/project-install-e2e/hosted-fixture-portability-plan.md b/docs/plans/project-install-e2e/hosted-fixture-portability-plan.md new file mode 100644 index 000000000..e04aa1a0d --- /dev/null +++ b/docs/plans/project-install-e2e/hosted-fixture-portability-plan.md @@ -0,0 +1,85 @@ +# Hosted project fixture portability correction — R2 + +This is a fixture-only follow-up to the twice-reviewed canonical seed plan. +Actual source46b4678 hosted Core fast run40145167029 failed the macOS project +contracts job121024834276 and Windows job121024834278. Terminal jobs121024833733 +and121024833729 repeated the Claude/Grok fixture failures. Linux project contracts +passed. These are observed fixture defects; Linux cannot qualify the corrected +macOS/Windows execution. + +## Scope and observed defects + +1. Codex provider fixture uses os.MkdirTemp without resolving the existing seed; + Kimi/Kiro SDK lifecycle tests use raw t.TempDir. Canonical guards correctly + refuse /var versus /private/var and RUNNER~1 aliases. Resolve each existing + positive seed using filepath.EvalSymlinks before constructing children and + expected authority, as in the accepted canonical fixture plan. Failure is fatal. + Preserve deliberately authored alias negatives and every exact result assertion. + Claude's equivalent list/SDK/CLI fixture follow-up belongs to the Claude lane. +2. Grok inspect JSON test interpolates unescaped Windows paths in strings.Replace. + The intended invalid candidates remain unchanged valid JSON. Marshal only + fixture path string literals, replace the exact projectRoot field, assert the + replacement occurred, and retain each noncanonical/traversal/type/duplicate + negative. Do not normalize actual client output or modify the strict parser. +3. Grok entry-budget fixture uses 129 adjacent Unicode characters as names. + Case-insensitive filesystems collapse some pairs, so fewer than129 entries + exist. Use unique lowercase ASCII indexed names and verify129 entries before + invoking the unchanged bounded scanner. Keep the128-entry production budget. +4. Grok public CLI fixture uses /inert/... as its injected executable identity. + It is not an absolute Windows path. Bind a clean absolute inert path underneath + the existing canonical fixture seed in detector, plan and exact runner assertion. + Do not execute a vendor client or change the production executable guard. +5. Windows post-read Grok recheck fixture tries renaming an ancestor while its + descendant file is open; hosted Windows reports Access is denied. Keep the + existing before-leaf anchored-open deterministic replacement test unchanged. + For the recheck test, open the canonical original leaf, assert a successful + stableProjectPath for that exact path, then supply a separately created + ancestor symlink spelling pointing to the same still-open leaf. Verify + os.SameFile independently and require stableProjectPath(alias, file, info) + refusal. This proves same-object ancestor-alias refusal by the recheck; it + does not claim the hosted filesystem allowed a post-open ancestor rename. + No handle is closed before the check and no production share flags change. + +## Boundaries and acceptance + +Only the finite fixture files above change. No production adapter, path policy, +native parser, platform skip, linter exclusion, workflow selector, retry budget, +assertion relaxation or runtime claim changes. Existing symlink prerequisite +handling stays visible; positive and negative contracts remain separate. + +Run focused Codex provider and Kimi/Kiro SDK lifecycle tests, Grok strict/budget +tests and composed TLS Git lifecycle, relevant changed-line and architecture lint. +Compile the Windows-only test and obtain actual hosted Windows/macOS passes after +integration. A compile is not execution. Record exact reviewed source and commands. +Two independent plan rounds precede these new Grok fixture edits; frozen code is +reviewed independently before integration and publication to the working PR. + +## R1 critique and finite R2 map + +Independent APM and Skillshare R1 architecture GOs reviewed b24201c. R2 pins +the following finite source locations; there are no production edits. + +| File | Fixture correction | +| --- | --- | +| providers/project_codex_fixture_test.go | Physicalize existing short MkdirTemp seed before child paths | +| installer/project_kimi_lifecycle_test.go | Physicalize positive base and immutable source-copy seed | +| installer/project_kiro_lifecycle_test.go | Physicalize positive base and immutable source-copy seed | +| clients/grok/project_contract_test.go | Escaped exact root-field replacement,129 unique entries and inert absolute request identity | +| clients/grok/project_open_windows_test.go | Canonical positive and same-open-object ancestor-alias recheck negative | +| cli/cmd/agentplugins/project_grok_e2e_test.go | One canonical absolute inert executable identity in detector and runner | + +The first five paths are under install/integrationctl/agentplugins. Keep each +noncanonical suffix literal; never use filepath.Join to clean a traversal +negative. JSON replacement must alter the selected field exactly once and every +negative must differ from the valid original. Existing wrong-type, folded-key and +duplicate-key negatives remain. The129-entry case must prove129 distinct directory +entries, then require the existing bounded-discovery-entries refusal specifically. + +The Windows descriptor remains open through canonical positive, alias SameFile +proof, rejected recheck and a second canonical positive. The anchored no-reparse +open of the alias must return the same refusal as the recheck, so a generic closed +handle or changed-metadata failure cannot satisfy the case. Keep the separate +before-leaf actual swap negative and do not claim a post-open rename occurred. + +Record source-only review, Linux runs, Windows compile-only and actual hosted +Windows/macOS execution separately. A missing hosted pass remains open. diff --git a/docs/plans/project-install-e2e/integration-code-review-r1.md b/docs/plans/project-install-e2e/integration-code-review-r1.md new file mode 100644 index 000000000..48023e18a --- /dev/null +++ b/docs/plans/project-install-e2e/integration-code-review-r1.md @@ -0,0 +1,9 @@ +# Project integration conflict resolution — independent R1 + +Reviewed integration `daa705adbfb1681d52f8d6259db7cdf716b75195` and the bounded conflict resolution `ac58f2270a7d183c0b2b6666f1794d598f025ddd^1..ac58f2270a7d183c0b2b6666f1794d598f025ddd` in `cli/internal/agentpluginscli/deps_commands.go` on 2026-10-23. **GO.** This is a cross-merge preservation review; individual client/source implementations retain their existing independent reviews. + +The merge retains ChatGPT project-target preflight immediately after dependency-mutation admission and before acquiring the process lock. The authoritative installer recovery inspection remains after lock acquisition and before reading/resolving the manifest; its extraction into `requireDependencyLockRecoveryClear` preserves the previous error and `ErrRecoveryRequired` behavior. Saved target validation still runs before resolution and now rejects unsupported project targets before personal/manual guidance. + +The source implementation's `publishResolvedDependencyLock` remains intact and called only for a changed, writable, non-dry-run result. It still rereads and compares the manifest, populates the selected dependency store through `ensureDependencyStore`, and then writes the dependency lock. No API or source cache-population step was discarded by the ChatGPT merge. + +Independent bounded regression: `go test -p=2 ./cli/internal/agentpluginscli ./cli/cmd/agentplugins -run '^TestDirectSourceSavedLifecycleRealHTTPS$|^TestProjectChatGPT' -count=1 -timeout=3m` passed (internal CLI 5.619s; public CLI 0.511s), using the prescribed Go toolchain/cache and Node-enabled PATH. These tests exercise real HTTPS saved-source/cache lifecycle and unsupported project intents/saved-target refusal before effects. No native client, auth, trust or model action was performed. diff --git a/docs/plans/project-install-e2e/integration-code-review-r2.md b/docs/plans/project-install-e2e/integration-code-review-r2.md new file mode 100644 index 000000000..6027c5877 --- /dev/null +++ b/docs/plans/project-install-e2e/integration-code-review-r2.md @@ -0,0 +1,78 @@ +# Combined project client integration independent review R2 + +Reviewer: cursor_clients, 2026-10-23. Frozen integration: +`daa705adbfb1681d52f8d6259db7cdf716b75195` in `work/uap-project`. +This is a bounded integration/merge review, supplementing the separate client +implementation reviews. No implementation changes were made during review. + +## Decision: GO + +The merge retains the independently reviewed Codex, ChatGPT, Cursor, OpenCode +and Claude client implementations and their fixtures. No blocker was found in +the two substantive conflict resolutions. + +### Client and fixture preservation + +Compared each final client adapter directory against the corresponding second +parent of its integration merge. Every adapter directory is byte-identical: + +| Client | Reviewed leaf integrated | +| --- | --- | +| Codex | `e38612444534df661adac2b864a6e9bcc05c5080` | +| ChatGPT | `0a7f6442a50c14dc0b6f556904d4692a70e1cdf0` | +| Cursor | `1f7315e9cb7dd786de92857f5d7e2188ce88311a` | +| OpenCode | `72254cd3ae2c2ed96103d78334a8316a3ad14ad5` | +| Claude | `ca4de139dfcb3d19b3869385504eb2a1d7fadc00` | + +Also compared all tracked client-named test/fixture files from each leaf; +none changed or disappeared. The integration deletes no tracked path relative +to neutral-component base `c96fd096`. Existing Gemini/source/core work remains +in the ancestry. The shared client definition changes retain all existing +user traits and add project scope only for the approved client lanes; ChatGPT +remains an explicit unsupported project target. No legacy capability removal +or VS Code modification is introduced by these merges. + +### Native source selection conflict + +- PROJECT_TESTS contains both the Codex project component lifecycle and Claude + project plugin lifecycle. The workflow passes --project-components for + either client, while OpenCode retains its original selection. +- Distinct Codex and Claude exact-selection tests survive; neither silently + overrides the other Python method. Both compare the anchored selection with + actual Go declarations and the union of each client's legacy and project + required suites. Neither project test enters released CLIENT_TESTS. +- Existing PINS, PATTERNS and REQUIRED_TESTS are structurally unchanged. + Among existing runner functions, only main changes; require_hosted, + provisioning/attestation, profile creation and release preparation helpers + are unchanged. The project selector rejects released, draft, prepared-input + and prepare-only combinations before hosted admission or effects. +- Failure, omitted required tests and any skipped native test remain failures. + The current-source project flags do not qualify historical/released proof. + Workflow permissions, platforms, exact source checkout, credentials handling, + timeouts and evidence upload remain unchanged by the integration. + +### Dependency lock conflict + +The ChatGPT project's read-only target preflight runs before environment lock +creation. The locked operation still checks recovery, rereads and validates the +authoritative manifest, and resolves against that snapshot. The source lane's +publication helper remains intact: reread manifest equality is required before +publishing the dependency store and lock. Extracting the recovery inspection +into a helper preserves its error and recovery-required behavior. Thus the +merge retains unsupported-target refusal and source-cache publication safety. + +## Independent bounded verification + +- `python3 -m unittest discover -s scripts -p test_native_client_matrix.py`: + **18 tests PASS**. +- `python3 -m unittest discover -s scripts -p test_released_native_evidence.py`: + **42 tests PASS**. +- Read the combined merge diffs, complete runner selection changes, workflow + condition and dependency lock implementation; compared adapter and fixture + Git objects against all five integrated leaves. + +No native client payload was executed, no hosted environment was fabricated, +and no broad compile was repeated. This GO covers merge correctness and +preservation. It does not substitute for the pending genuine hosted native, +cross-platform or release qualification runs. The separately reviewed CI +routing amendment is still required to trigger applicable jobs on stack bases. diff --git a/docs/plans/project-install-e2e/integration-eight-clients-r1.md b/docs/plans/project-install-e2e/integration-eight-clients-r1.md new file mode 100644 index 000000000..6503f6853 --- /dev/null +++ b/docs/plans/project-install-e2e/integration-eight-clients-r1.md @@ -0,0 +1,19 @@ +# Eight-client integration merge — independent R1 + +Reviewed frozen aggregate `e18b94a68ea280414e199a286142dd335fdfdf7d`, Kiro merge `0c2edce6` and Copilot merge `e18b94a6` on 2026-10-23. This is limited to root-authored integration preservation; reviewed client implementations retain their own independent GO reports. **GO.** No source was changed or broad additional test run for this review. + +The Copilot merge's `nativeconfig.supportedCodec` retains the complete earlier set, including `CodecCodex`, and adds `CodecCopilot`. Both constants and dispatch branches remain present. Earlier Codex-only authentication/TOML checks and options remain intact; Copilot's new project transport codec remains the same reviewed implementation. No codec was substituted for another. + +The domain table retains independent project declarations for Cursor and Copilot, plus the previously reviewed Codex, Claude, Gemini and OpenCode declarations and the new reviewed Kiro declaration. ChatGPT keeps its explicit project refusal/manual user boundary; no project capability is invented for it or any later client. Copilot's unsupported command wrapper and existing user shared-backend policy remain intact. + +Independent byte comparisons show all **13 Copilot client files** identical to reviewed leaf `3f4a527c`, all **29 Kiro client files** identical to reviewed leaf `0ec535e2`, and both public Copilot composed E2E files identical to the final reviewed leaf. The final exact-package and direct-delivery corrections therefore survive integration. The first-parent merge diffs introduce only their reviewed client/native codec/domain/tests/evidence changes, with no other semantic conflict resolution. + +For each of the four compatibility goldens, independently decoded and compared both sides of each merge: all 14 client rows remain in order. Kiro merge `0c2edce6` changes only Kiro's scope list from `[user]` to `[user,project]`; Copilot merge `e18b94a6` changes only Copilot's corresponding list. Every other field and row is unchanged in each comparison, preserving all earlier client scope declarations. + +The documentation conflict preserves the earlier OpenCode row and adds only Copilot's reviewed two-GO/native-pending status. Pending native discovery/trust/MCP/runtime evidence is not relabeled by this merge. Parent's bounded combined regression run remains separate execution evidence; no main/release publication authority is added by this integration GO. + +## Parent documentation follow-up R1 + +Reviewed documentation-only `bbc0473ae554491eafac5d9b8d50be6b9465bbde` plus clarification `d194b7f00e5cb2cff764a1d7a1ddeb74992afda4`. **GO.** README and dependency guide preserve default user scope, explicit project selection, invocation-directory fallback without ancestor search, and selected-scope export/import with a separate exchange directory. The new project guide correctly separates portable intent from local `.uap` ownership/data/recovery, exact recorded repair/removal, immutable cache, unsupported ChatGPT and per-client prepared/native activation limits. Attestation does not bypass a project adapter's admission; there is no automatic trust/auth action or all-14/native/runtime/release completion claim. + +Requested one wording clarification: “a local path or archive” could misleadingly include eligible remote HTTPS archives, whose saved intent is supported. `d194b7f0` explicitly says “a local directory or local archive,” preserving that distinction. Scope/root commands, maintenance/recovery and transfer examples are consistent with the reviewed CLI contracts. No broader test run was needed for these prose-only changes; the independently reported bounded aggregate test run remains separate execution evidence. diff --git a/docs/plans/project-install-e2e/integration-eight-clients-r2.md b/docs/plans/project-install-e2e/integration-eight-clients-r2.md new file mode 100644 index 000000000..21283808b --- /dev/null +++ b/docs/plans/project-install-e2e/integration-eight-clients-r2.md @@ -0,0 +1,25 @@ +# Eight-client integration seam — independent R2 + +## Decision and scope + +**GO** for root's merge-only production integration `e18b94a68ea280414e199a286142dd335fdfdf7d`, plus documentation through `d194b7f00e5cb2cff764a1d7a1ddeb74992afda4` (including `bbc0473ae554491eafac5d9b8d50be6b9465bbde`). Reviewer codex_claude, 2026-10-23. This review covers the requested merge resolutions, preservation of independently reviewed leaf code/goldens, and scope documentation. It does not replace final leaf reviews, establish all 14 clients complete, or establish native/runtime/release qualification. No production code changed or native process executed by this review. + +## Production seam + +The nativeconfig conflict resolution preserves `CodecCopilot` and `CodecCodex` in both the declarations and `supportedCodec` union, alongside every previously supported codec. Copilot's MCP projection path remains present; the Codex-specific TOML collection, authentication-option fencing and project projection from the other parent remain present. The merge does not silently route either client through another dialect. + +The domain resolution retains both Cursor and Copilot `withProjectScope(..., LifecycleNativeConfig)` wrappers, including Copilot's existing `withoutCommands` policy. Existing Codex, Claude, Gemini, OpenCode and Kiro project wrappers survive; ChatGPT's explicit unsupported project boundary and the still-unmerged clients' prior policies remain intact. The wrappers preserve user lifecycle defaults and the existing traits rather than replacing them with project-only values. + +Independent byte comparison against the reviewed Copilot parent `3f4a527cfe33cada14e6c53be6be233664efd8aa` found 20 source/test/golden files identical. The seven differing shared files consist of the combined nativeconfig project/types files, domain policy, and four compatibility goldens; their differences preserve the already integrated Codex/other-client work. Independent comparison of all 36 selected Kiro client/project fixture/provider/SDK files against reviewed leaf `0ec535e2dd12e9d7ee965c916714cd2ae4ba3c76` found no byte differences. The four planner goldens preserve every non-Scopes JSON value from both merge parents; their combined scope changes reflect the admitted client union without changing component support or readiness facts. No reviewed assertion or adapter leaf was deleted by these merge resolutions. + +Root reported the combined bounded selection PASS at this frozen production SHA: nativeconfig 0.039s, domain 0.003s, planner 0.018s, and public CLI 29.299s. Those are root-run results, not an independent rerun. No extra broad compilation was necessary for this merge-only check. + +## Documentation R2 + +The README, UAP dependency guide and new `docs/PROJECT_INSTALLATION.md` correctly mark project installation source-only/unreleased. They keep user scope as the default, require explicit project selection, document invocation-directory behavior when the project root flag is omitted, and avoid implicit parent/Git-root discovery. Examples consistently keep add, locked install, update, repair, removal and recovery in the selected environment. The source text correctly distinguishes a **local** directory/archive imperative flow from a ready HTTPS archive that can contribute saved dependency intent; the final `d194b7f0` wording closes that distinction. + +The portable `uap.toml`/`uap.lock` pair is distinguished from local `.uap` ownership, journals, managed packages and retained data. Project export/import examples explicitly select their source/destination project and a separate exchange directory, preserve the exact agreement pair, and do not claim to transfer credentials, settings, receipts or plugin data. Cold restore remains subject to exact-byte availability and source access. Removal retains data/content by default and recovery stays pinned to the same selected authority. + +Prepared configuration is accurately distinguished from native discovery, authentication, trust and runtime. Documentation constrains `--activation-complete` to adapter-supported attestation and does not let it override unsupported, shadowed or unobserved project behavior. User Cursor's existing operator-attestation behavior remains labeled user-only. ChatGPT project requests remain explicitly unsupported, including no-save. The docs defer client-specific qualification to supported-target reporting and reviewed per-client evidence instead of claiming every client is implemented or genuinely native-tested. + +The docs-only diff passes `git diff --check`. No new tests were required for this prose change. diff --git a/docs/plans/project-install-e2e/integration-implementation-evidence.md b/docs/plans/project-install-e2e/integration-implementation-evidence.md new file mode 100644 index 000000000..f220062cf --- /dev/null +++ b/docs/plans/project-install-e2e/integration-implementation-evidence.md @@ -0,0 +1,106 @@ +# Project client stack integration evidence + +Recorded 2026-10-23. These records distinguish the production CLI/filesystem +journey from genuine native-client observations and release qualification. + +## Reviewed composition checkpoint + +`daa705adbfb1681d52f8d6259db7cdf716b75195` integrates the separately reviewed +Gemini, Codex, Cursor, Claude and OpenCode project lanes plus ChatGPT's explicit +unsupported-project admission boundary. It includes the reviewed source +transports, shared component transaction helper, project namespace guard and +Windows identity correction. Both independent integration reviews are GO. + +Merge resolutions preserve both sides of the two functional seams: + +- Dependency locking keeps project-target preflight before the process lock, + followed by recovery inspection and authoritative validation under the lock. + Lock publication retains the manifest recheck and immutable-store population + before writing the portable lock. Neither client admission nor source cache + restoration is dropped by the merge. +- Native source selection is the union of the reviewed Codex project component + and Claude project plugin tests. Both distinct exact-selection regressions, + all existing required user tests, disposable-host guards and release/draft + restrictions remain intact. + +The independently compared client adapter directories and client-named tests +match the reviewed leaf commits. No tracked implementation or test was deleted +by this integration. Shared-helper review records retain their later whole-file +lint follow-up rather than replacing it with an earlier report. + +## Bounded combined execution + +Go 1.26.10, GOMAXPROCS=3, `-p=2 -count=1 -timeout=3m`, exact integrated +production code at the checkpoint above: + +```sh +go test -p=2 ./cli/cmd/agentplugins ./cli/internal/agentpluginscli \ + ./install/integrationctl/agentplugins/domain \ + ./install/integrationctl/agentplugins/planner \ + -run 'Project|CursorProject|Compatibility' -count=1 -timeout=3m +``` + +| Package | Observed result | +| --- | --- | +| Public CLI composition and project lifecycle selection | PASS, 19.434s | +| Internal CLI scope and admission selection | PASS, 0.124s | +| Domain target and compatibility selection | PASS, 0.005s | +| Planner compatibility selection | PASS, 0.021s | + +The retained native matrix selector/guard suite passes all 18 tests. The +independent integration reviewer also ran all 42 released-evidence verifier +tests successfully. These inert tests do not execute a native client or qualify +MCP, trust, authentication, model behavior or an installer release. + +## Actual stack CI routing + +The native workflow previously accepted only PRs targeting main. Core CI also +missed temporary client/checkpoint bases. The amendment and its two independent +plan reviews precede the two-line routing implementation `7a351367`; both final +implementation reviews are GO. Existing jobs, paths, platforms, action pins, +permissions, guard code and failure semantics are unchanged. + +After publication, GitHub created Native Client Runtime runs +[40011489662](https://github.com/777genius/universal-agent-plugins/actions/runs/40011489662) +for the Codex client head `0d1319f947c7f4403691b3a60b1193baa0074ee9` and +[40011624952](https://github.com/777genius/universal-agent-plugins/actions/runs/40011624952) +for the Claude client head `2d8102782861ca3005ca4b8c5572ebde26b91d08`. +Run creation confirms routing, not a passing outcome. Native project evidence +remains pending until the exact required tests complete on genuine disposable +GitHub-hosted machines. The final PR status and artifacts are authoritative for +those outcomes. + +## Remaining work at this checkpoint + +Other client leaves and their public lifecycle coverage are in progress. This +document is an integration checkpoint, not an all-client completion or release +claim. Client PRs merge only into the project feature; the two deliverable +feature PRs remain unmerged into main until separately authorized. + +## Reviewed eight-client integration + +Production checkpoint `e18b94a68ea280414e199a286142dd335fdfdf7d` adds the +independently reviewed Kiro and Copilot project adapters to the earlier +composition. Both final leaf reviews and both root merge reviews are GO. +The merge preserves the union of Codex/Copilot native codecs, every earlier +project capability, and all non-scope compatibility golden fields. Client +implementation and E2E files match their reviewed leaves byte for byte. + +A bounded combined run on that exact production checkpoint used Go 1.26.10, +GOMAXPROCS=3 and `-p=2 -count=1 -timeout=3m` with +`-run 'Test(Project|Compatibility|Copilot|Codex)'`. Observed results: +nativeconfig **0.039s**, domain **0.003s**, planner **0.018s**, production public +CLI project journeys **29.299s**, all PASS. This is configuration/installer +execution, not native client discovery, trust or MCP runtime qualification. + +The source-only project guide and updated dependency/README instructions at +`bbc0473a` plus `d194b7f0` also received two independent GO reviews. The latter +clarifies that only local directories/local archives use imperative source +intent; eligible remote HTTPS archives retain saved dependency support. + +Gemini leaf commit `938bcb8fa00da3fa402430de058d27df472a3bab` now has actual +project-contracts PASS on Ubuntu 24.04, macOS 15 and Windows 2025 in Core run +[40008798665](https://github.com/777genius/universal-agent-plugins/actions/runs/40008798665). +Windows job 120603490211 passed the corrected namespace identity and complete +selected scoped lifecycle checks. This does not relabel the still-pending +other-client/native jobs or claim all-client completion. diff --git a/docs/plans/project-install-e2e/integration-kimi-devin-evidence.md b/docs/plans/project-install-e2e/integration-kimi-devin-evidence.md new file mode 100644 index 000000000..5986b67ec --- /dev/null +++ b/docs/plans/project-install-e2e/integration-kimi-devin-evidence.md @@ -0,0 +1,17 @@ +# Restored Kimi and Devin assembly integration + +Source checkpoint: a9dd44022252a0ee96c82d6840b4e9e880e5c606, published with the identical tree at 48333d69312c5133f135c7eded4b2a0517a3d10c. The published Kimi and Devin client heads retain their two independent implementation review records. + +Kimi merged mechanically. The subsequent Devin merge had exactly two manual conflicts: nativeconfig project dispatch now retains both independent Kimi and Devin branches, and supportedCodec retains both codec IDs with all earlier entries. Domain declarations and compatibility goldens merged automatically. Direct byte comparison found no differences in the Kimi/Windsurf client directories or their public CLI lifecycle/fixture files against the corresponding published client heads. + +Fresh Go 1.26.10 selected nativeconfig Kimi/Devin tests passed 0.043s; full domain passed 0.053s. Full planner ran and FAILED on two legacy scope expectations: TestPreparationRequiresSupportedNativeUserTarget accepts the newly qualified Kiro project trait, and TestPlannerFailsClosedForUndetectedClientAndUnsupportedScope requests Cursor project scope without a canonical ProjectRoot. A separate reviewed correction must determine and retain the meaningful negative boundary, not simply remove assertions. These are not counted as planner passes. + +The known inherited Codex TOML dependency-closure issue remains an independent required shared correction; no guard snapshot is widened. Combined native runtime and final all-client feature qualification remain pending. Two manual-integration reviews are requested at this exact source. + +## Fresh combined public CLI execution + +At documentation-only successor 6401a953 (production unchanged from a9dd4402), the actual command `go test -p=2 ./cli/cmd/agentplugins -run 'Test.*Project' -count=1 -timeout=5m` passed in **97.217s**, using verified Go 1.26.10 and the production composed CLI tests on Linux. This includes the assembled project source/lifecycle/ownership contracts; it is not native vendor-client execution, a Windows/macOS pass, or a claim that the full planner/architecture gates are green. Both previously recorded planner failures remain required follow-ups. + +## Independent integration reviews + +Two GPT-6.1 Sol xhigh reviewers independently approved the exact production union at a9dd44022252a0ee96c82d6840b4e9e880e5c606. Their preserved reports are `integration-kimi-devin-restored-r1.md` and `integration-kimi-devin-r2.md`. Both distinguish the successful Linux public CLI and selected codec/domain checks from the remaining planner, architecture, cross-platform, and native-runtime qualification gates. This documentation checkpoint changes no production or test bytes. diff --git a/docs/plans/project-install-e2e/integration-kimi-devin-r2.md b/docs/plans/project-install-e2e/integration-kimi-devin-r2.md new file mode 100644 index 000000000..2110f2c41 --- /dev/null +++ b/docs/plans/project-install-e2e/integration-kimi-devin-r2.md @@ -0,0 +1,23 @@ +# Kimi and Devin assembly union — independent R2 + +Reviewer finish_grok_integration. GO for merge-only production integration +`a9dd44022252a0ee96c82d6840b4e9e880e5c606`, scoped to the two nativeconfig conflict +resolutions and preservation of the independently reviewed leaves. No main, +release, all-fourteen/native/runtime qualification follows. + +Inspected both merge-parent conflict views: CodecKimi and CodecDevin survive +in declarations and supportedCodec alongside every prior codec. projectServer +dispatches each to its own projector, preserving existing Codex/OpenCode option +fences and order; no shared dialect fallback is substituted. The domain retains +both project-scope wrappers and original user traits. + +Byte comparisons against the first parent found the Kimi client directory and +Kimi codec/tests unchanged. Comparisons against the reviewed second parent found +the Devin client directory and selected provider/installer/public-CLI lifecycle +fixtures unchanged. The merge-only nativeconfig delta passes whitespace checks. + +Root's actual fresh codec0.043s, domain0.053s and combined public project CLI +97.217s passes support this seam; the reviewer did not rerun or relabel them. +Two full-planner failures remain separately assigned to the reviewed strict +project-intent amendment. This GO does not conceal or waive those failures. +No source edit, native client or compiler execution occurred in this review. diff --git a/docs/plans/project-install-e2e/integration-kimi-devin-restored-r1.md b/docs/plans/project-install-e2e/integration-kimi-devin-restored-r1.md new file mode 100644 index 000000000..bb616b815 --- /dev/null +++ b/docs/plans/project-install-e2e/integration-kimi-devin-restored-r1.md @@ -0,0 +1,29 @@ +# Restored Kimi/Devin union: independent integration review R1 + +Reviewer: finish_sources_vscode_cline. **GO for the integration delta** at +a9dd44022252a0ee96c82d6840b4e9e880e5c606, with the separate generic planner +and inherited Codex dependency-closure followups still required. + +Inspected the combined merge conflict resolution. projectServer retains both +independent Kimi and Devin dispatches after the existing OpenCode-options +refusal; neither can erase or redirect the other. supportedCodec is the exact +union of both new IDs and all prior IDs. The auto-merged catalog/golden changes +keep each leaf's separate project policy and preserve user traits. + +Independently compared the Kimi and Windsurf client directories and their public +fixture bytes against the surviving published leaf refs: no differences. The +integration does not rewrite those reviewed implementations or conflate their +project config formats. + +The root's actual fresh native-codec selection PASS0.043s, complete domain +PASS0.053s and all public CLI tests matching Test.*Project PASS97.217s support +the combined dispatch/catalog/composition behavior. No duplicate compiler or +native client process was run by this reviewer, and those fixture tests are +not native vendor discovery/runtime evidence. + +The two full-planner failures remain explicit separate work: malformed Kiro +project preparation needs strict contextual admission, and qualified Cursor's +missing project root must not retain an obsolete unsupported-scope expectation. +The inherited Codex TOML dependency-closure gate is not waived or called green. +Both followups must be correctly reviewed and verified before assembled feature +qualification. No blocking finding in this pure Kimi/Devin integration delta. diff --git a/docs/plans/project-install-e2e/kimi-code-review-r1.md b/docs/plans/project-install-e2e/kimi-code-review-r1.md new file mode 100644 index 000000000..599684246 --- /dev/null +++ b/docs/plans/project-install-e2e/kimi-code-review-r1.md @@ -0,0 +1,77 @@ +# Kimi Code project adapter — independent code R1 + +Reviewed frozen clean `7111008f61adc7a7859d6ce44dbee848943db74c` in +`work/uap-project-kimi`, including adapter checkpoint `456cb039` and its public +composition follow-up. **GO.** Reviewer did not author the implementation. +The original client plan has two GOs, pre-code pinned evidence is committed, +and the exact shared host-version prerequisite retains its separate two GOs. +No Kimi/native client, launcher, model, trust/auth or MCP process was executed +by this review. + +## Source and authority + +Independently read the captured official exact-tag 2.3.1 configLoader, +config-schema and skillRoots source, with recorded blob IDs/provenance. These +confirm strict JSON loading, user then worktree-root compatibility then selected +cwd branded MCP precedence, explicit stdio/http/sse transport, and nearest .git +skill-root selection. The adapter follows that current component contract; +the preserved user plugin registry is not reused as a project installer. +Static source/help evidence remains separate from actual project discovery. + +New install/update/repair requires the selected absolute clean kimi/kimi.exe +and exact host-observed 2.3.1. No adapter/SDK probe or global environment change +is added. UserConfigRoot remains the original selected root. Scope, native root, +exact physical-artifact leaf and `.uap/managed/clients/kimi/` must +agree. The generic entry gate plus both project hooks qualify only project +scope; user SSE/declarative support is preserved. A selected skill root cannot +silently move into another Git ancestor or follow an aliased marker. + +Both composed and direct activation reject a rebound/empty delivered path; +pre-staging preflight alone allows an absent Delivery. Receipt/layout checks +refuse wrong scope, package, root, unknown/duplicate/path-escaped objects and +stale foreign entries. Active projection recomputes and compares the complete +stored native entry against the canonical selected package/data/cwd/launcher +without writing immutable bytes. Wrong selected data requires refresh rather +than adopting old locators. Native mutation uses the already reviewed shared +component transaction and exact kernel receipts, preserving foreign disabled +members, unknown policy, skills and other environments. + +Positive activation attestation is rejected before generic fallback. Configured +files remain prepared with installation verification, never attested/loaded. +Native/user/compatibility precedence is warning context, not evidence of trusted +effective session loading. Exact recorded removal reconstructs package/native +authority without current version or Git admission and never invokes a user +registry/native cleanup command. Recovery and persistent data retain the +existing core ownership rules. Four compatibility goldens change only Kimi's +scope list, retaining every other field; old client tests/source remain present. + +## Independent verification + +- Project provider suite: **PASS 0.087s**, including scope/artifact/direct-path, + receipt/collision/stale ownership, data binding, nearest Git, component update/ + repair/removal and attestation boundaries. +- Public SDK project suite: **PASS 0.149s**; dedicated native codec tests: + **PASS 0.003s**. +- All three public composed CLI scenarios: **PASS 8.388s**, using genuine + loopback TLS Git/ZIP acquisition and production loader/store/planner/installer/ + transactions, with expressly inert detection/security and unexecuted launcher + bytes. They cover saved lifecycle, exact old-pin repair, pending directory + journal recovery, selected update/remove, cold portable restore, no-save + composition and user/B/data preservation. +- Complete preserved Kimi client suite: **PASS 0.013s**; `git diff --check` + passed. The author's exact changed/full-file lint evidence is separate. + +No blocking finding remains. Genuine project discovery, effective extra roots, +workspace trust, session policy, MCP initialization, model/auth and restart +remain pending; source/help and fixture E2E do not qualify those or a release. +Root owns publication and feature integration after the other independent GO. + +## Mechanical integration preservation closure + +GO remains valid at clean `83c85702acf1697a59d724c26159e341bc8ba942`. Independently compared every path in the authored `3e072c99..7111008f` delta against the refreshed head: all 33 paths are byte-identical. The merge brings the reviewed root base and evidence without changing the Kimi implementation or its assertions. No new compilation or native process was needed for this byte-preservation check. + +## Final authority/admission correction closure + +**GO at frozen `95b5f34cc9fb7796e79e0a76e3b853d8a8ce5386`.** Independently inspected the narrow delta after the byte-preserved 83c baseline. Project ValidateBindingProfile now requires literal ClientID kimi before profile/root checks, and a copied codex binding is refused with exact unchanged inventory. The admission fixture now supplies pathpolicy.Policy, first proves the qualified host reaches PlanReady, and requires the actual current-version/harness diagnostic for every negative case. This removes the prior possibility of a generic missing-Paths error satisfying those negatives. No remaining code blocker found. + +The prior independent full provider/SDK/codec/public CLI/legacy passes at711 remain recorded above. Latest independent two-case execution was attempted once and failed during setup writing /dev/shm/go-build.../_testmain.go with ENOSPC, before assertions; it is not a pass. Parent directed static final closure using the unchanged prior evidence plus separately attributed author's corrected provider PASS0.139s/focused0.016s and changed lint0, rather than repeating resource failures. Genuine native qualification remains pending and publication remains the parent's decision. diff --git a/docs/plans/project-install-e2e/kimi-code-review-r2.md b/docs/plans/project-install-e2e/kimi-code-review-r2.md new file mode 100644 index 000000000..1287aaabc --- /dev/null +++ b/docs/plans/project-install-e2e/kimi-code-review-r2.md @@ -0,0 +1,37 @@ +# Current Kimi Code project adapter — independent implementation review R2 + +Reviewed initial authored checkpoint `7111008f`, the byte-preserving static component-base merge `83c85702acf1697a59d724c26159e341bc8ba942`, and final corrective checkpoint **`95b5f34cc9fb7796e79e0a76e3b853d8a8ce5386`** in `work/uap-project-kimi`. Reviewer cursor_clients did not author or edit the adapter. **GO for the final source checkpoint**, with the explicit independent-execution resource limitation below. No native project/runtime, release or all-client qualification is conferred. + +## Findings and final closure + +1. The direct project `ValidateBindingProfile` boundary omitted the exact client discriminator. Higher-level inspection already rejected contradictory client IDs, but a direct caller could present an otherwise exact Codex-tagged binding. Final95b requires literal `binding.ClientID == "kimi"` only in the project branch. The added regression invokes that direct boundary, requires its client-specific refusal and compares unchanged whole inventories. Existing user behavior is retained. + +2. `TestProjectKimiRequiresCurrentObservedHarnessAndNearestGit` constructed a Planner without its required path policy, so its version-negative cases could pass on an unrelated earlier failure. Final95b supplies `pathpolicy.Policy{}`, first proves the exact observed current harness reaches `PlanReady`, and requires every missing/older/prerelease/wrong-primary case to report the current-harness admission reason. Whole-inventory no-effect assertions remain. This closes the false-evidence risk without changing production version policy. + +The corrective commit changes exactly the direct client gate, the two regression files and explanatory evidence. No additional production behavior was introduced in the correction. + +## Native contract and scope review + +Independently read retained official docs and source at exact Kimi Code2.3.1 commit `684bae9ea6a437f3bae98bba1dcb797a917d500b`, including `configLoader.ts`, `config-schema.ts` and `skillRoots.ts`, and checked recorded artifact provenance/help boundaries. Native MCP layers are user, nearest Git-root compatibility `.mcp.json`, then selected working-directory `.kimi-code/mcp.json`; the selected project layer wins same-name entries. Native project skills use the nearest upward `.git` root. The dedicated project codec correctly emits explicit stdio/http/sse transport fields. Legacy user plugin registration is user-only and remains separate. + +New installation/update/repair requires the selected primary clean absolute kimi/kimi.exe and exact normalized2.3.1 version. It does not borrow archived entrypoints, probe from adapters/SDK, manufacture a user profile or grant trust. Skill delivery refuses a missing/aliased/different nearest Git marker rather than silently writing an ancestor. Project MCP-only behavior remains scoped to the selected cwd. Lower-priority user/compatibility names produce an honest warning and retain original bytes. Effective extra skill roots, command/session selectors, trust and runtime loading remain unobserved. + +Project entry points bind exact scope/original user-context equality, native root, artifact leaf and `.uap/managed/clients/kimi/` path. Direct activation requires the delivered active package; only prestaging accepts an absent delivery locator. SelectedDelivery validation and exact owned object validation prevent package/receipt rebinding. Unknown native object kinds remain visible to refusal rather than being silently dropped; the generic managed package receipt remains core-owned. + +The stdio projection copies the trusted managed launcher, expands selected package/data/cwd/environment and binds original command arguments without executing them. Read-only active reuse re-renders the expected complete entry from the source envelope and selected data receipt, JSON-normalizes it and compares the actual immutable entry. Missing, relative or other-project data therefore requires a fresh projection with unchanged package/native/user/B inventories. + +## Lifecycle, ownership and preservation + +Skill/MCP writes use the reviewed shared component mutation and native configuration transaction kernel. Existing-member/disabled/foreign collisions, malformed documents, unsafe namespace paths, stale owned receipts, rebound artifacts and writer-lock failure are checked by meaningful composed tests. Exact recorded removal is independent of a current executable/version or still-present Git marker, consumes the recorded component authority and preserves foreign members and retained data. Fresh-session removal wording accurately distinguishes already-open native sessions. + +Project materialization returns prepared plus installation-verified; neither direct/composed activation nor generic activation attestation may upgrade this to loaded/active proof. No model, MCP process, native plugin registration, folder-trust write, autotrust switch or account operation is used. Existing Kimi user branches, plugin registry format and user SSE capability policy are retained. Only the source-qualified project codec/qualifier accepts SSE. Shared domain/codec/golden changes are additive to the reviewed d39 component baseline; no other client implementation is removed. New public tests all begin TestProject and match the existing cross-platform project-contract selector. + +## Evidence assessment and execution limitation + +Inspected public production CLI TLS Git lifecycle, exact installed-pin repair while source advances, fresh-state update/status, pending-journal recovery, A-only removal and user/B preservation, plus cold HTTPS ZIP export/import agreement. Inspected no-save production CLI and public SDK A/B stdio/data, restart, repair, version-independent removal and retained-data assertions. The injected host/security/launcher seams are explicitly inert; these tests compose real filesystem/state/acquisition/planning/staging/native mutation and never assert genuine native MCP initialization or tool/data execution. + +The author reports final authored public saved Git+cold archive PASS7.695s, imperative/no-save PASS0.705s, SDK PASS0.187s, providers PASS0.121s, existing user client/native codec/planner passes and both lint gates zero. At corrective95b the author independently reports focused2 PASS0.016s, complete project providers PASS0.139s and changed-line lint zero. These remain author-run results, not this reviewer's rerun. + +The reviewer independently attempted `go test -p 1 -count=1 -timeout=90s ./providers -run '^TestProjectKimi'` at95b with the parent-granted single Go slot, GOMAXPROCS2 and owned RAM TMPDIR/GOTMPDIR/cache. Compilation failed before test execution writing `$WORK/b125/_pkg_.a` with ENOSPC. No passing independent provider execution is claimed. The slot was explicitly released; no optional repeated builds or cache cleanup were performed. Independently compared all 6 authored public CLI/SDK Kimi lifecycle/fixture paths at7111008f and95b: every byte is unchanged from the prior independently observed R1 public passes (CLI8.388s, SDK0.149s). Final `git diff --check` passed. Source inspection and the exact closure regressions support this GO; hosted/normal-capacity checks must still provide execution evidence. + +Official genuine version/help observations remain bounded pre-code evidence. Native Kimi discovery, workspace trust, invocation, MCP initialization/data and fresh-session removal are pending, as are any broader Desktop/release or all-client claims. diff --git a/docs/plans/project-install-e2e/kimi-implementation-evidence.md b/docs/plans/project-install-e2e/kimi-implementation-evidence.md new file mode 100644 index 000000000..b3acf131a --- /dev/null +++ b/docs/plans/project-install-e2e/kimi-implementation-evidence.md @@ -0,0 +1,198 @@ +# Kimi Code project component implementation evidence + +Implementation owner codex_claude, 2026-10-23. The original current-Kimi client +plan and its two independent corrected GO reviews are retained. This checkpoint +records pre-code official artifact/help and exact-tag source evidence supplied +by root; it is not native project/runtime qualification. + +## Pinned artifact and genuine help + +- Product: current MoonshotAI Kimi Code **2.3.1**, not archived kimi-cli. +- Official release: https://github.com/MoonshotAI/kimi-code/releases/tag/%40moonshot-ai%2Fkimi-code%402.3.1 +- Exact official source tag commit: `684bae9ea6a437f3bae98bba1dcb797a917d500b`. +- Archive: https://github.com/MoonshotAI/kimi-code/releases/download/%40moonshot-ai/kimi-code%402.3.1/kimi-code-linux-x64.tar.gz +- Vendor GitHub asset and measured archive SHA256: + `0dbc7d771ce4d761acc150ade72325bcd8fc51973d481f7040295f4cf062fdbe`; + 65,922,255 compressed bytes, exactly one regular `kimi` member. +- Binary: 192,285,888 bytes, SHA256 + `781ec1c24e5e79a01aaaa31ad859e8818ae11ffe792e93645cdc0c068a264ba0`. +- Genuine bounded `--version`, `--help`, `doctor --help`, `acp --help` all exited0 + in root's fresh HOME/XDG/project, networkless `bwrap --unshare-all` with + read-only verified binary/system libraries and empty proc. Version stdout is + exactly `2.3.1`. No account, trust, model, MCP or plugin mutation occurred. +- Logs/provenance remain under `tools/native-probes/kimi/2.3.1` in the task + evidence area. The hash-verified extracted copy was reclaimed; do not download + it again merely to reproduce help. No standalone read-only MCP/skill listing + exists in the captured help, so the adapter stays prepared after file delivery. + +## Exact-tag documentation and implementation + +All following sources are at commit `684bae9ea6a437f3bae98bba1dcb797a917d500b` +in https://github.com/MoonshotAI/kimi-code : + +| Exact source path | Inspected contract | +| --- | --- | +| `docs/en/customization/mcp.md` | Working-directory `.kimi-code/mcp.json`; user `.kimi-code` or KIMI_CODE_HOME; project overrides user; workspace trust required; open-session removal differs from fresh-session absence | +| `docs/en/customization/skills.md` | `.kimi-code/skills` plus `.agents/skills`; nearest `.git` project root; frontmatter name/description; extra directories can affect effective selection | +| `docs/en/customization/plugins.md` | Plugin registry remains user-only; project components must not register a project plugin there | +| `packages/agent-core-v2/src/app/mcpConfig/configLoader.ts` | Merge user mcp.json, nearest Git worktree root `.mcp.json`, then selected cwd `.kimi-code/mcp.json`; project can be excluded when untrusted | +| `packages/agent-core-v2/src/mcpCore/config-schema.ts` | Explicit `transport:stdio/http/sse`; inference from command/url when absent; env/cwd or headers; enabled/deferred/tools/timeout policy | +| `packages/agent-core-v2/src/features/skill/catalog/skillRoots.ts` | Upward nearest `.git` existence controls project skills; branded `.kimi-code` and generic `.agents` roots; realpath normalization | +| `packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts` | Root-file skill discovery and implicit context require conservative status | +| `packages/agent-core-v2/src/workspace/workspaceMcpConfig/workspaceMcpConfigService.ts` | Workspace trust gates loading and configuration changes | +| `packages/agent-core-v2/src/workspace/workspaceTrust/workspaceTrustService.ts` | Trust is independent persisted workspace authority; installer must never grant it | + +Root's provenance records each retrieved blob ID and SHA256; the reviewer can +reproduce by exact commit rather than a mutable website. These are static source +observations, not a successful native list/connection. + +## Narrow implementation decisions + +Deliver exact project skills and MCP entries through reviewed +`shared.ProjectComponents` and the existing transaction/native-config kernel. +Native root is `/.kimi-code`; managed immutable package remains +`/.uap/managed/clients/kimi/`. Preserve detected original +user root, user plugin layout/registry/lifecycle, and KIMI_CODE_HOME. Do not +emit a project plugin manifest or invoke user registration. + +Only the current exact observed2.3.1 `kimi`/`kimi.exe` executable/version pair +qualifies new project install/update/repair. Use the separately reviewed shared +optional ClientVersion propagation once its frozen implementation has two GO; +no new probe belongs in Kimi's adapter or SDK Discover. Layout/reservation and +exact receipt removal/recovery remain independent of a running current binary. + +Use a narrow project-only native codec for explicit stdio/http/sse transport +keys, with no implicit user capability expansion. Stdio uses the existing +trusted managed launcher and validates immutable package/data/cwd/environment +against the selected receipts during active reuse. Preserve unsupported +components as unsupported; rendered command skills follow existing projection. + +A selected skill project must be its nearest `.git` ancestor (a regular gitfile +or actual directory, never a symlink). Reject a different ancestor rather than +silently installing skills where Kimi will not scan. Without any `.git`, fail +closed for the automatic skill lane until exact fallback semantics are further +qualified. No ancestor file is mutated. User/root compatibility MCP sources +are lower priority but remain visible as warnings/collision context; disabled +or foreign selected entries cannot be adopted or enabled. Effective extra skill +roots, aliases, session flags, trust, loaded MCP and authentication are unobserved. + +Configuration materialization returns prepared + installation-verified; it +never claims native loaded/runtime-active. Activation attestation is refused +before the generic composed-provider branch, including direct activation. +No KIMI_CODE_TRUST_WORKSPACE, --auto, --yolo, archived entrypoint or model prompt +is used to manufacture evidence. Exact removal only consumes recorded component +receipts and leaves user/B/foreign settings, retained data and package store. + +## Verification checkpoint + +Pending implementation: production provider boundary and public CLI/SDK +lifecycle tests for selected A versus B/user, exact roots/artifacts/receipts, +wrong data/rebound contexts, foreign/disabled collisions, nearest-Git mismatch, +current/missing/wrong version evidence, prepared/attestation refusal, rollback, +locked no-op/update/exact repair/recovery/removal and cold portable restore. +Two independent final implementation reviews are required before publication. +Genuine native project discovery/trust/session/restart/MCP/data evidence remains +pending; passing fixture composition does not change that boundary. + +## First implementation checkpoint (not review/publication-ready) + +The reviewed shared host contract64c7ee6d was incorporated exactly as3e072c99; +its two independent code GO reports are preserved alongside this evidence. +Project adapter now has scoped current-version/nearest-Git admission, dedicated +stdio/http/sse codec, exact package/artifact/native-root and original-user-root +fences, selected immutable stdio data/launcher validation, project-only SSE +qualification, component receipts and the reviewed native transaction helper. +User plugin behavior is unchanged. Project state stays prepared/installed and +attestation cannot override unobserved trust/discovery. + +Bounded checks before this checkpoint: full existing Kimi client suite +PASS0.014s; full nativeconfig suitePASS3.331s; new composed project provider +suitePASS0.069s, then with current-harness/nearest-Git and version-independent +removal casesPASS0.101s. Public saved TLS Git lifecycle reached the first real +installation but could not copy the trusted launcher because task temporary +storage returned ENOSPC; this is a resource failure, not a passed public E2E. +Root is coordinating headroom before rerun. Source files were then mechanically +split by responsibility without broad recompilation. The final public SDK +stdio lifecycle, public CLI successful run, lint and two final code reviews +remain pending; this commit is a recoverable implementation checkpoint only. + +## Final authored implementation verification checkpoint + +After root restored only task-owned reproducible cache headroom, the real public +saved TLS Git lifecycle passed: A/B installation, locked no-op, exact installed- +pin repair after immutable/native drift while the source advanced, fresh-state +update/status, bounded pending-journal recovery, exact removal/repeat refusal, +retained data and unchanged user/B. A separate cold HTTPS ZIP export/import +restored the exact agreement and source identity into a fresh content cache. +The existing no-save local route also passed through production CLI composition +with no environment receipt, selected current host metadata, prepared/installed +project binding and exact removal preserving user/B. No fake native executable +was run; detector/security/trusted-launcher bytes are explicit inert test seams. + +Public SDK tests passed A/B distinct selected stdio/data receipts, exact +launcher/args/environment/data cwd projection, retained data markers, locked +no-op, caller-supplied exact-pin repair after source advance, fresh Engine +restart/update/inspection and exact removal without current version/executable. +The launcher is copied trusted inert bytes and never executed in this fixture; +this does not establish a genuine MCP initialize/tool/data workload. + +Additional provider coverage passes current/missing/older/prerelease/wrong +harness refusal, nearest-Git mismatch with unchanged inventories, version/Git- +independent owned removal, malformed/disabled/foreign/symlink/stale native +ownership, scope/artifact/delivery mismatch, immutable selected-data refusal, +project-only SSE with preserved user SSE policy, and same-name lower-priority +user/nearest-Git compatibility entries preserved byte-for-byte. Their presence +is exposed as a warning; configured trust/effective loading stays unobserved. + +Final bounded results: + +| Check | Result | +| --- | --- | +| Public saved Git + cold archive import | PASS7.695s | +| Public imperative local/no-save | PASS0.705s | +| Public SDK project stdio/data lifecycle | PASS0.187s | +| Project composed provider boundaries/lifecycle | PASS0.121s | +| Existing Kimi user adapter suite | PASS0.014s | +| Full nativeconfig suite including Kimi codec | PASS3.331s | +| Compatibility planner/goldens | PASS0.013s | +| Exact changed-code lint2.13.2 | Zero issues | +| Whole changed-file size/complexity/architecture lint2.13.2 | Zero issues | + +An overly broad initial test expression also selected an existing Codex/Claude +user sibling-compatibility test and hit the known duplicate inherited proxy +environment rejection; that same exact existing test passed0.291s after removing +only ambient upper/lowercase proxy variables for its child environment. It is +not recorded as a Kimi failure or hidden as native qualification. A new test +fixture initially omitted the required planner path policy; it was corrected +without changing production admission. + +The final implementation is frozen for two independent code reviews. Genuine +Kimi project discovery, folder trust, skill invocation, MCP initialization/data +and fresh-session removal remain pending. Neither these tests nor the genuine +help-only artifact justify active/attested status or all-client/release claims. + +## Independent R2 corrections + +After the byte-preserving component-base merge at `83c85702`, independent R2 +identified two narrow omissions. The direct project profile validator now +requires the literal Kimi client ID before accepting an otherwise exact native +binding. A copied Codex-tagged binding refuses with unchanged inventories; the +higher-level inspector already rejected this mismatch. + +The dedicated harness-version admission test also lacked the required planner +path policy. It now supplies that policy, positively proves the selected current +2.3.1 harness reaches `PlanReady`, and requires each missing/old/prerelease/wrong +executable case to report the current-harness admission reason. This prevents +an unrelated earlier planner failure from satisfying the negative assertions. +Both focused regressions passed0.016s, and the complete project Kimi provider +selection passed0.139s. Production version, scope, trust and prepared-status +contracts are unchanged. + +Both independent final implementation reviews are GO at corrective source +`95b5f34cc9fb7796e79e0a76e3b853d8a8ce5386`, after the component-base merge and +both narrow R2 corrections. Their full reports preserve earlier independent +composed passes and separately identify final independent retries that failed +before assertions from actual ENOSPC. Those retries are not reported as passes; +the actual corrected author provider and lint results above remain separately +attributed. Parent requested no redundant compilation during the shared resource +shortage. No native Kimi project or runtime qualification is added by approval. diff --git a/docs/plans/project-install-e2e/kiro-code-review-r1.md b/docs/plans/project-install-e2e/kiro-code-review-r1.md new file mode 100644 index 000000000..6391725b9 --- /dev/null +++ b/docs/plans/project-install-e2e/kiro-code-review-r1.md @@ -0,0 +1,19 @@ +# Kiro project implementation — independent R1 + +Reviewed initial `9eeadc1a47650d95c803ae043fa9411ce51ae21c`, scoped Kiro diff `426a82b3..9eeadc1a`, and final correction `b3b10e7ac15b0c6d5049826b831c1f84d143513d` in `work/uap-project-kiro` on 2026-10-23. Reviewer did not author Kiro. **Final GO at b3b10e7a; the narrow scope-admission finding is closed.** + +## Finding + +`clients/kiro/scope.go:kiroProjectRoot` compares the original user configuration root only when `TargetContext.UserConfigRoot` is nonempty. A caller can therefore drop a known original user profile from the selected project context while retaining a nonempty detected `Client.ConfigRoot`; the scoped admission no longer verifies that the original authority was preserved. Require literal equality between the validated context's user root and detected config root (both empty remains valid), and extend the existing composed-provider mismatch table with omitted user context. This is a narrow project admission correction; user lifecycle should remain unchanged. + +Closure: `b3b10e7a` changes the project admission to exact equality and adds the omitted-user case to the existing refusal/unchanged-byte table. The user branch is unchanged. Independently reran `TestProjectKiroScopeRootAndBindingAuthorityRefuse`: **PASS 0.027s**. No other blocking finding remains. + +## Otherwise reviewed + +The implementation selects explicit project `.kiro` native paths and `.uap` package/data/state without redirecting the detected user profile. It reuses the existing exact JSONC/locked native transaction and verified skill swaps/rollback, checks exact project artifact and recorded binding, and validates kinds, IDs, duplicate receipts, portable source paths, native paths and aliases before mutation. Core-owned package receipts remain outside the native writer. Stdio selection binds the trusted launcher to absolute selected package/data/cwd and active reuse refuses a different data receipt without rewriting immutable bytes. + +Project add, prepare and verify-only remain prepared with installed-file evidence. The real provider preflight rejects all project activation attestations before its generic manual branch; no user ACP invocation, trust migration, account/model or MCP process is substituted for discovery. Genuine pinned Kiro version/help is kept separate from the project fixtures and does not qualify an active custom agent, engine or IDE. No hosted guard or native runner was changed. + +The public SDK uses the production engine/filesystem/state with inert unexecuted launcher bytes and checks A/B data separation, old-pin repair despite moving source, a fresh engine, retained data and exact removal. Public CLI tests use production scope composition, solver/store/installer/native transactions with genuine loopback TLS Git/ZIP, plus labelled detection/security seams. They exercise no-op/read views, repair/update, portable export/cold import and isolation without claiming native Kiro execution. + +Independent bounded verification at the frozen SHA: `go test -p=2 ./install/integrationctl/agentplugins/clients/kiro ./install/integrationctl/agentplugins/providers ./install/integrationctl/agentplugins/installer ./cli/cmd/agentplugins -run '^TestProjectKiro' -count=1 -timeout=3m` passed: client **0.009s**, providers **0.085s**, SDK **0.171s**, public CLI **5.387s**. No test/policy assertion was changed during this review. diff --git a/docs/plans/project-install-e2e/kiro-code-review-r2.md b/docs/plans/project-install-e2e/kiro-code-review-r2.md new file mode 100644 index 000000000..daa83ed3c --- /dev/null +++ b/docs/plans/project-install-e2e/kiro-code-review-r2.md @@ -0,0 +1,90 @@ +# Kiro project implementation independent review R2 + +Reviewer: codex_claude, 2026-10-23. Reviewed the cumulative Kiro adapter delta +`426a82b3..9eeadc1a47650d95c803ae043fa9411ce51ae21c` and the final correction +`b3b10e7ac15b0c6d5049826b831c1f84d143513d` in work/uap-project-kiro. +The reviewer did not author this implementation or edit that worktree. + +## Decision: GO at b3b10e7a + +No remaining implementation blocker found. The independent R1 omitted-user +context finding is closed: project admission now requires literal equality to +the detected original user ConfigRoot, including empty/nonempty mismatch. +The explicit omitted-user regression independently passes alongside wrong-user, +wrong native root/scope, copied-project and native-root alias refusals. Both +empty contexts retain their supported file-only behavior. This GO covers the +configured project lane and its production fixture composition; it is not a +native workspace/agent/runtime or release qualification. + +## Reviewed production boundaries + +- Project TargetContext selects exact .kiro native authority while retaining + the separate original user profile. Planning validates context before target + resolution, then requires the exact selected physical-artifact package under + .uap/managed/clients/kiro. Projection, inspection, lifecycle and binding reuse + revalidate their selected project/native/package roots. Native skill/config + paths remain .kiro/skills and .kiro/settings/mcp.json; package/state/data stay + in the project .uap namespace. No user-profile surrogate is introduced. +- Project native receipts retain the existing Kiro kind/ID format and exact + JSON object digest. The portable shared validator checks the normalized IDs, + exact native layout, clean absolute paths, source-relative paths, digest and + duplicate/unknown ownership. Only the core-managed package receipt is kept + outside the native writer; foreign skill/config documents are never owned + wholesale. Copied bindings must match scope/root/artifact/locator exactly. +- The existing Kiro JSONC/exact-file conditional transaction and exclusive + skill-swap/rollback implementation remain the native writer. Project routes + pass the explicit validated native root instead of calling legacy global + ConfigRoot wrappers. Missing writer/launcher and malformed/foreign/disabled + ownership refuse before effects. Visible-write failure rolls back the exact + original document and staged skill effects; committed cleanup degradation + retains exact receipts plus a conservative prepared result. +- Project MCP emits documented command/url configuration without portable + type tags or an assumed native cwd. Stdio receives the existing trusted + launcher, absolute package/data/cwd bindings and original source arguments. + Active reuse checks the selected current data receipt, launcher path, + environment and protocol root/cwd prefix without rewriting digest-covered + bytes. Empty/relative/different data authority requires projection refresh. +- Activation preflight runs before generic attestation admission, and direct + project activation repeats the guard. Project activation/verify-only remain + prepared plus installed-file verification. Attestation cannot turn native + policy, skill discovery or MCP initialization into an observed success. + Existing user CLI/ACP behavior and engine selection are preserved; no project + process invokes global ACP, migrates trust/account state or changes engine. +- Scoped domain policy selects the existing native-config effect/recovery + lifecycle while user CLI-registry traits stay intact. The project preparation + branch is explicit and does not loosen user preparation admission. + +## Public composition and isolation evidence + +The public installer SDK fixture uses real loader/planner/stager/state/native +transactions with inert launcher bytes. A/B have separate stdio/data locators; +unchanged update is a no-op, repair consumes the installed pinned bytes despite +moved source, a fresh installer instance performs update/status/remove, and +retained data markers, global configuration and B ownership remain unchanged. + +The production CLI fixture exercises real loopback TLS Git and ZIP acquisition, +saved A/B installation, locked no-op sync, read-only views, missing-skill repair, +update retaining source/binding authority, exact portable export/cold import +and owned remove. It preserves user/B native bytes/state, foreign settings, +skills and hooks. TestProjectKiro naming is selected by the existing hosted +project-contract gate. Security/detection boundaries and native runtime seams +are explicitly inert; they are not relabeled as genuine Kiro proof. + +## Independent bounded execution at corrected checkpoint + +Go 1.26.10, GOMAXPROCS=3, -p=2/-count=1/-timeout=3m, selection +`^Test(ProjectKiro|Kiro)`: + +- Kiro client project plus existing TestKiro contracts: **PASS 1.440s**. +- Composed providers: **PASS 0.084s**. +- Public installer SDK stdio/data/repair/restart/remove: **PASS 0.154s**. +- Production CLI Git/ZIP saved source lifecycle: **PASS 5.301s**. +- The same selection found no domain tests; no fresh domain execution is + claimed by that command. + +No actual Kiro binary, configured MCP process, model, login, account mutation, +auto-trust or hosted native suite ran in this review. Pinned successful help, +file receipts and these fixtures do not qualify project discovery, IDE/mobile, +active custom-agent includeMcpJson/resources policy, native initialization/tool +use, native restart or release readiness. Those remain separate genuine hosted +observations under the unchanged disposable guard. diff --git a/docs/plans/project-install-e2e/kiro-implementation-evidence.md b/docs/plans/project-install-e2e/kiro-implementation-evidence.md new file mode 100644 index 000000000..a3d8ac093 --- /dev/null +++ b/docs/plans/project-install-e2e/kiro-implementation-evidence.md @@ -0,0 +1,181 @@ +# Kiro project scope: pre-code research continuation + +Prepared by cursor_clients, 2026-10-23. This continues the Kiro lane already +approved in both independent reviews of `client-plans.md`; it does not replace +or expand that plan. No Kiro implementation has started at this checkpoint. + +## Current official evidence + +Checked current official pages before coding: + +- https://kiro.dev/docs/mcp/configuration/ +- https://kiro.dev/docs/configuration/ +- https://kiro.dev/docs/custom-agents/configuration-reference/ +- https://kiro.dev/docs/skills/ +- https://kiro.dev/docs/cli/ +- https://kiro.dev/changelog/cli/ + +The MCP reference describes nested `mcpServers`, local command/args/env and +remote URL/headers, with workspace `.kiro/settings/mcp.json`. Whole workspace +entries override global entries, and custom-agent entries take precedence over +workspace entries. Disabled members and permissions remain native policy. +Custom agents use `includeMcpJson` to include those configuration files. UAP +must preserve these settings and cannot infer the active custom agent. + +The skills reference confirms `.kiro/skills//SKILL.md`, global skills +under the user's Kiro directory, and custom-agent resources controlling skill +availability. Project configuration alone does not prove native selection. + +The CLI changelog lists 2.33.1 on October 21. Its October 20 predecessor adds +interactive V3 workspace trust before folder configuration loads, recording +trust per workspace root under the user's `workspace-roots` directory. No +trust/default-engine/config migration is authorized or needed for file +installation. Native CLI observation must state its actual engine/version and +must not be presented as IDE/mobile/web/Powers qualification. + +## Official package pin and measured version/help + +Read the official installer script without executing it: +https://cli.kiro.dev/install +Local snapshot: `research/kiro-cli-install.sh`. + +The script selects stable channel metadata from +https://prod.download.cli.kiro.dev/stable/latest/manifest.json +Local snapshot: `research/kiro-cli-stable-manifest.json`. + +The retrieved manifest reports version **2.33.1** and gives an exact Linux +x86_64 GNU headless archive: + +- URL: https://prod.download.cli.kiro.dev/stable/2.33.1/kirocli-x86_64-linux.tar.xz +- Vendor manifest SHA256: + `e798062c22adb3be53ea1845f9b309b83df017376860f8329293f3c2954177cb`. +- Vendor manifest length: 130,643,852 bytes. + +The archive was acquired from that exact versioned URL. Its measured length +and SHA256 match the vendor manifest. The complete official distribution was +inspected without running its installer: main CLI 42,879,904 bytes, companion +chat 205,977,448 bytes, and terminal 33,369,248 bytes, plus wrappers and metadata. +Main binary SHA256 is +`801e11f74e31d26f883f5f56cde96c006124c94ee8e063c58f45ad183f34f147`; +chat SHA256 is +`6c143853239d75ecbb51099ab1dc768c1a03707de6b9d5393fa3dacdf24bc036`; +terminal SHA256 is +`11b172eb92513bfda7524cb61b03042ca58b970680266388ca8c2bb2f3a45606`. +BUILD_VERSION is 2.33.1, build hash +`2d6b9103124a3b0dab1f4517a5c3400cd297ec36`. + +Eight genuine read-only probes exited zero with a fresh profile/project in a +networkless namespace using empty /proc: --version, --help, --help-all, +mcp --help, mcp list --help, mcp add --help, acp --help and agent --help. +Version output is exactly `kiro-cli 2.33.1`. MCP scope help names default, +workspace and global. ACP defaults to v2 and exposes the separate v3 engine; +UAP will not silently change it. No list/session/model/MCP/auth/trust operation +was run. Initial private-proc launcher refusals are recorded as launcher +failures, separately from those successful help/version probes. + +Provenance, archive and all logs remain under the task's +`tools/native-probes/kiro/2.33.1`. Reproducible extraction was reclaimed after +hash/help evidence capture; the original pinned archive is retained. These +version/help observations do not qualify project discovery, runtime, IDE or +any particular active custom-agent policy. + +## Existing code reuse and project boundary + +The current Kiro adapter already supplies exact native skill/MCP receipts, +JSONC-preserving MCP patching, locked ExactFile conditional replacement, +verified skill swaps and rollback. Project support should pass an explicit +validated project native root into those services, preserving the original +DetectedClient.ConfigRoot. It must not disguise the project as a user profile. + +The existing user automatic lane also performs ACP session verification. +Project file installation requires a separate conservative prepared outcome: +do not reuse user ACP success as project proof, invoke a user-only session by +accident, or allow generic activation attestation to upgrade discovery. A +project inspector must validate binding scope, selected native root, exact +object kinds/paths/digests and selected artifact before calling the existing +native mutation. Unknown ownership must fail closed; only the core-managed +package receipt belongs outside the native writer. + +The approved source scope remains project `.uap` for package/state/data plus +`.kiro/settings/mcp.json` and `.kiro/skills` for native files. User lifecycle, +legacy source and engine distinctions remain intact. Native helpers named +ActivateNative/DeactivateNative currently use request.Client.ConfigRoot; +project callers must select an explicit root instead of reaching those user +wrappers unchanged. Project prepared intent must remove the current user-only +assumption only on its branch. Active reprojection must read and validate the +bound package/data locators without rewriting immutable bytes. + +## Implementation checkpoint + +The original Kiro section of client-plans.md and its two independent GO reviews +remain the pre-code authority. This evidence checkpoint precedes production +changes. The Kiro leaf true-merges reviewed integration daa705ad before its own +changes. It adds an explicit project native root and read-only scope/receipt +validation, reuses Kiro's existing exact native transaction, and binds stdio to +the existing trusted launcher and selected package/data receipts. Project +activation remains prepared with installed-file verification; generic +activation attestation cannot upgrade it. No workflow guard or native runtime +proof claim is added by the file adapter. Separate independent implementation +reviews and composed source lifecycle checks follow the code checkpoint. + +## Implemented project lane and verification + +Project support is additive to the original user lane. The selected workspace +owns `.uap` package/state/data plus `.kiro/settings/mcp.json` and +`.kiro/skills/`. The detected global profile is retained independently. +Explicit project root/layout, selected artifact, binding scope/native root and +exact receipt identities/portable paths are validated before mutation; copied, +unknown, duplicate, nonclean or foreign ownership refuses. The existing Kiro +JSONC ExactFile transaction and skill swap/rollback service remain the only +native mutation mechanism. Existing global CLI/ACP behavior is unchanged. + +The project selection renders Kiro's documented command/url shape without the +portable transport type member. Stdio uses the existing trusted launcher to +establish cwd and receives absolute selected package/data locators. Active +reprojection validates launcher, environment and protocol package/data/cwd +arguments against the selected data receipt without changing immutable bytes. +Missing or different data authority requires projection refresh. Project +activation and verify-only both remain prepared with installed-file evidence; +activation attestation cannot turn that into native discovery. Prepare intent +uses only this explicit project branch and does not relax the user lane. + +Independent implementation review is pending at this code checkpoint. Bounded +author verification includes: + +- Existing Kiro user test selection: PASS 1.364s; compatibility goldens PASS + 0.009s. Only Kiro's declared project scope changes in the four capability + goldens; the other client entries remain intact. +- Production loader/planner/stager/provider fixtures: PASS 0.243s, covering + remote and stdio bindings, A/B/user isolation, repeats, update/removed MCP, + missing-skill repair, native root aliases, copied scope/root/bindings, + malformed and disabled foreign collisions, missing writer/launcher, + forged/duplicate/unknown receipts and activation-attestation refusal. +- Public installer SDK stdio lifecycle: PASS 0.251s. Separate A/B data receipts + and launcher roots, no-effect repeat update, repair from the installed pin + despite a moved source, fresh installer update, inspection and exact remove + retain data markers and leave user/B ownership unchanged. +- Project native fault cases verify visible-write error rollback restores + original bytes and removes staged skill effects; committed lock-cleanup + degradation retains exact receipts and the conservative prepared status. +- Production CLI TLS Git and ZIP source journeys: PASS 5.980s. Add both projects, + locked no-op sync, read-only views, repair, update identity preservation, + exact portable export and cold import, owned removal and scope isolation. + The test starts TestProjectKiro so the existing cross-platform project gate + selects it. Source HTTP/security boundaries are inert test seams; no native + Kiro, launcher, MCP, model or trust process is executed by these fixtures. + +No Kiro project native-discovery/runtime qualification is claimed. Genuine +pinned version/help is separate from configured-file and public-API evidence. +The existing hosted native guard is untouched, no account/trust migration is +performed, and CLI evidence cannot qualify the IDE. Native source qualification +must still observe the selected actual workspace/agent and interpret permissions, +includeMcpJson and resources rather than infer effective policy from these files. + +## Independent review correction: retained user context + +R1 identified that project root validation accepted an omitted user context +when the detected original profile was nonempty. The project lane now requires +exact equality, including empty/nonempty mismatches; both empty remains valid. +An omitted-user regression verifies refusal before native effects alongside +the existing wrong-user/scope/root/copied-project cases. This retains user +profile authority explicitly even though the file-only lane never mutates it. diff --git a/docs/plans/project-install-e2e/landing-browser-ci-synchronization.md b/docs/plans/project-install-e2e/landing-browser-ci-synchronization.md new file mode 100644 index 000000000..4016d56b6 --- /dev/null +++ b/docs/plans/project-install-e2e/landing-browser-ci-synchronization.md @@ -0,0 +1,46 @@ +# Landing browser CI: isolate viewport cases and await rendered setup + +2026-10-25. This narrow test amendment addresses the two genuine failures at +integration `c7bad1b0ee074c70e45070c706502c7f928b74b2`, tree +`be8bd8c0a93985a363cb851282fc23a9f87369d3`. It changes no landing product source, +registry/catalog/authentication behavior, workflow timeouts, retries or sleeps. + +## Exact observed evidence + +Landing run `40185973996`, job `121146717043`, reported 238 passing tests, two +failures and one existing skip. Artifact `12596570336` is 15,906,743 bytes, +SHA-256 `953a2a38dca97cc620802caeeb09e4e0722c904344ab1dabcde5fa93e3183f3c`. +The downloaded archive matches both size and digest. Its original bytes and +trace ZIPs remain unchanged. + +- `hero-layout.spec.ts:28` puts four full navigations and independent geometry + checks into one 30-second test. The trace completes the 1440, 1024 and 390 + viewport assertions, enters the 320 viewport at approximately 24.9 seconds, + and reaches the test deadline before its geometry checks. It records no + failed layout assertion for the completed cases. +- `landing.spec.ts:174` reads initially rendered sections without synchronizing + hydration/render-frame setup. The product intentionally applies + `scroll-reveal-ready` in one animation frame and `scroll-reveal-active` in the + next. The class expectation ends at trace time 361245.970 ms; the retained + after-snapshot at 361779.034 ms already contains both classes. This observation + does not prove why the browser scheduled that frame late or excuse the failed + assertion. It establishes the missing rendering precondition in this test. + +## Exact change + +1. Generate four independent named hero viewport tests by moving the existing + width loop outside the test declaration. Preserve the default 30-second + budget, setup, all assertions and optional screenshot name for every width. +2. In the existing below-fold reveal test, await the existing `hydrated(page)` + helper and then two real `requestAnimationFrame` callbacks before checking + the initial public classes and opacity. Preserve the five-second class + assertion and the one-second opacity completion after actual scrolling. +3. Keep all other tests and product code unchanged, including reduced-motion + and JavaScript-disabled coverage. Do not edit the separate catalog or auth + indicator PRs. + +Two independent plan rounds precede implementation. Review the frozen delta, +collect the affected browser tests and run them against the production artifact +when the local browser/dependencies are available. The next assembled hosted +Landing run remains the full gate; trace analysis and test collection alone do +not establish that the revised browser assertions pass. diff --git a/docs/plans/project-install-e2e/linux-ordinary-containment-proc-plan.md b/docs/plans/project-install-e2e/linux-ordinary-containment-proc-plan.md new file mode 100644 index 000000000..cf6ab21c1 --- /dev/null +++ b/docs/plans/project-install-e2e/linux-ordinary-containment-proc-plan.md @@ -0,0 +1,95 @@ +# Linux ordinary containment proc prerequisite plan — R2 + +## Observed failure and preserved evidence + +The unchanged Linux bootstrap test TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant failed persistently in the root serial run, after the integrated-core-f9 suite. One authorized exact reproduction used the ordinary production source at 4a1634f4906453eac0b87eddbcc54246226dcfdd, Go1.26.10 via run_go.py, -p=1, and syscall-only strace. It failed in1.440s with context cancellation plus the existing one-second sole-Wait cleanup deadline. No deadline or test assertion was modified, no vendor executable was invoked, and no second test reproduction was run. + +The raw full trace remains at /dev/shm/uap-project-runtime-5e2d9d9f7f5b/bootstrap-cancel-once.strace. A bounded literal syscall excerpt and the separately captured live-thread proc proof are retained with hashes under docs/evidence/process-containment/linux-proc-children-unavailable/. The excerpt is not represented as a full trace. + +The actual test supervisor killed the npm process group, observed/reaped that manager and exited. The setsid descendant1428797 was not killed by production supervision and survived until the test's stable-pidfd fallback killed it about1.02s later. This descendant held inherited output pipes; the parent process was already reaped, but exec.Cmd.Wait could not finish its I/O drain inside the existing bound. The diagnostic is truthful; increasing its timeout would mask the escaped descendant. + +An independent read-only probe found /proc//task//children and /proc/thread-self/children both absent with ENOENT while the same process's task directory and stat were valid. No child was launched by that proc proof. This establishes a missing proc interface on this environment; it does not identify a particular kernel configuration or claim a generic namespace defect. + +## Source cause and product boundary + +linuxDirectChildIdentities in install/integrationctl/adapters/process/osrunner_tree_linux.go reads every task's children file. It currently treats ENOENT/ESRCH as a disappearing-thread race without proving that the thread disappeared. When every live thread lacks that interface, an ordinary supervisor can infer an empty descendant tree. Three time-separated empty scans repeat the same blind observation and cannot repair this capability gap. This is an unsupported environment plus a product fail-open observation classification, rather than evidence that the existing cleanup deadline is too short. + +Do not fall back to numeric PID polling, /proc-wide ancestry sampling, process groups alone, a relaxed empty-tree rule, or a longer cleanup interval. Keep the current stable pidfd signaling and all existing cleanup assertions. + +## Bounded correction proposed for two independent plan rounds + +1. Add a narrow ordinary-containment launch preflight in the process adapter. On Linux it checks the required proc children interface for a positively live current thread, before wrapping/starting a supervisor, and the supervisor repeats this check before starting the requested command. Lock the current OS thread for this short self check so its own identity cannot disappear through Go thread migration. Missing children for that known-live thread produces a distinguishable unsupported-prerequisite error. Other read/identity errors remain failures; they are not silently skipped. The capability query certifies this prerequisite, not successful future descendant shutdown. +2. Expose the same process-inert prerequisite query through a small OS method so Linux bootstrap runtime tests can check the actual production preflight before launching a descendant. Non-Linux ordinary behavior remains unchanged; no other platform capability is newly asserted. Prefer an explicit unsupported error sentinel/type over matching diagnostic text. Do not change public command/result schemas or lifecycle policy. +3. During real descendant scans retain legitimate disappearing-thread handling only after a positive identity check: capture/revalidate the relevant task identity and accept missing children only when that exact task is proven gone/exiting, or belongs to a different reused identity. If the same live task still exists, missing children is an observation error and cannot contribute an empty snapshot. Unknown/permission/read errors also fail closed. Use a tiny private dependency seam for inert read/identity tests; no production environment switch or fault knob. +4. The bootstrap containment test may skip only on the exact distinguishable unsupported preflight before any manager/descendant launch. Other errors fail. Keep every existing cancellation, stable-identity, setsid, captured staging, deadline and sole-reaper assertion unchanged on supported hosts. Record any unsupported local run as not qualified, not a containment pass. Existing genuine hosted Linux containment must still qualify the runtime behavior. +5. Keep bootstrap manager source/staging behavior and custom inert Runner tests unchanged. If inspection reveals additional real-process tests in the affected packages need the same prerequisite, enumerate them for reviewers first; do not add blanket package skips or reinterpret unrelated execution errors. + +## Mandatory regressions and finite validation + +- Inert injected missing children file for a positively live same-identity task refuses; no successful empty-tree result and no command launch callback is reached. +- A known disappearing task can still be omitted without an error; a reused task identity cannot authorize signaling of its replacement. Unknown/read/permission errors are retained. +- Unsupported self-preflight is observed before requested command execution; the fixture's command marker remains absent. A supported self interface with an empty child list permits the ordinary launch path. +- Existing descendant traversal identity, quiescent-empty, stable pidfd, bounded reaper and cleanup causality tests remain unchanged and pass where their declared primitive is available. Do not run a broad suite for this correction. +- Exact bootstrap runtime test on this environment must report explicit pre-start unsupported capability (if still missing), with no manager/setsid artifacts. That result is not a substitute for supported-host cancellation containment evidence. +- Two independent R1 and R2 plan reviews must precede production edits, then two frozen-source code reviews must precede publication. Root decides the bounded implementation after review. No production source has been edited for this plan. + +## R2 precision and affected surface inventory + +R1 exact ec1c179b received independent architecture GOs from Root and apm_vercel_scope. Both required this explicit impact inventory and precise unsupported classification before code. Root additionally required bounded proc reads and strict task identity parsing. + +All proc task/stat/children reads introduced or reused by the new prerequisite/absence classifier must have finite byte limits and strict parsing of PID/TID/starttime/state/parent fields. Use a 4KiB stat cap and 64KiB children cap; read at most that cap plus one byte, refuse overflow/trailing malformed content and unknown identity errors, and keep the existing traversal process/deadline bounds. Use per-call small read/identity operations for inert injection, never mutable package globals. The unsupported sentinel is reserved for ENOENT of the required children interface while BEFORE and AFTER observations prove the same live task; permission errors, malformed identity, unknown read errors and a changed task are failures or explicit race outcomes, not unsupported skip authority. + +The ordinary wrapper fronts OS.Run, RunWithTreeExitGrace and RunWithDescendantExitGrace. On this host all of those production commands must now refuse before requested-child launch, including ordinary Git acquisition, scanners, client version/list/probe calls, legacy integrations and dependency bootstrap. This is a deliberate fail-closed unsupported-host contract. It is not a claim that only one bootstrap test is affected. The strict duplex/cgroup path remains unchanged. + +The first implementation will gate only these named real-process tests on the exact production prerequisite before any child launch (helper-only child branches must retain their existing behavior): + +| File | Exact tests needing ordinary prerequisite | +| --- | --- | +| bootstrap/process_linux_test.go | TestDefaultRunnerKeepsCapturedStagingAndContainsSetsidDescendant; TestDefaultRunnerExecutesOnlyLockedCommandWithSanitizedEnvironment; TestDefaultRunnerCancellationStopsDescendants | +| process/osrunner_test.go | TestOSRunnerPreservesCommandOutputAndExitCode; TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess; TestOSRunnerBoundsStdoutAndReturnsLimitError; TestOSRunnerStdoutLimitStopsProducerBeforeRemainingOutput; TestOSRunnerStdoutLimitPreservesNonzeroExit; TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands; TestOSRunnerBoundsStderrWhileRetainingDiagnosticEdges | +| process/osrunner_tree_linux_test.go | TestOSRunnerSurvivesRemovedOwnExecutable; TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup; TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable; TestOrdinarySupervisorDescriptorsAreClosedBeforeCommandExec; TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit; TestOSRunnerRapidShellCommandsWithShortLivedChildrenPreserveNaturalExit; TestOSRunnerCleansSameProcessGroupMemberAfterLeaderNaturalExit; TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit; TestOSRunnerDescendantGraceStillRejectsForcedCleanup; TestOSRunnerPreservesDiagnosticAndStatusWhileCleaningNaturalExitGroup; TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession; TestOSRunnerCancellationContainsImmediateSetsidDescendant | + +Existing requireDuplexCapability gates are not weakened or repurposed; a dual prerequisite test keeps both. Inert tests of injected traversal, quiescent-boundary counting, stable identities, output buffers and sole-reaper behavior remain unconditional. A named runtime test that reports unsupported is not a runtime containment pass. + +Production composition/caller inventory traced read-only: cli/cmd/agentplugins/compose.go injects OS into client discovery, lifecycle and scanner boundaries; clients/shared/native_registry.go uses tree grace; clients/claude/probe.go uses descendant grace; agentplugins/adapters/sourceacquisition/{acquirer,git_transport}.go directly use ordinary/tree-grace OS for Git; integrationctl_service.go and adapters/{source,claude,gemini} retain ordinary legacy defaults; authoring/bootstrap/bootstrap.go invokes OS.Run. Existing external source-acquisition, scanner and client probe integration tests may therefore refuse on this host too. No broad package skip or fallback runner will be added to make those tests appear qualified; supported hosted CI must exercise them. Any additional per-test prerequisite edits require their exact test names and review before expanding this bounded list. + +A successful self prerequisite query proves availability of the bounded proc interface only. Actual supported-host containment qualification still requires the unchanged real cancellation/setsid/cleanup assertions. This environment's unsupported result will remain explicitly distinct from those hosted qualifications. + +R2 exact 09e24c58 independently received PLAN GO from Root and apm_vercel_scope before production edits. Root approved explicit numeric cap supplement (stat 4KiB+1; children 64KiB+1; existing 1000-process traversal/deadline). These are prerequisite bounds, not runtime qualification. No bootstrap/process production source has yet been edited. + +## Implemented candidate and bounded validation + +The candidate now performs the same process-inert prerequisite in the ordinary parent constructor and again in its isolated supervisor before starting the requested command. The public OS.OrdinaryPreflight query reports that prerequisite only. Runtime task scans in both the supervisor traversal and passive parent tracker reuse the same per-call observation helper. A missing interface for a still-live task has a distinct unsupported sentinel and deliberately does not wrap ENOENT: existing ancestor traversal must not misread it as a disappeared process. Missing stat inside a still-present task and changed/malformed/permission/unknown observations fail closed. + +Concrete bounds are stat 4KiB plus one, children 64KiB plus one, and task-directory enumeration 1000 entries plus one, reusing the existing traversal count. A process exceeding that task enumeration budget produces an observation error; it cannot contribute an empty-tree proof. Task names and PID/starttime/state fields are parsed strictly. Operations are injected only per call in private inert test seams; there is no environment override or mutable fault global. Strict duplex cgroup handling, existing cleanup/reap deadlines and supported-host containment assertions remain unchanged. + +Focused local Go1.26.10 validation with -p=1: + +- Process inert observation/cap/identity/prelaunch tests plus existing traversal/quiescent-boundary/sole-reaper checks: PASS0.021s. Covered 10 disappearance/error subcases, exact/over byte and directory bounds, invalid identities, actual unsupported OS.Run refusal with command marker absent, and injected available empty interface retaining ordinary wrapping without starting a child. +- The exact22 reviewed real-process tests reported explicit pre-launch unsupported capability and SKIPPED on this host (process0.004s; bootstrap0.011s). Their raw log is retained. This result is not runtime containment qualification and no manager/setsid child was launched. +- Existing custom inert Runner captured-staging regression PASS0.020s. Custom bootstrap runners keep their existing contract. + +Raw selected local test logs and member hashes are saved under docs/evidence/process-containment/linux-proc-children-unavailable/. The original real failure trace/proc proof remains preserved. No vendor, native client, MCP server, model, authentication, trust change or broad suite ran for this candidate. Supported hosted Linux runtime qualification is still required. The source must receive two independent frozen-source code reviews before publication. + +### Final review correction and validation + +Root's review of candidate4a1b536e required the new ordinary prerequisite to run after helper-only exit/return branches. The final delta moves only that added query in14 helper-bearing tests; original helper behavior and existing duplex-query order stay intact. Other8 ordinary-query guards remain at test start. As a result, final local runtime accounting is16 ordinary-prerequisite skips plus6 existing duplex-prerequisite skips,22 unqualified runtime tests in total. No test assertion, timeout, skip predicate or cgroup rule was weakened. + +The new helper's three deliberate non-errno wrapping boundaries render the original errno as message text; the unsupported sentinel remains the sole wrapped cause. This retains the required distinction from disappeared ancestors and satisfies errorlint. State membership, integer range and constructor-only CommandContext corrections preserve behavior. + +Final combined focused validation after those corrections: process PASS0.037s (inert assertions passed; runtime skips excluded from qualification), bootstrap PASS0.025s (custom Runner assertion passed;3runtime tests unsupported). The final raw combined log is retained separately from earlier candidate logs. Pinned golangci-lint2.13.2 changed-source check over only process+bootstrap, concurrency1/GOMEMLIMIT512MiB/90s: PASS0issues. No broad module lint or vendor run was performed. + +## Final independent code review receipts + +Both reviewers approved exact final source932ed0f9fc86d5bde6b2c8c08f91354be4afce63 (treef7f9c53093ffd88f873ace86be7405ba0aab7913) before publication. + +- **Root (`/root`) — final CODE GO.** Reviewed the full4a1b implementation and final4-file correction. All14 added guards follow helper-only exits/returns and precede parent child launches; existing duplex order is preserved. Explicit errno message rendering keeps missing-live-task failures distinct from ENOENT ancestry disappearance. IndexByte, integer range and constructor-only CommandContext changes preserve behavior. Accepted the inert local passes and22 explicit unqualified runtime skips; supported hosted Linux runtime qualification remains mandatory. +- **APM (`/root/apm_vercel_scope`) — final independent CODE GO.** Reviewed the full4a1b production/test delta and final932 correction. Confirmed helper branches and existing duplex order are intact, bounded task observations fail closed, missing-live children produce the exact unsupported sentinel and other observation errors cannot provide empty-tree proof. Accepted the equivalent lint corrections and truthful final SKIP provenance. Performed source review only during this final review; no new build, vendor or native execution was claimed. + +The separate Claude hosted success run40139625617 is preserved under docs/evidence/project-scope-native/claude-2.1.263-prepared-success-40139625617/. It qualifies the committed earlier Claude/grouped Prepared fix, not these later Linux containment changes. + +## Subsequent genuine hosted bootstrap runtime subset + +Existing Authoring native offline run40143431447, Linux amd64 job121019719395, ran exact published source1bc5b898d8085e707fb405ed0c8f84ca2454157f and succeeded. The original archive and raw JSONL prove all three reviewed real bootstrap runtime tests ran and passed without bootstrap skips: captured staging plus detached setsid containment0.08s; locked command plus sanitized environment0.07s; descendant cancellation0.04s. Full bootstrap package passed0.318s. Artifact12579492503 (109006bytes, SHA256078da3d0930e82647a0ed78139f423932621edc6eff5eaad2a5ba0036af6795e) was independently checked for exact length/hash/CRC. All104 original bootstrap-package JSONL lines and original metadata are preserved at docs/evidence/process-containment/linux-proc-children-unavailable/hosted-bootstrap-40143431447/ with full-source/member hashes and an explicit byte-line extraction predicate. + +This closes the supported-host qualification of those three bootstrap runtime tests only. The workflow package inventory does not include the separate nineteen ordinary process adapter tests. Their current-source full qualification remains assigned to the unchanged Required/test main-base lane invoking make test-required, whose unfiltered ./cli/... and ./install/integrationctl/... commands include both packages. Local unsupported prerequisite skips remain unqualified. No workflow, selector, assertion or retry was added; no vendor ran locally while preserving evidence. diff --git a/docs/plans/project-install-e2e/local-git-cleanup-reproduction.md b/docs/plans/project-install-e2e/local-git-cleanup-reproduction.md new file mode 100644 index 000000000..fc78599ce --- /dev/null +++ b/docs/plans/project-install-e2e/local-git-cleanup-reproduction.md @@ -0,0 +1,127 @@ +# One bounded local-Git cleanup observation + +On exact candidate `e81e9daf2fbce3acf36aee33eb954a68fcf18acc`, terminal Linux +run `40194870304`, job `121173439610`, fails the dependency reconciliation +`missing_edge` fixture. Its existing fixed diagnostic positively reports +`phase=fetch class=ordinary_supervisor_descendant_cleanup exit_status=unavailable`. +Core's separate normalization fixture fails before normalization without a +more specific label. Full Coverage passes. These observations do not identify +the exact cleanup cause or establish that every acquisition failure is shared. + +The terminal resolver takes `acquireDependencyGit`'s `DirectAcquirer` branch: +`dependencyTestSource.AcquireGit` delegates to `Acquirer.AcquireGit`, which passes +`native=true` into `acquireGitTransport`. GitHub shorthand and the local +`URLForRepo` mapping do not change that branch. Its observed error is explicitly +`acquire direct Git source`. The separate Core `AcquireGitHub` path adds +`--filter=blob:none`; this experiment targets the positively classified terminal +fetch without that flag and does not pretend to reproduce both call paths. + +The finite origin-category amendment is separate. This test-only slice observes +the original process error at an entirely controlled local-Git boundary, before +the public acquisition layer intentionally sanitizes it. A passing attempt is +not a fix or an explanation for the historical failure. + +## Fixed experiment + +- Add one Linux-only process-package test, explicitly opt-in through + `AGENTPLUGINS_GIT_CLEANUP_DIAGNOSTIC=1`. Normal suites report this diagnostic + as skipped, not as runtime qualification. In the opted-in test, the real + `OS.OrdinaryPreflight` must succeed; unavailable containment fails the test. + Do not substitute a runner, proc source, process group or native host claim. +- Create a private temporary HOME, source repository and work directories. + The child environment is an explicit allowlist: the host PATH for locating + Git, private HOME/XDG/TMP paths, disabled global/system Git configuration, + noninteractive/no-credential settings and constant inert author identity. + No original user profile, ambient Git/SSH options, network endpoint, token, + SSH agent, package executable or vendor client enters the experiment. +- Run every Git command, including setup, with the ordinary production + `OS.RunWithTreeExitGrace` path and its existing five-second natural-exit grace. + Disable hooks, fsmonitor, automatic maintenance, GC and recursive submodules. + Permit only the local file protocol. Setup creates one small, fixed local + repository and captures its immutable commit; no source text enters a shell. +- Execute exactly 512 serial iterations, each initializing a fresh destination + with the existing acquisition remote/settings and fetching that same immutable + local commit. Match the positively observed native fetch graph: `fetch --quiet + --depth=1 --no-tags --no-recurse-submodules origin ` after + `-C `; verify `FETCH_HEAD^{commit}` afterward. Each + iteration must succeed naturally and preserve the expected revision. Stop at + the first failure; + this is a fixed process-race experiment, not retrying a failed command. + Bound each command context to 15 seconds, the complete child context to five + minutes and the test to seven. Context cancellation still uses unchanged + production cleanup; every process failure is a failed observation. +- On failure emit fixed phase/iteration/class/status metadata. Only for the + positively identified ordinary descendant-cleanup class from this controlled + OS invocation, emit at most 4 KiB of its original process error. Its command, + repository, files and complete child environment are fixture-owned. Do not + print stdout, stderr, arguments, environment or general acquisition errors. + All existing production and generic fixture loggers retain their prohibition + on reading or formatting arbitrary error text. Success emits a single fixed + completion count; no per-iteration output or additional artifacts. + Admission uses the existing closed classifier's generic cleanup label or its + recognized fixed reason suffixes; no arbitrary error text establishes a class. + +## Publication and observation + +Change only the new test, this plan and the existing temporary +`project-git-diagnostic.yml` workflow. Retain the same Ubuntu runner, Go pin, +read-only permissions and 15-minute job budget. Replace the already completed +prior diagnostic selections with one exact new test invocation, `-count=1`, +explicit opt-in and a seven-minute test deadline. Do not add retries, a matrix, +concurrency or a repeated full-suite invocation. + +Publish reviewed plan/code first on the separate reproduction branch, which +does not trigger the focused workflow. After frozen independent code reviews, +advance the existing focused branch once to the reviewed combined source and +record its exact SHA, tree, runner, test count and result. Inspect the actual +named test result; a missing, skipped, canceled or timed-out observation is not +qualified. No local runtime execution is attempted on the already unsupported +executor. Inert compile and configured affected-package/workflow checks are +sufficient before the hosted observation. + +Both independent plan rounds precede implementation. Both independent frozen +implementation reviews precede the one hosted observation. Any behavior change +based on its result requires its own evidence and reviewed plan. Existing full +Required/Core/Coverage/native gates remain mandatory for the final candidate. + +## Runner-provider parity amendment + +The first observation completed successfully on source +`14c35e9d35fcd976b5d74bcf4c02ff3986073b5e`, tree +`cfec35f7921e4547e2e6724ddbc74080a53f5619`: run `40198070979`, job +`121183139474`, all 512 iterations without a skip in 115.044 seconds. Its +actual host was Ubicloud Ubuntu 24.04, image `20261021.1.0`, selected by +`vars.CI_LINUX_RUNNER || 'ubuntu-latest'`. That result qualifies only this +bounded observation; it neither identifies nor corrects the historical cause. + +The positively classified terminal failure, run `40194870304`, job +`121173439610`, instead used the explicit `ubuntu-24.04` selector on a +GitHub Actions runner image, version `20261012.334.1`. Core's separate, +unclassified normalization failure used Ubicloud and a different, filtered +`AcquireGitHub` fetch path. Do not combine these failures or infer a shared +cause from their host names. + +Make one additional observation on the terminal failure's runner provider and +OS selector. This amendment supersedes only the previous instruction to retain +the workflow's runner expression: set `runs-on: ubuntu-24.04`. The test blob +`f579c6b68c5d625e9e87e4e51b288990ddfcd1a4`, exact invocation, child environment, +512-iteration count, first-error stop, containment, deadlines, permissions and +all other workflow settings remain unchanged. Do not add a matrix, retry, +fallback host, additional Ubicloud attempt or process behavior change. + +The selector does not pin the historical GitHub image version. Record the +new observation's actual provider, image version, source SHA/tree and test +blob, plus the named test result and absence of skips. Preserve the first +Ubicloud result separately. A pass on either host, or a difference between the +two results, does not by itself establish the historical failure's cause. +A failure may supply a controlled original error for a separately reviewed +correction; a pass remains a noncausal observation. All final-candidate +Required/Core/Coverage/native gates remain mandatory. + +Two independent plan-review rounds precede this one-line workflow change; +two independent frozen code reviews precede its one hosted invocation. Publish +the reviewed amendment and workflow on the reproduction branch, then advance +the focused branch once from `14c35e9d35fcd976b5d74bcf4c02ff3986073b5e` using an +expected-head lease. Verify YAML, the exact single-line workflow diff and the +unchanged test blob; repeat neither the already passed inert Go validation nor +any native execution on the unsupported local executor. diff --git a/docs/plans/project-install-e2e/opencode-code-review-r1.md b/docs/plans/project-install-e2e/opencode-code-review-r1.md new file mode 100644 index 000000000..b1d24bca7 --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-code-review-r1.md @@ -0,0 +1,96 @@ +# OpenCode project adapter independent implementation review R1 + +Reviewer: codex_claude, 2026-10-23. Reviewed cumulative leaf +`d9f3f757..f94906f42e788791e15d198127144ad8fe2c40f2` in +`work/uap-project-opencode`. Reviewer did not author this implementation. + +## Decision: interim NO GO pending selected-data authority correction + +One confirmed blocker remains in active native reprojection: the +`ProjectActiveNative` data-path argument is discarded, while the native object +builder reads the stored projection's DataRoot. Validation checks that the +stored root is absolute but does not compare it with the currently selected +data authority. An intact, digest-valid package therefore cannot establish +agreement with a different data root supplied by the owning lifecycle. Root/R2 +already identified this independently; the author is preparing the correction. + +The fix must compare against the selected data authority without rewriting +digest-covered package bytes, preserve existing user behavior where applicable, +and reject mismatches with no package/native effects. Final R1 closure will +review the frozen fix and independently execute its negative regression. + +## Other reviewed boundaries: no additional blocker found + +- Project root config candidates are exact `opencode.json`/`opencode.jsonc`, + while native skills resolve separately below `.opencode/skills`. Prepared + packages and persistent data remain in selected project `.uap` authority. + Original detected user ConfigRoot stays distinct; neither package paths nor + ambient cwd select project scope. +- The internal layout travels through projection, native namespace checks, + inspect, apply, verification and remove. Project objects require exact IDs, + names, absolute clean paths and containment. MCP logical keys retain their + exact slash/space spelling instead of becoming filenames. Foreign same-name + disabled entries and aliases retain closed refusal. +- The scoped domain declaration uses core's native-config lifecycle. Existing + exact-entry kernel ownership, conditional writes, exclusive skill swaps and + receipts remain the mutation machinery; no second configuration engine or + alternative plugin registry lifecycle is added. +- Durable dialect transition discovery/recovery derives split native layouts + from persisted binding scope and root. Every production recorder constructor + now receives selected ProjectRoot. Missing/mismatching selected context + refuses before reconciliation. Old/target state, object receipts, skill + intents and journal root checks remain bound to the same selected layout; + user-only helper wrappers preserve their original layout. +- Successful project configuration reports prepared plus installed, with + effective policy, native discovery, permissions and MCP initialization + unobserved. Project preflight rejects ActivationComplete before provider + attestation shortcuts and on direct adapter dispatch. Native prepared status + is also retained through durable transition target-state publication. +- The production public saved CLI fixture uses real TLS Git acquisition, + standard loading, immutable store, planner, staging, installer and native + transactions. Its clearly named inert TEST executable handles only version + contract probing. The fixture is not represented as real OpenCode discovery. +- The saved journey checks A/B installs, exact unchanged locked state, old-pin + repair after moving source, interrupted directory recovery, update with + retained source/binding/data authority, prepared status, exact removal and + repeat-removal no-effects. User/B inventories, foreign disabled settings, + comments, skills and retained persistent data are checked separately. + +## Independent verification at the reviewed checkpoint + +Go 1.26.10, GOMAXPROCS=3, `-p=2 -count=1 -timeout=3m`, selection +`^Test(ProjectOpenCode|NativeTransition)`: + +- OpenCode client PASS **0.479s**; +- providers PASS **0.040s**; +- production public CLI PASS **3.544s**. + +These include the authored scoped contracts, durable transition tests and +composed saved project journey. They do not cover the missing selected-data +negative until the correction lands. No genuine OpenCode client, model, +configured MCP server, trust action or account operation was executed. + +Genuine pinned hosted effective-config, skill discovery, marker handshake/tool +result and restart observations remain separate pending native gates. File +receipts and the inert version probe cannot supply those observations. + +## Final R1 closure: GO at 90fcc0af + +Independently reviewed corrective commit +`90fcc0af34008a4416758c0c9d83678e72323d5d`. The active-project entrypoint now checks cancellation, validates +scope/layout and exact selected active root, reads the existing immutable +projection, and requires its exact package root. For projected stdio servers, +the selected data locator must be absolute, already clean and exactly equal to +the stored DataRoot. An empty, relative, other-project or changed receipt +returns ErrProjectionRefreshRequired without rewriting package bytes. Legacy +user reprojection retains its earlier behavior. Remote-only/skill-only plans do +not invent a data requirement. + +The added regression checks the accepted exact projection against the original +objects; exercises empty, relative, project-B and another selected data root; +refuses another active package authority; and compares complete project, +user and project-B inventories after the read-only calls. Independently reran +`^TestProjectOpenCode` with Go1.26.10/GOMAXPROCS3/-p2/count1: client **PASS +0.135s**, providers **PASS 0.030s**. The selected-data blocker is closed and no +additional blocker was found. The genuine hosted native boundary stated above +is unchanged. diff --git a/docs/plans/project-install-e2e/opencode-code-review-r2.md b/docs/plans/project-install-e2e/opencode-code-review-r2.md new file mode 100644 index 000000000..1c1c7febb --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-code-review-r2.md @@ -0,0 +1,95 @@ +# OpenCode project adapter independent implementation review R2 + +Reviewer: cursor_clients, 2026-10-23. Initial reviewed checkpoint +`f94906f42e788791e15d198127144ad8fe2c40f2` in `work/uap-project-opencode`, +including production adapter `79cc15cf`, qualification fixes `69f97713` and the +public composed saved lifecycle. Governing instructions, original two-GO +client plan and author implementation evidence were read. + +## Initial decision: NO GO pending selected-data authority correction + +`clients/opencode/project.go` ProjectActiveNative ignores its selected +data-path argument and derives MCP ownership from the old package's stored +OpenCodeProjection.DataRoot. Native projection validation only requires that +stored data path be absolute; it does not compare it with the independently +selected DataReceipt.Locator supplied by core. Thus a read-only reprojection +can silently acknowledge old-locator desired receipts when the selected data +authority differs. The corresponding Cursor adapter review found the same +class of issue. This is not a request to rewrite immutable package bytes. + +Required correction: on the project branch, read and validate the existing +projection against selected native/config/package authority and compare the +stdio data locator with the selected data-path argument. A mismatch must +refuse or require refreshed projection. Add a correct-data positive case and +wrong-data refusal with unchanged immutable bytes. Author/core acknowledged +and is implementing this narrow correction; no reviewer-owned production +edit was made. + +## Boundaries reviewed without another blocker so far + +- Resolved project layout keeps root `opencode.json[c]` separate from + `.opencode/skills`, preserves the original detected user ConfigRoot and + validates explicit target context/native root through projection, registry, + apply, verify and removal. Containment refuses native aliases. +- Native ownership validates project object IDs/names/exact paths. Skills + remain leaf paths; MCP keys retain native slash/space semantics and are not + converted into filenames. Unowned/disabled foreign MCP entries reserve their + keys and cannot be adopted. JSONC preservation and exact receipt/conditional + kernel behavior remain in use. +- Existing durable dialect transition schema retains binding scope and native + root authority. Project pending/reconciliation selection requires an equal + explicit ProjectRoot, rejects missing/different project contexts before + mutation, and derives private skill transaction roots from the stored split + layout. Current state and recorded old-state layouts must agree. Project + committed state remains prepared plus installed verification. +- Activation preflight precedes generic attestation admission and direct + activation reuse. Exact configured bytes do not become effective native + config/discovery/permissions/MCP proof. User lifecycle and legacy user helper + layouts remain separate. +- Production recorder constructions pass the selected ProjectRoot in CLI and + installer service/observation/recovery. Namespace preflight derives the root + from neutral validated context without importing a concrete client adapter. +- The public composed journey uses real TLS Git with an explicitly inert + version-only TEST process. It covers saved A/B isolation, locked no-op, + exact-old-pin repair after source advance, interrupted directory recovery, + moving-source update, conservative status, retained data and exact/repeated + removal. Those are fixture contracts; genuine native discovery/runtime and + the generic Git/ZIP transfer proof remain separate evidence. + +## Independent verification and fix follow-up + +After coordinated capacity recovery, independently inspected frozen correction +`90fcc0af34008a4416758c0c9d83678e72323d5d`. Project active reprojection now +validates the scope-selected split native layout, exact active root and +contained package path, original config candidates and package root. A stdio +selection requires the caller's nonempty clean absolute data locator to equal +the immutable stored DataRoot. Mismatch returns the existing projection-refresh +error instead of rewriting or acknowledging old package bindings. Existing +user reprojection behavior remains intact. + +The new regression supplies matching package/data authority and requires +identical desired objects. Empty/relative/different-project/different-receipt +data and a mismatched active package root refuse. It checks unchanged package, +native, user and project-B byte inventories after the read-only refusals. + +Independent Go 1.26.10 execution with GOMAXPROCS=3, `-p=2 -count=1`: + +- `./clients/opencode ./providers -run '^TestProjectOpenCode|^TestNativeTransition'`: + client PASS, 0.964s; providers PASS, 0.143s. This includes the project durable + crash/reopen boundaries, selected-context refusal and new data contract. +- Production public CLI `./cmd/agentplugins -run '^TestProjectOpenCodeComposedSavedLifecycle$'`: + PASS, 3.526s. + +The compiled crash/version helpers are explicitly inert TEST processes. No +genuine OpenCode process, authored MCP command, package payload, model call, +login, trust or account action was run by this review. + +## Final decision: GO at corrected checkpoint 90fcc0af + +The selected-data blocker is closed, with no remaining implementation blocker +found in the reviewed scope. GO for configured project components, scoped +durable recovery and the composed saved lifecycle. Effective native config, +real skills/MCP discovery and harmless runtime markers, precedence and fresh +native restart/removal remain genuine hosted evidence gates. The passing +fixture journey and this review do not establish those facts or release +readiness. diff --git a/docs/plans/project-install-e2e/opencode-implementation-evidence.md b/docs/plans/project-install-e2e/opencode-implementation-evidence.md new file mode 100644 index 000000000..d59e7d05a --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-implementation-evidence.md @@ -0,0 +1,172 @@ +# OpenCode project implementation evidence + +Status: scoped adapter, catalog and public composed lifecycle checkpoint, +2026-10-23. +The original all-client plan and its two independent reviews remain +authoritative. Both independent implementation reviews are GO at corrected +checkpoint `90fcc0af`; hosted native qualification remains a separate pending +gate. + +## Native layout and authority + +The accepted native paths are root `opencode.json` or `opencode.jsonc` for MCP +configuration and `.opencode/skills//SKILL.md` for skills. The original +detected user `ConfigRoot` is preserved. The internal resolved layout carries +both native locations through projection, inspection, apply, verification, +removal and the existing dialect transaction/recovery code. Legacy user helper +entrypoints continue to select their original `/skills` layout. +No package path, changed ambient cwd or fake user profile selects project scope. + +The existing namespace preflight validates the explicit selected target context +and uses its exact project config candidates, without reaching into a concrete +adapter from the generic provider. A disabled same-name server in the selected project is a +collision; the distinct preserved user config is not mistaken for the project +registry. Exact project object IDs, names, absolute paths and containment are +checked before native writes. + +The public current documentation was checked on 2026-10-23: + +- [Configuration](https://opencode.ai/docs/config/) documents JSON/JSONC, + root project config, upward discovery to the Git boundary and merging of + user, project, custom-directory, inline and managed settings. +- [Skills](https://opencode.ai/docs/skills/) documents `.opencode/skills`, + compatible `.claude`/`.agents` paths and their ancestor discovery. +- [MCP](https://opencode.ai/docs/mcp-servers/) describes native local/remote + configuration; transport/dialect eligibility still comes from the existing + immutable prepared host authority, rather than guessing from config content. + +The existing hosted native matrix pins OpenCode 1.18.34 with per-platform +artifact integrity. This checkpoint does not change that pin or execute the +hosted runner locally. + +## Existing durable transaction + +No new journal schema or native mutation engine was added. Version1 private +OpenCode records already bind complete old/target state, including each +ClientBinding.Scope and NativeProfileRoot. Recovery resolves the split layout +from that binding authority and requires the selected ProjectRoot to agree. +A project recorder with missing or different context refuses discovery and +reconciliation before native mutation. All production recorder constructions +receive the selected root, including CLI lifecycle and installer observation, +service and recovery. + +The existing exclusive skill moves, receipt-bound MCP dialect transition, +state disposition, uncertainty, retained backups and committed-cleanup behavior +remain in use. The legacy skill kind spelling is retained as a receipt codec +identifier; scope and the exact native path determine project ownership. + +Successful project file configuration is conservatively prepared activation +with installed verification. Generic activation attestation cannot establish +native discovery and is refused before the provider shortcut and on direct +adapter reuse. Effective config, permissions, native skill discovery and MCP +initialization remain unobserved. User activation behavior is preserved. + +## Executed contract evidence + +Final complete OpenCode package: PASS1.035s. After adding scoped contracts, +planner/stager qualification and the namespace correction, the focused run passed: + +```sh +go test -p=1 ./install/integrationctl/agentplugins/clients/opencode \ + ./install/integrationctl/agentplugins/providers \ + ./install/integrationctl/agentplugins/domain \ + ./install/integrationctl/agentplugins/planner \ + -run '^TestProject|^TestNativeTransition|^TestCompatibilityGolden|^TestPlanGolden' \ + -count=1 -timeout=2m +``` + +Final focused results: OpenCode PASS1.197s, providers PASS0.181s, domain +PASS0.004s and planner PASS0.016s. These are production provider, +native-kernel, filesystem and compiled inert TEST-process contracts, not real +OpenCode runtime evidence. They cover: + +- root JSONC comments/foreign disabled entries with skills in `.opencode`, + repeated automatic/verify-only configuration and same-pin missing native + skill/MCP repair; +- exact repeat removal, untouched user/project-B inventories and refusal of + tampered IDs/names/paths, wrong selected roots and activation attestation; +- selected-project namespace collision checks, exact slash/space MCP keys + without filename normalization, and contained native alias refusal; +- production loader/planner/command-renderer/stager/activator composition, + rendered command skill ownership and read-only active native reprojection; +- six actual abrupt TEST-process crash boundaries for both existing user and + project v1-to-v2 transitions: prepared record, backup, skill publication, + native config, state and cleanup, followed by reopened idempotent recovery; +- selected project context refusal before recovery, with native bytes/state + unchanged and durable pending authority retained. + +The installer and production CLI composition packages compiled successfully at +the new context seams. The focused regex had no matching tests there, so this +is not a claim of installer/CLI lifecycle execution at this checkpoint. + +## Scope and gate assessment + +The OpenCode catalog row adds only its qualified project native-config policy. +The four compatibility golden changes are exactly OpenCode's Scopes array from +[user] to [user, project]; a deep comparison checked that all other values are +unchanged. User planning goldens pass unchanged. + +Exact golangci-lint2.13.2 changed-code and whole-file size/complexity/architecture +gates both report zero issues. The initial concrete-adapter import violation +was removed from the generic namespace provider. One complexity finding was +fixed by extracting unchanged managed-context validation. No baseline, +architecture exception, hosted guard or native artifact pin was changed. + +## Remaining checkpoint work + +- Hosted native effective-config/skill/MCP discovery and harmless marker proof, + with A/B/user precedence, fresh restart and removal observations. Higher + priority inline/managed/custom directory and ancestor behavior must be + observed by the real pinned host; file presence cannot establish activation. + +## Public composed saved lifecycle + +The production public CLI composition now has a dedicated OpenCode saved +project journey. Real TLS Git acquisition, standard loading/planning/staging, +immutable storage, installer, native kernel and receipt/state publication are +used throughout. The native version-contract boundary is the existing compiled +inert TEST process, limited to `--version`; it supplies no real OpenCode native +discovery or runtime evidence. Production host preparation and exact executable +revalidation still execute against those fixture bytes. + +The test installs saved dependencies into projects A/B and preserves the +distinct user config. Unchanged locked sync must retain exact state bytes. +Managed package drift and a missing native skill are repaired from the old +immutable pin even after the Git source advances. Durable interrupted directory +recovery and repeat recovery preserve committed native config and caller-owned +staging. Moving-source update must preserve source/binding/data authority; +saved status reports project/prepared. Removal consumes exact owned skills and +MCP keys, retains foreign disabled configuration/comments/skills and persistent +data, and repeat removal has no further project effects. User and all project-B +files remain byte-identical. + +Final dedicated public composition contract: **PASS 3.345s**, Go 1.26.10, +GOMAXPROCS=3, `-p=2`, count1. Exact golangci-lint2.13.2 changed-code and whole-file +size/architecture gates on the affected public CLI package both report +**0 issues**. No adapter production code, native guard or lint policy changed +for this proof. The generic source-core Git/ZIP cold-restore/transfer suite +remains separate; this is not a transport-by-client cross-product claim. + +## Independent R2 correction: active projection data authority + +R2 identified that project ProjectActiveNative ignored the selected dataPath +supplied by core after validating the persisted data receipt. The existing +stored projection could therefore retain its old DataRoot while presenting +desired receipts for a different selected locator. + +The project-only active check now validates matching target scope/layout, +exact active package root, containment, native config roots and stored +PackageRoot before deriving receipts. For stdio, the selected data locator must +be nonempty, clean and absolute, and equal the stored immutable DataRoot. A +mismatch returns ErrProjectionRefreshRequired without rewriting package bytes. +The existing user projection path is preserved. + +The focused regression includes a correct selected locator plus missing, +relative, another-project and different-receipt locators, and a mismatching +active package root. All failures must leave package/native/user/project-B +inventories unchanged. Project provider contracts pass **0.022s**; the public +saved project lifecycle passes **3.395s** with its real stdio data journey. Both +exact golangci-lint2.13.2 changed-code and whole-file size/architecture gates +report **0 issues**. Both independent reviewers inspected this correction and +closed GO at `90fcc0af`; their reports are recorded alongside this evidence. +No native host or model execution was added. diff --git a/docs/plans/project-install-e2e/opencode-native-boundary-exceptions.md b/docs/plans/project-install-e2e/opencode-native-boundary-exceptions.md new file mode 100644 index 000000000..bfc4d8463 --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-native-boundary-exceptions.md @@ -0,0 +1,27 @@ +# OpenCode native fixtures: exact removed-contract scan exceptions + +2026-10-25. Required run `40185974015`, job `121146724823`, passed its full +test and manifest stages, then failed the removed-contract boundary scan on +reviewed native-fixture controls and their plan. This amendment changes no +OpenCode adapter, fixture behavior or public environment compatibility. + +In `scripts/check-removed-contract-boundary.sh`, retain the existing OpenCode +rule's expression, scanner function, failure behavior, rg/git-grep paths and +existing exclusions. Add exactly three literal file exclusions to that rule: + +- `repotests/agentplugins_opencode_project_native_contract_test.go` +- `repotests/agentplugins_opencode_project_native_e2e_test.go` +- `docs/plans/project-install-e2e/opencode-project-native-gate-plan.md` + +These files contain explicit disposable native observations and negative +ambient-inheritance assertions. They do not restore a public compatibility +surface. Add a short explanation beside the rule; add no directory wildcard, +new general exemption mechanism or exclusion for any other rule. + +Two independent plan rounds precede the shell change, followed by independent +frozen code review. Validate shell syntax and the complete current-source scan. +In a temporary miniature Git repository, execute the actual copied script with +rg and with rg unavailable so its git-grep fallback is used. Both must admit the +three exact files and reject matching control references in CLI, SDK, an +adjacent test and an adjacent plan. Preserve the original failure and scanner +error handling. This bounded verification does not replace final Required CI. diff --git a/docs/plans/project-install-e2e/opencode-platform-admission-recovery.md b/docs/plans/project-install-e2e/opencode-platform-admission-recovery.md new file mode 100644 index 000000000..141e12c2a --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-platform-admission-recovery.md @@ -0,0 +1,118 @@ +# OpenCode platform admission and exact public progress: recovered candidate + +This source reconstruction follows the previously twice-reviewed platform plan +31393f78 / final 1e5b742701ded5ad3de3fe62b00e9e0c026a8870. It needs two fresh +code approvals on its new tree. No old whole-candidate approval is inherited: +0ecab091's whole approval was withdrawn when its repeat-add stderr expectation +was found unreachable in the actual public command dispatch. + +## Actual observations and limits + +Genuine run 40145166981, source 46b4678ea66d7804311f98270b70ebd3cceb928a, +failed before project discovery qualification. The macOS OpenCode job +121024834151 passed all four existing user lifecycle gates, then stopped on the +first public UAP user add because successful public progress was treated as +dirty stderr. Its bounded project artifact retained false qualification flags. + +Windows OpenCode job 121024834126 and Codex job 121024834145 failed in the +matrix safety tests before native execution. Their observed Python traceback was +3.12.10: importing the POSIX resource helper failed, and the positive project ZIP +admission test returned false without retaining its cause. The Windows metadata +explanation below is source-backed, not a locally reproduced Windows diagnosis. + +No vendor binary is acquired or run by this patch's local tests. No workflow +selector, native argv, guard, user gate, capture limit, model/MCP/trust contract, +or project ownership assertion is relaxed. Hosted Windows/macOS success remains +a subsequent requirement. + +## Platform model + +The matrix records the actual ambient Python implementation, version and +executable. An upstream source tag is evidence about implementation semantics; +it does not pin the hosted interpreter. + +CPython v3.12.10 path stat copies Windows birth time into ctime, whereas held-fd +stat exposes ChangeTime. The publisher retains the complete identity before and +after within each domain, including each domain's ctime. Cross-domain matching +requires positive volume/file identity and birth time, identical mode/link +count/size/mtime/attributes, and absence of reparse attributes/tags. Missing or +zero identities refuse. It never compares unlike ctime meanings or discards +same-domain change detection. + +Primary source: +- https://github.com/python/cpython/blob/v3.12.10/Modules/posixmodule.c +- https://github.com/python/cpython/blob/v3.12.10/Python/fileutils.c +- https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew +- https://docs.python.org/3.12/library/msvcrt.html#msvcrt.open_osfhandle + +Windows opens use typed pointer-width CreateFileW, read-only existing-only, +noninheritable handles with OPEN_REPARSE_POINT. The descriptor transfer requests +binary mode. Invalid handles never reach transfer/close, failed transfer closes +the raw handle exactly once, and successful transfer gives the descriptor sole +ownership. Win32 failures remain typed; there is no ordinary-open fallback. + +The publisher admits bounded immutable in-memory bodies and verified stream +hashes. It rechecks the original root/member path metadata after ZIP assembly +and before publication within the original four-second budget. It does not +claim continuous writer exclusion. Subsequent writes cannot change the bytes +already captured in the admitted ZIP. + +Only the six POSIX capture tests are skipped on non-POSIX systems, before +importing their POSIX-only helper. A pure import regression verifies that scope. +All other safety/admission/native gates remain enabled. + +## Exact public progress contract + +Capture/context/pipes errors refuse before any semantic or progress parsing. +Only ordinary public UAP commands admit these exact bytes. Native and Git stderr +stay strict. Unknown labels, extra bytes, missing lines, CRLF substitutions and +warnings refuse. + +| Labels | Exact stderr, each displayed line ends in LF | +| --- | --- | +| add-user | Resolving and validating one Agent Plugin package for every selected target... / Installing uap-opencode-user-control for 1 selected clients... | +| add-A, add-B, add-same-user, add-same-A, add-json | Resolving and validating one Agent Plugin package for every selected target... / Installing uap-commands-fixture for 1 selected clients... | +| repeat-add-A | Resolving and validating one Agent Plugin package for every selected target... / Applying transactional client package... | +| unchanged-update-A, update-A | Resolving and validating one updated Agent Plugin package for every selected target... | +| intact-repair-A, missing-repair-A | Resolving and validating each unique exact installed package revision once... | +| remove-A, remove-json | Empty | +| repeat-remove-A | Separate existing exact absence contract: pure unwrapped ExitError, exit 1, empty stdout and one of the two source-established fixed refusal lines; owned state/files unchanged | + +The first/repeat add distinction is measured by +TestOpenCodeProjectPublicRepeatProgressSource through the public Cobra command +and production scoped composer, planner, stager, kernel and saved state. It uses +only an inert compiled TEST --version process, an explicit disposable profile +and inert security admission. The first add takes the group Installing path. +The existing same-scope binding makes repeat add enter runAddLoaded before that +line and take Applying transactional client package. The measured repeat +reports mutated=false, keeps Prepared / installation_verified, and preserves +the complete original user tree and project bytes/modes, including state, +managed package and native skill/MCP configuration. Only the expected process +mutation-lock payload changes; its presence/type/mode remain checked. + +Fresh measurement after reconstruction: PASS 0.988s. The earlier exploratory +measurement was PASS 1.439s; an intermediate stronger whole-tree comparison +correctly exposed the process-lock payload refresh and was narrowed explicitly +to preserve all persistent content rather than falsely asserting lock bytes +were immutable. + +## Local verification and review gate + +- Five platform tests cover separate path/fd ctime stability, every positive + cross-domain field, root/member/held reparse refusal, binary CRLF/Ctrl-Z bytes, + post-ZIP mutation refusal, typed flags/handle transfer and failed-transfer + ownership. Together with five existing envelope tests: 10 PASS 0.120s. +- Public source first/repeat proof: PASS 0.988s; no vendor execution. +- Focused inert parser/capture/loopback recorder suite: PASS 0.209s. The first + sandboxed invocation refused the loopback socket before the test body; the + same finite suite passed under the already authorized execution environment. +- Configured changed-source repotests lint was attempted with the unchanged + 90-second bound twice. Both ended with exit 4 / timeout after reporting zero + issues, so neither is a lint pass. The first used an accidental read-only + default cache; the second used the explicit shared writable cache and had no + cache warnings. Full exact-source lint remains a hosted gate. Budgets and + checks are unchanged. + +Two fresh code GOs and parent-controlled publication are required before this +candidate reaches an executable hosted workflow. No native qualification is +promoted by these local checks. diff --git a/docs/plans/project-install-e2e/opencode-project-intact-repair-contract.md b/docs/plans/project-install-e2e/opencode-project-intact-repair-contract.md new file mode 100644 index 000000000..59dab2371 --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-project-intact-repair-contract.md @@ -0,0 +1,57 @@ +# OpenCode project: report actual intact group repair mutation + +At exact source `e81e9daf2fbce3acf36aee33eb954a68fcf18acc`, tree +`587193cb6cf17ea277630ecd8a03ef9f5fc7753e`, native run `40194870389` +passes repeat add and unchanged update but fails the newly reached intact +repair assertion on both platforms: `mutated = true, want false`. + +The macOS artifact `12600345253` is 647,797 bytes, SHA-256 +`3a948f788587492286ffb853302fe6a26959feed51d84bb6bd61fa05883267f0`; +Windows artifact `12599574306` is 635,744 bytes, SHA-256 +`024a89ef76c5f9ac9022628b1aad217484fcdf43455562a3362c639a0ac46d4b`. +Their closed runner digests/source identity were verified. Both raw repair +outputs are real completed single-target batches, Prepared and installation +verified, with `mutated:true` and no `no_change`. Recorder counts remain zero. +The remaining four required native tests pass without skips. Later project +stages were not reached and remain unqualified; the failed envelope does not +contain the working authority's full state or journals. + +## Existing contract and exact correction + +The public group repair path calls `RepairGroup`. In `group_plan.go`, repair is +explicitly excluded from the unchanged-package shortcut, so it restages and +commits the managed package. The existing +`TestProjectPreparedGroupPublicLifecycleAndHandoff` explicitly requires actual +mutation and changed state for intact group repair while native bytes remain +equal. The separate single-target usecase repair can be unchanged; that is not +the public batch path under test. Preserve production behavior and both tests. + +Change only this native project's assertion flow and narrow pure contracts: + +- Keep repeat add and unchanged update in the current strict no-mutation loop, + including exact state/config/skill bytes and the existing assertion reader. +- Run the same already-budgeted intact repair separately. Require its real + completed batch, actual boolean `mutated:true`, and absent or boolean-false + `no_change`; refuse malformed or contradictory values without defaults. +- Require state bytes actually to change. Snapshot the exact managed package + directory, native config and native skills before/after and require identical + byte/mode inventories. Keep the following fresh `stable-A` config/skill pair. +- Compare the complete before/after binding, excluding only `UpdatedAt` and the + appended transaction receipt history. Require the nonempty old receipt prefix + exactly preserved and exactly one new committed `directory_swap` receipt with + matching binding/path and before/after digests equal to the last owned package + receipt's nonempty after digest. All other + binding fields, including identity, selected delivery, package revision, + native ownership, profile authority, lifecycle, authentication, policy and + pending-attempt fields remain exact; explicitly refuse unresolved attempts. +- Keep bounded before/after state/native snapshots and the binding comparison + facts in the existing summary, without a new ZIP member or collector change. + Describe the stage as unchanged replay plus mutating, byte-preserving repair. + +No command, timeout, cap, environment, native parser, original user test, +production lifecycle or later stage changes. Expected implementation is one +small native helper and one pure receipt/binding assertion with focused positive +and refusal contracts, not another lifecycle harness. Root and a second peer +review two frozen plan rounds and the final code. Run the pure contracts, compile, +existing project group lifecycle contract and configured affected-source lint. +No local vendor execution; exact final-head hosted replay is still required. diff --git a/docs/plans/project-install-e2e/opencode-project-local-repair-source.md b/docs/plans/project-install-e2e/opencode-project-local-repair-source.md new file mode 100644 index 000000000..ca97aa2c6 --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-project-local-repair-source.md @@ -0,0 +1,41 @@ +# OpenCode project repair: preserve the selected local directory revision + +The remaining missing-skill repair stage has a source-backed expectation +mismatch. It deliberately advances a mutable local source after installation, +then expects the public CLI to recover the old package automatically. That stage +has not yet been reached by hosted execution; this finding comes from the public +source path and its existing contracts, not a newly observed native failure. + +`AcquireLocal` in `sourceacquisition/acquirer.go` records an absolute canonical +directory with an empty source kind. `repairSource` in CLI `lifecycle.go` returns +that directory. `repair_multi.go`'s revision selection/resolution therefore takes +`loadPackageFor` and reacquires the current local directory. `RepairGroup` and +`repairPackageRevisionMatches` require its exact applied package digest. This is +deliberate refusal, covered by +`TestChatGPTUnsupportedRepairRendersStructuredRecoveryWithoutMutation` in +`cli_test.go`: a changed direct local source is non-reproducible and must fail +without changing the managed state. Immutable Git/archive and saved dependency +repair have separate source and pin contracts; this local fixture cannot claim +their acquisition behavior. + +## Exact test-only correction + +After deleting the exact recorded native guide directory, leave the current +local source at the already applied A-two revision. Remove only the deliberate +`UNINSTALLED-SOURCE-ADVANCE` append and the read used solely for that mutation. +Replace its explanatory comment and the stage reason with truthful unchanged +local-source wording. Preserve the missing owned directory, the same public +repair invocation, actual mutation requirement, Prepared binding/digests and +fresh native observation using the installed managed package's A-two bytes. + +Do not change production acquisition or repair, the changed-source refusal +contract, any result reader, source update, command table, subprocess count, +timeout, cap, passive recorder or later lifecycle stage. The fixed 43-command +hosted path remains unchanged. Source-only expectation repair does not qualify +native repair until the final hosted stage passes. + +Two independent plan rounds precede the narrow source diff; two independent +frozen code reviews precede publication. Run affected pure project contracts and +compile plus configured affected-source lint, with coordinated disk/build usage. +Preserve the already reviewed intact-repair assertions and all existing public +CLI tests. No local vendor execution or speculative product change is needed. diff --git a/docs/plans/project-install-e2e/opencode-project-native-gate-implementation.md b/docs/plans/project-install-e2e/opencode-project-native-gate-implementation.md new file mode 100644 index 000000000..c4880a3c2 --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-project-native-gate-implementation.md @@ -0,0 +1,110 @@ +# OpenCode current-source project gate implementation + +2026-10-24. Source-only fixture; no native execution or client acquisition locally. +The existing project adapter is unchanged. Native publication remains blocked +until two independent code GOs at the frozen implementation checkpoint. + +## Reviewed plan + +R1 exact44003dd5 received a root NO-GO pending all-child capture/aggregate limits, +real repository boundaries and an enabled-MCP bootstrap trace. The independent +Claude reviewer agreed the architecture and requested capture and exact warning +classification. Final R2 exacte44fc424, published with identical tree as +2b0ef18146c28da9b6a9b1ef86a0fb413d950eee, received both independent GOs. The +[plan and immutable primary sources](opencode-project-native-gate-plan.md) +record those constraints and their actual limits. + +## Implemented contract + +The actual public installer runs in one fresh original authority with separate +A/B, ordinary-user and JSON-case Git repositories. The complete direct command +table contains four Git setup commands, fourteen UAP operations and twenty-five +OpenCode observations. Live stdout/stderr caps and per-command plus fixture +deadlines apply to every class. The neutral test capture uses the reviewed +capped-writer/CommandContext/WaitDelay mechanics without changing existing +Claude callers; it is direct-child containment, not a process sandbox. + +The main JSONC lifecycle uses distinct user-control names, exact native selected +skill paths/content and MCP marker config, unchanged add/update/intact repair, +changed skill/command/MCP update, immutable-pin missing-skill repair, removal and +recognized repeated-removal refusal. Saved project receipts remain Prepared and +installation_verified. Original user state/config/control, B installed state, +foreign comments/settings and caller-owned data are preserved. Secondary +same-name and inline observations retain actual winners and unrecognized +warnings as blocked evidence; they cannot replace mandatory positive discovery. +The short JSON route records actual file format and exact removal separately. + +The passive loopback endpoint counts accepted TCP and HTTP requests, returns503 +and implements no MCP protocol. Every child and the joined shutdown check both +counters remain zero. Unexpected connections fail the fixture; no initialization, +tool, model, auth, trust, enterprise/ancestor, IDE or default-plugin proof follows. +Vendor bootstrap/cache/download effects are explicitly distinct from ownership. + +Working authorities are nested below a prefix the wrapper never uploads. Go +admits captured bytes under a cumulative32MiB/128file envelope and atomically +publishes a distinct flat directory. The wrapper's separate five-second Python +child rechecks regular single-link identities, bounded exact stream receipts and +closed member set, then atomically publishes only a ZIP_STORED envelope outside +its private staging area. Refused/timeout sources produce only a bounded false- +qualification fallback; raw sources/cache trees are never a fallback upload. +Existing user fixture collection and all four required user tests are preserved. +Released, draft and prepared-installer project selection still refuses. + +## Inert verification + +- New Go strict parser, selected path/body, config source, finite command table, + environment, passive recorder and live capture/deadline contracts passed0.102s. + The guarded native fixture compiled and skipped with opt-in absent. +- Affected69 Python native-client matrix/draft/artifact tests passed3.789s. + Actual envelope tests cover links, special/directory entries, extra members, + changed receipts, caps, replacement, deadline, ZIP bytes and timeout fallback + preserving a foreign symlink target. +- Python syntax, Go formatting and diff whitespace checks passed. No new broad + suite or large native artifact was created under current shared space pressure. + +Two independent code reviews, relevant final lint/platform compilation and the +actual hosted macOS-arm64/Windows-amd64 project observations remain pending. +Nothing here qualifies a released installer or successful MCP initialization. + +## Independent code review correction + +Initial frozen c8a5eaa6 received NO-GO from both Root and Skillshare. Both found +that exit0 with a nonempty exec.ErrWaitDelay could admit valid partial JSON, and +that exact B/opencode.jsonc bytes were missing from the original snapshot. Root +also required bounded cap+1 snapshot reads after the initial size check. All +findings are corrected in the narrow revision: capture/context errors are typed +separately from a pure *exec.ExitError and refuse before parsing; the sole expected +repeat-removal exit1 is classified by exact source-established absence text; +B's entire native config bytes are included; stable no-follow held-file snapshots +read only the remaining byte cap plus one and revalidate path/held identity. +Malformed present MCP enabled fields now refuse as well. + +The actual early repeated-removal error precedes the JSON renderer: +remove_multi.go prepare/selectInstallation (read.go645) or buildInputs116 returns +an error, and cmd/agentplugins/main.go emits one fixed agentplugins stderr line. +Only empty stdout plus the exact two absence messages for the fixed fixture +name/target/scope, a pure ExitError with exit1, and unchanged owned state/files +are accepted. Other stderr, extra output, signal, capture/context error or failed +native JSON cannot become successful discovery. No public schema was invented. + +New inert negatives cover exit0 plus WaitDelay and valid JSON, wrapped exit/capture +causes, both exact absence forms and counterexamples, post-stat growth/size/leaf +alias refusal, and malformed enabled values. The complete focused inert Go set +passed0.075s with guarded native opt-in absent; earlier69 Python admission/matrix +tests remain unchanged by this Go-only correction. Both narrow code re-review +GOs and final platform/lint gates remain required before publication. + +## Final code review and platform compile receipts + +Both Root and Skillshare independently issued semantic CODE GO on frozen +4f70dc58c08f5e50f54696cdf1da2d6903847544 after tracing the initial implementation +and complete narrow correction. Their only remaining format request was standard +os imports separated from x/sys in the two platform snapshot helpers; this final +revision changes those import groups only and records the receipts. + +Root independently cross-compiled the complete repotests package at exact4f70 +for Windows amd64 using Go1.26.10 and -p1:19,473,408bytes, SHA256 +f07ecaa98cb8f8305f5f69d506dc43d262eb3a34a5d49b026b81244c66d76560. +No Windows test or native executable was run. Final hosted source gate and lint +remain required. The typed pure ExitError classification is Go's exposed cause, +not a new assertion that a failed process tree or all inherited pipes are gone. diff --git a/docs/plans/project-install-e2e/opencode-project-native-gate-plan.md b/docs/plans/project-install-e2e/opencode-project-native-gate-plan.md new file mode 100644 index 000000000..ae58aa7df --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-project-native-gate-plan.md @@ -0,0 +1,251 @@ +# OpenCode: pinned current-source project native gate + +2026-10-24. Plan only. The existing reviewed project adapter remains unchanged. +Two independent plan review rounds precede new fixture/selector/workflow code; +two independent code reviews precede publication that could execute a client. +This gate concerns source qualification, not a release or a native registry. + +## Pinned source and controls + +Keep the native matrix's existing OpenCode **1.18.34** pins and acquisition. +Official `anomalyco/opencode` tag `v1.18.34` points directly to commit +`aec0b9a6d8898f68f923aaf08b7306d931fd9d76`. Primary files inspected at that +immutable commit: + +- [debug/config.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/cli/cmd/debug/config.ts): resolve effective config, redact, print JSON. +- [debug/skill.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/cli/cmd/debug/skill.ts): list actual Skill service inventory and locations. +- [effect-cmd.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/cli/effect-cmd.ts) and [bootstrap.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/project/bootstrap.ts): both commands load a real instance. Config, plugin and other vendor services initialize; these are not filesystem-pure reads. +- [config/config.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/config/config.ts) and [paths.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/config/paths.ts): user, upward project, `.opencode`, custom and inline sources merge. Config loading may create its own `.gitignore` and fork dependency bootstrap. +- [core/npm.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/core/src/npm.ts): vendor dependency installation uses Arborist with `ignoreScripts:true`; do not claim no download or no vendor cache effects. +- [plugin/index.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/plugin/index.ts), [runtime-flags.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/effect/runtime-flags.ts) and [index.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/index.ts): `OPENCODE_PURE=1` skips external configured plugins; it does not disable project config merging. Correct the misleading existing fixture comment without changing its behavior. +- [skill/index.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/skill/index.ts): native scans include global, project and compatible skill paths; duplicate names can replace a prior entry. A path-bearing winner is evidence; an empty result is not discovery. + +The existing matrix provides verified CLI bytes, exact source build identity, +scanner provisioning and the genuine hosted admission. No native execution on +the task host, fake hosted variables, copied credentials, login, trust edit or +model request. Start with the existing macOS-arm64/Windows-amd64 source workflow; +no new Linux isolation claim or runner substitution is introduced by this plan. + +Use one explicit fresh original HOME/XDG authority outside sibling A/B and an +ordinary user working directory. Create a real fresh Git repository boundary in +each A/B and the user working directory, and a fourth for the short JSON case. +All are canonical temporary directories outside the checked-out UAP source and +outside each other. Use only fixed bounded `git -c core.hooksPath=/dev/null init` +with the fresh HOME/Git configuration; no hooks, remote or Git config import. +Assert each native cwd resolves to its own expected worktree before observation, +so native upward discovery cannot inherit the parent checkout or sibling. Every UAP/native child shares that authority. +Never use a project as HOME or `OPENCODE_CONFIG_DIR`. Preserve the explicit +platform environment, `OPENCODE_PURE=1`, and source-supported literal controls +`OPENCODE_DISABLE_AUTOUPDATE=1`, `OPENCODE_DISABLE_MODELS_FETCH=1`, +`OPENCODE_DISABLE_DEFAULT_PLUGINS=1`, `OPENCODE_DISABLE_LSP_DOWNLOAD=1`. +These restrict this observed CLI lane; they do not qualify default-plugin, +model, LSP or unrestricted session behavior. No configured JS/TS plugin, hook, +remote skill URL, legacy TOML migration input, well-known auth source or account +file is supplied. `OPENCODE_CONFIG`, `OPENCODE_CONFIG_CONTENT`, +`OPENCODE_CONFIG_DIR` and `OPENCODE_DISABLE_PROJECT_CONFIG` are absent in the +positive lane. A later isolated inline-precedence observation is explicit. +Vendor bootstrap may create its own cache/config metadata; retain that fact and +separate it from exact UAP ownership/foreign preservation assertions. + +## Fixture and supported contract + +Add `TestAgentpluginsOpenCodeNativeProjectComponentsLifecycle` behind its own +`AGENTPLUGINS_OPENCODE_PROJECT_NATIVE_E2E=1`, then the unchanged real disposable +host guard before any acquisition/client operation. Keep all existing required +OpenCode user lifecycle, tool-collision, runtime and command tests in the source +selector; no existing test is renamed, weakened or dropped. + +Use actual public CLI install/update/repair/remove with `--scope project` and +explicit canonical `--project-root`, original user authority, exact native +executable and no saved dependency intent where the existing native fixture uses +local source. Separate installer envelope/state/file receipts from native JSON. +The adapter's public lifecycle stays Prepared + installation verified. Native +observations in this test do not promote persisted UAP activation. + +Use root `opencode.jsonc` with preserved foreign JSONC comments/settings for the +main A/B lifecycle, and `.opencode/skills/` as the actual skills +surface. Managed package/data receipts remain under the selected `.uap` root. +A short separate JSON fixture proves root `opencode.json` selection; it need +not duplicate every lifecycle step. Never invent a v2 schema or silently change +the pinned version/dialect when a native command fails. + +Fixture packages contain only standard inert skills/portable commands and +remote loopback MCP declarations pointing at a parent-owned passive HTTP +recorder on an ephemeral `127.0.0.1` port, with no local stdio +server, JS/TS plugin, hook or payload executable. The passive recorder implements +no MCP protocol or authentication: it counts accepted TCP connections and HTTP +requests, and returns HTTP 503. Record bounded connection/request counters and a +bounded method/path metadata sample (no request bodies/credentials), with bounded +server read/header/write/idle deadlines. Both aggregate counters across all child +operations and their deltas for each positive config/skill probe must remain +zero. Any accepted connection or request fails that stage and is retained as +attempted connection evidence, never as a handshake. Close it under the fixture +deadline. Zero counters only describe this live recorder endpoint; they do not +prove the absence of unrelated network traffic. +The MCP declarations exercise +exact effective config selection, not connection or initialization. Do not run +`mcp list`: the existing helper explicitly attempts live connections. Actual MCP +handshake/tool qualification remains a separate gate and is marked not evaluated +in this fixture, even if a configured entry is enabled. + +Positive ownership phase: original user has a differently named owned/control +skill and foreign settings; A/B receive the same project plugin with identical +logical skill/server names. With no conflicting user name, fresh native A/B +config+skill lists must identify their exact selected project paths/bodies and +MCP marker URLs, omit the sibling's exact path/marker, and retain the user +control. Native effective config is not ownership authority for deletion. + +Then separately exercise the identical plugin name and identical skill/server +names at user+A+B, preserving all three receipts and files. Retain actual +same-name native winners by path/body from fresh user/A/B processes. If native +names collapse or a higher-priority source wins, record precedence/isolation as +blocked or unqualified, rather than demanding three simultaneously loaded +copies or overwriting user settings. This stage is separate from the positively +proved unique-marker project discovery stage. + +## Finite lifecycle and observation stages + +Use at most 30 direct native observation processes and 30 public UAP commands +per complete fixture, with an independent six-minute fixture deadline inside +the existing matrix's 15-minute test budget. UAP's own pinned host preparation +may invoke additional bounded version probes; do not misreport the direct-probe +ceiling as a measured total of all descendants. Setup uses at most four fixed +Git commands, with an overall 60 captured-child ceiling across these classes. +All new subprocess classes share bounded capture and the same aggregate artifact +budget, including UAP and setup failure output. +A fixed command list permits only `--version`, `debug config`, and `debug skill`. +No general shell, command taken from output, arbitrary path lookup, MCP list, +run/prompt/session, plugin install, trust, account or updater command is allowed. + +1. Measure exact version/binary identity and record source/build provenance. +2. Install positive user/A/B fixtures through actual UAP; fresh A/B lists must + prove exact project config/skills and user control, with raw path-bearing JSON. +3. Repeat add/unchanged update and intact repair; assert mutation flags/state + stability and retained Prepared guidance without treating listing as activation. +4. Change skill body and portable command invocation selection/MCP marker URL; + update A. Actual UAP owned bytes and fresh A native lists must advance while + original user and B owned files/state/foreign settings remain exact. +5. Delete only the exact recorded A skill directory, run repair from the recorded + immutable package pin, and require the original selected path/body again. +6. Remove A, repeat removal through its supported public result, and observe + its owned native skill and MCP key absent. User/B/foreign settings and retained + caller-owned preexisting plugin-data markers survive. These remote-only fixtures + do not create a new stdio data receipt; its complete lifecycle remains a + separately passed composed contract. Verify actual absence as well as native lists. +7. A separate same-name user+A+B phase records exact winners and conservative + precedence boundaries. Add one fixed inline override of only a nonsecret MCP + marker in a fresh process; observe its winner, clear the override, and verify + ordinary restart returns to the file-selected value. Never rewrite owned + project bytes to manufacture the override or adopt an observed foreign row. +8. The short root-JSON case installs/observes/removes exact project components + and records its format independently from the main JSONC journey. + +The count is a ceiling, not an invitation to add probes; retain an explicit +stage/command table in code and fail before a 31st direct observation or public +installer command. If the full table +cannot fit the cap/deadline together with required user tests, reduce repeated +native reads while keeping ownership/lifecycle assertions, or review a concrete +separate gate. Do not increase global native timeouts silently. + +## Capture, interpretation and artifact contract + +Do not copy the unbounded old `CombinedOutput` helpers into the new fixture. +Reuse the already reviewed bounded diagnostic capture primitive behind a +client-neutral test wrapper if necessary, parameterizing limits without changing +existing caller behavior. It applies to EVERY new child, including public UAP +operations and Git setup, not only direct native observations. Require fixed +executable/argv/cwd/environment, empty stdin, separate live 128 KiB stdout/stderr +limits, <=10 seconds for direct native/Git and <=30 seconds for public UAP, each +also capped by remaining fixture time, bounded pipe drain/WaitDelay and child +termination/reap. Never reuse old unbounded f.uap/f.native/CombinedOutput paths +in the new fixture. A failure, +deadline, overflow, nonzero exit, dirty warning or unrecognized JSON is recorded +and fails the relevant stage. No failed output becomes an empty successful list. +Any helper extraction is behavior-preserving and independently code-reviewed. + +Only in the isolated same-name precedence phase, a recognized exact duplicate- +skill warning may be retained as a blocked/unqualified precedence observation +with its valid path-bearing winner. Do not strip or globally ignore warnings. +Validate the known name and every reported source against exact fixture paths; +if the warning format is unknown, retain it as a blocked/failed observation. +Mandatory positive phases continue to fail on dirty warnings, malformed output, +nonzero exit or truncation. No failure becomes an empty or successful discovery. + +Decode one whole JSON document with duplicate/case-ambiguous keys, trailing +bytes, nonfinite values and malformed required field types refused. The new +project skill parser compares native locations literally/physically against the +selected known roots; it does not clean an arbitrary reported traversal into +an owned path. Verify required names and bodies; built-in/unrelated rows do not +count as managed discovery. Config expectations use exact known MCP entries, +source markers and transport fields, not a substring that can appear in logs. + +Keep separate attempted/passed/blocked/not-evaluated stages. Mandatory passed +stages cover actual UAP Prepared file lifecycle and the positive unique-marker +project discovery/config source. Same-name precedence and inline shadowing +retain their measured limits. MCP initialization/tool use, authentication, +model use, trust policy, managed enterprise/ancestor sources, IDE and default plugins are +explicitly not evaluated. No stdout or filesystem fallback can upgrade them. + +Retain only bounded flat JSON/log evidence, exact stdout/stderr lengths/hashes, +source/native/installer identities and foreign/owned state snapshots. No native +cache, database, binary, auth data or node_modules is uploaded. The aggregate +<=32 MiB/128 flat-file upload envelope includes ALL public-UAP/native/Git streams, +state snapshots, metadata and failure diagnostics. At most 60 captured child +commands with 128 KiB/stream contribute at most 15 MiB; reserve at most 120 stream +files and 8 metadata/snapshot files within the 128 file cap. The remaining bounded +budget covers summaries/snapshots. Enforce the cumulative limit while recording +and again at admission; stop further probes on exhaustion. Never admit only the +new native logs while leaving UAP/snapshot output outside the budget. A +refused envelope produces only a bounded false-qualification failure record; +raw refused trees are not the always-upload path. Reuse an already reviewed +admission helper if its actual boundary matches, otherwise review the narrow +new envelope code before publication. Preserve original errors on cleanup or +artifact refusal; fixture cleanup has an independent finite deadline. + +## Selection and verification + +Add OpenCode only to `PROJECT_TESTS` and its exact source-only selector. Released, +draft/prepared installer selection must continue to refuse project fixtures. +Assert the union with all four existing OpenCode required tests, no skipped or +omitted project test, and no source-scope option enabled by an environment alias. +Enable `--project-components` for OpenCode in the existing source workflow only +after two code GOs. Do not alter acquisition pins, hosted admission, permission +scope or released-native verification. + +Before publication, inert contracts cover strict native parsers, actual stage +sets, scoped fixture environment/command table, unsupported source selection, +old user test union, capture/deadline/overflow, artifact upload admission and +foreign state preservation. Compile the fixture and run affected Python matrix +contracts plus syntax/diff checks. No native process runs locally. Genuine +artifacts are then downloaded, hashed and independently interpreted; their exact +source/version/platform and remaining boundaries enter the final client matrix. + + +## R1 corrections and MCP source boundary + +Both independent R1 critics requested the all-child capture/aggregate budget and +known duplicate-warning distinction; root additionally required explicit Git +boundaries and a concrete enabled-MCP bootstrap trace. Those requirements are +incorporated above, without changing the adapter or promoting native lifecycle. + +At exact upstream commit `aec0b9a6`, [mcp/index.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/mcp/index.ts) +creates a shared MCP service whose per-instance state is registered through +`InstanceState.make`. This is a service/cache allocation, not a per-server +transport/client construction. [instance-state.ts](https://github.com/anomalyco/opencode/blob/aec0b9a6d8898f68f923aaf08b7306d931fd9d76/packages/opencode/src/effect/instance-state.ts) +constructs a ScopedCache with a deferred lookup; only `InstanceState.get` invokes +the initializer. MCP's initializer enumerates enabled entries and creates the +actual Client/transports, so operations such as `MCP.status/clients/tools/connect` +can cause connections. `createClient`/`client.connect` are in those invoked +paths, rather than the shared service constructor. + +The fixed debug config handler only calls Config.Service.get; fixed debug skill +only calls Skill.Service.all. InstanceBootstrap eagerly invokes Config.get and +Plugin.init plus its listed LSP/share/format/VCS/snapshot/project services, not +MCP.status/clients/tools/connect. The declared controls disable default plugins +and external plugin execution, and the fixture supplies no plugin, hook or skill +URL with another execution route. This is a static trace supporting the narrow +observation route, not a universal no-side-effects or no-network claim. The +passive loopback recorder adds actual attempted-connection evidence for the +only configured MCP URLs. If an accepted connection or request occurs despite this trace, keep that +failure and investigate before changing assertions or invoking another command. +No successful MCP initialization, tool use or network isolation is inferred. diff --git a/docs/plans/project-install-e2e/opencode-project-result-envelope.md b/docs/plans/project-install-e2e/opencode-project-result-envelope.md new file mode 100644 index 000000000..2c2621827 --- /dev/null +++ b/docs/plans/project-install-e2e/opencode-project-result-envelope.md @@ -0,0 +1,42 @@ +# OpenCode project native assertions: preserve both public result envelopes + +At `1a19373e024ba4c2ac0748fda1306ce0ffd95429`, native run `40191320869`, +macOS job `121162735576`, the project test fails on repeat add even though the +raw result now correctly contains `no_change:true`, `mutated:false`, Prepared +and installation verified. The old user helper insists on batch `data.status`, +which the public single `data.result` envelope does not contain. + +Artifact `12597788376` is 648,648 bytes, SHA-256 +`375dfd61790eac7b237c9fc2c9556d51cddcbd786a05cc6aed010b035da21f6a`. +Closed runner membership/digests and exact source/tree were independently +verified. The other four required native tests pass with no skips; project +positive A/B discovery passed before replay, with passive recorder counts zero. +Interrupted later stages are not qualified. + +## Exact bounded correction + +Only the project fixture's assertion reader and narrow inert contracts change. +Keep the original user `openCodeAssertCompleted`, every native command, stage, +state/file/runtime assertion, capture, limit and original raw JSON unchanged. + +Before downstream project assertions, validate one of these actual shapes: + +- Batch: retain the real `data.status == completed`, exactly one OpenCode + target and its actual result. Reject malformed or non-single targets. +- Single unchanged project result: require actual top-level success, + `no_change:true` and `mutated:false` booleans, OpenCode client, project scope, + Prepared and installation verified, with no attestation. Do not invent any + status, replay flag, activation or verification field. + +Both shapes retain exact project client/scope/Prepared identity checks. Refuse +mixed `targets`/`result` envelopes and explicit failure or contradictory status. +Only the single assertion view is wrapped with the existing assertion-envelope +helper so current plan/mutation checks can read its actual result. Never rewrite +the raw captured map or evidence, or route user operations through this adapter. + +Two independent plan rounds precede implementation, then frozen code review. +Inert contracts admit the actual batch/single cases, preserve raw input, and +refuse mixed, malformed/non-single, failed/contradictory status, wrong +client/scope, missing or ill-typed replay flags and contradictory lifecycle. +Run affected pure contracts/compile and configured changed-source lint. No local +vendor execution; exact final-head native replay remains required. diff --git a/docs/plans/project-install-e2e/ordinary-cleanup-failure-origins.md b/docs/plans/project-install-e2e/ordinary-cleanup-failure-origins.md new file mode 100644 index 000000000..e545a2e6e --- /dev/null +++ b/docs/plans/project-install-e2e/ordinary-cleanup-failure-origins.md @@ -0,0 +1,100 @@ +# Ordinary supervisor cleanup: fixed origin diagnostics + +## Observed boundary + +At source `e81e9daf2fbce3acf36aee33eb954a68fcf18acc`, terminal Linux +job `121173439610` fails +`TestReconciliationCLIValidatesPinnedDeclarations/missing_edge` with the +existing fixed record `phase=fetch`, +`class=ordinary_supervisor_descendant_cleanup`, `exit_status=unavailable`. +This proves a supervisor cleanup failure for that acquisition. It does not +identify the precise cleanup origin or prove that every other historical +acquisition refusal shares the same cause. Core job `121173439891` separately +fails `TestNormalizationPreservesRawTreeAndStableManifestAcrossMove` at +`skillsource_test.go:138`; that fixture has no runner diagnostic. Coverage +passes this source. Do not add another per-fixture logger or rerun passed +focused selections. + +## Concrete files and immutable semantics + +Production changes are restricted to a new Linux origin helper, +`osrunner_ordinary_failure.go`, `osrunner_tree_linux.go`, and +`osrunner_ordinary_proc_linux.go` in `install/integrationctl/adapters/process`. +Add inert contracts beside those files. No fixture logger, workflow, command, +environment, transport, credential, timing, cleanup or native gate changes. + +Wrap only the already-created error at an existing failure return/join. A +private Linux carrier delegates `Error()` to the exact original cause and +`Unwrap()` returns that cause itself. Nil and unknown reason inputs are no-ops. +An already-owned recognized origin survives broader wrapping unchanged; joined +causes retain all original errors, with the first recognized origin used only +as a bounded diagnostic. Origin lookup never reads error text, arguments, +streams, paths, arbitrary type names or environment. Existing gone/unsupported +decisions retain their exact outcomes; no disappearance becomes an error and +no uncertainty becomes success. + +## Frozen origin map + +| Fixed reason | Existing function and failure return/join | +|---|---| +| `exit_observation` | `superviseLinuxOrdinaryChildren`: failed leader `Waitid` observation | +| `task_directory_read` | `linuxDirectChildIdentities`: failed bounded task-directory read, preserving gone handling | +| `task_identity_unavailable` | `linuxTaskGoneAfterIdentityError`: identity missing while the task directory independently remains present | +| `task_children_observation` | `linuxDirectChildIdentities`: invalid task ID or failed task-children observation, preserving a more specific existing tag | +| `child_identity_read` | `linuxDirectChildIdentities`: observed child's identity read fails after the existing gone acceptance | +| `child_identity_changed` | `linuxDirectChildIdentities`: the same child ID appears with another start time during task inspection | +| `pidfd_open` | `linuxDescendantIdentitiesWith`: stable handle acquisition fails after the existing gone/reused acceptance | +| `pidfd_identity_read` | `linuxDescendantIdentitiesWith`: identity revalidation after handle acquisition fails after the existing gone acceptance | +| `pidfd_identity_changed` | `linuxDescendantIdentitiesWith`: handle-bound identity start time differs | +| `parent_identity_read` | `linuxDescendantIdentitiesWith`: changed-parent revalidation has a non-gone identity-read error | +| `parent_changed` | `linuxDescendantIdentitiesWith`: child attribution differs while the exact parent remains proved live/owned | +| `tracked_identity_read` | `liveLinuxTrackedProcess`: non-gone identity-read error after the existing stable-handle check | +| `tracked_identity_changed` | `liveLinuxTrackedProcess`: observed identity start time differs from the handle-bound identity | +| `traversal_deadline` | `linuxDescendantIdentitiesWith` and `linuxDirectChildIdentities`: existing traversal deadline returns | +| `traversal_limit` | the same two functions: existing process-visit/child-count bound returns | +| `descendant_signal` | `superviseLinuxOrdinaryChildren` and `killLinuxSupervisorDescendants`: existing non-ESRCH group/pidfd signal failures | +| `cleanup_deadline` | `superviseLinuxOrdinaryChildren`: existing forced-cleanup deadline return | + +Where a bounded task-directory error is currently passed to the caller's +existing gone decision, keep a gone/unsupported error object unchanged rather +than assigning an origin; only annotate its non-gone failure. This metadata +decision changes neither the returned error cause nor the caller's control. +Broader contextual errors and existing joins retain specific leaf origins. + +## Private wire and existing classifier + +Add optional `FailureReason` to the private supervisor result. The existing +cleanup failure producer keeps its exact error string and stage and copies +only the first recognized fixed origin into this field. A nil cleanup error +does nothing. Introduce no additional error formatting or observation. + +The receiver admits a reason only after the existing strict one-record/EOF/ +close admission, with a nonempty Error and `descendant_cleanup` stage. A +nonempty reason with an empty Error or another stage is contradictory: it +fails strict admission and establishes neither a stage nor a reason class. +Malformed, trailing and unknown-field records likewise remain unclassified +failures. Unknown reason plus valid cleanup/error remains the existing generic +cleanup failure. Missing reason retains legacy behavior. + +Keep the existing portable `OrdinarySupervisorFailureClass` signature. Only a +recognized, strictly admitted cleanup reason adds its fixed suffix to +`ordinary_supervisor_descendant_cleanup`. Other stages and unrelated errors +retain their existing classes. This lets current bounded fixture loggers expose +the origin without changing their precedence, caps, delegation or messages. +No arbitrary wire string becomes diagnostic output. + +## Contracts and qualification + +Use existing inert task/traversal seams to verify applicable origin returns, +and private carrier contracts for all fixed labels, nil/unknown no-ops, first +recognized joined cause, exact original pointer in Unwrap, errors.Is, unchanged +messages and panic-Error privacy. Exercise the actual cleanup producer helper +through inert JSON/status pipes and the existing receiver for every known +reason, legacy/unknown reason, wrong stage, empty error, unknown fields and +malformed/trailing records. No subprocess is needed for these contracts. + +Run focused process and existing fixture-classifier contracts, vet and the +configured affected-package lint plus unfiltered size/architecture gate. Two +independent plan rounds and frozen source review precede implementation +publication. The next required full gates provide the actual observation; +the metadata is diagnostic only and does not claim to fix runtime behavior. diff --git a/docs/plans/project-install-e2e/planner-project-intent-amendment.md b/docs/plans/project-install-e2e/planner-project-intent-amendment.md new file mode 100644 index 000000000..c45aa8d5e --- /dev/null +++ b/docs/plans/project-install-e2e/planner-project-intent-amendment.md @@ -0,0 +1,57 @@ +# Qualified project preparation and planner regression amendment + +2026-10-24. Root's actual assembled a9dd4402 domain/codec/publicProjectCLI gates +passed, but full planner exposed two old cases. Two independent plan GOs BEFORE +source/test edits; fresh focused tests and two independent code GOs afterwards. +This amendment does not treat all failures as obsolete assertions. + +## Actual diagnosis + +TestPreparationRequiresSupportedNativeUserTarget still expects every Kiro project +prepare request to fail. Kiro now explicitly qualifies project scope and inherits +its supported prepare intent. However the actual direct synthetic plan has only +ScopeProject + relative NativeRegistryRoot="native", with no target context, +physical artifact or active layout. ApplyInstallIntent currently checks only the +scope's allowed intent then Kiro.RefinePreparation checks nonempty native root; +that accepts a malformed context. Preserve this case as a real negative. + +The second old planner case uses Cursor project scope with no ProjectRoot and +expects PlanUnsupported/scope_not_supported. Cursor project is qualified now; +missing canonical context must fail validation. Preserve the undetected-user +case; use actually unsupported ChatGPT project scope with a real valid root for +the unsupported-scope negative, plus an explicit qualified-Cursor missing-root +error/no-resolved-artifact case. Do not remove unsupported or malformed tests. + +## Narrow source correction + +For prepare intent only, if either scope field is project OR TargetContext has a +nonempty ProjectRoot, ApplyInstallIntent validates matching project scope and +NewTargetContext BEFORE changing InstallIntent/refining readiness. This refuses +missing/relative/nonclean roots and user-labeled project context. Continue the +existing scope trait/compatibility checks; do not invent generic target layouts +or add a client-ID switch. Unsupported plans remain unsupported and user-only +legacy preparation semantics remain unchanged for genuine user contexts. + +Kiro.RefinePreparation, including direct calls, routes every project candidate +through its existing kiroProjectPlanRoot with the plan's original UserConfigRoot. +Require exact project/native .kiro root, physical ID and active artifact, preserving +user profile context; project preparation uses existing KiroProjectAction and +prepared/package-valid. It never writes native config, grants trust or attests +loading. This reuses the already reviewed project authority helper rather than +loosening its root/path contract. Keep ordinary user PrepareAction unchanged. + +## Meaningful regressions and gates + +Retain the malformed relative-root case and add missing/wrong native/root/scope/ +artifact/active variants, including direct refiner calls and no readiness change +on refusal. Add a valid qualified Kiro project prepare positive from real absolute +canonical fixture roots/exact layout, asserting prepared/package-valid and no +filesystem effects/original-user preservation. Keep unsupported ChatGPT project +and missing-root Cursor cases separate from undetected-user admission. Preserve +all existing compatibility/unsupported-package tests, user tests and goldens. + +Run complete planner plus targeted Kiro/intent/provider contracts and relevant +lint. Do not rewrite unrelated native guard or dependency snapshots; root's +separate Codex parser extraction resolves inherited closure drift. Checkpoint +small approved source increments to root immediately; root alone publishes and +merges only project feature stacks, never main/release. diff --git a/docs/plans/project-install-e2e/prepared-project-file-repair-verifier-plan.md b/docs/plans/project-install-e2e/prepared-project-file-repair-verifier-plan.md new file mode 100644 index 000000000..9679961eb --- /dev/null +++ b/docs/plans/project-install-e2e/prepared-project-file-repair-verifier-plan.md @@ -0,0 +1,262 @@ +# Prepared project files: preserve fresh verifier evidence during repair — R1 + +2026-10-24. Separate bounded core convergence plan exposed by the twice-reviewed +Qoder Prepared adapter acceptance path. Two independent plan rounds precede any +shared source edit; two frozen code reviews follow. The Qoder adapter and its +strong failing assertions remain intact. No generic source has been changed. + +## Actual result and exact call graph + +The composed Qoder provider contracts pass0.129s, including selected command and +skill assets, a closed importable view, neutral native discovery and a forbidden +runner. The public SDK single lifecycle passes2.224s for default and observer +compositions, but its initial test checked repaired bytes without rechecking the +saved verification enum. The actual saved TLS Git CLI journey's stronger check +fails after drift repair: exact pinned bundle is restored, activation remains +Prepared, native/user/B files remain independent, and saved verification is +package_validated rather than installation_verified. This is a lifecycle evidence +gap, not failed file restoration or proof of native registration. + +Current usecase path: + +1. Repair verifies current target/recorded ownership and resolves the caller's + exact source pin. A modified or absent managed package goes to repairPackage. +2. commitRepairPackage sets verifiedState.Verification=VerificationPackageValid, + then commitRepairDirectory performs the existing verified transaction and + persists the exact new managed package receipt plus real Mutated=true. +3. reactivateRepaired reactivates nativeLifecycleClient routes, or calls + verifyRepairedLauncher for UsesManagedStdioLauncher. A scoped LifecyclePrepared + route does neither, so its exact file verifier is never invoked and the + package-only state remains. Do not falsely declare native or stdio traits. +4. Intact repair goes through repairNative, activeNativeDelivery and + nonNativeRegistrationComplete (true for a file-only Prepared route), then + verifyClientReadOnly. That method first uses the static domain + ShouldReadOnlyVerifyForScope gate. LifecyclePrepared has no dedicated branch; + Qoder's inherited auth-hint branch only admits a nonempty executable. The + actual runner-free project contract has an empty native executable and does + not need a client binary. The existing actual VerifierAvailable capability is + not consulted to enable this file-only route, so intact repair after the + downgrade cannot restore fresh file-verifier evidence either. +5. Ordinary repeat add/update already invokes the real adapter and uses existing + verifiedPreparedObservationUnchanged only after exact receipt/lifecycle facts + agree. Preserve that guard and the separate SDK handoff correction. + +The public SDK requires an explicit PackageRoot; an advanced explicit root is +correctly rejected by its active-revision guard. The SDK test now retains that +refusal and supplies an immutable captured pin for repair. The saved CLI exercises +its actual source/cache resolver with a later advanced Git source. Do not remove +or relax either revision boundary. + +## Minimal correction + +Add one private operation predicate in usecase, with no client-ID switch or new +port/schema. It requires explicit project input and plan scope, a matching +project TargetContext with nonempty selected root and original user root equal +to the actual client, matching plan/client IDs, zero SelectedDelivery, and +scoped LifecyclePrepared with no tracked native effects. Require the existing +ActivationVerifierClassifier.VerifierAvailable for this exact client/plan/backend. +Missing or false capability must preserve the existing fallback; it cannot grant +verification. No path canonicalization, filesystem traversal, executable check, +new client discovery or authority inference belongs in this predicate. + +Use this predicate at precisely two existing seams: + +- In verifyClientReadOnly, permit the existing read-only activation path for this + positive file-verifier contract even when static ShouldReadOnlyVerifyForScope + is false. Keep the existing static gate and behavior for every other route. + Pass the exact recorded managed delivery/objects and VerifyOnly=true as today. +- After commitRepairDirectory succeeds, reactivateRepaired may use the existing + read-only verification/persistence shape for this contract. Name any extracted + helper for its actual read-only purpose rather than pretending that the bundle + is a stdio launcher. Pass the freshly committed delivery's exact active path, + owned base, artifact digest and managed receipt, current plan/context, unchanged + original client and preserved authentication. Request VerifyOnly=true and do + not create a native attempt or execute an acting activation path. Keep native + and launcher branches otherwise unchanged. + +Only the adapter's actual successful file verification may return Prepared plus +installation_verified; do not synthesize those values from directory existence, +previous state or a true capability boolean. Preserve the adapter's fresh actions +and remaining native import/discovery limits. Keep the new managed package +receipt and all existing attempt/intent fences. Never adopt arbitrary returned +native objects or convert a NativeEffectCommitted/Uncertain/unknown report into +an Unchanged claim. Preserve real mutation facts even if the verifier or state +persistence fails; a post-commit verification failure remains incomplete/error +with the repaired package retained and no fabricated native completion. Reuse +existing lifecycle/error machinery; a newly discovered effect/recovery protocol +gap requires another explicit plan rather than a shortcut here. + +User, project CLIRegistry, tracked NativeConfig, nonzero selected delivery, +unsupported/false-verifier and all other lifecycle behavior remain unchanged. +Grok's current project verifier reports unavailable, so this change must not turn +its source/package evidence into installation_verified or native discovery. +No flags, commands, classifiers, timeouts, workflows or user/native parsers change. + +## Required finite acceptance + +1. Strengthen the real Qoder SDK lifecycle assertions after every drift/missing + repair and reopen: exact immutable bundle, Prepared/installation_verified, + original binding identity/current managed receipt, useful fresh actions, + NativeDiscoveryAttempted=false/Reconciled=false and zero forbidden native calls. + Every repeat add/unchanged update/intact repair freshly verifies and leaves + exact state bytes and Mutated/NoChange truthful. +2. The existing actual saved TLS Git CLI journey must pass its unchanged strong + post-repair enum/bytes assertion after source advancement. Keep deps sync + --locked, distinct user and A/B state, host-free status, changed update and + exact removal/data preservation. Do not insert a separate add/update between + repair and its verification assertion. +3. Focused core regressions use an existing injected verifier seam to prove the + exact fresh delivery/context/receipt and VerifyOnly=true reach the verifier; + failed verification and failed lifecycle persistence keep real Mutated=true + and error, without new native ownership/attempt acknowledgement. Missing or + false capability, malformed scope/client/context, nonzero delivery, user, + CLIRegistry, tracked effects and actual acting requests cannot enter this + exception. A verifier declaring unavailable must not be called. +4. Keep real pending-attempt/recovery and known foreign/drift refusal tests; + successful file verification does not grant recovery eligibility or native + convergence. Existing Grok, user Qoder and launcher behavior stay unchanged. +5. Run only focused Qoder provider/SDK/CLI and core cases plus small architecture + and changed-source lint gates, with the shared pinned Go runtime/concurrency1 + after capacity is granted. No vendor process, MCP, trust/model/auth operation + or broad module suite is needed. Record unsupported local process prerequisites + honestly if the separately reviewed containment change is integrated. + +This correction qualifies installed bundle files only. Actual Qoder registration, +loaded selection, shared cache ownership, MCP, trust, authentication and restart +remain unqualified exactly as the native evidence and adapter plan state. + +## R2 — exact saved identity and no contradictory read-only success + +Root and Claude independently gave R1 architecture GO on exactb6106274 after +reading the plan and actual core call graph. Both required an explicit outcome +gate: the existing zero-selection read-only wrapper delegates to the adapter, +and correctIntactLifecycle otherwise ignores NativeEffect. A Committed report +with unchanged saved dimensions could therefore be misreported as NoChange. +This correction will not rely on VerifierAvailable alone to establish success. + +Distinguish applicability, valid authority and availability. The private helper +first recognizes an explicitly project-signaled route whose actual client's +scoped policy is LifecyclePrepared without tracked native effects. Any project +signal includes input/plan project scope, TargetContext project scope or a nonempty +ProjectRoot. For this candidate, mismatched scope/client/context, nonzero selection +or inconsistent saved identity is an error, never permission to fall through to +the inherited static executable/auth-hint gate. A valid candidate with missing or +false verifier capability performs no new verifier call. User and other lifecycle +routes retain their current branches and do not borrow this exception. + +For a candidate require input.Scope and plan.Scope and TargetContext.Scope all +project, plan.ClientID equal actual client, the already selected nonempty clean +project root and original UserConfigRoot equal actual ConfigRoot. AddInput has no +independent ProjectRoot field: use its existing plan/target-resolver authority, +without inventing another root or doing filesystem canonicalization here. Require +zero selected delivery. Saved binding must have matching client/project scope, +exact computed binding ID/active locator/physical artifact/native profile and +namespace/authority under the current plan, ActivationPrepared, no outstanding +NativeActivationAttempt or PendingNativeIntent. The managed delivery must match +the exact saved package path, digest and complete managed receipt set. The +post-commit branch uses the freshly committed binding/delivery held by the repair +session, not stale predecessor objects. This is additional matching of existing +facts, not adoption of supplied paths or independent digest verification. + +The new positive verifier route has a concrete success contract after preserving +existing authentication: no error, ActivationPrepared, VerificationInstalled, +PolicyAllowed, unchanged saved authentication, no activation/authentication +attestation, no AuthoritativeObservation, nil LocalEntryObservation, no returned +NativeObjects, and nonempty fresh UserActions. NativeEffect must be exactly +Unchanged or the existing empty legacy value for this declared nontracking +Prepared contract. Empty means not reported, not inferred Unchanged. No native +discovery observation is created. Grok's unavailable verifier still cannot gain +file-installed or runtime evidence. + +Before any lifecycle-success or NoChange classification, reject Committed, +Uncertain and unknown nonempty effects and every contradictory outcome above. +Never rewrite the reported NativeEffect or adopt its returned objects. A reported +Committed effect sets/retains result.Mutated=true even when an error is returned +or saved lifecycle fields are identical; no result may simultaneously claim +NoChange. Uncertain/unknown effects remain visible in the returned actual outcome +and an explicit error with NoChange=false; they do not manufacture certainty or +new ownership/recovery authority. Existing known mutation facts remain unchanged. +This is a contract-violation report, not native reconciliation or a successful +recovery. No attempt/intent is cleared and no automatic recovery is authorized. + +For an intact-package verifier failure, preserve the complete observed outcome in +the operation result and return error before correctIntactLifecycle or acting +fallback. Use a small private error classification if needed so existing +repairNative error handling cannot reapply or discard this exact route's observed +effect. Do not change errors for other routes. For a post-commit failure retain +Mutated=true, the exact repaired bundle and already committed PackageValid state, +and return incomplete/error without persisting a manufactured successful +lifecycle. A genuine adapter verification error retains its error cause. A failed +lifecycle persistence likewise cannot erase the actual package mutation. + +Both seam paths require focused contradictory-outcome regressions, including +Committed with identical lifecycle values and an actual inert effect marker, +Uncertain/unknown, native objects, LocalEntryObservation, both attestations, +authoritative negative observation, missing actions and wrong lifecycle/policy/auth. +Assert exact saved-state/receipt preservation, actual effect reporting and no +acting retry, plus post-commit mutation retention. Add explicit saved binding +scope/client/locator/digest/receipt mismatch and outstanding attempt/intent cases. +Retain the complete actual SDK and saved CLI journeys; strengthen the SDK's +post-repair binding assertions rather than accepting its earlier byte-only pass. + +Only the existing two verification seams and the precise error propagation they +require are in scope. No generic native-effect/recovery protocol, new domain +trait, interface, schema, launcher behavior or client-ID switch is authorized. +No shared production source has been changed. Both independent final R2 GOs are +required before implementation. + +## Final plan approvals before implementation + +Root and Claude independently gave final R2 PLAN GO on exact +3ffa62e1a8724d682148892b38e0a83debd5ac77 before shared source edits. +Both confirmed exact candidate/authority/capability separation, the strict real +outcome gate before correction, explicit committed-effect mutation retention, +no object adoption or attempt clearing, and post-commit failure truth. + +## Candidate verification before frozen code review + +The private core helper now validates the existing configured Service.ProjectRoot +against the selected plan root as part of the same frozen authority comparison. +Intact verification and post-commit verification use exact managed delivery and +saved objects. Typed refusals preserve the observed outcome and prevent both +repair's acting fallback and the existing authoritative-preview persistence path +from consuming a contradictory file-only report. No public contract, domain +trait or client-ID branch was added. + +Go1.26.10, GOMAXPROCS3 and -p=1 focused checks passed: core contradiction/identity/ +capability/persistence tests0.024s; Qoder composed provider tests0.139s; real public +SDK lifecycle2.010s; actual saved CLI/TLS Git lifecycle5.708s. SDK default and +enabled-observer compositions both use a forbidden native runner. Strong direct +post-repair Prepared/installation_verified assertions now pass after drift and +whole-package absence, with immutable-pin restoration despite advanced source. +Changed source update, state-stable repeat/intact repair, host-free status and +user/A/B/caller-data preservation are retained. Genuine raw Qoder global-list +bytes remain a conservative duplicate-ID user-parser refusal fixture. + +These are source/inert-client lifecycle checks, not new Qoder native runtime +qualification. Full/changed-source lint, platform compilation and independent +frozen code review remain separate gates; no vendor, trust, authentication, +model or MCP command ran locally for this implementation. + +## Independent code review: mutable packet correction + +Root's frozen c317 review identified a concrete input-alias risk: an injected +capability classifier or verifier could edit an ownership slice shared with the +frozen repair binding, then return an otherwise valid file result. The finite +regression against unchanged c317 production failed0.014s in all four combinations +of classifier/activator and intact/post-commit verification. The intact case +corrects PackageValid to Installed and can persist the altered receipt. + +The narrow correction gives capability classification and activation separately +cloned ownership/plan/observation/authority/client packets. A classifier's changes +cannot become the activator's authority. The returned transient guidance and +observations are copied as well. Existing immutable selected delivery and +profile namespaces keep their semantics; no schema or recovery protocol changes. +The complete focused PreparedProjectFile suite, including the four mutation +regressions and prior contradictory-effect/failure cases, passed0.026s after this +correction. No native command or model/MCP/trust action ran. Final independent +source reviews and exact-source static/platform gates remain required. + +## Final ownership isolation review and verification + +Root's independent review of frozen `c3170ac5` identified a real mutable input receipt alias. The added four-case regression failed on that unchanged source for intact lifecycle correction/post-commit verification and classifier/activator mutation. Correction `96117cd43064e50a4541387d5dc9a3aeddbdadeb` supplies separate copied classifier and activation authority/observation/ownership packets and freezes returned transient data; it neither adopts verifier objects nor changes state schemas. Root gave final full CODE GO after tracing the correction, and Skillshare independently gave full CODE GO for the entire Qoder/core source plus the later private Qoder-only mechanical delta. Exact final-source meaningful regressions, focused vet, configured changed-source lint, architecture/conformance and Windows SDK compile-only results are recorded in `qoder-project-lint-extraction-plan.md`. No vendor or native activation evidence is implied. diff --git a/docs/plans/project-install-e2e/priority-trusted-native-plan.md b/docs/plans/project-install-e2e/priority-trusted-native-plan.md new file mode 100644 index 000000000..56619da8e --- /dev/null +++ b/docs/plans/project-install-e2e/priority-trusted-native-plan.md @@ -0,0 +1,103 @@ +# Priority clients: separate trusted disposable native evidence + +2026-10-25. This amendment adds positive native observations without changing +the existing untrusted project tests or production trust behavior. It is +source-only qualification, with exact source/client identities and raw artifacts. + +## Authority and supported setup + +The owner requested complete project E2E, prioritizing Claude Code and Codex. +All execution retains the existing pinned client, disposable hosted-machine, +scanner, explicit environment and current-source matrix gates. Fixtures author +trust only for their own canonical A/B directories in the original disposable +user configuration, before recording preservation baselines. UAP never writes +trust; project directories never become HOME or native user profiles. + +Codex's [official configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference) +documents `projects..trust_level`. The exact pinned 0.153.4 source at +[`3d2ee51ca2d5db578f328aa75e20aa22c0197c9a`](https://github.com/openai/codex/blob/3d2ee51ca2d5db578f328aa75e20aa22c0197c9a/codex-rs/config/src/config_toml.rs) +defines `ProjectConfig.trust_level` and recognizes `TrustLevel::Trusted`. +Its [writer](https://github.com/openai/codex/blob/3d2ee51ca2d5db578f328aa75e20aa22c0197c9a/codex-rs/core/src/config/mod.rs) +uses `[projects.""]` with `trust_level = "trusted"`. The release tag +`rust-v0.153.4` was independently resolved to that commit. Both A and control B +are explicitly trusted, so B's absent project servers are not explained by +untrusted-config suppression. Actual pinned-client output must qualify this +setup; documentation alone never passes a native stage. + +Claude's [official permission guide](https://code.claude.com/docs/en/permissions#what-runs-before-you-trust-a-folder) +documents setting `projects[""].hasTrustDialogAccepted=true` in +`.claude.json`. Its [container guide](https://code.claude.com/docs/en/devcontainer#persist-authentication-and-settings-across-rebuilds) +confirms that explicit `CLAUDE_CONFIG_DIR` moves that file inside the selected +configuration directory. The fixture records this separately authored trust +input. Pinned 2.1.263 native listing/details must establish whether it works; +a suppression result remains a genuine blocker rather than a bypass candidate. + +## Codex MCP scope and minimum implementation + +Add a separate opt-in source test, leaving the skills-only untrusted test intact. +Reuse `nativeFixture`, `writePackage`, `nativeScriptedProvider`, pinned scanner +provisioning, `commandsRunInstaller`, `startNativeRPC`, `nativeDecodeFacts` and +`nativeAssertEvent`. No new acquisition, process, MCP server or model harness. +Expected size is approximately 180-220 Go lines: one lifecycle test, one session +helper and one small persisted-binding helper. Add the required source-only +matrix test name and its existing selection assertion; no historical lane changes. + +The fixture writes the ordinary user provider configuration with a loopback +provider and `requires_openai_auth=false`, plus canonical trusted A/B entries. +Both projects have real Git markers and remain outside HOME/CODEX_HOME. A starts +without `.codex/config.toml`; UAP creates the entire initial project document. +Keep independent user, B and A foreign sentinels, and hash them after every +public operation and native session. Do not seed any user MCP server, account, +credential, external endpoint or automatic plugin install. + +Public UAP adds the existing local `native-proof` stdio fixture into A with +project scope and no-save. Persisted exact scope/client binding, owned MCP +receipts and data receipt supply expected package/data paths. Fresh app-server +sessions initialize, start an ephemeral read-only thread and list MCP status. +Require the exact `default` and `explicit` native configuration names once, +empty pluginId, complete pagination and declared inspect_runtime tool. Direct +tool calls carry a random nonce and correlate initialize/tools-list/tools-call +events; compare revision, project managed root, recorded data path, default and +explicit cwd, exact args and literal/substitution bytes. A user plugin cache +path cannot satisfy the assertion. Do not reuse `nativeSession`'s user-plugin +identity/cache assumptions. Never start a model turn; require zero captured +Responses requests throughout. + +Drive initial add, unchanged add/update and removal. Retain the tool's bounded +marker through replay and after removal. Replay requires mutated=false plus +exact state/config bytes. Saved activation remains Prepared even after the +separate native observation. The existing project component test independently +covers changed update and missing-skill repair; this narrow added test does not +claim trusted MCP runtime replay across update or missing-component repair. + +Run fresh B sessions before/after A changes and fresh A/B sessions after removal. +Require no project fixture servers and no new nonce-correlated MCP events in +those absent cases. Hash original user/B/foreign bytes after each boundary. +Record exact source/tree/client/installer identities, authored trust input, +native RPCs, event receipts and explicit evidence limits. MCP initialization and +direct fixture tools can pass; real model behavior, OAuth, arbitrary policy, +external services, network isolation and release qualification remain untested. + +## Claude positive discovery scope + +Add a separate opt-in trusted project listing/details test using +`newClaudeProjectNativeFixture`, the public installer and existing bounded +diagnostic capture/skill-byte assertions. Seed the exact A trust entry in +`ConfigDir/.claude.json` before baselines. Trust both A and control B; B uses the +same original profile and has no A plugin. Preserve existing disabled foreign +user/A/B entries. + +Require fresh exact enabled project identity/version/canonical installPath in +A, exact native plugin details and rendered guide/ordinary-command bytes, no A +entry in B, and removal followed by fresh A/B absence. Record scope explicitly; +a user-scoped same-name plugin is not project proof. Preserve user/B bytes and +the authored trust entry. Estimated size is 100-150 lines reusing current +fixture and capture helpers. No session, model, MCP initialization or trust-dialog +execution is claimed. Existing suppression and failed/indeterminate gates stay. + +## Review and qualification + +Two independent plan rounds precede code. Review frozen changes and meaningful +parser/selection contracts, then publish immediately. Genuine macOS/Windows +native artifacts on the assembled SHA, independently interpreted with hashes, +qualify results. A local compile or a historical client pass is insufficient. diff --git a/docs/plans/project-install-e2e/proc-task-release-observation.md b/docs/plans/project-install-e2e/proc-task-release-observation.md new file mode 100644 index 000000000..8da233889 --- /dev/null +++ b/docs/plans/project-install-e2e/proc-task-release-observation.md @@ -0,0 +1,137 @@ +# Prove task disappearance after a kernel release snapshot + +## Observed failure and source evidence + +The one provider-parity observation failed on source +`fa59fd25dcac087296a8a95ed0ee2fee5d1ec7b5`, tree +`42fed9b76c7e0a5cd4e43a752c95463546303bfd`, run `40199085334`, job +`121186190348`. The actual GitHub-hosted Ubuntu 24.04 image was +`20261012.334.1`. The unchanged bounded test stopped at its first failure, +fetch iteration 497, after 102.598 seconds. Its controlled original process +error ends with `malformed proc task stable identity`; the fixed class is +`ordinary_supervisor_descendant_cleanup`. The earlier Ubicloud 512-pass +observation remains a separate result. + +This identifies the task-stat stable-identity parser's refusal. The raw stat +record was not logged, so its exact numeric fields are inferred from the +producer, not captured evidence. Do not assign this origin to the earlier +unclassified Core normalization failure or claim the earlier parent-change +hypothesis was observed. + +The Linux producer admits a valid release snapshot that the parser currently +rejects: + +- [Linux v6.8 `do_task_stat`](https://github.com/torvalds/linux/blob/v6.8/fs/proc/array.c) + (blob `34a47fb0c57f2570a4f7cb1f45373ddaf2afa883`) initializes parent to zero + and process group/session to minus one. It replaces them only after obtaining + the task's signal-handler lock. The task state is sampled earlier, so that + state alone cannot classify the later snapshot as released. +- [Linux v6.8 `__lock_task_sighand`](https://github.com/torvalds/linux/blob/v6.8/kernel/signal.c) + (blob `c9c57d053ce4f64d9a832f358b4e1ee837959b8b`) can return no lock after + the task's signal-handler pointer becomes null. +- [Linux v6.8 `__exit_signal`](https://github.com/torvalds/linux/blob/v6.8/kernel/exit.c) + (blob `dfb963d2f862ada6a2f06259c6df4923272cb218`) unhashes the process + before clearing that pointer. [Linux v6.17's stat producer](https://github.com/torvalds/linux/blob/v6.17/fs/proc/array.c) + (blob `d6a0369caa931ebd6baa3fbb01612d04ddf9d3bf`) retains the same stat + defaults and lock boundary. These are pinned producer examples, not a claim + that the hosted job exposed either exact kernel version. + +## Narrow correction + +Change only `osrunner_ordinary_proc_linux.go`, a new inert regression test file +beside it, and this plan. Build on the reviewed fixed-origin diagnostic slice. +Do not change traversal, pidfds, group signaling, quiescence, process limits, +timeouts, profiles, command/environment construction or native admission. + +1. Retain the existing bounded read and strict TID, state, field-count and + numeric parsing. All ordinary accepted identities and malformed failures + retain their existing behavior. Only after that parsing succeeds, recognize + the literal canonical parent/group/session tuple `0`, `-1`, `-1`, with + successfully parsed TID/start time and a complete modern stat record (at + least the 50 fields beginning with state, through the exit-code field). + Every numeric field in this special record must be canonical decimal: + no leading plus, redundant zeroes or negative zero. Other negative groups, + noncanonical tuple spellings, truncated records and malformed values remain + ordinary errors and receive no new disappearance authority. +2. Return a private typed release-observation error carrying only the parsed + stable identity. Its error text remains exactly + `malformed proc task stable identity`. It is not ENOENT, ESRCH, unsupported + containment or a successful identity. Do not wrap a gone sentinel. Admit + this new marker only by its direct private nonnil pointer type; wrappers, + joins and unrelated errors with the same text do not acquire its authority. + Detect a private marker nested in a wrapper or join before entering the + existing gone-error path. Return a private opaque uncertainty error for that + whole input, including marker-plus-ENOENT/ESRCH combinations. The uncertainty + carrier retains the exact original error object and delegates its text but + intentionally exposes no `Unwrap` or `Is` method: the caller's existing + `linuxProcessGone` must never mistake that uncertainty for disappearance. +3. At the existing identity-error boundary, allow exactly one independent + `taskExists` observation for this marker, in addition to the unchanged + existing gone-error path. Only a positively absent task directory permits + the existing gone result. A still-present directory returns the original + release error unchanged. A permission or unknown directory-observation + failure remains failed and retains both the release cause and the new + observation error. Join the direct release marker with an opaque uncertainty + carrier holding the observation error; do not expose an observation errno + through the public unwrap chain. The marker remains discoverable as the + original parse cause, while the exact observation object remains available + in its private carrier. This also protects against a broken observer + returning an error that wraps or joins a gone errno. The production + `linuxTaskDirectoryExists` already normalizes ordinary missing-directory + errors to `(false, nil)`; document and test that contract separately. + Existing unrelated gone-error handling and `linuxProcessGone` itself stay + unchanged. Do not retry a stat read, sleep, or treat uncertainty as an empty + children list. +4. If the marker follows a valid initial identity and a missing children + interface, compare its carried PID/start time with that initial identity + before the directory observation. Preserve the existing changed-identity + refusal even if the replacement task subsequently disappears. The first + identity-read path has no prior identity to compare and still needs the + same independent absence proof. + +The marker alone never proves safe completion. The surrounding supervisor +continues to discover adopted children, retain pidfd ownership, wait for its +existing three clean empty observations and fail on every unrelated unknown +or containment error. A released current locked thread still fails ordinary +preflight; it cannot authorize command launch. + +## Regression and validation + +Use per-call inert proc operations; run no child command locally. Demonstrate +that the old implementation refuses the exact released-record-plus-absence +case, then require the corrected implementation to pass it. Cover both first +identity and missing-children/second-identity paths, including same identity, +changed start time with an absent directory, present directory, permission and +unknown observation errors. Assert exact call counts and path/size bounds. + +Also cover direct-marker admission, non-gone/error-text compatibility, wrapped +or joined marker refusal, same-text unrelated errors, wrong TID, truncated or +nonnumeric fields, noncanonical decimal spellings, other negative groups and +invalid start time. Unknown malformed input must not call `taskExists` at all. +Include marker-plus-ENOENT/ESRCH inputs and wrapped/joined gone observation +errors: assert no directory-based acceptance, exact retained cause objects and +`linuxProcessGone == false` through the real contextual wrappers/joins. Test +the real directory observer's absent/present/error normalization with inert +temporary filesystem entries. Error classification must not read arbitrary +error text, including through the opaque uncertainty carrier. +Preserve the current valid/live, missing-interface, changed-identity, genuinely +gone and prelaunch-refusal contracts. Verify that failed observations retain +their exact cause and do not become unsupported containment or disappearance. + +Run the focused new and existing inert process contracts, vet, configured +affected-package lint and the unfiltered size/architecture gate. Keep the +previously reviewed diagnostic, OpenCode and native fixture source intact. +Two independent plan rounds precede coding; two independent frozen code +reviews precede publication for hosted execution. + +After publication, advance the focused branch once from its observed failing +head `fa59fd25dcac087296a8a95ed0ee2fee5d1ec7b5` to the reviewed corrected +source using an expected-head lease. Run the same immutable +`f579c6b68c5d625e9e87e4e51b288990ddfcd1a4` test once on the unchanged +`ubuntu-24.04` workflow: 512 serial fetches, first-error stop, unchanged +environment, containment and all deadlines. Record the actual new source, +tree, provider/image, named result and no-skip status. This is validation of +a substantive parser correction, not a retry of the failing source. Preserve +both prior observations and do not claim that one finite pass proves absence +of every process race. The final integrated candidate still requires all +normal Core/Required/Coverage and native-client gates. diff --git a/docs/plans/project-install-e2e/project-components-code-review-r1.md b/docs/plans/project-install-e2e/project-components-code-review-r1.md new file mode 100644 index 000000000..169143e0b --- /dev/null +++ b/docs/plans/project-install-e2e/project-components-code-review-r1.md @@ -0,0 +1,83 @@ +# Shared project component implementation independent review R1 + +Reviewer: sources_lead, 2026-10-23. Reviewed new shared-only implementation +`91738de1f0245d2ba00b991c6e00cfde07d17675` plus corrective +`5e987e979661cde95d178f7d2eba99cd6a7cafe1`, read-only for all shared helper +files. The reviewer is implementing the independent Codex consumer, not this +helper. The helper files in the Codex integration tree are byte-identical to +the reviewed shared commits; no reviewer helper edits were made. + +## Decision + +**R1 GO at corrective 5e987e97.** No distinct confirmed implementation blocker +remains. The other reviewer's portable-spelling finding is closed by the +corrective commit: padded logical names are rejected before the historical +leaf-ID validator can trim them, and source-relative components use the +existing portable NFC/UTF-8/reserved-name grammar. + +## Independent component assessment + +- The public boundary matches the twice-reviewed amendment: explicit project, + skills and config roots, a caller-selected codec and declared ownership kinds; + prepared server values and exact desired receipts; no payload loader, + transport mapper, settings/trust engine or native process dispatch. Existing + client implementations remain unchanged. +- Layout and object validation enforce exact clean paths, strict project + containment with no-follow ancestor checks (including contained aliases), + known kinds, exact kind:name IDs, unique identities and nonempty digests. + Active package roots and portable skill source-relative directories are + independently validated. Infrastructure package ownership is deliberately + an adapter/core concern; adapters must remove only that known receipt before + passing native component objects to this closed helper. +- Prepared server maps must exactly cover desired MCP names: both cardinality + and lookup are checked. Caller-reported server values are reprojected through + Kernel DesiredReceipt and compared to the staged digest/path before any native + mutation. An extra, missing or mismatched prepared server cannot silently + change the selected component set. +- Previous ownership is verified before mutation, allowing missing entries only + for repair/removal. New same-name native objects collide. Missing same-pin + skills are staged and recreated; equal receipts skip only an actually present + real directory with the exact digest, after independently checking the active + source bytes. +- Skill staging uses private same-filesystem directories, records their inode + identity and exact contents, verifies staged bytes, exclusively moves old + objects to backups and rechecks isolated digests. Installed desired skills and + cancellation are checked before the single nativeconfig batch. +- Native MCP edits retain existing writer locks, complete-entry receipts, + selected-path DesiredReceipt checks, conditional replacement, rollback and + postwrite verification. Distinct skill/config roots and a different format + such as Codex TOML are ordinary layout/codec inputs, without borrowing a + Cursor path or JSON serializer. +- Failure rollback removes only newly installed directories that remain exact, + restores exact backups without replacing a competing destination, and retains + the recovery root when a destination, backup, private inode or unknown private + entry changed. Safe observed failures may report unchanged; unknown MCP commit + failures remain uncertain even after skill restoration. +- A successful batch or CommittedCleanupError remains authoritative. Cleanup + checks the private identity, complete entry inventory and each remaining + directory digest before deletion; cleanup degradation cannot roll back + committed config or erase externally changed recovery bytes. No data or + foreign native directory is adopted or removed. + +## Independent decisive execution + +At the corrected shared tree, `go test -p=2 -count=1 ./clients/shared -run +'^TestProjectComponent'` passed **0.081s**. The selected contracts exercise +complete add/no-op/missing-pin repair/update/repeat-remove, extra/missing server +maps, staged-source/relative-path/receipt tamper, native aliases, portable +spelling, MCP failure after skill swap with exact restoration, externally edited +install preservation with retained recovery, changed/unknown committed backup +cleanup refusal, and cancellation before mutation. + +The independent in-progress Codex consumer additionally passed its production +loader/planner/renderer/stager/provider contracts **0.070s**, using actual +`.agents/skills` and `.codex/config.toml` roots with CodecCodex. This checks +helper interoperability with a distinct TOML config root, repeat verification, +missing-pin recreation, exact removal, foreign user/cache/project-B preservation +and conservative prepared status. The Codex adapter itself still requires its +own implementation reviews; that consumer result is not an adapter GO. + +No package payload, native agent session, configured MCP command or model was +executed. Native product qualification and final integration CI remain separate. +No corrective request beyond the independently discovered portable-spelling +fix was necessary in this rotation. diff --git a/docs/plans/project-install-e2e/project-components-code-review-r2.md b/docs/plans/project-install-e2e/project-components-code-review-r2.md new file mode 100644 index 000000000..1bc97d39d --- /dev/null +++ b/docs/plans/project-install-e2e/project-components-code-review-r2.md @@ -0,0 +1,101 @@ +# Project component helper implementation independent review R2 + +Reviewer: project_core, 2026-10-23. Starting committed helper `91738de1`, final +frozen helper `f02663a56cf7b1cfd88f1b8fa70fb7aff64d1822`, including corrective +`5e987e979661cde95d178f7d2eba99cd6a7cafe1`. Reviewed only the committed new +clients/shared helper and contracts; uncommitted Cursor implementation is +excluded. The original amendment and both independent plan reviews remain +separate evidence. + +## Decision + +**R2 GO at f02663a5.** One portable-spelling finding is independently verified +closed. The planned MCP-only/no-skill path gained a focused positive contract +at the final checkpoint. No confirmed implementation blocker remains in the +shared helper. Consumer adapters, their native codecs, project qualification +and hosted discovery still need their own review and evidence. + +## R2-1 — strict portable source and logical-name spelling + +At 91738de1, componentRelative rejected empty/dot/traversal/drive/backslash and +control paths, but did not apply the existing portable segment rules. Trailing +dot/space, Windows reserved device names, wildcard characters, invalid UTF-8 +and non-NFC names could pass this helper despite its frozen portable-child +contract. ValidateObjects and inspection called ValidateLeafID, whose legacy +internal trimming accepts padded logical names without changing the original +name later used in a physical path. + +**Closed in 5e987e97.** Each source-relative segment now passes the existing +pathpolicy.ValidatePortablePathSegment; original logical names must exactly +equal their unpadded spelling before leaf validation in ownership/inspection. +The negative contract covers the listed portable forms plus dot/traversal and +duplicate separators, and verifies padded ownership is rejected. No new path +grammar or compatibility exception was introduced. + +## Mutation and ownership assessment + +- Explicit selected project, source/staging roots, skills root and exact + config candidates are clean absolute no-follow project children. Desired + and previous ownership has exact kind/name/path/digest; unknown kinds and + duplicate IDs are refused. Source directories are checked inside the active + immutable package and digested before staging and after copying. +- Missing same-pin native skills are reconstructed. A no-op requires an + observed exact present directory, rather than only equal receipts or + allowMissing success. New skill/server names collide with unowned objects. +- Prepared MCP map cardinality and per-desired-name lookup jointly establish + exact name-set equality. DesiredReceipt is recomputed from the selected + codec/server/path and must match staged ownership before skill/config writes. + The helper does not read descriptors, expand placeholders, discover clients, + execute MCP, grant trust or substitute a user profile. +- Skills are staged/backed up/published on one filesystem with exclusive + rename and digest checks. The helper rechecks installed skills/cancellation + before one existing Kernel.ApplyBatch. No alternate native config writer or + solver is introduced. +- Prior exact whole-entry MCP receipts are checked before staging and again + by the kernel's locked mutation preconditions. Current kernel ErrCollision + and ErrNotOwned occur before config publication; complete observed skill + rollback can report unchanged in those cases. Other kernel failures remain + conservatively uncertain, including an unknown config commit state. +- Failed rollback does not overwrite/remove changed external installs and + retains exact old backups with a recovery location. Private cleanup checks + recorded physical transaction identity, exact known entry set and digests; + changed/unknown recovery material is retained. +- Successful config commit, committed lock-cleanup degradation and subsequent + private-cleanup refusal retain installed desired skills. Cleanup degradation + is CommittedCleanupError and cannot roll back committed native state. +- MCP-only input calls the existing kernel without creating a skill root or + private transaction. Removal tolerates missing exact owned objects without + consuming foreign disabled entries. Existing clients and user lifecycle + code are untouched by the committed shared-only diff. + +## Independent execution + +Ran exact frozen shared contracts with Go1.26.10, `GOMAXPROCS=3`: + +```sh +go test -p=2 ./install/integrationctl/agentplugins/clients/shared \ + -run '^TestProjectComponents' -count=1 -timeout=2m -v +``` + +**PASS0.058s.** Execution covers add/repeat/missing same-pin repair/update/repeat +remove; MCP-only add/update/verify/remove with no skill root; source/map/receipt +tampering before effects; contained skill/config aliases; strict portable +spelling; MCP failure restoring old skill; changed external install preserving +backup and uncertain effect; committed cleanup preserving modified/unknown +recovery material; and cancellation before mutation. Foreign disabled MCP data +is retained. + +No production or test file was edited by this reviewer. No real native client, +model, MCP process or hosted certification was executed. Architecture/whole-file +lint and consumer-specific end-to-end qualification remain applicable gates. + +## Whole-file gate follow-up + +Independently inspected `cc40930d866cf050c2036da3ceb0c70b8efb2b35`. The new +validateObjectPath function is an unchanged extraction of the existing +kind-specific exact path/source checks followed by clean-path and no-follow +containment checks. ValidateObjects still performs layout, unpadded/leaf name, +exact/unique ID and nonempty digest checks before that call, in the same order. +No policy, public API, error classification or mutation code changed. +**R2 GO remains valid through cc40930d.** Author's final exact changed-code and +whole-file gates report zero issues; no baseline was expanded. diff --git a/docs/plans/project-install-e2e/project-composition-code-review-r1.md b/docs/plans/project-install-e2e/project-composition-code-review-r1.md new file mode 100644 index 000000000..f3e9b68f0 --- /dev/null +++ b/docs/plans/project-install-e2e/project-composition-code-review-r1.md @@ -0,0 +1,67 @@ +# Cohesive project/source CLI implementation independent review R1 + +Reviewer: sources_lead, 2026-10-23. Reviewed checkpoint +`c11a071b17419025bf4c6214033c762c28d9036e` read-only. Independent execution used +clean frozen `91475b5120b4a21b92d87cacd15cb91d9ccd1248`, whose only changes above +the reviewed checkpoint are four Gemini compatibility golden scope arrays; +production code and reviewed test files are identical. + +## Decision + +**R1 GO.** No confirmed implementation or fixture-boundary blocker remains in +the selected source/project composition seam. This proves the implemented UAP +CLI, source acquisition, immutable storage, selected project authority and native +file transactions. It does not certify a native Gemini session, trust, MCP +initialization, model invocation or executable release. + +## Independent assessment + +- The integration correction is narrow and correct: `selectLockedLifecycle` + passes the selected App's `environmentID()` to `savedAliasInstallation`. + Exact installation-ID repair now resolves the project receipt rather than + entering the compatibility user-default lane. Adjacent ownership helper calls + retain their explicit selected environment. No portable source or agreement + identity is changed by scope. +- The fixture constructs the actual `newAgentpluginsCLIApp` runtime and wraps + its actual ForScope composer only to reapply bounded host detection/security + and loopback source boundaries. Public Cobra selection, loader, solver/lock, + planner, installer facade, environment/content stores, native kernel, data + ownership and transactions remain production implementations. There is no + alternate runtime or lifecycle adapter hidden in this journey. +- Real TLS Git and ZIP variants cover user, A and B installation of the same + package, unchanged locked synchronization, exact installed-pin repair after + upstream advance, moving update, selected removal and foreign preservation. + Source/binding identity stays stable, package revision advances only on + update, data remains in the selected namespace, and user/B state, native + entries, managed package and marked data remain unchanged. +- The same portable manifest/lock pair is byte-identical across scopes and + excludes local user/project/cache/repository paths. Content uses the shared + immutable user store; mutable project state/data remain under each .uap. +- Export/import use an explicit exchange pair and a genuinely fresh cache and + project namespace. Same-pair export refuses without writes. Changed original + ZIP bytes fail their exact SHA256 before native/state/intent/content + publication, without changing the exported agreement. +- Recovery reuses the existing dirswap fault seam and durable pending-journal + fixture. The public selected-project recover command observes and consumes + that journal, preserves committed native/state bytes and unpublished caller + staging, and repeats harmlessly. The evidence accurately describes this as + a transaction/journal fixture rather than a live native process crash. +- Negative scope/root/state-copy and native ownership contracts remain covered + in the separately reviewed core and adapter packages. This seam adds the + production generic Git/archive path without inventing a client-by-transport + cross product or broadening existing user behavior. + +## Independent bounded execution + +```sh +go test -p=2 -count=1 -timeout=2m -v ./cmd/agentplugins -run '^TestProjectComposed' +``` + +**PASS 8.872s**: both real TLS Git/ZIP lifecycle variants and both cold portable +agreement transfer variants. Lifecycle subtests passed 3.94s/2.36s; transfer +subtests passed 1.60s/0.95s. No package payload, configured MCP server or model +was executed. No broad suite was repeated because no additional concrete risk +remained after the decisive production composition checks. + +No corrective change was requested in this review rotation. The final remote +CI and the independent second composition review remain separate gates. diff --git a/docs/plans/project-install-e2e/project-composition-code-review-r2.md b/docs/plans/project-install-e2e/project-composition-code-review-r2.md new file mode 100644 index 000000000..c404e564e --- /dev/null +++ b/docs/plans/project-install-e2e/project-composition-code-review-r2.md @@ -0,0 +1,81 @@ +# Cohesive project/source CLI implementation independent review R2 + +Reviewer: core_clients, 2026-10-23. Reviewed composed implementation +`c11a071b17419025bf4c6214033c762c28d9036e` at frozen integration +`c96fd09604adc7bfeaad32f2b140e27ffb5d8342`, work/uap-project. The reviewed +production correction and all five project_lifecycle fixture files are +byte-identical between those commits. Later integration adds independently +reviewed core/helper work; this review does not repeat their separate reviews. +No source, test or production file was edited by this reviewer. + +## Decision: R2 GO + +No remaining confirmed blocker was found in the composed generic source, +scope/ownership, recovery or portable transfer seam. This approves the bounded +implemented CLI/filesystem journey; it supplies no native Gemini session, MCP +runtime, trust, model execution, cross-platform runtime or release proof. + +## Production correction + +selectLockedLifecycle now supplies app.environmentID() to savedAliasInstallation +instead of taking its compatibility user-default argument. This retains the +selected project ownership receipt when an exact installation ID resolves the +saved alias. Both alias and exact-ID lifecycle paths still validate the locked +selection, source/package identity, binding and selected environment. Scope does +not enter the portable source or manifest/lock identities. + +## Composition and source assessment + +- Tests construct the production newAgentpluginsCLIApp and retain its actual + ForScope composer. Every operation uses public Cobra scope/root selection. + The wrappers reapply only disposable detection, explicit inert security + evaluation and private loopback source transport. Installer, solver, + manifest/lock, immutable store, planner, native kernels and ownership/data + transactions remain production implementations. +- The Git variant uses real native Git HTTP-backend service through TLS; + native Git receives the fixture's private CA through its supported boundary. + ZIP uses the actual HTTP archive acquisition path and original-byte digest. + Neither variant bypasses TLS validation or substitutes a custom package + acquisition result. Security fixture boundaries are described honestly. +- User/A/B additions use the same source/tree identity and exact portable pair. + Persisted scope, environment ID, native profile and artifact/data locators + are checked against each selected namespace. Content uses the shared + immutable user store; mutable project receipt/data state stays in `.uap`. +- Locked no-op sync requires byte-stable state. After selected managed-package + drift and upstream Git/ZIP advance, exact-ID repair restores the installed + pin. Moving update alone advances selected A while retaining source/binding + and persistent-data authority. Removal consumes only owned native/package + objects and environment ownership; foreign settings/skills and data survive. + User and project B retain state, native/package bytes and data markers. +- Portability assertions inspect manifest/lock for every fixture machine/root + spelling. Export preserves the exact pair; same-directory export refuses + without effects. Cold import starts with absent cache and project namespace, + then reacquires the locked source/tree. Altered original ZIP bytes fail SHA256 + before manifest/lock/state/content or native publication, preserving the + exported agreement and foreign native configuration. +- Recovery reuses dirswap's existing fault seam and receipt format. The fixture + intentionally persists a pending intent for the interrupted transaction; + actual selected-project inspection/recover observes and consumes it, keeps + committed native/state bytes intact and preserves unpublished caller staging. + Repeat recovery is harmless. This is accurately bounded transaction/journal + evidence; it is not claimed as an abrupt real native-client process crash. + +## Independent bounded execution + +Go 1.26.10, GOMAXPROCS=3, `-p=2 -count=1 -timeout=3m`, frozen c96fd096: + +```sh +go test -p=2 ./cli/cmd/agentplugins \ + -run '^TestProjectComposed|^TestProjectCLI|^TestProjectRuntime' \ + -count=1 -timeout=3m +``` + +**PASS 9.098s.** This includes real TLS Git and ZIP full lifecycle, both cold +portable-agreement restores, ZIP original-byte refusal, selected scope/runtime +composition and CLI transfer/admission boundaries. No native Gemini process, +authored MCP command, package payload, model, trust or login operation ran. + +The original R1, author exact lint gates, separately reviewed source transports +and project core remain supporting records. Actual hosted platform and native +client outcomes must continue to be reported separately from this passing +production CLI/filesystem contract. diff --git a/docs/plans/project-install-e2e/project-composition-evidence.md b/docs/plans/project-install-e2e/project-composition-evidence.md new file mode 100644 index 000000000..5dbbc5efb --- /dev/null +++ b/docs/plans/project-install-e2e/project-composition-evidence.md @@ -0,0 +1,97 @@ +# Cohesive project/source CLI lifecycle evidence + +Date: 2026-10-23. Production source/project integration base: true merge +`936523458613cf3ae88a50179192cb2b913b10d6`, containing reviewed source transports +through `8b133b63`, reviewed Gemini project qualification and the shared core +review corrections. Independent review of the cohesive checkpoint follows this +author verification; this document does not substitute for that review. + +## Actual composition exercised + +The new `cli/cmd/agentplugins/project_lifecycle_*_test.go` contracts construct +the production `newAgentpluginsCLIApp` / `newScopedAgentpluginsCLIApp` runtime. +Every command passes through the public Cobra CLI and its normal scope/root +selection. The fixture wraps ForScope to reapply only host boundaries after +the production composer reconstructs the selected runtime: + +- Explicit disposable Gemini detection; the detected ConfigRoot remains the + same original user profile for both project operations. +- An inert security evaluator with absent signed-feed fixtures explicitly + disabled. Existing security qualification is separate; these tests do not + claim a production scanner or signed advisory service was exercised. +- Real source acquisition with a private disposable TLS CA and native Git HTTP + backend or ZIP download. The Go HTTP client trusts only the fixture's TLS + server; native Git receives the fixture CA through its existing supported + authentication/CA boundary. No insecure TLS option is enabled. + +The standard package loader, immutable digest store, dependency solver/lock, +scope capability policy, planner, installer facade, filesystem/native kernels, +ownership receipts, data lifecycle and transactions are production components. +The configured inert MCP server is never launched. No Gemini session, model, +native discovery, workspace trust or hosted certification is claimed. + +## Journeys and assertions + +Both real TLS Git and ZIP variants execute these journeys: + +1. Add the same saved package to user scope, project A and project B. All receive + the native skill and MCP configuration while preserving foreign settings and + skills. Scope, actual native profile, environment ID and artifact containment + are checked in persisted state. User remains active under its existing + behavior; project file-only success remains prepared/installation verified. +2. Compare canonical source identity and package tree across all environments. + The exact portable manifest/lock pair is identical and contains none of the + fixture's user, project, cache, repository or native profile paths. All use + the verified user content store; neither project creates its own store. +3. Allocate and mark persistent data independently in each namespace. Repeat + strict locked sync in A and require byte-identical receipts afterward. +4. Drift A's managed package, advance the remote Git commit/ZIP bytes, and repair + using A's exact installation ID. Repair restores the installed pin rather + than following the moving source. User and B retain their old pin. +5. Reuse the existing dirswap fault seam and durable pending-journal fixture + under A's actual `.uap` namespace. Inspect observes the journal; public + `deps recover --yes` consumes it and leaves committed package/native state + unchanged. Existing rollback semantics preserve unpublished caller staging; + recovery does not adopt it. A second recovery is harmless. +6. Update A to the advanced source. Its source/binding authority and data + locators remain stable, its tree/revision and native skill advance, and its + project readiness remains prepared/installation verified. +7. Remove A's saved alias. Exact managed/native objects disappear, environment + ownership is released, foreign configuration/skills and persistent data + remain, and user/B state, native objects, managed bytes and data markers are + unchanged. +8. Export A's exact portable pair to an explicit exchange directory. Export to + the same project directory refuses with no writes. Import from the exchange + directory with `--install --yes` into a fresh project and empty content cache + restores the same source/tree/pair using locked acquisition. +9. For ZIP, change the endpoint bytes after export and attempt a second cold + import. Original-byte SHA256 refusal occurs before native mutation or + manifest/lock/state/content publication; the exchange pair is unchanged. + +## Integration correction discovered by the journey + +The first exact-ID project repair reproduced a real missed ownership context: +`selectLockedLifecycle` called `savedAliasInstallation` without the selected +environment ID, allowing its compatibility default to select user-default. +The fix threads `app.environmentID()` through that call. The end-to-end test +retains the exact installation-ID selector which previously failed; it does +not replace it with a less discriminating selector. Other production calls to +the variadic ownership helpers were audited for explicit context. + +## Author verification + +Go1.26.10, `GOMAXPROCS=3`, `-p=2`, disposable roots, independent local TLS: + +```sh +go test -p=2 ./cli/cmd/agentplugins -run '^TestProject' -count=1 -timeout=3m -v +``` + +**PASS 9.815s**, including existing read-only scope composition/init/transfer/ +unsupported boundaries and both new composed Git/ZIP lifecycle/transfer tests. +Exact golangci-lint2.13.2 changed-code and whole-file size/architecture checks on +`cmd/agentplugins/...` and `internal/agentpluginscli/...`: **0 issues** each. +No lint baseline, ownership assertion or native hosted-only guard was relaxed. + +The existing core-fast Linux, macOS and Windows project-contract job discovers +these TestProject tests. Applicable remote CI and independent cohesive review +remain required before integration approval or any release claim. diff --git a/docs/plans/project-install-e2e/project-core-code-review-r1.md b/docs/plans/project-install-e2e/project-core-code-review-r1.md new file mode 100644 index 000000000..10a019550 --- /dev/null +++ b/docs/plans/project-install-e2e/project-core-code-review-r1.md @@ -0,0 +1,34 @@ +# Project core implementation independent review R1 + +Reviewer: sources_lead, 2026-10-23. Frozen cumulative implementation: `3ea24e30..3f421660c0d5856c9f951759ade176c393394e4d` (context/ownership contracts, scoped lifecycle authority, CLI composition and the final compile/lint corrections). Read-only review; no project implementation edits. + +## Verdict + +**R1 GO for the shared core implementation.** No remaining confirmed correctness or authority blocker found in this review. This verdict covers the shared context/namespace/policy/CLI seams, and does not certify the forthcoming client adapters or executable release. The representative cohesive project lifecycle and each client's native layout/runtime proof remain required when those stacks integrate. + +## Component review ledger + +| Component | Independent assessment | +| --- | --- | +| Target context and SDK admission | GO. Missing SDK scope remains user; project request and engine roots must agree before package reads. Operational context fields are JSON-private and do not enter source/lock identity. The original detected user profile remains distinct from the selected project and native registry. | +| Core policy and capability gating | GO. Project policy has an inert zero value; project admission requires declared scope and concrete project/native layout hooks. Effective lifecycle and backend families are explicitly scoped. Existing user traits/backend aliases and constructors remain unchanged. ChatGPT project requests are rejected before acquisition. | +| Physical namespace | GO. `.uap` is fixed beneath the selected physical project, contained mutable children are checked before access, existing project/namespace identities are pinned and rechecked before effect/recovery boundaries, and non-project engines cannot accept project requests. Namespace symlink/identity replacement tests exercise refusal. | +| State and recovery ownership | GO. Project state rejects user-scoped bindings/data and copied absolute locators from another project; project receipts use `project-default` and remain disjoint from user-default ownership. Native registry, package target, profile namespace and data locators are validated under the selected root. Existing recovery paths reuse the same selected engine and physical namespace checks. | +| Lifecycle and backend handling | GO. Scope/root reaches planning, staging, inspection, activation, deactivation, repair, groups and removal. Deactivation receives the persisted native registry and original target context rather than inferring a project from an artifact path. Project config lanes do not inherit user CLI registry/backend sibling behavior. Native identity kernel checks now use `plan.Scope`. | +| Production CLI composition | GO. `App.ForScope` composes independent mutable roots under project `.uap` and project-root intent/lock, while retaining original user detection and shared source/security/content caches. Every selected route preserves the selected App; unsupported migration/binding routes fail explicitly. No project composition/read-only inspection creates `.uap`. | +| Saved dependency and transfer seams | GO. Resolver/coordinator/observation/alias ownership use the selected environment ID. Signed Directory requests carry selected scope. Portable exchange is only manifest/lock; same-directory project transfer refuses before writes. New direct-source helpers must retain this selected App during the planned source/project merge. | +| User compatibility | GO by structure and focused regression evidence. Old `Observe`/user constructors remain wrappers, user-default identity stays exact, and new project policy is opted in only with a qualified adapter. Existing source identity schema is not changed by this core branch. | + +## Provisional observation closed without a code change + +The reviewer initially questioned ignored second results from `NativeRegistry` in target/authority capture. Signature verification establishes that the existing interface returns `(root, executable string)`, not an error; ignoring the executable in target-only resolution is intentional. Actual error-returning project target, precondition and contextual authority hooks propagate their failures. No interface change is requested. + +## Verification + +Independent focused tests cover project context JSON/private validation, selected environment ownership, project state-copy refusal, namespace symlink/replacement rejection, SDK cross-environment refusal before package access, production composition isolation, unsupported target refusal and same-directory transfer refusal. These run against the frozen correction checkpoint with Go 1.26.10, GOMAXPROCS=3 and the task-owned shared caches. Results are recorded below on completion. No external services, package payloads or real agent processes are run by this focused review. + +## Integration requirements + +Preserve source branch `identityFromLocked` on every shared store/cache/replay path, additive source fields in state projections, typed acquired-source validation and exact old GitHub identity. Preserve project branch selected scope/root on each new source lifecycle helper and repair request. The top-level source/project integration needs a cohesive generic Git/archive project run after the first qualified adapter lands; fixture core checks alone cannot establish that whole product journey. + +Focused independent test invocation passed all five packages: domain 0.003s; environment 0.012s; statev2 0.010s; installer 0.304s; production CLI composition 0.213s. **R1 GO confirmed on3f421660.** diff --git a/docs/plans/project-install-e2e/project-core-code-review-r2.md b/docs/plans/project-install-e2e/project-core-code-review-r2.md new file mode 100644 index 000000000..e57d93109 --- /dev/null +++ b/docs/plans/project-install-e2e/project-core-code-review-r2.md @@ -0,0 +1,95 @@ +# Shared project core implementation independent review R2 + +**Current status: R2 GO after `caa27b4f` corrective verification.** Both +findings below are closed; their initial rationale remains as review history. + +Reviewer: `client_plans`, 2026-10-23. Reviewed frozen shared core through +`49ef94ac614a204caaa9a59478df907a24cfecef`, its original plan and R1 review, +and the subsequent root-native authority correction `08b1604a`. Gemini adapter +implementation has separate independent reviews; uncommitted cohesive CLI +fixture files are excluded from this frozen core review. + +## Decision + +Two narrow corrections were identified and independently verified closed in +`08b1604a` and `caa27b4f`. No additional architecture or broad implementation +change is requested. Parent owns combined source/project and native client +qualification after the core/adapters are integrated. + +## Core-R2-1 — root-native registry accepted by planning could not persist + +The planner deliberately accepts `NativeRegistryRoot == ProjectRoot`, required +for native configuration such as workspace-root `opencode.json`. State's +`validateProjectBinding` applied strict-child containment to both package target +and NativeProfileRoot, rejecting the same registry root that planning admitted. +This would block project clients whose native configuration lives directly at +the repository root. + +Author confirmed the mismatch and corrected it in `08b1604a`: only the exact +NativeProfileRoot equality case uses the selected root's parent as its +no-follow containment base, after absolute/clean/non-root validation. Package, +data and native object locators remain strict project children. Tests accept +Save/Load of exact root-native authority and reject other project roots, +relative roots, project-root artifacts and symlink authority. **Closed** by +independent code and focused test verification. + +## Core-R2-2 — copied namespace mutex must retain shared physical authority + +Parent's actual CI vet identified the new embedded projectNamespaceGuard mutex +being copied by existing `projected := *e` preview code. Beyond vet, copied +guard state would split the session's pinned project/namespace identity when a +derived preview observes a new directory identity. The required correction is +one shared guard pointer initialized by New and retained by both preview copies +and WithTemporaryRoot, without a suppression or independent replacement guard. +Author corrected this in `caa27b4f`: New allocates the guard; shallow projected +views share it; WithTemporaryRoot retains the original pointer and immediately +revalidates its pins. Missing/uninitialized project guards fail closed. New +tests prove that a namespace first observed by either derived view remains +pinned for both observers, and that an already-replaced namespace cannot be +adopted by creating a temporary view. **Closed** by independent frozen code, +focused test and vet verification. + +## Component ledger + +| Core boundary | Review result | +| --- | --- | +| Context and capability admission | GO: absent SDK scope remains user; request/engine scope and physical project roots agree before package reads. Project admission requires explicit catalog qualification plus project and native layout ports. Unsupported/remote/manual-only requests fail before acquisition. Operational context is JSON-private and excluded from portable source identity. | +| User versus project profile | GO: detected ConfigRoot remains original user authority. Actual project NativeRegistryRoot is separately selected and threaded through plan, staging, observer, activation, exact binding checks, deactivation, repair and grouped lifecycle. Contextual physical capture returns existing authority tokens instead of substituting user ConfigRoot. | +| Scoped lifecycle and backend policy | GO: one opt-in declarative project policy supplies effective traits. Existing user traits/backend aliases remain unchanged; project Copilot/VSCode families are independent. Native-config project branches acquire attempt/recovery and launcher policy through scoped helpers. | +| Physical namespace | GO: fixed selected `.uap`, contained mutable paths, shared pinned project/namespace identities and rechecks at apply/recovery/mutation boundaries. Context/namespace construction does not create directories. | +| State and ownership | GO after Core-R2-1: project state refuses copied outside locators, user bindings/data, wrong profile namespaces and receipt paths; selected project and user environment IDs are disjoint. Exact root-native registry is permitted without broadening managed child locators. | +| Native attempt and recovery | GO: effect tracking uses plan/binding scope and actual NativeRegistryRoot; persisted attempts retain their binding scope. Namespace/physical profile revalidation remains inside existing mutation/recovery boundaries. | +| CLI composition | GO: each entry point selects the App before routing; ForScope composes independent project mutable roots and root portable intent/lock while retaining user detection and shared read-only/content caches. Unsupported binding/migration surfaces fail explicitly. Derived previews must share namespace authority as above. | +| Saved lifecycle and transfer | GO: selected environment ID and scope reach observation, coordinator, authorities, Directory eligibility, repair/update/remove/recover and public status. Export/import transfers only intent/lock and rejects same physical project-directory transfer. Source integration must preserve both selected App and identity-from-lock. | +| User compatibility and public metadata | GO by structure and existing independent R1 evidence. Zero project policy enables nothing; user wrappers and user-default ownership remain exact. Newly qualified adapter catalog scopes require explicit compatibility golden changes, handled separately in the Gemini stack. | + +## Independent execution and limits + +Exact Go1.26.10 focused context/project tests on the root-native correction +passed: domain0.005s; environment0.008s; statev2 0.014s; installer0.276s. +Planner/usecase packages reported no matching tests for that narrow regex and +are not represented as independent project execution proof. The clean Gemini +branch's post-49ef real merge independently passed provider project composition +0.084s, nativeconfig0.010s, statev2 0.008s and selected production CLI composition +0.031s. Author/root CI owns broader lint, full suite, platform and combined +lifecycle gates. + +No native agent process/model/MCP runtime was executed in this review. No core +production file was edited by the reviewer. The original user owner request +authorizes narrowly scoped project work; unrelated frozen behavior remains +outside this review. + +## Final frozen verification + +Reviewed `caa27b4fdd8f017f174b03224bf0f2d9aa0f05fb` read-only, cumulatively +including `08b1604a` and shared core `49ef94ac`. Independently ran focused +project namespace, preview and cross-environment SDK admission tests: +**PASS0.010s**. Independently ran installer `go vet`: **PASS**, no copied-lock +diagnostic. The earlier root-native state/context tests remain passing as +recorded above. Author's exact changed-code and full-file size/architecture +gates report zero issues for the frozen corrections. + +**R2 GO.** No confirmed core correctness, ownership or review finding remains +open. This does not certify uncommitted cohesive CLI fixtures, future adapter +stacks, hosted native clients or an executable release. Parent retains those +integration and applicable CI gates. diff --git a/docs/plans/project-install-e2e/project-core-plan.md b/docs/plans/project-install-e2e/project-core-plan.md new file mode 100644 index 000000000..48b57e2c0 --- /dev/null +++ b/docs/plans/project-install-e2e/project-core-plan.md @@ -0,0 +1,359 @@ +# Project-scope core: research and implementation plan + +Status: two independent plan review/fix rounds complete; parent implementation +authorization granted and the shared core contracts, scoped lifecycle and CLI +composition implemented. Initial research base +`2bf6c3d703640ffbecc14346996709b4aec36ddc`; contracts and hardcoded scope paths +rechecked at implementation base `3ea24e3014ddf6679ca86729c56f1069112b0d3a`. +Assigned worktree: `/workspace/scratch/2819fbb48645/work/uap-project`, branch +`feat/project-install-e2e`. This plan requires two independent +plan review/fix rounds and parent authorization before code changes. + +## Implementation checkpoint + +The original plan reviews remain recorded in `project-core-review-r1.md` and +`project-core-review-r2.md`. Shared contracts landed at `57bc1367`, project +receipt identity and CI at `3fc822da`, scoped lifecycle and namespace guards at +`f7267e03`, scoped CLI composition at `85da71ab`, and the verified compiling +checkpoint at `3f421660`. Independent implementation review R1 is GO for that +checkpoint; its evidence is in `project-core-code-review-r1.md`. + +The current correction preserves authoring command help and rejects the +installer-only `--project-root` flag before authoring execution. It also splits +the touched core and CLI functions to meet the existing whole-file size and +architecture gates, without expanding their baseline. Focused public-help, +attestation, scoped state and installer checks pass. Independent implementation +R2 is now GO after the root-native registry correction `08b1604a` and shared +preview namespace guard correction `caa27b4f`; its evidence is preserved in +`project-core-code-review-r2.md`. The cohesive project/source lifecycle proof +follows the first qualified client stack. Client qualification and native hosted certification remain +separate gates; this checkpoint does not claim those are complete. + +## Governing scope + +Read `AGENTS.md`, `docs/AUTHORING_CAPABILITY_PRESERVATION.md`, the owner decisions +at the start of `docs/STANDARD_FIRST_AUTHORING_ENGINE_IMPLEMENTATION_PLAN.md`, +`docs/plans/uap-unified-dependency-manifest.md`, and +`docs/plans/uap-all-agent-dependencies.md`. Preserve useful legacy source and +tests, the one standard engine, current source/release distinctions, and immutable +historical release evidence. The parent's current owner instruction supersedes +old hosted-model requirements. Parent confirmed the owner's request for all +supported agents narrowly reopens VS Code for this project-scope work. Preserve +its unrelated behavior and freeze; the VS Code client lane owns its adapter, +dedicated tests and qualification. Core may add shared scope/context contracts. + +Deliver explicit project scope for the existing dependency lifecycle, then +qualify each client in a separate reviewed stack. Reuse the current TOML decoder, +graph solver, lock schema, immutable store, digest/security gates, state receipts, +and filesystem/native transactions. No new solver, sandbox, automatic host trust, +HOME substitution, path dependency language, or publication belongs to core. + +## Research findings + +* `cli/internal/agentpluginscli/root.go` rejects project scope after recognizing it. + `deps_commands.go` separately requires user scope. Saved add, status, sync, + repair, remove and transfer paths use `ScopeUser` and `EnvironmentUserDefault`. +* `installer.Request` has an explicit user `ClientConfigRoot`, but no scope or + project root. Prepare, confirmation, group and selected lifecycle code hardcode + user scope; selection and recovery must be changed together. +* `domain.PlanRequest.Scope`, binding scope, `ComputeClientBindingID` and data + receipt scope already exist. `clients.TargetLayout.TargetRoot` has no scope or + project context. `planner.ResolveTarget` checks scope but resolves a user path. +* Native staging/activation already receives the complete `DeliveryPlan`. This + is the appropriate place to carry an explicit project root without changing + the meaning of detected user profiles or global process environment. +* `environment.Coordinator` and `Observe` hardcode `user-default`; these are + environment ownership policy, not solver or lock-format requirements. +* `cli/cmd/agentplugins/compose.go` currently composes one user namespace. Most + paths already derive from that namespace, while `EnvironmentStoreRoot` is + independently configurable. This supports isolated project mutable state with + the existing immutable store shared across scopes. + +## Contract and storage decisions + +1. `--scope` remains `user` by default. Add `--project-root PATH`, valid only with + `--scope project`. Project scope defaults the root to the invocation working + directory; there is no implicit Git-root or parent-manifest search. The CLI + canonicalizes an existing directory once before effects. SDK inputs require + an explicit absolute clean project root. Refuse filesystem root, unsupported + scopes, conflicting context, and unsafe aliases before acquisition/mutation. +2. Project intent lives at `/uap.toml` and `/uap.lock`, with the + existing schema and semantics. Both are portable. Absolute project/profile/ + cache/state paths never enter manifest intent, canonical source identity, + lock bytes, or package pins. A package's own TOML keeps its existing package + meaning when acquired; the selected root operation determines environment + meaning. +3. The local mutable namespace is `/.uap/`: existing `state-v2.json`, + `mutation.lock`, `environment.lock`, operations/journals, managed projections, + installer temporary files, and retained `plugin-data` all derive from it. + Use `project-default` for receipts within this isolated namespace; keep + `user-default` unchanged in the current user namespace. No global project + registry or path-derived portable environment identifier is needed. + Before reading/creating mutation authority, use the existing path-policy + primitives to check `.uap/` itself and every existing owned descendant + without following symlinks, junctions or reparse aliases. Reject namespace + aliases/ancestor escapes such as `A/.uap -> B/.uap`; string-prefix containment + is insufficient. Bind to the selected canonical project directory and + revalidate existing namespace identity immediately before apply/recovery. + Do not add a global registry or new persisted portable directory identity. +4. Share the existing user `store` through `EnvironmentStoreRoot`; acquisition + and feed caches retain their current user-root policy. Their content is not + installed ownership. Project removal never deletes immutable store content, + another environment's receipts, or retained data by default. Document `.uap/` + as local generated state; do not silently edit an existing `.gitignore`. +5. An unchanged user operation preserves current paths, receipt identities, + authority and output behavior. A, B and user have independent mutation and + environment locks. Shared content-store concurrency keeps its existing + digest-verified publication rules. + +## Stable client interface (core owns these additions) + +Add this operational, nonportable DTO in `domain/target_context.go`: + +```go +type TargetContext struct { + Scope InstallScope `json:"-"` + ProjectRoot string `json:"-"` // empty for user; canonical for project + UserConfigRoot string `json:"-"` // original user profile; never HOME +} +``` + +`UserConfigRoot` always equals the original selected `DetectedClient.ConfigRoot`. +It may be empty only when the client's existing user contract permits that. +It is never rewritten to a project directory. Context is excluded from public +JSON and portable formats. Add `TargetContext` to `clients.PlanInput` and +`domain.DeliveryPlan` and `domain.DeactivationRequest`; preserve existing public +`DeliveryPlan.Scope` and require them to agree. Also carry context through +read-only binding facts/target facts that construct lifecycle requests. Add +`ProjectRoot` to `domain.PlanRequest`. An internal helper +validates/constructs context from request scope, project root, and selected client. +All added containing `TargetContext` fields and operational `ProjectRoot` fields +also carry explicit `json:"-"` tags, or stay behind current dedicated private +projections. Existing status fields may report selected scope and operational +paths intentionally; the new DTO does not widen public JSON by default. + +Keep the current user-only `clients.TargetLayout` interface unchanged. Add: + +```go +type ProjectTargetLayout interface { + ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, + managedRoot string, target domain.TargetContext) (anchor, root string, err error) +} +``` + +The planner calls this interface only for project scope; the entry-boundary +capability gate below rejects missing project hooks before acquisition. There +is no user-layout fallback for +project scope. Project adapters implement this method and may use existing plan +hooks, with `PlanInput.TargetContext`, to select native registry/config paths and +truthful component support. Core does not choose host-specific directories. + +Project adapters must also implement the existing `NativeRegistryLayout`, using +`PlanInput.TargetContext` to choose the exact project registry/config root. +The user-profile default is forbidden for project plans. Core validates the +selected native root and supplies it consistently to activation, verification +and deactivation. Add `NativeRegistryRoot` to `DeactivationRequest`; reconstruct +it from the configured namespace plus exact recorded binding and compare the +current deterministic layout before dispatch. Adapter mutation code must use +this explicit native registry rather than the preserved user's ConfigRoot. +Registry structural authority, when required by the adapter, is captured and +revalidated for that actual native registry independently of user detection. +For adapters already opting into durable physical authority, add one optional +`clients.ProjectPhysicalAuthority` hook in `clients/detection.go`: + +```go +CaptureProjectAuthority(ctx context.Context, client domain.DetectedClient, + target domain.TargetContext, nativeRegistryRoot string) (domain.ProfileAuthority, error) +``` + +It returns the existing token; reuse existing `RevalidateProfileAuthority` after +checking token root, scope, context and namespace agreement. Preserve current +user capture/revalidation APIs. Never manufacture a DetectedClient whose +ConfigRoot is the project. Pass the actual native project root to existing +`ValidateBindingProfile(root,binding)` and let the qualified client lane adapt +that validator only where its current policy assumes user scope. Adapters that +do not use durable physical authority retain exact native-object/path ownership +checks; project scope does not automatically opt every adapter into a new token. + +Current static domain capabilities list user scope only. Each qualified client +stack owns adding `ScopeProject` to that client's declarative scopes, alongside +both project layout and native registry hooks. Add one shared read-only +`installer.ClientScopeSupported(registry, id, scope)` policy: user preserves the +current saved-client support policy; project additionally requires the declared +scope and both registered hooks. Planner and Discover use the same declaration +and hook requirements, without a second capability registry. The CLI invokes +this selected-scope check before source acquisition in add and saved dependency +entry paths (including every selected manifest target), and installer Prepare +checks it before loading/sealing local package bytes. Package/component-specific +admission remains in the existing planner after loading. A counting-acquirer +test proves unsupported scope makes zero acquisition calls or installed effects. +Core does not pre-advertise any unqualified client or remote-only ChatGPT. + +Client R2 identified shared policy consumers beyond layout. Add declarative +`ClientDefinition.ProjectPolicy` (lifecycle kind and physical backend family) +and `SelectedDelivery.EffectiveTraitsForScope`, plus scope-aware backend family/ +sibling/equality helpers. Qualified client stacks fill only their policy row. +Direct project components use the existing native-config attempt/recovery +policy; real project CLI plugin delivery retains CLI behavior. User helper +behavior remains unchanged. No project policy falls back to user traits or a +historical TEST selected mode. Distinct project Copilot/VS Code registries do +not share ownership merely because user backend families match. Core updates +native kernel admission, attempts, readiness/projection, profile checks, +repair/remove/recovery, group selection and data consumers once. Cover an +interrupted CLI-user/native-project edit and independent project sibling MCP +entries, with a user-sharing regression. + +Use existing activation preflight to reject a positive completion flag that +contradicts known blocked/disabled project evidence. Cline additionally rejects +durable completion of session-only project MCP before the generic shortcut. +No new attestation framework is needed. ChatGPT has no native project scope: +both saved and `--no-save` project requests reject before acquisition, retaining +existing user/manual preparation. VS Code's qualified project product adapter +is selected only in the project App registry; user `vscode.New` and TEST +`NewLocal` remain unchanged. Scoped admission may qualify that project adapter +without changing the historical user saved-dependency deferral. + +Add explicit `Scope` and `ProjectRoot` to `installer.Request`; empty scope retains +the established SDK user default. `ClientConfigRoot` retains its user-profile +meaning. Add `ProjectRoot` to `installer.Config` and `planner.Planner` so the +existing `DeliveryTargetResolver.ResolveTarget(ctx, client, scope, artifact)` +can reconstruct the same project target during repair, remove and recovery. +Project requests must match the engine's configured project root; a user engine +cannot accept a project request. All internally generated `PlanRequest`s carry +the selected scope/root. `Plan`, binding facts, inspect and recovery retain the +actual scope. Group requests, if used, select one common project root; existing +group client restrictions remain unchanged. + +This deliberately keeps existing ports and user adapter interfaces stable. +No client should encode a project directory in `ClientConfigRoot`, rewrite HOME, +or infer the root from the active package path. A client may require separate +native config authority; it must capture/revalidate that authority using the +actual project path and the existing transaction boundaries. + +## Ownership and implementation sequence + +| Owner | Files and responsibilities | +| --- | --- | +| Core | `domain/target_context.go`, `domain/planning.go`, operational context/native-registry fields in `domain/clients.go`; `clients/planning.go`; planner context/layout selection and matching target resolution, including deactivation. | +| Core | `installer/{types,config,prepare,apply_confirm,group,selected_lifecycle,engine,target_facts,identity}*.go` as needed: eliminate hardcoded selected user scope, validate request/engine context, retain exact-binding recovery. Do not refactor unrelated lifecycle policy. | +| Core | `environment/{model,coordinator,observe}*.go`: add `Coordinator.EnvironmentID` with empty-to-user compatibility and `ObserveForEnvironment(view,id)`; keep `Observe(view)` as current-user wrapper. Scope every generated authority and receipt lookup. Solver/portable lock contract stays unchanged. | +| Core | `cli/cmd/agentplugins/compose.go`, `agentpluginscli/{app,root,deps_*,saved_*,install,add,update,repair,remove,list,info,outdated,doctor}*.go` only where selected runtime/scope is needed. Add scoped app composition; retain source acquisition, loaders and user-profile detection. | +| Client lane | Its `clients/` adapter and dedicated fixtures/tests; adding that client's qualified `ScopeProject` declaration; narrowly shared rendering/native-config code only with core coordination; host docs/evidence; scope-aware skills/MCP plus supported native components. | +| Parent | Root review coordination, recording the narrow VS Code owner exception, integration/stack merges, final documentation/support artifact composition and publication. | +| Source lane | Generic/private native Git and archive transports, their additive decoder/lock/source/provenance/restore work, and separate top PR. Persisted local path dependency language is not requested. Core must not add source variants or second acquisition/solver paths. | + +Implement core in small commits: (1) context/layout and focused unit coverage; +(2) isolated CLI/runtime composition and environment ownership; (3) lifecycle +scope propagation, exact recovery/removal and cohesive E2E. Expose project CLI +only for client scopes actually qualified by the adapter; core alone must not +claim all targets work. Each client starts implementation only after its own +two plan review/fix rounds. All commits/pushes/reviews are parent coordinated. + +Scope selection must happen after Cobra parses flags and before command work. +Use one `App.ForScope(scope, projectRoot)` composition callback plus an +`app.forOptions(opts)` selector at command entry points. The callback reconstructs +the existing runtime with the selected mutable namespace and shared immutable +store; it is not a second engine. Command constructors continue receiving App +values. Do not mutate a copied App in a pre-run hook and assume existing RunE +closures observe that mutation. Tests exercise real production composition. +Audit every command reachable with project scope: no path may silently retain a +hardcoded user request. Any route not qualified in this delivery rejects project +scope before acquisition/effects. Existing signed Directory requests carry the +selected scope and continue enforcing signed scope eligibility; project support +does not override a user-only release policy or manufacture compatibility evidence. + +`deps export/import` operates on the selected environment and preserves the +existing exact-byte/regular-file/race checks. When the canonical exchange pair +equals the active project pair (including default `.` from the project root), +refuse before writes and tell the caller to pass a different exchange directory. +Never silently read/write a user or parent manifest. Explicit exchange directory +transfer remains the same portable pair and locked restore contract. + +## Lifecycle invariants + +* Preflight unsupported targets/components and mismatched context before effects; + existing digest/security and host detection rules still apply. Dry run neither + creates `.uap/` nor changes project/user configs, intent, state, or journals. +* Desired-state observation, repair selection, manual-completion facts and saved + aliases are scoped to selected namespace and environment ID. Never adopt an + imperative or another environment's binding because its source/name matches. +* Native commands receive the actual project working directory or documented + project flags from the client lane. Core does not automatically grant trust, + approve MCP execution, or equate a prepared file with observed activation. + Deactivation receives the same validated context/native registry even when + invoked from an unrelated cwd. There is no path inference from the artifact. +* Sync remains unchanged for exact pins and converged effects. Update advances + only selected moving requirements. Repair restores the recorded exact pin and + retains data. Remove preserves foreign config entries and pending uninstall + ownership; recovery remains explicit and bound to the selected namespace. +* Copied/moved `.uap/` state with old absolute locators must fail deterministic + target/profile-namespace checks before mutation. Portable restore copies only + manifest/lock and creates fresh receipts. No new automatic migration is added. +* An interrupted project operation cannot block or recover user/A/B operations + in other mutable namespaces. Shared-cache corruption remains an integrity + error and is never disguised as projection drift or repaired to a newer pin. + +## Focused E2E and acceptance + +Use disposable profiles, HOME/data roots, two projects A/B, real CLI composition, +local Git fixtures, the existing acquirer/loader/resolver/store and real transaction +receipts. Native process fixtures remain explicitly fixtures. The representative +adapter is chosen from the first independently qualified client stack. + +1. Existing user saved lifecycle still passes with identical default paths and + `user-default` ownership. Legacy SDK requests without scope stay user scoped. +2. Install the same package into user, A and B with different pins and aliases. + Verify separate manifest/lock/state/data/projections and shared digest storage; + no accidental user config changes during either project operation. +3. A locked no-op sync leaves intent, lock, owned config, state, data and journals + byte-identical and makes no mutation/ref-refresh calls. Status reflects A only. +4. A targeted update does not affect B/user. Drift in A refuses sync; exact repair + restores A's recorded digest and retains its data marker. +5. Copy A's manifest/lock to fresh C with an empty mutable namespace and reproduce + exact package digests; no local paths appear in portable bytes. Copying A's + mutable state to C instead fails safely before effects. Also export A to an + explicit exchange directory and import/locked-restore C; source==destination + default transfer refuses with unchanged input/state bytes. Marshal the new + operational DTOs and portable pair and assert selected root/profile literals + never enter public DTO output, lock bytes, intent digest or source identity. +6. Inject one filesystem interruption and one native-config interruption when + applicable. Recover A through its own receipt/journal, preserving B/user. +7. Remove A's target intent and binding, preserving foreign config entries, + B/user, shared cache and retained data. Keep ownership when external uninstall + is incomplete; finish through the existing explicit attestation contract. +8. A generic contract fixture uses three different paths for the user profile, + project native registry and managed package. It covers plan/apply/inspect/ + repair/remove, including removal invoked from an unrelated cwd; user profile + bytes remain unchanged. +9. Negative coverage: missing/unsafe project root; project-root with user scope; + request/engine mismatch; symlink escape; unsupported client/layout; mixed + supported/unsupported selection; dry run with no created state; root selection + from a nested cwd; two profiles and two same-named sibling projects. Include + `A/.uap` as a symlink/junction to real B mutable state and assert no change to + A, B or user bytes and no acquisition/native calls. + +Run focused domain/planner/installer/environment tests, production composition +tests and the representative cohesive CLI scenario with race detection. Run the +expanded client matrix as stacks merge, then the parent's required repository +gates. Do not start duplicate broad jobs or claim live-native/runtime/release +qualification from fixture E2E. Stop optional testing once concrete risks and +required gates are covered. + +## Review ledger + +* Round 1: client-plans review at `project-core-review-r1.md`; fixed missing + deactivation context/native root, explicit project NativeRegistryLayout, + entry-boundary scope gates, and client-owned scope declarations. Incorporated + the reviewer's follow-up minimal optional project authority capture and reuse + of existing revalidation/validators; no fake detected user profile. Reviewer + verified all findings closed and gave GO on 2026-10-23. +* Round 2: sources-lead review at `project-core-review-r2.md`; fixed physical + no-follow namespace entry/revalidation gates, explicit private JSON fields, + and project transfer source==destination refusal with explicit-exchange E2E. + Reviewer verified all fixes and gave GO on 2026-10-23. +* Client R2: all 14 plans verified GO after scoped lifecycle/backend policy, + preflight attestation, reachable ChatGPT unsupported, and product VS Code + composition clarifications. These shared implementation requirements are + recorded above and in `client-plans-review-r2.md`. +* Implementation: parent authorized after client R2 fix verification on + 2026-10-23; implementation starts with reviewable contract checkpoints. diff --git a/docs/plans/project-install-e2e/project-core-review-r1.md b/docs/plans/project-install-e2e/project-core-review-r1.md new file mode 100644 index 000000000..5fd10da8b --- /dev/null +++ b/docs/plans/project-install-e2e/project-core-review-r1.md @@ -0,0 +1,85 @@ +# Independent project core plan review — round 1 + +Reviewer: client-plans lane. Reviewed plan `project-core-plan.md` and current +`domain/clients.go`, `domain/state.go`, `domain/planning.go`, planner dispatch, +client identity/lifecycle implementations, and command port. Review date: +2026-10-23. Verdict: **changes required before implementation**, with the +existing engine/storage direction accepted. + +## Required fixes + +### P1 — project context stops before deactivation + +The proposed context reaches `DeliveryPlan`/activation but not +`domain.DeactivationRequest`. That existing request has neither scope nor a +project root/plan. Claude/Qoder/Grok removal must run the native command from +the recorded project and use the correct scope; Gemini/OpenCode/Kiro direct +config removal must distinguish the project registry from the unchanged user +`DetectedClient.ConfigRoot`. Reconstructing context from the managed artifact +path would violate the plan's own invariant. + +**Fix:** carry validated `TargetContext` through deactivation and any read-only +binding/target-facts path used to construct it. Reconstruct from the configured +namespace plus exact recorded binding, validate scope/root agreement before +dispatch, and add focused removal-from-unrelated-cwd tests. The command port +already has `Dir`; no new runner abstraction is required. + +### P1 — native registry authority has no complete project contract + +The planner currently invokes `setNativeRegistry` before target resolution and +defaults it to the detected user config root. Several native adapters use the +plan registry for identity checks but `request.Client.ConfigRoot` for mutation. +Adding `ProjectTargetLayout` alone does not establish which exact native root +is authoritative, and treating a project's `.gemini` or `.kiro` root as the +user profile would break the explicit no-profile-substitution contract. + +**Fix:** specify that project adapters implement the existing +`NativeRegistryLayout` with `PlanInput.TargetContext`, selecting their actual +project registry/config root (or explicit unsupported/manual remote state). +Core validates the resulting plan context and supplies it consistently to +activation/deactivation/verification. Per-client implementations must branch +on context rather than overwrite detected user config. Add a generic contract +fixture where package target, native project root, and user profile are three +different paths, covering plan/apply/inspect/repair/remove. + +### P2 — the before-acquisition unsupported gate is not assigned + +The plan says absence of `ProjectTargetLayout` rejects a project client before +source acquisition, but the current planner runs after the envelope is loaded. +The new interface by itself cannot deliver that ordering. Client definitions +also still declare only user scope. + +**Fix:** identify a reusable read-only selected-scope capability check at the +installer/CLI entry boundary before acquisition, backed by registered adapter +support. Client stacks own adding `ScopeProject` only after qualification. +Keep package/component-specific admission in the existing planner after load. +Test an unsupported project's add/deps-add using a counting acquirer: zero +acquisition, state, journal, and native effects. Do not pre-advertise all clients +in the base PR. + +## Accepted components + +| Component | Assessment | +| --- | --- | +| Explicit cwd default and `--project-root` | GO; deterministic root, no hidden Git-root search. Document nested cwd semantics in CLI help. | +| `uap.toml` / `uap.lock` reuse | GO; no scope/path additions to portable identity. | +| `.uap/` mutable state and project-default receipt | GO; namespace separation avoids a global project registry. Existing foreign/symlink path guards must apply at namespace creation. | +| Shared digest store | GO; no cache removal or cache-as-ownership confusion. | +| Existing user APIs and defaults | GO; preserve empty-scope SDK compatibility and user-default output/paths. | +| `TargetContext` and optional layout hook | GO after P1 fixes; adequate for native registry, component projection, explicit session-config, and manual remote clients without a larger framework. | +| Scoped App reconstruction | GO; correctly identifies Cobra App-copy closure risk. Cover production composition, not only an injected test App. | +| Coordinator/observation parameterization | GO; retain Observe wrapper; inspect every receipt lookup and internally generated authority. | +| Exact recovery, copy/move refusal | GO; compare reconstructed paths plus current authority before effects. Portable restore must start from manifest/lock only. | +| Focused E2E and evidence labels | GO; fixture command success is not genuine client discovery/runtime proof. | + +No style-only changes requested. No implementation performed during review. + +## Fix verification + +The revised plan was checked on 2026-10-23. All three required findings are +addressed: deactivation/binding facts receive context and the exact native root; +project registry selection is mandatory and preserves the original detected +user profile; a shared entry-boundary capability gate prevents acquisition for +unsupported scopes. Optional contextual physical-authority capture reuses the +existing token/revalidation contract rather than manufacturing a profile. +**Round 1: GO to independent round 2.** This does not authorize implementation. diff --git a/docs/plans/project-install-e2e/project-core-review-r2.md b/docs/plans/project-install-e2e/project-core-review-r2.md new file mode 100644 index 000000000..4c86fba00 --- /dev/null +++ b/docs/plans/project-install-e2e/project-core-review-r2.md @@ -0,0 +1,39 @@ +# Project core plan — independent R2 review + +Reviewer: source lane. Reviewed revised `/workspace/scratch/2819fbb48645/research/project-core-plan.md` against implementation-base source at `3ea24e3014ddf6679ca86729c56f1069112b0d3a`. Read-only review; no implementation edits. + +## Verdict + +The architecture is suitably narrow: one selected runtime, separate mutable namespaces, shared verified store, unchanged portable source graph, explicit project context and no fabricated user profile. Approve after the three small clarifications below are applied and verified. They do not require a new engine, lock format or extra client abstraction. + +## Findings to fix in the plan + +### R2-1: make namespace physical containment an entry gate + +The plan rejects unsafe root aliases and copied `.uap` locators, but does not explicitly say the `.uap` directory itself and each existing ancestor/owned child are checked without following symlinks/junctions before creating/reading mutation authority. `/.uap -> /.uap` defeats logical namespace separation even when all string paths are correctly rooted under A. A copied namespace can also carry another project's live locks/data paths. + +Required clarification: canonicalize the explicitly selected project directory; reject `.uap` symlink/junction/reparse aliases and ancestor escapes; bind mutable namespace authority to the selected canonical root. Revalidate existing namespace identity immediately before apply/recovery. No need for a new global registry or persistent path-derived portable ID. Add one negative using a `.uap` symlink/junction to a real other project's mutable directory and assert zero changes to both environments. + +### R2-2: distinguish operational facts from portable serialization explicitly + +The plan says `TargetContext` is excluded from public JSON/portable formats, while adding it to exported domain structures and adding ProjectRoot to Request/config/plan structures. Default Go JSON encoding would expose exported operational fields unless exact tags/custom projections are specified. Source-derived lock/manifest intent must remain portable across machines; project root must still be available to the process that owns deactivation. + +Required clarification: `TargetContext` and operational ProjectRoot fields use explicit `json:"-"` on structures that can be encoded, or are omitted through existing dedicated public projections. Inspection/status may intentionally report selected scope/paths via existing nonportable status fields, but neither manifest digest nor `uap.lock` gets project/native/profile/cache paths. Add a marshaling/portable-pair check proving a literal selected root does not enter the lock or acquisition source identity. No broad output rewrite is needed. + +### R2-3: define project transfer when default exchange equals source + +`deps export/import` currently derives the active environment directory from App and defaults the exchange directory to `.`. After selecting project scope, invoking from the project root makes these the same directory. The existing global command wording and default are no longer sufficient to explain a safe result. + +Required clarification: export/import operate on the explicitly selected environment; if the canonical exchange pair equals the selected pair, refuse/no-op before writes with guidance to pass a different exchange directory. Preserve exact input bytes and current race/regular-file checks. Do not invent a second portable root format or silently read a parent/user manifest. Add one source==destination negative plus project-to-fresh-project locked restore using an explicit exchange directory. + +## Additional review observations + +* Empty SDK Request.Scope retaining user default while a project-configured Engine rejects a mismatched request is coherent; preserve the request/engine mismatch negative. +* Source lane will add new remote Git/archive variants to the same environment Requirement/LockedSource contracts. Core should continue treating package acquisition opaquely and scope only runtime ownership/selection. Source identity must not acquire ProjectRoot or Context fields. +* Shared store bytes are safe to reuse only with existing digest/executable verification; project namespace metadata must be reconstructed from the current selected lock instead of adopting cached ownership. Existing core plan already states this. +* Optional ProjectPhysicalAuthority uses the existing token/revalidation mechanism and avoids a second universal authority system. Exact native registry/root must be revalidated during deactivation/recovery as the revised plan says. +* Independent plan reviews are complete only after the author applies these clarifications and this reviewer confirms the updated sections. Root remains the implementation authorization gate. + +## Applied-fix verification + +Verified the revised core plan on 2026-10-23. R2-1, R2-2 and R2-3 are all explicitly fixed in the storage contract, DTO contract, transfer semantics and E2E negatives. **R2 GO; no remaining plan findings.** Root remains the code authorization gate. diff --git a/docs/plans/project-install-e2e/project-fixture-canonical-roots-plan.md b/docs/plans/project-install-e2e/project-fixture-canonical-roots-plan.md new file mode 100644 index 000000000..83d876179 --- /dev/null +++ b/docs/plans/project-install-e2e/project-fixture-canonical-roots-plan.md @@ -0,0 +1,37 @@ +# Canonical fixture seed roots — recovery plan + +Recover the former fixture-only contract (lost source818dcb90/final1fa8b098, +prior plan GO946b/code GOb287 recorded in retained handoff). Old passes are not +proof of restored bytes. Obtain two fresh independent plan GOs before edits, +then two fresh code reviews and affected tests. No production code changes. + +Actual macOS /var versus /private/var and Windows short RUNNER~1 spellings can +name the same existing test directory. Production composition canonicalizes the +selected root, so raw t.TempDir seeds produce false exact receipt/path assertion +failures. Canonicalize the EXISTING fixture-created temporary root first through +filepath.EvalSymlinks and then construct all expected children/compose the app. +Fail if resolution fails; never clean/rewrite native output, production receipts, +actual state, caller path spelling or assertions after observation. + +Add one CLI projectJourneyTemp(t) seed helper beside the shared project fixture. +Use it for shared journey home/A/B, transport/source/git-home/certificate seed +roots, Cursor/Kiro journey seeds, explicit project composition fixture roots, +OpenCode inert probe executable root and lifecycle transfer exchange roots. +Existing alias-specific tests keep their deliberately authored aliases. Providers +already have canonical projectGeminiTemp; use it for remaining OpenCode base/ +source seeds rather than add a second helper. Leave unrelated fixtures alone. +Kimi/Devin/Cline/Grok restoration must use canonical seeds on their own leaves; +root coordinates equivalent followups only after those leaves join assembly. + +This is fixture setup only. Preserve all exact path, scope, digest, receipt, +original-user/B isolation, readonly, drift, rollback and removal assertions. +No fake native proof, mock guard, production path normalization, trust/model/ +auth operation, new dependency, platform skip or loosened expectation follows. + +Verify the helper through affected project composition/lifecycle fixtures using +real UAP/TLSGit transactions and inert host boundaries. Existing helper ensures +physical roots locally; macOS/Windows actual execution remains separately +attributed to hosted CI, never claimed from Linux. Check authored whitespace +and relevant lint, then immediately checkpoint internal +feat/project-fixture-canonical-roots through expected-head exact-tree publication. +No extra PR or main/release merge. diff --git a/docs/plans/project-install-e2e/project-fixture-switch-lint.md b/docs/plans/project-install-e2e/project-fixture-switch-lint.md new file mode 100644 index 000000000..d2f0c4993 --- /dev/null +++ b/docs/plans/project-install-e2e/project-fixture-switch-lint.md @@ -0,0 +1,21 @@ +# Project identity fixture: preserve branches in a tagged switch + +Required job `121162735808` at source +`1a19373e024ba4c2ac0748fda1306ce0ffd95429` reports staticcheck QF1003 at +`installer/project_identity_test.go:72`: the fixture's client ID if-chain can +use a tagged switch. + +Only `newProjectFilesFixture` changes: replace `if id == "cline"`, +`else if id == "vscode"`, and `else` with `switch id`, `case "cline"`, +`case "vscode"`, and `default`. Keep the six existing `filepath.Join` +assignments byte exact and in their existing order. The default retains the +Codex paths for every other ID. Preserve registry setup, request/modes, +`f.reopen` placement and every assertion. There is no production change or +additional fixture behavior. + +Validate the full affected installer package with the repository's configured +lint and the same `--new-from-merge-base=origin/main` used by CI, then its +unfiltered size/architecture lint gate. A new test mirroring this syntax change +is unnecessary. Existing focused fixture tests may run only to resolve a +concrete remaining concern. Two independent plan rounds and frozen source +review precede the implementation commit and prompt publication. diff --git a/docs/plans/project-install-e2e/project-git-fixture-diagnostic-plan.md b/docs/plans/project-install-e2e/project-git-fixture-diagnostic-plan.md new file mode 100644 index 000000000..aee0d25cf --- /dev/null +++ b/docs/plans/project-install-e2e/project-git-fixture-diagnostic-plan.md @@ -0,0 +1,146 @@ +# Bounded test-only immutable Git failure diagnostics — R1 + +## Observed failure and narrow objective + +Required run40181365678/job121132946340 failed only +TestCLIModule -> TestProjectComposedAgreementColdRestoreUsesPortablePins/git, +at its initial public add (project_lifecycle_transfer_test.go:15, 0.51s). +AcquireGit intentionally replaces errors from acquireGitTransport with one +credential-safe public message. Its command sequence covers init, remote, +config, fetch, rev-parse, ls-tree, optional sparse checkout and checkout, followed +by package-path inspection/snapshot. Existing logs cannot establish which failed. +A different earlier immutable acquisition failure is not proof of the same cause. + +Add diagnostics only to the disposable project's existing fixture. Do not change +product errors, acquirer/runner/process code, retries, timeouts, cleanup, +containment preconditions, public command semantics or test assertions. + +## Exact proposed implementation + +- Add cli/cmd/agentplugins/project_git_diagnostic_test.go with a private Runner + decorator. It delegates once to sourceacquisition.OSRunner with the original + context/Command unchanged and returns the exact original bytes/error object. + A private delegate seam is used only by inert decorator tests. +- The fixture transport owns one shared mutex-protected recorder and passes it + into both configureProjectJourney Acquirer and SourceAcquirerForDryRun. Every + production ForScope reconstruction reapplies that same decorator. No env knob, + executable replacement or fake Git success is added. +- Never log argv, environment, stdout, stderr or source bodies. Operation labels + are a closed mapping after optional -C : init, remote-add, config, + ls-remote, check-ref-format, fetch, rev-parse, ls-tree, sparse-init, sparse-set, + checkout and show. Any other grammar is labelled unknown; its cause is omitted. +- Successful operations only increment this fixed label map; no per-success log. + On failure emit at most four diagnostic records per transport and one bounded + snapshot of the operation counts. Drop later diagnostics without affecting + execution/results. This is an observational limit, not a command-admission gate. +- Error class is a fixed enum, selected through errors.Is: context_cancelled, + context_deadline, ordinary_unsupported, stdout_limit, other_git_or_process. + The opaque final class cannot establish authentication or containment success. +- A known operation's nested error may be logged only for this exact credential- + free fixture authority: HTTPS127.0.0.1 endpoint with no userinfo/query/fragment, + known disposable workspace and repo operands. Replace those locators with + fixed placeholders, redact every remaining URI (including userinfo/query), + and omit the entire cause if credential-style assignments/headers occur. + Unknown/unrecognized authority prints only cause_omitted. Quote controls and + truncate the final quoted cause to at most2048 ASCII bytes. No real repository + URL or credential is admitted to the diagnostic output. +- The existing public CLI error remains unchanged. If all Git commands succeed + before that generic error, report only that the remaining failure is in the + post-command package validation/snapshot phase; do not invent an exact cause. + A later narrower observation would require a separate proposal. + +## Finite verification and next observation + +Inert delegate tests prove exact one-call context/Command/bytes/error identity, +success silence, exact operation mapping/unknown omission, typed error classes, +known locator/URI/credential redaction, escaped/control and final2048-byte cap, +and four-record saturation without affecting returned errors. No subprocess is +needed for those tests. Existing real TLS transfer and shared source lifecycle +tests may run once at the normal configured limits; no retry-until-green loop. + +Publish only after two plan rounds and two fresh code reviews. The next genuine +Required run will either preserve a bounded nested failure class/cause or pass +the unchanged test; one passing run alone does not explain the prior failures. +No failure is labelled flaky and no native qualification follows. + +## R2 — concrete review corrections, superseding conflicting R1 wording + +Root's source trace and Claude's independent R1 both confirmed that +OSRunner.Run intentionally includes argv and merged streams in nonnative errors. +The cause paragraph above is narrowed as follows; it does not authorize logging +an arbitrary wrapped error. + +1. **Nonnative causes are always omitted.** If NativeCredentials is false, emit + only the fixed operation/class and literal cause_omitted; do not call Error(), + scan, quote or retain that wrapper text. This applies even to a known init, + config, checkout or tree operation. Unknown grammar/authority also omits cause. + Preserve the original error object and bytes to the caller without wrapping. + +2. **Positive cause admission is exact and bounded.** Only NativeCredentials=true + plus an explicitly known HTTPS127.0.0.1 fixture endpoint with no userinfo, + query or fragment and known disposable workspace/repository operands can + admit cause text. That OSRunner branch already avoids Git argv/streams in its + native-error wrapper. After obtaining its existing error string, require + valid UTF-8 and at most8192 input bytes; oversized/invalid input is omitted + before any URI/credential scan or quoting. This does not claim a bound on an + arbitrary externally implemented Error() method; the production delegate is + the unchanged known OSRunner, and the test seam has only inert finite errors. + Redact known locator operands and all remaining URI forms, omit credential- + style assignments/headers, then cap the final quoted ASCII cause at2048 bytes. + +3. **Counts are observational, not acquisition-phase proof.** The transport + recorder is shared across Resolve/Acquire and scope reconstruction. With no + observed Runner error, emit exactly “no observed Runner failure; remaining + source phase undetermined.” Workspace creation can fail before the first + command, and later validation/snapshot can fail outside invocations. + Neither aggregate counts nor a final checkout proves a complete acquisition. + Remove the R1 suggestion that this uniquely identifies a snapshot failure. + +4. **All accounting is finite.** Store only the closed fixed label/class map, + saturate each operation count at65535, and emit at most four failure records + per transport, each with a fixed-size count snapshot. Unknown labels use one + fixed unknown slot. Saturation only suppresses diagnostics; delegate calls + and returned results are untouched. No URL, argv, output stream or environment + is retained in successful-operation accounting. + +5. **Pin negative observations in inert tests.** A known nonnative init error + deliberately contains argv, stdout/stderr and a credential marker; assert + Error() is not inspected and none reaches diagnostic output. Include + unknown/native external-URI authority, oversized native error text, control/ + Unicode escaping, URI userinfo/query, credential-style headers/assignments, + pre-command empty counts, prior Resolve counts, four-record saturation and + exact original context/Command/bytes/error identity. Every omitted-cause case + retains fixed diagnostic facts and the real unchanged failure to its caller. + +These refinements preserve the original test-only scope and no-retry/no-budget/ +no-guard/no-product-output invariants. R1 reviews authorize this revised plan +for final R2 review, not implementation. No diagnostic source has been written. + +## Implemented candidate and author checks + +The implementation is based on published d94d6c1b09e89634220e471cb1f2c03d758b0511, +not the earlier plan branch's product tree. Only the two approved plan commits +were cherry-picked. Root and Claude independently returned final PLAN GO for +772422310444bf235222e7ef430d49db7f376d0e before source edits. + +Production acquirer/runner/process/CLI behavior is unchanged. Four test files +add the private wrapper, its finite contracts and existing fixture wiring. +Known native origin fetch receives cause text only after an observed successful +exact fixture remote-add in the same owned workspace. This uses one bounded +provenance slot; unknown grammar or another remote-add invalidates it. Failure +to qualify only omits diagnostics and cannot alter the delegated result. + +- Initial five private inert contracts: PASS0.176s. +- Final endpoint query/fragment precision plus the same contracts: PASS0.033s. + No vendor, Git transport, model or MCP process was used by these contracts. +- One predeclared unchanged TLS source-lifecycle plus cold-transfer selection + ran once: FAIL9.913s. Both Git branches refused their first init because this + local host lacks the ordinary proc prerequisite. The new fixed diagnostics + recorded phase=init, class=ordinary_unsupported, cause=cause_omitted and + counts init:1/all others:0. ZIP branches produced no failure. This is the + known local prerequisite refusal, not a reproduction or explanation of the + genuine hosted later immutable-fetch failures. No skip, retry or guard change + was made. Full log is preserved in the task runtime as + apm-recovery/git-diagnostic-source-lifecycle.log. +- Fresh source/code reviews and configured changed-source lint remain gates. + No native-client or hosted failure diagnosis is promoted from local checks. diff --git a/docs/plans/project-install-e2e/project-group-prepared-completion-plan.md b/docs/plans/project-install-e2e/project-group-prepared-completion-plan.md new file mode 100644 index 000000000..84e4f5aed --- /dev/null +++ b/docs/plans/project-install-e2e/project-group-prepared-completion-plan.md @@ -0,0 +1,236 @@ +# SDK project groups: successful Prepared operation result — R1 + +2026-10-24. Plan only. Two independent plan rounds precede source edits, then +two frozen code reviews. This is separate from the reviewed grouped mutation +reporting amendment and does not broaden its already frozen source. + +## Actual defect and existing contracts + +The actual default SDK Cline+Codex group repair regression added in reviewed +source c86859167511dbb3244fab1b3dfe20b3924a501f restores a missing managed +package from its immutable pin, preserves exact native skill/MCP files, and +leaves both bindings Prepared/installation_verified. The core group completes +without an activation error. The SDK instead returns OutcomeIncomplete and +`committed binding is not activated`. Single SDK operations accept this same +prepared file result. Fresh project native loading remains a separate claim. + +Source cause: reconcileGroupHostHandoff marks prior Prepared bindings pending, +even without an OnCommittedBinding callback. That conservatively forces the +core to run again, which is useful and must remain. After fresh successful core +completion, finishGroup asks anyGroupHostHandoffPending, which uses the prior +activation-oriented predicate and rejects the newly verified Prepared result. +It therefore conflates an incomplete native session with an incomplete file +installation operation. A no-change retry can also never reach the SDK's +OutcomeUnchanged branch while this pending boolean remains true. + +The core already exposes operation-specific evidence: GroupPhaseCompleted, +per-target GroupTargetExternalCompleted, a nil failure, and the actual fresh +ActivationOutcome. These fields describe completion of this operation, not +native runtime activation. Do not reimplement the core verifier/receipt/digest +engine inside SDK presentation or introduce a client-ID switch. + +## Minimal proposed correction + +1. Preserve initial pending detection, host callback execution, profile checks, + confirmation and actual group invocation. Do not change the early unchanged + Apply branch, transactions, observer composition, native attempts, recovery, + client policies or automatic activation. Native callbacks/errors remain + authoritative; an error cannot be downgraded to successful preparation. +2. After core completion and existing error handling, recognize only an explicit + project operation whose fresh GroupResult.Phase is Completed and whose + nonempty target set matches the requested project target identities. Require + every target to be ExternalCompleted with no failure; exact project scope and + selected canonical project context; and a terminal successful operation + outcome. Pending targets may be Prepared only with installation_verified and + no activation attestation or uncertain native effect. Other targets may retain + their already successful Active/NotRequired outcome. Empty, Manual, Failed, + unknown verification, partial/not-attempted phases, wrong/mixed scope or + missing/duplicate target identities cannot qualify. No newly invented + native-discovery evidence, attestation or lifecycle dimension is written. +3. For that actual successful project operation only, the prior pending flag no + longer forces an Incomplete SDK result or blocks the existing unchanged + classification. Keep all per-client activation/authentication/policy and + verification values from the actual outcome and saved state. A mutated file + operation returns Completed; a truly unchanged reobservation returns Unchanged. + Reuse the separately reviewed Mutated propagation: no stale preview may + override an actual effect. User scope retains existing behavior. +4. Preserve the actual per-target UserActions in the existing Result.ManualActions + array in target order, copying strings without rewriting executable arguments + or deduplicating distinct client instructions. No new public schema is needed. + Prepared success must retain the client's native-loading/trust/session limits + and useful next steps; it does not mean all manual actions are complete. +5. State reload failures, committed uncertainty and host callback failures remain + outside this success exception. A missing or inconsistent live binding cannot + establish a completed Prepared handoff. Use the already loaded exact state in + finishGroup when validating the matching target view; do not load it again + through an ambiguous boolean that turns unknown state into absence. Preserve + existing mutation facts even when inspection prevents a completed result. + +Keep the shared helper small and presentation-only: the fresh core operation and +existing live binding are trusted evidence from the already verified service. +Do not run new client commands, traverse files, reproduce native receipt checks +or persist state merely to classify a result. Any discovery that requires a +new lifecycle/ownership protocol is a separate plan, not an implementation +shortcut in this slice. + +## Required regressions and finite verification + +- Upgrade the actual two-target Cline+Codex default SDK repair regression to + require restored managed bytes, exact untouched native/user/A/B/data state, + Mutated=true, OutcomeCompleted, both Prepared/Installed and retained actions. + Its foreign-file branch still refuses before publication with unchanged state. +- Exercise real grouped install, unchanged replay/update/intact repair, actual + native missing-file repair where the current core supports it, changed source + update, reopen and removal. Check exact states/receipts and mutation flags. A + newly exposed underlying repair defect requires its own explicit diagnosis; + do not force expected results or relax evidence to close this SDK report. +- Cover mixed project targets, missing/duplicate target identities, mismatched + project context, Manual/Failed/unknown states, incomplete core/target phases, + uncertain effect, activation attestation, missing/failed reload and actual host + callback failure. Every refusal preserves real mutation facts and existing + failure reasons without claiming native convergence. +- Verify unchanged Prepared retries still invoke the real observer/adapter; the + classification change cannot bypass fresh discovery or ownership admission. +- Preserve existing user-scope grouped handoff, callback, attempt, cancellation + and mutation-reporting regressions. Run only focused installer/usecase tests + and the changed-package lint/architecture gates, with the shared pinned Go + runtime and bounded build concurrency. No vendor process or native gate + assertion changes are required by this SDK presentation correction. + +No implementation has been written for this plan. + + +## R2 — exact identity and fail-closed presentation after both R1 reviews + +Both independent reviewers (APM and Skillshare) gave R1 architecture GO with +concrete admission refinements. This revision incorporates every refinement +before source edits; their final R2 reviews remain required. + +The exception is scoped to the final presentation of an actual completed project +operation. It does not change initial handoff detection or skip fresh core work. +The shared helper receives the existing prepared operation, fresh GroupResult, +and the state already loaded by finishGroup. It performs no new I/O, native +commands, persistence, receipt verification, or lifecycle transition. + +Exact identity must agree across the frozen prepared target, fresh core delivery +plan and loaded binding: installation ID, binding ID, client ID, physical artifact, +active locator, explicit project scope, canonical project root, original user +configuration root, selected delivery, profile authority and native profile +namespace/root. Use the exact binding ID (derived by the existing domain helper +where the core result lacks a direct field), not the first binding with a matching +client ID. Reject empty, duplicate, missing, extra or inconsistent target identities. +Each target must belong to the same requested installation and project context. +No path canonicalization, discovery or new authority inference is added here; +these are the already canonical values supplied by preparation and the core. + +Recognize only positive known completion values. Require GroupPhaseCompleted, +every target GroupTargetExternalCompleted with nil failure, and the existing +successful terminal lifecycle contract. Prepared requires installation_verified, +no ActivationAttested, and the real fresh UserActions retained; it stays Prepared. +Active and NotRequired may retain their successful existing verification contract. +Authentication is limited to known NotRequired, NotChecked, Pending or Authenticated +values; Failed and unknown values refuse. Pending and NotChecked never imply +successful authentication. Require PolicyAllowed: blocked or unknown policy cannot +qualify. NativeEffect must be explicitly Unchanged or Committed; unknown or uncertain +effects refuse. The loaded binding must have an empty NativeActivationAttempt and +nil PendingNativeIntent. Do not introduce new native discovery or runtime evidence. + +For project groups, a failed state load or missing/inconsistent exact binding +returns Incomplete/error with the observed Mutated value preserved. It must not +fall through the legacy anyGroupHostHandoffPending boolean, which treats unknown +state as absence. Validate using the already loaded state and retain the existing +core/host callback error when present. The user-scope branch keeps its behavior. +An unqualified project result cannot become Completed merely because no older +Prepared binding was found. + +Only that complete exact project result clears the old pending condition for +final SDK result classification. Preserve the separately reviewed actual mutation +propagation and require !result.Mutated before OutcomeUnchanged, even if a stale +prepared plan says NoChange. Copy UserActions from the fresh target outcomes in +target order, unchanged, to Result.ManualActions; never substitute preview actions +or rewrite structured JSON command guidance. + +Tests must include exact binding mismatches (including another binding of the +same client), physical/locator/context/profile/delivery mismatch, outstanding +attempt/intent, each rejected unknown/negative dimension, missing/failed loaded +state with and without mutation, and fresh outcome action propagation. Exercise +both successful host callbacks that leave Prepared and failing callbacks, and +prove fresh core observation still executes on a repeat Prepared operation. +The actual Cline+Codex grouped lifecycle and foreign-file refusal remain the +acceptance path; user scope, native attempts, cancellation and recovery regressions +are preserved. No weakening of core guards, assertion meaning or evidence gates +is authorized by this reporting correction. + +No production source has been changed for this plan revision. + +## Independent final plan approvals + +APM and Skillshare independently gave final R2 PLAN GO on frozen +bd721cd3ef1bed12b983bc11575d8afd2e0c800e after reading the full revised +plan and actual SDK/core seams. Both confirmed that their R1 refinements are +incorporated, including positive outcome enums, exact binding authority, +pending-attempt refusal, fail-closed loaded state, real mutation preservation, +fresh actions and callback behavior. Neither review executed a vendor client +or treated presentation completion as native activation. Implementation and +two frozen code reviews follow this approved plan. + +## R2 contract clarification — legacy absence of a native-effect report + +Implementation source inspection found that Claude project listing intentionally +returns the existing StartedActivation outcome with an empty NativeEffect. The +core explicitly preserves legacy non-native activators without an effect contract +(usecase/lifecycle.go), and its strict Prepared unchanged predicate allows this +empty value. Requiring an explicit effect from every client would reject a real +successful project operation or encourage fabrication of effect evidence. + +Both independent reviewers agreed with this narrow correction in principle. +Final admission accepts exact empty NativeEffect only when the existing scoped +traits declare !TracksNativeEffects and LifecycleCLIRegistry or LifecyclePrepared. +This means only that the contract did not report an effect; do not replace it +with NativeEffectUnchanged or infer native discovery. Tracked effects still +require explicit Unchanged/Committed, and Uncertain or any arbitrary nonempty +unknown value refuses for every target. All exact identity, live binding, +positive lifecycle, attestation, pending-attempt and fresh completed-core fences +remain. A mixed-target regression must distinguish a legacy empty effect from +a tracked client's empty effect. No client adapter or core effect protocol is +changed by this compatibility rule. + +SelectedDelivery comparisons stay exact: current project policy admits only its +zero value. This does not pre-authorize a future selected project delivery mode +or ignore its projection revision. NativeProfileRoot is compared with the +existing authority token's canonical root when present, otherwise the exact +project registry root recorded by the existing scoped binding contract. + +## Implemented presentation candidate and finite validation + +Both independent reviewers gave final narrow PLAN GO to legacy-effect addendum +e2c327d8. The implementation validates the existing fresh completed group and +its exact frozen/live identities using the already loaded state, retains real +mutation facts on failures and preserves fresh opaque actions in target order. +It changes no client lifecycle, native observation, state, receipt or filesystem. + +Go1.26.10 focused installer regressions passed1.682s: actual two-client default +SDK restoration of a missing managed package while both remain Prepared, the +foreign-file refusal, exact/mixed group reporting, all negative identity/outcome +cases, legacy-versus-tracked effect handling, state reload failures and existing +user-scope mutation reporting. The real broader lifecycle regression separately +found the pre-existing SDK handoff forcing VerifyOnly=false on intact project +files; that acting-path correction has its own two-round plan and remains work +in progress. Do not label that full lifecycle green from this narrower result. + +Two independent frozen code reviews and the size/correctness gates are required +before the complete integration is accepted. No vendor process was run by these +SDK tests, and Prepared completion does not qualify native discovery or MCP. + +## Independent implementation review + +APM and Skillshare independently gave CODE GO on exact +652bccf27c4f35bd958dda68711b718a38e32e2b after tracing the pure classifier, +SDK integration, complete negative fact table and real missing-package repair. +Both confirmed no new IO, state mutation, activation or ownership authority. +Skillshare noted that the shared result helper also appended fresh actions for +user groups. Limit that append to the target's explicit project scope so the +approved project-only change preserves the previous user output exactly. +This is the sole source follow-up to those reviews; request both exact delta +reviews before publishing it. The separate SDK handoff slice validates the +complete actual file replay without suppressing reported effects. diff --git a/docs/plans/project-install-e2e/project-group-remove-admission-plan.md b/docs/plans/project-install-e2e/project-group-remove-admission-plan.md new file mode 100644 index 000000000..3f1348b29 --- /dev/null +++ b/docs/plans/project-install-e2e/project-group-remove-admission-plan.md @@ -0,0 +1,93 @@ +# Project group removal admission — R1 + +The actual composed SDK Cline+Codex project lifecycle now passes install, +read-only replay/unchanged update, successful/failed host callback, changed +update and typed missing native-directory repair. Its final grouped removal is +blocked before effects by installer/group_remove.go's historical beta guard: +zero-selected group removal permits only Claude/Codex. Both single removals +already work and core RemoveGroup supports scoped per-client deactivation. + +This slice admits the already supported project clients at that SDK guard only. +Preserve the user-scope historical guard, selected-delivery authority, exactly-two +SDK target cardinality and OpenCode host-snapshot restrictions. No new client +capability, native command, data purge, schema, interface or release claim. + +## Existing authority chain + +Public Prepare first validates request scope/project root against the configured +engine and ClientScopeSupported for every requested project client; this checks +both the declared policy and concrete project layout/native-root hooks. Group +validation then checks the existing cardinality, duplicate IDs and registry. +planRemoveGroup finds each saved binding and always calls removalPreflight for +a present target. That verifies binding scope/client/profile, selected native +entry, recorded managed digest, independently resolved exact target and owned +PLUGIN_DATA. Apply repeats removalPreflight, confirms exact frozen bindings and +profiles, and calls core RemoveGroup. The core resolves exact scoped bindings, +rechecks directory/receipt ownership and deactivation before transaction commit. +No new admission can bypass those checks. + +## Minimal change + +Replace only the historical non-Claude/Codex refusal with a positive project +exception: explicit request ScopeProject, a nonempty selected project root equal +to the configured engine root, and ClientScopeSupported for the current target. +Use the existing capability API, no new list of agent IDs. Keep the full original +condition otherwise. Do not infer scope from a saved path or counterfeit HOME. +Absent qualified project targets may retain the existing NoChange path; a present +binding must still pass all existing preflight/frozen-binding checks. Nonzero +selected delivery retains the original behavior and authority checks. + +The helper is pure capability/context admission; do not add filesystem reads, +profile discovery, effects or new fallback. The public boundary already performs +canonical project validation. Preserve errors and all user-scope behavior. + +## Finite acceptance + +- Real SDK Cline+Codex project group install, reopen and remove must remove only + owned native skills/MCP entries and managed packages, with original user, B and + unrelated A files preserved and forbidden runner called zero times. +- Repeated group remove and a group with one already absent target preserve + truthful Unchanged/Mutated facts and do not create extra mutation journals. +- A changed managed package, foreign native object, changed project/request + scope, missing/unsupported project capability, and stale binding after Prepare + must refuse before deactivation/publication. Retain real pending-journal and + PLUGIN_DATA refusal tests. +- A user-scope non-Claude/Codex zero-selected group is still rejected by the same + historical guard. Selected-delivery tests retain their existing behavior. +- Integrate grouped remove into the full project lifecycle once this independent + slice is approved; the handoff correction itself can finish its current cleanup + through the already supported per-client Remove operations. + +Use focused installer tests and changed-source/static architecture checks only. +Two independent plan rounds and two frozen code reviews are required. No +production group-removal source has been edited for this plan. + +## R2 — current target capability and unchanged historical contract + +APM and Claude independently gave R1 architecture GO on exact +be90b4ac05617dba3a697208778831cd16ea1d8e after reading the actual public +scope validation, historical boolean guard and removal preflight/core paths. +Their refinements are explicit constraints on implementation: + +- Apply the exception only when the original historical refusal condition is + true. Keep that original expression, its parentheses, found/absent handling, + selectedCapable discovery and nonzero SelectedDelivery semantics unchanged. +- Qualify the current iterated target, never the first group target: explicit + request ScopeProject, nonempty canonical request ProjectRoot equal to the + configured engine ProjectRoot, and existing ClientScopeSupported for that + target and project scope. Missing/mixed/mismatched roots or capability grant + no exception; the public context validator continues to reject them first. +- Present targets always pass the existing removalPreflight during Prepare and + Apply, exact frozen binding confirmation and core scoped ownership checks. + A swapped or changed saved profile/binding cannot borrow another target's + admission. Absent targets create no synthetic binding, receipt or context, + retain the original NoChange path and do not dispatch deactivation. +- Keep user zero-selected guard/error, nonzero selected behavior, exactly-two + target cardinality and OpenCode host snapshot restrictions unchanged. + +The finite negative acceptance snapshots saved state and untouched user/B files +for each refusal, including a changed binding/profile after Prepare. Existing +pending-journal, selected-delivery and missing/changed PLUGIN_DATA cases remain +required. The positive actual Cline/Codex end-to-end test will switch its final +per-client cleanup to group removal only with this independently reviewed slice. +Both final plan GOs precede source edits. No group-removal source has changed. diff --git a/docs/plans/project-install-e2e/project-group-result-extraction-plan.md b/docs/plans/project-install-e2e/project-group-result-extraction-plan.md new file mode 100644 index 000000000..21ee217ea --- /dev/null +++ b/docs/plans/project-install-e2e/project-group-result-extraction-plan.md @@ -0,0 +1,52 @@ +# Group result error classification extraction — R1 + +The bounded full-file installer architecture lint on the assembled source reports +only finishGroup: 47 statements (limit40) and cyclomatic complexity24 (limit20). +The pure exact project completion helper and read-only handoff predicate pass +these guards. Do not alter budgets, exclusions, error meaning or project checks. + +Extract exactly the existing `if err != nil` body in finishGroup into a private +`failedGroupResult(result Result, err error) (Result, error)` function in the same +file. The caller remains at the exact same point after wrapLifecycleError, state +load, live result attachment and fresh attempt/actions attachment, and returns +the helper only when that already-wrapped error is nonnil. It precedes all +project-success, host-pending and no-change classification exactly as today. + +The helper preserves current field updates and order: start Incomplete with the +original error message; ErrPlanChanged selects Conflict/plan_changed; the +existing update-separately marker or ErrProjectionRefreshRequired selects +Conflict/update_required; ErrProjectionPinChangeRequired is checked last and +selects Conflict/pin_change_required. These remain independent ordered ifs, +not an exclusive switch that would alter multi-error precedence. Return the +same error object. Retain all already observed Mutated, binding, target and +fresh guidance fields by passing/returning the full result value. No IO, state, +context, callbacks or observations move into this helper. + +No new generic error framework or public API is introduced. The existing project +completion and user pending paths are unchanged. Run the current complete +TestFinishProjectGroup/TestFinishGroupPreservesActualMutationFacts and real +project group lifecycle tests, plus the existing conflict/error regressions. +If current tests do not pin the ordered conflicting-error contract, add one +small result-level table for exact Reason/Outcome/error identity and preserved +mutation/targets/actions, rather than tests of helper layout. Re-run bounded +installer full-file architecture lint and changed-source correctness lint. +Two independent plan rounds precede the extraction and two frozen code reviews +follow; this plan does not claim the current lint failure is already fixed. + +## R2 — exact wrapped identity and overlapping classification + +APM and Claude independently gave R1 PLAN GO on exact3bd3f4a2181eefb7c1130c4477148c0fb9882fb2 +after reading the whole current finishGroup and this plan. The helper receives +and returns the same already-wrapped nonnil error object; finishGroup's existing +wrapLifecycleError may wrap its original input before any state read, and that +behavior is preserved. No nil-helper call is introduced. No load-error branch, +attachment, project completion, pending logic or reporting call moves. + +Add a small actual finishGroup result table using joined errors: PlanChanged +alone selects plan_changed; PlanChanged plus ProjectionRefreshRequired selects +update_required; either plus ProjectionPinChangeRequired selects pin_change_required. +The existing textual update marker keeps its same position. Assert the same +wrapped identity/cause, known mutation (never NoChange), live target/failure facts +and freshly attached project manual guidance survive every classification. +This pins the externally reported precedence, not the private function layout. +No production extraction has happened. Obtain both final R2 plan GOs first. diff --git a/docs/plans/project-install-e2e/project-local-ownership-observer-plan.md b/docs/plans/project-install-e2e/project-local-ownership-observer-plan.md new file mode 100644 index 000000000..1f354df88 --- /dev/null +++ b/docs/plans/project-install-e2e/project-local-ownership-observer-plan.md @@ -0,0 +1,306 @@ +# Mandatory project file ownership with optional native discovery + +2026-10-24. Plan only; obtain two independent plan-review rounds before code. +This is a narrow shared composition correction for the authorized project +installation program. It adds no native command, schema, client capability, +configuration writer, or automatic recovery mechanism. + +## Reproduced problem + +The Cline implementation worker reproduced a foreign project skill collision +through the public SDK with the default EnableNativeObserver=false. The foreign +skill survived and no client ran, but the result was incomplete/Mutated=true and +state-v2.json grew from absent to 4080 bytes. The managed package had committed +before ProjectComponentMutation's requireAbsent refused the native skill. +The diagnostic source/result are retained at the task's cline-observer-gap path. +This diagnostic is not a passing acceptance test. + +Engine.service currently composes providers.NativeIdentityObserver only for +EnableNativeObserver && Runner!=nil. Its fallback inspects plan.ActivePath, +which is the managed package, not separately projected component files. Merely +calling ObservePreparedIdentity currently does not fix Cline: its prepared +registry hook validates plan identity, while InspectNativeRegistry receives the +full binding receipts and native kernel needed to inspect actual components. + +Local file ownership is necessary admission for project component installation. +The live-registry opt-in must continue to control native process discovery. +Neither a prepared filesystem check nor a file-registry match establishes +native session discovery, activation, or permission to clear an uncertain attempt. + +## Concrete design + +Reuse the existing NativeIdentityObserver, PreparedIdentityObserver port, +PreparedRegistryInspector, RegistryInspector, native kernel and scope policies. +Do not add client-ID switches, a generic installer, new CLI flags, or another +ownership/path implementation. + +1. Before selecting the extra file-inspection path, require matching client + and plan ClientID, both plan.Scope and TargetContext.Scope equal to project, + a nonempty exact canonical ProjectRoot and valid original-user TargetContext. + A malformed project hint must refuse, not select a user/fallback path. For + an admitted project whose effective scoped lifecycle is LifecycleNativeConfig, + also inspect + the adapter's existing file-backed native registry with a copy of the + observer whose Runner is nil. The exact injected kernel, client authority, + selected plan and prior binding remain unchanged. This is the same file + inspector already used by normal project lifecycle admission, including + symlink/member/digest checks. Native CLI/whole-plugin scope policies retain + their current prepared-registry behavior. +2. Merge that file finding into the prepared finding using the existing + conservative ordering: collision, indeterminate, expected, clear. Propagate + errors. Finish with native registry finding Clear and attempted=false so + NativeDiscoveryAttempted and NativeDiscoveryReconciled stay false; do not + launder a file match into native recovery evidence. Existing managed-package + digest/receipt verification remains necessary. An absent interface or unknown + file finding remains indeterminate, never clear. +3. In the SDK composition root, preserve the existing fully enabled observer + when the caller enables it and supplies a runner. When the Engine has an + explicit project root but live observation is unavailable/disabled, compose a + small prepared-only observer adapter. Both its port methods delegate to the + existing provider's prepared path. It receives no command runner. The default + user-scope compatibility composition remains unchanged. +4. Because normal preview already calls ObservePreparedIdentity, it receives + the same mandatory project file checks without invoking a host. Ordinary + saved-state Inspect remains host-free and is not changed. No lifecycle, + NativeEffect, transaction, receipt, or convergence rule is rewritten. Making + a formerly nil observer non-nil can enable the existing + observeGroupRecoveryEligibility branch for SupportsPreparedRecovery. Verify + this configuration effect explicitly: only known-receipt, exclusively owned + missing-managed restoration is eligible, with current file collisions refused. + That existing prepared restoration contract does not claim native discovery + and does not authorize clearing an unresolved native attempt. +5. Clarify the existing SDK flag documentation: disabling live observation does + not disable project filesystem ownership admission. Do not invent a second + flag or an opt-out from foreign-file checks. + +The narrow assumption to verify is that a qualified project's native-config +policy describes a file-backed registry. Inspectors receive Runner=nil even if a +caller supplies a live runner. A client that cannot perform the needed check +without a process must refuse, or require a separately reviewed capability +contract; no fallback to a user profile or broad automatic CLI invocation. +Existing concrete adapters remain responsible for validating both scope fields, +project/original-user identity and complete receipts before reading native paths. + +## Required validation + +- Convert the actual default-SDK Cline foreign-skill example into an acceptance + regression after integrating the reviewed Cline adapter. Add a foreign MCP + member case. Exercise both omitted runner and supplied forbidden runner with + observation disabled, and enabled observation with nil runner. Public + Prepare/Apply must refuse before managed-package/state/receipt/data publication; + exact foreign/user/B bytes remain. Temporary preparation snapshots and + operational lock metadata are distinct from durable installed state. +- Exercise existing owned project lifecycle with observation disabled: initial + install, unchanged replay, update, missing-component repair, removal and reopen; + tampered owned skill/MCP must refuse with previous installed state intact. + Cover Cline plus an already supported representative component adapter rather + than duplicating the entire source-by-client matrix. Exercise actual public + group repair of a missing managed directory with live observation disabled, + known receipts and exclusive restoration; a foreign native component must + still refuse. Retain the existing prepared-restoration eligibility meaning. +- Provider prepared-observation contracts cover component collision and malformed + config, exact supplied receipts/kernel, unowned Expected refusal, missing + inspector/unknown finding fail-closed, and no attempted/reconciled discovery + bits. Include a project native-CLI adapter with a forbidden runner to prove + its prepared path still never launches a process. +- Add a real pending-attempt negative regression through normal SDK effects + (or reuse the genuine process-death fixture), not a forged clean journal. + Even a valid managed package receipt plus Expected local file observation + must not clear that attempt or promote lifecycle through the new default + prepared-only composition. Verify owner recovery refusal and exact retained + state/bytes, beyond merely asserting that discovery flags are false. +- Retain all original-App namespace/physical-profile guards, user behavior, + source acquisition rules, activation attestation preflight and crash fencing. + Existing all-client provider, installer, usecase and public project CLI suites + exercise the composed union; relevant vet/architecture gates must pass. +- Obtain two independent code reviews at a frozen source checkpoint before merge. + Fixtures prove local ownership, not vendor discovery or release availability. + +## Review record + +R1 reviewed exact local 96259f319fb870bec05f4f9dd9fa2794d2b26218, +published with identical tree as 689296a62ea2415b32fd00036cceaac4790df250. + +- skillshare_scope: GO. Audited the current qualified project file inspectors, + including Codex/Copilot whose global user-CLI trait is true. Required explicit + scope/client admission and a real pending-attempt recovery refusal test. +- claude_codex_scope: GO. Confirmed prepared ports, physical-profile wrappers, + kernel and recovery boundaries. Added the missing-managed group-restoration + eligibility case because composing a non-nil observer changes that branch. + +Those conditions are incorporated above. No global +UsesNativeRegistryExecutable()==false gate is introduced: scoped policy and +an explicitly runner-free inspection control this path. Missing inspector, +missing kernel and unknown findings remain fail-closed. + +R2 independently reviewed exact local +`75e2f12f1ac7c587e817092ad90ce2d4fa9a2dc2`, before production implementation: + +- skillshare_scope: GO. Traced groupRecoveryPostApplyVerify and its exact recorded + digest requirement; prepared restoration does not require or promote native + reconciliation. The genuine pending-attempt and foreign-component negatives + remain mandatory implementation gates. +- claude_codex_scope: GO. Rechecked complete scope/client/canonical-root/original + authority admission, runner-free dispatch, missing capability refusal and the + documented group-restoration eligibility effect. No remaining plan blocker. + +Both review rounds are complete. Production implementation must still pass the +concrete regressions above and two independent frozen-source code reviews. + + +## Repair admission amendment — R2 + +The required real public SDK regressions exposed a second boundary while the +observer implementation was still uncommitted. First-install foreign skill/MCP +collisions now refuse before managed/state publication for all three disabled or +unavailable observer configurations. Single-target `Service.Repair`, however, +does not call either identity observer. It verifies the managed package, then +begins a native attempt; a changed Cline/Codex skill is only refused by the +adapter after the binding has been persisted as failed (`Mutated=true`). The +foreign bytes survive, but this does not satisfy the plan's intact-state refusal +requirement. The failing lifecycle regression remains unchanged. + +Proposed bounded correction after both independent R1 GOs; R2 review is required before repair code: + +1. Add one project-native-config prepared admission helper to the existing repair + preconditions. Invoke it after exact target/receipt/digest preflight, before + either intact-native repair or managed-package restoration. Both dry-run and + confirmed single repair use it, as does the same existing preflight used by + RefreshProjection. User scope, CLI-registry and prepared-plugin policies keep + their current paths. No lifecycle outcome, transaction, native-attempt, schema, + registry writer or capability declaration changes. +2. Require the existing PreparedIdentityObserver capability for that project + file policy; no nil/fallback package-only observation may stand in for local + component ownership. Call through the existing prepared observer and complete + frozen plan/binding so the independently supplied kernel, root and receipt + checks remain authoritative. The prepared path never invokes a process. +3. Compare the whole prepared observation with the already measured managed + package condition. For an intact package require Managed with the same + nonempty receipt digest and ReceiptReconciled=true, using the existing identity + validator plus the explicit receipt requirement. For a known + absent managed package (typed VerificationAbsent) permit only Absent. For a + typed VerificationDigestMismatch permit + only Indeterminate carrying exactly the same nonempty measured actual digest. + The provider attaches a digest only after local component findings are clear + or expected; collision/unknown/malformed local findings cannot supply that + managed-drift proof. Contradictory receipt/discovery flags are not accepted as + prepared ownership proof. Arbitrary errors, an unmatched state/digest, or a changed + package since the first verification refuse without state publication. +4. A missing exact-owned native component remains repairable: its clear local + finding plus the intact managed package still produces Managed. Existing + controlled managed-directory restoration, receipt validation and commit-time + preconditions remain unchanged. Repeat this same prepared admission in the + existing managed repair precommit path before the transaction publishes, and + immediately before reapplying intact native state starts its attempt. A + foreign edit during staging must refuse before managed publication. These + checks do not claim a portable atomic guarantee against a noncooperating + writer in the final read-to-syscall interval; the existing native mutation + primitives still enforce their own immediate ownership checks. + Do not treat a known digest mismatch as + native discovery or use it to clear uncertainty. Existing unresolved-attempt + recovery fences still precede this branch. +5. Retain the concrete Cline+Codex tests: intact state on tampered skill and MCP + refusal, normal missing-native repair, managed drift/absence restoration, + real two-target group repair with known receipts and foreign-native refusal, + and a genuine callback-created pending attempt whose managed digest and local + native files both verify but still cannot recover. Add any gap identified by + independent critics before implementing this amendment. In particular cover + managed absence/drift together with foreign skill/MCP contents, missing or + contradictory receipt evidence, unknown/error observations, a mismatched + ActualDigest or package changing between the two measurements, and a native + file edited during staging before the second admission. + +This amendment is a precondition addition required by observed behavior. It does +not relax the original plan's separation of filesystem ownership and native +activation evidence. Production repair code has not been edited. + +### Amendment review record + +R1 exact3510549a8c8bd2ae8b31bdc03c8a338dfac1f212 received independent GOs: + +- claude_codex_scope traced the actual repair/refresh preflight and provider + digest boundary. Requested explicit ReceiptReconciled for intact ownership, + typed managed absence/digest mismatch and contradictory/changing evidence tests. +- skillshare_scope traced the unresolved-attempt fence before repair and the + provider's refusal-before-digest ordering. Requested combined managed/native + negatives and an explicit staging-time recheck or honest race boundary. + +R2 incorporates both: existing precommit and pre-attempt rechecks, exact evidence +matching and the stated final noncooperating-writer race limit. No production +repair code was edited before these plan reviews. + +### Final amendment approvals and compatibility finding + +Both independent reviewers, claude_codex_scope and skillshare_scope, issued R2 +GO on exact e3b7bd206782c2760b594a63295b741eb3e697f1 before repair source edits. +They confirmed typed integrity evidence, explicit receipt reconciliation, the +precommit/pre-attempt checks and the unresolved-attempt fence. + +The complete existing SDK suite identified an overly strict draft provider +guard: current Devin project installation deliberately supports an absent +legacy Windsurf user profile (TestProjectDevinPublicSDKAbsentLegacyProfileAndVersionAdmission, +version3000.11.205). Valid original TargetContext therefore means the original +profile locator exactly matches DetectedClient and passes NewTargetContext; it +may be absent where the concrete adapter already supports that absence. The +shared guard must not impose a new nonempty user-profile requirement. Concrete +Cline/Codex and other adapter authority checks remain unchanged, with no client +ID switch or fallback. Explicit canonical nonempty project root stays required. + +## Implementation checkpoint and finite validation + +The prepared provider now combines project component ownership with package +identity using the exact scoped adapter and injected native kernel, with no +runner. The SDK composes that prepared observer even when live observation is +disabled or no runner exists. Repair checks it after managed integrity +verification, before managed publication, and before an intact native attempt. +Typed absent/drift evidence must match the independent package measurement; +unknown errors, foreign components, contradictory receipts and package-only +fallback observations refuse. Existing unresolved-attempt fencing is unchanged. + +Actual SDK regressions cover all three observer configurations, Cline and +Codex install/replay/update/reopen/repair/remove, two-target managed restoration, +foreign skill/MCP/malformed collisions, combined managed damage and foreign +components, a foreign edit during real staging, and an actual persisted pending +attempt created after managed commit. The latter independently verifies exact +managed and native receipts yet still requires recovery; file observations do +not fabricate native discovery or clear uncertainty. The inherited SDK group +Prepared handoff remains Incomplete. Its separately reviewed mutation-reporting +omission is tracked in the Claude grouped Prepared update amendment. + +Focused provider, SDK and usecase tests passed (0.015s, 3.911s and 0.010s). +The complete affected usecase package passed in 26.343s. A broader provider/SDK +run found two distinct failures: the valid absent legacy Devin profile described +above, and a preexisting executed-fixture append that returned ETXTBSY. The +provider fixture now writes the same changed bytes to a sibling inode and +renames it, preserving the changed-after-preflight assertion. Exact focused +regressions including both corrections then passed: providers 0.681s, installer +4.153s, conformance 0.028s. These results do not claim the entire assembled +repository is green: remaining native qualification, compatibility goldens and +ordinary-process prerequisite work retain their own gates. + +The ordinary process cancellation diagnostic establishes that this local host +lacks a required proc interface. Tests above ran before the separately planned +fail-closed prerequisite correction; no descendant-containment qualification is +implied by these ownership regression results. + +## Independent frozen-source code reviews + +Both independent reviewers inspected exact source commit +`c86859167511dbb3244fab1b3dfe20b3924a501f` +(tree `693086518d9076fc266bea5a6ced1dd1630828de`) before publication. + +- skillshare_scope: CODE GO after tracing default composition, canonical original + authority, runner-free component inspection, conservative finding precedence, + typed repair evidence, staging/attempt rechecks and actual pending refusal. + The absent legacy Devin profile follows the existing domain/adapter contract. +- apm_vercel_scope: CODE GO after all fourteen changed files and surrounding + provider/repair paths. Confirmed exact managed drift evidence cannot mask + foreign components; observed native files cannot authorize recovery; existing + Prepared group handoff stays incomplete. The test-only inode replacement + preserves its changed-after-preflight assertion. + +Both reviews were source-only under the shared build reservation and accepted +the separately recorded focused/full-usecase results without claiming new +execution. The original Cline code review's conditional shared ownership gate +is therefore closed at this source. Final reporting, native and integration +gates remain independent. diff --git a/docs/plans/project-install-e2e/project-prepared-guidance-plan.md b/docs/plans/project-install-e2e/project-prepared-guidance-plan.md new file mode 100644 index 000000000..5e17df928 --- /dev/null +++ b/docs/plans/project-install-e2e/project-prepared-guidance-plan.md @@ -0,0 +1,42 @@ +# Prepared project activation guidance — pre-code plan + +Root assigned this shared component after the actual composed Grok fixture +exposed a misleading deps-status action. Two independent plan GOs are required +before implementation, followed by two independent code GOs and fresh tests. + +Current dependencyNextActions appends attest_activation/--activation-complete +for every activation_manual/prepared fact. Project Grok, Cline MCP and other +prepared project lanes reject that promotion in the composed provider; status +must not recommend it. This change is presentation only: preserve all provider, +attestation, native discovery, failed/disabled state and readiness gates. + +Add an internal nonserialized Scope field to dependencyStatusFact. Seed it from +App.scope for desired and retained/orphan environment facts; where inspected +bindings supply scope, preserve project classification if EITHER context is +project (never downgrade an app-project fact to user because malformed metadata +says user). Empty/ordinary user contexts retain existing user behavior. + +For project-scoped manual/prepared activation, emit a review_project_activation +next action explaining that the selected project's client instructions and +native loading need review/verification; do not emit --activation-complete or a +user-scope command without the selected project context. No speculative client-ID +switch or new core policy port is needed: all project-prepared guidance can be +conservative. Preserve explicit SDK next actions and existing authentication +next-action behavior; this slice changes only the automatic status activation +suggestion. Do not manufacture active/installed/attested state, launch a client, +mutate saved state or run any native/trust/auth/model command. + +Propagate scope before both appendBindingStatusFacts and appendReceiptStatus +compute actions, covering desired locked bindings and retained/orphan receipts. +Keep JSON wire schema unchanged (Scope json:"-"); the existing outer status +scope remains authoritative to the user. Add focused status-helper tests for +user compatibility, project prepared/manual, both activation/auth facts, desired +and orphan binding paths, inconsistent project/user metadata, missing binding +and SDK action preservation. Add a composed project deps-status assertion where +available to prove no activation-complete suggestion, while provider refusal +remains tested on client leaves. + +Run focused agentpluginscli tests and relevant lint; no broad native or platform +suite is necessary for string guidance. Checkpoint immediately after approved +plan records and source increments through internal expected-head publication; +root merges this shared change into the project feature, never main/release. diff --git a/docs/plans/project-install-e2e/project-prepared-readonly-handoff-plan.md b/docs/plans/project-install-e2e/project-prepared-readonly-handoff-plan.md new file mode 100644 index 000000000..ecb660a8c --- /dev/null +++ b/docs/plans/project-install-e2e/project-prepared-readonly-handoff-plan.md @@ -0,0 +1,106 @@ +# Preserve a verified project file reobservation at the SDK handoff — R1 + +The actual composed SDK Cline+Codex lifecycle regression exposed a remaining +underlying defect while validating Prepared group completion. A repeated install +reported Mutated=true although saved state and native file bytes were identical. +The real Cline adapter observed PackageUnchanged=true, VerifyOnly=false and +NativeEffectCommitted. Do not suppress this actual reported effect in presentation. +The current complete-native-config contract reports committed when its Activate +path is invoked; identical bytes alone do not prove absence of file writes. + +Source cause: installer/seams.go seamActivator.Activate unconditionally clears +VerifyOnly for a resumed binding whose activation is Prepared. The core already +computed VerifyOnly=true after verifying the installed package, recomputing the +complete native projection and matching its owned receipts. The generic handoff +predicate confuses project files being verified with native session activation. + +Keep initial pending detection, actual host callbacks, profile revalidation, +confirmation, ownership admission, native attempt/recovery guards, and core +invocation unchanged. In the existing resume branch only, preserve an incoming +VerifyOnly=true when PackageUnchanged is true, the delivery is explicit project +scope with matching project TargetContext, and its existing scoped traits declare +LifecycleNativeConfig. Use no client ID list and infer no native discovery. +The existing core read-only decision is the authority; the SDK must not invent it. + +Do not turn any false VerifyOnly into true, skip the fresh adapter verifier, +clear an attempt/intent, grant activation, or modify receipts/state to make an +operation appear unchanged. User scope and project CLIRegistry/Prepared/manual +lifecycles retain their existing handoff behavior. Changed/missing projections +continue through the core's existing acting path and report their real effect. + +The actual public SDK group test must cover install, replay/update/intact repair, +changed source update, missing native-file repair, callback success leaving +Prepared, callback failure, reopen and remove, with original user/B state intact. +Add a narrow seam regression distinguishing preserved project-native-config +VerifyOnly from user scope, other project lifecycle kinds and actual acting +requests. Existing selected-delivery, pending attempt and handoff regressions +remain required. The presentation completion correction stays separate; its +Mutated=true and failed-load evidence must never be overridden by this change. + +Two independent plan rounds precede this narrowly scoped source edit and two +frozen code reviews follow. No production source for this handoff correction +has been changed. + +## R2 — exact predicate and preserved group authority + +APM and Skillshare independently gave architecture R1 GO exactee36799f after +reading the actual SDK/core call sites. Incorporate both sets of refinements: +the exception is inside the existing resume branch and requires incoming true +VerifyOnly, PackageUnchanged, zero SelectedDelivery, matching Client/Plan IDs, +explicit project Plan.Scope and TargetContext.Scope, a nonempty canonical project +root and original TargetContext.UserConfigRoot matching the actual client. The +existing scoped trait must be LifecycleNativeConfig. Do not canonicalize paths +or infer new filesystem/profile authority in this pure decision helper. + +In grouped operations a.facts can describe the first target. Compare the current +request's client, plan and target context; never require each client to equal the +first group's a.facts.ClientID/BindingID. All already established core ownership, +receipt and selected projection checks remain authoritative. No new bypass for +unknown/missing state or unresolved attempts is introduced. + +Tests cover incoming false, missing/mixed scope/root/user context, client-plan +mismatch and nonzero selected delivery. User scope, project CLIRegistry and +Prepared lifecycles retain their existing acting behavior. Actual SDK group and +single replay must freshly verify, successful callback must still precede the +verifier/profile recheck, failed callback must retain its failure, and missing or +changed native projections must stay acting or refuse under existing ownership +rules. Preserve actual native effects and all existing pending/recovery tests. + +No production seam source has been edited for this correction. Final independent +R2 reviews precede that edit. + +## Implementation and focused acceptance + +Both APM and Skillshare gave final R2 PLAN GO on exact49028da815077c4ce9793bad271a5d5655032f0e +before production edits. The source exception preserves only incoming VerifyOnly +for an unchanged zero-selected explicit project NativeConfig request whose +client, validated TargetContext and original user root agree. It uses existing +scoped traits and NewTargetContext (pure lexical validation), without IO or a +client-ID switch. Acting requests, user/CLIRegistry/Prepared lifecycles and +selected-delivery confirmation retain their previous routes. + +Focused installer acceptance on the assembled source passed3.667s: actual group +and single file replay, narrow real seam/provider dispatch, strict Prepared group +reporting, existing committed-callback retry and file lifecycle regressions. +The actual Cline+Codex group covers fresh read-only repeated install/update, +intact repair, callback success/failure, changed update, partial native-directory +refusal, positive whole-directory absence repair, reopen and per-client removal. +Original user/B files remain identical and the forbidden runner is never called. +The single replay checks the actual adapter request, observation, state and file +bytes; existing completed-with-no-mutation presentation is preserved. + +Group Repair intentionally restages managed packages under the existing core +contract (!Repair in the unchanged predicate); its real state/receipt mutation is +reported Completed/Mutated even if native bytes match. Removing just SKILL.md +from a still-present owned native directory is integrity drift and remains a +refusal; removing the entire independently owned object is typed absence and is +repairable. Neither contract was relaxed to satisfy a test. Group removal's +historical SDK beta guard has a separate twice-reviewed admission plan; this +handoff test uses existing public single removals until that slice is complete. + +The focused seam test preserves user/project-CLI/project-Prepared behavior, +incoming false, malformed/mixed context, client mismatch and selected-delivery +refusal. Profile failure remains fatal and a resumed request never repeats the +post-commit callback here. Group facts deliberately identify another client so +the exception cannot accidentally be tied to the first target. Architecture and +changed-source lint results will be recorded separately, before publication. diff --git a/docs/plans/project-install-e2e/project-process-death-evidence.md b/docs/plans/project-install-e2e/project-process-death-evidence.md new file mode 100644 index 000000000..47f1d5a00 --- /dev/null +++ b/docs/plans/project-install-e2e/project-process-death-evidence.md @@ -0,0 +1,102 @@ +# Project process death and fresh SDK reopen + +Status: current-source fixture evidence; not vendor runtime or release proof. +Source base: `75cece1488cdab0674b4817ea7a30ea95f7aad06` plus the two process-death +test files introduced with this record. Platform: Linux/amd64, Go 1.26.10. + +## Independent boundary review + +The root integrator (`/root`) and independent APM reviewer +(`/root/apm_vercel_scope`) each reviewed and approved both concrete boundaries +before test implementation. Both traced the production ordering and required +parent-side checks before the actual kill, bounded child handling with guaranteed +kill/reap cleanup, ordinary adapters for setup and fresh reopen, retained native +uncertainty, unchanged ownership receipts, and user/project B isolation. No new +production injection option or synthesized journal/state was authorized. + +The APM review identified that a managed-package-only interruption would not +cover the completion plan's literal native skill swap/MCP boundary. The second +case was therefore separately traced and independently approved before coding. + +## Tested boundaries + +| Label | Production boundary | Independently verified before parent kill | +| --- | --- | --- | +| `managed_package_committed_before_native_activation` | Existing public `Config.OnCommittedBinding` after the managed directory transaction and durable native-attempt marker, before the client adapter's activation | New managed package skill; old native project skill and MCP config; unchanged last native ownership; nonempty durable native attempt; stable selected project, binding and data receipt | +| `native_skill_swapped_before_mcp_lock_and_commit` | Real Codex activation and `ProjectComponentMutation.commit`, after native skill swap/verification and before `nativeconfig.Kernel.ApplyBatch` acquires the MCP writer lock | New managed package and native project skill; old MCP config; unchanged last native ownership; durable native attempt; exactly one real recovery directory retaining the old skill backup; stable project, binding and data receipt | + +The second child alone registers a test adapter embedding the real Codex adapter. +It delegates the actual scope, ownership, projection and mutation behavior, and +supplies the existing exported `nativeconfig.NewWithLockAcquirer` primitive. +The barrier accepts only the Codex codec and project A's exact +`.codex/config.toml` path. `WithCodexDocumentParser` preserves that acquirer. +This is disclosed primitive instrumentation, not an ordinary vendor session. +All initial installations and reopened engines use the ordinary Codex adapter. + +The ordering is established by the existing production path: + +- `usecase/apply_commit.go` commits the managed package directory. +- `usecase/native_attempt.go` persists the native attempt before dispatching the + activator; `installer/seams.go` then invokes the public committed-binding hook. +- Codex project activation delegates to `ProjectComponentMutation`, whose commit + swaps and verifies native skills before calling the MCP kernel batch. +- `nativeconfig/kernel.go` validates the batch/parser, then calls its lock + acquirer before resolving, rendering or writing the MCP document. + +## Actual process death and recovery behavior + +The parent installs the same inert package through the public SDK in a disposable +original Codex user profile and projects A/B, with real environment receipts and +owned data markers. It advances the source skill and MCP arguments, then starts +a separate SDK child process. The child announces the selected barrier only +after reading and checking the genuine persisted state and filesystem. The +parent independently checks those facts again and calls `Process.Kill` and +`Wait`; the child does not return an injected error or call `os.Exit`. + +A completely new project A engine then performs read-only `Inspect`. The ordinary +project Codex delivery has no typed selected native-intent reconciler, so the +persisted attempt remains unresolved. The public inspection reports +`Recovery.Required` with `native attempt has no persisted intent; retain uncertainty`. +This is an honest refusal to infer ownership or acknowledgment from matching +files. No state or journal is rewritten by the test to manufacture recovery. + +Valid update, repair, removal and a distinct fresh package installation remain +fenced. Where preparation yields a handle, confirmed application must return +`ErrRecoveryRequired` without mutation. Removal may refuse earlier through its +specific retained-native-attempt validation. Owner-scoped `RecoverEnvironment` +also refuses and retains the attempt; a project B engine rejects project A's +observation. Repeated inspection returns the same pending facts and launches no +native process or host callback. + +The test compares complete project A filesystem facts (entries, modes and file +hashes) after death with the facts after inspection, retry and recovery refusal. +It also compares the complete original user profile, user UAP state/data tree and +project B tree. New managed bytes, the native swap's old backup where applicable, +old MCP config, foreign skill/config files, ownership state and persistent data +all remain intact. + +## Verification + +The focused command is: + +```sh +go test ./install/integrationctl/agentplugins/installer \ + -run 'TestProjectProcessDeath' -count=1 -v +``` + +Both parent table cases passed in the recorded Linux run (focused package +17.363 s; parent table 17.32 s). A final rerun of the complete test source, +including the reused Windows hold, also passed (48.299 s under shared build load). The child-only helper +skips when invoked outside its explicit parent-controlled subprocess arguments. +The parent bounds the barrier and child lifetime, limits protocol output, caps +stderr at 64 KiB with synchronized diagnostic access, and kills/reaps a running +child on every parent failure path. The test reuses the existing Windows +managedstdio execute-bit hold; this Linux result does not remove that platform +limitation or establish a Windows process-death pass. + +This evidence establishes actual UAP process death, partial filesystem effects, +fresh composition, mutation fencing and retained recovery uncertainty. The +native-skill case covers the completion plan's skill-swap-before-MCP-commit +interruption under the disclosed existing primitive instrumentation. It does +not establish successful automatic reconciliation, vendor discovery, trust, +authentication, MCP initialization, tool execution or release qualification. diff --git a/docs/plans/project-install-e2e/project-removal-file-preflight-plan.md b/docs/plans/project-install-e2e/project-removal-file-preflight-plan.md new file mode 100644 index 000000000..997ce1540 --- /dev/null +++ b/docs/plans/project-install-e2e/project-removal-file-preflight-plan.md @@ -0,0 +1,85 @@ +# Project removal: observe owned native files before an attempt — R2 + +The new real Cline+Codex grouped removal test exposed a specific pre-existing +SDK gap. A foreign replacement of an owned native skill is preserved and removal +fails, but only inside confirmed deactivation, after core beginNativeAttempt has +persisted a pending attempt. Saved state changes although no native file effect +was performed. The existing single/group SDK removalPreflight validates the +managed package and selected-delivery entries; historical zero-selected project +component files are not observed there. Do not weaken the foreign-file test, +clear the attempt after failure or change generic effect/recovery semantics. + +## Reuse the already mandatory prepared observation + +Add a private installer helper called from existing removalPreflight after the +independently resolved exact target and managed package digest have passed, and +before PLUGIN_DATA checks finish. Limit it to explicit configured project scope, +zero selected delivery and existing scoped LifecycleNativeConfig. Other scopes, +Prepared/CLIRegistry lifecycles and selected native-entry preflight retain their +existing paths. No client ID list, new port/schema or native executable. + +Build only the observation DeliveryPlan from the existing resolved DeliveryTarget +and saved binding: exact target context/root/anchor/active/native root, client and +project scope, physical artifact, saved selected delivery, saved previous native +objects and existing profile authority/namespace. Thread the actual installation +DeclaredName through the existing private removalPreflight call sites; do not +invent a name, reparse moving source or re-plan desired components. Components +are empty because removal inspects the recorded set only. Existing adapter +ProjectComponents.Inspect first verifies every prior object with allowMissing; +unrelated native entries remain outside ownership and are preserved. + +Invoke the existing providers.NativeIdentityObserver.ObservePreparedIdentity with +real Stager, nativeconfig kernel and registry, with Runner omitted. It already +validates canonical project/original user context and combines prepared registry +and scoped file ownership. Require no error, NativeIdentityManaged, exact recorded +managed digest and ReceiptReconciled, with NativeDiscoveryState exactly +NativeIdentityAbsent and neither NativeDiscoveryAttempted nor +NativeDiscoveryReconciled. An empty or unknown discovery state is not absence. Unknown, collision, mismatch, unavailable inspector or +read errors refuse. Missing native objects retain existing allowed absence; +a missing/changed managed package still refuses under the earlier package guard. +No observation becomes native discovery or runtime/activation evidence. + +All existing Prepare, Apply, under-lock confirmation and immediately-pre-dispatch +calls go through the same removalPreflight helper, so this check repeats at each +existing fence. Thread the name through confirmRemovalPreflight using its exact +approved installation from already-loaded state, not an extra mutable lookup. +Clone the supplied ownership slice and observation/authority values so an +inspector cannot mutate the frozen in-memory comparison basis. Keep existing +whole-state/journal callback comparisons and profile checks in place. + +This is a repeated read-only fence, not exclusion of every concurrent filesystem +writer. A foreign change after the last pre-attempt observation may still be +refused inside the native adapter after an attempt has been persisted. Preserve +that pending evidence and uncertainty; never clear it to manufacture a no-change +result. The no-attempt acceptance cases concern foreign content already present +at preparation or introduced after Prepare but before Apply observes the files. + +## Finite acceptance + +- Real single and grouped Cline/Codex removal reject foreign native skill or MCP + bytes before any pending attempt, saved-state/journal change or deactivation; + original user/B and every unrelated native member remain identical, runner0. +- Repeat these failures after Prepare and with the second group target foreign, + proving no first target is removed before the later target is checked. +- Missing owned native objects can still be removed when the managed package is + intact; actual normal, one-absent and repeated group cleanup remain passing. +- Keep actual managed-package drift, profile/binding changes, pending journal, + missing PLUGIN_DATA and selected-delivery guards. A swapped saved profile that + remains inside the project must be refused; outside-project receipt saving is + already rejected by the store and is not bypassed for a test. +- Narrow recording-inspector negatives check exact context/receipts/no runner, + malformed/unknown findings, and unchanged nonproject/nonNativeConfig routes. +- Full-file installer architecture and changed-source correctness lint remain + mandatory. This is early detection of existing refusal, not new removal + authority or a generic native-effect protocol correction. + +Two independent plan rounds precede this shared preflight source edit, and two +frozen code reviews follow. The group admission change remains a separate slice; +its strong foreign-file acceptance is currently failing as described above. + +## Independent R1 critique resolved + +Both APM and Skillshare reviewed R1 commit ce4c9443e49986f03120bbc789f72afe7c769204 +and gave architecture GO. R2 makes the positive absent discovery-state requirement +explicit and limits the no-attempt claim to the actual observation boundaries. +No source or native effect contract changed during either plan review. diff --git a/docs/plans/project-install-e2e/project-retained-data-guidance-code-r1.md b/docs/plans/project-install-e2e/project-retained-data-guidance-code-r1.md new file mode 100644 index 000000000..e2664e288 --- /dev/null +++ b/docs/plans/project-install-e2e/project-retained-data-guidance-code-r1.md @@ -0,0 +1,11 @@ +# Project retained-data follow-up correction — independent code R1 + +**GO** at frozen `33004e06cf5d65f112efad0455cc793bc0a56f39` in work/uap-project-followup-guidance, reviewer codex_claude, 2026-10-23. Reviewed diff from b485a24a, where both plan GO reports were committed before source. No production code changed by reviewer or vendor/native process executed. + +Both actual producers now call the same small retainedDataPurgeCommand renderer. Its user result is byte-for-byte the prior command, and the surrounding JSON/human words remain unchanged. Project rendering appends explicit scope plus the validated canonical selected App.ProjectRoot through the existing quoteResumeArgument convention. Saved renderNextAction receives the already selected App and uses its environmentID for the same alias/target test; it does not fall back to raw options, cwd, a receipt/package locator or another namespace. Selector, target/attestation omission, JSON keys, ownership/confirmation/purge logic and portable source/manifest/lock/receipt identities are unchanged. + +The added public test meaningfully covers imperative JSON, imperative human and saved human producers. It creates populated user/A/B installations and retained data, with A containing spaces, apostrophes and a literal dollar sign. A bounded inert POSIX set/printf parses generated word boundaries without running agentplugins/purge in a shell; exact argv preserves the installation ID, project scope and one complete root argument. The public CLI replays those argv after changing to an unrelated cwd, verifies only A's retained receipt/data is purged, and compares complete user/home and B inventories (including shared cache). Existing user assertions are not weakened. sh absence skips only this POSIX presentation proof and does not imply Windows shell qualification. + +Independent three-route public test PASS2.004s with pinned Go1.26.10, GOMAXPROCS3/-p2/-count1. Author evidence reports affected existing user/saved/removal tests PASS0.075s and both pinned2.13.2 lint gates zero; those existing-suite/lint results were not independently rerun because the production diff is only render/context plumbing and the new public replay directly resolves its concrete risk. No additional broad/native test needed. + +No blocker found. This correction fixes operational guidance exposed by the genuine Codex artifact; it does not expand native, all-client or release qualification. diff --git a/docs/plans/project-install-e2e/project-retained-data-guidance-code-r2.md b/docs/plans/project-install-e2e/project-retained-data-guidance-code-r2.md new file mode 100644 index 000000000..963b5c3ab --- /dev/null +++ b/docs/plans/project-install-e2e/project-retained-data-guidance-code-r2.md @@ -0,0 +1,13 @@ +# Retained project data guidance — independent code R2 + +**GO** at frozen clean `33004e06cf5d65f112efad0455cc793bc0a56f39` in `work/uap-project-followup-guidance`, reviewed by cursor_clients on 2026-10-23. The five-file diff from `b485a24a` is the small rendering helper, its two producer integrations, one public regression file and evidence note. Both plan GO reports precede source. No native/client process or production purge behavior changes are introduced. + +The helper starts with exactly the original user command and adds scope/root only when the already selected App is project-scoped. Its root is App.ProjectRoot, not raw options/cwd/a receipt-derived path. Existing quoteResumeArgument supplies the established POSIX word presentation. The installation selector stays the same. Imperative recordRetainedData now inserts that command into the same JSON/human sentence. Saved lifecycle passes its selected App into renderNextAction, preserves the exact same app.environmentID() alias/target selection, and inserts the helper into the unchanged surrounding human sentence. JSON keys/order, retained receipt fields, portable manifests/locks, native operations, confirmation and purge ownership checks are untouched. + +The public regression separately exercises imperative JSON, imperative human and saved human removal. It uses the production source/CLI/store/planner/installer composition with user/A/B data markers; A's canonical root has spaces, apostrophes and a literal dollar sign. The emitted command is passed only to inert POSIX set/printf to observe actual argument boundaries; it never launches agentplugins or purge through a shell. The exact expected argv includes the original installation ID plus explicit project scope and one unchanged root argument. Replaying that argv through the public in-process CLI after changing to an unrelated cwd purges A's owned retained data and removes its receipt. Whole user/home and B inventories prove other native/config/state/data and the shared cache remain unchanged. The sh availability gate and evidence note correctly avoid a universal Windows-shell claim. + +Independently traced the selected App's earlier forOptions canonicalization/composition/namespace checks and both renderer call sites. User scope takes the same constant command bytes as before; both original sentences are byte-equivalent after formatting. Existing no-purge-before-binding-removal, incomplete removal and owned purge logic are unchanged. The only saved-render signature caller was updated coherently, preserving its existing environment ID. + +Independent verification: all three public project guidance routes passed in2.130s. Affected existing user/grouped/saved removal, purge and lifecycle selections passed in6.258s. git diff --check passed. Exact source inspection confirms the change does not execute the follow-up automatically, introduce flags or add project roots to receipt/portable fields. No vendor session, trust/auth, model or MCP process was run. + +No blocking implementation finding remains. Root can coordinate feature-stack publication after both code review reports are committed. This corrects the genuine project-removal artifact's operational hint; it adds no native runtime, all-client or release qualification. diff --git a/docs/plans/project-install-e2e/project-retained-data-guidance-evidence.md b/docs/plans/project-install-e2e/project-retained-data-guidance-evidence.md new file mode 100644 index 000000000..8201b7cca --- /dev/null +++ b/docs/plans/project-install-e2e/project-retained-data-guidance-evidence.md @@ -0,0 +1,49 @@ +# Retained-data project follow-up correction — implementation evidence + +2026-10-23. The separately reviewed plan and both independent GO reports were +committed at `b485a24a` before source changes. This is an output correction for +the genuine Codex project-removal artifact's missing scope/root, with no change +to removal, confirmation, retained-data ownership or native client behavior. + +The shared CLI renderer retains the exact existing user command. In project +scope it appends explicit project scope and the selected canonical App root, +using the existing argument-quoting convention. Both producers use it: +imperative grouped removal's JSON/human retained_data_action and the human +saved-remove hint. Saved rendering now receives its selected App and continues +using that App's environment ID for alias/target selection. Portable formats, +receipt fields and user output do not gain project roots. + +The new public composition regression exercises imperative JSON, imperative +human and saved human removal separately. Each installs user/A/B with retained +data markers, with A's root containing spaces, an apostrophe and a literal +dollar sign. An inert POSIX `set`/`printf` step captures the emitted command's +actual word boundaries; it never executes agentplugins or purge through a +shell. The captured argv must exactly retain the installation ID, project scope +and root. Feeding those argv to the public CLI from an unrelated cwd purges +only A's owned retained data/receipt. Complete user/home and B snapshots also +cover other native/state/data bytes and shared immutable cache preservation. +The presentation proof requires sh and makes no universal Windows-shell claim. + +The initial test compilation hit actual shared RAM ENOSPC before execution; +root reclaimed only old reproducible task-cache entries and the bounded rerun +completed. The first executed fixture attempt used a nonexistent remove --yes +flag; the test was corrected to the existing explicit-target command contract +without changing production confirmation or guards. All three public routes +then passed in **2.046s**. The final rerun after test-only lint corrections passed +in **1.987s**. + +The selected existing user/grouped-removal, saved-removal and ownership-checked +purge regressions passed in **0.075s**. Existing path-free user JSON and human +wording assertions remain unchanged. Exact pinned golangci-lint **2.13.2** +changed-code and whole-file size/complexity/architecture gates both report +**0 issues**. Python/native/client execution is unnecessary for this correction; +no vendor process, trust/auth action, model or MCP payload was run. + +Both independent implementation reviews are **GO** at frozen source +`33004e06cf5d65f112efad0455cc793bc0a56f39`, with complete reports committed beside +this note. R1 independently replayed all three public routes in 2.004s; R2 did +so in 2.130s and also passed affected existing user/saved/removal tests. Both +verified exact selected App context and unchanged user sentence bytes. No +production correction was required after review. Root owns feature-stack +publication. No native, all-client E2E or release qualification is added by +this correction. diff --git a/docs/plans/project-install-e2e/project-retained-data-guidance-plan-r1.md b/docs/plans/project-install-e2e/project-retained-data-guidance-plan-r1.md new file mode 100644 index 000000000..1950d4b70 --- /dev/null +++ b/docs/plans/project-install-e2e/project-retained-data-guidance-plan-r1.md @@ -0,0 +1,11 @@ +# Retained-data project follow-up guidance — independent plan R1 + +**GO** at docs-only frozen `35e225b98d4c1802dbee70a7945577d10bf3bd59` in `work/uap-project-followup-guidance`, reviewer codex_claude, 2026-10-23. No production/test code changed or native process executed during review. + +Independently traced both actual producers: removeManySession.recordRetainedData builds JSON/human RetainedDataAction without selected context, and savedLifecycleSelection.renderNextAction separately formats the saved human hint. The downloaded genuine Codex macOS project-remove JSON indeed records a project receipt while emitting a default-user command. Therefore including only project scope would still leave future cwd ambiguity; the plan correctly carries both explicit scope and the canonical selected App.ProjectRoot. + +One small shared render helper is appropriate. Retain the exact existing user command and surrounding words, selector and JSON schema; use existing quoteResumeArgument for the operational root argument without introducing a new shell dialect. Thread selected App/root through the saved producer alongside its existing environment ID. Do not infer the root from paths, receipts, raw options or cwd, and do not add target/attestation that could alter purge semantics. No manifest/lock/source/receipt identity changes or automatic deletion is needed. + +The planned tests exercise the real composed public CLI, not a string-only mirror: project A root with spaces, user and B data/native/state/store sentinels, separate imperative JSON/human and saved-human producers, command argument capture under the existing POSIX presentation convention, and public replay from an unrelated cwd with ordinary explicit confirmation. Exact user bytes and incomplete-removal/no-early-purge behavior remain covered. This directly demonstrates both context selection and actual A-only purge; existing shell grammar is not overclaimed as universal Windows shell support. + +Bounded affected tests, exact changed/whole-file lint and two independent implementation reviews are sufficient. This output correction neither requires another vendor run nor establishes broader native/all-client/release qualification. No plan blocker found. diff --git a/docs/plans/project-install-e2e/project-retained-data-guidance-plan-r2.md b/docs/plans/project-install-e2e/project-retained-data-guidance-plan-r2.md new file mode 100644 index 000000000..461c7f0ed --- /dev/null +++ b/docs/plans/project-install-e2e/project-retained-data-guidance-plan-r2.md @@ -0,0 +1,13 @@ +# Retained project data guidance — independent plan R2 + +**GO** for docs-only frozen `35e225b98d4c1802dbee70a7945577d10bf3bd59` in `work/uap-project-followup-guidance`, reviewed by cursor_clients on 2026-10-23. No production code/test changed and no native process was executed. This is a narrow operational output correction under the existing project/removal contract; both plan GO reports must precede implementation and final source still needs two independent code reviews. + +Independently opened the genuine `research/hosted-evidence/codex-darwin-40011489662/project-remove-installer.json`. It reports successful project binding removal, an owned retained project data receipt, and a follow-up command with neither project scope nor root. Replaying that command under the default user scope cannot select its original project namespace. Adding scope alone would remain cwd-dependent. The plan correctly binds both fields to the previously validated selected App. + +Traced both actual producers: `removeManySession.recordRetainedData` creates the JSON/human RetainedDataAction after reloading retained installation state; `savedLifecycleSelection.renderNextAction` prints a separate human saved-removal hint. The saved lifecycle caller already has the selected App and currently passes only its environment ID. `App.forOptions` canonicalizes the requested root, composes the selected runtime, verifies exact scope/root agreement and checks namespace/target admission before the lifecycle. Passing that selected App or its validated context into rendering preserves this existing authority instead of consulting future cwd, raw flags, native receipt paths or inferred parents. + +The proposed helper retains the existing user command exactly and appends explicit `--scope project --project-root ` only for the selected project. The existing `quoteResumeArgument` is a POSIX presentation convention: simple safe values remain bare and single quotes use the standard escaped apostrophe sequence. Reusing that helper does not establish cmd.exe/PowerShell qualification, and the plan accurately limits its claim. The installation selector and surrounding user wording/JSON keys/order remain intact; there is no new target or external-uninstalled attestation in the purge follow-up. + +The public verification is adequate and exercises the real defect. Separate imperative JSON/human and saved human removal paths must emit their project command. Parse a generated space-containing root as one shell argument using an inert argument-capture step, then replay the resulting argument vector through the public CLI from an unrelated cwd with ordinary explicit confirmation. Prove that only A's retained receipt/data is purged while user/B native files, state and owned markers plus the immutable shared store remain unchanged. Apostrophe/metacharacter examples cover the existing quoting convention; they must never execute an emitted purge command through a shell. Both user producers remain byte-identical and path-free, with incomplete-removal/no-premature-purge behavior preserved. + +No native transaction, portable manifest/lock/source identity, stored receipt, default user output, flag, cleanup policy or automatic deletion changes are needed. Root appears only in the operational project command where namespace selection requires it. Focused public regressions, affected existing user removal/saved lifecycle tests and the stated changed/full-file lint checks are sufficient; a broad native rerun is unnecessary for this rendering repair. No blocking plan finding remains, and no native runtime/all-client/release qualification follows from this GO. diff --git a/docs/plans/project-install-e2e/project-retained-data-guidance-plan.md b/docs/plans/project-install-e2e/project-retained-data-guidance-plan.md new file mode 100644 index 000000000..519aa9e63 --- /dev/null +++ b/docs/plans/project-install-e2e/project-retained-data-guidance-plan.md @@ -0,0 +1,102 @@ +# Preserve selected project context in retained-data follow-up commands + +Pre-code correction plan, 2026-10-23. This supplements the approved core scope +contract and its removal acceptance case. Root requested a concrete correction +after a genuine Codex macOS project-removal artifact exposed the wrong hint. +No source or test implementation is changed at this checkpoint. Both independent +plan/fix GO reports must be committed before implementation. + +## Evidence and exact render paths + +The actual hosted artifact +`research/hosted-evidence/codex-darwin-40011489662/project-remove-installer.json` +records a project data receipt and successful retained-data removal, but its +`retained_data_action` says only: + +`agentplugins remove --purge-data` + +The default CLI scope is user, so copying that instruction loses the selected +project and cannot address its retained receipt. It is also dependent on the +future invocation cwd if corrected with project scope alone. No new native run +is needed to establish this rendering defect. + +The bounded source audit found two producers: + +1. `removeManySession.recordRetainedData` in + `cli/internal/agentpluginscli/remove_multi.go` builds RetainedDataAction, + which is emitted in JSON and human `Next:` output. +2. `savedLifecycleSelection.renderNextAction` in + `cli/internal/agentpluginscli/saved_lifecycle.go` emits the human saved-remove + `PLUGIN_DATA preserved; after removal completes, run ...` hint. + +The selected App already owns canonical scope/root after `forOptions` and +production ForScope composition. The first producer has that App; the saved +lifecycle caller has it before rendering. The render method currently receives +only the environment ID rather than the selected root. Existing user tests in +`cli_test.go` assert the current words and path-free user result. The existing +`quoteResumeArgument` helper supplies the repository's shell-escaped command +presentation contract for spaces, apostrophes and metacharacters. + +## Narrow implementation + +Add one small CLI rendering helper for the retained-data purge command. Its +user result remains exactly `agentplugins remove --purge-data`. +For a selected project App it appends both explicit `--scope project` and +`--project-root `. Use the original validated +App.ProjectRoot, not raw option text, the current cwd, a managed package path, +a receipt locator or an inferred parent. Quote the argument with the existing +`quoteResumeArgument` convention; do not interpolate an unescaped root into a +shell fragment or invent a new platform shell grammar. + +Use this helper in both producers. Thread the selected App or its already +validated scope/root into saved renderNextAction, retaining the selected +environment ID used for alias/target selection. Keep the surrounding user words, +JSON keys, ordering, installation selector and post-removal purge policy. +Do not add a target or external-uninstalled attestation to the hint: the +follow-up acts only after bindings are removed under existing ownership rules. + +The root appears only in an operational project follow-up command, where it is +needed to select the original namespace. It must not enter manifests, locks, +source identity, persisted receipts, path-free retained-data receipt objects or +default user output. No automatic command execution, data deletion, native +cleanup, trust/auth flow, new flag, new dependency or lifecycle behavior is added. +Actual explicit purge remains guarded by the existing selected-scope receipt +ownership and confirmation rules. + +## Meaningful verification + +Extend the real public CLI composition fixtures with a selected project A whose +directory name contains spaces, alongside a populated user environment and +project B. Use the existing inert host/security boundary and production loader, +store, planner, installer and native file transactions; no genuine vendor +session is required for this output correction. + +Exercise the two paths separately: + +- Imperative local/no-save add and removal produce JSON retained_data_action + and the matching human command. Assert the selected project scope/root is + retained as one escaped argument, with the installation ID unchanged. +- Saved add and human saved removal produce the context-preserving saved hint; + do not silently skip that producer because JSON saved output has no hint. +- Reuse the emitted project command from an unrelated cwd through the public + CLI selection path, with normal explicit confirmation where required, and + prove only A's retained data/receipt is purged. Preserve user and B native, + state, owned data markers and the shared immutable store. Snapshot other + namespaces before the follow-up to detect a wrong default-user selection. +- Verify the actual shell word boundary for a space-containing root rather than + merely searching for a quoted substring. On POSIX hosts, an inert shell + argument-capture step can parse only the generated fixed command and return + its arguments for the public in-process CLI; it must never launch the purge + command through a shell. Cover apostrophe/metacharacter escaping under the + existing POSIX presentation convention without claiming universal cmd.exe or + PowerShell quoting qualification. +- Retain the established user tests and assert both user hints remain byte-for- + byte unchanged, including no project flags/root leakage. Preserve existing + failure/incomplete-removal wording and the no-purge-before-removal guard. + +Run only the new public regressions and affected existing user removal/saved +lifecycle tests, plus exact changed/whole-file lint and diff checks. No broad +native matrix or new vendor process is needed. Freeze the implementation for +two independent code reviews and fixes, commit their reports, then send root a +clean SHA for coordinated feature-stack publication. This correction makes no +new native, all-client E2E or release qualification claim. diff --git a/docs/plans/project-install-e2e/project-stack-ci-routing-amendment.md b/docs/plans/project-install-e2e/project-stack-ci-routing-amendment.md new file mode 100644 index 000000000..90451216c --- /dev/null +++ b/docs/plans/project-install-e2e/project-stack-ci-routing-amendment.md @@ -0,0 +1,47 @@ +# Project stack CI routing amendment + +Research and plan, 2026-10-23. This changes only which pull-request base +branches receive existing CI jobs; it does not broaden native runtime authority. + +## Observed gap + +The native client workflow currently matches only pull requests targeting main. +The existing core workflow additionally matches the exact project/source feature +bases, but misses temporary project client and integration checkpoint bases. +Consequently a source-only native project test can be authored and reviewed in +a client leaf without ever running in its stacked pull request. Retargeting that +leaf to the project feature alone would still not trigger the native workflow. + +## Proposed change + +1. Extend both existing pull-request branch filters to the task-owned + `feat/project-*` stack and `feat/source-transports-e2e`. Preserve main and the + core workflow's existing `refactor/**` filter. +2. Retain all path filters, events, permissions, action pins, timeouts, jobs, + platforms, exact source checkout and credential handling. +3. Preserve the existing disposable GitHub-hosted runtime guard and all + historical release/draft/source-selection restrictions. Never fabricate + runner environment locally, dispatch native payloads locally, or skip a + required native test to obtain a passing result. +4. Carry the additive routing change to the active client leaves through true + merges so their next publication triggers the applicable jobs. Reconcile + the Codex and Claude source-only selections by union; retain both existing + exact-selection tests and the legacy required suites. +5. Keep successful package configuration, genuine native discovery, MCP runtime + and release qualification as separate evidence. A queued or absent run is + not a pass. GitHub client PR merges remain into the project feature only. + +## Validation and review + +Two independent plan review/fix rounds are required before changing workflow +code. Verify the final diff is an additive base-routing change, run the native +matrix selector/guard contracts, and observe actual pull-request Actions runs +on the exact published client heads. Cross-platform project contracts and +native source tests must retain their original assertions and failure behavior. +No test, lint, baseline, schema digest, trust, or authentication gate is weakened. + +## Scope + +The temporary checkpoint branch is internal to this task. It creates no third +feature PR and is not a release channel. The two deliverable feature PRs remain +sources and project installation; each client retains its own reviewed stack PR. diff --git a/docs/plans/project-install-e2e/project-stack-ci-routing-code-r1.md b/docs/plans/project-install-e2e/project-stack-ci-routing-code-r1.md new file mode 100644 index 000000000..a847d119e --- /dev/null +++ b/docs/plans/project-install-e2e/project-stack-ci-routing-code-r1.md @@ -0,0 +1,7 @@ +# Project stack CI routing — independent implementation R1 + +Reviewed frozen implementation `7a3513673451c93a3d0e0e332e6719886c9d1f4c` against its parent on 2026-10-23, after the separately reviewed routing plan. **GO.** + +The entire commit changes exactly two branch-filter lines: native client pull requests gain `feat/project-*` and `feat/source-transports-e2e`; core replaces the exact project feature base with the superset `feat/project-*`, retaining main, source and `refactor/**`. This implements the approved additive routing and admits the task-owned flat client/checkpoint bases. + +No path filter, event, job, matrix, action pin, permission, timeout, checkout/source selection, credential policy, native guard, release restriction or assertion changes. The real disposable-host/native-platform guard remains untouched. The implementation introduces no executable/native logic that needs additional local execution; parent independently reports the existing native matrix contracts passing (18 tests). Actual exact-head hosted runs remain required evidence and are not implied by this routing GO. diff --git a/docs/plans/project-install-e2e/project-stack-ci-routing-code-r2.md b/docs/plans/project-install-e2e/project-stack-ci-routing-code-r2.md new file mode 100644 index 000000000..bea169db6 --- /dev/null +++ b/docs/plans/project-install-e2e/project-stack-ci-routing-code-r2.md @@ -0,0 +1,26 @@ +# Project stack CI routing implementation independent review R2 + +Reviewer: cursor_clients, 2026-10-23. Frozen code: +`7a3513673451c93a3d0e0e332e6719886c9d1f4c`. + +## Decision: GO + +The exact commit changes only two pull-request branch-filter lines, matching +the independently approved routing plan. Native Client Runtime adds +`feat/project-*` and `feat/source-transports-e2e` alongside main. Core fast +replaces its exact project feature spelling with the encompassing +`feat/project-*` and retains main, refactor/** and the source feature. + +All other workflow bytes are unchanged: events/path filters, permissions, +action pins, jobs/matrices/platforms, timeouts, source checkout, credential +handling, actual test commands and evidence uploads. The patch changes no +runner, fixture, assertion, lint baseline, release condition or native guard. +The integrated Codex/Claude source-only selection union and distinct selector +tests remain intact. Temporary stack admission does not create a release +channel or expand native process authority. + +Independent bounded verification: `python3 -m unittest discover -s scripts -p +test_native_client_matrix.py` — **18 tests PASS**. Inspected the complete +two-file diff and changed-path list. No native payload was executed or hosted +environment fabricated. Actual Actions execution on published exact client +heads remains pending; this code review does not claim those runs passed. diff --git a/docs/plans/project-install-e2e/project-stack-ci-routing-plan-r1.md b/docs/plans/project-install-e2e/project-stack-ci-routing-plan-r1.md new file mode 100644 index 000000000..f15b0f2b1 --- /dev/null +++ b/docs/plans/project-install-e2e/project-stack-ci-routing-plan-r1.md @@ -0,0 +1,11 @@ +# Project stack CI routing amendment — independent plan R1 + +Reviewed frozen plan `94be629c7d3a3675828214f6f84659f9ddc7a0b4`, the existing `.github/workflows/core-fast.yml` and `.github/workflows/agentplugins-native-clients.yml`, and `scripts/run-native-client-matrix.py` on 2026-10-23. **GO for the additive routing implementation.** No workflow code was changed for this review. + +The observed gap is present: the native workflow's pull-request base filter is `[main]`; core additionally admits the exact project/source feature bases and `refactor/**`, but omits other project stack bases. GitHub pull-request branch filters select the base branch, so the planned `feat/project-*` addition admits the task's flat client/checkpoint bases and subsumes the existing exact project base. Adding `feat/source-transports-e2e` to native admits the second authorized feature stack. `main` and core's `refactor/**` remain admitted. + +The amendment is limited to existing workflow base routing. It explicitly preserves path filters, dispatch events, permissions, pinned actions, jobs/platforms/timeouts, exact source checkout and credential handling, native guards, source/draft/release selection, and required assertions. The existing real guard still requires explicit disposable GitHub-hosted admission and the actual native target; routing does not grant local execution authority. The Codex/Claude source-selector union is described as preservation of both already reviewed selections, with exact selector tests retained, rather than a new runtime capability. + +The proposed validation is sufficient for this bounded change: inspect the final additive filter diff, run existing selector/guard contracts, and observe actual Actions runs on exact published heads. Missing or queued jobs remain pending evidence; configuration, discovery, runtime and release qualification remain separate. Carrying the change by true merges preserves stack ancestry and does not authorize merges into main/release. + +No requested plan correction remains. Implementation still requires the second independent plan GO before editing workflows and the stated bounded verification afterward. diff --git a/docs/plans/project-install-e2e/project-stack-ci-routing-plan-r2.md b/docs/plans/project-install-e2e/project-stack-ci-routing-plan-r2.md new file mode 100644 index 000000000..a11e10a65 --- /dev/null +++ b/docs/plans/project-install-e2e/project-stack-ci-routing-plan-r2.md @@ -0,0 +1,37 @@ +# Project stack CI routing amendment independent plan review R2 + +Reviewer: cursor_clients, 2026-10-23. Reviewed pre-implementation plan at +`94be629c7d3a3675828214f6f84659f9ddc7a0b4`: +`docs/plans/project-install-e2e/project-stack-ci-routing-amendment.md`. +Inspected current `.github/workflows/agentplugins-native-clients.yml`, +`.github/workflows/core-fast.yml`, the merged native selector and its tests. + +## Decision: GO for additive routing implementation + +The observed routing gap is real: the current native workflow accepts only +main as a pull-request base, and core-fast accepts the exact project/source +feature bases but misses task-owned temporary project client/integration bases. +Adding `feat/project-*` and `feat/source-transports-e2e` matches the intended +stack. The wildcard also retains the existing exact project feature base; +main and core-fast's refactor/** filter must remain. + +The plan changes only pull-request base admission. Existing path filters, +events, permissions, action pins, platform matrices, jobs, tests, timeouts, +source checkout and credential controls remain intact. It explicitly preserves +the disposable-hosted opt-in/platform guard and all draft, release, historical +and source-selection restrictions. Routing to a branch cannot itself confer +native authority or qualify absent/queued evidence. + +The Codex/Claude selector union and both distinct exact-selection tests are +already preserved in integration `daa705ad`. Independent bounded checks passed +18 matrix tests and 42 released-evidence tests. The additive routing patch must +retain those assertions and the original legacy required suites. Carrying it +through true merges into active leaves is consistent with the reviewed stack; +the temporary checkpoint creates neither another deliverable feature nor a +release channel. Client PR merges remain into the project feature only. + +No plan blocker found. After both independent plan GO reports are committed, +implementation review must check the exact workflow diff is limited to these +branch-filter additions, rerun the selector/guard contracts, and observe actual +Actions runs on the published exact client heads. An unrun native fixture or a +successful configuration fixture cannot be reported as genuine native proof. diff --git a/docs/plans/project-install-e2e/project-windows-identity-fix-evidence.md b/docs/plans/project-install-e2e/project-windows-identity-fix-evidence.md new file mode 100644 index 000000000..39539027a --- /dev/null +++ b/docs/plans/project-install-e2e/project-windows-identity-fix-evidence.md @@ -0,0 +1,47 @@ +# Project Windows directory identity correction + +Author: core_clients, 2026-10-23. Corrective scope follows the existing approved +project-core plan. The hosted project-contracts Windows job failed both retained +namespace replacement assertions and exact embedded-schema digest validation. +Linux/macOS project jobs passed at that checkpoint. + +## Production defect and correction + +Go 1.26.10 `src/os/types_windows.go` stores a pathname in an `Lstat` FileInfo +and defers obtaining the volume/file ID until `SameFile` invokes `loadFileId`. +Consequently a retained namespace FileInfo can observe a replacement at its old +path on its first comparison. The original directory remains present under its +renamed path in both failing fixtures, so this was not file-ID recycling. + +The guard now captures directory FileInfo using `os.Open` and `File.Stat`, +closing the handle before return. Its stored FileInfo contains the already +observed handle identity. Both selected-project and `.uap` observations use this +route. The existing pre-open real-directory check and SameFile consistency +check remain fail-closed, matching the established nativeconfig Cline lock +identity capture pattern. No long-lived handle, new lifecycle mechanism, +durable identity schema or relaxed replacement check was added. + +Existing namespace and preview replacement tests retain their exact refusal +assertions. A focused selected-project replacement contract additionally proves +that root identity is pinned and a refused replacement creates no `.uap`. + +The project-contracts checkout step now sets `core.autocrlf=false` through the +checkout process environment before files are written. This is the existing +exact-Git-byte pattern used by adjacent Windows lanes; expected embedded schema +digests and schema bytes remain unchanged. + +## Local verification + +Go 1.26.10, GOMAXPROCS=3, `-p=2`, exact project-focused regression selection: + +- installer: PASS 0.277s; +- internal production CLI: PASS 0.014s; +- public CLI composition: PASS 0.184s; +- golangci-lint 2.13.2 changed-code gate: 0 issues; +- golangci-lint 2.13.2 whole-file size/architecture gate: 0 issues; +- `git diff --check`: PASS. + +This task host is Linux. Genuine Windows validation requires rerunning the +unmodified hosted project-contracts lane; neither cross-compilation nor this +local run is a Windows runtime result. Independent corrective reviews remain +separate records. diff --git a/docs/plans/project-install-e2e/project-windows-identity-review-r1.md b/docs/plans/project-install-e2e/project-windows-identity-review-r1.md new file mode 100644 index 000000000..c58ac09a3 --- /dev/null +++ b/docs/plans/project-install-e2e/project-windows-identity-review-r1.md @@ -0,0 +1,36 @@ +# Project namespace Windows correction independent review R1 + +Reviewer: cursor_clients, 2026-10-23. Reviewed the working diff in +`work/uap-core-publication` for `installer/project_namespace.go`, its existing +namespace contract tests, and `.github/workflows/core-fast.yml`. The author +subsequently froze the same reviewed code plus its implementation evidence at +`c49a50542da071223e9a858096ca9f3508f2ac53`. + +## Decision: GO for the narrow correction + +The namespace guard now retains open-handle `File.Stat` identity for both the +selected project root and `.uap`, matching the established nativeconfig lock +directory identity pattern in this repository. This addresses Windows' lazy +path-based file-ID population in an earlier retained `Lstat` FileInfo: a later +SameFile call must compare the originally captured physical directory rather +than reopen its old name after replacement. + +The helper keeps the before-open Lstat real-directory/no-symlink admission, +compares the opened handle against that observation, rejects an unexpected +directory change with ErrPlanChanged, handles Stat/Close failures, and returns +the eager handle snapshot for subsequent guard comparisons. Existing exact +namespace layout and containment checks are preserved. A missing namespace +remains permissible until first creation; once pinned, disappearance or +replacement refuses. Both original failing contracts remain intact, and the +new selected-project replacement test additionally proves refusal before a +new `.uap` is created in the replacement root. + +The checkout change sets core.autocrlf=false for the project-contract checkout, +preserving Git bytes for embedded schema digest checks on Windows. It changes +neither schema bytes nor native execution/approval guards. + +Independent verification: complete `installer -run '^TestProjectNamespace' +-count=1` selection PASS, 0.010s on Linux with Go 1.26.10. Inspected the new +helper alongside `nativeconfig/lock.go`'s existing handle identity capture. +No Windows runtime was available to this review; the genuine hosted Windows +result remains the platform verification gate, not a claim supplied here. diff --git a/docs/plans/project-install-e2e/project-windows-identity-review-r2.md b/docs/plans/project-install-e2e/project-windows-identity-review-r2.md new file mode 100644 index 000000000..70e40ef29 --- /dev/null +++ b/docs/plans/project-install-e2e/project-windows-identity-review-r2.md @@ -0,0 +1,63 @@ +# Project Windows namespace identity correction — independent R2 + +Reviewer: codex_claude, 2026-10-23. Frozen correction reviewed: +`c49a50542da071223e9a858096ca9f3508f2ac53` in +`work/uap-core-publication`. Independent of the correction's author and R1. + +## Decision: GO for the narrow corrective implementation + +No blocker found. The correction pins the physical identity actually obtained +from the open directory handle, rather than retaining Windows' deferred +pathname-only `Lstat` identity across later namespace replacement. The existing +project-root and `.uap` refusal contracts are preserved. Genuine Windows CI is +still the platform runtime verification gate; this review does not replace it. + +## Independent source trace + +Inspected the installed, pinned Go 1.26.10 source directly: + +- `src/os/stat_windows.go`: `File.Stat` calls `statHandle` using the open handle; + ordinary directory handles reach `newFileStatFromGetFileInformationByHandle`. +- `src/os/types_windows.go`: that constructor immediately fills the volume + serial and high/low file indexes, leaving `fileStat.path` empty specifically + to prevent a later `SameFile` from fetching identity again by pathname. +- In contrast, ordinary `Lstat` can take the fast attribute path, call + `saveInfoFromPath`, and retain only the old pathname. `sameFile` calls + `loadFileId`, which reopens that stored pathname when it is nonempty. A renamed + original plus a replacement at the old path can therefore make an earlier, + still-lazy observation refer to the replacement on its first comparison. + +`projectDirectoryIdentity` now returns the eager handle-derived FileInfo for +both retained guard observations. The opened handle is closed before return; +Stat and Close failures are joined and returned. The pre-open real-directory +and no-symlink checks, opened-directory check and `SameFile` consistency check +remain. Its shape matches the established Cline lock-directory identity helper +in `adapters/nativeconfig/lock.go`; it adds no durable identity format, +long-lived handle, lifecycle engine or relaxed replacement check. + +## Scope and fixture checks + +- The guard still requires exact selected `/.uap` state authority, + preserves containment checks for all mutable/temporary paths, and shares its + pinned observations across preview engine views. +- A not-yet-created `.uap` remains allowed. Once a namespace exists and is + pinned, disappearance or a different physical directory refuses with the + retained `ErrPlanChanged` contract. +- The test diff is strictly additive. The original namespace replacement and + preview-view assertions are unchanged; no skip, weakened expectation, + deletion, workaround platform branch or regenerated expected digest was added. +- The new selected-project replacement fixture retains the original under a + renamed path, creates a replacement, requires `ErrPlanChanged`, and checks + that refusal did not create `.uap` in the replacement project. It therefore + does not depend on file-ID recycling behavior. +- The project-contracts checkout sets `core.autocrlf=false` in Git's process + configuration before checkout writes the working tree. This matches adjacent + Windows job configuration. Schema source and pinned embedded-schema digest + constants are unchanged; the fix preserves their exact-Git-byte contract. +- `git diff --check c49a5054^ c49a5054`: PASS. Worktree was clean on inspection. + +Root requested static review while coordinating scarce build-cache headroom; +this R2 did not launch another compile or claim a new local runtime result. +Author/R1 results remain attributed to their separate records. Re-run the +unchanged hosted project-contracts Windows lane on the published corrective +commit before claiming Windows runtime success. diff --git a/docs/plans/project-install-e2e/qoder-capture-limit-amendment.md b/docs/plans/project-install-e2e/qoder-capture-limit-amendment.md new file mode 100644 index 000000000..c91f43e31 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-capture-limit-amendment.md @@ -0,0 +1,75 @@ +# Qoder phase-one capture correction — restored pre-code amendment + +This restores the correction approved before the runtime reset, from retained +conversation context. Original final plan d53a9de8 and historical R1/R2 GOs +preceded lost source24ace90d. New reconstructed source must receive fresh +implementation reviews and tests; original executions are not current proof. +Obtain two fresh plan confirmations for this recovered checkpoint before code. + +## Observed defect and unchanged native prerequisite + +Genuine run40011845496/artifact12532010944 failed with the identical83-byte +native extraction EFBIG warning on all six commands. The actual downloaded +ZIP digest918333b4690c1076d1714a19e036c7e66d62625d58881ae216449590c980ac54 +has been verified again after reset, and all thirteen original members are +restored unchanged under docs/evidence/project-install/qoder-40011845496/. +The inherited child-wide RLIMIT_FSIZE1MiB also limits native cache files. +Do not ignore warnings, lift the stream cap or relabel this failed collection. + +## Exact baseline and finite source changes + +Recover the shared-helper factoring from the published Grok branch's exact +commit249a9e5731822b79df3b07d9da0ef9b432258caa: only +scripts/native_client_research.py and scripts/research-qoder-native.py. +The published Grok implementation/reviews document mechanical extraction of +the original Qoder helpers from reviewed3107bced. Verify initial file equality +to that published tree; import no Grok adapter, workflow or other client code. + +Correct the shared capture helper, Qoder's strict stderr check, focused inert +tests and the existing Qoder workflow's helper/test trigger and safety-step +paths. Preserve genuine require_hosted before all effects, pins, acquisition, +explicit fresh profile/environment, exact checkout, contents:read, disabled +credential persistence, ten-minute job timeout and exactly the six existing +read-only version/help/empty-list argv. No local vendor/native execution. + +## Live bounds and cleanup + +Replace output files directly attached to child descriptors and RLIMIT_FSIZE +with parent-owned stdout/stderr nonblocking pipes and a selector. Retain +RLIMIT_CORE0 only. Each read is at most64KiB and at most the remaining per-stream +allowance plus one overflow byte. Store at most1MiB in each separate original +log while running; exact-cap EOF succeeds, any excess byte fails immediately +and kills the original group. No unbounded memory, spool or later truncation. + +Keep DEVNULL stdin, no shell, explicit argv/cwd/env, start_new_session and the +monotonic maximum30s child deadline. Deadline covers inherited-pipe draining; +selector waits are bounded by a short interval/remaining time. On leader exit, +kill surviving original-group descendants and drain only bounded buffered data +until EOF/deadline. All normal, startup/read/write error, timeout, overflow and +enclosing-interruption paths kill the group, reap the leader within the existing +five-second cleanup bound and close pipe/selector/output descriptors. This is +process-group cleanup, not a general sandbox for deliberately escaping children. + +Preserve existing argv/cwd/error/exit/elapsed/file/count/hash record fields. +Reject every nonempty stderr byte stream for all six Qoder probes before +unchanged version/help/JSON shape checks, including whitespace-only stderr. +Keep failed startup/nonzero/unknown version/malformed/nonempty list refusal. +Do not modify native registry schemas, qualification flags or hosted guards. + +## Meaningful fresh verification and publication + +Use inert Python children only: exact-cap EOF, one-byte overflow on each +stream, simultaneous output beyond pipe capacity, DEVNULL, unrelated2MiB +regular-file write with a much smaller stream cap, actual-startup descendant +cleanup on timeout/leader exit, capture failure, enclosing interruption, +descriptor closure and bounded startup failure. Retain archive/pin/env/strict +version/JSON tests and real unmodified guard refusal with no effects; no forged +hosted flags. Add all-six whitespace/warning stderr refusal assertions. + +Run syntax/workflow/whitespace and focused capture plus preserved Qoder tests. +Freeze reconstructed source for two fresh independent code reviews, fix +findings and publish immediately to this feature branch with exact expected +head/no-force. A genuine warning-free phase-one replay must be downloaded, +verified and independently interpreted before populated phase-two runner or +production project-adapter source. No trust, account, model, MCP, skill, +runtime, project ownership or release claim follows from capture tests. diff --git a/docs/plans/project-install-e2e/qoder-capture-plan-review-r1.md b/docs/plans/project-install-e2e/qoder-capture-plan-review-r1.md new file mode 100644 index 000000000..13530aac3 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-capture-plan-review-r1.md @@ -0,0 +1,20 @@ +# Historical capture plan R1 — restored review record + +This is a recovered historical record, not a fresh approval of recovered code. +Reviewer finish_sources_vscode_cline returned GO on original docs-only +0311921cf4651cae7b22134294b55b213987235a and final clarification +d53a9de82c924f709900368e7308d46de6c5db1e before lost source implementation. + +The reviewer inspected authentic EFBIG evidence, original runner/workflow and +the exact reviewed helper factoring baseline. Parent nonblocking separate +caps, cap-plus-one overflow detection, deadline through draining, core-only +limits and original-group cleanup closed the concrete process-wide file-size +defect without lifting stream bounds. Exact factoring had to preserve pins, +acquisition/env/argv and genuine guard. The final clarification required all-six +nonempty stderr refusal and version/help warning tests. Meaningful inert +exact-cap/overflow/unrelated-large-file and started-descendant checks were +required. No native or current project/runtime success was approved. + +The runtime reset removed that local implementation and its evidence files. +Recovered source now requires fresh reviews/testing; this record alone cannot +satisfy current implementation review or native replay gates. diff --git a/docs/plans/project-install-e2e/qoder-capture-plan-review-r2.md b/docs/plans/project-install-e2e/qoder-capture-plan-review-r2.md new file mode 100644 index 000000000..1aa0060b7 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-capture-plan-review-r2.md @@ -0,0 +1,19 @@ +# Historical capture plan R2 — restored review record + +This restores the actual prior review from retained conversation, not a new +approval. Reviewer finish_grok_integration returned GO at original docs-only +0311921cf4651cae7b22134294b55b213987235a and final stricter-validator +clarification d53a9de82c924f709900368e7308d46de6c5db1e before lost source. + +The reviewer independently verified every genuine phase-one stdout/stderr +count and SHA256. All six zero exits with identical83-byte EFBIG stderr and +overall success false remained failed evidence. Approved separate live caps, +exact-cap EOF/one-byte overflow, nonblocking <=64KiB reads, one draining +deadline, original-group kill/bounded reaping/descriptor cleanup including +interruption, and a direct unrelated-large-file regression. Exact reviewed +baseline factoring and unchanged pins/env/argv/guard were mandatory. The +reviewer's requested all-six warning refusal was explicitly added to the plan. + +No populated adapter, native/runtime/trust result or guard relaxation was +approved. Historical execution/review cannot qualify recovered code after +reset; fresh source review and tests are required before publication/replay. diff --git a/docs/plans/project-install-e2e/qoder-capture-recovery-checkpoint.md b/docs/plans/project-install-e2e/qoder-capture-recovery-checkpoint.md new file mode 100644 index 000000000..4ec6d91fd --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-capture-recovery-checkpoint.md @@ -0,0 +1,66 @@ +# Qoder capture correction: recovery checkpoint + +The workspace runtime reset removed all unpublished worktrees, tooling and +local Git objects. This checkpoint reconstructs the retained decisions and +observations from the completed prior conversation. It does not claim the +lost implementation or its earlier test executions still exist or have been +verified in the recovered workspace. + +Published recovery base: `1dfe89a5eb79386333d50b82e1705c26659e79fc` on +`feat/project-qoder-e2e`. The lost final local checkpoint was +`919d54b9800436e4326aa693a4faa71cf24ea041`; its final source was +`24ace90d572ebb582eeb3a5de85b7c19c99a0234`. + +## Genuine phase-one failure retained + +Run40011845496/job120613942005 at the published recovery base failed overall. +Artifact12532010944, qoder-1.1.78-precode-research-linux-amd64, had actual ZIP +SHA256 `918333b4690c1076d1714a19e036c7e66d62625d58881ae216449590c980ac54`. +The prior author downloaded the ZIP and verified that hash against GitHub's +artifact metadata. Its thirteen original members must be downloaded and +verified again before being restored as local raw evidence. + +All six native commands exited zero. Version stdout was1.1.78, help exposed +project/user/local scopes, local source, JSON and scoped uninstall keep-data; +empty-list stdout was exactly `[]` plus newline. All six stderr streams also +contained the same83-byte warning: + +`[qodercli] Warning: failed to extract native modules: EFBIG: file too large, write` + +Stderr SHA256 was +`eeeb556610ef3a555aa2f6566f36fa8654d8ea6ef8221a60b9d5a40d7c227e78`. +The strict empty-list validator rejected it and success remained false. This +failed run must never be relabeled as a successful native collection. + +## Prior reviewed correction and current requirements + +The inherited child-wide RLIMIT_FSIZE1MiB limits unrelated cache files as well +as output logs. The approved correction replaces it with parent-owned live +nonblocking separate pipe capture, at most1MiB per stream. Retain core-dump +refusal, DEVNULL, fixed argv/environment/cwd, monotonic child deadline including +inherited-pipe draining, original-group cleanup, bounded reaping and descriptor +closure on all paths including enclosing interruption. Exact-cap EOF succeeds; +one excess byte fails and never expands the stored stream beyond its cap. + +The prior final plan at d53a9de8 received independent R1 GO from +finish_sources_vscode_cline and R2 GO from finish_grok_integration, committed +at bbf178ef before source. The final clarification rejects every nonempty +stderr stream on all six probes, including whitespace, before unchanged +version/help/JSON shape checks. No warning whitelist is admitted. + +Both reviewers also approved the lost final implementation24ace90d after +meaningful inert capture and preserved Qoder tests. Those historical results +are not current proof. Reconstructed code requires two fresh independent code +reviews and new meaningful tests before publication/replay. Any material new +implementation decision requires two fresh independent plan reviews first. + +Do not change the actual hosted guard, pins, acquisition, native environment, +six read-only argv, native qualification rules or ownership behavior. No +vendor/native client executes on this task host. Preserve the exact official +archive SHA2565e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea +and binary SHA2560da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953. + +After fresh reviews/tests, publish the recovered correction immediately to its +own feature branch with an exact expected-head lease and no force. A genuine +warning-free phase-one replay must then be downloaded, verified and interpreted +before any populated phase-two runner or production Qoder adapter code. diff --git a/docs/plans/project-install-e2e/qoder-hosted-research-code-review-r1.md b/docs/plans/project-install-e2e/qoder-hosted-research-code-review-r1.md new file mode 100644 index 000000000..956795131 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-hosted-research-code-review-r1.md @@ -0,0 +1,13 @@ +# Qoder hosted research runner — independent implementation R1 + +Reviewed frozen `dfb262a80b37952a7156e37f5e53b787a90a3fd1` in `work/uap-project-qoder` on 2026-10-23, after the separately approved pre-code plan and both committed plan GO reports. **GO for publication of the bounded hosted research runner.** No Qoder process was acquired or executed during this review. + +The runner dynamically imports the adjacent existing native matrix and invokes its real, unchanged `require_hosted('linux-amd64')` before output/temp directories, acquisition or native launch. Guard rejection returns 2 with a bounded diagnostic and no requested output parent. The workflow uses pinned checkout/upload actions, exact PR head source, contents read, disabled checkout credential persistence, Ubuntu 24.04, explicit disposable-host opt-in and a ten-minute job deadline. It does not provide repository/model/account tokens to the native child environment. + +The exact HTTPS URL, archive digest and binary SHA256 are hard-coded. Actual download reads, expanded gzip data, tar member count, member types/names, and binary bytes are bounded before writing a single fixed executable. No extraction API, installer script or vendor install command is invoked. The fresh HOME/XDG/npm/Git environment is an explicit mapping with literal system PATH; ambient credentials, proxies and product settings do not reach the binary. + +The only native invocations are the six approved version/help/empty-list probes. Each uses DEVNULL input, no shell, separate bounded output files, a live inherited file-size limit, a deadline, and process-group cleanup, including after normal parent completion. Output/deadline/launch/nonzero/version/JSON failures remain non-passing evidence. The list validator requires exactly an empty JSON array and clean stderr. Summary metadata explicitly labels pre-code research and unobserved schema/project installation/ownership, skill execution, MCP, model/account/trust and restart. It claims no runtime network isolation. + +Independent verification: `python3 -m unittest discover -s scripts -p 'test_qoder_native_research.py'` passed all 7 tests in 0.934s. These use inert bytes/processes and the actual unmodified guard refusal, covering archive/pin ordering, explicit environment, closed stdin, live output limit, descendant timeout cleanup, exact validation/allowlist and zero-effects guard rejection. Existing native runner guard source is unchanged. The reported prior 54 safety contracts are additional author evidence, not independently rerun here. + +No blocking issue found. Genuine hosted outputs must still be inspected before any Qoder mutation adapter proceeds; an empty list is not evidence of a nonempty native record schema or project E2E qualification. diff --git a/docs/plans/project-install-e2e/qoder-hosted-research-code-review-r2.md b/docs/plans/project-install-e2e/qoder-hosted-research-code-review-r2.md new file mode 100644 index 000000000..fd5c8ce8c --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-hosted-research-code-review-r2.md @@ -0,0 +1,59 @@ +# Qoder hosted pre-code research implementation independent review R2 + +Reviewer: cursor_clients, 2026-10-23. Frozen source: +`dfb262a80b37952a7156e37f5e53b787a90a3fd1`, work/uap-project-qoder. +Reviewed the complete runner, inert tests, dedicated workflow and evidence +description against the pre-code plan and both preceding GO reports. + +## Decision: GO for bounded hosted research + +No implementation blocker found. This approves publishing the research leaf +and interpreting its eventual genuine version/help/empty-list artifacts. No +Qoder mutation adapter, installation or project runtime is qualified. + +- The actual unchanged imported require_hosted guard runs before output + creation, acquisition or launch. Refusal returns a bounded diagnostic and + creates no output parent. Workflow source is exact, checkout credentials + are not persisted, contents permission is read-only, runner is ubuntu-24.04 + and the outer timeout is ten minutes. Existing matrix guard/helper bytes + are unchanged in this leaf's implementation delta. +- Exact versioned HTTPS acquisition has a 90-second request timeout and a + 96MiB cap measured during reads. Vendor archive hash is checked before + decompression. Expanded gzip bytes, tar member count, regular binary size, + normalized names and duplicate entries are bounded; links, special files, + traversal/absolute names, extra regular files and missing/duplicate binaries + refuse. Only the fixed verified binary path is written mode0700, after the + captured binary digest matches. No installer script is executed. +- Fresh profile_environment output plus literal PATH/TMPDIR/LANG is the entire + native environment. Ambient tokens, credentials, proxy/config variables and + workflow metadata do not reach Qoder. Every probe uses the fresh project cwd, + DEVNULL stdin, argv execution without shell, and a new process session. +- The six exact probes match the reviewed allowlist. Each child inherits a + hard RLIMIT_FSIZE output cap and disabled core dumps; live polling checks + both log sizes and the 30-second deadline. Cleanup sends SIGKILL to the + process group even when the direct child has exited. Thus the output cap is + enforced during execution and ordinary surviving descendants are closed; + it is not merely a post-exit truncation. Captures are read only after cleanup + and retain byte counts, hashes, exit/signal, elapsed time and error status. +- Exact version-token and strict empty JSON-array checks fail on wrong version, + nonzero exit, output/deadline error, malformed/unrecognized/nonempty list or + list stderr. Main records every completed allowlisted probe, does not turn + failed probes into empty evidence, writes a post-admission failure summary, + and requires all six accepted results for success. Artifact upload always + runs. Native help text remains evidence to interpret, rather than a claim + that an ancestor's help proves a particular mutation verb. +- Evidence wording leaves populated record schema, project ownership, + discovery, skill execution, MCP initialization, trust/account/model and + restart unobserved. Runtime network isolation is expressly unclaimed. + +## Independent verification + +- Inert research contracts: **7 tests PASS** (0.923s), including actual ordinary + Python child output limits, DEVNULL stdin and descendant timeout cleanup. +- Existing `test_native_client*.py` safety/evidence suite: **54 tests PASS**. +- The direct refusal test invokes the real imported guard in an ordinary empty + environment and proves no requested output directory appears. It neither + patches the guard nor forges hosted variables. + +No native Qoder binary was executed by this review. Genuine hosted outputs are +still pending and must be evaluated before any later registry mutation code. diff --git a/docs/plans/project-install-e2e/qoder-hosted-research-evidence.md b/docs/plans/project-install-e2e/qoder-hosted-research-evidence.md new file mode 100644 index 000000000..cff1957e8 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-hosted-research-evidence.md @@ -0,0 +1,60 @@ +# Qoder hosted pre-code research implementation checkpoint + +The research-only plan at `683eaea8` received independent R1/R2 GO reviews, +committed in `adcfffb2` before runner code. The original approved Qoder project +client plan and its native schema gate remain unchanged. + +The new runner imports the existing matrix's require_hosted helper unchanged +and invokes it before output/probe paths, network acquisition or execution. +The fixed official 1.1.78 vendor archive hash and measured binary hash are +checked before execution. Actual compressed and expanded reads are bounded; +tar member count, names, types and exact single binary admission are checked +without archive-wide extraction. Native runtime receives only the existing +fresh-profile helper's variables plus literal PATH/TMPDIR/LANG values. + +Every process receives DEVNULL stdin, fixed empty-project cwd, separate bounded +stdout/stderr files, a 30s deadline, child-enforced file size limits and process +group cleanup. Only the six plan-approved version/help/empty-list arguments +exist. The final summary records each exit, elapsed time, byte counts, hashes +and accepted/failed result. Any omitted/failed/unrecognized observation fails +the overall research. Guard refusal produces only a bounded diagnostic and +nonzero exit; it creates no requested output directory or summary. Failure +summaries apply only after hosted admission. + +The separate workflow selects an actual GitHub-hosted ubuntu-24.04 runner, +read-only repository permissions, an exact source checkout with persist-credentials:false, no token passed to the native runtime, and artifact upload on +success/failure. Runtime egress isolation is not claimed. No installation +script, plugin install/uninstall, trust, authentication, model, hook or MCP +initialization command is present. + +## Author verification + +- Inert research contracts: **7 tests PASS 0.983s**. These include live child + output limits, DEVNULL stdin, deadline termination of an actual inert child + and descendant group, archive admission, pin-before-parse, fresh explicit + runtime authority and exact version/empty-array rules. +- The direct guard-refusal test invokes the real imported guard without a + hosted opt-in. No forged hosted variables or patched guard is used and no + native client runs. The requested output parent remains absent. +- Existing matrix/native safety suite at this leaf's `c96` base: **54 tests + PASS 0.458s**. Existing matrix runner, guards and tests remain unchanged. +- Python syntax, workflow YAML parse/admission and git diff whitespace: PASS. + +These results qualify only the research plumbing contracts. The genuine hosted +research has not run at this checkpoint. The retained task-host Bun abort134 +logs do not establish successful native help. Even a future successful empty +list will not establish populated project record schema, exact ownership, +project installation, runtime discovery, skill execution, MCP initialization, +native restart or all-client E2E completion. Qoder registry mutation adapter +code remains on hold pending sufficient genuine native schema observations. + +## Independent implementation closure + +Both independent reviewers returned **GO** at the frozen runner checkpoint +`dfb262a80b37952a7156e37f5e53b787a90a3fd1`. Their committed reports record +independent seven-test inert safety passes (R1 0.934s, R2 0.923s); R2 also reran +the unchanged existing 54-test safety selection (0.421s). Neither reviewer +executed Qoder, acquired a native artifact, spoofed the hosted guard or claimed +native project qualification. The early research PR can now obtain genuine +hosted version/help/empty-list outputs for inspection. Project registry mutation +code and its stronger native schema/installation/runtime gates remain pending. diff --git a/docs/plans/project-install-e2e/qoder-hosted-research-plan-review-r1.md b/docs/plans/project-install-e2e/qoder-hosted-research-plan-review-r1.md new file mode 100644 index 000000000..d064a8b28 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-hosted-research-plan-review-r1.md @@ -0,0 +1,14 @@ +# Qoder hosted research plan — independent R1 + +Reviewed frozen plan `683eaea8fb6aa370d0382b528e3e3b423212d050` in `work/uap-project-qoder` on 2026-10-23. Reviewer did not author the plan or runner. **GO for the bounded research implementation.** No runner source or adapter code was present for this review. + +The plan preserves the imported, unchanged `require_hosted('linux-amd64')` as the first admission step before acquisition, output/probe directories, or native execution. Its explicit GitHub-hosted Linux admission is retained; no local hosted-variable spoofing or weakened guard is allowed. The separate read-only workflow grants only contents read and does not pass checkout credentials or workflow/model tokens into the fresh native environment. + +Acquisition is restricted to the exact vendor HTTPS archive and both recorded archive/binary SHA256 values, with bounded compressed reads, tar member/type/path/size checks, no general extraction, and a fixed private binary path. The native command allowlist contains only version, help, and an empty-profile JSON list. There is no plugin install/uninstall, payload execution, trust/auth/model/MCP action, or claim of runtime network isolation. An empty list cannot establish a nonempty schema or qualify project mutation/native installation. + +Two R1 clarifications were requested and are now explicit in the frozen plan: + +- Every child has `stdin=DEVNULL`, bounded output files and deadline/process-group cleanup, so read-only probes cannot hang on interactive input. +- Failure summaries are written only after hosted admission. Guard rejection emits a bounded diagnostic/nonzero exit and creates no output/probe directory, acquires no bytes, and launches no native process. + +The stated tests use inert injected operations and the real guard refusal; they prohibit monkeypatching/weaking the guard or executing Qoder on the task host. The author's additional expanded-gzip bound and live child file-size enforcement strengthen the recorded resource limits without broadening runtime authority. Both independent plan GO reports must be committed before runner code, and implementation review and genuine hosted evidence remain separate gates. This review executed no Qoder process. diff --git a/docs/plans/project-install-e2e/qoder-hosted-research-plan-review-r2.md b/docs/plans/project-install-e2e/qoder-hosted-research-plan-review-r2.md new file mode 100644 index 000000000..62be097c3 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-hosted-research-plan-review-r2.md @@ -0,0 +1,58 @@ +# Qoder hosted pre-code research plan independent review R2 + +Reviewer: cursor_clients, 2026-10-23. Reviewed +`work/uap-project-qoder/docs/plans/project-install-e2e/qoder-hosted-research-plan.md` +before runner or adapter implementation. Inspected the unchanged imported +require_hosted/profile_environment helpers, official manifest/provenance +snapshots and current official plugin/reference documentation. + +Final plan refresh: `683eaea8fb6aa370d0382b528e3e3b423212d050` adds explicit +DEVNULL stdin and clarifies that failure summaries are written only after +hosted admission. Guard rejection creates no output/probe directory, acquires +no artifact and launches no process. Those clarifications preserve this GO. + +## Decision: GO for the bounded research plan + +No plan correctness blocker found. This approves implementing the separately +guarded version/help/empty-list research workflow only. It does not approve +Qoder registry mutations or qualify a project installation/runtime. + +- Exact versioned HTTPS acquisition is bound to the vendor manifest digest, + compressed read limit, tar member/type/path limits, declared binary size and + the already measured binary digest before execution. It materializes one + fixed binary path without vendor installation scripts or archive-wide + extraction. The supplied manifest/provenance agree on version 1.1.78 and + the pinned Linux x64 archive/binary hashes. +- The script imports and calls the actual unchanged hosted guard before + directories, acquisition or execution. The workflow is disposable + GitHub-hosted Linux amd64 only, with fixed source, read-only permissions and + no persisted checkout credentials. Tests do not spoof hosted variables or + patch/weaken the guard to run native code on the task host. +- A literal fresh profile/environment and fixed empty project cwd isolate + read-only probes from user/native configuration. No workflow token, + credential, proxy, model key or permission/trust flag reaches Qoder. Runtime + egress isolation is explicitly unclaimed. Existing task-host private-proc + launcher failures and empty-proc Bun aborts remain failure evidence. +- The six allowlisted commands are only version, plugin/help forms and an + empty registry list. Probe deadlines, separate bounded stdout/stderr files, + whole-process-group termination, total workflow timeout and per-probe logs, + hashes and failure summaries make the result reviewable. Implementation + review must verify those output bounds remain enforced while a process is + running, including descendant termination on timeout/limit breach; successful + post-exit truncation alone would not satisfy this plan. +- Version validation requires the exact token, and empty-list validation + accepts exactly a JSON empty array. Nonzero exits, signals, malformed JSON, + unexpected/nonempty shapes, timeout, output-limit failure and skipped probes + cannot be relabeled as successful empty evidence. Artifact upload runs on + failure as well as success. +- Current official reference independently confirms that list JSON reports + recorded scopes, project/local rows carry projectPath, and effective enabled + status belongs to command cwd. The plan correctly does not infer a populated + schema or project ownership from an empty list. Genuine sufficient schema + evidence remains a later precondition for adapter mutation code. + +Meaningful inert implementation tests and a direct real-guard refusal check +remain mandatory before publication. Commit both independent plan/fix GO +records before runner code. The eventual hosted evidence must be interpreted +on its actual measured outputs, and it cannot replace the separate native +project E2E qualification. diff --git a/docs/plans/project-install-e2e/qoder-hosted-research-plan.md b/docs/plans/project-install-e2e/qoder-hosted-research-plan.md new file mode 100644 index 000000000..75a9fc595 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-hosted-research-plan.md @@ -0,0 +1,109 @@ +# Qoder pinned hosted research before project adapter code + +Owner request, 2026-10-23: obtain genuine pinned version/help/list evidence on +an approved disposable GitHub-hosted runner before implementing the Qoder +project registry mutation adapter. The original two independently approved +client plans remain in force. This narrow plan describes research plumbing, +not a replacement adapter design or a native project qualification. + +## Reason and evidence boundary + +The official release manifest pins Qoder CLI **1.1.78**, Linux x64 archive: +`https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/1.1.78/qodercli-linux-x64.tar.gz` +with vendor SHA256 +`5e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea`. +Manual bounded acquisition yielded one regular `./qodercli` binary, 182,982,112 +bytes, SHA256 +`0da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953`. +The task-host read-only probes encountered private-proc mount refusal, then +Bun1.4.2 abort134 in the more restrictive networkless namespace with empty +/proc. No successful task-host help is claimed and host /proc was never used. +Captured provenance/crash logs remain separate evidence. + +Official references: +- https://docs.qoder.com/cli/plugins +- https://docs.qoder.com/cli/plugins-reference + +The reference states `plugins list --json` includes all recorded scopes; +project/local rows have projectPath, while enabled reflects the command cwd. +The empty hosted list can confirm only the top-level empty-list shape. It +cannot establish a nonempty record schema, native project discovery, +installation, ownership, MCP initialization, skill execution or restart. +Project mutation implementation remains gated on sufficient genuine schema +evidence; neither static binary strings nor an empty list will be relabeled. + +## Planned files and approved actions + +Add `scripts/research-qoder-native.py` and a separate research-only workflow +`.github/workflows/qoder-native-research.yml` after this plan receives two +independent GO reviews. Keep the existing native matrix runner/guards unchanged. +The script dynamically imports the adjacent existing +`run-native-client-matrix.py` and calls its unchanged +`require_hosted('linux-amd64')` **before** network access, artifact/probe directory +creation or native execution. The workflow uses only GitHub-hosted ubuntu24.04, +a fixed source checkout, contents:read, persist-credentials:false and explicit +AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED=1. Trigger it for this leaf's pull-request +changes or explicit workflow_dispatch. It has a short timeout and uploads +research outputs with always(), even when a probe fails. No repository secrets +or workflow tokens are passed to the native process. + +Acquire only the exact HTTPS archive above with a 90s request timeout, a +96MiB compressed byte cap checked against actual reads, and the exact vendor +hash before parsing. Inspect the tar without extract/extractall: at most16 +members, only directories plus exactly one regular basename qodercli, no links, +special files, traversal/absolute member names or another regular file. Bound +the binary to256MiB before reading, read exactly its declared bytes, require +its captured exact binary SHA256, then write one fixed binary path mode0700. +Never invoke either vendor installation script or `qoder install`. + +Create a fresh temporary parent with separate binary, home, project, tmp and +output directories. Use the existing profile_environment helper for fresh +HOME/XDG/npm/Git config files, plus a literal allowlist for PATH (/usr/bin:/bin), +TMPDIR and LANG. No ambient environment, user settings, credentials, proxies, +GH_TOKEN, GITHUB_TOKEN, model keys or permission flags reach the binary. +Set cwd to the fresh project for every command. Run exactly: + +1. `qoder --version` +2. `qoder plugins --help` +3. `qoder plugins list --help` +4. `qoder plugins install --help` +5. `qoder plugins uninstall --help` +6. `qoder plugins list --json` + +Each process has a30s deadline, bounded stdout/stderr capture (1MiB each) using +files rather than unbounded PIPE accumulation, stdin explicitly DEVNULL, +no shell, and process-group +termination on timeout/output-limit breach. Keep an independent total workflow +timeout. Record arguments, elapsed time, exit/signal, stdout/stderr byte counts, +SHA256 and separate log files. Run the allowlisted probes even if one fails; +the final script exit is nonzero on any failure, unexpected version or +nonempty/unrecognized empty-list output. The version must contain exact1.1.78 +with version-token boundaries. Parse the final list as JSON; require exactly +an empty array. Do not treat error, truncation, unexpected shape or skipped +probe as an empty result. After hosted admission, preserve a machine-readable summary even on failure. +A hosted-guard rejection instead prints only a bounded diagnostic and exits +nonzero without creating any output/probe directory, acquiring bytes or +launching a native process; summary creation must never precede admission. + +This research does not install/uninstall plugins, create hooks, edit settings, +run trust/auth/login commands, contact a model, initialize MCP or execute a +plugin payload. The read-only list may create native scratch state only inside +the disposable empty profile/project. Runtime network isolation is not claimed; +the native environment is credential-free and no connected account is used. + +## Verification before publication + +Use Python syntax/static checks and meaningful unit tests for bounded archive +admission, pins, version/JSON validation, subprocess output/deadline handling, +explicit environment and guard ordering. Tests must use injected inert +functions/processes and must not spoof hosted environment variables, monkeypatch +or weaken require_hosted, or execute Qoder on the task host. A direct invocation +without hosted opt-in must fail through the real imported guard before outputs +or acquisition. Existing matrix safety tests remain unchanged and pass. + +Commit plan plus both independent review/fix GO reports before adding runner +code; then complete bounded implementation checks, commit source and send SHA +to root. Root publishes the early Qoder leaf/PR to run hosted evidence. Review +and interpret genuine uploaded evidence before implementing registry mutation. +The research artifact is explicitly named as pre-code research and cannot be +substituted for the separate project E2E native proof gate. diff --git a/docs/plans/project-install-e2e/qoder-log-alias-amendment-plan.md b/docs/plans/project-install-e2e/qoder-log-alias-amendment-plan.md new file mode 100644 index 000000000..dd03a3434 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-log-alias-amendment-plan.md @@ -0,0 +1,101 @@ +# Qoder collector amendment: exact log alias metadata + +Plan only, 2026-10-24. The genuine failed run40130660701 and artifact12575528479 +are preserved with verified raw members and original false qualification fields +in `docs/evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701`. +Both independent artifact interpretation and two fresh plan review rounds are +required before collector source changes; two code GOs precede publication. + +## Observed blocker and narrow change + +The four pinned Qoder1.1.78 help/version calls succeeded in the actually verified +networkless/nonroot namespace. Its logger created `home/.qoder/logs/latest` as a +symlink. The first bounded inventory correctly refused it before any list or +install. Only its lstat mode/type and 42-byte target length were observed; the +artifact supplies no target contents. No populated installation schema exists. + +Amend the collector inventory for this one exact label/path only: +`home/.qoder/logs/latest`. Treat it as **link metadata**, never as a regular file +or directory and never as a package/discovery path. Native stage inventories may +opt into this exception through an explicit argument that defaults false. The +complete source-baseline inventory and every before/after source-pin check keep +the strict default. Only the two native before/after-stage inventory call sites +may opt in. Source labels remain source-user/source-A/source-B, never home. An input path or native-emitted string cannot add exceptions. + +The current anchored held directory supplies `os.stat(...,follow_symlinks=False)` +and `os.readlink(...,dir_fd=fd)` for the exact entry. Before readlink, require +`st_nlink == 1` and `0 <= st_size <= 4096`, with sufficient remaining total +inventory byte budget. Pass a bytes name and preserve the returned literal +target bytes (or exact `os.fsencode` bytes), without Unicode replacement or +operational path decoding. After readlink, check the budget again, require the +actual byte count to equal the pre-read st_size and stay within4096/aggregate +bounds, and require matching full before/after lstat identity. Include those +bytes in the existing total inventory byte budget. Record kind=`symlink_metadata_only`, target byte count, +SHA256 and an exact hex representation; explicitly record `target_followed:false`. +Do not decode it into an operational path, resolve it, open it, stat the target, +scan it, execute it or use it to choose any later native command. + +The literal may be relative, absolute, dangling, external or point at a special +file: only the bounded bytes are data. Neither target validity nor its contents +are asserted. A changed/oversized/multiply-linked entry refuses the stage with +partial metadata. Every other symlink, hardlink/special-file case and every +fixture symlink still refuses. All ordinary file/directory identity and byte/ +entry/depth/path bounds remain unchanged. If the known alias is an ordinary +file/directory instead, its existing typed bounded inventory rules still apply; +it does not create a broad exemption from checking that path. + +## Unchanged execution and qualification + +Keep official acquisition pins, real hosted admission, actual gitHEAD check, +original/effective UID/GID/capability/network proof, finite19native-command table, +source full-tree equality before and after every probe, strict JSON/error +fail-stop, original HOME/profile authority, independent8minute collection/ +5second cleanup+staging deadlines and separately admitted64MiB upload directory. +No new native operation or target-path read is added. Never skip the alias silently: +metadata must appear at each successful stage observation. + +The failed artifact remains failed. A new exact-source hosted run is required. +Even a completed amended collection retains all eight qualification fields false; +actual populated rows need independent interpretation before a concrete adapter +plan, two plan rounds and product implementation. No production Qoder adapter is +included in this amendment. + +## Inert contracts and review gates + +Add focused no-vendor tests proving: + +1. Exact allowed native home alias records literal metadata and hashes without + opening its target, including a dangling target, foreign readable file and + FIFO/directory target. Foreign bytes are never read and no call can block on + a target. Include exact non-UTF8 bytes roundtrip and keep no-follow directory + admission checks, including an aliased intermediate .qoder/logs directory. +2. The exception defaults off; identical aliases under each source fixture, + differently named home entries and aliases in intermediate directories still + refuse, with no target dereference. +3. Oversized readlink bytes, multiple links and lstat/readlink replacement races + refuse while preserving partial metadata and existing deadline behavior. +4. Complete source snapshots still detect additions/removals/mode/hash changes + before every native probe and after it. Existing inventory, capture, bounded + cleanup/upload and real hosted-guard refusal suites continue to pass. + +No Qoder binary is executed locally. Freeze the small collector/test delta, +obtain two independent code GOs, publish only those reviewed bytes, and verify/ +retain the next actual artifact regardless of outcome. Unexpected further +native aliases require another concrete review; they do not widen this rule. + + +## Independent R1 artifact interpretation and plan decisions + +Two independent reviewers, `skillshare_scope` and `claude_codex_scope`, verified +all15 preserved raw member lengths/SHA256 hashes and all7 stdout/stderr pairs. +The former also independently verified ZIP size/hash/CRC. Both confirmed the +same exact failure boundary, four successful help probes, absent populated +list/install evidence, unchanged false qualification fields and completed +cleanup. No client or acquisition ran during either interpretation. + +Both gave **R1 GO** at `a665f6b81b4e5e072b6de6de245519c5de0c0370` for this +metadata-only amendment. Their concrete R2 precision is incorporated above: +pre-read nlink/size/budget admission, exact raw bytes and size equality, post-read +identity/budget checks, two opt-in native call sites only, intermediate alias +refusal and non-UTF8/race tests. No target interpretation or broader alias policy +was added. Fresh R2 review remains required before collector source changes. diff --git a/docs/plans/project-install-e2e/qoder-phase1-replay-evidence.md b/docs/plans/project-install-e2e/qoder-phase1-replay-evidence.md new file mode 100644 index 000000000..60e9bb722 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-phase1-replay-evidence.md @@ -0,0 +1,32 @@ +# Genuine Qoder phase-one replay interpretation + +Run40079977099/job120831313308 executed source62cdc6124804ec8c4030828fb976dc65b74bad47 +on the real hosted ubuntu24.04 research lane. GitHub reports job and capture +success. Artifact12555655148 has actual downloaded ZIP SHA256 +1c5e0f3400451ebc1d8841f4df4a09f2049f2b8276c7526cf50eaf2161e85306. All13 +original flat members are retained under docs/evidence/project-install/qoder-40079977099. +Every original stream's byte count and SHA256 was independently rechecked +against the genuine summary. Acquisition remains vendor archive SHA256 +5e3213d2d4256c86c5ad2c8bf73fd392f92ba80d0d38f822806542805a48a8ea and +182982112-byte binary SHA256 +0da0976e13fe00bc28f725d6fe7c2f0fe9fa9a8c37eeb2ee4e80ad4c0fc58953. + +All six fixed commands exited0, errornull, acceptedtrue and literal empty stderr +(0bytes). Version is1.1.78. Each help response is nonempty; actual install help +accepts local paths and explicit user/project/local scopes; list exposes JSON; +uninstall exposes scope and keep-data. Final actual list is exactly [] plus +newline, SHA25637517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570. +The EFBIG warnings from failed40011845496 are absent here; that failed run and +its bytes remain failed evidence, not replaced or relabeled. + +This establishes genuine warning-free pinned phase-one admission only. Empty +array does not reveal populated schema, project ownership, enabled precedence, +MCP initialization, skill execution, trust/auth/model or session restart. +Runtime network isolation was not claimed by this phase-one collector. No native +client executed locally during evidence download/verification. Independent +interpretation and a fresh phase-two plan with two GOs are required before +populated research runner code. Current official primary references, re-read +2026-10-24, are https://docs.qoder.com/cli/plugins and +https://docs.qoder.com/cli/plugins-reference; their documented local skill-only +manifest layout and per-cwd enabled caveat motivate observation, not guessed +production schema. diff --git a/docs/plans/project-install-e2e/qoder-populated-implementation-evidence.md b/docs/plans/project-install-e2e/qoder-populated-implementation-evidence.md new file mode 100644 index 000000000..f01f0d5ea --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-populated-implementation-evidence.md @@ -0,0 +1,81 @@ +# Qoder populated collector: current implementation and review receipts + +This is research infrastructure only. It implements the previously twice-reviewed +[qoder populated recovery plan](qoder-populated-research-recovery-plan.md). +No production Qoder adapter, native ownership parser, native activation, loaded +skill, trust, model, authentication, session or MCP qualification follows from +collector implementation or inert tests. + +## Frozen code and publication + +Final independently reviewed local code: +`c3c4b8e680c2d56f849356d32c04a28d0e22a0c0`. +Published equivalent: +`e29237fe546d8aebde98adaae59d346142143d2d`, tree +`15dcc674b81ab5bbd73e7d46de36f99c8e605fe3`. +The publisher compared exact trees before synchronizing the isolated worktree. +The final branch update published the reviewed chain atomically. + +Initial code review refused publication for three concrete issues. `/sys/class/net` +was not evidence of the child's actual network namespace; it was replaced by +`socket.if_nameindex()`. Six file pins did not detect new source hooks/files; +complete bounded descriptor-relative source snapshots now freeze exact names, +types, modes, sizes and hashes before the first native call and are compared +before and after every probe. `TemporaryDirectory` could start recursive cleanup +after its one-shot deadline had expired; it was replaced by an explicit inert +cleanup child with a separate five-second timeout and held-root identity. + +The upload boundary was also strengthened before publication. The workflow never +uploads raw output. A separate private pending tree receives only complete, +stable, ordinary unaliased files: at most 64 files and 64 MiB in aggregate, +with a five-second copy deadline. Only a complete admitted tree is renamed to +the actual upload directory. Rejected, oversized, aliased or nonregular raw +output produces a small failure summary in that directory instead. Incomplete +experiment cleanup forces failure without replacing the original probe error. +There is no recursive temporary-directory finalizer. + +Actual `/usr/bin/git rev-parse HEAD` is bounded to 128 stream bytes and five +seconds and compared with the exact expected 40-character commit before +acquisition. The parent records this observation, preserving expected and +measured source identities. The child checks actual namespace, interfaces, +original nonroot real/effective UID/GID, empty supplementary groups, zero +capabilities and no-new-privileges before any Qoder command. Acquisition is the +only networked phase, and no hosted flags, tokens or proxy authority reach +Qoder's explicit fresh profile environment. + +## Independent implementation decisions + +Both independent reviewers read the final frozen code and approved only this +finite research collector before publication: + +- **R1, root: GO.** Complete fixture-tree pins; actual interface enumeration; + strict JSON/error fail-stop across the finite 19 native commands; measured + exact checkout; original/effective identities; independent five-second + held-root cleanup; bounded stable staging into a separate upload directory. + Independently ran all 12 inert collector contracts, PASS (0.273 s). +- **R2, skillshare_scope: GO.** Independently rechecked the same corrected + boundaries and original-failure retention. Independently ran all 12 inert + collector contracts, PASS (0.280 s). + +Author verification: 12 populated collector contracts PASS (0.328 s), six shared +live-capture contracts PASS (2.235 s), seven existing phase1 Qoder research +contracts PASS (3.714 s), Python compilation and `git diff --check` clean. +The genuine unchanged hosted guard was exercised on the task host in a clean +subprocess environment: refusal before output creation. No vendor acquisition +or Qoder process ran locally, and hosted-admission variables were not forged. + +## Genuine observation gate + +The first genuine populated run is +[40130660701](https://github.com/777genius/universal-agent-plugins/actions/runs/40130660701), +at published source `e29237fe546d8aebde98adaae59d346142143d2d`. +The first run later completed with an honest inventory failure. Its verified raw +[artifact and provenance](../../evidence/project-scope-research/qoder-1.1.78-linux-amd64-40130660701/README.md) +are preserved separately. Four actual help probes passed, but the native log +symlink stopped inventory before any list/install. This is not a successful +populated observation. No hosted guard, platform or approval was weakened. + +Raw actual observations must be downloaded, verified and independently +interpreted before proposing a production project adapter. Unknown output or +inventory refusal remains a failed observation. Any concrete source-contract +proposal then needs two independent plan review rounds before production code. diff --git a/docs/plans/project-install-e2e/qoder-populated-recovery-plan-review-r1.md b/docs/plans/project-install-e2e/qoder-populated-recovery-plan-review-r1.md new file mode 100644 index 000000000..a330af295 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-populated-recovery-plan-review-r1.md @@ -0,0 +1,18 @@ +# Qoder populated research fresh independent pre-code R1 + +Reviewer: finish_sources_vscode_cline. GO at docs-only +5f64936496ce3d6db008e6e6fce6f603ea0e17ba before any populated source. +Independently rechecked all twelve actual phase1 stream lengths/SHA256 and +all six exits/errornull/accepted/stderr0. Final list is exactly[]newline. +Observed pinned version/help/local-path/scope syntax only; no populated schema +or project/native/runtime/ownership claim. + +Full phase2 plan's finite three text-only same-ID installs and twelve scoped +lists, <=19 processes, no uninstall/guessing, stop-on-failure, genuine guard +before effects, whole-collector fresh netns/nonroot/groups/caps/no-new-privileges +verification, fresh allowlisted native environment and retained pins/capture +are coherent. Anchored no-follow bounded inventories fail on overflow, never +skip caches or lift limits. Total deadline/cleanup and explicit false +qualification preserve boundaries. Two fresh code GOs and genuine hosted +evidence interpretation remain required before adapter source. No native +client, compiler or model executed by review; no blocking finding. diff --git a/docs/plans/project-install-e2e/qoder-populated-recovery-plan-review-r2.md b/docs/plans/project-install-e2e/qoder-populated-recovery-plan-review-r2.md new file mode 100644 index 000000000..eca5bda38 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-populated-recovery-plan-review-r2.md @@ -0,0 +1,18 @@ +# Qoder populated research fresh independent pre-code R2 + +Reviewer: finish_grok_integration. GO at docs-only +5f64936496ce3d6db008e6e6fce6f603ea0e17ba before any populated source. +Independently rechecked all twelve actual phase1 stream lengths/SHA256 and +all six exits/errornull/accepted/stderr0. Final list is exactly[]newline. +Observed pinned version/help/local-path/scope syntax only; no populated schema +or project/native/runtime/ownership claim. + +Full phase2 plan's finite three text-only same-ID installs and twelve scoped +lists, <=19 processes, no uninstall/guessing, stop-on-failure, genuine guard +before effects, whole-collector fresh netns/nonroot/groups/caps/no-new-privileges +verification, fresh allowlisted native environment and retained pins/capture +are coherent. Anchored no-follow bounded inventories fail on overflow, never +skip caches or lift limits. Total deadline/cleanup and explicit false +qualification preserve boundaries. Two fresh code GOs and genuine hosted +evidence interpretation remain required before adapter source. No native +client, compiler or model executed by review; no blocking finding. diff --git a/docs/plans/project-install-e2e/qoder-populated-research-recovery-plan.md b/docs/plans/project-install-e2e/qoder-populated-research-recovery-plan.md new file mode 100644 index 000000000..44d42370d --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-populated-research-recovery-plan.md @@ -0,0 +1,101 @@ +# Qoder populated scope research — fresh recovery plan + +Genuine warning-free phase1 is now retained atbcce0fe8: run40079977099, source +62cdc612, artifact12555655148, verified ZIP1c5e0f3400451ebc1d8841f4df4a09f2049f2b8276c7526cf50eaf2161e85306. +All six actual commands exit0/stderr0 with verified stream hashes; empty list +reveals no populated schema. Require independent phase1 interpretation and two +fresh plan GOs before populated runner source. Lost historical phase2 GOs are +context only. This research implements no production adapter/identity parser. + +## Finite inert observations + +Add a separate script and separate hostedubuntu24.04 workflow. Keep phase1, +shared capture, acquisition pins and real matrix require_hosted unchanged. +Reuse phase1 exact acquisition/archive checks (official1.1.78 archive and +182982112-byte binary pins). Guard admission precedes effects. A fresh nonce +parent contains binary, fresh original HOME, user-cwd, distinct projectA/B, +tmp, three source directories and output. Fixtures contain only two text files: +.qoder-plugin/plugin.json with stable identical name uap-scope-research, versions +0.9.0 user/1.0.0 A/1.1.0 B, and skills/marker/SKILL.md with distinct inert +USER/A/B markers. No dependencies, hooks, MCP, executables, settings, model or +plugin code; fixed input digests recorded before native calls and checked after. + +Exactly four stages: empty, user-installed, A-installed, B-installed. At every +stage run plugins list --json from each exact user-cwd/A/B (12 read-only lists). +Between stages run exactly three installs from exact user-cwd/A/B: +plugins install --scope user|project --json. Add one +version command and at most three fixed read-only help commands, total<=19 +native processes. No uninstall/enable/disable/reload/details/update/task/model/ +trust/auth command, no guessed installed ID, no reading an emitted external +path. Stop later mutations after any failed/dirty/unknown command; retain its +raw streams and stage. JSON is decoded with duplicate-key refusal, but preserve +unknown successful native JSON as observation, never guess production fields. +Lists must be JSON arrays, empty stage exactly[], populated lists nonempty only +after a successful install. Schema/version/identity/projectPath/enablement +interpretation remains a later independent evidence review. Native installed +rows alone do not establish skill execution/session/native active ownership. + +## Genuine hosted admission and networkless native execution + +Acquisition is the only networked phase. The collector first calls the real +require_hosted; validates original nonroot uid/gid and actual host namespace; +acquires pinned bytes, writes one executable and text fixtures, then starts one +whole collection subprocess through sudo -n unshare --net followed by setpriv +to the exact original nonroot uid/gid, clear supplementary groups, drop caps +and set no-new-privileges. The wrapper uses env -i with only explicitly carried +actual hosted-admission values/source commit and Python/fixture arguments. +Do not fabricate hosted variables or invoke this runner on the task host. + +The isolated collector rechecks real hosted admission and binary+fixture +digests before any native execution, then verifies its own network namespace +differs from parent, uid/gid equal recorded original nonroot, groups empty, +capability sets zero/no-new-privileges, and interfaces contain loopback only. +Fail closed if namespace/setup verification is unavailable; no host-network +fallback. All native commands inherit this same verified fresh network +namespace and original uid, with no passed namespace handles. Native env is +phase1's explicit fresh profile plus PATH /usr/bin:/bin, TMPDIR and LANG; no +ambient credentials/proxies/workflow tokens or hosted flags reach Qoder itself. +Hosted /proc is used only to verify this disposable runner isolation, not a +task-host native-execution exception. No native process executes as root. + +Use unchanged shared run_probe per native child: DEVNULL, shellfalse, own group, +separate live1MiB stdout/stderr caps+1 overflow, <=30s per command, group kill/ +bounded reap on error/overflow/timeout/interrupt. An independent8minute total +collector deadline covers acquisition, writes, inventory, commands and drain; +remaining budget bounds requests and children. Acquisition+wrapper have bounded +cleanup; real workflow timeout10minutes. Preserve failure summary after +admission even on isolation/setup/command failure. Upload always, exact source +checkout, contentsread and persistcredentialsfalse remain unchanged. + +## Bounded no-follow inventory and evidence + +Capture inventories of all exact fresh HOME, user-cwd/A/B and three source +roots before and after each stage. Exclude the separately verified binary and +output directories, never native-emitted paths. Use anchored descriptor-relative +NOFOLLOW/NONBLOCK directory/file opens, fd regular/sameidentity checks before +and after read plus path recheck, reject links/specials/races. Bounds:4096 +entries/depth32/1MiB per regular file/64MiB total and bounded path/metadata. +Inventory overflow, including a native scratch/cache file, is an explicit +research failure: retain known bytes/metadata and do not silently skip files or +lift the bound; a concrete later amendment is needed if observed. Record modes, +length and SHA256; no file content except fixed fixture manifests/skills and +raw bounded command streams. Fixed source digests must remain unchanged. + +Summary records exact stage/argv/cwd/environment, actual isolation facts, +acquisition/source digests, separate output hashes/lengths, exits/errors/deadlines, +inventories and explicit false projectownership/activation/runtime/MCP/trust/ +model/auth/session qualification. Initial empty and populated observations have +distinct provenance. No failed command or inventory is normalized to empty or +success. No installer/production schema behavior changes in this slice. + +## Fresh proof before publication + +Inert tests cover exact finite argv/cwds/stages/mutation stop, text-only fixture +shape/digests, actual isolation verifier refusal paths without forging admission, +separate acquisition/collection ordering, source/binary pin tamper, duplicate/ +trailing/unknown JSON, warningstderr, no-follow/special/oversize/race inventory, +total deadline+cleanup and unchanged real hosted guard rejection with noeffects. +Reuse shared capture tests; never execute vendor code locally. Syntax/workflow/ +diff checks and two fresh code GOs precede root publication/actual hosted run. +Interpret real artifact independently before project adapter implementation; +source contract, scoped ownership and safe removal require their own plan/GOs. diff --git a/docs/plans/project-install-e2e/qoder-post-observation-plan.md b/docs/plans/project-install-e2e/qoder-post-observation-plan.md new file mode 100644 index 000000000..e1db88d48 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-post-observation-plan.md @@ -0,0 +1,204 @@ +# Qoder: project-owned Prepared skill bundle after genuine observation + +2026-10-24. R2 plan only. Two independent plan rounds precede adapter changes, +then two frozen code GOs precede publication. This plan follows the +[owner scope contract](../../PROJECT_INSTALL_OWNER_SCOPE.md): all supported agents +receive an explicit project path, while preparation, native discovery and runtime +qualification remain separate. It adds no native registry or cache ownership. + +## Evidence and architectural decision + +The [genuine populated artifact](../../evidence/project-scope-research/qoder-1.1.78-linux-amd64-40135008690/README.md) +at source2b0ef181 is verified independently by the author and Skillshare critic. +Nineteen native commands succeed. Native list is global: user/A/B CWDs return +identical rows at each stage. Rows retain exact projectPath attribution, but all +three payloads occupy the original user's shared plugin cache and the user's +registry changes. A/B remain empty in these **non-Git** fixtures. Different +versions were used; actual selected skills, same-version cache behavior, native +update/uninstall, Git-root behavior and runtime isolation are not established. +All eight original qualification fields remainfalse. + +Do not turn the native `scope:project` label into project-owned files or treat +list visibility as loaded selection. Do not rename the portable plugin identity, +redirect HOME, delete a native cache directory or rewrite native registry JSON. +The existing user adapter is preserved, including its conservative refusal of +unrecognized/duplicate global rows. This slice does not qualify same-name native +user/project coexistence for that existing user lifecycle. + +Implement a narrow **Prepared project skill bundle** using the already supported +Qoder plugin format. Its files are owned by the selected project's UAP namespace; +UAP does not register it in the native user registry. The bundle contains standard +skills and eligible portable commands rendered as skills. Project MCP, hooks, +agents and other unobserved native resources remain unsupported in this slice. +This is useful project installation with an explicit native import boundary, +not automatic native project activation. + +## Exact scope and immutable authority + +Reuse the resolver, source/manifest/lock formats, immutable package cache, existing +project environment lock, state receipts, digest verifier and directory transaction. +No second installer, client-ID switch in generic core, global cwd mutation or new +schema is needed. Project behavior is isolated in the Qoder adapter. + +- Default user scope preserves current behavior. Dispatch the project branch before any existing Qoder PromoteNativeReady, list/install/uninstall, runner or executable availability helper if + either plan/target scope is project or ProjectRoot is nonempty; reject mixed + labels rather than falling through to user lifecycle. +- Require the exact client, both project scope fields, canonical existing project + directory and original user ConfigRoot equality with TargetContext. Refuse + profile/project overlap, aliases, replaced roots, malformed physical IDs and + copied receipt namespaces through existing authority checks. +- Use existing managed layout: anchor `/.uap/managed`, owned base + `clients/qoder`, active ``. NativeProfileRoot is the + canonical selected project namespace; NativeRegistryExecutable is empty for + this Prepared path. The original detected user profile remains unchanged. +- Mark only Qoder project scope supported with LifecyclePrepared. Qualify its + selected components as prepared skills/portable command skills. A project MCP + requirement must refuse honestly; unselected unsupported resources must not + acquire runtime/data effects. Keep user capabilities unchanged. + +## Projection and local ownership + +The actual importable native view is a closed subdirectory +`/.agentplugins-qoder-native/`, containing only the generated +`.qoder-plugin/plugin.json` and selected `skills//` trees (including +rendered eligible commands and their selected skill assets). Reuse the existing +Qoder manifest/skill encoding, not a second loader. Reject a source collision +with the reserved projection subdirectory before publishing. Keep original +standard package/source payload outside that native view and preserve immutable +source bytes. Manual import must point at this exact view, never the whole +managed package. Unknown root auxiliary native files therefore cannot cross the +import boundary: no raw `.mcp.json`, hooks, agents or other native discovery +surface is copied into this closed view. Verify its complete allowed path set, +manifest skill declaration and absent unselected native resources before final +file verification. Do not solve this by merely omitting mcpServers while copying +an authored raw native config/hook into the same importable root. + +Validate staging as a real contained direct child of the exact managed base and +active path as the exact selected physical artifact. Reuse the prepared namespace +scanner, which already recognizes `.qoder-plugin/plugin.json`, for duplicate, +foreign and malformed manifest claims. Existing full managed-directory digest and +receipt checks remain required; RegistryClear is only namespace evidence. + +The project PreparedRegistryInspector carries local package/namespace findings. +The project branch of InspectNativeRegistry validates exact authority and then +returns neutral RegistryClear; it must never return a positive RegistryExpected +merely because a prepared package exists. Errors/collisions may still refuse. +That neutral native finding cannot set NativeDiscoveryReconciled. A project +NativeRegistryExecutable stays empty, so NativeDiscoveryAttempted also stays +false even when the SDK enables its observer with a supplied forbidden runner. +Neither branch calls plugins list/install/uninstall. A native +same-name user row is not a collision for materializing an independently owned +bundle; it does not make that bundle active. Before first add, a clear absent +managed target is admissible; an unowned same-name/physical claim refuses. With +receipts, exact digest identity is required; tampered bytes refuse or follow the +existing reviewed known-drift repair contract without adoption. + +NativeObjectOwnership contains only the exact managed_package_directory receipt; +no native cache/registry/profile/data ownership is manufactured. Bindings validate +scope/client/physical ID/selected profile/locator and reject foreign object kinds. + +## Lifecycle and truthful verification + +AutomaticallyActivates is false for project scope. Preflight validates selected +identity and rejects ActivationComplete before state effects, including first +add and replay/update/repair after reopening. Early unstaged preflight permits an +empty delivery but checks every supplied identity. Final activation requires the +exact populated delivery and declared name before returning a result. + +Return Prepared with file installation_verified only after the real managed +package/projection verifier proves the exact installed skill bundle. This field +means installed bundle files; no native discovery or runtime claim follows. A +runner-free read-only project verifier uses the same package/receipt evidence; +absence, changed bytes, unknown projection or foreign claims cannot pass it. +Its outcome has NativeEffectUnchanged, no attestation and no newly invented +native objects. Existing generic proven Prepared no-change logic may then keep +actual repeat add/unchanged update/intact repair Mutated=false. If this composed +route exposes a generic convergence gap, freeze a separate concrete correction +plan; never weaken the proof or silently report a write as unchanged. + +All project add/update/repair/inspect/remove/recover paths preserve the original +user authority and other project. Removal validates the recorded bundle identity +and removes only UAP's exact directory through the existing transaction; it never +calls native uninstall or removes a native/user cache. Persistent caller-owned +project data remains untouched. Ordinary status remains host-free. + +## Native use and explicit remaining limitation + +User actions identify the exact compatible native view directory +`/.agentplugins-qoder-native/` and project working directory. The observed native importer accepts local `.qoder-plugin/plugin.json` +and skill files. If an operator chooses native registration, the explicit argv +is `plugins install --scope project --json` from the selected +project directory; UAP does not execute it. Explain that this operation writes +the user's native registry/cache, and same-name/same-version replacement remains +unqualified. Require operator review of existing native registrations/cache before +that separate action. No `/plugins reload`, trust approval or model task is run. + +A manually imported cached skill copy is native state independent of UAP's bundle +receipt. UAP remove does not unregister or delete that copy; native cleanup is a +separate native project-scope operation and must be reviewed independently. No +stdio/MCP path into the bundle is projected in this slice, so this guidance does +not create an owned executable registration pointing at a later deleted package. +Do not suggest that bundle removal undoes manual native/global changes. + +A later automatic native project-registry route would need additional actual +Git/non-Git and same-name/same-version observations, native scoped update/remove +with user/A/B preservation, cache/body ownership semantics, and actual fresh +runtime skill selection. The current global list schema is insufficient. Such +work requires its own two plan rounds and actual native evidence before promotion. + +## Concrete acceptance and finite verification + +1. Provider contracts compose actual loader/planner/stager/package verifier with + a forbidden runner. Check exact `.uap` layout, the closed native-view path set and `.qoder-plugin` skill declaration, + portable command rendering/withholding, unsupported MCP/hook/agent exclusion, + original profile, all malformed scope/root/delivery/receipt combinations and + direct adapter attestation refusal. Require zero native commands. In both + observer-enabled+forbidden-runner and default/disabled compositions require + managed ReceiptReconciled=true with NativeDiscoveryAttempted=false and + NativeDiscoveryReconciled=false; a positive prepared finding never becomes + positive native registration. Retain the existing actual unresolved-attempt + recovery fences if a broader composer already created an attempt. +2. Real public SDK default/no-runner and forbidden-runner paths install/replay/ + update/intact and missing/drift repair/remove/reopen with Prepared file facts, + byte-stable unchanged state, exact mutation flags and preserved user/A/B/data. + First add, copied receipts, foreign target, tampering and state-root replacement + refuse without adoption or user fallback. Package repair uses the existing + exact immutable pin rather than later mutable source content. +3. Actual saved CLI Git+TLS journey uses the existing dependency and source + boundaries, user+A+B authority, `deps sync --locked`, pin-preserving repair, + update, removal and host-free status. A runner refuses every vendor/MCP call. + No test-created native tuple is called real discovery. +4. Existing user Qoder registry/lifecycle suites remain unchanged and pass. Add + actual raw populated JSON as a conservative schema fixture proving it remains + global/scoped inventory evidence rather than flattening repeated IDs into one + owned row. Do not broaden user native mutation as part of this project slice. +5. Run only focused client/provider/SDK/CLI tests and relevant vet/format/diff + gates with the shared pinned Go runtime after build capacity is granted. + Cross-compile Windows if capacity permits; compilation is not native execution. + Freeze for root and peer code reviews before publication. No new native + collector command or local vendor process is authorized by this adapter plan. + +Final matrix must label this Qoder route **project-owned Prepared skill bundle**. +Native project registration/cache ownership, actual loaded selection, MCP, trust, +authentication, model and session restart remain blocked/not evaluated as above. + +## Independent R1 review record + +R1 exact55df20ac (published identical tree as4cc8d905) received Root and +Skillshare architecture GOs after both independently interpreted the genuine +artifact. Root required neutral native findings so local prepared files cannot +become NativeDiscoveryReconciled. Skillshare required a closed importable native +surface and bypass of all user native-ready/runner/CLI helpers on every project +entry. R2 incorporates those conditions above. No adapter code has been written. + + +## Independent final R2 review record + +Exact3038d3ce8e359d5bbf083f5cb152c49e76628f87 received independent final +plan GOs from Root and Skillshare before adapter implementation. Both read the +complete frozen plan and accepted the closed native view, reserved-source path +refusal, neutral project native finding, early user-helper bypass, exact package +receipt verification and explicit manual native-cache lifetime limits. The genuine +research evidence remains inventory evidence only; no native qualification is +added. Implementation now proceeds under that plan and requires two independent +frozen code reviews before publication. diff --git a/docs/plans/project-install-e2e/qoder-project-lint-extraction-plan.md b/docs/plans/project-install-e2e/qoder-project-lint-extraction-plan.md new file mode 100644 index 000000000..8c719bda5 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-project-lint-extraction-plan.md @@ -0,0 +1,25 @@ +# Qoder project lint extraction plan + +## R1: bounded mechanical changes + +Frozen source: `96117cd43064e50a4541387d5dc9a3aeddbdadeb`. Actual changed-source lint reports exactly three Qoder findings: `verifyProjectView` cognitive complexity 29 above 25; a De Morgan simplification in the selected-skill predicate; and an unused string result from `projectPlanRoot`. + +1. Extract the complete existing closed-directory path-set loop and selected-skill regular-file loop into a private helper receiving the same active path, view path, and selected-name map. Call it at the exact existing point after containment validation and before manifest reads. Preserve every iteration, containment/type check, error, and return order; no new permissive fallback or changed set membership. +2. Rewrite only the selected-skill boolean with equivalent De Morgan grouping. Unsupported entries still skip before it; supported native skills and projected command skills remain the exact admitted set. +3. Rename the private validator `projectPlanRoot` to `validateProjectPlan`, returning only error. All callers currently discard its string result. Preserve internal root computation, validation ordering and final RequireContainedChild call; update every caller without moving native/project dispatch. + +No public API, lifecycle, ownership, schema, runner, native qualification, output contract or test expectation changes. Existing project Qoder provider/SDK/actual CLI and Prepared-file regressions remain meaningful gates; rerun only affected Qoder contracts plus changed-source lint and focused vet/Windows compile checks. No vendor execution. + +This plan requires two independent R1 reviews, then a final R2 freeze and two reviews before source edits; source publication still requires two independent code approvals. + +## R2: reviewed ordering and admission precision + +Root and Claude independently approved R1 at `016060d3`. Preserve the existing map iteration (no newly promised order), each first error in the moved loops, and their call position after view containment but before bounded manifest reading and manifest-key allowlist validation. Preserve the outer full-bundle digest before and after view verification. The equivalent predicate admits exactly `Skill && (Native || (Projected && Command))` after the existing Unsupported skip. Search all private validator references and update all six callers; every currently ignored root result disappears without reordering any checks or dispatch. Existing exact negative contracts, provider/SDK/CLI journey and narrow lint remain the gates. Both final plan reviews are required before mechanical source edits. + +## Implementation and independent code reviews + +The complete Qoder/Prepared-file implementation was frozen at `96117cd43064e50a4541387d5dc9a3aeddbdadeb`. Root independently reviewed the full source/tests and confirmed its final ownership-packet alias correction; Skillshare independently reviewed the full adapter/core/provider/SDK/CLI slice and returned GO for that source plus the mechanical `da0948cb5eb6eb8fa15db7cd0e9de7c2b1ec96c8` delta. Claude independently reviewed the complete six-file mechanical delta under both approved plan rounds and returned GO for exactly `da0948cb`. These are source reviews, not native/runtime qualification. + +Exact `da0948cb` verification: meaningful PreparedProjectFile/ProjectQoder tests passed in usecase 0.055s, providers 0.319s and installer 2.381s. The complete existing Qoder adapter suite passed in 15.186s; internal architecture tests passed in 0.660s and conformance in 0.413s. Focused four-package go vet exited zero. Configured golangci-lint 2.13.2, changed-source baseline `49a5549d7912bfe34ef97d4bf6db8254c45f090d`, concurrency 1, 512 MiB heap target and 90-second deadline reported zero issues for Qoder/usecase/providers/installer. No exclusion or budget was changed. The actual saved TLS-Git CLI journey passed at the preceding complete `96117cd4` source in 5.966s; the later private extraction does not change its dispatch or assertions. + +Windows AMD64 compile-only checks passed on exact `da0948cb`: Qoder test binary 7,671,808 bytes SHA256 `719d6d53d5ffaecf5e25f8a058dbdf1c7c0233bca1757fedd4e8063f0cf28071`; SDK test binary 18,800,640 bytes SHA256 `b31ad020d0f8ddf77f79187c7023ece4614ae7047edcee1ca0587b77048853f8`. These owned completed binaries were removed after hashing. No Windows test execution, Qoder vendor process, MCP initialization, model use or automatic native activation is claimed. The only genuine native Qoder observations remain the preserved non-Git/different-version global-cache research evidence. diff --git a/docs/plans/project-install-e2e/qoder-recovery-code-review-r1.md b/docs/plans/project-install-e2e/qoder-recovery-code-review-r1.md new file mode 100644 index 000000000..7dc5d9c1c --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-recovery-code-review-r1.md @@ -0,0 +1,17 @@ +# Fresh recovered capture implementation R1 + +Reviewer finish_sources_vscode_cline. **Final GO** atf3c0d3f927d3bf10b62d875ccb7088ee3681a358. +Full production review at55d57fc7 followed two fresh pre-code GOs atfce7217b. +Nonblocking cap-plus-one reads/at-most-cap writes, separate logs, draining +deadline, leader-exit/error/interruption group cleanup/five-second reap and +descriptor closure match the plan. Only RLIMIT_CORE remains. Literal stderr +byte refusal covers all six probes including whitespace. Pins/acquisition/env/ +six argv/real guard are unchanged; baseline function AST comparison verified. + +Author's fresh capture6 PASS2.284s/Qoder7 PASS0.938s initially supported source. +R2 then observed the preserved0.2s inert deadline expire before actual startup +under load. Final narrow delta changes only test timing to1s/2s/2.1s plus an +honest failure record; startup/no-survival assertions and production30s/caps +remain. Author's fresh Qoder7 PASS3.410s/capture6 PASS2.449s close it. No blocker, +native or Go process by reviewer. This approves recovered bytes, never native +runtime; failed authentic phase one and phase-two gate are preserved. diff --git a/docs/plans/project-install-e2e/qoder-recovery-code-review-r2.md b/docs/plans/project-install-e2e/qoder-recovery-code-review-r2.md new file mode 100644 index 000000000..1c840966f --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-recovery-code-review-r2.md @@ -0,0 +1,15 @@ +# Fresh recovered capture implementation R2 + +Reviewer finish_grok_integration. **Final GO** atf3c0d3f927d3bf10b62d875ccb7088ee3681a358. +Reviewed all production delta at55d57fc7: exact known selector/live separate +caps plus one, remaining deadline, group cleanup, core-only/no-FSIZE. Independent +function-AST comparison found every Qoder function identical to published249a9 +except the approved all-byte-stderr validator. Capture6 independently PASS3.295s. + +Initial preserved Qoder suite failed its0.2s actual-startup assertion under +host load; not counted as pass. The narrow1s deadline/2s marker/2.1s post-wait +correction retains actual startup and killed-group proof, with production +unchanged. Independent final Qoder7 rerun PASS3.243s. Workflow/pins/guard/env/ +six argv unchanged except focused helper paths/tests. Authentic native failure +is not relabeled and phase two stays gated. No blocker, vendor/native or Go +process executed by this review; inert Python children only. diff --git a/docs/plans/project-install-e2e/qoder-recovery-implementation-evidence.md b/docs/plans/project-install-e2e/qoder-recovery-implementation-evidence.md new file mode 100644 index 000000000..664f6629d --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-recovery-implementation-evidence.md @@ -0,0 +1,51 @@ +# Recovered Qoder capture implementation: fresh evidence + +Fresh recovered plan R1/R2 GOs at2395c514 were committed atfce7217b before +source. Published249a9's exact two-file helper/Qoder factoring baseline was +materialized and byte equality verified before modification. Reconstructed +source preserves the approved capture behavior and final any-nonempty-stderr +correction; old executions are not used as current evidence. + +Parent nonblocking separate pipes cap each original log live to1MiB; exact-cap +EOF succeeds and one extra byte fails. The original deadline includes draining; +leader exit/error/timeout/overflow/interruption kill original-group descendants, +reap within the existing bound and close descriptors. Core dumps remain disabled, +no RLIMIT_FSIZE limits unrelated native cache files. No guard/native argv/pin/ +acquisition/environment or native qualification rule changed. All six Qoder +probes reject every nonempty stderr byte stream including whitespace. + +## Fresh recovered-workspace checks + +- Capture regression suite6 PASS2.284s: actual exact cap/overflow on both + streams, concurrent262KiB streams, DEVNULL, unrelated2MiB cache write under + a64-byte stream cap, started descendants/leader-exit/error/interruption + cleanup, descriptor closure and bounded failed startup. +- Preserved Qoder suite7 PASS0.938s, including the actual unchanged hosted + guard's zero-effect refusal, original pins/archive admission and actual + inert timeout/descendant cleanup. Added all-six whitespace/warning refusal. +- Function-AST comparison to the exact published factoring baseline: every + Qoder function identical except the approved stricter validate_probe. +- Python syntax, read-only workflow/ten-minute contract and whitespace PASS. + +These checks executed inert Python children only. No vendor/native client, +acquisition, trust, model, MCP or hosted-guard spoofing occurred. Failed genuine +phase one remains failed and its reverified raw bytes are preserved. Two fresh +independent reconstructed-code reviews are required before root publication +and a genuine warning-free phase-one replay; phase-two/adapter source stays +blocked on that future artifact and independent interpretation. + +## Fresh independent R2 fixture finding + +R2 independently passed the six capture checks3.295s, but the preserved old +Qoder deadline test failed its actual-startup assertion under host load: its +0.2s deadline expired before the inert descendant wrote ready. This failure +is retained, not counted as pass. The meaningful fixture keeps its startup +and killed-group/no-survival assertions, using a1s inert deadline,2s delayed +survival marker and2.1s post-wait. Production maximum30s and cleanup behavior +are unchanged. Fresh current checks must close this test-only correction. + +Final restored sourcef3c0d3f9 passed fresh author Qoder7 tests3.410s and capture6 +tests2.449s. R1 closed the narrow test-only delta; R2 independently reran final +Qoder7 tests PASS3.243s. Both fresh final code GOs are now recorded, with all +old/failed checks kept distinct. Root publication and genuine warning-free +phase-one replay/interpretation are the next gates; no native success is inferred. diff --git a/docs/plans/project-install-e2e/qoder-recovery-integration-evidence.md b/docs/plans/project-install-e2e/qoder-recovery-integration-evidence.md new file mode 100644 index 000000000..4f38bae33 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-recovery-integration-evidence.md @@ -0,0 +1,46 @@ +# Qoder recovery integration checkpoint + +Published Qoder recovery b88934a2babb2c5787edfcd7e958d48c82d861e7 has exact +tree4f170c9758bd2682d731377db76d1e5ca71f7e63, identical to reviewed local +4ead80660a5b3b114d95304843cbf8d3b014add9 before executor loss. +The complete96-entry delta and retained native evidence were recovered without +behavioral reconstruction. Existing published branch4cc8d905 and integrated +root base49a5549d are preserved as parents. + +Prior source review closure: full implementation/ownership-alias correction96117 +had root and Skillshare CODE GOs; mechanical extractionda0948 had Claude and +Skillshare CODE GOs. Final4ead changed receipt documents only. Exact tree identity +preserves source attribution; it does not turn old runs into new execution. + +## Fresh assembly review + +Local integration8c92161584bc6538ba378202c5d938b8c7bfa94d, +tree4d89a9645f99164984367843f986081cc5f9ae34, merged without conflicts. +Both /root/claude_codex_scope and /root/skillshare_scope independently gave +integration CODE GO after comparing both parents and tracing the core hooks. +Every one of the73 changed merge blobs equals the reviewed published Qoder +parent; root removal/group/Claude fixes remain from the first parent. + +They confirmed the sole Qoder project Prepared capability addition, exact +binding/context/profile/receipt/no-pending-attempt checks, copied classifier and +activation packets, and strict read-only outcome/effect gates. Unavailable +verification cannot become installation proof; effect contradiction retains +actual mutation and uncertainty. NativeConfig and CLIRegistry clients remain +outside this prepared-file candidate path. No new native-cache ownership, +native discovery, attestation, model or MCP claim follows. + +## Fresh assembly checks + +At4d1d5bcc8077fa26f6e28d06ee755cf151a999dd (same Go source as8c921615), +verified Go1.26.10 focused tests passed: +prepared-project verifier regressions0.042s, Qoder provider coverage0.780s, +and SDK Qoder/removal/group/Kimi/Kiro lifecycle coverage18.939s. +The qoder adapter package had no test matching this selector and is not counted +as an additional test pass; its project behavior is exercised by providers/SDK. +Changed-source golangci-lint2.13.2 over qoder/usecase/providers/installer/domain +against published92623d4 reported0 issues. + +This is a reviewed source integration and fixture checkpoint. Final required CI, +actual platform execution and the remaining OpenCode/Cline/VS Code lanes remain +open. Qoder's optional explicit native import uses a separate user cache; UAP +project removal owns only its prepared project bundle. No release was published. diff --git a/docs/plans/project-install-e2e/qoder-recovery-plan-review-r1.md b/docs/plans/project-install-e2e/qoder-recovery-plan-review-r1.md new file mode 100644 index 000000000..4b6611ce5 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-recovery-plan-review-r1.md @@ -0,0 +1,13 @@ +# Fresh recovered Qoder capture plan R1 + +Reviewer finish_sources_vscode_cline, independent of author. **GO** at +docs-only2395c514f7af4e2d749b125804bbc9626dc62c9b after reading the full +restored amendment. Failed authentic artifact was reverified; published249a9 +two-file factoring is an appropriate immutable replacement for lost local3107. + +Exact per-stream cap-plus-one/selectors/deadline including draining/core-only +limits/process-group cleanup/all-six literal stderr-byte refusal preserve the +previously reviewed semantics. Historical tests and GOs are explicitly stale. +No new source/native permission or project qualification follows. Fresh focused +inert tests and two fresh code GOs are required before source publication. +No finding or blocking plan issue. No Go/native execution by reviewer. diff --git a/docs/plans/project-install-e2e/qoder-recovery-plan-review-r2.md b/docs/plans/project-install-e2e/qoder-recovery-plan-review-r2.md new file mode 100644 index 000000000..3924f4e73 --- /dev/null +++ b/docs/plans/project-install-e2e/qoder-recovery-plan-review-r2.md @@ -0,0 +1,15 @@ +# Fresh recovered Qoder capture plan R2 + +Reviewer finish_grok_integration, independent of author. **GO** at recovered +docs-only2395c514f7af4e2d749b125804bbc9626dc62c9b. Restored plan faithfully +preserves the known contract: published249a9 factoring equality before code, +real guard/pins/environment/six argv unchanged, parent-owned separate live +caps plus overflow byte/exact EOF, nonblocking dual draining/remaining deadline, +group cleanup/bounded reaping, core-only/no FSIZE, and rejection of every +nonempty stderr stream including whitespace on all six probes. + +Failed918333b artifact stays failed. Meaningful inert unrelated2MiB cache write, +simultaneous streams, actual descendant startup and interruption cleanup tests +plus two fresh code GOs precede publication. Genuine warning-free phase one +must pass before populated phase-two source. No blocker, source edit, native +client or compiler was run by reviewer. Historical results are not current proof. diff --git a/docs/plans/project-install-e2e/reviews/devin-code-review-r1.md b/docs/plans/project-install-e2e/reviews/devin-code-review-r1.md new file mode 100644 index 000000000..dc076eecc --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-code-review-r1.md @@ -0,0 +1,83 @@ +# Current Devin project adapter — independent code R1 + +Reviewed frozen `132b8c38226f87a39724637cf5ac11b7e0978ea9` in +`work/uap-project-windsurf` on 2026-10-23. Reviewer did not author the adapter. +**NO-GO pending the native-comments correction below.** The separately reviewed +shared host-version prerequisite `64c7ee6d` already has both implementation GOs; +its approval is not repeated or widened here. The selected-versus-sibling facts +amendment has both plan GOs before the exact three-line collector exclusion. +No vendor/native client, launcher, model, trust/auth or MCP process was executed. + +## Blocking finding + +`clients/windsurf/project_policy.go:readDevinProjectDocument` directly passes +native config.json/config.local.json/mcp_config.local.json into the strict JSON +decoder. `nativeconfig.parseCodecDocument` also routes new CodecDevin target +mcp_config.json through strict JSON admission. Current official config-file +documentation explicitly supports JSON comments and the pinned distribution's +documentation agrees. Ordinary unrelated comments in a main/local file or +target therefore block an otherwise qualified project installation. + +Correct native policy reads and CodecDevin only using the existing comments-only +AST grammar, keeping duplicate-key/name/path checks, original foreign bytes, +receipt digests and ownership/rollback intact. Preserve legacy CodecWindsurf, +portable JSON and Gemini's existing grammar/error text. Reject undocumented +trailing commas rather than broadening to all hujson syntax. The author accepted +the finding and froze amendment `e09d06d3`; its R1 plan is GO in the separate +report, with R2/source/implementation closure still required. + +Required regressions: commented unrelated main/local files and target patches +preserve exact foreign comments/disabled entries; commented legacy MCP and +same-name local shadowing still refuse without effects; duplicate/trailing +input remains refused in both reader paths; legacy Windsurf strict and Gemini +comments contracts remain unchanged. + +## Inspected remaining boundaries + +The new automatic lane requires the selected primary absolute clean devin or +devin.exe and exact host-observed version 3000.11.205. It does not infer current +Devin from legacy editor presence, probe from SDK/adapter, rewrite HOME, choose a +fallback profile or execute native mutation. An absent original legacy profile +is legitimate only under the previously reviewed project SDK admission. +The original user root must still agree exactly with TargetContext. + +Plan/client/scope/native-root/artifact and exact managed package layout are +checked before effects. Direct activation requires the delivered package path; +pre-staging alone permits it absent. Previous/desired native receipt paths, +kinds, IDs, duplicates and source paths use the reviewed shared validation. +Binding inspection rejects copied scope/artifact/package/native roots. Active +stdio reuse verifies selected package/data/cwd launcher coordinates and reserved +environment values without rewriting immutable bytes. Project migration and +same-name higher local policy refuse; other imported/effective policy is +truthfully unobserved. + +The dedicated current codec retains command/args/env without native cwd and +explicit remote transport, separate from legacy Cascade serverUrl. All writes +use the reviewed component transaction/kernel; the authored tests cover stale +ownership, collision, visible replacement rollback and committed cleanup +receipts. Project attestation is refused, and configured results remain prepared +with installation verification. Exact recorded removal does not require a +current executable/version and does not call default-local native remove. + +The selected KnownTargets exclusion follows the existing sibling-only contract; +primary Request/prepared client evidence remains authoritative, while all +unselected sibling root/executable/BindingID checks remain unchanged. Four +compatibility goldens change only Windsurf's project scope list. Legacy user +source/selection and useful tests are retained. Public TLS Git/ZIP and SDK +scenarios are honestly fixture composition with no native runtime claim. + +Independent execution of the final corrected frozen checkpoint will be recorded +with closure below. The author's existing passes remain separate evidence, +not independently rerun results in this interim report. + +## Additional exact client authority finding + +Concur with independent R2: `validateDevinProjectBinding` must also require literal `binding.ClientID == "windsurf"`. Scope/root/artifact/object checks alone allow a copied contradictory owner to reach direct binding inspection. Require refusal in direct validation/inspection with a copied-client-ID unchanged-effects regression. The author accepted this original exact-client authority correction; final R1 remains pending both this and native-comments closure at a frozen SHA. + +## Final corrected source and integration closure + +**GO at clean frozen `d69091b3328a1b626d9475643e8e97a8bf040b4a`.** Both accepted blockers are closed. Both native-comments plan GO reports precede source4f5d9558. CodecDevin and read-only policy inspection now reuse the existing comments-only AST admission: duplicate keys at every nesting level and undocumented trailing commas refuse. Strict normalized JSON is used only for read-only inspection; original file bytes and AST remain the mutation/ownership authority. Gemini's existing comments grammar/error text and legacy CodecWindsurf/portable strict admission are preserved. New tests cover unrelated commented main/local policies without rewrites, commented legacy/shadow refusal, foreign target trivia preservation, duplicate/trailing refusal and legacy strictness. Exact literal Windsurf ClientID is required by shared project binding validation, with direct and inspection copied-codex refusal. + +Independently inspected all three manual union conflicts in the true merge of reviewed d39: Codex TOML dispatch remains first, Gemini plus Devin comments-only parsing remains separate; project server dispatch retains Copilot and Devin with all prior codecs; supportedCodec retains Codex, Copilot, Devin and every previous codec. No authored Windsurf/provider/SDK path changed from4f5 to the merge, and every other client directory is byte-identical to d39. No new semantic merge resolution found. + +The authored public TLS Git/ZIP/SDK/provider/legacy passes remain separately attributed evidence. Parent reports the other independent reviewer ran final full nativeconfig PASS3.471s and preserved legacy client PASS0.006s. This R1 did not repeat those results or execute a native client. Local compiler capacity was exhausted; parent explicitly directed static final closure instead of redundant broad rebuilds, reserving the next slot for genuinely new Source corrections. The complete static review and targeted unchanged-authority regressions leave no code blocker. Genuine project discovery/trust/session/MCP/native loading and release qualification remain pending; prepared/installation evidence must not be reported as attestation. diff --git a/docs/plans/project-install-e2e/reviews/devin-code-review-r2.md b/docs/plans/project-install-e2e/reviews/devin-code-review-r2.md new file mode 100644 index 000000000..53a87e691 --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-code-review-r2.md @@ -0,0 +1,59 @@ +# Current Devin project adapter — independent implementation R2 + +## Frozen initial review + +Reviewer codex_claude, 2026-10-23. Initial target `132b8c38226f87a39724637cf5ac11b7e0978ea9` in work/uap-project-windsurf. **Not yet GO:** the accepted R1 native-comments seam and the bounded direct-binding client discriminator finding below require closure at a corrected frozen source SHA. Reviewer has not edited this worktree or executed a vendor/native process. + +## Findings + +1. R1 independently identified documented comments in current native Devin configuration. The selected target CodecDevin and native main/local policy decode currently reject them as strict portable JSON. Exact vendor3000.11.205 docs confirm line/inline/block comment grammar. The separate docs-only comments correction plan `e09d06d372575b0ad8edc0bf5ffa5f9ecc030e0f` has this reviewer's R2 GO in research/devin-native-comments-plan-r2.md; retain Gemini grammar/errors, all legacy/portable strictness, duplicate/trailing refusal and original comment bytes. No parser source changed by this reviewer. + +2. `validateDevinProjectBinding` validates scope/root/artifact/path/objects but omits `binding.ClientID`. An otherwise identical direct binding tagged for another client is therefore passed to ProjectComponents.Inspect as ownership. That helper deliberately checks component receipt mechanics, not client identity, so the native record can be reported RegistryExpected despite contradictory owner identity. Require exact existing product ID `windsurf`, leaving user behavior unchanged. Update the valid copied-binding fixture to include its real ClientID and add a wrong-client inspector case. Higher production composition already fences IDs; this correction makes the direct project ownership boundary agree with its stated contract. + +## Static coverage completed + +Traced the current selected primary basename/version gate, original user-context equality including legitimate both-empty current-only profile, exact project/native root and `.uap/managed/clients/windsurf/` binding, selected-delivery/path fences, shared host-version propagation and selected-versus-sibling exclusion, project-only native lifecycle/backend policy, new Devin transport codec versus preserved Cascade codec, skill/MCP object receipts, no-follow paths, immutable selected stdio package/data/cwd/environment reuse, direct/composed attestation refusal, and version-independent exact recorded removal. No additional blocker found in those boundaries. + +Project policy refuses legacy main-config MCP migration and same-name local override rather than modifying it; unrelated imported/user/ancestor/aliases/cloud/team/session effects remain unobserved. Project materialization uses the reviewed component transaction/native kernel with existing rollback and committed-cleanup receipt semantics. It never calls default-local native remove or registers a user plugin. Existing detection/channel ordering and legacy user paths/lifecycle remain preserved. + +Inspected meaningful fixture coverage: production CLI saved TLS Git/ZIP add/locked-no-op/read/repair/update/export/cold-import/remove plus no-save, public SDK A/B stdio/data/exact repair/restart/remove and missing/wrong-version/empty-profile/non-injection admission, selected-only KnownTargets correction with exact sibling facts, direct/composed root/artifact/delivery/receipt refusal, foreign disabled/stale/symlink preservation, visible-write rollback and committed-cleanup verification. These are configuration/ownership fixtures with inert host/security/launcher boundaries, not genuine native loaded/runtime proof. Ten actual bounded version/help observations and pins remain pre-code evidence only. + +Independent affected test results and final correction closure will be appended at the next frozen checkpoint. Native discovery/trust/permissions/authentication/MCP initialization/runtime/Desktop/release and all-client completion remain unqualified by this review. + +## Final correction and component-base closure + +**GO** at frozen clean `d69091b3328a1b626d9475643e8e97a8bf040b4a`. +Both initial findings are closed. The native-comments amendment and both plan +GO reports were committed ate8f638ed before corrective4f5d9558 source. Its new +Devin branch reuses the existing comments-only AST admission and retains Gemini +error labels/explicit JSONC behavior. Duplicate keys and trailing commas refuse +in the target codec and the ephemeral policy normalizer. Original foreign +comments/entries remain in the mutation AST. Legacy Cascade and portable strict +JSON retain their previous grammar. Commented main/local migration and same-name +shadowing policy has meaningful preserved-byte success/refusal coverage. + +The direct project validator now requires literal binding.ClientID `windsurf`. +Its valid binding includes that discriminator, and copied-client bindings are +checked by both direct validation and native inspection. User-scope validator +behavior remains unchanged. + +Independently examined all three manual d39e40ef component-base merge conflicts: +Codex TOML dispatch precedes the Gemini/new-Devin comments parser, projectServer +retains both Copilot and Devin dispatches, and supportedCodec retains every +baseline member plus Devin. All foreign client directories are byte-identical +to d39e40ef; the domain diff is only the Windsurf project wrapper. All four +compatibility JSON goldens preserve every non-Scopes field. The first static +comparison used lowercase `scopes` and correctly failed; rerunning with the +actual `Scopes` field confirmed the narrow change. No source changed in review. + +Independent full nativeconfig tests passed3.471s at this frozen target, including +Codex/Copilot/Gemini/Devin codec routes and existing kernel ownership/rollback +checks; the preserved Windsurf client suite passed0.006s. A subsequent independent +project provider/SDK/public CLI selection could not execute its intended +assertions because package snapshot creation under actual /tmp failed ENOSPC. +Those are recorded resource failures, not passes or semantic regressions. Parent +explicitly requested no duplicate broad builds under the shared-space shortage. +Prior authored actual composed/provider/public SDK/CLI successes recorded above +remain evidence for unchanged paths, and the focused native codec execution plus +static correction/merge review support this source GO. No new vendor/native +process ran, and current effective native discovery/runtime remains pending. diff --git a/docs/plans/project-install-e2e/reviews/devin-native-comments-plan-r1.md b/docs/plans/project-install-e2e/reviews/devin-native-comments-plan-r1.md new file mode 100644 index 000000000..0470a135f --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-native-comments-plan-r1.md @@ -0,0 +1,40 @@ +# Devin native comments amendment — independent plan R1 + +Reviewed documentation-only frozen +`e09d06d372575b0ad8edc0bf5ffa5f9ecc030e0f` in `work/uap-project-windsurf` on +2026-10-23. **GO** after both plan GO reports are committed. The reviewer +identified the original native-comments admission defect but did not author +this amendment or corrective source. No native process was executed. + +The defect is present in both strict native-policy inspection and CodecDevin's +target document path. Official current configuration documentation and the +author's exact-version distribution documentation support JSON comments; they +do not establish trailing commas. The plan correctly treats that as a narrow +native grammar correction rather than relaxing portable JSON or inferring +native project execution. Automatic approval review rejected the earlier source +attempt because this shared parsing amendment lacked two plan GOs; the command +did not execute, so source remains frozen pending this gate. + +Reusing the existing comments-only AST admission preserves exact foreign trivia, +duplicate-key/name/path checks, no-follow IO, locks, canonical entry digests and +transaction/receipt behavior. Gemini delegates with its existing label and +error/grammar contract; only CodecDevin additionally opts in. Legacy Windsurf +and other codecs keep their current grammar. The read-only normalizer uses that +same comments-only admission before standardization and the existing strict +unique-key object decoder, preserving original policy bytes and refusing +undocumented trailing commas rather than accepting all hujson extensions. + +Planned meaningful regressions cover commented target add/remove with exact +foreign disabled-policy trivia, duplicate/trailing refusal in both entry paths, +unchanged legacy strict refusal and Gemini behavior. Composed unrelated main/ +local override files must remain byte-identical while commented legacy MCP or +same-name shadowing still refuse with user/B unchanged. Existing exact context, +artifact, data, attestation, visible-write rollback and cleanup receipt cases +remain mandatory. No second writer, migration, permission/trust write, profile +fallback, process, acquisition or parser-policy change outside these native +inputs is introduced. + +No blocking plan issue found. Freeze the implementation for both independent +R1/R2 refreshes before root publication. The underlying Devin adapter R1 stays +pending until that correction is independently inspected and verified; this +plan GO creates no native/runtime/project-success or release claim. diff --git a/docs/plans/project-install-e2e/reviews/devin-native-comments-plan-r2.md b/docs/plans/project-install-e2e/reviews/devin-native-comments-plan-r2.md new file mode 100644 index 000000000..a9ff9c399 --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-native-comments-plan-r2.md @@ -0,0 +1,13 @@ +# Devin native comments correction — independent supplemental plan R2 + +**GO** at docs-only frozen `e09d06d372575b0ad8edc0bf5ffa5f9ecc030e0f`, work/uap-project-windsurf, reviewer codex_claude, 2026-10-23. No corrective source or native process executed by this review. Original client plan and final implementation review requirements remain in force. + +Independently read the exact3000.11.205 distribution's pinned reference/configuration/config-file.mdx JSON-with-Comments section: line, inline and block comments are explicitly supported. Independently traced the current readDevinProjectDocument strict portable decode and CodecDevin native target path. They reject documented native comments; no genuine native project success can be inferred or fabricated from this static grammar correction. The amendment transparently records the earlier automatic approval review rejection and that the source command did not execute. + +The proposed reuse is suitably narrow. Extracting Gemini's existing comments-only admission into a private labeled helper preserves its original AST, duplicate-key rejection, commentsOnly rendering and trailing-comma refusal/cleanup. Keep Gemini's existing branch behavior and exact error labels/text, including its existing explicit JSONC branch, unchanged. Opt only the new CodecDevin into the helper; all old codecs, especially legacy CodecWindsurf, retain their previous grammar and ownership behavior. Do not broadly accept all JSONC features merely because hujson can parse them. + +A read-only StandardizeCommentedJSON wrapper using the same admitted syntax followed by standardization is appropriate for Devin main/local policy inspection. Strict unique-key object decoding remains after normalization; original bytes/AST and native file/receipt digests are never replaced by standardized inspection bytes. No portable decoder, manifest, runtime policy, migration, local override, no-follow IO, transaction, rollback or trust/attestation authority needs to change. + +The proposed codec and composed-provider regressions resolve the concrete risk: accept and preserve valid foreign comments/policy, refuse duplicate keys and unsupported trailing commas in both codec and normalizer, keep legacy Windsurf strict and existing Gemini grammar/errors, refuse commented migration/same-name local shadowing with unchanged user/A/B inventories, and retain exact receipt/data/rollback/committed-cleanup gates. Bounded affected tests/lint, two plan GO reports before code and refreshed R1/R2 code reviews before root publication are sufficient. + +No plan blocker found. This syntax GO does not establish native project discovery/runtime or release qualification. diff --git a/docs/plans/project-install-e2e/reviews/devin-selected-facts-plan-r1.md b/docs/plans/project-install-e2e/reviews/devin-selected-facts-plan-r1.md new file mode 100644 index 000000000..d4b7206e3 --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-selected-facts-plan-r1.md @@ -0,0 +1,47 @@ +# Selected-client facts follow-up — independent plan R1 + +Reviewed frozen documentation-only `c48bbc58` in `work/uap-project-windsurf` on +2026-10-23. **GO** for the narrow selected-client exclusion, after both plan GO +reports are committed. No correction source or native operation was performed +by this reviewer. The unfinished later adapter is outside this plan review. + +Independently traced dependencyInstaller.Prepare: it selects the primary client +from the detected map and builds Request.ClientID/ClientConfigRoot/executable/ +ClientVersion before populateDependencyTargets. The collector currently loops +every existing binding and duplicates that selected primary into KnownTargets. +The published Request comment explicitly reserves KnownTargets for unselected +installed siblings. Its knownTargetIndex correctly requires each sibling's +clean absolute root and exact BindingID; weakening that separate boundary to +admit a legitimate absent primary profile would be the wrong correction. + +The planned early exclusion compares only binding.ClientID to the already +selected Request.ClientID before sibling lookup/collection. All unselected +binding facts, missing-sibling errors, executable selection, binding IDs and +strict knownTargetIndex checks remain unchanged. No new empty-root exception, +version field, group behavior or user capability is added. For the current +single-client dependency request there is no need to duplicate selected facts. + +Independently traced SDK detectedClients and compatibilityChecks. The primary +continues through Request reconstruction under the reviewed exact engine/scope/ +namespace admission. Compatibility uses the Request's primary root/executable +directly when binding.ClientID equals req.ClientID; only a different binding +looks up KnownTargets and compares its owned ClientBindingID. The subsequent +compatibilityBindingCheck restores the prepared target.Client for the selected +client, retaining its fresh captured version/authority. The duplicate selected +KnownTargets entry supplies no extra selected-binding authority there. Existing +installation/binding/environment selection, package pin, target/native object, +profile and namespace validation still guard the mutation. + +The proposed verification covers the actual composition defect: current-only +no-legacy-profile public saved install must continue through locked sync, +repair, update, transfer and removal. The focused builder regression must show +the selected client absent from KnownTargets while an exact unselected sibling +remains; existing missing/invalid sibling profile, stale BindingID and separate +executable compatibility tests must stay intact and pass. The earlier genuine +failure is valid author evidence, while final execution proof belongs to the +frozen correction and adapter implementation review. + +No blocking plan issue found. This permits no profile guessing, default HOME, +version-dependent cleanup, native process, trust/model/account/MCP action or +discovery/runtime qualification. Freeze the correction with the final adapter +for both independent implementation reviews; root owns publication. diff --git a/docs/plans/project-install-e2e/reviews/devin-selected-facts-plan-r2.md b/docs/plans/project-install-e2e/reviews/devin-selected-facts-plan-r2.md new file mode 100644 index 000000000..9bd0b9297 --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-selected-facts-plan-r2.md @@ -0,0 +1,11 @@ +# Selected Devin facts versus sibling facts — independent plan R2 + +**GO** at docs-only frozen `c48bbc58` (`docs/plans/project-install-e2e/devin-selected-facts-amendment.md`, work/uap-project-windsurf), reviewer codex_claude, 2026-10-23. No correction source changed or native process executed by this review. + +Independently traced populateDependencyTargets, knownTargetIndex/detectedClients and compatibilityChecks/compatibilityBindingCheck. The CLI collector currently repeats every installed binding as KnownTargets, including the selected single request client. knownTargetIndex eagerly applies its sibling-only nonempty clean ConfigRoot contract even to that redundant primary record. Current-only Devin correctly has no legacy Cascade root, so the duplicate record conflicts with the separately reviewed project-only primary admission. Its rejection does not establish missing selected authority. + +Skipping only binding.ClientID == request.ClientID before sibling lookup/appending matches the existing Request field contract. Primary request reconstruction still carries the selected current executable/version/original profile through its strict project/adaptor gates; selected compatibility checks use request facts, and selected prepared target.Client replaces observational reconstruction. Installation, binding/environment identity, physical scope/profile/native roots, immutable package and object receipts remain enforced. No selected KnownTargets duplicate is an additional ownership token. Groups and all unselected sibling fact collection/BindingID/executable/root/duplicate validation remain unchanged; no empty-root exception belongs in knownTargetIndex. + +The proposed failing real saved TLS Git/ZIP current-only no-profile install/sync/repair/update/cold-import/remove journey plus affected-builder selected-omission/exact-sibling assertion and unchanged existing missing/invalid sibling tests directly cover the defect. Keep user/group behavior neutral and exact removal/recovery free of new version gates. Freeze the correction with final Devin adapter code for both independent implementation reviews. + +No blocker found. This GO is not a native discovery/runtime/Desktop/account/trust/release claim. diff --git a/docs/plans/project-install-e2e/reviews/devin-shared-code-review-r1.md b/docs/plans/project-install-e2e/reviews/devin-shared-code-review-r1.md new file mode 100644 index 000000000..89484d3be --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-shared-code-review-r1.md @@ -0,0 +1,47 @@ +# Devin shared host evidence prerequisite — independent R1 + +Reviewed frozen `64c7ee6d48084da6247a200eb3e77e8e6eb0dc29` after the separately +approved two-GO amendment. **GO for this shared prerequisite.** Reviewer did not +author the implementation. This is not a review or qualification of the later +Devin project adapter. + +The production delta is narrow: an optional operational Request.ClientVersion +string with json exclusion, copying it to the reconstructed DetectedClient, +selected.Version propagation from each of the two existing CLI installer request +builders, and permitting an absent original user ConfigRoot only in a configured +project engine. There is no new subprocess, probe, detection ordering, registry +row, native capability, portable schema or group expansion. A request cannot +obtain the operational version through Request JSON. The immutable prepared +request copies the string by value, so a caller changing its original request +cannot replace captured version evidence. + +Public Prepare validates exact request/engine scope and project root, declared +project support and the real namespace before profile capture or reconstruction. +The empty-profile exception is therefore confined to an already validated +project engine/request. User scope still requires a profile, every nonempty +profile must remain absolute and clean, and existing ProfileResolver hooks still +run. A project root is never substituted for the missing user profile; later +adapters must retain their exact original-context and native-layout checks. + +The saved request builder preserves fresh selected evidence for install/update/ +repair, and the no-save facade adds the same selected metadata without widening +its supported-client route. The existing no-save usecase already retains its +DetectedClient directly. Group/known-target DTOs are unchanged under the approved +Claude/Codex-only group boundary. Removal and status retain their existing +recorded authority and do not acquire a new version requirement or process. +Absent evidence stays unknown. Supplying this host field does not establish +binary provenance, native discovery, effective session policy or runtime proof. + +Independent verification at an isolated exact-commit checkout: +`go test -p=2 ./installer -run '^TestProject(PreparePreservesSelectedHostVersion|HostEvidence)' -count=1` +passed in **0.010s** using the prescribed existing toolchain/cache. The tests cover +prepared value capture, JSON exclusion/injection refusal, unchanged original +profile, invalid nonempty project profiles, user empty-profile refusal, legitimate +generic project absence and stricter Gemini resolver refusal. An initial attempt +in the author's actively edited worktree overlapped unfinished later adapter +source; it was discarded and no conclusion drawn from that non-frozen build. + +The full Devin adapter still needs exact selected executable/version admission, +both public paths and SDK lifecycle coverage, missing/older/wrong-host refusals, +current-only absent-profile behavior and two independent implementation reviews. +No vendor process or native mutation was executed during this review. diff --git a/docs/plans/project-install-e2e/reviews/devin-shared-code-review-r2.md b/docs/plans/project-install-e2e/reviews/devin-shared-code-review-r2.md new file mode 100644 index 000000000..043fb2e55 --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-shared-code-review-r2.md @@ -0,0 +1,11 @@ +# Shared selected-host evidence implementation — independent R2 + +**GO** at frozen `64c7ee6d48084da6247a200eb3e77e8e6eb0dc29`, `work/uap-project-windsurf`, reviewer codex_claude, 2026-10-23. Plan and both GO reports preceded source at f3a4149d. This review covers the small shared prerequisite, not the forthcoming Devin/Kimi adapters. + +The optional Request.ClientVersion string is excluded from JSON decode/encode and copied into the selected reconstructed DetectedClient. Existing value-copy preparation preserves it after caller mutation. Both existing CLI builders copy Version from the same selected detection record; project backendExecutable selects that record's primary executable. No portable schema, detector/probe/env/order, process runner, group fields, capability or request route is expanded. Windsurf no-save still uses its existing direct usecase and selected domain client, and the limited SDK facade remains limited to its existing qualified clients. Selected update compatibility checks restore target.Client, retaining its already prepared version; no persistent package version becomes host evidence. + +Generic absent-profile admission is limited to an engine configured for project scope. Public Prepare checks exact request/engine scope and project root, client project qualification and project namespace before reconstruction. Nonempty roots remain clean absolute; user scope still rejects absence. Existing profile resolvers still execute, including Gemini's stricter empty-root rejection. There is no fake profile or fallback native root. Client adapters retain exact selected original user-context equality and scoped root/receipt checks. Exact removal and journal recovery receive no new version gate from this shared change. + +Independent focused SDK selection `^TestProject(PreparePreservesSelectedHostVersion|HostEvidence)` PASS0.012s using pinned Go1.26.10, GOMAXPROCS3/-p2/-count1. It exercises public prepared version preservation/JSON non-injection, invalid nonempty project profile, user empty-profile refusal and the generic project-only empty-profile/strict-resolver boundary. The empty-profile generic test intentionally calls reconstruction because Devin admission is not yet enabled; complete public absent-profile Devin lifecycle and both CLI routes remain required in its adapter checkpoint. No native process was executed. + +No blocker found. Kimi may reuse this exact source commit after the second independent code GO. Neither this GO nor the tests establish current Devin/Kimi source provenance, native discovery, trust/account readiness, MCP initialization, all-client completion or release qualification. diff --git a/docs/plans/project-install-e2e/reviews/devin-version-plan-r1.md b/docs/plans/project-install-e2e/reviews/devin-version-plan-r1.md new file mode 100644 index 000000000..90d284d7d --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-version-plan-r1.md @@ -0,0 +1,21 @@ +# Devin host-version evidence amendment — independent plan R1 + +Reviewed initial `11c17c87c51a9e29a223446650ffaa9b0d62e3e3` and frozen expanded amendment `20dc9db18c47c9ad6879d3a6c8cc0cc7067f5714` in `work/uap-project-windsurf` on 2026-10-23, including the current public installer request/reconstruction, no-save facade, saved dependency request builder, removal and compatibility paths. **Final GO at 20dc9db1 for the narrow metadata propagation, project-only absent-user-root alignment and adapter qualification implementation.** No source was changed during this review. + +The observed gap is real: `installer.Request` currently carries client ID, config root and executable but no version; `Engine.detectedClient` reconstructs a `DetectedClient` without Version. The existing CLI facade and `dependencyInstaller.Prepare` both have the selected detected client available but discard its version when constructing that request. A plain optional operational string, excluded from request diagnostic JSON and copied with the immutable prepared snapshot, is an appropriately narrow bridge. It introduces no probe, mutable shared evidence object, portable source authority or second installer engine. + +The plan correctly retains the current host's existing version acquisition rather than running a new process from SDK Discover/Prepare or the adapter. CLI saved replay/update/repair use the same selected client's fresh request-scoped metadata; an embedding SDK must explicitly supply its own already observed value. Unknown evidence stays unknown. Version metadata is not binary provenance: only the adapter's selected primary `devin`/`devin.exe` plus its exact admitted current version may qualify the new project configuration lane. A coexisting legacy Windsurf primary cannot borrow a separate Devin surface/version. Existing user detection order, backend/group behavior and the original detected user root remain intact. + +Implementation should preserve the stated meaning at existing sibling-backend call sites: the shared field carries selected-client metadata and must not become an authority to qualify a different sibling executable. The actual Devin project lane has no such sibling substitution and explicitly checks its primary executable. No new generic client-ID/version policy or group-target extension is needed. Keep any interpretation/gating inside the project adapter; merely carrying a version must not impose a new user/other-client prerequisite. + +Removal already bypasses package replanning and builds from exact recorded binding authority. The plan explicitly keeps removal version-independent and requires no native cleanup process, so an unknown or changed current host version cannot strand owned project files. Read-only status/inspection should retain that recorded-authority behavior and must not trigger a fresh version probe or invent a native qualification; install/update/repair are the newly gated operations. Compatibility checks preserve the selected prepared client instead of silently reconstructing an unversioned replacement for that same target. + +The proposed meaningful tests cover the required risk: both real CLI request paths and the public SDK must deliver the exact selected metadata; missing/older/other-harness input refuses before effects; portable package manifests cannot populate it; SDK Discover remains no-execution; existing user/other-client behavior and recorded removal remain unchanged. The original client tests additionally cover dedicated modern files, migration/shadowing refusal, exact scope/ownership/data, rollback/recovery and prepared/no-attestation status. Independent final implementation review must trace these paths and observe the stated regressions. + +No blocking plan correction remains. Two independent plan GO reports must be committed before code. This review does not qualify the Devin binary, native discovery/trust/policy, authentication/model use, runtime or release, and grants no main/release merge authority. + +## Project-only absent original profile follow-up + +Independently traced the added section at `20dc9db1`. `Engine.Prepare` validates request/engine scope and exact selected project context before profile capture or `detectedClient`; `Engine.scope()` derives project scope only from the validated project engine configuration. `NewTargetContext` already permits an absent original user profile. It is therefore correct to stop the later generic SDK reconstruction from demanding a fabricated legacy profile when the declared project client has no such profile. Allowing empty `ClientConfigRoot` only for that already validated project path does not make it a native mutation root: the scoped adapter still supplies and validates its explicit selected project layout, artifact, receipts and exact original-context equality. + +User scope must keep the existing nonempty clean absolute profile requirement. Nonempty project user roots keep the same validation/resolution, and clients with a stricter existing resolver/admission retain it; no user root fallback, alias/profile authority token or new discovery execution is introduced. The planned absent-profile public SDK/CLI positive case, user empty-root refusal and invalid nonempty-root refusal directly cover this limited admission change. Existing exact scope mismatch/engine guards remain required. No further plan correction is needed. diff --git a/docs/plans/project-install-e2e/reviews/devin-version-plan-r2.md b/docs/plans/project-install-e2e/reviews/devin-version-plan-r2.md new file mode 100644 index 000000000..5f04ab914 --- /dev/null +++ b/docs/plans/project-install-e2e/reviews/devin-version-plan-r2.md @@ -0,0 +1,23 @@ +# Devin version evidence amendment — independent plan review R2 + +## Decision + +**GO** for the docs-only frozen amendment `20dc9db18c47c9ad6879d3a6c8cc0cc7067f5714` in `work/uap-project-windsurf`. This supersedes the initial review target `11c17c87c51a9e29a223446650ffaa9b0d62e3e3` and includes the project-only absent-user-profile admission clarification. Reviewer: codex_claude, 2026-10-23. No production code changed, tests compiled, or native process executed during this plan review. Implementation still requires two independent final code reviews. + +## Evidence and reasoning + +The proposed optional operational `installer.Request.ClientVersion` is a narrow repair to an observed composition gap. The existing detector pairs its normalized version with its selected primary executable, while both `runInstallerFacadeLocalAdd` and `dependencyInstaller.Prepare` currently forward the executable without the version. `Engine.detectedClient` reconstructs the selected client and currently drops that evidence. Copying the host-supplied string into `DetectedClient.Version` preserves the existing detector and SDK Discover no-execution contracts. `clonePrepareRequest` begins with a value copy, so a string remains frozen in the prepared request without another mutable evidence object. Keeping the field excluded from diagnostic JSON and absent from portable manifests avoids package-authored qualification. + +For the project lane, `backendExecutable` returns the nonempty selected executable before considering sibling fallbacks. Windsurf detection deliberately selects `windsurf` ahead of `devin` in coexistence environments. The amendment correctly preserves this legacy ordering and requires both the selected basename `devin`/`devin.exe` and exact normalized `3000.11.205` evidence. Presence of a secondary Devin surface, an editor installation, a version from another executable, a missing version, or a different/older version must not qualify the lane. The existing version normalizer extracts a version token; implementation tests must establish exact equality, including rejection of unrelated suffix/prerelease values rather than a loose numeric prefix or minimum-version comparison. This host assertion remains configuration admission, not proof of binary provenance or native runtime discovery. + +Saved install, update and repair reach the common `dependencyInstaller.Prepare` request builder, so forwarding fresh selected detection there covers all three. `compatibilityChecks` reconstructs observational clients, but `compatibilityBindingCheck` explicitly restores `target.Client` for the selected client; its prepared version therefore survives the selected update compatibility preview. No new version fields are needed in the existing Claude/Codex-only group target contract. The adapter gate must apply to new configuration preparation and activation, rather than broadening `ReserveIdentity`'s layout-only prospective identity calculation into another detector. No version probe, stale recorded version, retained package metadata, or manifest field may substitute for fresh request evidence on install/update/repair. + +Exact owned removal is deliberately version independent and launches no native cleanup. Configuration/transaction recovery is likewise driven by the existing observed scope, physical authority, journals and receipts; it must not become a request for a current Devin executable/version or implicitly re-run installation. A separately requested post-recovery sync obtains fresh selected host evidence as usual. Missing/changed harness evidence must therefore prevent new installation or repair while allowing safe exact removal/recovery of recorded authority. + +The additional absent-profile clause addresses a real composition mismatch: current-only Devin detection legitimately yields no legacy Cascade ConfigRoot, `NewTargetContext` already allows an absent original user root, and `detectedClient` presently requires one unconditionally. Allowing absence only within an already validated project engine/request is coherent. A nonempty root remains clean and absolute; user scope retains its required-root contract. Existing profile resolvers and client-specific stricter checks remain in force. Both-empty original user context is legitimate, while any nonempty context mismatch must fail. Project authority must derive from the explicit project root and its exact native layout/artifact/receipts, never a fabricated legacy profile or an empty-root fallback. + +## Required implementation verification + +The amendment's proposed composed SDK and both CLI-path tests are meaningful and sufficient in scope, subject to final code review. They must cover exact selected Devin evidence, missing/older/wrong-harness evidence before effects, current-only Devin with no legacy user profile, unchanged user empty-root refusal and malformed nonempty project profile refusal, frozen request evidence, non-injection from a source manifest, saved update/repair, and version-independent exact removal/recovery. Existing user detection order, other clients, SDK Discover no-execution, project ownership/data binding, local shadowing/migration refusal, lifecycle rollback, prepared/installed status and project attestation refusal must remain intact. + +No native discovery, effective plugin/MCP loading, account/trust/model success, all-client completion or release qualification is established by this GO. diff --git a/docs/plans/project-install-e2e/shared-restoration-code-reviews.md b/docs/plans/project-install-e2e/shared-restoration-code-reviews.md new file mode 100644 index 000000000..fd57e07db --- /dev/null +++ b/docs/plans/project-install-e2e/shared-restoration-code-reviews.md @@ -0,0 +1,33 @@ +# Fresh shared restoration code review closures + +Both independent GPT6.1 reviewers reviewed source after the recorded two pre-code +plan GOs. These approvals apply to clean136de85701c5f5554d382205744f6446cf24f8df +(final production8ebe5f4e/ec8acad6), not lost historical objects or future merges. + +## R1 — finish_sources_vscode_cline — FINAL GO + +Canonical seed helpers run before composition and preserve intentional alias +negatives and exact observations. Planner validates matching scope/canonical +context before intent or readiness; direct Kiro reuses exact authority and09cb +preserves Unsupported. R1's concrete nonproject concern was closed8ebe by returning +before the new SupportsScope check. Internal scope guidance preserves user/SDK/auth +semantics and confers no activation. Actual current prepare/status0.015/0.031 and +both pinned changed-code/full-file size/architecture0issues close the static hold. +Earlier full CLI73.279, planner/Kiro0.052/1.724 and actual Copilot status8.045 are +truthfully attributed to their own source checkpoints. No unresolved blocker. + +## R2 — finish_codex_claude_qoder — FINAL GO + +Reviewed full e370..136 delta and fresh evidence. New scope admission applies only +to explicit project candidates; genuine nonproject and direct PlanUnsupported +semantics remain. Scope json:"-" is seeded desired/retained and chooses project if +either app/binding is project, changing only prepared/manual guidance. Explicit +SDK/auth/user actions and readiness remain. Meaningful helper regressions cover +orphan mismatches/missing bindings/wire omission, and composed Copilot checks the +actual status without forbidden attestation. Supplied source-tagged fresh tests +and both lint0issues close the static condition. No unresolved blocker. + +Neither reviewer ran a duplicate compiler or native process. These scoped GOs do +not waive current platform CI, native proof, the unchanged contract dependency +closure after Codex extraction, source forward integration or all14 assembly gates. +Root alone publishes/merges only the project feature branches, never main/release. diff --git a/docs/plans/project-install-e2e/shared-restoration-implementation-evidence.md b/docs/plans/project-install-e2e/shared-restoration-implementation-evidence.md new file mode 100644 index 000000000..24abb410a --- /dev/null +++ b/docs/plans/project-install-e2e/shared-restoration-implementation-evidence.md @@ -0,0 +1,59 @@ +# Fresh shared project correction evidence + +The restored fixture, project preparation and prepared-status slices received two +fresh independent pre-code plan GOs recorded in shared-restoration-plan-reviews.md. +Historical lost commits and their runs are not proof for these reconstructed bytes. +Root alone publishes this internal branch; it has no additional client PR. + +## Scoped changes and review corrections + +The e7713527 fixture slice canonicalizes existing t.TempDir seed roots before +building children, expected receipts and composition. It preserves exact actual +paths and observations and intentional alias negatives. No production path is +normalized and no platform is skipped. Already canonical Kimi/Devin A/B helpers +are retained; Kimi's separate transfer seed uses the common canonical helper. + +e370bab7 validates explicit project context before applying prepare intent and +uses Kiro's existing exact project layout helper on direct refinement. The old +malformed relative Kiro root remains a negative; unsupported ChatGPT with a real +root and qualified Cursor with a missing root are separate cases. Self-review +09cb0766 preserved PlanUnsupported in direct Kiro refinement. R1's scope concern +was closed in 8ebe5f4e by admitting genuine nonproject contexts before the new +SupportsScope check; the preexisting intent trait checks remain unchanged. + +ec8acad6 carries nonserialized scope through desired and retained dependency +status facts, choosing project guidance if either app or binding says project. +Prepared/manual project facts ask for selected-project review rather than an +activation-complete attestation; user guidance, explicit SDK actions, auth actions +and provider readiness/ownership/state gates remain unchanged. The concrete +Copilot TLS Git lifecycle asserts actual deps status text and absence of the +invalid attestation action. 4a8bb63a corrected a test's supposed user context: +its ProjectRoot had been nonempty, correctly classifying it as project. Only that +fixture setup changed; its user-compatibility assertions were retained. + +## Fresh observed checks, with source attribution + +Verified workspace Go1.26.10, workspace caches and GOMAXPROCS2/-p2 were used. +No native vendor, model, auth, trust or MCP process executed. + +- At e370bab7, focused planner/preparation PASS0.027s, Kiro PASS2.123s and provider + project selection PASS1.326s; all public CLI Test.*Project PASS73.279s. This full + CLI result qualifies that source checkpoint, not later unexecuted edits. +- At 09cb0766, complete planner PASS0.052s and complete Kiro PASS1.724s. +- At ec8acad6, composed Copilot lifecycle with new real status assertions + PASS8.045s. The initial helper run failed on the erroneous user test context + described above; that failure is not counted as a pass. +- At final source 8ebe5f4e, current preparation and scoped-status regressions + PASS0.015s planner /0.031s CLI internal. A prior session response was lost at + context transition; its unobserved result is not counted and this rerun is. +- Pinned golangci-lint2.13.2 changed-code gate from pre-code checkpoint42621cf3 + PASS0issues over planner, Kiro, providers, public CLI and CLI internal packages. +- Exact whole-file revive/funlen/gocyclo/gocognit/dupl/depguard gate over the same + packages PASS0issues. No lint baseline, dependency snapshot, platform sweep or + architecture guard was widened. Formatting and authored diff checks PASS. + +Both reviewers' fresh static reviews found no remaining blocker at the final +source; two final evidence closures remain pending at this frozen checkpoint. +The source transport forward integration, unchanged contract-closure guard after +Codex parser extraction, genuine platform CI and all-client assembly qualification +remain separate gates. Fixture proof is not native client discovery or activation. diff --git a/docs/plans/project-install-e2e/shared-restoration-plan-reviews.md b/docs/plans/project-install-e2e/shared-restoration-plan-reviews.md new file mode 100644 index 000000000..8d9a01feb --- /dev/null +++ b/docs/plans/project-install-e2e/shared-restoration-plan-reviews.md @@ -0,0 +1,39 @@ +# Fresh independent pre-code plan approvals after runtime reset + +Both reviewers inspected docs-only frozen f9ce0258 (canonical fixtures and +prepared guidance, unchanged through d9bb8784) and d9bb8784 (planner amendment). +Historical lost-runtime approvals/passes are not proof of the restored source. +No component source or tests were edited before this approval record. + +## R1 — finish_sources_vscode_cline + +- Canonical fixture plan: GO. Resolve existing t.TempDir seeds before children, + app composition and expected receipts; fail resolution errors. Preserve + intentional aliases, exact actual/native/path/digest assertions and all + production behavior. Use one CLI helper and existing provider helper. +- Prepared guidance: GO. Propagate internal nonserialized scope through desired + and retained/orphan facts; classify project if either context is project. + Conservative review action replaces unsupported activation attestation only; + explicit SDK/auth actions and provider/readiness/state/wire gates remain. +- Planner amendment: GO. Preserve malformed relative-root Kiro negative as an + actual authority bug. Prepare-only context validation before mutation and + direct Kiro reuse of the exact project helper close it. Qualified positive, + unsupported ChatGPT and qualified Cursor missing-root cases stay separate. + +No blocking plan finding; no implementation edit, compiler or native execution. + +## R2 — finish_codex_claude_qoder + +- Canonical fixture plan: GO. Existing seed roots only, before expected children; + preserve deliberate aliases and all exact observations/assertions. No + production normalization, platform skip or native proof follows. +- Prepared guidance: GO. Internal scope before desired/orphan action derivation; + project if either app/binding says project; never automatically suggest + activation-complete there. Preserve SDK/auth actions and all gate behavior. +- Planner amendment: GO. Validate matching project NewTargetContext before + intent/readiness mutation for either project field/nonempty root; existing + scope traits and exact Kiro helper remain. Keep malformed negatives and real + canonical positive without deleting tests or silently changing goldens. + +No blocker or source/compiler/native execution by review. Fresh focused tests, +relevant lint and two final code reviews remain mandatory before qualification. diff --git a/docs/plans/project-install-e2e/skills-picker-complexity-plan.md b/docs/plans/project-install-e2e/skills-picker-complexity-plan.md new file mode 100644 index 000000000..c2d0805d2 --- /dev/null +++ b/docs/plans/project-install-e2e/skills-picker-complexity-plan.md @@ -0,0 +1,36 @@ +# Skills picker selection: preserve behavior while passing the size gate — R1 + +Hosted job 121008406005 at source428bdf5d reports cyclomatic complexity24 +for skillsAddOptions.run (limit20). Changed-lines correctness lint passes; +this separate full-file architecture gate must also pass without exclusions, +new budgets or assertion weakening. + +Extract only the existing browser-selection boolean into a small private method +on skillsAddOptions. Pass the already computed interactive flag, options, exact +path flag and whether the resolved source has an exact path. Preserve the exact +operator grouping: explicit UI OR interactive automatic selection with no noUI, +no dry run, no skill names, no explicit path, no exact source path and no target. +The method has no I/O and makes no decisions about scope or filesystem authority. + +Keep validation, app.forOptions, target parsing, project context admission, +context cancellation, website dispatch, source path validation and source +acquisition in the same order. Keep the existing browser fallback and terminal +flow unchanged. No user-visible text, flags, request schema, lifecycle or SDK +behavior changes. Existing actual project Skills CLI/picker regressions and the +narrow CLI correctness plus full-file size gate provide verification; do not add +an implementation-mirroring boolean table for this mechanical extraction. + +Two independent plan rounds precede this small production change, followed by +independent code reviews. No production changes for this slice have been made. + +## R2 — independent R1 findings incorporated + +APM and Skillshare independently gave R1 GO exactc7d13ed5 after reading the +actual skillsAddOptions.run. Both confirmed that extracting only the original +boolean preserves operator grouping and all prior admission/acquisition order. +The private helper receives already computed values; it performs no context +checks, source resolution or I/O. Keep explicit UI as the unchanged left-hand +OR branch. No browser/terminal fallback, format, option or scope changes, and +no linter exclusion/budget adjustment. The same existing actual scoped CLI and +picker tests plus the full-file size gate will verify the extraction. Final +independent R2 reviews precede the source edit. diff --git a/docs/plans/project-install-e2e/vscode-collector-code-reviews.md b/docs/plans/project-install-e2e/vscode-collector-code-reviews.md new file mode 100644 index 000000000..02dd26a30 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-collector-code-reviews.md @@ -0,0 +1,43 @@ +# VS Code publisher collector: independent code reviews + +2026-10-24. These are receipts of actual independent reviews. They approve only +the pinned help/version research collector; no workspace adapter, native project +discovery, MCP, extension-host, trust or model behavior is qualified. + +## Initial checkpoint and findings + +The initial local frozen source was +`619b07347aedd8707225027eef0c3b49d287e4c3`. Root initially gave GO after +independently tracing archive integrity before parsing, bounded fixed-header +extraction, omitted symlinks, descriptor containment, namespace/privilege proof, +finite commands, output/deadline bounds, anchored cleanup and the exact-head +workflow; 11 inert tests passed in 0.168s. That initial GO was superseded. + +Independent reviewer `apm_vercel_scope` gave **NO-GO**: failure of the raw +artifact cap still allowed the workflow's `always()` upload of that raw +directory. It also required rejecting GID 0 and checking/recording matching +real/effective original UID/GID. The reviewer independently passed 11 collector +and six capture tests; no vendor executable was acquired or launched. + +## Revised frozen checkpoint + +Local `d6014d88aabffe7589ede046fbcd006f8e620b65` fixes both findings. It stages +raw files through a bounded ZIP_STORED envelope in a separate upload directory; +invalid source trees yield only a bounded false-qualification failure ZIP, +retaining the original error. The workflow uploads only the exact independently +admitted ordinary, single-link, at-most-32MiB ZIP. Source member count, actual +bytes and stable ordinary identities are checked before publication. The parent +and same-process proof now reject root group or real/effective identity mismatch. + +Root independently reviewed that narrow delta: **GO**, dedicated 13 inert tests +passed in 1.136s. `apm_vercel_scope` independently reviewed the same delta: +**GO**, independently reran the 13 collector plus six capture tests, Python +compilation and diff checks. Its prior extraction/CRC/no-follow/anchored cleanup/ +real guard review remains valid. Both reviewers explicitly ran no vendor code. + +Only after both final GOs did root atomically publish the two-commit chain at +remote `4cba9df5cab26f12fafa4c0e68a119953485e652`, identical final tree +`13532aa65ea0c5af431331896d8437a82ae1e3d2`; the initial checkpoint maps to +remote `c4e4e348`. No intermediate unreviewed workflow was admitted. This record +permits the genuine hosted collection and subsequent interpretation; a further +reviewed workspace-evidence/adapter plan is still required. diff --git a/docs/plans/project-install-e2e/vscode-collector-implementation-evidence.md b/docs/plans/project-install-e2e/vscode-collector-implementation-evidence.md new file mode 100644 index 000000000..0900a0d65 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-collector-implementation-evidence.md @@ -0,0 +1,63 @@ +# VS Code publisher collector: fresh implementation checkpoint + +Implements the twice-reviewed restored collector plan in this branch. This is +help/version research only; no workspace adapter or production client behavior +is added. The existing Cline finite command/environment contracts are unchanged. + +The collector calls the real unchanged hosted guard before output, acquisition +or native execution. It streams the exact pinned publisher archive to disk, +verifies full size/SHA256 before parsing, reads fixed TAR headers before bounded +metadata, refuses sparse/hardlink/special/duplicate/case-colliding paths, omits +all symlinks and materializes ordinary files descriptor-relative with no-follow +and exclusive leaf creation. Gzip CRC/end and zero TAR trailing data must close +before either finite native command is scheduled. + +Only the shipped ELF plus shipped cli.js receives --version or --help. Each +process uses a fresh profile/cwd and the reviewed sudo/unshare/setpriv/env-i +chain. The same process verifies network namespace, original nonroot uid/gid, +empty groups, zero capabilities, no-new-privileges and loopback-only interfaces +before execve. File hashes/identities are checked before and after each probe. +Cleanup is a separate bounded inert Python child, anchored to the held task-root +identity and using no-follow descriptor-relative traversal. Failed cleanup keeps +collection incomplete. Flat research artifacts have an actual32MiB aggregate cap. + +The shared live-output helper and its six inert tests are byte-for-byte the +already reviewed versions from origin/feat/project-qoder-e2e. Its independent +stdout/stderr caps do not limit unrelated native cache files. No shared helper +semantics were changed here. + +## Actual local checks + +- Dedicated VS Code synthetic archive/inert child tests: 11 PASS, 0.203s. +- All affected *_native_research suites: 34 PASS, 1.319s. +- Reused shared live-capture suite: 6 PASS, 2.197s. +- Native safety suites including that capture suite: 63 PASS, 2.861s. +- Python syntax and git diff --check passed. + +No publisher/vendor/native executable, archive acquisition, trust/account/model, +extension host, MCP process or guard spoofing occurred. Tests used synthetic +archives and inert Python children. Two fresh independent implementation reviews +are required before publishing this source to trigger the hosted native workflow. +Successful help/version capture will still require independent interpretation +and a new reviewed workspace-evidence/adapter plan. It never sets +native_project_qualified true. + +## Independent review findings and narrow corrections + +Root's initial source review was GO for help/version only. Independent second +review correctly found that a failing raw-directory cap did not prevent the +workflow's always-upload from uploading that same raw directory. That initial +source is not admitted for publication/native execution. The revision now creates +one validated stored ZIP in a distinct upload directory. Source files are bounded, +regular, unaliased, flat-named and identity-checked while staging; invalid raw data +produces only a bounded failure-summary ZIP. The workflow uploads only that exact +ZIP after a separate regular-file/link-count/32MiB admission step, never raw logs +or a partially staged temporary. Extra bounds reserve ZIP headers and cap members. + +The second review also required original nonroot GID and real/effective UID/GID +matching. Both parent admission and the same-process verifier now enforce them; +recorded namespace facts include euid/egid and are checked again after the probe. +New inert regressions exercise overflow/nonregular raw output through the actual +published envelope, preserved original failure, and root-group/effective-ID refusal. +Dedicated revised suite13 PASS0.183s; affected/full shared checks are recorded at +the next source freeze. No publisher acquisition or native client execution occurred. diff --git a/docs/plans/project-install-e2e/vscode-display-descriptor-amendment.md b/docs/plans/project-install-e2e/vscode-display-descriptor-amendment.md new file mode 100644 index 000000000..bc89b536e --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-display-descriptor-amendment.md @@ -0,0 +1,37 @@ +# VS Code display assets: retain exact kernel identities until cleanup + +2026-10-25. Ubuntu 22.04 run `40187145475`, job `121150237501`, source +`1cb91d570cc94b5930d47aa0312a11f2bd1fdbad`, image `20261012.322.1`, failed the +inert replacement test before native execution. Immediate unlink/recreation can +recycle the inode and metadata time quantum; saved FileInfo alone is insufficient +for the intended lifetime identity contract. No native artifact or positive +workspace claim follows from that job. + +After the existing exact PID/UID/type/single-link/socket readiness checks, open +only the two already captured display assets with `O_PATH|O_NOFOLLOW|O_CLOEXEC`. +Compare each held descriptor's `fstat` to its captured FileInfo and the named +no-follow identity before accepting either. Keep both descriptors open through +strict display teardown and the existing authorized cleanup decision. An open +descriptor prevents its original inode being recycled into a replacement path. +If opening/validating the second asset fails, close the first; on every execute +exit, close all retained descriptors after cleanup. No descriptor is inherited +by publisher processes, and no new executable/command is added. + +Unlink still requires the existing strict tree-reaped success, captured exact +asset metadata, root-owned sticky parent and final named identity validation. +Readiness failure or unproved teardown grants no global unlink authority. Never +replace that gate with a successful close or a missing child handle. Add held +identity verification to the cleanup boundary: for a present named asset, a +missing/closed handle or changed held identity refuses unlink. Close every held +handle after each existing execute cleanup branch, joining close failures into +the original failure and explicit collection failure. No native argv, privilege, +trust, cgroup or policy change. + +Keep the original wrong-PID/owner/type/replacement assertions. Make the +replacement test retain the original lock descriptor through unlink/recreation, +matching production's actual lifetime contract, and assert the replacement has +a distinct kernel identity. Add focused inert lock/socket descriptor admission, +partial-open failure/close and no-follow/replacement refusal checks. Existing +socket tests and strict teardown gates remain intact. Run the dedicated Go +tests/vet, then obtain independent frozen code reviews before publication and +the next hosted observation. This amendment does not admit product code. diff --git a/docs/plans/project-install-e2e/vscode-display-readiness-extraction-plan.md b/docs/plans/project-install-e2e/vscode-display-readiness-extraction-plan.md new file mode 100644 index 000000000..aff4679cd --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-display-readiness-extraction-plan.md @@ -0,0 +1,41 @@ +# VS Code display readiness complexity correction + +Final assembled source `1a19373e024ba4c2ac0748fda1306ce0ffd95429` +failed the configured Required lint gate because `displayReady` has cognitive +complexity 28, above the limit of 25. This amendment is a behavior-preserving +extraction inside the existing reviewed collector. It adds no evidence or +native qualification. + +## Exact change + +Replace only the body of the already-entered `se == nil` branch in +`displayReady` with a call to `c.retainReadyDisplay(lock, lockInfo, info, proof)`. +Move that exact body into the existing `display_authority_linux.go` file: + +1. Call `validateDisplayReady` with the same lock bytes, captured lock/socket + metadata and isolation proof. +2. Repeat the same socket `Lstat` and `sameFile` comparison with the captured + socket identity; preserve the same refusal text. +3. Call `holdDisplayAssets` with the same two fixed paths and metadata. Preserve + its O_PATH/no-follow/CLOEXEC, held/named/pair verification and partial-close + behavior without modification. +4. Assign `c.displayAssets` only after that call succeeds, then return nil. + +Every existing branch and error return outside that branch remains unchanged. +The ten-second timer, 25ms ticker, context cancellation, missing-file retry, +probe order, syscall order and teardown remain unchanged. No runtime flags, +privilege, namespace, sandbox, qualification, test assertion or lint exclusion +changes. The helper is called only after the original lock/socket presence +conditions have succeeded. + +## Verification and review + +Run the complete affected package's existing inert readiness, descriptor, +replacement, teardown and cleanup suite in a private Unix-socket-capable +executor, plus vet. Run the configured full-package correctness, size and +architecture lint without a changed-line filter. A mirror test for a pure +extraction is unnecessary; preserve the existing meaningful tests verbatim. + +Two independent reviewers must approve this exact plan in both review rounds +before source editing, then review the frozen source diff. The final hosted +native metadata replay remains independently required at the next source. diff --git a/docs/plans/project-install-e2e/vscode-frontend-cleanup-amendment.md b/docs/plans/project-install-e2e/vscode-frontend-cleanup-amendment.md new file mode 100644 index 000000000..e900d7e7e --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-frontend-cleanup-amendment.md @@ -0,0 +1,38 @@ +# VS Code frontend cleanup: final ownership-only amendment + +2026-10-25. The genuine failed workspace run `40182639794` separately found +frontend cleanup could not unlink `mkall.sh` from its own Go module cache's +read-only directories. This amendment changes only final cleanup of the exact +frontend-owned input tree. It does not close the failed workspace observation +gate, change native arguments, enable a sandbox helper or authorize a retry. + +Add a workspace-only fixed `cleanup_frontend` command using the existing bounded +`run_probe`: `/usr/bin/python3 -I`, trusted cwd `/`, finite environment, live +output caps and five-second deadline. Keep the inherited help/version cleanup +implementation unchanged. Root path/expected device/inode/owner are captured by +the trusted frontend before acquisition, never read from a native child result. +Accept only the exact ordinary `/tmp/vscode-workspace-input-*` root. Open `/tmp` +and its root leaf with directory/no-follow descriptors; require the opened root +to match captured device/inode/original UID and the current nonroot cleanup UID. + +At final cleanup only, each opened ordinary same-owner/same-device directory +must match its no-follow named identity before `fchmod(fd,(mode&0777)|0700)`. +This strips privileged directory bits and restores owner permissions needed to +remove read-only Go cache entries. Never chmod leaves, symlinks or foreign-owned +directories. Leaf removal is no-follow; outside symlink targets retain modes and +bytes. Refuse foreign owners, different-device directory trees and changed +identities. Recheck each named child and root device/inode/owner immediately +before removal so replacement is not accepted. Report cleanup failure separately +and preserve the original collection/probe error and false qualification flags. + +Inert actual-child regressions run under an ordinary UID, using a fixed +`setpriv` drop into an exclusively created/chowned fixture only when the test +executor is root. Prove removal of a 0555 module directory and 0444 `mkall.sh`, +preservation of outside symlink target bytes/modes, and refusal of replaced roots +and foreign-owned directories. No vendor executable, hosted-guard spoof or +native sandbox change is involved. + +ROOT and CI/SDK independently gave R1 concept GO. This final revision incorporates +their trusted-frontend identity origin, opened-directory ownership/identity, +privileged-bit stripping and original-failure precedence clarifications. Final +R2 plan and frozen code reviews are recorded separately. diff --git a/docs/plans/project-install-e2e/vscode-project-adapter-plan.md b/docs/plans/project-install-e2e/vscode-project-adapter-plan.md new file mode 100644 index 000000000..00d4b541f --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-project-adapter-plan.md @@ -0,0 +1,163 @@ +# VS Code project component adapter: conditional implementation plan + +2026-10-25. Prepared against assembly `d94d6c1b` and the exact collector source +in PR #515 (`37fc30df`). This is a concrete design for review, not permission to +skip the existing observation gate. The owner project-only exception is recorded +in `docs/PROJECT_INSTALL_OWNER_SCOPE.md`. + +## Evidence gate and admitted claim + +The pinned help/version observation succeeded; the genuine workspace observation +run is [40182639794](https://github.com/777genius/universal-agent-plugins/actions/runs/40182639794). +Its job was still queued at this checkpoint. Product implementation starts only +after the real artifact establishes relevant selected-folder MCP and skill +metadata, an independent critic verifies that interpretation, and this concrete +plan receives the required independent reviews. A failed sandbox, unavailable +proposed API, or empty/blocked inventory keeps the corresponding gate open. + +Verify the downloaded outer artifact digest and inner envelope, exact source SHA, +publisher commit/version, nonroot and namespace proof, strict process-tree cleanup, +all four selected single-folder observations, ungranted trust, original HOME, +source hashes and metadata attribution. Preserve every native row and duplicate. +Require A/B separation, A restart, and removal of A markers while user/B files +remain unchanged. Skill attribution must use its emitted source URI; MCP attribution +is the stated inference from unique sealed marker URLs, not an emitted config path. +Keep `native_project_qualified=false`: inventory does not prove trust, MCP startup, +model/skill execution, active-session isolation or effective Local/Copilot selection. + +## Small dedicated backend + +Add `vscode.NewProject()` returning a distinct project adapter type without +embedding `Adapter` or `Local` and promoting their methods; preserve `New()` +and `NewLocal()` source and user/Local/TEST behavior. Reuse existing detection, +but implement every project lifecycle/identity/layout capability explicitly so +the Copilot CLI backend cannot be reached by project calls. Reject user/local, +remote and multi-root contexts at the project adapter boundary. Target authority +is the canonical existing project root plus the original detected user profile. +Never substitute HOME, a profile directory or a package path for scope. + +Add explicit `clients/all.Project()` registry composition and select it in +`cli/cmd/agentplugins/compose.go` after canonical scope resolution, before detection, +planning or acquisition. Default composition and authoring/user routes retain +`vscode.New()`. SDK callers choose the explicit project registry/constructor; +missing project composition fails with actionable admission rather than falling +back to the Copilot adapter. Keep generic packages free of concrete client imports. +`NewProject` detection reports the actual VS Code executable and original VS Code +profile; the Copilot executable/profile never supplies presence or registry +authority. The project constructor explicitly rejects non-project operations, +and the retained user constructor rejects project requests in its plan preflight, +so a global capability declaration cannot admit a Copilot fallback. Put scope +selection in composition, not new concrete-client switches in generic core code. +The retained user adapter must not implement `ProjectTargetLayout`, keeping +`clients.SupportsScope(all.Default(), vscode, project)==false` even after the +domain capability is declared. The project adapter implements +`UsesNativeRegistryExecutable()==false` and exact native-file registry inspection. +Wire the same selected project registry through detection, planning, staging, +SDK facade, activation, identity observation and maintenance; scope switching +rebuilds composition before any source acquisition. + +After evidence admission, declare VS Code `LifecycleNativeConfig` project policy +in the existing client table. Project backend family is `vscode`, distinct from +Copilot. Select only skills and MCP: hooks, commands, apps and editor-plugin +registration remain unsupported. An unsupported explicit selection must fail +before source acquisition, intent, locks or mutable project state. + +## Native layout and ownership + +Use `/.github/skills/` and `/.vscode/mcp.json`, with the +editor's `servers` member collection. Do not create portable `.mcp.json`, +`chat.pluginLocations`, a marketplace, user configuration or profile settings. +Record native registry root `/.vscode`; managed packages and plugin data +remain under the existing selected `.uap` namespace. + +Add a narrow `nativeconfig.CodecVSCode` with `servers`, explicit `type:stdio` +and portable `streamable-http` mapped to native `type:http`, using the pinned +editor schema's +command/args/env/cwd or url/headers subset. Preserve all unrelated top-level +members, inputs, sandbox fields, comments, other entries and enablement. Own only +the complete exact managed member; unknown/changed fields inside it invalidate +the receipt and refuse update/removal. No shared neutral Server shape expansion +is needed: portable environment values remain strings, while foreign native +numeric values are preserved untouched. Unsupported dialect options fail rather +than being discarded. JSONC syntax is accepted in the actual `mcp.json` file; +there is no invented sibling `mcp.jsonc` selection. +The immutable [mcpConfiguration.ts schema](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts), +reread directly on 2026-10-25, defines remote `type` and `transport` enums as +`http`/`sse` and allows comments/trailing commas in the `servers` document. This +supports the narrow portable SSE to native `type:sse` mapping; it is source +schema evidence, not proof of an SSE connection or a native observed SSE row. +Reject every other transport before acquisition rather than guessing a mapping. + +Reuse `shared.ProjectComponents`, `ProjectComponentMutation`, nativeconfig +conditional writes/locks and existing scoped journal/recovery. Use distinct +`vscode_project_skill_directory` and `vscode_project_mcp_server` receipt kinds, +exact stable digests, portable source-relative skill paths and the selected +package/data roots. Reuse the existing managed stdio launcher rather than adding +a second shell/placeholder resolver. Reject copied receipts, foreign same-name +targets, aliases/symlinks, changed package bytes and replaced project roots. +Do not adopt another client's same-name skill merely because its bytes match. + +## Result and maintenance behavior + +Automatically apply exact file preparation and verify it read-only. Report +`Prepared + Installed` with `ActivationAttested=false` and +`NativeDiscoveryReconciled=false`, even when research inventory succeeded. +Installed means owned configuration, never a running MCP server. Supply the +selected project path and action to open it in VS Code, review workspace/server +trust and inspect skills/MCP. Never grant trust, start MCP, accept operator +attestation as native observation, or run Copilot/code add-MCP for this lifecycle. + +Replay preserves exact bytes and reports no mutation. Update/repair/removal use +the original scoped binding and exact local receipts. Missing owned files may +be repaired; changed or foreign files refuse. Removal deletes only exact owned +skill directories and MCP members, preserving user/B artifacts and plugin data. +Retain the common transaction's interrupted-attempt fencing and recover/refuse +behavior; introduce no alternate recovery engine. + +## Necessary verification and publication + +Use meaningful codec preservation/collision fixtures and the existing public +SDK/CLI project lifecycle harness: user/A/B, unchanged replay, version update, +missing-file repair, removal/retained data, same-name foreign and cross-client +skills, scoped locks/receipts, aliases and replaced root admission. Assert zero +Copilot/vendor executions and unchanged user backend. Reuse the existing process +death recovery fixture for the skill-swap/MCP-commit seam rather than duplicating +its process engine. Cover mismatched scope/registry rejection before acquisition. + +Run affected Go tests/vet/lint, generated capability checks, necessary terminal +and public CLI contracts, and the existing three-platform project CI. Review the +frozen implementation independently, resolve findings, then integrate into #499 +with original traceability. Update the capability matrix and five-language +project guidance to describe preparation and actual observed limits. Do not +rename historical release evidence or claim a new release. + +## R1 independent review resolution + +ROOT reviewed `c230d66` and gave conditional architecture GO, requiring concrete +SDK project composition, actual VS Code host authority, explicit retained-user +project refusal, and proven transport spelling. This revision specifies those +boundaries, streamable-http to http mapping, and conditional SSE support. Native +observation and independent final plan review remain required before code. + +The independent Claude/Codex critic gave R1 architecture GO on `c230d66` after +checking existing user/Local adapters and shared ownership. This revision also +closes its distinct-type, executable-free registry inspection, default registry +scope refusal, and consistently selected composition clarifications. Evidence +admission is component-specific: observing skills does not qualify the MCP +dialect, and observing MCP does not qualify a skill path. Either missing native +contract remains an outstanding component gate. + +## Final R2 review receipt + +Both independent critics gave conditional final R2 PLAN GO on immutable +`d80c8bbe8fc9a6cb869e5ea0b86fab82549ee3d2`: + +- ROOT verified explicit project composition/default refusal, exact native + transport mapping and ownership scope without claiming runtime qualification. +- Claude/Codex independently reread the immutable publisher schema through the + GitHub connector and verified the distinct type, consistent registry wiring, + `servers`/JSONC shape and `http`/`sse` mappings against source. + +These source-only approvals satisfy the concrete design review gate. They do +not satisfy or remove the genuine workspace artifact and independent component +interpretation gate, which remained queued at this review checkpoint. diff --git a/docs/plans/project-install-e2e/vscode-restored-collector-plan.md b/docs/plans/project-install-e2e/vscode-restored-collector-plan.md new file mode 100644 index 000000000..e38988096 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-restored-collector-plan.md @@ -0,0 +1,139 @@ +# VS Code pinned hosted research: fresh reconstruction plan + +2026-10-24. Runtime reset erased the unpublished e61a84db collector and its +local-only Git objects. The historical source/checks/reviews cannot certify +new bytes. This is a fresh, narrower collector design: obtain two independent +plan GOs before code, then fresh inert checks and two implementation GOs before +publishing the feature checkpoint. Keep the owner project-only VS Code exception +and every unrelated frozen user/Local/TEST/native-hook implementation intact. +No project adapter is included; actual successful hosted evidence and independent +interpretation remain prerequisites for that next component. + +## Re-established primary facts + +The official update API was reread after reset at +https://update.code.visualstudio.com/api/update/linux-x64/stable/latest . It +still reports productVersion/name1.143.1, commit +e646868227c3e3a9e3f24277c8c7220be42c0be7 and publisher SHA256 +48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be. +The exact immutable publisher URL is +https://vscode.download.prss.microsoft.com/dbazure/download/stable/e646868227c3e3a9e3f24277c8c7220be42c0be7/code-stable-x64-1792691793.tar.gz . +HEAD reports350844741 bytes. An actual HTTP206 bytes0-65535/350844741 prefix +decompressed only the first512-byte TAR header and observed VSCode-linux-x64/. +Retain these bounded raw metadata receipts. They are partial research, not a +full archive integrity or execution result. The collector requires the complete +exact byte count and publisher SHA256 before opening gzip/TAR or native code. + +## Admission, workflow and finite commands + +Add only a standalone Python collector, its inert tests and an ordinary +pull_request/workflow_dispatch workflow. The real unchanged imported matrix +require_hosted('linux-amd64') executes before output/temp directories, acquisition, +summary or subprocesses. Local guard-refusal tests call that real guard without +forged hosted flags or patching. No local VS Code binary is launched. + +Use Ubuntu24.04, pinned checkout/upload actions, contents:read, +persist-credentials:false, exact PR-head-or-dispatch checkout and identical +EXPECTED_COMMIT, a ten-minute job and an independent eight-minute collector +budget. Watch only this collector/test/workflow and the unchanged matrix guard +and its native_client_draft import. No privileged event, push, schedule, token, +credential, trust, auth, model, extension, MCP or UI command is added. Enforce +an actual32MiB total regular-file artifact bound before upload; retain bounded +failure outputs only after real admission. + +After successful archive admission and extraction, invoke exactly two direct +publisher ELF commands: shipped code plus shipped resources/app/out/cli.js, +then --version or --help, with ELECTRON_RUN_AS_NODE=1. Do not execute bin/code, +a wrapper, installer, updater, extension host or default editor/session. The +three required code/CLI/product files must be ordinary files at their exact +observed relative paths and remain hashed/identity-checked before and after +each probe. Version output must contain exact product1.143.1 and exact commit; +help must be nonempty, exit0 and not overflow/deadline. Any missing asset or +failed probe fails collection; there is no alternate execution fallback. + +## Simplified archive boundary: materialize no links + +Stream the fixed HTTPS URL with a90s/remaining-budget timeout, actual compressed +byte count ceiling350844741+1 and exact digest before parsing. Disable proxy, +cookie and authentication inheritance; redirects may stay only on the exact +HTTPS publisher host, with a finite redirect count. Allocate only a fresh +task-owned disposable root with at least4GiB free. Do not load the archive into +memory or spool an expanded TAR. Stream gzip/TAR under a2GiB actual expanded +read bound, at most100000 members,512-byte metadata names/targets, depth32, +ordinary-file size512MiB and aggregate ordinary bytes2GiB. All loops check the +independent budget. + +Permit canonical directory and ordinary regular entries rooted exactly at the +observed VSCode-linux-x64. Reject absolute/backslash/colon/control/dot/parent/ +empty segments, duplicate or case-folding member names, file/parent conflicts, +hard links, devices/FIFOs/sockets and sparse records. Bound PAX/GNU long metadata +before stdlib allocation, and reject GNU sparse processing hooks before any +payload-map allocation. Require full gzip CRC/end-of-stream, valid TAR ending +and no nonzero trailing expanded data before native execution. + +For this narrower help/version collector, **omit all symbolic links rather than +rebuilding a link graph**. Their member names still participate in duplicate, +path/type and byte/name limits; record only bounded count/hash metadata of the +omitted links. Do not materialize or follow any archived link target. Regular +files below a link-shaped path may only create ordinary contained directories; +the collector is an explicitly regular-file-only materialization, not a claim +that the complete vendor filesystem topology was reproduced. If omitted links +are needed by the finite commands, fail honestly and review a separate design; +never silently add a fallback or relax path admission. + +Create directories and files through held root/parent directory descriptors, +O_NOFOLLOW/O_EXCL and exact leaf names, with ordinary files only. Never use +TarFile.extract/extractall or archived paths as arbitrary output roots. Validate +all admitted files and stream boundaries before scheduling either native child. +No untrusted symlink is written, so no alias graph can redirect a later write. + +## Actual child namespace and live process bounds + +Use a fixed sudo/unshare/setpriv/env-i chain only on the genuine disposable +hosted worker. Before dropping privileges, create a separate network namespace; +then drop to the original nonroot UID/GID and clear supplemental groups. A +literal inline verifier in the same process that will execve the shipped ELF +checks a different current network-namespace inode, nonroot expected UID/GID, +empty groups and socket.if_nameindex()==loopback-only. Record those bounded +facts then execve, preserving the same process and namespace. Do not rely on +inherited /sys interface listings. No namespace failure fallback/native launch. + +Each probe gets fresh HOME/XDG/temp/user-data/extensions/cwd and an explicit +finite environment, no ambient account/token/proxy/IPC/product configuration. +Use DEVNULL stdin, no shell, start_new_session and live parent-owned separate +nonblocking stdout/stderr pipes capped1MiB each. Each read is at most64KiB and +remaining allowance+1; exact-cap EOF is admissible, an extra byte fails and +kills the original group. Retain RLIMIT_CORE0; do not impose RLIMIT_FSIZE on +native cache writes. A20s/remaining-budget deadline includes inherited-pipe +draining; leader exit, errors, overflow and interruption kill the original +group, bounded-reap the leader and close all descriptors. This is bounded group +cleanup, not a general sandbox for descendants deliberately escaping a group. + +Do not use an automatic recursive TemporaryDirectory finalizer. Cleanup may +touch only the held identity of the newly allocated task root and must run +descriptor-relative/no-follow in a fixed inert remover with a total5s process +deadline and group cleanup. Refuse a replaced root; retain explicit incomplete +cleanup metadata and the original collection error if cleanup fails. + +## Evidence, checks and next gate + +Record exact source commit, official archive URL/size/digest, required file +hashes, finite argv/cwd/environment and current namespace/drop facts, exit/error, +elapsed and separate bounded stream bytes/hashes. Summary always keeps +native_project_qualified:false and interpretation pending, even on success. +No help/version observation proves workspace skill/MCP loading, trust/restart, +extension-host/UI, attestation, all-client completion or release. + +Fresh inert tests use synthetic bounded archives and Python children only. +Cover real zero-effect guard refusal, hash-before-parser/probe ordering, +compressed/expanded/member/path/type/metadata/sparse/CRC/trailing bounds, +descriptor-relative writes, omitted-link safety and required-file tamper +refusal; exact finite argv/environment, same-process namespace verifier shape, +exact-cap/overflow/DEVNULL/deadline/descendant behavior; root replacement, +bounded cleanup and preservation of original failure; exact workflow refs, +events, pins and artifact gate. Do not patch native admission or spoof a hosted +run in these tests. Syntax, workflow parsing, authored whitespace and relevant +existing native-safety tests must pass. Publish small reviewed checkpoints +immediately with exact expected-head/no-force, then obtain the genuine hosted +artifact, verify its download digest and independently interpret it before any +project adapter code. No previous lost source test result is reused as proof. diff --git a/docs/plans/project-install-e2e/vscode-restored-plan-r1.md b/docs/plans/project-install-e2e/vscode-restored-plan-r1.md new file mode 100644 index 000000000..0745b8929 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-restored-plan-r1.md @@ -0,0 +1,20 @@ +# Fresh VS Code restored collector plan review R1 + +Independent reviewer: finish_codex_claude_qoder. **GO** at +a7e4de5be22f2bb7f8744fe2d09583440e4c1a12 after complete plan review. + +Regular-file-only materialization with no symlinks is a coherent narrower +collector. Omitted names still participate in duplicate/type/byte bounds; no +target is written or followed, and a missing required link-dependent asset +fails without fallback. Full exact archive count and publisher hash precede +parsing or execution. Expanded/member/metadata/sparse/CRC/trailing bounds, +anchored no-follow/O_EXCL writes, held required-file identity/hash and exactly +two direct ELF+cli.js commands with fresh per-probe environments are retained. + +Actual same-process netns/UID/groups/if_nameindex verification before exec, +parent live caps/core-only rlimit/deadline through draining, bounded +identity-aware cleanup, real guard before effects, 32MiB artifact admission +and false qualification are coherent. Implementation review must substantiate +preallocation metadata/sparse and boundary ordering with fresh inert tests. +No blocking finding; no source edit, Go or native process in this review. +No prior lost execution, collected binary or project proof is inferred. diff --git a/docs/plans/project-install-e2e/vscode-restored-plan-r2.md b/docs/plans/project-install-e2e/vscode-restored-plan-r2.md new file mode 100644 index 000000000..5e6d94b58 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-restored-plan-r2.md @@ -0,0 +1,18 @@ +# Fresh VS Code restored collector plan review R2 + +Independent reviewer: finish_grok_integration. **GO** at +a7e4de5be22f2bb7f8744fe2d09583440e4c1a12. + +Exact official size/hash before parsing, bounded gzip/TAR metadata and sparse/ +CRC/trailing gates, descriptor writes and zero materialized links make the +narrow design safe. Omitted names are still counted and hashed; ordinary +directories never follow link targets. Required assets must work without a +fallback. The genuine host guard precedes effects. Same-process actual netns, +UID/groups/loopback verification precedes exec. Fresh profiles and finite +ELF+cli.js version/help only, live pipe caps without FSIZE, process deadlines, +bounded descriptor cleanup, 32MiB artifact gate and raw false qualification +preserve the authority boundary. + +No project/native runtime/trust result follows from help or historical lost +passes. Implementation must keep all declared preallocation and cleanup bounds +actual. No blocking finding, source edits, native execution or compiler run. diff --git a/docs/plans/project-install-e2e/vscode-workspace-collector-implementation.md b/docs/plans/project-install-e2e/vscode-workspace-collector-implementation.md new file mode 100644 index 000000000..8962a63a6 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-workspace-collector-implementation.md @@ -0,0 +1,116 @@ +# VS Code workspace collector recovery candidate + +This is a new reconstruction after the executor filesystem reset. The lost +b5086b2c8a0192e4a48c561157f4afc1061bfa95 tree is not claimed as reproduced. +Its two historical source reviews remain useful defect reports, not approvals of +this new source. The published workspace observation plan remains the contract; +two fresh exact code approvals must precede any native workflow execution. + +## Recovered and reconstructed parts + +The frontend, observer runner, bounded filesystem/JSON helpers, merged capture +and privileged controller were restored from retained full output and correction +scripts. The missing model and Node shape/package originals were reconstructed +against the already reviewed plan and pinned primary-source notes. Formatting, +comments and source hashes may differ. Observer pins are regenerated from these +new exact four authored files before review. No old pin is treated as identity +proof for reconstructed bytes. + +The narrow project-only owner exception still applies. This directory is a +research executable only. The VS Code user/Copilot adapter and historical NewLocal +route remain untouched; no project adapter is introduced by this candidate. +All project/runtime/MCP-initialization qualification flags remain false. + +## Required invariant checks before native execution + +- Exact official1.143.1 archive/body identities, original nonroot IDs and real + disposable hosted admission. No guard spoofing or local vendor execution. +- Root-owned immutable executable/source closure, exactly two executable + publisher ELF paths, literal setup commands, isolated Python startup/cwd, + zero capabilities/NNP and a separate loopback-only network namespace. +- Existing strict RunDuplexWithPlannedShutdown primitive, Xvfb outer lifetime, + ordered four inner GUI exchanges, fixed deadlines and independent cleanup. +- Trust enabled and ungranted; MCP autostart never; no trust/sandbox fallback, + extra executable helper, MCP start/gateway, model request or configuration edit. +- Actual activation context mode, strict raw metadata shapes, preserved native + order/duplicates, watched-URI duplicate rejection, honest marker attribution. +- Sealed watched source ancestors checked around snapshots, atomic exclusive + pending/link/unlink/ready result publication, bounded merged diagnostics and + failure/join handling. +- Preloaded forked artifact admission, denied late imports, closed flat artifact + names, stable ordinary single-link files and independent bounded ZIP upload. + +## Fresh review corrections and current verification + +The reconstructed candidate does not inherit the historical b508 code approvals. +Root's preliminary read found and resolved three concrete schema/boundary defects: +zero-byte evidence names now use map presence for exclusive admission; source +snapshot capacity admits256 entries and refuses257; a success envelope rejects +any stage_error field, including an empty one. Finite inert regressions cover each. +Digest/removal diagnostics also preserve real error causes without formatting a +nil error as a wrapped error. + +A timed-out or unreaped fork cannot leave a canonical later-uploadable success +ZIP. The already loaded publisher's normal bounded false-evidence return has the +closed exit10; unexpected exception/deadline/unproved cleanup takes the exact +original output directory into quarantine via held-parent rename. The canonical +output parent is never recreated. An inert deliberately late child proves it +cannot publish into the old absolute path after quarantine; another case covers +an exception after a ZIP exists, and changed authority refuses quarantine. The +existing four-second work plus one-second reap budget remains unchanged. + +Local verification of the reconstructed 9e17 candidate before the final API correction: + +- Dedicated inert Go package PASS1.073s; go vet exit0, Go1.26.10, -p=1, + GOMAXPROCS2 and512MiB heap target. +- Configured golangci-lint2.13.2 dedicated changed-package run:0issues, + --new-from-rev=46b4678ea66d7804311f98270b70ebd3cceb928a, concurrency1, + 90-second deadline. No exemption or budget change. +- Python33 inert contracts PASS3.322s (14workspace,13existing help/extractor, + six neutral capture). Seven Node metadata/context/file-protocol tests PASS616ms. +- Formatting and diff whitespace checks passed. + +The first recovered Python run caught a transcription error in the test's +expected archive hash. Only that expected literal was corrected; the actual +published publisher pin was unchanged. An initial import-group lint check found +five reconstructed import blocks; their grouping was corrected and the final +configured package run passed. These were implementation checks, not vendor runs. + +There has been no local vendor, GUI, sudo helper, real cgroup child, MCP process, +hosted happy-path or workspace discovery observation. No full-module lint or +Windows/macOS execution is claimed. Old test results and old code approvals are +not represented as qualification of this candidate. Root and Claude must review +this new frozen source independently before its native workflow may run. + + +## Final fresh-review MCP API correction + +The complete fresh independent review of 9e17 found a real mismatch with the +pinned API declarations: McpStdioServerDefinition.env admits string, finite +number and null values. The reconstructed projection admitted string/null only. +The corrected Node projection preserves finite numbers and null; the Go wire +admission preserves json.Number and rejects nonfinite/out-of-range values. +HTTP header values remain strings. Optional container defaults now apply only +when a field is undefined, so explicit null cannot become an empty container. +HTTP and stdio fields in the same row refuse instead of silently dropping one +transport. The exact authored shape hash is regenerated in the sealed-source +pin table. No native argv, isolation, trust, lifetime, artifact or attribution +contract changes. + +Primary declaration: +https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vscode-dts/vscode.d.ts#L20427 + +The previous fresh source GO was withdrawn when this finding arrived. This +child needs both independent critics to review the corrected exact source +before runnable publication. Node metadata tests now cover finite env values, +null preservation, invalid env/header values, absent versus null containers, +and mixed transports. Go covers the actual bounded JSON wire admission and +malformed/nonfinite/transport negatives. + +Corrected-child verification: dedicated Go package PASS1.117s; go vet exit0; +configured dedicated changed-package golangci-lint2.13.2 completed0issues in +9.77s with the unchanged90-second bound/concurrency1/512MiB target; all ten +Node contracts PASS369ms. All four authored observer pins match their exact +files. No Python or process/lifetime source changed in this child, so the prior +Python33 result remains the last execution of those unchanged files. No native +observation is claimed here. diff --git a/docs/plans/project-install-e2e/vscode-workspace-host-plan.md b/docs/plans/project-install-e2e/vscode-workspace-host-plan.md new file mode 100644 index 000000000..ed7472783 --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-workspace-host-plan.md @@ -0,0 +1,85 @@ +# VS Code workspace research: compatible hosted sandbox environment + +2026-10-25. This amendment changes the standard hosted image for the existing +workspace observation workflow. It does not change the publisher, native argv, +process primitive, privilege policy, executable modes, trust or observation gates. + +## Observed blocker and source-supported hypothesis + +The genuine Ubuntu 24.04 run +[40182639794](https://github.com/777genius/universal-agent-plugins/actions/runs/40182639794) +stopped before producing workspace metadata. Its bounded artifact +`12596073531` is 5,185 bytes with SHA-256 +`21bad1e1d409fe33476cc2b9ec96d64aaafc38c0609d7262aec8617eb897d977`. +The native fatal diagnostic requests a root-owned mode-4755 `chrome-sandbox`. +The recorded same-process prelude proves original UID/GID 1001, no groups, zero +capabilities, NNP and a separate loopback-only network namespace. Observation is +null and cleanup is not proved; the failed stage remains failed. + +The exact VS Code publisher source +[`e6468682` .npmrc](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/.npmrc) +selects Electron 43.7.7. That release resolves to +[`ddb762f5` DEPS](https://github.com/electron/electron/blob/ddb762f50fbd70384d0385302697e60ada549024/DEPS), +which selects Chromium 150.0.7871.250, resolved to `f916f1b7`. +Its [zygote selection](https://github.com/chromium/chromium/blob/f916f1b7136907af19d96b95b891533d091d932a/content/browser/zygote_host/zygote_host_impl_linux.cc) +prefers the native user-namespace sandbox when +`CanCreateProcessInNewUserNS()` succeeds. Only the fallback selects the setuid +helper. The helper's +[PrependWrapper check](https://github.com/chromium/chromium/blob/f916f1b7136907af19d96b95b891533d091d932a/sandbox/linux/suid/client/setuid_sandbox_host.cc) +contains the observed fatal and requires new privileges when launching it. +It is incompatible with this collector's NNP/ordinary-executable contract. + +Ubuntu's [24.04 release notes](https://documentation.ubuntu.com/release-notes/24.04/) +and [security feature table](https://documentation.ubuntu.com/security/security-features/security-features-tables/) +document default AppArmor restrictions on unprivileged user namespaces in 24.04 +and their absence in 22.04. This supports a host-compatibility hypothesis; the +artifact does not itself identify the rejected syscall or AppArmor rule. +Do not claim AppArmor was proved to be the cause or that another host will pass. + +## Exact change and preserved admission + +After two independent plan-review rounds, change only `runs-on` in +`.github/workflows/vscode-workspace-research.yml` from `ubuntu-24.04` to the +explicit standard `ubuntu-22.04` hosted image. Keep the exact client archive, +Go version, workflow permissions, inert contracts, deadlines, upload admission +and every existing runtime guard. Record the actual runner image/version from +the eventual job; a label alone is not an execution result. + +The inspected official +[image inventory at `2deeef00`](https://github.com/actions/runner-images/blob/2deeef005a7e79479c137ef48d6f07f2cca37880/images/ubuntu/Ubuntu2204-Readme.md) +lists image 20261023.322.3, kernel 6.8.0-1064-azure, GTK and Xvfb. The workflow +still requires actual installed root-owned Xvfb/xauth, the exact Python/Go +contracts and successful strict cgroup/CLONE_INTO_CGROUP admission. A missing +dependency or unsupported kernel/host condition remains a failed observation. +The image's announced deprecation makes this a bounded research host choice, +not a general support promise or a product platform change. + +Do not install packages, alter kernel/AppArmor/sysctl policy, enable setuid, +delete the publisher helper, pass sandbox-disabling flags, spoof hosted guards +or fall back to a different archive/process runner. The standard Electron +sandbox must initialize under the unchanged native invocation. Do not weaken +the existing untrusted-workspace, original-HOME or no-MCP-startup conditions. + +## Verification and outcome + +Review the one-line workflow delta and retained guard/argv source. No new test +that merely mirrors the runner label is needed. Run the meaningful existing +collector/observer inert contracts on the assembled source, then obtain one +genuine hosted artifact with exact source/client/image identities. Independently +interpret the complete A/B, restart, removal and cleanup evidence before +allowing the separately reviewed project-adapter implementation to proceed. +Neither this amendment nor a host launch alone qualifies project support. + +The independent correction for cleanup of read-only Go module-cache directories +is a separate slice. Integrate both reviewed fixes for the next observation; +preserve this first failure and its raw evidence unchanged. + +## Independent review record + +Both `popular_clients` and `ci_sdk` independently read the exact `8ae4b9bb` +plan and primary-source support. R1: GO, with no unresolved source-plan finding. +Both then gave explicit final R2 GO on the unchanged plan, retaining the +compatibility-hypothesis wording and all strict admission and observation gates. +The actual image version will be verified in the exact job's official GitHub +setup log and linked to that head and bounded artifact. No new diagnostic +command or inherited environment authority is introduced. diff --git a/docs/plans/project-install-e2e/vscode-workspace-observation-plan.md b/docs/plans/project-install-e2e/vscode-workspace-observation-plan.md new file mode 100644 index 000000000..3c131e07c --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-workspace-observation-plan.md @@ -0,0 +1,409 @@ +# VS Code 1.143.1: shipped-workbench workspace observation + +2026-10-24. Plan only. The genuine help/version artifact at source `0a34182a` +is [preserved with provenance](../../evidence/project-scope-research/vscode-1.143.1-linux-amd64-40132182435/README.md). +Root and the implementing worker independently verified the outer/inner ZIPs, +all seven text members/six process streams, exact version/publisher commit and +namespace/privilege proof. Both admit only the next observation plan. Obtain +two independent plan-review rounds, then two frozen code reviews before any new +native publication. No VS Code adapter is included here. + +## Required gate and observation meaning + +The original all-client plan, section 14, requires genuine workspace evidence; +help/version does **not** close that gate. Actual help labels --add-mcp as a user +profile writer and supplies no read-only workspace inventory command. Do not use +that writer, a Copilot CLI listing, the old NewLocal TEST tuples or a synthetic +extension-host shim as a substitute for the shipped workbench. + +This next collector opens only inert task-owned single-folder workspaces in the +actual pinned shipped editor, with one reviewed test extension. It observes the +editor's configured MCP definitions and parsed skill metadata without a chat +request or server start. It does not establish effective Local/Copilot session +selection, trust acceptance, MCP initialization, skill execution or model use. +Workspace trust remains enabled/ungranted; require the expected selected local +folder and actual isTrusted=false. Pinned workspaceTrust.ts was checked: disabled +trust would report true; no extension-development exception was found in its +calculation. An unexpected true value fails this untrusted-stage contract rather +than becoming trust evidence. A trust or policy block remains explicit negative +evidence rather than a reason to bypass it. +Even a successful inventory leaves native_project_qualified false. After actual +artifact interpretation, a separately reviewed concrete adapter plan is still +required. The original stronger after-trust/runtime proof remains unqualified. + +## Exact pinned source and limited observer APIs + +Pin the same publisher version 1.143.1 / commit +`e646868227c3e3a9e3f24277c8c7220be42c0be7`, archive length/digest and required +ELF/CLI/product hashes. Reuse the previously reviewed acquisition, fixed-header +streaming extractor, CRC/TAR closure, descriptor containment, required-file +checks, budget, held-root cleanup and bounded upload admission. No npm, VSIX, +marketplace, new package dependency or publisher update is fetched or run. + +The immutable primary source exposes two **proposed** read-only APIs suitable +only for this pinned research extension: + +- [mcpServerDefinitions.d.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts): `vscode.lm.mcpServerDefinitions`, synchronized from all editor collections by [mainThreadMcp.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/api/browser/mainThreadMcp.ts). Subscription/publication reads definitions; launching/gateway APIs are separate and forbidden. +- [chatPromptFiles.d.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts): `vscode.chat.getSkills(token)`, dispatched through [mainThreadChatAgents2.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/api/browser/mainThreadChatAgents2.ts) to the actual prompt service's parsed skill inventory. It does not request model invocation. + +[extHost.api.impl.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/api/common/extHost.api.impl.ts) +requires the exact proposals `mcpServerDefinitions` and `chatPromptFiles`. Enable +only those for the one `uap-research.workspace-observer` test extension using +its manifest and the exact --enable-proposed-api ID; record this research-only +instrumentation. Do not enable an agent harness, alter product user settings or +claim a stable public inventory API. Missing/denied APIs fail that stage; no +private command, monkeypatch, service injection or API substitution fallback. + +[MCP conversion](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/api/common/extHostTypeConverters.ts) +returns labels and resolved HTTP/stdio configuration, but drops collection/source +IDs. Record that limit: exact unique marker URL attribution is an inference from +the sealed fixtures, not a native-emitted config-file path. Skill metadata does +include URI and source. Preserve all native rows/duplicates; never collapse by +name before checking attribution. The source's workspace/personal skill priority +is a hypothesis until an actual same-name observation supports it. + +### Exact nonprivileged executable modes for the GUI slice + +The help collector's extractor intentionally creates every regular file 0600 and +only enables its top-level code ELF. Do not silently reuse that mode policy for +a GUI, restore arbitrary TAR modes or turn on setuid/setgid bits. After the exact +archive length/digest and streaming extraction succeed, the workspace collector +permits mode 0555 on **exactly** these two regular, single-link, bounded ELF paths: + +- VSCode-linux-x64/code; +- VSCode-linux-x64/resources/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/linux-x64/rg. + +Check no-follow parent/leaf identity, ELF magic, individual length/hash and exact +archive-derived source before sealing; record both individual digests. All other +publisher files are read-only ordinary data, and root-owned directories are +non-publisher-writable. The archive's privileged/other executable mode metadata +cannot authorize another executable. A missing/mis-typed required path or any +additional required executable stops the stage for review, without exploratory +native probes or permission widening. Keep the old help collector's default +materialization behavior unchanged. + +The exact pinned [packager](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/build/gulpfile.vscode.ts) +unpacks the ripgrep-universal bin tree; [platform filtering](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/build/lib/copilot.ts) +keeps linux-x64, and [rgDiskPath](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/base/node/ripgrep.ts) +rewrites node_modules.asar to node_modules.asar.unpacked. That is source support +for the fixed search-helper path, not a runtime success claim. Pass the fixed +--disable-crash-reporter flag: [mainImpl.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/mainImpl.ts) +configures reporting only on its explicit directory/runtime-preference path; +there is no need to enable an unobserved crashpad executable. Never enable +chrome-sandbox setuid mode or disable/replace the real Electron sandbox. A real +sandbox/helper failure remains a failed observation stage. Inert tests cover +exact allowlist, missing/non-ELF/link/alias refusal, read-only sealed modes and +all privileged/unlisted executable metadata being ignored/refused. + +## Reviewed observer extension + +Add a small repository-authored CommonJS extension and test entry with no runtime +dependencies, providers, commands, tools, hooks or network calls. It declares +untrusted-workspace support only so observation can run without granting trust. +The test entry exports the documented async test runner shape, reads workspace +folders/trust, the two metadata APIs and resource-scoped read-only configuration +inspection. It writes one bounded final JSON result, then keeps the test-runner promise +pending until the controller requests planned kernel shutdown. Returning normally +could cause the extension-test leader to exit before the strict shutdown boundary, +so a final file is not permission to return/exit. It never registers +skills/MCP definitions, creates an MCP gateway, starts a server, invokes a prompt, +modifies settings, installs extensions, accepts dialogs or reads resource bodies +outside the exact inert marker set. + +Allow at most 128 native MCP/skill rows, 512 KiB result JSON, bounded strings and +portable expected local URIs. Record actual API shapes before semantic assertions; +unknown shapes, duplicate JSON keys, missing fields or output overflow fail. +Subscribe/poll to allow normal asynchronous discovery for at most 15 seconds; +record the final raw inventory, including empty/blocked results. Record the +read-only effective trust, skills-enabled, MCP-access and MCP-autostart setting values; do not +copy the entire settings/profile. Empty markers with untrusted context are a +blocked attribution stage; do not invent a causal trust explanation without +explicit native evidence. Parent validates +an ordinary single-link stable result file under the exact per-probe root before +reading. A result from another probe/root or before the current process starts +is refused. The source extension/fixtures are hash- and type-checked before and +after every native process; output cannot authorize another native command. + + +The result schema records publisher version/appRoot/appHost, extension identity/ +mode/proposals, exactly one selected folder URI and trust boolean, read-only +policy values, and bounded native arrays. MCP rows contain actual label/version +and HTTP URI or stdio fields as exposed by the API (no invented origin path). +Skill rows contain actual URI/name/description/source/sessionTypes and invocation +flags. Serialize data only, not API functions/providers; bound every string and +record unknown/unsupported shapes as a failed contract. No returned field is +used as a path to execute or a source of controller arguments. + +## Hosted-only workbench execution and actual tree containment + +Keep the real unchanged hosted admission before any output/acquisition/process, +exact HEAD verification, contents-read checkout, no persisted credentials and +explicit clean child environments. Use the same actual original nonroot IDs, +network namespace separation, loopback-only interfaces, zero capabilities, +empty groups and NNP proof immediately before each publisher exec. No vendor +process runs locally, and no hosted guard or namespace proof is spoofed. + +The full GUI can spawn detached descendants. The existing Python process-group +helper alone is insufficient for this new boundary. Reuse the repository's strict +`process.OS.RunDuplexWithPlannedShutdown` in a dedicated fixed-purpose research +Go helper. Its Linux path requires atomic CLONE_INTO_CGROUP creation and kernel +cgroup teardown; do not add a second process manager or loosen that primitive. +Build the helper from exact reviewed source with the existing pinned Go workflow +setup, recording its digest. Build is nonroot, has a 60-second deadline and live +1 MiB limits on each output stream, and may use only the already pinned module +set. A build failure remains bounded evidence; no on-the-fly package setup. +Its sole task is the finite sequence below, not an +arbitrary command/JSON execution endpoint. No production process-runner change. + +The hosted controller uses sudo only for this fixed audited helper so existing +strict containment can create task-owned cgroups. The helper itself executes no +publisher code while privileged: construct literal unshare/setpriv/env-i plus +the existing same-process nonroot/network/capability verifier before the ELF. +Before native work, the fixed helper requires actual/effective root controller +identity and validates the original nonzero real/effective UID/GID against the +trusted sudo-populated SUDO_UID/SUDO_GID and the frontend's pre-sudo measurement; +arbitrary command-line IDs alone cannot authorize a different identity. The +frontend itself must have matching nonroot real/effective IDs. The helper seals +its own executable, publisher tree, observer source and fixture executable parents +into task-only root-owned, non-publisher-writable directories, verifies the exact +reviewed hashes/types, and checks them before/after every run. Root-private +control paths are mode 0700. Only declared HOME/profile/result directories are +writable by the original identity; untrusted results cannot replace executables. +Validate every derived path before launch. +Task data/results are untrusted data, never executable controller inputs. Failure +to obtain the required cgroup/CLONE_INTO_CGROUP or namespace capability stops +before workbench code; no ordinary-runner/process-group/no-sandbox fallback. + +Use only the already installed absolute system Xvfb and xauth executables; do +not apt install them. Record ordinary root-owned executable identities/version. +Missing tools fail before publisher GUI launch. Use the literal task display :93, +Xvfb -nolisten tcp, and one random task-only Xauthority cookie. Before launch, +lstat the exact /tmp/.X93-lock and /tmp/.X11-unix/X93 and require both absent; +a collision fails without querying or connecting to that display. Xvfb uses the +same strict contained lifecycle and original nonroot identity. Its same-process +verifier records its PID before exec. Within ten seconds, readiness requires the +new ordinary lock to contain that exact PID and the new Unix socket to have the +expected UID and stable identity. A preexisting/replaced socket or wrong PID/UID +fails before any GUI process; no attach-or-retry display search. + +Create the authority file with the fixed xauth -f source - command, +feeding only the reviewed add-display operation plus cookie on its explicit +stdin. Cookie bytes never appear in recorded argv or controller summaries. Give +the native identity read-only access through a root-owned nonwritable auth parent +and file; keep it separate from mode 0700 controller assets. Exclude the file from +upload and redact any occurrence in bounded xauth diagnostics, explicitly marking +that stream as redacted. This is task display isolation, not product/account +authentication. No inherited DISPLAY/XAUTHORITY or new vendor shell wrapper is +used. The reused strict primitive does perform its existing fixed inert shell +loop capability probe; that established probe is not an editor invocation. + +Run shipped ELF directly as an editor (without ELECTRON_RUN_AS_NODE), explicit +fresh user-data/extensions dirs, --new-window, exact extensionDevelopmentPath, +extensionTestsPath, the one proposal-enabled extension ID and one selected local +folder. The exact argument spelling is in pinned argv.ts; no default chat call. +Never pass --disable-workspace-trust, --no-sandbox, --disable-setuid-sandbox or +change kernel/AppArmor policy. If the real Electron sandbox cannot initialize, +retain its bounded diagnostics and stop. No second archive/runtime fallback. + +### One nested lifetime owner and exact cleanup reserve + +The helper installs its signal/overall contexts before creating any child. One +outer RunDuplexWithPlannedShutdown call owns Xvfb. Its callback starts the bounded +Xvfb diagnostic drain, proves readiness, then runs all four inner GUI strict +calls sequentially. Each GUI exchange watches only its current result, validates +it, and returns nil solely to request planned kernel shutdown; it returns a +non-nil cause on missing/invalid result, overflow or deadline. It never returns +nil just because the publisher exited. Join each inner strict call and its +bounded drain before starting another. Only after all inner trees are reaped +may the callback return and request Xvfb teardown. A failed stage stops the +remaining table, then cleans the current GUI tree before the display tree. + +Use separate work cancellation and display-lifetime contexts: cancellation of +a GUI/API work deadline signals the current inner exchange first, and does not +simultaneously kill the outer Xvfb owner while that inner tree is being reaped. +An external signal requests the same ordered shutdown. The outer context is a +later independent safety deadline, not the parent canceled work context. + +Reuse the exact process primitive, not copied kernel code. At the current source, +Linux creates an exact task cgroup beneath its own unified cgroup2 membership; +required events/kill/procs files and an open cgroup FD precede atomic +CLONE_INTO_CGROUP/Setpgid launch. Each invocation first runs the existing strict +capability probe. Planned teardown allows a one-second pidfd stop/causality wait, +then cgroup.kill and up to one second for leader exit plus populated=0; exceptional +leader-kill fallback can add one second. Sole Wait/reap has a one-second bound, +WaitDelay is100 ms, and containment close owns only its created cgroup. The +ordinary two-second duplex exit grace applies to the nonplanned natural-exit +path, not as a substitute for planned teardown. Reserve ten seconds per strict +scope including its established preflight/cleanup/fallback windows and report +any failure from the primitive. These are bounded userspace operations; an +uninterruptible kernel syscall is not promised an impossible hard bound. + +The existing strict runner retains only a bounded32 KiB diagnostic stderr buffer +and does not export a successful stderr stream. Therefore, immediately before +exec, the same-process nonroot verifier redirects stderr to stdout for Xvfb and +GUI children. Each exchange drains one explicitly **merged** diagnostic stream +with a live1 MiB retained limit; overflow cancels work and cannot become a nil +exchange result. Keep result JSON separate (512 KiB). Join drains after the +primitive closes their pipes, bounded by the enclosing cleanup reserve. Record +merged byte counts/hashes, overflow and planned stop; never invent separate +stderr=0 or a natural successful exit. Planned teardown can truncate a final log +tail, which is stated separately from the validated final API JSON boundary. +Pre-exec verifier failures retain the primitive's bounded diagnostic error. + +All absolute offsets below start at the frontend's monotonic eight-minute +budget before acquisition/build. Check remaining budget before each phase; do +not start another inner GUI if less than 65 seconds of work budget remain. Each +GUI has a 45-second work bound including its 15-second API discovery wait; Xvfb +readiness is ten seconds. At 420 seconds stop/cancel work, allowing up to ten +seconds to finish inner teardown and another ten for outer Xvfb teardown. The +root helper then gets an independent five-second held-root cleanup window for +its root-owned assets and returns its bounded summary by 450 seconds. Its normal +return deadline precedes the frontend watchdog: at 460 seconds the watchdog sends +TERM, permits signal cleanup until 470 seconds, then may kill the helper only as a failed, +unproved-cleanup outcome. Reserve the last ten seconds for frontend-owned +filesystem cleanup (five) and upload admission (five). Partial collection is +allowed if earlier acquisition/build consumed the work window; deadlines are +not silently extended to finish the table. No new child starts during cleanup. + +Root-created control/sealed trees are cleaned by their root helper owner using +the existing anchored, descriptor-relative bounded cleanup pattern; the +nonroot frontend cannot claim to remove root-owned descendants. Frontend cleanup +handles only its own held task root. A killed controller, cleanup timeout or +unproved cgroup emptiness forces collection incomplete. Never convert VM +disposal or a missing process handle into successful cleanup. + +### Every-child capture and upload admission + +GUI/Xvfb diagnostics are merged as above. The helper build has separate live1 MiB +stdout/stderr limits and 60 seconds; fixed system identity/version and xauth setup +commands have five seconds and 64 KiB per stream. Controller stdout/stderr each +have 64 KiB, and no child command or cleanup failure uses an unbounded fallback. +Every child is in a literal finite command table; no executable/argument comes +from metadata. All retained raw/redacted streams, four <=512 KiB API results, +bounded identity/cgroup/cleanup records and failure summaries share an enforced +<=32 MiB/64 flat-file envelope. Stop on cumulative exhaustion. The previously +reviewed five-second separate ZIP_STORED admission and independent upload gate +remain the only upload path; rejected raw trees yield a bounded false-claim +failure ZIP, never an always-upload of raw profiles or caches. + +## Inert fixtures and finite observation table + +Prepare only A/B folders with no Git hooks/code, fresh owned home/profile data, +text-only skills with disable-model-invocation:true, and remote loopback port-9 +MCP markers (no stdio commands, credentials, OAuth or external endpoint). Fixtures +and extension sources are immutable during each observation. Native cache/log/ +database writes are isolated from authored baselines and never uploaded wholesale. +Fresh profile preferences disable updater, telemetry, extension auto-update and +welcome prompts; they keep workspace trust enabled and ungranted. All four runs +use the **same** original task HOME outside A/B; only their explicit user-data +profiles are fresh clones of the declared inert baseline, not of another run's +mutable state or the actual user's profile. + +Set chat.mcp.autostart="never" explicitly in that baseline and record its actual +effective value. This is a restrictive research setting, not a claim about the +editor's default startup or enabled session behavior. At the exact pinned source, +[mcp.contribution.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts) +registers editor discovery and services. [mcpService.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/mcpService.ts) +constructs definition/enablement/cache observers; its autostart method checks +Never and returns before collection activation, candidate enumeration or +startServerAndWaitForLiveTools. [mcpManagement.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/platform/mcp/common/mcpManagement.ts) +defines that exact setting/value. [mcpRegistry.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts) +separately gates workspace connection resolution on trust; the observer never +calls that start path or accepts trust. Preserve metadata discovery without +enabling an agent, harness or autostart. The absence of a port 9 server is **not** +proof of zero connection attempts. This collector does not instrument accepted +TCP or prove universal absence of attempts; MCP connection/handshake/runtime +remain not evaluated regardless of returned metadata. + +Use one user skill at the documented personal root and user-profile User/mcp.json; +A/B each have .github/skills and .vscode/mcp.json with unique marker names/URLs. +Also include one same-name user/A/B skill with distinct inert descriptions to +observe actual path precedence, without inferring that three copies are active. +Do not create portable .mcp.json, chat.pluginLocations, copied personal settings, +Local TEST registrations or installed external extensions. + +Exactly four shipped-workbench observations are allowed, each a fresh process +and fresh profile cloned only from the declared inert baseline: + +| Probe | Selected single folder | Authored fixture difference | Observation | +| --- | --- | --- | --- | +| A | A | A/B/user marker fixtures present | Actual folder/trust, editor MCP metadata and parsed skill paths. | +| B | B | Same immutable fixtures | A/B/user selection separation and same-name skill URI. | +| A-restart | A | Same immutable fixtures | Fresh-process inventory, with no claim about an existing session. | +| A-after-removal | A | Controller removes only its declared A marker files before launch | Absence of exact A markers; user and B authored files remain. This is fixture removal, not UAP ownership proof. | + +Unexpected row structure, native failure, sandbox/privilege failure, mutation of +sealed sources, missing result or deadline fails collection and retains bounded +raw diagnostics. A collected empty/blocked inventory is recorded as negative, +not silently promoted to successful source selection. Never substitute hand- +parsed files for an unavailable native API or mix filesystem data into native rows. + +## Verification, interpretation and conditional product design + +Inert tests cover real guard refusal before effects; literal finite argv and +proposal/extension/autostart limits; no forbidden commands/security bypass; +trusted sudo identity and root-owned executable parents; cookie-free/redacted +records; preexisting display refusal, exact PID/socket readiness and ten-second +startup failure; fixed HOME/fresh baseline; path/output/identity/freshness caps; +native-shape/empty/duplicate/API-denied distinctions; pending extension lifetime; +GUI-before-display teardown order, cancellation and joined bounded drains; +all-child/aggregate overflow, build/xauth failure; source mutation refusal; +bounded clean failure envelope; and exact source pins. Reuse existing process containment tests, +including immediate setsid descendants and cancellation; do not copy their kernel +implementation into the collector. Validate build/vet/format/workflow and run +the relevant no-vendor suites. Obtain two frozen code GOs before publication. + +Independently interpret actual metadata attribution and each source/trust limit. +This collector never sets native_project_qualified, Local-harness/runtime, UAP +ownership, MCP initialization or skill-execution flags true. Only an actual +shipped-workbench inventory stage can be passed here. If it cannot run safely, +the workspace gate stays blocked; no product adapter follows from static source. + +After positive relevant observations and independent interpretation, freeze a +separate project adapter implementation plan. Its likely narrow design is a new +project-only VS Code constructor registered by project composition, exact +.vscode/mcp.json component receipts and documented project skill paths, shared +native-config mutation and Prepared lifecycle. The legacy New user/Copilot +backend and NewLocal1.140 TEST/profile/hook source stay untouched. Single local +folder only; remote/multi-root/Local-session effectiveness and trust are separate +limits. No adapter source or new scope capability is admitted by this plan. + +## Independent R1 review receipt and R2 scope + +Both critics reviewed immutable 01f55f6218427ce97e44d6e167507ad06e5cbcf2 +(published equivalent 9c43f29fcd4e580672ef2adfd7f8cbc559de3c57), without native +execution. Root independently read the pinned proposed APIs, extension-host +gates/MainThread publication and trust calculation, and gave architecture GO +conditional on explicit nested display/GUI lifetime, readiness and cleanup +reserve, every-child caps and blocked-versus-denied classification. APM's +independent R1 architecture GO additionally required a workbench autostart +boundary, fixed original HOME, trusted sudo caller identity, root-owned +nonwritable controller/source parents and cookie-free recorded argv. + +This R2 plan makes those conditions concrete, corrects final-test return into a +pending promise, and describes the existing runner's merged-diagnostic limit +honestly. An empty inventory or a known API response with blocked/unknown attribution +may complete safe collection with false attribution/qualification; explicit API +denial, unknown/malformed output shape, changed +trust or sandbox failure is a failed stage. No sandbox fallback, trust grant, +collector code or project adapter is authorized before the two final R2 GOs. + +The root critic gave final R2 GO on5995254a after independently checking the +MCP autostart enum/service and trust gate. Before collector code, implementation +feasibility review found the extractor's intentionally non-executable file modes. +The explicit two-file nonprivileged-mode section above is a narrow R2 addendum +requiring both critics' final review at this new frozen checkpoint. It enables no +extra native probe, privileged publisher mode or project adapter. + +Both independent critics gave final R2 PLAN GO on immutable +2b9f825bb91d305fabe112f23677a064a60f2430. Root and APM each independently read +the pinned autostart service/enum, crash-reporter startup condition, packager +unpack/filter and ripgrep loader. Both accepted the exact two-ELF nonprivileged +allowlist, sealed root-owned ancestors/trusted sudo identity, fixed HOME, +pending observer, nested strict GUI-before-display lifetime, every-child capture +and reserved cleanup budget. Both performed source-only review without native +execution. This admits implementation of this research collector only; two fresh +frozen code GOs are still required before native publication. A real sandbox, +extra helper, API or trust failure remains an honest failed/blocked observation, +and no project adapter is admitted before actual workspace evidence and a new +reviewed adapter plan. diff --git a/docs/plans/project-install-e2e/vscode-workspace-primary-source-notes.md b/docs/plans/project-install-e2e/vscode-workspace-primary-source-notes.md new file mode 100644 index 000000000..2aef0d97b --- /dev/null +++ b/docs/plans/project-install-e2e/vscode-workspace-primary-source-notes.md @@ -0,0 +1,59 @@ +# VS Code workspace evidence: primary source notes + +2026-10-24. Research only, pending the genuine pinned publisher help/version +artifact. This document does not approve a workspace collector or adapter. +The narrow owner exception permits project installation work; all unrelated +VS Code user/Local/TEST/hook implementation remains frozen and preserved. + +## Current documented harness boundaries + +Official documentation was reread with two search systems and checked against +source at the actual publisher commit +`e646868227c3e3a9e3f24277c8c7220be42c0be7` (VS Code 1.143.1). + +| Primary reference | Consequence for the next plan | +| --- | --- | +| [MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration) | Workspace `.vscode/mcp.json` uses `servers`; portable workspace `.mcp.json` uses `mcpServers`. Copilot reads the portable format and receives eligible editor-format servers through forwarding. `code --add-mcp` targets the user profile. Neither a CLI user install nor a portable Copilot file qualifies the selected Local workspace route. | +| [MCP destinations](https://code.visualstudio.com/docs/agent-customization/mcp-servers) | The legacy workspace destination remains supported for Local capabilities; the guided flow labels it deprecated. Preserve that compatibility boundary rather than silently migrate or toggle an experimental setting. | +| [Agent skills](https://code.visualstudio.com/docs/agent-customization/agent-skills) | Documented repository skill directories include `.github/skills`, `.agents/skills` and `.claude/skills`. Duplicate names across workspace roots and parent-repository discovery have their own selection rules. File delivery cannot prove effective skill use. | +| [MCP operations](https://code.visualstudio.com/docs/agents/run/manage-mcp-servers) | A setting controlling editor autostart does not stop another harness from starting its independently configured servers. A future fixture must keep the harness and real observation explicit. | + +No literal credentials, user inputs or profile secrets should be copied into a +workspace. Existing inputs, sandbox fields, unrelated servers and enablement +must be preserved under exact member ownership. A native help/version result +cannot prove any of these workspace policies. + +## Pinned source checked, without execution + +The following files were retrieved directly from the immutable publisher commit. +These source observations support future design; they are not runtime evidence. + +| Source at `e6468682` | Checked boundary | +| --- | --- | +| [mcpConfigurationDestination.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/browser/mcpConfigurationDestination.ts) | Destination validation requires a file rooted in an open workspace folder; root-format add is gated while legacy-format choice remains available. The legacy guided destination receives its deprecated label. | +| [mcpConfiguration.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts) | Editor-format schema defines stdio cwd/default workspace handling, HTTP/SSE transport, inputs and sandbox capabilities. Do not treat the portable schema as interchangeable. | +| [workspaceDotMcpDiscovery.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/discovery/workspaceDotMcpDiscovery.ts) | Watches each workspace-root portable file with an explicit collection identity, folder provenance and workspace storage scope. Its parser is separate from the editor-format route. | +| [installedMcpServersDiscovery.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts) | Installed editor-format registrations are attributed to actual configuration resource and workspace folder; stdio default cwd and roots use that folder. User/unknown defaults are explicit fallbacks, not evidence of a selected project. | +| [mcpTypes.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/mcpTypes.ts) | User profile, remote, workspace-folder editor format and portable workspace format have distinct provenance. Exact collection/resource attribution matters more than a matching displayed server name. | +| [mcpRegistry.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts) | Workspace server use checks workspace trust before per-server trust behavior. A collection marked trusted is not permission to bypass the workspace decision. | +| [promptFileLocations.ts](https://github.com/microsoft/vscode/blob/e646868227c3e3a9e3f24277c8c7220be42c0be7/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts) | Built-in skill roots distinguish the three workspace directories from three personal directories. Static availability does not prove the selected Local harness loaded an exact skill. | + +## Existing UAP boundary to preserve + +Current `vscode.New()` delegates plugin lifecycle/identity to the user Copilot +backend and must remain intact. `NewLocal` is a separate explicitly TEST-qualified +selected-profile plugin/hook route (old 1.140.0 tuples), not a project workspace +MCP adapter. Reusing that TEST qualification for 1.143.1 or relabeling it as native +workspace proof is invalid. The approved original all-client plan calls for a +new dedicated project adapter registered only in project composition, one local +filesystem folder initially, explicit Local harness, exact component receipts +and no user Copilot install/uninstall side effects. + +After the genuine publisher artifact is available, independently interpret its +exact version/commit/help/namespace results. Then freeze a new concrete workspace +observation and adapter plan, obtain both plan-review rounds, and independently +review any new collector before hosted execution. A shipped-host extension/UI +observation needs its own bounded execution and authority contract; a synthetic +extension-host protocol or `code --version` cannot substitute. Do not use new +trust grants, default editor launch, `--add-mcp`, profile redefinition or another +harness to hide that remaining evidence gate. diff --git a/docs/plans/project-install-e2e/windows-atomic-publication.md b/docs/plans/project-install-e2e/windows-atomic-publication.md new file mode 100644 index 000000000..5a959abb2 --- /dev/null +++ b/docs/plans/project-install-e2e/windows-atomic-publication.md @@ -0,0 +1,185 @@ +# Windows atomic publication with concurrent readers + +## Observed failure + +The exact integrated candidate is `6a1d53f4b69f6761dd62039bd36404dad7119a04`, +tree `48a00b68a4f935aee2eddc6c7b761fa787da3bc1`. Terminal run `40201578627`, +Windows job `121193687175`, failed its `concurrent-add` resilience case. The +four public add commands produced no successful result. The preserved artifact +is `12601824719`, 32,106,740 bytes, SHA-256 +`83e4c68487ccbb72886079ff3361d16f2d71501a648b115ca2b23283a829323f`. + +The artifact's `resilience-evidence/concurrent-add/add-N.stderr` and `.stdout` +retain all four independent results. Commands 0 and 1 refused the held mutation +lock. Command 2 failed replacing an assessment cache entry with access denied. +Command 3 obtained the mutation lock but failed replacing an operation journal, +also with access denied, and reported `managed_rolled_back`. The later broad +Windows CLI contracts passed. Linux and macOS Terminal jobs also passed. + +The failed cache publication is `securityscan/cache.go`: on Windows it deletes +the destination before renaming a synced temporary file. Its reader uses +`os.ReadFile`. The failed operations-v2 journal uses `dirswap.Manager.Load` +(`dirswap.go`) with `os.ReadFile`, and `Manager.save` with `atomicfile.Write`. +This is the new directory-swap journal, distinct from the legacy FileJournal. + +No handle owner or native handle flags were captured in the failing run. The +two access-denied results establish publication failures; the specific +concurrent reader overlap remains an inference to exercise deterministically. +Do not assign either error to Linux process supervision or relax the four-add +case's required successful winner. + +## Primary mechanism and bounded scope + +The pinned Go 1.26.10 implementation has two relevant API paths: + +- Ordinary `os.Open`/`os.ReadFile` uses Windows sharing without delete sharing; + ordinary `os.Rename` uses `MoveFileEx` replacement. +- `os.Root.Open`/`Root.ReadFile` uses the rooted Windows open with read, write + and delete sharing. `Root.Rename` uses a held parent directory and native + POSIX replacement. The pinned implementation makes one traditional native + rename attempt after a failed POSIX attempt; neither path removes the target + first or contains a delay/retry loop. + +References: pinned Go source `src/os/root.go`, `src/os/root_windows.go`, +`src/os/root_openat.go`, `src/internal/syscall/windows/at_windows.go` and +`src/syscall/syscall_windows.go`; [Microsoft rename flags](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information), +[rooted rename structure](https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_rename_info) +and [sharing semantics](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew). +[Git's independently reviewed implementation](https://code.googlesource.com/git/+/391bceae4350136a05d977573caeaa07059f2136) +also requires destination readers to share deletion for POSIX replacement. +Changing only the replacement primitive does not repair an ordinary open +reader, which remains a required refusal control. + +Use the pinned standard library instead of custom unsafe Windows ABI buffers. +Root methods handle native alignment, UTF-16 byte lengths, Unicode names and +long paths. Convert relative caller paths lexically to cleaned absolute paths +without resolving links; relative inputs therefore retain their existing +current-directory meaning. A root is opened on the cleaned parent directory; every operation +receives only the generated temporary or target basename. Root follows the +parent directory as the existing path APIs do, but prevents a leaf traversal +from escaping that parent. For reads, absolute or outside leaf symlinks +therefore fail closed. Rename acts on the final link entry itself rather than +following it; `atomicfile.Write` retains its existing explicit Lstat refusal +of a symlink or nonregular target. Cache publication does not falsely acquire +an existing symlink-refusal guarantee: its prior Windows implementation also +replaced the link entry. No broader filesystem or ownership API is changed. + +The existing `atomicfile.Write` call sites include filesystem-port writes, +safe mutation and directory-swap journals; state-v2; native configuration and +shared client JSON; selected OpenCode, Kimi and ChatGPT project metadata; +migration backups; staged environment metadata; and retained-data markers. +The writer's same-directory atomic contract remains common to those callers. +Only the two positively implicated reader sites, directory-swap journals and +assessment cache entries, opt into the new read helper. Legacy filesystem-port +reads, state-v2 reads, client configuration reads and every other reader remain +unchanged. This slice does not qualify all other possible reader/writer races. + +## Frozen implementation boundaries + +Production files are limited to `atomicfile/write.go`, new platform-specific +`atomicfile/read_{windows,other}.go` and `replace_{windows,other}.go`, +`dirswap/dirswap.go`, and `agentplugins/adapters/securityscan/cache.go`. +Tests are new Windows-only files in atomicfile and securityscan; the separate +focused workflow and this plan complete the slice. No state-v2 or legacy +filesystem reader is edited. + +1. Add a small `atomicfile.ReadFile` helper with platform files. Non-Windows + behavior remains `os.ReadFile`. A private Windows `openReadFile` opens + `os.Root` on `filepath.Dir` and calls `Root.Open` on `filepath.Base`. + Root closure is checked before returning the file; if it fails after opening + the file, close the file as well and retain all failures. ReadFile uses + `io.ReadAll` and a checked file close. Return any original open/read/rename + error object unchanged when closing succeeds; only join when an additional + close failure occurs. Do not unconditionally call `errors.Join(err, nil)`: + the changed concrete type would break existing `os.IsNotExist` callers. + This small private decomposition allows a test to hold the exact production + read handle without a global hook or additional exported handle API. + Keep existing decoding, ownership checks, + cache size/strict-JSON checks and failure behavior in the callers. +2. Change only `dirswap.Manager.Load` and `securityscan.FileCache.Load` to use + that helper. A cache read failure remains a miss; journal read failures remain + errors. Do not change the generic filesystem port or unrelated readers. +3. Add `atomicfile.Replace(source, target)` for an already synced and closed + temporary file. It creates no directory, changes no mode and removes neither + name on failure. Route `atomicfile.Write`'s existing final publication through + it. Non-Windows uses the original `os.Rename`. Windows first + requires cleaned absolute source and destination to have the same parent, opens that + parent once, and calls `Root.Rename` with their single-component basenames. + Preserve the existing target symlink/nonregular refusal, temporary mode, + complete write, sync, checked close, cleanup and parent sync. Check root + closure and retain native failures. No ACL-ignore or readonly-ignore flags, + custom native calls, removal fallback, sleeps or application retries. +4. Keep every existing FileCache key validation, parent creation with mode + 0700, JSON encoding, temporary creation/mode 0600, write, sync, checked close + and deferred cleanup. Replace only its final Windows remove plus rename + with `atomicfile.Replace(temporaryName, target)`, retaining the cache commit + error boundary. The common primitive does not create a missing parent, so a + removed cache root cannot be recreated with a wider mode. Do not treat a + failed cache write as successful or suppress its error. + +## Deterministic contracts and hosted observation + +Use private local temporary files, no native client, scanner, account or +network. Native Windows contracts must distinguish these cases: + +- Hold an ordinary `os.Open` reader. The legacy replacement control fails; + corrected publication must still refuse it, retain the old named bytes and + clean its temporary file. This establishes the required sharing boundary. +- Hold the exact private production `openReadFile` reader. Establish that + legacy `os.Rename` cannot replace + the open destination, then require corrected `atomicfile.Write` to succeed. + The original handle must still read the complete old bytes, while a fresh + production read returns the complete new bytes. No intermediate deletion or + partial file is accepted. +- Exercise actual FileCache Store/Load on an existing assessment entry with a + held delete-sharing reader; require the updated valid assessment and exact + old bytes through the retained reader. The deterministic held-reader contract + exercises the former remove-before-rename path directly; no stress loop or + additional concurrent-store experiment is added. +- Readonly targets, directory targets, missing paths and permanent errors + continue to fail appropriately. Verify + old target bytes and absence of leaked temporary files on refusal. Include + Unicode and long-parent-path names. Existing ordinary write/read and strict + journal/cache validation contracts remain required. +- The existing target-link Lstat refusal stays byte-identical in production; + existing Unix symlink guard tests remain required. This bounded Windows + observation does not newly qualify symlink creation or refusal: the historic + Windows symlink test skips, and it is outside the four named native tests. +- Missing-parent and missing-leaf read contracts each require both + `os.IsNotExist(err)` and `errors.Is(err, os.ErrNotExist)`. Preserve the existing journal missing-load + test and caller unchanged; it must keep recognizing the original failure. + +Run only necessary affected-package contracts, vet and configured lint with +the actual CI merge base plus the unfiltered size/architecture gate. Native +Windows behavior is not qualified by local Linux compilation. Coordinate all +builds because local disk capacity is limited. + +The exact new names are `TestWindowsAtomicPublicationHeldReaders`, +`TestWindowsAtomicPublicationRefusals`, +`TestWindowsAtomicPublicationUnicodeLongPath`, and +`TestWindowsAtomicPublicationSecurityCache`. The first three live in +`adapters/atomicfile`; the fourth exercises `agentplugins/adapters/securityscan`. +The positive atomicfile test holds `openReadFile`; the cache test holds the +same pinned Root.Open API and exercises the real cache Store/Load. Existing +atomicfile package tests include a Unix-only mode assertion and a Windows +symlink skip; do not claim those are a portable native Windows qualification. +All four new required tests must execute and pass without skips. + +After both independent plan rounds and frozen source reviews, add one finite +`.github/workflows/project-windows-atomic-diagnostic.yml` job triggered only by +push to `fix/project-windows-atomic-20261025` or explicit workflow dispatch. +Use `windows-2025`, checkout pin +`3d3c42e5aac5ba805825da76410c181273ba90b1`, setup-go pin +`b7ad1dad31e06c5925ef5d2fc7ad053ef454303e`, Go `1.26.10`, cache disabled, +read-only repository permissions, a ten-minute job deadline and one `go test` +invocation across exactly the two affected package paths. Use `-count=1`, +`-timeout=2m`, `-v` and an anchored union of the four exact test names. +Preserve raw output even on failure, exact source/tree and measured Go version; +record the actual runner image from GitHub's Set up job log. No retries or +additional environment/containment authority are added. The ordinary-reader +refusal and legacy delete-sharing failure controls execute in the same +observation as the corrected success cases. Do not rerun unchanged failing +source for green. A finite log check requires all four exact top-level PASS +records and rejects missing tests or any skip, before reporting qualification. +Final integration still requires the unchanged concurrent-add resilience +assertions and all normal Required/Core/Coverage/native gates. diff --git a/docs/plans/project-install-e2e/windsurf-devin-code-checkpoint.md b/docs/plans/project-install-e2e/windsurf-devin-code-checkpoint.md new file mode 100644 index 000000000..916093a60 --- /dev/null +++ b/docs/plans/project-install-e2e/windsurf-devin-code-checkpoint.md @@ -0,0 +1,138 @@ +# Current Devin project adapter implementation checkpoint + +2026-10-23. Original client plan2GO is preserved. The narrow host-evidence/ +project-only absent-profile amendment has both plan GO reports committed before +source and both independent implementation GO reports at64c7ee6d. The composed +saved repair exposed redundant selected-client KnownTargets collection; its +separate c48bbc58 amendment and both plan GO reports at2a328849 precede the +selected-client exclusion. Sibling admission is unchanged. + +## Implementation + +Only the selected primary Devin with normalized exact observed3000.11.205 enters +the new project configuration lane. Detection and the legacy Windsurf user +implementation stay intact. Package/state/data remain in .uap; native skills +use .devin/skills and MCP uses .devin/mcp_config.json. A narrow current-Devin codec +preserves command/args/env without cwd and url/headers with explicit http/sse; +legacy Cascade codec behavior is untouched. No vendor process is launched by +the project adapter or public SDK. + +All project entry points bind exact scope/original user context, native root, +client identity, artifact and .uap managed package path. Binding validation and +native receipt validation reject copied/unknown/duplicate/nonclean/traversal +objects. Active reuse checks selected stdio package/data/cwd launcher arguments +and env read-only; disagreement requires a fresh projection. Native writes use +the reviewed shared ownership kernel/component transaction. Legacy project main +config MCP and same-name local overrides refuse; unrelated local policy, foreign +disabled MCP, skills/hooks, user and project B bytes are preserved. Exact owned +remove and recovery do not depend on current version or launch default-local +native removal. + +Files remain prepared plus installation-verified. Project activation attestation +refuses. No native discovery, effective aliases/imports/policy, workspace trust, +account/model, MCP initialization/runtime, Desktop UI or release is qualified. +Ten genuine isolated version/help successes and vendor/archive/binary pins are +pre-code evidence only; actual project native proof remains pending. + +## Composed evidence + +- Production loader/planner/stager/provider suite passed0.426s, including full + skill+remote lifecycle/update/repair/component removal, read-only stdio data + mismatch rejection, exact scope/binding/artifact/receipt failures, migration/ + local-shadowing refusal, changed native aliases, attestation refusal, visible + MCP write failure with byte/skill rollback, and committed cleanup retaining + the exact new receipts. +- Public SDK suite passed0.590s: A/B stdio/data separation, repair from original + pin, fresh-installer update, exact remove without current harness/version, + current-only empty legacy profile, exact/unknown/older/wrong-primary version + admission and source-manifest non-injection. +- Public production CLI suite passed6.531s with real loopback TLS Git/ZIP source + acquisition: saved add, locked no-op, read-only commands, repair, update, + export and cold import with exact portable pins, A-only removal and user/B + preservation; direct no-save current-only add/remove and unqualified host + refusals also passed. No configured launcher/native/MCP process is executed. +- Both existing CLI metadata boundaries and selected-versus-sibling collection + regression passed0.020s. Windsurf stays outside the qualified no-save facade; + its direct usecase retains selected domain metadata, while the existing + qualified facade test proves that builder's selected version reaches SDK. +- Full client/domain/planner/native-codec suites passed0.012/0.034/0.031/3.539s. + All four changed compatibility goldens add only Windsurf project scope; + existing user planning goldens are unchanged. +- Affected legacy Windsurf provider/strict SDK target-facts/saved client tests + passed0.076/0.015/1.743s. Existing stricter user/sibling roots and profile + resolver contracts are preserved. + +Exact changed-code lint and full-file complexity/architecture lint both reported +zero issues with golangci-lint2.13.2 across all affected packages. git diff --check +passed. Two independent final adapter reviews are required on this frozen source +checkpoint. Root owns the separate stacked client PR and publication; no main/ +release merge is authorized by this checkpoint. + + +## Independent review correction checkpoint + +R1 found that documented native comments were rejected in both the read-only +project policy files and the new target MCP codec. The initial source command +was rejected by automatic approval review before execution for missing amendment +plan reviews. The e09d06d3 native-comments plan then received both independent GO +reports, committed ate8f638ed before source. The implementation reuses the existing +comments-only native AST grammar for new CodecDevin and ephemeral native policy +normalization, preserving foreign bytes and refusing duplicates/trailing commas. +Gemini's accepted/rejected grammar and error wording, explicit JSONC branches, +legacy Cascade and all portable decoders retain their existing behavior. + +R2 additionally identified that direct project binding inspection omitted the +binding.ClientID discriminator. Both reviewers confirmed that requiring the +existing exact Windsurf product identity is within the original exact-client +contract. The validator now requires ClientID `windsurf` and the copied-binding regression checks it directly and via inspection; +user-scope additional validation remains unchanged. + +The focused current-Devin/existing-Gemini codec tests passed0.014s. The next +provider relink hit actual shared-storage ENOSPC before execution; that attempt +is a resource failure, not a regression pass. Parent requested true merge of +reviewed static component baseline d39e40ef before final review refresh. Final +merged scope/provider/composed tests and lint will be recorded after headroom. + + +## Reviewed component baseline integration + +Parent requested true merge of d39e40ef, the static reviewed eight-client/shared +component baseline. Three manual native-codec dispatch conflicts were resolved +as a union: parseCodecDocument keeps Codex TOML dispatch before the unchanged +Gemini plus new-Devin comments-only branch; projectServer retains both the +Copilot and new-Devin dispatches; supportedCodec retains Codex/Copilot plus Devin +and every prior codec. Domain rows and all four compatibility scope goldens +merged automatically as a union. No foreign client source/test file was manually +edited. This merge is part of the final R1/R2 refresh target, not fresh native +qualification. Bounded post-merge compilation awaits parent resource headroom. + + +## Final independent review closure + +Both independent implementation reviews are GO at frozen merged +`d69091b3328a1b626d9475643e8e97a8bf040b4a`; their full reports are retained in +`reviews/devin-code-review-r1.md` and `reviews/devin-code-review-r2.md`. Both +accepted findings are closed: documented comments in current native policy and +MCP target configuration use the approved comments-only grammar, and direct +project binding validation requires exact product ClientID `windsurf`. The +reviewers independently checked all three manual codec-union resolutions and +confirmed foreign client implementation directories are unchanged from the +reviewed static component baseline. + +Independent final-code full nativeconfig tests passed3.471s and the complete +preserved Windsurf client suite passed0.006s. The original public TLS Git/ZIP and +SDK lifecycle test source is unchanged by these narrow corrections and baseline +integration. Final composed provider/SDK/CLI retry attempts encountered actual +temporary-storage ENOSPC before meaningful test execution; these are recorded +resource failures, not passing reruns. The earlier author-run composed passes +and lint evidence above remain attributed to their original checkpoints. Root +requested no optional duplicate compilation after recurring capacity exhaustion. +Review GOs therefore combine full source/authority/union inspection, the exact +new regressions, the passing final codec/client tests and the prior composed +evidence, with transparent final execution limits. Hosted/normal-capacity gates +must still provide fresh execution evidence. + +No native project discovery, effective policy, trust, account, MCP initialization +or runtime proof is inferred. Materialized state remains prepared/installed and +project attestation remains refused. Root owns remote publication and the +separate stacked client PR; this checkpoint authorizes no main/release merge. diff --git a/docs/plans/project-install-e2e/windsurf-devin-implementation-evidence.md b/docs/plans/project-install-e2e/windsurf-devin-implementation-evidence.md new file mode 100644 index 000000000..9b7eb458c --- /dev/null +++ b/docs/plans/project-install-e2e/windsurf-devin-implementation-evidence.md @@ -0,0 +1,81 @@ +# Windsurf / current Devin Local project pre-code evidence + +2026-10-23, cursor_clients. The original client plan and both independent GO +rounds remain authoritative. No Windsurf/Devin production code is changed at +this checkpoint. The implementation is the single newer dedicated-MCP lane, +separate from preserved legacy Cascade user behavior. + +## Official pin and genuine bounded version/help + +Root captured the official stable manifest from the vendor installer metadata, +without executing the installer. Exact version **3000.11.205**, Linux x86_64: + +- https://static.devin.ai/cli/3000.11.205/devin-3000.11.205-x86_64-unknown-linux.tar.gz +- Vendor manifest and measured archive SHA256: + `2e65c8b93682258fc91f5e4cead92e3698338cebf1b31dbc32a3201cb63705c4` +- Measured archive length: **73,679,375 bytes**. +- Complete official inventory: 250 directory/regular-file members, + **218,025,070 bytes** total payload, including the single `bin/devin` + executable and its exact-version official documentation. +- Binary length **217,494,824 bytes**, measured SHA256 + `642c82e32cb3dec90cf5ab01975520b425307872c7a650c9fb5b0653f37681f4`. + +Ten actual version/help-only probes all exited zero: --version, --help, +mcp --help, skills --help, mcp list/get/add/remove --help, and skills +list/paths --help. Version is `devin 3000.11.205 (6dea39440ae3)`. + +The complete distribution was mounted read-only in a fresh networkless +unshare-all namespace for each process, with empty /proc, fresh HOME/XDG/project, +DEVNULL stdin, minimal literal environment, bounded logs and deadlines. No host +user configuration or credentials were mounted/inherited. Exact argv and all +outputs/provenance are retained under task tools/native-probes/devin/3000.11.205. +The verified archive and tiny pinned official docs are retained; only the +reproducible218MB extraction was reclaimed after those observations. + +No list/discovery, session, model, configured MCP server, auth, trust, migration, +plugin registry or account operation was run. Help is not native project proof. + +## Exact-version and current official configuration references + +The exact distribution's docs agree with the current canonical references: + +- https://docs.devin.ai/cli/extensibility/mcp/configuration +- https://docs.devin.ai/cli/reference/configuration/config-file +- https://docs.devin.ai/cli/reference/configuration/global-vs-local +- https://docs.devin.ai/cli/reference/configuration/read-config-from +- https://docs.devin.ai/cli/extensibility/skills/overview +- https://docs.devin.ai/desktop/devin-local + +Since3000.3/Local3.6 the current lane uses `.devin/mcp_config.json`, with a +separate higher local override `.devin/mcp_config.local.json`; user config is +independent. Native startup migrates old mcpServers in main config files. UAP +must not initiate migration or write the obsolete main config layout. + +Native current MCP is nested mcpServers: command/args/env for stdio and +url/headers plus optional transport http/sse for remote. There is no documented +native cwd field; use the existing trusted launcher. Native MCP remove defaults +to local scope, so project removal uses exact file receipts and never invokes +that command. MCP list help exposes no --json flag. Skills list help does expose +--json, but no actual list was run. Skills can come from .devin, .windsurf, +.agents and configured/imported sources; configuration delivery alone cannot +prove effective names, permissions or selected Desktop harness behavior. + +## Implementation boundary + +Keep original user detection/channel selection and legacy Cascade mutation +unchanged. Require explicit selected primary Devin executable with exact +observed3000.11.205 version evidence for the new automatic project lane. +Legacy Windsurf primary, older/unknown/unobserved version or ambiguous selection +must not be reinterpreted as current Devin. The narrow host-version propagation +amendment is separately reviewed before code because the public installer +currently reconstructs a client without its observed Version and rejects an +absent original legacy profile even for validated project-only authority. + +Project package/state/data stay under .uap; native skills use .devin/skills and +MCP uses only .devin/mcp_config.json. Validate exact context/artifact/binding/ +receipt/data authority and reuse the existing reviewed no-follow native kernel +and component transaction. Refuse legacy main-config MCP requiring migration; +preserve local overrides and refuse same-name local shadowing rather than +changing policy. Other imported/user/ancestor/plugin policy remains unobserved. +Prepared+installed files cannot become active via generic attestation. Native +project discovery/runtime and Desktop UI qualification remain separate gates. diff --git a/install/integrationctl/adapters/atomicfile/publication_windows_test.go b/install/integrationctl/adapters/atomicfile/publication_windows_test.go new file mode 100644 index 000000000..19e99ad80 --- /dev/null +++ b/install/integrationctl/adapters/atomicfile/publication_windows_test.go @@ -0,0 +1,175 @@ +//go:build windows + +package atomicfile + +import ( + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestWindowsAtomicPublicationHeldReaders(t *testing.T) { + t.Run("ordinary_reader_remains_a_refusal", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "state.json") + if err := Write(path, []byte("old complete bytes"), 0600); err != nil { + t.Fatal(err) + } + reader, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + closePublicationReader(t, reader) + if err := Write(path, []byte("new complete bytes"), 0600); err == nil { + t.Fatal("publication bypassed an ordinary reader's delete denial") + } + requirePublicationBody(t, path, "old complete bytes") + requirePublicationReader(t, reader, "old complete bytes") + requireNoPublicationTemps(t, filepath.Dir(path)) + }) + t.Run("production_reader_keeps_old_file", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "state.json") + if err := Write(path, []byte("old complete bytes"), 0600); err != nil { + t.Fatal(err) + } + reader, err := openReadFile(path) + if err != nil { + t.Fatal(err) + } + closePublicationReader(t, reader) + legacy := filepath.Join(filepath.Dir(path), "legacy-source") + if err := os.WriteFile(legacy, []byte("legacy candidate"), 0600); err != nil { + t.Fatal(err) + } + if err := os.Rename(legacy, path); err == nil { + t.Fatal("legacy rename unexpectedly replaced a held destination") + } else { + t.Logf("controlled legacy replacement refusal: %v", err) + } + requirePublicationBody(t, path, "old complete bytes") + if err := os.Remove(legacy); err != nil { + t.Fatal(err) + } + if err := Write(path, []byte("new complete bytes"), 0600); err != nil { + t.Fatal(err) + } + requirePublicationReader(t, reader, "old complete bytes") + requirePublicationBody(t, path, "new complete bytes") + requireNoPublicationTemps(t, filepath.Dir(path)) + }) +} + +func TestWindowsAtomicPublicationRefusals(t *testing.T) { + root := t.TempDir() + for _, path := range []string{filepath.Join(root, "missing"), filepath.Join(root, "missing-parent", "file")} { + _, err := ReadFile(path) + if !os.IsNotExist(err) || !errors.Is(err, os.ErrNotExist) { + t.Fatalf("missing-read compatibility changed: %T %v", err, err) + } + } + readonly := filepath.Join(root, "readonly") + if err := Write(readonly, []byte("protected bytes"), 0600); err != nil { + t.Fatal(err) + } + if err := os.Chmod(readonly, 0400); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Chmod(readonly, 0600); err != nil { + t.Error(err) + } + }) + if err := Write(readonly, []byte("must not replace"), 0600); err == nil { + t.Fatal("publication bypassed the target's readonly attribute") + } + requirePublicationBody(t, readonly, "protected bytes") + directory := filepath.Join(root, "directory") + if err := os.Mkdir(directory, 0700); err != nil { + t.Fatal(err) + } + if err := Write(directory, []byte("must not replace"), 0600); err == nil { + t.Fatal("directory target was accepted") + } + info, err := os.Stat(directory) + if err != nil || !info.IsDir() { + t.Fatal("directory target changed after refusal") + } + source := filepath.Join(root, "source") + if err := os.WriteFile(source, []byte("source remains"), 0600); err != nil { + t.Fatal(err) + } + if err := Replace(source, filepath.Join(directory, "outside-parent")); err == nil { + t.Fatal("publication escaped the source parent") + } + requirePublicationBody(t, source, "source remains") + requireNoPublicationTemps(t, root) +} + +func TestWindowsAtomicPublicationUnicodeLongPath(t *testing.T) { + root := t.TempDir() + t.Chdir(root) + parent := filepath.Join(strings.Repeat("long-parent-", 10), strings.Repeat("second-parent-", 9), "unicode-é-插件") + if err := os.MkdirAll(parent, 0700); err != nil { + t.Fatal(err) + } + path := filepath.Join(parent, "状態.json") + absolute, err := filepath.Abs(path) + if err != nil || len(absolute) <= 260 { + t.Fatal("fixture did not establish a long absolute path") + } + if err := Write(path, []byte("old unicode bytes"), 0600); err != nil { + t.Fatal(err) + } + reader, err := openReadFile(path) + if err != nil { + t.Fatal(err) + } + closePublicationReader(t, reader) + if err := Write(path, []byte("new unicode bytes"), 0600); err != nil { + t.Fatal(err) + } + requirePublicationReader(t, reader, "old unicode bytes") + requirePublicationBody(t, path, "new unicode bytes") + requirePublicationBody(t, absolute, "new unicode bytes") + requireNoPublicationTemps(t, parent) +} + +func closePublicationReader(t *testing.T, reader *os.File) { + t.Helper() + t.Cleanup(func() { + if err := reader.Close(); err != nil { + t.Error(err) + } + }) +} + +func requirePublicationReader(t *testing.T, reader *os.File, want string) { + t.Helper() + body, err := io.ReadAll(reader) + if err != nil || string(body) != want { + t.Fatalf("retained reader lost complete old bytes: %q %v", body, err) + } +} + +func requirePublicationBody(t *testing.T, path, want string) { + t.Helper() + body, err := ReadFile(path) + if err != nil || string(body) != want { + t.Fatalf("named file is not the complete expected version: %q %v", body, err) + } +} + +func requireNoPublicationTemps(t *testing.T, parent string) { + t.Helper() + entries, err := os.ReadDir(parent) + if err != nil { + t.Fatal(err) + } + for _, entry := range entries { + if strings.Contains(entry.Name(), ".tmp-") { + t.Fatal("publication left a temporary file") + } + } +} diff --git a/install/integrationctl/adapters/atomicfile/read_other.go b/install/integrationctl/adapters/atomicfile/read_other.go new file mode 100644 index 000000000..c37463f3c --- /dev/null +++ b/install/integrationctl/adapters/atomicfile/read_other.go @@ -0,0 +1,8 @@ +//go:build !windows + +package atomicfile + +import "os" + +// ReadFile reads an atomically published file. +func ReadFile(path string) ([]byte, error) { return os.ReadFile(path) } diff --git a/install/integrationctl/adapters/atomicfile/read_windows.go b/install/integrationctl/adapters/atomicfile/read_windows.go new file mode 100644 index 000000000..8b9abe46a --- /dev/null +++ b/install/integrationctl/adapters/atomicfile/read_windows.go @@ -0,0 +1,43 @@ +//go:build windows + +package atomicfile + +import ( + "errors" + "io" + "os" + "path/filepath" +) + +// ReadFile permits atomic replacement while this reader retains its old file. +func ReadFile(path string) ([]byte, error) { + file, err := openReadFile(path) + if err != nil { + return nil, err + } + body, err := io.ReadAll(file) + if closeErr := file.Close(); closeErr != nil { + return body, errors.Join(err, closeErr) + } + return body, err +} + +func openReadFile(path string) (*os.File, error) { + absolute, err := filepath.Abs(path) + if err != nil { + return nil, err + } + root, err := os.OpenRoot(filepath.Dir(absolute)) + if err != nil { + return nil, err + } + file, err := root.Open(filepath.Base(absolute)) + if closeErr := root.Close(); closeErr != nil { + if file != nil { + return nil, errors.Join(err, closeErr, file.Close()) + } + return nil, errors.Join(err, closeErr) + } + // Preserve the original PathError for legacy os.IsNotExist callers. + return file, err +} diff --git a/install/integrationctl/adapters/atomicfile/replace_other.go b/install/integrationctl/adapters/atomicfile/replace_other.go new file mode 100644 index 000000000..834c79a37 --- /dev/null +++ b/install/integrationctl/adapters/atomicfile/replace_other.go @@ -0,0 +1,9 @@ +//go:build !windows + +package atomicfile + +import "os" + +// Replace publishes an already synced and closed temporary file. Callers own +// cleanup and provide source and target names in the same directory. +func Replace(source, target string) error { return os.Rename(source, target) } diff --git a/install/integrationctl/adapters/atomicfile/replace_windows.go b/install/integrationctl/adapters/atomicfile/replace_windows.go new file mode 100644 index 000000000..c9018630a --- /dev/null +++ b/install/integrationctl/adapters/atomicfile/replace_windows.go @@ -0,0 +1,36 @@ +//go:build windows + +package atomicfile + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +// Replace publishes an already synced and closed temporary file. Root.Rename +// supports delete-sharing readers without removing the target first. +func Replace(source, target string) error { + source, err := filepath.Abs(source) + if err != nil { + return err + } + target, err = filepath.Abs(target) + if err != nil { + return err + } + parent := filepath.Dir(source) + if parent != filepath.Dir(target) { + return fmt.Errorf("atomic file replacement requires the same parent directory") + } + root, err := os.OpenRoot(parent) + if err != nil { + return err + } + err = root.Rename(filepath.Base(source), filepath.Base(target)) + if closeErr := root.Close(); closeErr != nil { + return errors.Join(err, closeErr) + } + return err +} diff --git a/install/integrationctl/adapters/atomicfile/write.go b/install/integrationctl/adapters/atomicfile/write.go index 5679ceec3..63c0fe717 100644 --- a/install/integrationctl/adapters/atomicfile/write.go +++ b/install/integrationctl/adapters/atomicfile/write.go @@ -54,7 +54,7 @@ func Write(path string, data []byte, mode os.FileMode) error { cleanup() return fmt.Errorf("close atomic file temp: %w", err) } - if err := os.Rename(tempPath, path); err != nil { + if err := Replace(tempPath, path); err != nil { cleanup() return fmt.Errorf("replace atomic file target: %w", err) } diff --git a/install/integrationctl/adapters/dirswap/dirswap.go b/install/integrationctl/adapters/dirswap/dirswap.go index 83ed38003..a6d0de167 100644 --- a/install/integrationctl/adapters/dirswap/dirswap.go +++ b/install/integrationctl/adapters/dirswap/dirswap.go @@ -311,7 +311,7 @@ func (manager Manager) Load(operationID string) (Receipt, error) { if err := pathpolicy.ValidateLeafID(operationID); err != nil { return Receipt{}, fmt.Errorf("unsafe directory swap operation id: %w", err) } - body, err := os.ReadFile(manager.journalPath(operationID)) + body, err := atomicfile.ReadFile(manager.journalPath(operationID)) if err != nil { return Receipt{}, err } diff --git a/install/integrationctl/adapters/process/osrunner.go b/install/integrationctl/adapters/process/osrunner.go index 30900223a..1e0ca8124 100644 --- a/install/integrationctl/adapters/process/osrunner.go +++ b/install/integrationctl/adapters/process/osrunner.go @@ -709,7 +709,7 @@ func runExplicit(ctx context.Context, command ports.Command, treeExitGrace time. return capturedResult, withDuplexDiagnostic(errors.Join(ctxErr, capturedErr, stdoutErr), stderr.Bytes()) } if termination.forcedMembers { - return capturedResult, withDuplexDiagnostic(errors.Join(fmt.Errorf("process left live descendants that required forced cleanup"), capturedErr, stdoutErr), stderr.Bytes()) + return capturedResult, withDuplexDiagnostic(errors.Join(ErrOrdinaryForcedCleanup, capturedErr, stdoutErr), stderr.Bytes()) } if stdoutErr != nil { return capturedResult, explicitStdoutError(capturedResult, capturedErr, stdoutErr, waitErr) diff --git a/install/integrationctl/adapters/process/osrunner_cleanup_origin_cases_linux_test.go b/install/integrationctl/adapters/process/osrunner_cleanup_origin_cases_linux_test.go new file mode 100644 index 000000000..8ca290aac --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_cleanup_origin_cases_linux_test.go @@ -0,0 +1,133 @@ +//go:build linux + +package process + +import ( + "errors" + "fmt" + "os" + "reflect" + "syscall" + "testing" + "time" +) + +func TestOrdinaryCleanupMissingTaskIdentityKeepsUnknownAndGoneDistinct(t *testing.T) { + for _, present := range []bool{true, false} { + ops := linuxTaskProcOps{taskExists: func(string) (bool, error) { return present, nil }} + gone, err := linuxTaskGoneAfterIdentityError("/inert/task", os.ErrNotExist, ops) + if !present { + if !gone || err != nil { + t.Fatal("disappeared task changed its existing accepted outcome") + } + continue + } + want := fmt.Sprintf("existing proc task identity is unavailable: %s", os.ErrNotExist.Error()) + if gone || err == nil || err.Error() != want || linuxProcessGone(err) || ordinaryCleanupFailureReason(err) != "task_identity_unavailable" { + t.Fatal("missing identity in a present task changed its original refusal") + } + broader := withOrdinaryCleanupOrigin("task_children_observation", err) + if reflect.ValueOf(broader) != reflect.ValueOf(err) || ordinaryCleanupFailureReason(broader) != "task_identity_unavailable" { + t.Fatal("broader task annotation replaced the specific existing failure") + } + } +} + +func TestOrdinaryCleanupTraversalOriginsRetainOriginalRefusalsAndHandles(t *testing.T) { + readFailure := errors.New("inert identity observation failed") + for _, tc := range []struct { + name, reason, message string + openErr, childErr, parentErr error + changedStart, changedParent bool + expired, limit, gone, reparent bool + }{ + {name: "pidfd open", openErr: syscall.EPERM, reason: "pidfd_open", message: "open stable supervisor descendant handle for 41: operation not permitted"}, + {name: "pidfd identity read", childErr: readFailure, reason: "pidfd_identity_read", message: "revalidate stable supervisor descendant 41: inert identity observation failed"}, + {name: "pidfd identity changed", changedStart: true, reason: "pidfd_identity_changed", message: "revalidate stable supervisor descendant 41: process start time changed"}, + {name: "parent identity read", changedParent: true, parentErr: readFailure, reason: "parent_identity_read", message: "revalidate stable supervisor descendant 41 parent 7: inert identity observation failed"}, + {name: "live parent changed", changedParent: true, reason: "parent_changed", message: "revalidate stable supervisor descendant 41: live parent changed from 7 to 8"}, + {name: "traversal deadline", expired: true, reason: "traversal_deadline", message: "Linux supervisor descendant traversal exceeded cleanup deadline after 1 visit(s)"}, + {name: "traversal limit", limit: true, reason: "traversal_limit", message: "Linux supervisor descendant traversal exceeded 1-process cleanup bound"}, + {name: "identity gone", gone: true}, + {name: "already accepted exiting parent", changedParent: true, reparent: true}, + } { + t.Run(tc.name, func(t *testing.T) { + closed, opened := 0, 0 + ops := linuxDescendantTraversalOps{ + directChildren: func(pid int, _ time.Time, _ int) (map[int]uint64, error) { + if pid == 7 { + return map[int]uint64{41: 101}, nil + } + if pid != 41 { + t.Fatal("traversed an unrelated fixture identity") + } + return nil, nil + }, + readIdentity: func(pid int) (linuxProcessIdentity, error) { + if pid == 7 { + return linuxProcessIdentity{pid: 7, ppid: 1, startTime: 11, zombie: tc.reparent}, tc.parentErr + } + if pid != 41 { + t.Fatal("read an unrelated fixture identity") + } + if tc.gone { + return linuxProcessIdentity{}, os.ErrNotExist + } + identity := linuxProcessIdentity{pid: 41, ppid: 7, startTime: 101} + if tc.changedStart { + identity.startTime++ + } + if tc.changedParent { + identity.ppid = 8 + } + return identity, tc.childErr + }, + openPidfd: func(pid, flags int) (int, error) { + if pid != 41 || flags != 0 { + t.Fatal("changed fixture pidfd acquisition") + } + if tc.openErr != nil { + return -1, tc.openErr + } + opened++ + return 91, nil + }, + closePidfd: func(fd int) error { + if fd != 91 { + t.Fatal("changed fixture handle authority") + } + closed++ + return nil + }, + } + deadline, visits := time.Now().Add(time.Second), 4 + if tc.expired { + deadline = time.Now().Add(-time.Second) + } + if tc.limit { + visits = 1 + } + result, err := linuxDescendantIdentitiesWith(7, deadline, visits, ops) + if tc.reason != "" { + if err == nil || err.Error() != tc.message || ordinaryCleanupFailureReason(err) != tc.reason || len(result) != 0 || closed != opened { + t.Fatal("origin changed original failure/message or leaked a fixture handle") + } + if tc.openErr != nil && !errors.Is(err, tc.openErr) || tc.childErr != nil && !errors.Is(err, tc.childErr) || tc.parentErr != nil && !errors.Is(err, tc.parentErr) { + t.Fatal("origin lost original wrapped failure") + } + return + } + if err != nil || ordinaryCleanupFailureReason(err) != "" { + t.Fatal("gone/accepted reparent outcome changed") + } + for _, entry := range result { + if err := ops.closePidfd(entry.process.pidfd); err != nil { + t.Fatal(err) + } + } + if closed != opened { + t.Fatal("fixture handle ownership changed") + } + }) + } +} diff --git a/install/integrationctl/adapters/process/osrunner_cleanup_origin_linux.go b/install/integrationctl/adapters/process/osrunner_cleanup_origin_linux.go new file mode 100644 index 000000000..6c72f9bd8 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_cleanup_origin_linux.go @@ -0,0 +1,42 @@ +//go:build linux + +package process + +import "errors" + +type ordinaryCleanupOriginFailure struct { + cause error + reason string +} + +func (failure *ordinaryCleanupOriginFailure) Error() string { return failure.cause.Error() } +func (failure *ordinaryCleanupOriginFailure) Unwrap() error { return failure.cause } + +// Metadata never changes disappearance/unsupported decisions or replaces an +// already-owned specific origin with a broader contextual origin. +func withOrdinaryCleanupOrigin(reason string, cause error) error { + if cause == nil || ordinarySupervisorCleanupReason(reason) == "" || ordinaryCleanupFailureReason(cause) != "" { + return cause + } + if linuxProcessGone(cause) || errors.Is(cause, ErrOrdinaryContainmentUnsupported) { + return cause + } + return &ordinaryCleanupOriginFailure{cause: cause, reason: reason} +} + +func ordinaryCleanupFailureReason(err error) string { + var failure *ordinaryCleanupOriginFailure + if errors.As(err, &failure) && failure != nil { + return ordinarySupervisorCleanupReason(failure.reason) + } + return "" +} + +func setOrdinarySupervisorCleanupFailure(result *linuxOrdinarySupervisorResult, err error) { + if err == nil { + return + } + result.Error = err.Error() + result.FailureStage = ordinarySupervisorDescendantCleanup + result.FailureReason = ordinaryCleanupFailureReason(err) +} diff --git a/install/integrationctl/adapters/process/osrunner_cleanup_origin_linux_test.go b/install/integrationctl/adapters/process/osrunner_cleanup_origin_linux_test.go new file mode 100644 index 000000000..41646b13b --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_cleanup_origin_linux_test.go @@ -0,0 +1,108 @@ +//go:build linux + +package process + +import ( + "encoding/json" + "errors" + "os" + "reflect" + "testing" +) + +func TestOrdinaryCleanupOriginPreservesExactCauseAndFirstJoinedReason(t *testing.T) { + original := errors.New("exact original cleanup failure") + for _, reason := range cleanupReasonFixtures { + failure := withOrdinaryCleanupOrigin(reason, original) + if failure.Error() != original.Error() || reflect.ValueOf(errors.Unwrap(failure)) != reflect.ValueOf(original) || !errors.Is(failure, original) || ordinaryCleanupFailureReason(failure) != reason { + t.Fatal("origin did not preserve exact message/cause/identity") + } + } + first := withOrdinaryCleanupOrigin("parent_changed", supervisorDiagnosticTextTrap{}) + joined := errors.Join(errors.New("earlier unclassified cause"), first, withOrdinaryCleanupOrigin("pidfd_open", original)) + if got := withOrdinaryCleanupOrigin("task_children_observation", joined); reflect.ValueOf(got) != reflect.ValueOf(joined) || ordinaryCleanupFailureReason(got) != "parent_changed" { + t.Fatal("broader annotation replaced first recognized joined origin") + } + for _, cause := range []error{nil, os.ErrNotExist, ErrOrdinaryContainmentUnsupported} { + if got := withOrdinaryCleanupOrigin("task_directory_read", cause); reflect.ValueOf(got) != reflect.ValueOf(cause) || ordinaryCleanupFailureReason(got) != "" { + t.Fatal("nil/gone/unsupported outcome changed") + } + } + trap := &supervisorDiagnosticTextTrap{} + if got := withOrdinaryCleanupOrigin("unknown secret reason", trap); reflect.ValueOf(got) != reflect.ValueOf(trap) || ordinaryCleanupFailureReason(got) != "" { + t.Fatal("unknown origin gained metadata or read error text") + } +} + +func TestOrdinaryCleanupProducerReceiverPreservesEveryReasonAndFact(t *testing.T) { + for _, reason := range cleanupReasonFixtures { + t.Run(reason, func(t *testing.T) { + const message = "original cleanup message\nsecond line" + for _, forced := range []bool{false, true} { + wire := linuxOrdinarySupervisorResult{Forced: forced} + setOrdinarySupervisorCleanupFailure(&wire, withOrdinaryCleanupOrigin(reason, errors.New(message))) + if wire.Error != message || wire.FailureStage != ordinarySupervisorDescendantCleanup || wire.FailureReason != reason { + t.Fatal("producer changed message/stage/reason") + } + body, err := json.Marshal(wire) + if err != nil { + t.Fatal(err) + } + containment := supervisorStatusFixture(t, body) + result := containment.terminateSupervisor() + if result.err == nil || result.err.Error() != message || OrdinarySupervisorFailureClass(result.err) != "ordinary_supervisor_descendant_cleanup_"+reason { + t.Fatal("strict wire lost exact cleanup cause/category") + } + if !result.leaderStopped || result.leaderTerminationInitiated || result.forcedMembers != forced || containment.supervisorStatus != nil || containment.supervisorRequest != nil { + t.Fatal("metadata changed termination facts or descriptor authority") + } + } + }) + } + unchanged := linuxOrdinarySupervisorResult{Forced: true, Error: "retained", FailureStage: "retained"} + before := unchanged + setOrdinarySupervisorCleanupFailure(&unchanged, nil) + if unchanged != before { + t.Fatal("nil cleanup changed existing result") + } +} + +func TestOrdinaryCleanupReasonWireRejectsContradictionsAndUnadmittedRecords(t *testing.T) { + for _, wire := range []linuxOrdinarySupervisorResult{ + {FailureReason: "parent_changed"}, + {FailureStage: ordinarySupervisorDescendantCleanup, FailureReason: "parent_changed"}, + {Error: "original", FailureReason: "parent_changed"}, + {Error: "original", FailureStage: ordinarySupervisorPreflight, FailureReason: "parent_changed"}, + {Error: "original", FailureStage: "unknown stage", FailureReason: "parent_changed"}, + } { + body, err := json.Marshal(wire) + if err != nil { + t.Fatal(err) + } + result := supervisorStatusFixture(t, body).terminateSupervisor() + if result.err == nil || OrdinarySupervisorFailureClass(result.err) != "" { + t.Fatal("contradictory cleanup metadata established a class or success") + } + } + for _, reason := range []string{"", "unknown secret reason"} { + body, err := json.Marshal(linuxOrdinarySupervisorResult{Error: "original", FailureStage: ordinarySupervisorDescendantCleanup, FailureReason: reason}) + if err != nil { + t.Fatal(err) + } + result := supervisorStatusFixture(t, body).terminateSupervisor() + if result.err == nil || result.err.Error() != "original" || OrdinarySupervisorFailureClass(result.err) != "ordinary_supervisor_descendant_cleanup" { + t.Fatal("legacy/unknown reason lost generic cleanup failure") + } + } + for _, body := range []string{ + `{"Error":"original","FailureStage":"descendant_cleanup","FailureReason":"parent_changed","Unknown":true}`, + `{"Error":"original","FailureStage":"descendant_cleanup","FailureReason":"parent_changed"}{}`, + `{"Error":"original","FailureStage":"descendant_cleanup","FailureReason":42}`, + `{"Error":"original","FailureStage":"descendant_cleanup","FailureReason":"parent_changed"`, + } { + result := supervisorStatusFixture(t, []byte(body)).terminateSupervisor() + if result.err == nil || OrdinarySupervisorFailureClass(result.err) != "" { + t.Fatal("unadmitted record established cleanup class/reason") + } + } +} diff --git a/install/integrationctl/adapters/process/osrunner_cleanup_reason_test.go b/install/integrationctl/adapters/process/osrunner_cleanup_reason_test.go new file mode 100644 index 000000000..03700da21 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_cleanup_reason_test.go @@ -0,0 +1,64 @@ +package process + +import ( + "errors" + "reflect" + "testing" +) + +var cleanupReasonFixtures = []string{ + "exit_observation", "task_directory_read", "task_identity_unavailable", "task_children_observation", + "child_identity_read", "child_identity_changed", "pidfd_open", "pidfd_identity_read", "pidfd_identity_changed", + "parent_identity_read", "parent_changed", "tracked_identity_read", "tracked_identity_changed", + "traversal_deadline", "traversal_limit", "descendant_signal", "cleanup_deadline", +} + +func TestOrdinaryCleanupReasonClassIsClosedAndNeverReadsError(t *testing.T) { + for _, reason := range cleanupReasonFixtures { + t.Run(reason, func(t *testing.T) { + cause := &supervisorDiagnosticTextTrap{} + failure := &ordinarySupervisorFailure{cause: cause, stage: ordinarySupervisorDescendantCleanup, reason: reason} + joined := errors.Join(errors.New("earlier cause"), failure) + if got := OrdinarySupervisorFailureClass(joined); got != "ordinary_supervisor_descendant_cleanup_"+reason { + t.Fatalf("cleanup class = %q", got) + } + if reflect.ValueOf(errors.Unwrap(failure)) != reflect.ValueOf(cause) || !errors.Is(joined, cause) { + t.Fatal("reason metadata changed original cause identity") + } + }) + } + for _, tc := range []struct{ stage, reason, want string }{ + {ordinarySupervisorDescendantCleanup, "", "ordinary_supervisor_descendant_cleanup"}, + {ordinarySupervisorDescendantCleanup, "secret unknown reason", "ordinary_supervisor_descendant_cleanup"}, + {ordinarySupervisorPreflight, "parent_changed", ""}, + {"", "parent_changed", ""}, + } { + failure := &ordinarySupervisorFailure{cause: supervisorDiagnosticTextTrap{}, stage: tc.stage, reason: tc.reason} + if got := OrdinarySupervisorFailureClass(failure); got != tc.want { + t.Fatalf("invalid/legacy reason class = %q, want %q", got, tc.want) + } + } +} + +func TestOrdinaryCleanupReasonConstructorPreservesExactMessage(t *testing.T) { + const message = "original supervised failure\nsecond line" + for _, reason := range append(append([]string{}, cleanupReasonFixtures...), "", "unknown reason") { + failure := ordinarySupervisorErrorWithReason(ordinarySupervisorDescendantCleanup, reason, message) + if failure.Error() != message || errors.Unwrap(failure).Error() != message { + t.Fatal("reason changed original error string") + } + want := "ordinary_supervisor_descendant_cleanup" + if ordinarySupervisorCleanupReason(reason) != "" { + want += "_" + reason + } + if got := OrdinarySupervisorFailureClass(failure); got != want { + t.Fatalf("constructed class = %q, want %q", got, want) + } + } + for _, stage := range []string{ordinarySupervisorPreflight, "", "unknown stage"} { + failure := ordinarySupervisorErrorWithReason(stage, "parent_changed", message) + if failure.Error() != message || OrdinarySupervisorFailureClass(failure) != "" { + t.Fatal("contradictory stage/reason established diagnostic authority") + } + } +} diff --git a/install/integrationctl/adapters/process/osrunner_git_cleanup_diagnostic_linux_test.go b/install/integrationctl/adapters/process/osrunner_git_cleanup_diagnostic_linux_test.go new file mode 100644 index 000000000..f579c6b68 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_git_cleanup_diagnostic_linux_test.go @@ -0,0 +1,147 @@ +//go:build linux + +package process + +import ( + "context" + "encoding/hex" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +const localGitCleanupIterations = 512 + +// This bounded observation runs only on the explicit diagnostic workflow. +// Passing the sample does not explain a previously observed cleanup failure. +func TestOSRunnerLocalGitFetchCleanupObservation(t *testing.T) { + if os.Getenv("AGENTPLUGINS_GIT_CLEANUP_DIAGNOSTIC") != "1" { + t.Skip("opt-in bounded local Git cleanup observation") + } + if err := (OS{}).OrdinaryPreflight(); err != nil { + t.Fatal("local Git cleanup observation requires the real ordinary containment prerequisite") + } + f := newLocalGitCleanupFixture(t) + source, revision := f.source(t) + for iteration := range localGitCleanupIterations { + f.fetch(t, source, revision, iteration) + } + t.Logf("local Git cleanup observation completed iterations=%d", localGitCleanupIterations) +} + +type localGitCleanupFixture struct { + ctx context.Context + root, git, template string + environment []string + options []string +} + +func newLocalGitCleanupFixture(t *testing.T) localGitCleanupFixture { + t.Helper() + git, err := exec.LookPath("git") + if err != nil || !filepath.IsAbs(git) { + t.Fatal("local Git cleanup observation requires a located absolute Git executable") + } + root := t.TempDir() + home, temporary := filepath.Join(root, "home"), filepath.Join(root, "tmp") + template, hooks := filepath.Join(root, "template"), filepath.Join(root, "hooks") + for _, directory := range []string{home, filepath.Join(home, ".config"), temporary, template, hooks} { + if err := os.MkdirAll(directory, 0700); err != nil { + t.Fatal("create private local Git fixture directory") + } + } + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute) + t.Cleanup(cancel) + return localGitCleanupFixture{ + ctx: ctx, root: root, git: git, template: template, + environment: []string{ + "PATH=" + os.Getenv("PATH"), "HOME=" + home, "USERPROFILE=" + home, + "XDG_CONFIG_HOME=" + filepath.Join(home, ".config"), "TMPDIR=" + temporary, "TMP=" + temporary, "TEMP=" + temporary, + "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=" + os.DevNull, "GIT_TERMINAL_PROMPT=0", + "GIT_ALLOW_PROTOCOL=file", "GIT_LFS_SKIP_SMUDGE=1", "GCM_INTERACTIVE=Never", "SSH_ASKPASS_REQUIRE=never", + "GIT_AUTHOR_NAME=fixture", "GIT_AUTHOR_EMAIL=fixture@example.invalid", + "GIT_COMMITTER_NAME=fixture", "GIT_COMMITTER_EMAIL=fixture@example.invalid", "LC_ALL=C", "LANG=C", + }, + options: []string{ + "-c", "core.hooksPath=" + hooks, "-c", "core.fsmonitor=false", "-c", "maintenance.auto=false", "-c", "gc.auto=0", + "-c", "fetch.recurseSubmodules=false", "-c", "submodule.recurse=false", "-c", "protocol.allow=never", + "-c", "protocol.file.allow=always", "-c", "credential.helper=", "-c", "commit.gpgsign=false", + }, + } +} + +func (f localGitCleanupFixture) source(t *testing.T) (string, string) { + t.Helper() + root := filepath.Join(f.root, "source") + f.run(t, "source_init", -1, "init", "--quiet", "--initial-branch=main", "--template="+f.template, root) + for index := range 8 { + body := strings.Repeat(fmt.Sprintf("fixed local Git cleanup payload %d\n", index), 128) + if err := os.WriteFile(filepath.Join(root, fmt.Sprintf("payload-%02d.txt", index)), []byte(body), 0600); err != nil { + t.Fatal("write fixed local Git cleanup payload") + } + } + f.run(t, "source_add", -1, "-C", root, "add", ".") + f.run(t, "source_commit", -1, "-C", root, "commit", "--quiet", "-m", "fixed local cleanup fixture") + revision := strings.TrimSpace(string(f.run(t, "source_revision", -1, "-C", root, "rev-parse", "--verify", "HEAD^{commit}"))) + decoded, err := hex.DecodeString(revision) + if err != nil || len(decoded) != 20 || revision != strings.ToLower(revision) { + t.Fatal("local Git fixture did not produce an immutable SHA-1 commit") + } + return root, revision +} + +func (f localGitCleanupFixture) fetch(t *testing.T, source, revision string, iteration int) { + t.Helper() + destination := filepath.Join(f.root, fmt.Sprintf("fetch-%03d", iteration)) + f.run(t, "destination_init", iteration, "init", "--quiet", "--template="+f.template, destination) + f.run(t, "remote", iteration, "-C", destination, "remote", "add", "origin", source) + for _, setting := range [][]string{{"core.autocrlf", "false"}, {"core.filemode", "true"}, {"core.symlinks", "true"}, {"fetch.recurseSubmodules", "false"}} { + f.run(t, "settings", iteration, "-C", destination, "config", setting[0], setting[1]) + } + // The positively classified terminal fixture uses direct AcquireGit: + // native=true and this exact fetch graph, without the anonymous filter flag. + f.run(t, "fetch", iteration, "-C", destination, "fetch", "--quiet", "--depth=1", "--no-tags", "--no-recurse-submodules", "origin", revision) + got := f.run(t, "verify", iteration, "-C", destination, "rev-parse", "--verify", "FETCH_HEAD^{commit}") + if strings.TrimSpace(string(got)) != revision { + t.Fatalf("local Git fetched revision mismatch iteration=%d", iteration) + } + if err := os.RemoveAll(destination); err != nil { + t.Fatalf("remove completed private Git destination iteration=%d", iteration) + } +} + +func (f localGitCleanupFixture) run(t *testing.T, phase string, iteration int, arguments ...string) []byte { + t.Helper() + ctx, cancel := context.WithTimeout(f.ctx, 15*time.Second) + defer cancel() + argv := append([]string{f.git}, f.options...) + argv = append(argv, arguments...) + result, err := (OS{}).RunWithTreeExitGrace(ctx, ports.Command{ + Argv: argv, Env: f.environment, Dir: f.root, StdoutLimitBytes: 4096, + }, 5*time.Second) + if err != nil { + class := OrdinarySupervisorFailureClass(err) + const cleanup = "ordinary_supervisor_descendant_cleanup" + if class == cleanup || strings.HasPrefix(class, cleanup+"_") { + // This is only the real process error from the fixed local fixture, + // with no inherited Git/auth/network inputs. Quote before applying the + // output cap so escaping cannot expand the emitted cause past 4 KiB. + cause := fmt.Sprintf("%q", err.Error()) + if len(cause) > 4096 { + cause = cause[:4096] + } + t.Fatalf("local Git cleanup phase=%s iteration=%d class=%s exit_status=unavailable cause=%s", phase, iteration, class, cause) + } + t.Fatalf("local Git cleanup phase=%s iteration=%d class=%s exit_status=unavailable", phase, iteration, class) + } + if result.ExitCode != 0 { + t.Fatalf("local Git cleanup phase=%s iteration=%d class=git_exit exit_status=%d", phase, iteration, result.ExitCode) + } + return result.Stdout +} diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_capability.go b/install/integrationctl/adapters/process/osrunner_ordinary_capability.go new file mode 100644 index 000000000..5c719db8f --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_capability.go @@ -0,0 +1,12 @@ +package process + +import "errors" + +// ErrOrdinaryContainmentUnsupported identifies a missing observation primitive +// for a positively live task. It is not a generic execution or permission error. +var ErrOrdinaryContainmentUnsupported = errors.New("ordinary containment proc prerequisite is unavailable") + +// OrdinaryPreflight checks process-inert launch prerequisites. On Linux this +// requires its proc children interface; success does not prove future shutdown. +// Other platforms retain their existing constructor and cleanup checks. +func (OS) OrdinaryPreflight() error { return ordinaryContainmentPreflight() } diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_capability_other.go b/install/integrationctl/adapters/process/osrunner_ordinary_capability_other.go new file mode 100644 index 000000000..c30b20220 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_capability_other.go @@ -0,0 +1,5 @@ +//go:build !linux + +package process + +func ordinaryContainmentPreflight() error { return nil } diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_capability_test.go b/install/integrationctl/adapters/process/osrunner_ordinary_capability_test.go new file mode 100644 index 000000000..6c85a80f8 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_capability_test.go @@ -0,0 +1,16 @@ +package process + +import ( + "errors" + "testing" +) + +func requireOrdinaryPreflight(t *testing.T) { + t.Helper() + if err := (OS{}).OrdinaryPreflight(); err != nil { + if errors.Is(err, ErrOrdinaryContainmentUnsupported) { + t.Skipf("ordinary containment runtime is not qualified on this host: %v", err) + } + t.Fatalf("ordinary containment preflight failed: %v", err) + } +} diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_failure.go b/install/integrationctl/adapters/process/osrunner_ordinary_failure.go new file mode 100644 index 000000000..9148b250b --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_failure.go @@ -0,0 +1,92 @@ +package process + +import "errors" + +// ErrOrdinaryForcedCleanup identifies an ordinary command whose live descendants +// required forced cleanup. The command remains failed even after successful reap. +var ErrOrdinaryForcedCleanup = errors.New("process left live descendants that required forced cleanup") + +const ( + ordinarySupervisorRequestDecode = "request_decode" + ordinarySupervisorSubreaperSetup = "subreaper_setup" + ordinarySupervisorPreflight = "preflight" + ordinarySupervisorCommandStart = "command_start" + ordinarySupervisorDescendantCleanup = "descendant_cleanup" +) + +type ordinarySupervisorFailure struct { + cause error + stage string + reason string +} + +func (failure *ordinarySupervisorFailure) Error() string { return failure.cause.Error() } +func (failure *ordinarySupervisorFailure) Unwrap() error { return failure.cause } + +// OrdinarySupervisorFailureClass returns a fixed category for a failure reported +// by the isolated Linux ordinary-command supervisor. It never reads error text, +// command arguments or streams. Unrelated errors on every platform return "". +func OrdinarySupervisorFailureClass(err error) string { + var failure *ordinarySupervisorFailure + if errors.As(err, &failure) && failure != nil { + if failure.reason != "" && failure.stage != ordinarySupervisorDescendantCleanup { + return "" + } + class := ordinarySupervisorStageClass(failure.stage) + if failure.stage == ordinarySupervisorDescendantCleanup { + if reason := ordinarySupervisorCleanupReason(failure.reason); reason != "" { + return class + "_" + reason + } + } + return class + } + return "" +} + +func ordinarySupervisorStageClass(stage string) string { + switch stage { + case ordinarySupervisorRequestDecode: + return "ordinary_supervisor_request_decode" + case ordinarySupervisorSubreaperSetup: + return "ordinary_supervisor_subreaper_setup" + case ordinarySupervisorPreflight: + return "ordinary_supervisor_preflight" + case ordinarySupervisorCommandStart: + return "ordinary_supervisor_command_start" + case ordinarySupervisorDescendantCleanup: + return "ordinary_supervisor_descendant_cleanup" + default: + return "" + } +} + +func ordinarySupervisorError(stage, message string) error { + cause := errors.New(message) + if ordinarySupervisorStageClass(stage) == "" { + return cause + } + return &ordinarySupervisorFailure{cause: cause, stage: stage} +} + +func ordinarySupervisorErrorWithReason(stage, reason, message string) error { + if reason == "" { + return ordinarySupervisorError(stage, message) + } + cause := errors.New(message) + if stage != ordinarySupervisorDescendantCleanup { + return cause + } + return &ordinarySupervisorFailure{cause: cause, stage: stage, reason: ordinarySupervisorCleanupReason(reason)} +} + +func ordinarySupervisorCleanupReason(reason string) string { + switch reason { + case "exit_observation", "task_directory_read", "task_identity_unavailable", "task_children_observation", + "child_identity_read", "child_identity_changed", "pidfd_open", "pidfd_identity_read", "pidfd_identity_changed", + "parent_identity_read", "parent_changed", "tracked_identity_read", "tracked_identity_changed", + "traversal_deadline", "traversal_limit", "descendant_signal", "cleanup_deadline": + return reason + default: + return "" + } +} diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_failure_linux_test.go b/install/integrationctl/adapters/process/osrunner_ordinary_failure_linux_test.go new file mode 100644 index 000000000..e1e3f1d3f --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_failure_linux_test.go @@ -0,0 +1,114 @@ +//go:build linux + +package process + +import ( + "encoding/json" + "errors" + "io" + "os" + "strings" + "testing" +) + +func supervisorStatusFixture(t *testing.T, body []byte) *commandContainment { + t.Helper() + status, statusWriter, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = status.Close() }) + if _, err := statusWriter.Write(body); err != nil { + _ = statusWriter.Close() + t.Fatal(err) + } + if err := statusWriter.Close(); err != nil { + t.Fatal(err) + } + request, requestWriter, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = request.Close(); _ = requestWriter.Close() }) + return &commandContainment{ + supervisorStatus: status, supervisorRequest: requestWriter, + observeExit: func() (bool, error) { return true, nil }, + } +} + +func TestOrdinarySupervisorFailureStatusPreservesMessageAndClass(t *testing.T) { + for _, test := range []struct{ stage, class string }{ + {ordinarySupervisorRequestDecode, "ordinary_supervisor_request_decode"}, + {ordinarySupervisorSubreaperSetup, "ordinary_supervisor_subreaper_setup"}, + {ordinarySupervisorPreflight, "ordinary_supervisor_preflight"}, + {ordinarySupervisorCommandStart, "ordinary_supervisor_command_start"}, + {ordinarySupervisorDescendantCleanup, "ordinary_supervisor_descendant_cleanup"}, + {"", ""}, + {"future_stage", ""}, + } { + t.Run(test.stage, func(t *testing.T) { + const message = "original failure: argument and stream content\nsecond line" + for _, forced := range []bool{false, true} { + body, err := json.Marshal(linuxOrdinarySupervisorResult{Forced: forced, Error: message, FailureStage: test.stage}) + if err != nil { + t.Fatal(err) + } + containment := supervisorStatusFixture(t, body) + result := containment.terminateSupervisor() + if result.err == nil || result.err.Error() != message { + t.Fatalf("wire failure did not preserve exact message: %v", result.err) + } + if got := OrdinarySupervisorFailureClass(result.err); got != test.class { + t.Fatalf("wire class = %q, want %q", got, test.class) + } + if !result.leaderStopped || result.leaderTerminationInitiated || result.forcedMembers != forced { + t.Fatalf("diagnostic changed termination facts: %+v", result) + } + if !containment.supervisorRead || containment.supervisorStatus != nil || containment.supervisorRequest != nil { + t.Fatal("status receiver retained descriptor authority") + } + } + }) + } +} + +func TestOrdinarySupervisorFailureStageCannotAdmitSuccess(t *testing.T) { + for _, stage := range []string{ordinarySupervisorPreflight, "future_stage"} { + body, err := json.Marshal(linuxOrdinarySupervisorResult{FailureStage: stage}) + if err != nil { + t.Fatal(err) + } + result := supervisorStatusFixture(t, body).terminateSupervisor() + if result.err == nil || !strings.Contains(result.err.Error(), "failure stage without an error") { + t.Fatalf("contradictory status admitted: %v", result.err) + } + if got := OrdinarySupervisorFailureClass(result.err); got != "" { + t.Fatalf("contradiction invented failure class %q", got) + } + } + for _, body := range []string{`{"Forced":false}`, `{"Forced":false,"Error":""}`, `{"Forced":false,"Error":"","FailureStage":""}`} { + if result := supervisorStatusFixture(t, []byte(body)).terminateSupervisor(); result.err != nil { + t.Fatalf("unchanged success status failed: %v", result.err) + } + } +} + +func TestOrdinarySupervisorFailureStageRetainsStrictWireAdmission(t *testing.T) { + for _, body := range []string{ + "", + `{"Forced":false,"FailureStage":"preflight","Unexpected":true}`, + `{"Forced":false,"Error":"original","FailureStage":"preflight","Unexpected":true}`, + `{"Forced":false}{"Forced":false}`, + `{"Forced":false,"Error":"original","FailureStage":"preflight"}{"Forced":false}`, + `{"Forced":false,"FailureStage":42}`, + } { + containment := supervisorStatusFixture(t, []byte(body)) + result := containment.terminateSupervisor() + if err := result.err; err == nil || errors.Is(err, io.EOF) { + t.Fatalf("invalid status admitted or lost explicit cleanup failure: %v", err) + } + if got := OrdinarySupervisorFailureClass(result.err); got != "" { + t.Fatalf("unadmitted status established class %q", got) + } + } +} diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_failure_test.go b/install/integrationctl/adapters/process/osrunner_ordinary_failure_test.go new file mode 100644 index 000000000..972d99b86 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_failure_test.go @@ -0,0 +1,52 @@ +package process + +import ( + "errors" + "testing" +) + +type supervisorDiagnosticTextTrap struct{} + +func (supervisorDiagnosticTextTrap) Error() string { + panic("structural supervisor diagnostics must not read error text") +} + +func TestOrdinaryForcedCleanupPreservesMessageAndIdentity(t *testing.T) { + const original = "process left live descendants that required forced cleanup" + if ErrOrdinaryForcedCleanup.Error() != original { + t.Fatal("forced cleanup changed its existing error text") + } + err := errors.Join(ErrOrdinaryForcedCleanup, supervisorDiagnosticTextTrap{}) + if !errors.Is(err, ErrOrdinaryForcedCleanup) || OrdinarySupervisorFailureClass(err) != "" { + t.Fatal("ordinary forced cleanup lost its distinct structural identity") + } +} + +func TestOrdinarySupervisorFailureClassNeverReadsErrorText(t *testing.T) { + for _, test := range []struct{ stage, class string }{ + {ordinarySupervisorRequestDecode, "ordinary_supervisor_request_decode"}, + {ordinarySupervisorSubreaperSetup, "ordinary_supervisor_subreaper_setup"}, + {ordinarySupervisorPreflight, "ordinary_supervisor_preflight"}, + {ordinarySupervisorCommandStart, "ordinary_supervisor_command_start"}, + {ordinarySupervisorDescendantCleanup, "ordinary_supervisor_descendant_cleanup"}, + {"", ""}, + {"unrecognized stage with sensitive text", ""}, + } { + t.Run(test.stage, func(t *testing.T) { + cause := supervisorDiagnosticTextTrap{} + failure := &ordinarySupervisorFailure{cause: cause, stage: test.stage} + err := errors.Join(errors.New("outer cause"), failure) + if got := OrdinarySupervisorFailureClass(err); got != test.class { + t.Fatalf("class = %q, want %q", got, test.class) + } + if !errors.Is(err, cause) || !errors.Is(errors.Unwrap(failure), cause) { + t.Fatal("classification carrier lost its original cause") + } + }) + } + for _, err := range []error{nil, supervisorDiagnosticTextTrap{}, (*ordinarySupervisorFailure)(nil)} { + if got := OrdinarySupervisorFailureClass(err); got != "" { + t.Fatalf("unrelated error received class %q", got) + } + } +} diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_proc_linux.go b/install/integrationctl/adapters/process/osrunner_ordinary_proc_linux.go new file mode 100644 index 000000000..bc6ec6189 --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_proc_linux.go @@ -0,0 +1,271 @@ +//go:build linux + +package process + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + "strconv" + "strings" + + "golang.org/x/sys/unix" +) + +const linuxProcStatLimit = 4 << 10 +const linuxProcChildrenLimit = 64 << 10 +const linuxProcTaskEntriesLimit = linuxSupervisorTraversalMaxVisits + +type linuxTaskProcOps struct { + readFile func(string, int) ([]byte, error) + taskExists func(string) (bool, error) +} + +// A release snapshot remains an error until the exact task directory is +// independently absent. Its stable identity also preserves replacement checks. +type linuxTaskReleaseIdentityError struct { + identity linuxProcessIdentity +} + +func (*linuxTaskReleaseIdentityError) Error() string { return "malformed proc task stable identity" } + +// Retain the original observation without allowing one of its errnos to stand +// for proven disappearance at an outer traversal boundary. No Unwrap or Is. +type linuxTaskIdentityUncertainty struct { + cause error +} + +func (failure *linuxTaskIdentityUncertainty) Error() string { return failure.cause.Error() } + +func ordinaryContainmentPreflight() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + return ordinaryContainmentPreflightWith(os.Getpid(), unix.Gettid(), linuxDefaultTaskProcOps()) +} + +func ordinaryContainmentPreflightWith(pid, tid int, ops linuxTaskProcOps) error { + _, present, err := linuxTaskChildrenWith(pid, tid, ops) + if err != nil { + return err + } + if !present { + return fmt.Errorf("current locked OS thread disappeared during ordinary containment preflight") + } + return nil +} + +func linuxDefaultTaskProcOps() linuxTaskProcOps { + return linuxTaskProcOps{readFile: readBoundedLinuxProcFile, taskExists: linuxTaskDirectoryExists} +} + +func readBoundedLinuxProcFile(path string, limit int) ([]byte, error) { + file, err := os.OpenFile(path, os.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + info, err := file.Stat() + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("proc observation is not a regular file: %s", path) + } + body, err := io.ReadAll(io.LimitReader(file, int64(limit)+1)) + if err != nil { + return nil, err + } + if len(body) > limit { + return nil, fmt.Errorf("proc observation exceeded %d bytes: %s", limit, path) + } + return body, nil +} + +func linuxTaskDirectoryExists(path string) (bool, error) { + info, err := os.Lstat(path) + if linuxProcessGone(err) { + return false, nil + } + if err != nil { + return false, err + } + if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return false, fmt.Errorf("proc task is not a directory: %s", path) + } + return true, nil +} + +func linuxTaskChildren(pid, tid int) ([]byte, bool, error) { + return linuxTaskChildrenWith(pid, tid, linuxDefaultTaskProcOps()) +} + +func linuxTaskChildrenWith(pid, tid int, ops linuxTaskProcOps) ([]byte, bool, error) { + task := fmt.Sprintf("/proc/%d/task/%d", pid, tid) + before, err := linuxReadTaskIdentity(task, tid, ops) + if err != nil { + gone, observeErr := linuxTaskGoneAfterIdentityError(task, err, ops) + return nil, !gone, observeErr + } + if before.zombie { + return nil, false, nil + } + body, err := ops.readFile(task+"/children", linuxProcChildrenLimit) + if err == nil { + if err := validateLinuxProcChildren(body); err != nil { + return nil, true, err + } + return body, true, nil + } + if !linuxProcessGone(err) { + return nil, true, fmt.Errorf("read live proc task children: %w", err) + } + after, identityErr := linuxReadTaskIdentity(task, tid, ops) + if identityErr != nil { + //nolint:errorlint // Only the direct private parser marker carries an admitted release identity. + if released, ok := identityErr.(*linuxTaskReleaseIdentityError); ok && released != nil && + (before.pid != released.identity.pid || before.startTime != released.identity.startTime) { + return nil, true, fmt.Errorf("proc task changed identity while its children interface was missing") + } + gone, observeErr := linuxTaskGoneAfterIdentityError(task, identityErr, ops) + return nil, !gone, observeErr + } + if before.pid != after.pid || before.startTime != after.startTime { + return nil, true, fmt.Errorf("proc task changed identity while its children interface was missing") + } + if after.zombie { + return nil, false, nil + } + // Do not wrap ENOENT: ancestor traversal treats that errno as a process + // disappearance. This task was independently proved to remain alive. + if os.IsNotExist(err) { + return nil, true, fmt.Errorf("%w: live task %d/%d has no children interface (%s)", ErrOrdinaryContainmentUnsupported, pid, tid, err.Error()) + } + return nil, true, fmt.Errorf("live task %d/%d children observation failed: %s", pid, tid, err.Error()) +} + +func linuxTaskGoneAfterIdentityError(task string, identityErr error, ops linuxTaskProcOps) (bool, error) { + //nolint:errorlint // Wrapped or joined release markers must not authorize a disappearance probe. + released, direct := identityErr.(*linuxTaskReleaseIdentityError) + if !direct || released == nil { + var nested *linuxTaskReleaseIdentityError + if errors.As(identityErr, &nested) { + return false, &linuxTaskIdentityUncertainty{cause: identityErr} + } + if !linuxProcessGone(identityErr) { + return false, identityErr + } + } + exists, err := ops.taskExists(task) + if err != nil { + if released != nil { + return false, errors.Join(released, &linuxTaskIdentityUncertainty{cause: err}) + } + return false, err + } + if !exists { + return true, nil + } + if released != nil { + return false, released + } + // A missing stat inside a still-present task is unknown observation, not + // evidence that the task disappeared or that its children list was empty. + return false, withOrdinaryCleanupOrigin("task_identity_unavailable", fmt.Errorf("existing proc task identity is unavailable: %s", identityErr.Error())) +} + +func linuxReadTaskIdentity(task string, tid int, ops linuxTaskProcOps) (linuxProcessIdentity, error) { + body, err := ops.readFile(task+"/stat", linuxProcStatLimit) + if err != nil { + return linuxProcessIdentity{}, err + } + return parseLinuxTaskIdentity(body, tid) +} + +func parseLinuxTaskIdentity(body []byte, tid int) (linuxProcessIdentity, error) { + raw := string(body) + opening, closing := strings.IndexByte(raw, '('), strings.LastIndexByte(raw, ')') + if opening <= 0 || closing < opening || strings.TrimSpace(raw[:opening]) != strconv.Itoa(tid) || tid <= 0 { + return linuxProcessIdentity{}, fmt.Errorf("malformed proc task identity") + } + fields := strings.Fields(raw[closing+1:]) + if len(fields) < 20 || len(fields[0]) != 1 || strings.IndexByte("RSDZTtXxKWPI", fields[0][0]) < 0 { + return linuxProcessIdentity{}, fmt.Errorf("malformed proc task state") + } + for _, field := range fields[1:] { + if field == "" || strings.HasPrefix(field, "+") { + return linuxProcessIdentity{}, fmt.Errorf("malformed proc task numeric field") + } + if _, err := strconv.ParseUint(strings.TrimPrefix(field, "-"), 10, 64); err != nil { + return linuxProcessIdentity{}, fmt.Errorf("malformed proc task numeric field: %w", err) + } + } + parent, parentErr := strconv.Atoi(fields[1]) + group, groupErr := strconv.Atoi(fields[2]) + started, startErr := strconv.ParseUint(fields[19], 10, 64) + if parentErr != nil || groupErr != nil || startErr != nil || parent < 0 { + return linuxProcessIdentity{}, fmt.Errorf("malformed proc task stable identity") + } + identity := linuxProcessIdentity{pid: tid, ppid: parent, pgid: group, startTime: started, zombie: strings.IndexByte("ZXx", fields[0][0]) >= 0} + if group < 0 { + if linuxTaskReleaseRecord(fields) { + return linuxProcessIdentity{}, &linuxTaskReleaseIdentityError{identity: identity} + } + return linuxProcessIdentity{}, fmt.Errorf("malformed proc task stable identity") + } + return identity, nil +} + +func linuxTaskReleaseRecord(fields []string) bool { + // do_task_stat leaves these defaults when the task's sighand was released. + // State is sampled earlier and does not itself establish disappearance. + if len(fields) < 50 || fields[1] != "0" || fields[2] != "-1" || fields[3] != "-1" { + return false + } + for _, field := range fields[1:] { + magnitude := strings.TrimPrefix(field, "-") + number, err := strconv.ParseUint(magnitude, 10, 64) + if err != nil || strconv.FormatUint(number, 10) != magnitude || strings.HasPrefix(field, "-") && number == 0 { + return false + } + } + return true +} + +func validateLinuxProcChildren(body []byte) error { + if len(body) > linuxProcChildrenLimit { + return fmt.Errorf("proc children observation exceeded %d bytes", linuxProcChildrenLimit) + } + for _, value := range strings.Fields(string(body)) { + pid, err := strconv.Atoi(value) + if err != nil || pid <= 0 || strconv.Itoa(pid) != value { + return fmt.Errorf("malformed proc children identity %q", value) + } + } + return nil +} + +func readBoundedLinuxTaskDirectory(path string) ([]os.DirEntry, error) { + file, err := os.OpenFile(path, os.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + entries, err := file.ReadDir(linuxProcTaskEntriesLimit + 1) + if err != nil && !errors.Is(err, io.EOF) { + return nil, err + } + if len(entries) > linuxProcTaskEntriesLimit { + return nil, fmt.Errorf("proc task enumeration exceeded %d entries", linuxProcTaskEntriesLimit) + } + return entries, nil +} + +func linuxTaskID(name string) (int, error) { + tid, err := strconv.Atoi(name) + if err != nil || tid <= 0 || strconv.Itoa(tid) != name { + return 0, fmt.Errorf("invalid proc task identity %q", name) + } + return tid, nil +} diff --git a/install/integrationctl/adapters/process/osrunner_ordinary_proc_linux_test.go b/install/integrationctl/adapters/process/osrunner_ordinary_proc_linux_test.go new file mode 100644 index 000000000..382a6e4ef --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_ordinary_proc_linux_test.go @@ -0,0 +1,202 @@ +//go:build linux + +package process + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +func taskStatFixture(tid int, start string) []byte { + fields := strings.Fields("R 1 44 44 0 -1 0 0 0 0 0 0 0 0 0 0 0 1 0 " + start) + return []byte(fmt.Sprintf("%d (inert fixture) %s\n", tid, strings.Join(fields, " "))) +} + +func TestLinuxTaskChildrenMissingInterfaceRequiresPositiveLiveIdentity(t *testing.T) { + for _, tc := range []struct { + name string + childErr, errorAfter error + before, after []byte + exists bool + wantUnsupported, wantGone, wantErr bool + }{ + {name: "same live task has no interface", childErr: os.ErrNotExist, exists: true, wantUnsupported: true, wantErr: true}, + {name: "task exits after initial identity", childErr: os.ErrNotExist, errorAfter: os.ErrNotExist, wantGone: true}, + {name: "task is replaced", childErr: os.ErrNotExist, after: taskStatFixture(44, "8"), exists: true, wantErr: true}, + {name: "missing stat inside present task is unknown", childErr: os.ErrNotExist, errorAfter: os.ErrNotExist, exists: true, wantErr: true}, + {name: "permission is not unsupported", childErr: os.ErrPermission, exists: true, wantErr: true}, + {name: "unknown read failure", childErr: syscall.EIO, exists: true, wantErr: true}, + {name: "ESRCH for still live task is not unsupported", childErr: syscall.ESRCH, exists: true, wantErr: true}, + {name: "malformed before identity", before: []byte("unknown"), exists: true, wantErr: true}, + {name: "malformed after identity", childErr: os.ErrNotExist, after: []byte("unknown"), exists: true, wantErr: true}, + {name: "available empty interface", exists: true}, + } { + t.Run(tc.name, func(t *testing.T) { + reads := 0 + ops := linuxTaskProcOps{ + readFile: func(path string, limit int) ([]byte, error) { + if strings.HasSuffix(path, "/children") { + if limit != linuxProcChildrenLimit { + t.Fatal(limit) + } + return []byte{}, tc.childErr + } + if limit != linuxProcStatLimit { + t.Fatal(limit) + } + reads++ + if reads > 1 { + if tc.errorAfter != nil { + return nil, tc.errorAfter + } + if tc.after != nil { + return tc.after, nil + } + } + if tc.before != nil { + return tc.before, nil + } + return taskStatFixture(44, "7"), nil + }, + taskExists: func(string) (bool, error) { return tc.exists, nil }, + } + _, present, err := linuxTaskChildrenWith(12, 44, ops) + if (err != nil) != tc.wantErr || errors.Is(err, ErrOrdinaryContainmentUnsupported) != tc.wantUnsupported || (!present) != tc.wantGone { + t.Fatalf("present=%t err=%v", present, err) + } + if tc.wantUnsupported && linuxProcessGone(err) { + t.Fatalf("unsupported live interface must not look like a disappeared ancestor: %v", err) + } + if errors.Is(tc.childErr, os.ErrPermission) && !errors.Is(err, os.ErrPermission) { + t.Fatalf("permission cause lost: %v", err) + } + }) + } +} + +func TestOrdinaryMissingLiveChildrenRefusesBeforeWrappingOrLaunch(t *testing.T) { + marker := filepath.Join(t.TempDir(), "must-not-launch") + cmd := exec.CommandContext(t.Context(), "/bin/sh", "-c", "printf started > \"$1\"", "sh", marker) + ops := linuxTaskProcOps{ + readFile: func(path string, _ int) ([]byte, error) { + if strings.HasSuffix(path, "/children") { + return nil, os.ErrNotExist + } + return taskStatFixture(44, "7"), nil + }, + taskExists: func(string) (bool, error) { return true, nil }, + } + containment, err := newOrdinaryCommandContainmentWithPreflight(cmd, time.Second, func() error { return ordinaryContainmentPreflightWith(12, 44, ops) }) + if !errors.Is(err, ErrOrdinaryContainmentUnsupported) || containment != nil || cmd.Process != nil || len(cmd.ExtraFiles) != 0 || cmd.Path != "/bin/sh" { + t.Fatalf("prelaunch refusal=%+v %v cmd=%+v", containment, err, cmd) + } + if _, err := os.Stat(marker); !os.IsNotExist(err) { + t.Fatalf("unsupported command was launched: %v", err) + } +} + +func TestOrdinaryActualUnsupportedPreflightCannotLaunch(t *testing.T) { + err := (OS{}).OrdinaryPreflight() + if err == nil { + t.Skip("this host supplies the proc prerequisite; supported runtime tests qualify actual shutdown") + } + if !errors.Is(err, ErrOrdinaryContainmentUnsupported) { + t.Fatal(err) + } + marker := filepath.Join(t.TempDir(), "must-not-launch") + _, runErr := (OS{}).Run(t.Context(), ports.Command{Argv: []string{"/bin/sh", "-c", "printf started > \"$1\"", "sh", marker}}) + if !errors.Is(runErr, ErrOrdinaryContainmentUnsupported) { + t.Fatalf("ordinary launch did not retain unsupported cause: %v", runErr) + } + if _, err := os.Stat(marker); !os.IsNotExist(err) { + t.Fatalf("unsupported command was launched: %v", err) + } +} + +func TestBoundedProcObservationsAndStrictIdentities(t *testing.T) { + path := filepath.Join(t.TempDir(), "proc-fixture") + for _, limit := range []int{linuxProcStatLimit, linuxProcChildrenLimit} { + if err := os.WriteFile(path, []byte(strings.Repeat("x", limit+1)), 0600); err != nil { + t.Fatal(err) + } + if _, err := readBoundedLinuxProcFile(path, limit); err == nil { + t.Fatalf("accepted proc read over %d bytes", limit) + } + if err := os.WriteFile(path, []byte(strings.Repeat("x", limit)), 0600); err != nil { + t.Fatal(err) + } + body, err := readBoundedLinuxProcFile(path, limit) + if err != nil || len(body) != limit { + t.Fatalf("exact bound rejected: bytes=%d error=%v", len(body), err) + } + } + for _, body := range [][]byte{[]byte("44 (x) R 1"), taskStatFixture(45, "7"), taskStatFixture(44, "-1"), taskStatFixture(44, "unknown")} { + if _, err := parseLinuxTaskIdentity(body, 44); err == nil { + t.Fatalf("accepted malformed task stat %q", body) + } + } + for _, body := range []string{"-1", "0", "+2", "02", "unknown", strings.Repeat("1", linuxProcChildrenLimit+1)} { + if err := validateLinuxProcChildren([]byte(body)); err == nil { + t.Fatalf("accepted malformed proc children %q", body[:min(len(body), 40)]) + } + } +} + +// This is an inert constructor check. It supplies the exact prerequisite +// observation through per-call operations and never starts the command. +func TestOrdinaryAvailableEmptyProcInterfacePermitsWrapping(t *testing.T) { + ops := linuxTaskProcOps{ + readFile: func(path string, _ int) ([]byte, error) { + if strings.HasSuffix(path, "/children") { + return []byte{}, nil + } + return taskStatFixture(44, "7"), nil + }, + taskExists: func(string) (bool, error) { return true, nil }, + } + cmd := exec.CommandContext(t.Context(), "/bin/true") + containment, err := newOrdinaryCommandContainmentWithPreflight(cmd, time.Second, func() error { return ordinaryContainmentPreflightWith(12, 44, ops) }) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := containment.close(); err != nil { + t.Error(err) + } + }() + if cmd.Process != nil || cmd.Path != "/proc/self/exe" || len(cmd.ExtraFiles) != 3 { + t.Fatalf("supported prerequisite did not retain ordinary supervision: %+v", cmd) + } +} + +func TestProcTaskEnumerationAndNamesAreBounded(t *testing.T) { + root := t.TempDir() + for i := range linuxProcTaskEntriesLimit + 1 { + if err := os.WriteFile(filepath.Join(root, fmt.Sprint(i)), nil, 0600); err != nil { + t.Fatal(err) + } + } + if _, err := readBoundedLinuxTaskDirectory(root); err == nil { + t.Fatal("unbounded task directory was accepted") + } + if err := os.Remove(filepath.Join(root, fmt.Sprint(linuxProcTaskEntriesLimit))); err != nil { + t.Fatal(err) + } + if entries, err := readBoundedLinuxTaskDirectory(root); err != nil || len(entries) != linuxProcTaskEntriesLimit { + t.Fatalf("exact task bound: %d %v", len(entries), err) + } + for _, name := range []string{"0", "-1", "+1", "01", "unknown"} { + if _, err := linuxTaskID(name); err == nil { + t.Fatalf("malformed task directory accepted: %q", name) + } + } +} diff --git a/install/integrationctl/adapters/process/osrunner_task_release_linux_test.go b/install/integrationctl/adapters/process/osrunner_task_release_linux_test.go new file mode 100644 index 000000000..a30d176de --- /dev/null +++ b/install/integrationctl/adapters/process/osrunner_task_release_linux_test.go @@ -0,0 +1,313 @@ +//go:build linux + +package process + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "syscall" + "testing" + "time" +) + +func taskReleaseStatFixture(tid int, start string, changes map[int]string) []byte { + fields := make([]string, 50) + for index := range fields { + fields[index] = "0" + } + fields[0], fields[2], fields[3], fields[5], fields[19] = "R", "-1", "-1", "-1", start + for index, value := range changes { + fields[index] = value + } + return []byte(fmt.Sprintf("%d (inert released task) %s\n", tid, strings.Join(fields, " "))) +} + +func TestLinuxTaskReleaseSnapshotRequiresProvenDisappearance(t *testing.T) { + for _, tc := range []struct { + name string + after, exists, changed, zombie bool + observationErr error + wantGone, wantErr bool + wantProbes int + }{ + {name: "initial_absent", wantGone: true, wantProbes: 1}, + {name: "initial_present", exists: true, wantErr: true, wantProbes: 1}, + {name: "initial_observation_error", observationErr: os.ErrPermission, wantErr: true, wantProbes: 1}, + {name: "after_absent", after: true, wantGone: true, wantProbes: 1}, + {name: "after_present", after: true, exists: true, wantErr: true, wantProbes: 1}, + {name: "after_observation_error", after: true, observationErr: syscall.EIO, wantErr: true, wantProbes: 1}, + {name: "after_changed_identity_absent", after: true, changed: true, wantErr: true}, + {name: "zombie_release_absent", zombie: true, wantGone: true, wantProbes: 1}, + {name: "zombie_release_present", zombie: true, exists: true, wantErr: true, wantProbes: 1}, + } { + t.Run(tc.name, func(t *testing.T) { + statReads, childrenReads, probes := 0, 0, 0 + ops := linuxTaskProcOps{ + readFile: func(path string, limit int) ([]byte, error) { + switch path { + case "/proc/12/task/44/stat": + if limit != linuxProcStatLimit { + t.Fatal("stat observation bound changed") + } + statReads++ + if tc.after && statReads == 1 { + return taskStatFixture(44, "7"), nil + } + start, changes := "7", map[int]string{} + if tc.changed { + start = "8" + } + if tc.zombie { + changes[0] = "Z" + } + return taskReleaseStatFixture(44, start, changes), nil + case "/proc/12/task/44/children": + if !tc.after || limit != linuxProcChildrenLimit { + t.Fatal("unexpected children observation or changed bound") + } + childrenReads++ + return nil, os.ErrNotExist + default: + t.Fatal("observation escaped the exact task") + return nil, nil + } + }, + taskExists: func(path string) (bool, error) { + if path != "/proc/12/task/44" { + t.Fatal("absence observation escaped the exact task") + } + probes++ + return tc.exists, tc.observationErr + }, + } + body, present, err := linuxTaskChildrenWith(12, 44, ops) + if len(body) != 0 || present == tc.wantGone || (err != nil) != tc.wantErr { + t.Fatalf("present=%t err=%v; want gone=%t error=%t", present, err, tc.wantGone, tc.wantErr) + } + wantStats, wantChildren := 1, 0 + if tc.after { + wantStats, wantChildren = 2, 1 + } + if statReads != wantStats || childrenReads != wantChildren || probes != tc.wantProbes { + t.Fatalf("observation counts: stat=%d children=%d directory=%d", statReads, childrenReads, probes) + } + if linuxProcessGone(err) || errors.Is(err, ErrOrdinaryContainmentUnsupported) { + t.Fatal("release or uncertain observation acquired unrelated errno authority") + } + if tc.changed && err.Error() != "proc task changed identity while its children interface was missing" { + t.Fatal("observed replacement lost the original identity refusal") + } + }) + } +} + +func TestLinuxTaskReleaseRecordRequiresCanonicalCompleteStat(t *testing.T) { + _, validErr := parseLinuxTaskIdentity(taskReleaseStatFixture(44, "7", nil), 44) + var released *linuxTaskReleaseIdentityError + if !errors.As(validErr, &released) || released == nil || released.identity.pid != 44 || released.identity.startTime != 7 || released.identity.ppid != 0 || released.identity.pgid != -1 { + t.Fatal("complete release record lost its private stable identity") + } + if validErr.Error() != "malformed proc task stable identity" || linuxProcessGone(validErr) || errors.Is(validErr, ErrOrdinaryContainmentUnsupported) { + t.Fatal("release marker changed original error text or acquired errno authority") + } + complete := strings.TrimSpace(string(taskReleaseStatFixture(44, "7", nil))) + truncated := []byte(complete[:strings.LastIndexByte(complete, ' ')]) + for _, tc := range []struct { + name string + body []byte + changes map[int]string + }{ + {name: "wrong_tid", body: taskReleaseStatFixture(45, "7", nil)}, + {name: "short_state", body: []byte("44 (inert) R 0 -1 -1")}, + {name: "missing_final_field", body: truncated}, + {name: "parent_negative_zero", changes: map[int]string{1: "-0"}}, + {name: "other_parent", changes: map[int]string{1: "1"}}, + {name: "group_leading_zero", changes: map[int]string{2: "-01"}}, + {name: "other_negative_group", changes: map[int]string{2: "-2"}}, + {name: "session_leading_zero", changes: map[int]string{3: "-01"}}, + {name: "other_session", changes: map[int]string{3: "0"}}, + {name: "start_leading_zero", changes: map[int]string{19: "07"}}, + {name: "negative_start", changes: map[int]string{19: "-1"}}, + {name: "start_plus", changes: map[int]string{19: "+7"}}, + {name: "unknown_start", changes: map[int]string{19: "unknown"}}, + {name: "counter_leading_zero", changes: map[int]string{20: "00"}}, + {name: "counter_negative_zero", changes: map[int]string{20: "-0"}}, + {name: "counter_plus", changes: map[int]string{20: "+1"}}, + {name: "unknown_counter", changes: map[int]string{20: "unknown"}}, + {name: "counter_overflow", changes: map[int]string{20: "18446744073709551616"}}, + {name: "unknown_state", changes: map[int]string{0: "?"}}, + {name: "malformed_state", changes: map[int]string{0: "RR"}}, + } { + t.Run(tc.name, func(t *testing.T) { + body := tc.body + if body == nil { + body = taskReleaseStatFixture(44, "7", tc.changes) + } + _, err := parseLinuxTaskIdentity(body, 44) + var marker *linuxTaskReleaseIdentityError + if err == nil || errors.As(err, &marker) || linuxProcessGone(err) { + t.Fatal("malformed record acquired release/disappearance authority") + } + ops := linuxTaskProcOps{taskExists: func(string) (bool, error) { + t.Fatal("malformed record attempted an absence probe") + return false, nil + }} + gone, retained := linuxTaskGoneAfterIdentityError("/inert/task", err, ops) + if gone || reflect.ValueOf(retained) != reflect.ValueOf(err) { + t.Fatal("ordinary malformed failure changed") + } + }) + } + identity, err := parseLinuxTaskIdentity(taskStatFixture(44, "007"), 44) + if err != nil || identity.pid != 44 || identity.startTime != 7 || identity.pgid != 44 { + t.Fatal("special release admission changed the existing ordinary parser") + } +} + +func TestLinuxTaskReleaseNestedErrorsNeverAuthorizeDisappearance(t *testing.T) { + _, marker := parseLinuxTaskIdentity(taskReleaseStatFixture(44, "7", nil), 44) + var nilMarker *linuxTaskReleaseIdentityError + for _, input := range []error{ + fmt.Errorf("wrapped release: %w", marker), + errors.Join(marker, os.ErrNotExist), + errors.Join(syscall.ESRCH, marker), + errors.Join(marker, errors.Join(os.ErrPermission, os.ErrNotExist)), + nilMarker, + } { + ops := linuxTaskProcOps{taskExists: func(string) (bool, error) { + t.Fatal("unadmitted marker attempted an absence probe") + return false, nil + }} + gone, err := linuxTaskGoneAfterIdentityError("/inert/task", input, ops) + var uncertainty *linuxTaskIdentityUncertainty + if gone || err == nil || !errors.As(err, &uncertainty) || uncertainty == nil || reflect.ValueOf(uncertainty.cause) != reflect.ValueOf(input) { + t.Fatal("unadmitted marker lost its exact uncertain cause") + } + if err.Error() != input.Error() { + t.Fatal("opaque uncertainty changed the original error text") + } + assertTaskReleaseRemainsUnknown(t, err) + } + for _, input := range []error{errors.New(marker.Error()), supervisorDiagnosticTextTrap{}} { + ops := linuxTaskProcOps{taskExists: func(string) (bool, error) { + t.Fatal("unrelated error attempted an absence probe") + return false, nil + }} + gone, err := linuxTaskGoneAfterIdentityError("/inert/task", input, ops) + if gone || reflect.ValueOf(err) != reflect.ValueOf(input) { + t.Fatal("unrelated error acquired marker authority") + } + assertTaskReleaseRemainsUnknown(t, err) + } +} + +func TestLinuxTaskReleaseObserverErrorsRetainCausesWithoutGoneErrnos(t *testing.T) { + _, marker := parseLinuxTaskIdentity(taskReleaseStatFixture(44, "7", nil), 44) + for _, observation := range []error{ + os.ErrPermission, errors.New("unknown observation"), os.ErrNotExist, syscall.ESRCH, + fmt.Errorf("wrapped missing directory: %w", os.ErrNotExist), + errors.Join(os.ErrPermission, os.ErrNotExist), supervisorDiagnosticTextTrap{}, + } { + for _, exists := range []bool{false, true} { + probes := 0 + ops := linuxTaskProcOps{taskExists: func(path string) (bool, error) { + if path != "/inert/task" { + t.Fatal("observer path changed") + } + probes++ + return exists, observation + }} + gone, err := linuxTaskGoneAfterIdentityError("/inert/task", marker, ops) + var uncertainty *linuxTaskIdentityUncertainty + if gone || err == nil || probes != 1 || !errors.Is(err, marker) || !errors.As(err, &uncertainty) || uncertainty == nil { + t.Fatal("failed observer lost release cause or granted disappearance") + } + if reflect.ValueOf(uncertainty.cause) != reflect.ValueOf(observation) || errors.Is(err, observation) { + t.Fatal("observer cause was replaced or its errno escaped quarantine") + } + assertTaskReleaseRemainsUnknown(t, err) + } + } + ops := linuxTaskProcOps{taskExists: func(string) (bool, error) { return true, nil }} + gone, err := linuxTaskGoneAfterIdentityError("/inert/task", marker, ops) + if gone || reflect.ValueOf(err) != reflect.ValueOf(marker) { + t.Fatal("present directory changed the exact release refusal") + } +} + +func assertTaskReleaseRemainsUnknown(t *testing.T, cause error) { + t.Helper() + contextual := withOrdinaryCleanupOrigin("task_children_observation", cause) + joined := errors.Join(errors.New("earlier non-gone failure"), contextual) + for _, err := range []error{cause, contextual, joined} { + if linuxProcessGone(err) || errors.Is(err, ErrOrdinaryContainmentUnsupported) { + t.Fatal("uncertainty became disappearance at an outer boundary") + } + } + if ordinaryCleanupFailureReason(joined) != "task_children_observation" { + t.Fatal("opaque uncertainty changed bounded classification") + } +} + +func TestLinuxTaskReleaseCannotAuthorizeOrdinaryPreflight(t *testing.T) { + for _, exists := range []bool{false, true} { + reads, probes := 0, 0 + ops := linuxTaskProcOps{ + readFile: func(path string, limit int) ([]byte, error) { + if path != "/proc/12/task/44/stat" || limit != linuxProcStatLimit { + t.Fatal("preflight attempted an unadmitted observation") + } + reads++ + return taskReleaseStatFixture(44, "7", nil), nil + }, + taskExists: func(path string) (bool, error) { + if path != "/proc/12/task/44" { + t.Fatal("preflight absence probe changed task") + } + probes++ + return exists, nil + }, + } + cmd := exec.CommandContext(t.Context(), "/bin/true") + containment, err := newOrdinaryCommandContainmentWithPreflight(cmd, time.Second, func() error { + return ordinaryContainmentPreflightWith(12, 44, ops) + }) + if err == nil || containment != nil || cmd.Process != nil || cmd.Path != "/bin/true" || len(cmd.ExtraFiles) != 0 || reads != 1 || probes != 1 { + t.Fatal("release snapshot authorized wrapping or command launch") + } + if linuxProcessGone(err) || errors.Is(err, ErrOrdinaryContainmentUnsupported) { + t.Fatal("release preflight acquired unrelated errno authority") + } + } +} + +func TestLinuxTaskDirectoryObserverNormalizesOnlyActualAbsence(t *testing.T) { + root := t.TempDir() + file := filepath.Join(root, "file") + if err := os.WriteFile(file, []byte("inert"), 0600); err != nil { + t.Fatal(err) + } + link := filepath.Join(root, "link") + if err := os.Symlink(root, link); err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + path string + wantExists, wantErr bool + }{ + {path: filepath.Join(root, "absent")}, + {path: root, wantExists: true}, + {path: file, wantErr: true}, + {path: link, wantErr: true}, + } { + exists, err := linuxTaskDirectoryExists(tc.path) + if exists != tc.wantExists || (err != nil) != tc.wantErr || linuxProcessGone(err) { + t.Fatal("directory observer confused absence, presence or uncertainty") + } + } +} diff --git a/install/integrationctl/adapters/process/osrunner_test.go b/install/integrationctl/adapters/process/osrunner_test.go index 50199a435..50bd90afd 100644 --- a/install/integrationctl/adapters/process/osrunner_test.go +++ b/install/integrationctl/adapters/process/osrunner_test.go @@ -48,6 +48,7 @@ func TestOSRunnerPreservesCommandOutputAndExitCode(t *testing.T) { fmt.Fprint(os.Stderr, "expected stderr") os.Exit(7) } + requireOrdinaryPreflight(t) environment := append([]string(nil), os.Environ()...) environment = append(environment, "AGENTPLUGINS_PROCESS_OUTPUT_HELPER=1") result, err := (OS{}).Run(context.Background(), ports.Command{ @@ -66,6 +67,7 @@ func TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess(t *testing.T) { fmt.Fprint(os.Stdout, "clean") os.Exit(0) } + requireOrdinaryPreflight(t) environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_CLEAN_EXIT_HELPER=1") result, err := (OS{}).Run(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerCleanExitWithoutOtherGroupMembersRemainsSuccess"}, Env: environment, @@ -80,6 +82,7 @@ func TestOSRunnerBoundsStdoutAndReturnsLimitError(t *testing.T) { fmt.Fprint(os.Stdout, "output beyond the configured bound") os.Exit(0) } + requireOrdinaryPreflight(t) environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_STDOUT_LIMIT_HELPER=1") result, err := (OS{}).Run(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerBoundsStdoutAndReturnsLimitError"}, Env: environment, StdoutLimitBytes: 6, @@ -104,6 +107,7 @@ func TestOSRunnerStdoutLimitStopsProducerBeforeRemainingOutput(t *testing.T) { _ = os.WriteFile(marker, []byte("producer completed"), 0o600) os.Exit(0) } + requireOrdinaryPreflight(t) marker = filepath.Join(t.TempDir(), "completed") environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_STDOUT_LIMIT_MARKER="+marker) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -124,6 +128,7 @@ func TestOSRunnerStdoutLimitPreservesNonzeroExit(t *testing.T) { _, _ = os.Stdout.Write([]byte(strings.Repeat("x", 4097))) os.Exit(23) } + requireOrdinaryPreflight(t) environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_STDOUT_LIMIT_EXIT_HELPER=1") result, err := (OS{}).Run(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerStdoutLimitPreservesNonzeroExit"}, Env: environment, StdoutLimitBytes: 16, @@ -141,6 +146,7 @@ func TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands( _, _ = os.Stdout.Write([]byte(strings.Repeat("x", 4097))) os.Exit(141) } + requireOrdinaryPreflight(t) environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_STDOUT_LIMIT_141_HELPER=1") result, err := (OS{}).Run(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerStdoutLimitPreservesConventionalPipeExitCodeForGenericCommands"}, Env: environment, StdoutLimitBytes: 16, @@ -966,6 +972,7 @@ func TestOSRunnerBoundsStderrWhileRetainingDiagnosticEdges(t *testing.T) { fmt.Fprint(os.Stderr, ":diagnostic-end") os.Exit(9) } + requireOrdinaryPreflight(t) environment := append([]string(nil), os.Environ()...) environment = append(environment, "AGENTPLUGINS_PROCESS_STDERR_HELPER=1") result, err := (OS{}).Run(context.Background(), ports.Command{ diff --git a/install/integrationctl/adapters/process/osrunner_tree_linux.go b/install/integrationctl/adapters/process/osrunner_tree_linux.go index e48e6f947..0da7bb293 100644 --- a/install/integrationctl/adapters/process/osrunner_tree_linux.go +++ b/install/integrationctl/adapters/process/osrunner_tree_linux.go @@ -65,8 +65,10 @@ type linuxOrdinarySupervisorSpec struct { } type linuxOrdinarySupervisorResult struct { - Forced bool - Error string + Forced bool + Error string + FailureStage string `json:",omitempty"` + FailureReason string `json:",omitempty"` } func init() { @@ -144,6 +146,13 @@ func newCommandContainment(cmd *exec.Cmd) (*commandContainment, error) { } func newOrdinaryCommandContainment(cmd *exec.Cmd, grace time.Duration) (*commandContainment, error) { + return newOrdinaryCommandContainmentWithPreflight(cmd, grace, ordinaryContainmentPreflight) +} + +func newOrdinaryCommandContainmentWithPreflight(cmd *exec.Cmd, grace time.Duration, preflight func() error) (*commandContainment, error) { + if err := preflight(); err != nil { + return nil, err + } request, status, err := wrapLinuxOrdinaryCommand(cmd, grace) if err != nil { return nil, err @@ -234,12 +243,20 @@ func runLinuxOrdinarySupervisor() int { var spec linuxOrdinarySupervisorSpec if err := json.NewDecoder(specFile).Decode(&spec); err != nil { result.Error = fmt.Sprintf("decode supervisor request: %v", err) + result.FailureStage = ordinarySupervisorRequestDecode writeResult() return 125 } _ = specFile.Close() if err := unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0); err != nil { result.Error = fmt.Sprintf("establish isolated child-subreaper boundary: %v", err) + result.FailureStage = ordinarySupervisorSubreaperSetup + writeResult() + return 125 + } + if err := ordinaryContainmentPreflight(); err != nil { + result.Error = fmt.Sprintf("ordinary command containment preflight: %v", err) + result.FailureStage = ordinarySupervisorPreflight writeResult() return 125 } @@ -251,6 +268,7 @@ func runLinuxOrdinarySupervisor() int { child.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err := child.Start(); err != nil { result.Error = fmt.Sprintf("start supervised command: %v", err) + result.FailureStage = ordinarySupervisorCommandStart writeResult() return 125 } @@ -266,7 +284,7 @@ func runLinuxOrdinarySupervisor() int { waitErr := child.Wait() result.Forced = forced if cleanupErr != nil { - result.Error = cleanupErr.Error() + setOrdinarySupervisorCleanupFailure(&result, cleanupErr) } writeResult() if waitErr == nil { @@ -301,7 +319,7 @@ func superviseLinuxOrdinaryChildren(leaderPID int, requested <-chan struct{}, gr if !waitComplete { var info unix.Siginfo if err := unix.Waitid(unix.P_PID, leaderPID, &info, unix.WEXITED|unix.WNOWAIT|unix.WNOHANG, nil); err != nil { - cleanupErr = errors.Join(cleanupErr, fmt.Errorf("observe supervised command exit: %w", err)) + cleanupErr = errors.Join(cleanupErr, withOrdinaryCleanupOrigin("exit_observation", fmt.Errorf("observe supervised command exit: %w", err))) } else if info.Signo != 0 { waitComplete = true deadline = time.Now().Add(grace) @@ -331,7 +349,7 @@ func superviseLinuxOrdinaryChildren(leaderPID int, requested <-chan struct{}, gr if !groupSignaled { groupSignaled = true if err := signalOwnedLinuxProcessGroup(leaderPID, waitComplete, syscall.Kill); err != nil && !errors.Is(err, syscall.ESRCH) { - cleanupErr = errors.Join(cleanupErr, fmt.Errorf("terminate supervised command group: %w", err)) + cleanupErr = errors.Join(cleanupErr, withOrdinaryCleanupOrigin("descendant_signal", fmt.Errorf("terminate supervised command group: %w", err))) } } cleanupErr = errors.Join(cleanupErr, killLinuxSupervisorDescendants(os.Getpid(), traversalDeadline)) @@ -346,7 +364,7 @@ func superviseLinuxOrdinaryChildren(leaderPID int, requested <-chan struct{}, gr } } if forced && !deadline.IsZero() && time.Now().After(deadline) { - return errors.Join(cleanupErr, fmt.Errorf("isolated ordinary-command supervisor did not empty before cleanup deadline")), forced + return errors.Join(cleanupErr, withOrdinaryCleanupOrigin("cleanup_deadline", fmt.Errorf("isolated ordinary-command supervisor did not empty before cleanup deadline"))), forced } time.Sleep(5 * time.Millisecond) } @@ -387,7 +405,7 @@ func linuxSupervisorLiveDescendants(root int, deadline time.Time) (int, error) { for pid, tracked := range identities { isLive, inspectErr := liveLinuxTrackedProcess(pid, tracked.process) if inspectErr != nil { - err = errors.Join(err, fmt.Errorf("classify stable supervisor descendant %d: %w", pid, inspectErr)) + err = errors.Join(err, withOrdinaryCleanupOrigin("tracked_identity_read", fmt.Errorf("classify stable supervisor descendant %d: %w", pid, inspectErr))) } else if isLive { live++ } @@ -402,12 +420,12 @@ func killLinuxSupervisorDescendants(root int, deadline time.Time) error { for pid, tracked := range identities { live, inspectErr := liveLinuxTrackedProcess(pid, tracked.process) if inspectErr != nil { - resultErr = errors.Join(resultErr, fmt.Errorf("classify stable supervisor descendant %d before termination: %w", pid, inspectErr)) + resultErr = errors.Join(resultErr, withOrdinaryCleanupOrigin("tracked_identity_read", fmt.Errorf("classify stable supervisor descendant %d before termination: %w", pid, inspectErr))) continue } if live { if err := unix.PidfdSendSignal(tracked.process.pidfd, unix.SIGKILL, nil, 0); err != nil && !errors.Is(err, syscall.ESRCH) { - resultErr = errors.Join(resultErr, fmt.Errorf("terminate stable supervisor child %d: %w", pid, err)) + resultErr = errors.Join(resultErr, withOrdinaryCleanupOrigin("descendant_signal", fmt.Errorf("terminate stable supervisor child %d: %w", pid, err))) } } } @@ -448,7 +466,8 @@ func linuxDescendantIdentitiesWith(root int, deadline time.Time, maxVisits int, visited := map[int]struct{}{root: {}} for len(queue) > 0 { if !time.Now().Before(deadline) { - return result, fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline after %d visit(s)", len(visited)) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return result, withOrdinaryCleanupOrigin("traversal_deadline", fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline after %d visit(s)", len(visited))) } parent := queue[0] queue = queue[1:] @@ -460,13 +479,15 @@ func linuxDescendantIdentitiesWith(root int, deadline time.Time, maxVisits int, } for pid, observedStartTime := range children { if !time.Now().Before(deadline) { - return result, fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline after %d visit(s)", len(visited)) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return result, withOrdinaryCleanupOrigin("traversal_deadline", fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline after %d visit(s)", len(visited))) } if _, exists := visited[pid]; exists { continue } if len(visited) >= maxVisits { - return result, fmt.Errorf("Linux supervisor descendant traversal exceeded %d-process cleanup bound", maxVisits) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return result, withOrdinaryCleanupOrigin("traversal_limit", fmt.Errorf("Linux supervisor descendant traversal exceeded %d-process cleanup bound", maxVisits)) } pidfd, err := ops.openPidfd(pid, 0) if err != nil { @@ -474,7 +495,7 @@ func linuxDescendantIdentitiesWith(root int, deadline time.Time, maxVisits int, if linuxProcessGone(err) || linuxProcessGone(currentErr) || (currentErr == nil && current.startTime != observedStartTime) { continue } - return result, fmt.Errorf("open stable supervisor descendant handle for %d: %w", pid, err) + return result, withOrdinaryCleanupOrigin("pidfd_open", fmt.Errorf("open stable supervisor descendant handle for %d: %w", pid, err)) } identity, err := ops.readIdentity(pid) if linuxProcessGone(err) { @@ -483,11 +504,11 @@ func linuxDescendantIdentitiesWith(root int, deadline time.Time, maxVisits int, } if err != nil { _ = ops.closePidfd(pidfd) - return result, fmt.Errorf("revalidate stable supervisor descendant %d: %w", pid, err) + return result, withOrdinaryCleanupOrigin("pidfd_identity_read", fmt.Errorf("revalidate stable supervisor descendant %d: %w", pid, err)) } if identity.startTime != observedStartTime { _ = ops.closePidfd(pidfd) - return result, fmt.Errorf("revalidate stable supervisor descendant %d: process start time changed", pid) + return result, withOrdinaryCleanupOrigin("pidfd_identity_changed", fmt.Errorf("revalidate stable supervisor descendant %d: process start time changed", pid)) } if identity.ppid != parent && !identity.zombie { parentIdentity, parentErr := ops.readIdentity(parent) @@ -500,11 +521,11 @@ func linuxDescendantIdentitiesWith(root int, deadline time.Time, maxVisits int, } if parentErr != nil && !linuxProcessGone(parentErr) { _ = ops.closePidfd(pidfd) - return result, fmt.Errorf("revalidate stable supervisor descendant %d parent %d: %w", pid, parent, parentErr) + return result, withOrdinaryCleanupOrigin("parent_identity_read", fmt.Errorf("revalidate stable supervisor descendant %d parent %d: %w", pid, parent, parentErr)) } if parentStillOwned { _ = ops.closePidfd(pidfd) - return result, fmt.Errorf("revalidate stable supervisor descendant %d: live parent changed from %d to %d", pid, parent, identity.ppid) + return result, withOrdinaryCleanupOrigin("parent_changed", fmt.Errorf("revalidate stable supervisor descendant %d: live parent changed from %d to %d", pid, parent, identity.ppid)) } // The first children snapshot proved ancestry and the pidfd plus // start time proves the same child survived. If its exact parent @@ -785,7 +806,13 @@ func (containment *commandContainment) terminateSupervisor() terminationResult { if exited { statusErr = containment.readSupervisorResult() if containment.supervisorResult.Error != "" { - statusErr = errors.Join(statusErr, errors.New(containment.supervisorResult.Error)) + stage := "" + reason := "" + if statusErr == nil { + stage = containment.supervisorResult.FailureStage + reason = containment.supervisorResult.FailureReason + } + statusErr = errors.Join(statusErr, ordinarySupervisorErrorWithReason(stage, reason, containment.supervisorResult.Error)) } } return terminationResult{ @@ -813,6 +840,13 @@ func (containment *commandContainment) readSupervisorResult() error { err = fmt.Errorf("isolated ordinary-command supervisor returned trailing status data: %w", trailingErr) } } + if err == nil && containment.supervisorResult.FailureStage != "" && containment.supervisorResult.Error == "" { + err = fmt.Errorf("isolated ordinary-command supervisor returned a failure stage without an error") + } + if err == nil && containment.supervisorResult.FailureReason != "" && + (containment.supervisorResult.Error == "" || containment.supervisorResult.FailureStage != ordinarySupervisorDescendantCleanup) { + err = fmt.Errorf("isolated ordinary-command supervisor returned a cleanup reason without its cleanup error stage") + } closeErr := containment.supervisorStatus.Close() containment.supervisorStatus = nil if errors.Is(err, io.EOF) { @@ -1283,34 +1317,37 @@ func liveLinuxTrackedProcess(pid int, tracked linuxTrackedProcess) (bool, error) return false, nil } if err != nil { - return false, err + return false, withOrdinaryCleanupOrigin("tracked_identity_read", err) } if identity.startTime != tracked.startTime { - return false, fmt.Errorf("process start time changed") + return false, withOrdinaryCleanupOrigin("tracked_identity_changed", fmt.Errorf("process start time changed")) } return !identity.zombie, nil } func linuxDirectChildIdentities(pid int, deadline time.Time, limit int) (map[int]uint64, error) { childrenByIdentity := make(map[int]uint64) - tasks, err := os.ReadDir(fmt.Sprintf("/proc/%d/task", pid)) + tasks, err := readBoundedLinuxTaskDirectory(fmt.Sprintf("/proc/%d/task", pid)) if err != nil { - return nil, err + return nil, withOrdinaryCleanupOrigin("task_directory_read", err) } for _, task := range tasks { if !time.Now().Before(deadline) { - return childrenByIdentity, fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline while inspecting process %d", pid) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return childrenByIdentity, withOrdinaryCleanupOrigin("traversal_deadline", fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline while inspecting process %d", pid)) + } + tid, parseErr := linuxTaskID(task.Name()) + if parseErr != nil { + return childrenByIdentity, withOrdinaryCleanupOrigin("task_children_observation", fmt.Errorf("invalid proc task identity: %w", parseErr)) } - children, err := os.ReadFile(fmt.Sprintf("/proc/%d/task/%s/children", pid, task.Name())) + children, _, err := linuxTaskChildren(pid, tid) if err != nil { - if linuxProcessGone(err) { - continue - } - return nil, err + return nil, withOrdinaryCleanupOrigin("task_children_observation", err) } for _, rawChild := range strings.Fields(string(children)) { if !time.Now().Before(deadline) { - return childrenByIdentity, fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline while inspecting children of process %d", pid) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return childrenByIdentity, withOrdinaryCleanupOrigin("traversal_deadline", fmt.Errorf("Linux supervisor descendant traversal exceeded cleanup deadline while inspecting children of process %d", pid)) } child, err := strconv.Atoi(rawChild) if err != nil { @@ -1321,15 +1358,17 @@ func linuxDirectChildIdentities(pid int, deadline time.Time, limit int) (map[int continue } if err != nil { - return nil, err + return nil, withOrdinaryCleanupOrigin("child_identity_read", err) } observedStartTime, exists := childrenByIdentity[child] if !exists && len(childrenByIdentity) >= limit { - return childrenByIdentity, fmt.Errorf("Linux supervisor descendant traversal exceeded %d-process cleanup bound", linuxSupervisorTraversalMaxVisits) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return childrenByIdentity, withOrdinaryCleanupOrigin("traversal_limit", fmt.Errorf("Linux supervisor descendant traversal exceeded %d-process cleanup bound", linuxSupervisorTraversalMaxVisits)) } if exists { if observedStartTime != identity.startTime { - return childrenByIdentity, fmt.Errorf("Linux supervisor child %d changed identity during task inspection", child) + //nolint:staticcheck // ST1005: preserve the byte-exact legacy error beginning with the proper noun Linux. + return childrenByIdentity, withOrdinaryCleanupOrigin("child_identity_changed", fmt.Errorf("Linux supervisor child %d changed identity during task inspection", child)) } continue } @@ -1380,7 +1419,7 @@ func (containment *commandContainment) scanDescendants() error { continue } seen[pid] = struct{}{} - tasks, err := os.ReadDir(fmt.Sprintf("/proc/%d/task", pid)) + tasks, err := readBoundedLinuxTaskDirectory(fmt.Sprintf("/proc/%d/task", pid)) if err != nil { if tracked, ok := containment.descendants[pid]; ok { live, classifyErr := liveLinuxTrackedProcess(pid, tracked) @@ -1393,14 +1432,13 @@ func (containment *commandContainment) scanDescendants() error { continue } for _, task := range tasks { - children, err := os.ReadFile(fmt.Sprintf("/proc/%d/task/%s/children", pid, task.Name())) + tid, parseErr := linuxTaskID(task.Name()) + if parseErr != nil { + scanErr = errors.Join(scanErr, parseErr) + continue + } + children, _, err := linuxTaskChildren(pid, tid) if err != nil { - // Threads can disappear between ReadDir and opening their children - // file. The remaining live threads are still inspected in this same - // snapshot, so this race is not an ancestry blind spot. - if linuxProcessGone(err) { - continue - } if tracked, ok := containment.descendants[pid]; ok { live, classifyErr := liveLinuxTrackedProcess(pid, tracked) if classifyErr != nil { diff --git a/install/integrationctl/adapters/process/osrunner_tree_linux_test.go b/install/integrationctl/adapters/process/osrunner_tree_linux_test.go index c1bf18c66..92f0a9308 100644 --- a/install/integrationctl/adapters/process/osrunner_tree_linux_test.go +++ b/install/integrationctl/adapters/process/osrunner_tree_linux_test.go @@ -42,6 +42,7 @@ func TestOSRunnerSurvivesRemovedOwnExecutable(t *testing.T) { } os.Exit(0) } + requireOrdinaryPreflight(t) self, err := os.Executable() if err != nil { t.Fatal(err) @@ -150,6 +151,7 @@ func TestOSRunnerCancellationContainsImmediateSetsidDescendant(t *testing.T) { } } + requireOrdinaryPreflight(t) pidPath := t.TempDir() + "/escape.pid" ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) @@ -199,6 +201,7 @@ func TestDuplexCapabilityRejectsNonCgroupDelegationBeforeStart(t *testing.T) { } func TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup(t *testing.T) { + requireOrdinaryPreflight(t) cmd := exec.Command("/bin/sh", "-c", "printf ordinary") var before int if err := unix.Prctl(unix.PR_GET_CHILD_SUBREAPER, uintptr(unsafe.Pointer(&before)), 0, 0, 0); err != nil { @@ -235,6 +238,7 @@ func TestOrdinaryCommandContainmentDoesNotRequireDelegatedCgroup(t *testing.T) { } func TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable(t *testing.T) { + requireOrdinaryPreflight(t) root := t.TempDir() if _, _, err := createDelegatedCgroupAt(root); err == nil { t.Fatal("test fixture unexpectedly supplied a cgroup v2 delegation") @@ -250,6 +254,7 @@ func TestOrdinaryRunWorksWhenDelegatedCgroupIsUnavailable(t *testing.T) { } func TestOrdinarySupervisorDescriptorsAreClosedBeforeCommandExec(t *testing.T) { + requireOrdinaryPreflight(t) result, err := (OS{}).Run(context.Background(), ports.Command{ Argv: []string{"/bin/sh", "-c", `if printf '{"Forced":false}\n' 2>/dev/null >&5; then exit 97; fi; printf supervisor-fds-closed`}, }) @@ -393,6 +398,7 @@ func TestLinuxSupervisorTraversalEnforcesProcessAndDeadlineBounds(t *testing.T) } func TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit(t *testing.T) { + requireOrdinaryPreflight(t) // /bin/true commonly becomes waitable before the runner can acquire its // post-Start pidfd. Exercise that attachment boundary enough times to prove // rapid natural exits remain ordinary successful command results. @@ -406,6 +412,7 @@ func TestOSRunnerRapidSuccessfulCommandsPreserveNaturalExit(t *testing.T) { } func TestOSRunnerRapidShellCommandsWithShortLivedChildrenPreserveNaturalExit(t *testing.T) { + requireOrdinaryPreflight(t) root := t.TempDir() script := root + "/client" body := `#!/bin/sh @@ -534,6 +541,7 @@ func TestOSRunnerCleansSameProcessGroupMemberAfterLeaderNaturalExit(t *testing.T os.Exit(0) } + requireOrdinaryPreflight(t) pidPath := t.TempDir() + "/descendant.pid" environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_RUN_LEADER_PID_PATH="+pidPath) _, err := (OS{}).RunWithTreeExitGrace(context.Background(), ports.Command{ @@ -580,6 +588,7 @@ func TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit(t *testing.T) { os.Exit(0) } + requireOrdinaryPreflight(t) _, err := (OS{}).RunWithDescendantExitGrace(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerDescendantGraceAcceptsOnlyNaturalHelperExit"}, Env: append(os.Environ(), "AGENTPLUGINS_PROCESS_GRACE_LEADER=1"), @@ -610,6 +619,7 @@ func TestOSRunnerDescendantGraceStillRejectsForcedCleanup(t *testing.T) { os.Exit(0) } + requireOrdinaryPreflight(t) _, err := (OS{}).RunWithDescendantExitGrace(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerDescendantGraceStillRejectsForcedCleanup"}, Env: append(os.Environ(), "AGENTPLUGINS_PROCESS_GRACE_LONG_LEADER=1"), @@ -646,6 +656,7 @@ func TestOSRunnerPreservesDiagnosticAndStatusWhileCleaningNaturalExitGroup(t *te os.Exit(47) } + requireOrdinaryPreflight(t) pidPath := t.TempDir() + "/descendant.pid" environment := append(os.Environ(), "AGENTPLUGINS_PROCESS_DIAGNOSTIC_LEADER_PID_PATH="+pidPath) _, err := (OS{}).RunWithTreeExitGrace(context.Background(), ports.Command{ @@ -696,6 +707,7 @@ func TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession(t *test os.Exit(0) } + requireOrdinaryPreflight(t) pidPath := t.TempDir() + "/setsid-descendant.pid" _, err := (OS{}).RunWithTreeExitGrace(context.Background(), ports.Command{ Argv: []string{os.Args[0], "-test.run=TestOSRunnerTreeGraceCleansImmediateDescendantThatCreatesNewSession"}, diff --git a/install/integrationctl/agentplugins/adapters/clientdetect/opencode_target_test.go b/install/integrationctl/agentplugins/adapters/clientdetect/opencode_target_test.go index 9a88eb669..ccf601a72 100644 --- a/install/integrationctl/agentplugins/adapters/clientdetect/opencode_target_test.go +++ b/install/integrationctl/agentplugins/adapters/clientdetect/opencode_target_test.go @@ -16,6 +16,19 @@ import ( "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) +// Replace disposable executable inodes instead of truncating bytes that a +// completed probe may still have mapped (ETXTBSY on Linux, signing cache on macOS). +func replaceOpenCodeFixture(t *testing.T, executable string, body []byte) { + t.Helper() + next := executable + ".next" + if err := os.WriteFile(next, body, 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(next, executable); err != nil { + t.Fatal(err) + } +} + func nativeOpenCodeFixture(t *testing.T, version, mode string) string { t.Helper() // All executed fixture bytes and probe working directories live in this @@ -130,19 +143,20 @@ func TestOpenCodeTargetIdentityAndWrapperRestriction(t *testing.T) { if err != nil { t.Fatal(err) } - body, err := os.ReadFile(replacement) + body, err := os.ReadFile(binary) if err != nil { t.Fatal(err) } - // Replace the inode: rewriting an executed Mach-O in place can leave the - // kernel's code-signing cache bound to the old bytes on macOS. - next := binary + ".next" - if err := os.WriteFile(next, body, 0700); err != nil { - t.Fatal(err) + replaceOpenCodeFixture(t, binary, body) + same, err := ProbeOpenCodeTarget(context.Background(), target) + if err != nil || same.Version != before.Version || same.ExecutableIdentity != before.ExecutableIdentity { + t.Fatalf("same-byte replacement changed identity: %+v %+v %v", before, same, err) } - if err := os.Rename(next, binary); err != nil { + body, err = os.ReadFile(replacement) + if err != nil { t.Fatal(err) } + replaceOpenCodeFixture(t, binary, body) after, err := ProbeOpenCodeTarget(context.Background(), target) if err != nil || after.Version != before.Version || after.ExecutableIdentity == before.ExecutableIdentity { t.Fatalf("same-version change: %+v %+v %v", before, after, err) @@ -178,9 +192,8 @@ func TestOpenCodeTargetIdentityAndWrapperRestriction(t *testing.T) { if !errors.Is(err, ErrUnverifiedProbeTarget) || evidence.Reason != "host_target_unverified" { t.Fatalf("wrapper: %+v %v", evidence, err) } - if err := os.WriteFile(binary, []byte("swapped runtime"), 0700); err != nil { - t.Fatal(err) - } + // selected now resolves to replacement, so change the actual runtime. + replaceOpenCodeFixture(t, replacement, []byte("swapped runtime")) } } diff --git a/install/integrationctl/agentplugins/adapters/codextoml/document.go b/install/integrationctl/agentplugins/adapters/codextoml/document.go new file mode 100644 index 000000000..186461b1d --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/codextoml/document.go @@ -0,0 +1,116 @@ +package codextoml + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "sort" + + "github.com/creachadair/tomledit" + "github.com/pelletier/go-toml/v2" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" +) + +type document struct { + body []byte + ast *tomledit.Document + original map[string]json.RawMessage + settings map[string]any +} + +// Parse supplies the retained concrete TOML backend at an explicit composition boundary. +func Parse(body []byte) (nativeconfig.CodexDocument, error) { + var values map[string]any + if err := toml.Unmarshal(body, &values); err != nil { + return nil, fmt.Errorf("%w: invalid Codex TOML", nativeconfig.ErrMalformed) + } + if values == nil { + values = map[string]any{} + } + servers, exists := values["mcp_servers"] + if !exists { + servers = map[string]any{} + } + collection, ok := servers.(map[string]any) + if !ok { + return nil, fmt.Errorf("%w: mcp_servers must be a table", nativeconfig.ErrMalformed) + } + entries := make(map[string]json.RawMessage, len(collection)) + for name, value := range collection { + entry, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("%w: invalid Codex MCP entry values", nativeconfig.ErrMalformed) + } + entries[name] = entry + } + ast, err := tomledit.Parse(bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("%w: invalid Codex TOML structure", nativeconfig.ErrMalformed) + } + delete(values, "mcp_servers") + return &document{body: bytes.Clone(body), ast: ast, original: entries, settings: values}, nil +} + +func (doc *document) MCPEntries() (map[string]json.RawMessage, error) { + return cloneEntries(doc.original), nil +} + +func (doc *document) RenderMCPEntries(current map[string]json.RawMessage) ([]byte, error) { + current = cloneEntries(current) + names := changedNames(doc.original, current) + if len(names) == 0 { + return bytes.Clone(doc.body), nil + } + for _, name := range names { + removeCodexTOMLEntry(doc.ast, name) + if body, exists := current[name]; exists { + if err := setCodexTOMLEntry(doc.ast, name, body); err != nil { + return nil, err + } + } + } + var result bytes.Buffer + if err := tomledit.Format(&result, doc.ast); err != nil { + return nil, fmt.Errorf("render Codex native config: %w", err) + } + parsed, err := Parse(result.Bytes()) + if err != nil { + return nil, err + } + observed := parsed.(*document) + if !reflect.DeepEqual(doc.settings, observed.settings) { + return nil, fmt.Errorf("rendered Codex config changed foreign settings") + } + if len(changedNames(current, observed.original)) != 0 { + return nil, fmt.Errorf("rendered Codex MCP entries differ from requested entries") + } + return bytes.Clone(result.Bytes()), nil +} + +func cloneEntries(entries map[string]json.RawMessage) map[string]json.RawMessage { + result := make(map[string]json.RawMessage, len(entries)) + for name, body := range entries { + result[name] = bytes.Clone(body) + } + return result +} + +func changedNames(previous, next map[string]json.RawMessage) []string { + changed := map[string]bool{} + for name, body := range previous { + changed[name] = !bytes.Equal(body, next[name]) + } + for name, body := range next { + changed[name] = !bytes.Equal(body, previous[name]) + } + result := make([]string, 0, len(changed)) + for name, differs := range changed { + if differs { + result = append(result, name) + } + } + sort.Strings(result) + return result +} diff --git a/install/integrationctl/agentplugins/adapters/codextoml/edit.go b/install/integrationctl/agentplugins/adapters/codextoml/edit.go new file mode 100644 index 000000000..de34b55f8 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/codextoml/edit.go @@ -0,0 +1,67 @@ +package codextoml + +import ( + "bytes" + "fmt" + + "github.com/creachadair/tomledit" + "github.com/creachadair/tomledit/parser" + "github.com/creachadair/tomledit/transform" + "github.com/pelletier/go-toml/v2" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" +) + +func removeCodexTOMLEntry(doc *tomledit.Document, name string) { + var remove []*tomledit.Entry + prefix := parser.Key{"mcp_servers", name} + doc.Scan(func(key parser.Key, entry *tomledit.Entry) bool { + if prefix.IsPrefixOf(key) { + remove = append(remove, entry) + } + return true + }) + for _, entry := range remove { + entry.Remove() + } +} + +func setCodexTOMLEntry(doc *tomledit.Document, name string, body []byte) error { + value, err := decodeTOMLJSON(body) + if err != nil || value == nil { + return fmt.Errorf("%w: Codex MCP entry must be a table", nativeconfig.ErrMalformed) + } + var encoded bytes.Buffer + if err := toml.NewEncoder(&encoded).SetTablesInline(true).Encode(map[string]any{name: value}); err != nil { + return fmt.Errorf("encode Codex MCP entry: %w", err) + } + parsed, err := tomledit.Parse(&encoded) + if err != nil { + return fmt.Errorf("parse projected Codex MCP entry: %w", err) + } + entry := parsed.First(name) + if entry == nil || !entry.IsMapping() { + return fmt.Errorf("projected Codex MCP entry is not a mapping") + } + parent := doc.First("mcp_servers") + if parent != nil && parent.IsMapping() { + if inline, ok := parent.Value.X.(parser.Inline); ok { + inline = append(inline, entry.KeyValue) + parent.Value.X = inline + return nil + } + return fmt.Errorf("%w: Codex MCP collection is not a table", nativeconfig.ErrMalformed) + } + if parent != nil && parent.IsSection() { + transform.InsertMapping(parent.Section, entry.KeyValue, false) + return nil + } + // An implicit parent may come from dotted keys or child tables. Adding a + // dotted global member handles both without redefining an existing table. + if doc.Global == nil { + doc.Global = &tomledit.Section{} + } + entry.Name = parser.Key{"mcp_servers", name} + transform.InsertMapping(doc.Global, entry.KeyValue, false) + return nil +} diff --git a/install/integrationctl/agentplugins/adapters/codextoml/isolation_test.go b/install/integrationctl/agentplugins/adapters/codextoml/isolation_test.go new file mode 100644 index 000000000..5d671d737 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/codextoml/isolation_test.go @@ -0,0 +1,55 @@ +package codextoml + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/pelletier/go-toml/v2" +) + +func TestProjectCodexConcreteCopiesAndLargeIntegers(t *testing.T) { + const original = "# foreign policy\n[mcp_servers.foreign]\nurl='https://inert.test/mcp'\nlarge=9007199254740993\n" + input := []byte(original) + doc, err := Parse(input) + if err != nil { + t.Fatal(err) + } + input[0] = '!' + entries, err := doc.MCPEntries() + if err != nil { + t.Fatal(err) + } + entries["foreign"][0] = '!' + independent, err := doc.MCPEntries() + if err != nil || !json.Valid(independent["foreign"]) { + t.Fatal("concrete entries alias retained document", err) + } + noop, err := doc.RenderMCPEntries(independent) + if err != nil || string(noop) != original { + t.Fatalf("no-op bytes changed: %q %v", noop, err) + } + noop[0] = '!' + again, err := doc.RenderMCPEntries(independent) + if err != nil || string(again) != original { + t.Fatal("no-op output aliases retained document", err) + } + independent["managed"] = json.RawMessage(`{"command":"TEST-not-executed","large":9007199254740993}`) + body, err := doc.RenderMCPEntries(independent) + if err != nil { + t.Fatal(err) + } + var native map[string]any + if err := toml.Unmarshal(body, &native); err != nil { + t.Fatal(err) + } + servers := native["mcp_servers"].(map[string]any) + for _, name := range []string{"foreign", "managed"} { + if value := servers[name].(map[string]any)["large"]; value != int64(9007199254740993) { + t.Fatalf("integer changed for %s: %v", name, value) + } + } + if !bytes.Contains(body, []byte("# foreign policy")) { + t.Fatal("foreign comment lost") + } +} diff --git a/install/integrationctl/agentplugins/adapters/codextoml/json.go b/install/integrationctl/agentplugins/adapters/codextoml/json.go new file mode 100644 index 000000000..b8399ad65 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/codextoml/json.go @@ -0,0 +1,55 @@ +package codextoml + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" +) + +func decodeTOMLJSON(body []byte) (map[string]any, error) { + var result map[string]any + decoder := json.NewDecoder(bytes.NewReader(body)) + decoder.UseNumber() + if err := decoder.Decode(&result); err != nil { + return nil, err + } + if _, err := decoder.Token(); err != io.EOF { + return nil, fmt.Errorf("codex MCP entry has trailing JSON") + } + converted, err := tomlJSONValue(result) + if err != nil { + return nil, err + } + result, _ = converted.(map[string]any) + return result, nil +} + +func tomlJSONValue(value any) (any, error) { + switch typed := value.(type) { + case json.Number: + if strings.ContainsAny(string(typed), ".eE") { + return strconv.ParseFloat(string(typed), 64) + } + return strconv.ParseInt(string(typed), 10, 64) + case map[string]any: + for key, child := range typed { + converted, err := tomlJSONValue(child) + if err != nil { + return nil, err + } + typed[key] = converted + } + case []any: + for i, child := range typed { + converted, err := tomlJSONValue(child) + if err != nil { + return nil, err + } + typed[i] = converted + } + } + return value, nil +} diff --git a/install/integrationctl/agentplugins/adapters/codextoml/lifecycle_test.go b/install/integrationctl/agentplugins/adapters/codextoml/lifecycle_test.go new file mode 100644 index 000000000..f81e66054 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/codextoml/lifecycle_test.go @@ -0,0 +1,156 @@ +package codextoml + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/pelletier/go-toml/v2" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" +) + +func TestProjectCodexTOMLExactLifecyclePreservesForeignData(t *testing.T) { + for name, collection := range map[string]string{ + "inline": "mcp_servers = {foreign = {url = 'https://foreign.test/mcp'}} # inline policy\n", + "dotted": "mcp_servers.foreign.url = 'https://foreign.test/mcp' # dotted policy\n", + "parent": "[mcp_servers]\nforeign = {url = 'https://foreign.test/mcp'} # parent policy\n", + "child": "[mcp_servers.foreign]\nurl = 'https://foreign.test/mcp' # child policy\n", + } { + t.Run(name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.toml") + before := "# preserve native settings\ncreated = 2026-10-23\n" + collection + "\n[foreign]\nkeep = [1, 2, 3] # foreign comment\n" + mustWrite(t, path, before) + kernel := newKernel() + request := nativeconfig.Request{Paths: nativeconfig.Paths{JSON: path}, Codec: nativeconfig.CodecCodex, Action: nativeconfig.ActionAdd, Name: "managed", Server: nativeconfig.Server{Type: "remote", URL: "https://managed.test/mcp", RemoteTransport: "streamable-http", Headers: map[string]string{"X-Fixture": "inert"}}} + receipt, err := kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + present, owned, err := kernel.Inspect(request.Paths, request.Codec, request.Name, &receipt) + if err != nil || !present || !owned { + t.Fatalf("exact Codex entry = %v/%v, %v", present, owned, err) + } + projectCodexAssertForeignTOML(t, path, before) + request.Action, request.Owned = nativeconfig.ActionUpdate, &receipt + request.Server = nativeconfig.Server{Type: "stdio", Command: "TEST-never-executed", Args: []string{"fixture"}, Env: map[string]string{"FIXTURE": "inert"}, CWD: filepath.Dir(path)} + receipt, err = kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + request.Owned = &receipt + unchanged, _ := os.ReadFile(path) + if _, err := kernel.Apply(request); err != nil { + t.Fatal(err) + } + assertBytes(t, path, string(unchanged)) + request.Action = nativeconfig.ActionRemove + if _, err := kernel.Apply(request); err != nil { + t.Fatal(err) + } + projectCodexAssertForeignTOML(t, path, before) + if present, _, err := kernel.Inspect(request.Paths, request.Codec, request.Name, nil); err != nil || present { + t.Fatalf("managed Codex entry survived: %v, %v", present, err) + } + }) + } +} + +func projectCodexAssertForeignTOML(t *testing.T, path, before string) { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var original, actual map[string]any + if err := toml.Unmarshal([]byte(before), &original); err != nil { + t.Fatal(err) + } + if err := toml.Unmarshal(body, &actual); err != nil { + t.Fatal(err) + } + delete(actual["mcp_servers"].(map[string]any), "managed") + if !reflect.DeepEqual(original, actual) { + t.Fatalf("foreign TOML data changed: %+v", actual) + } + for _, comment := range []string{"preserve native settings", "policy", "foreign comment"} { + if !strings.Contains(string(body), comment) { + t.Fatalf("foreign TOML comment %q lost", comment) + } + } +} + +func TestProjectCodexTOMLRejectsMalformedCollisionAndForeignEdit(t *testing.T) { + for name, body := range map[string]string{ + "duplicate": "[mcp_servers]\nx={command='one'}\nx={command='two'}\n", + "malformed": "[mcp_servers\n", + "scalar": "mcp_servers = false\n", + } { + t.Run(name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.toml") + mustWrite(t, path, body) + request := nativeconfig.Request{Paths: nativeconfig.Paths{JSON: path}, Codec: nativeconfig.CodecCodex, Action: nativeconfig.ActionAdd, Name: "managed", Server: nativeconfig.Server{Type: "remote", URL: "https://managed.test/mcp"}} + if _, err := newKernel().Apply(request); !errors.Is(err, nativeconfig.ErrMalformed) { + t.Fatalf("invalid TOML admitted: %v", err) + } + assertBytes(t, path, body) + }) + } + path := filepath.Join(t.TempDir(), "config.toml") + request := nativeconfig.Request{Paths: nativeconfig.Paths{JSON: path}, Codec: nativeconfig.CodecCodex, Action: nativeconfig.ActionAdd, Name: "managed", Server: nativeconfig.Server{Type: "remote", URL: "https://managed.test/mcp"}} + receipt, err := newKernel().Apply(request) + if err != nil { + t.Fatal(err) + } + if _, err := newKernel().Apply(request); !errors.Is(err, nativeconfig.ErrCollision) { + t.Fatalf("foreign entry was adopted: %v", err) + } + const edited = "[mcp_servers.managed]\nurl='https://managed.test/mcp'\nenabled=false # foreign policy\n" + mustWrite(t, path, edited) + for _, action := range []nativeconfig.Action{nativeconfig.ActionUpdate, nativeconfig.ActionRemove} { + request.Action, request.Owned = action, &receipt + if _, err := newKernel().Apply(request); !errors.Is(err, nativeconfig.ErrNotOwned) { + t.Fatalf("edited native entry accepted by %s: %v", action, err) + } + assertBytes(t, path, edited) + } +} + +func TestProjectCodexTOMLRemovesCompleteNestedOwnedEntry(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.toml") + const body = "[mcp_servers.managed]\ncommand='TEST-never-executed'\n[mcp_servers.managed.env]\nFIXTURE='inert'\n[mcp_servers.foreign]\nurl='https://foreign.test/keep' # keep\n" + mustWrite(t, path, body) + server := nativeconfig.Server{Type: "stdio", Command: "TEST-never-executed", Env: map[string]string{"FIXTURE": "inert"}} + receipt, err := nativeconfig.DesiredReceipt(path, nativeconfig.CodecCodex, "managed", server, nativeconfig.Placeholders{}) + if err != nil { + t.Fatal(err) + } + if _, err := newKernel().Apply(nativeconfig.Request{Paths: nativeconfig.Paths{JSON: path}, Codec: nativeconfig.CodecCodex, Action: nativeconfig.ActionRemove, Name: "managed", Owned: &receipt}); err != nil { + t.Fatal(err) + } + actual, _ := os.ReadFile(path) + if strings.Contains(string(actual), "managed") || !strings.Contains(string(actual), "# keep") { + t.Fatalf("nested native removal changed foreign entry or left owned child: %s", actual) + } + if _, err := nativeconfig.DesiredReceipt(path, nativeconfig.CodecCodex, "sse", nativeconfig.Server{Type: "remote", URL: "https://managed.test/mcp", RemoteTransport: "sse"}, nativeconfig.Placeholders{}); err == nil { + t.Fatal("Codex silently accepted unsupported SSE transport") + } +} + +func newKernel() nativeconfig.Kernel { return nativeconfig.New().WithCodexDocumentParser(Parse) } +func mustWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} +func assertBytes(t *testing.T, path, expected string) { + t.Helper() + body, err := os.ReadFile(path) + if err != nil || string(body) != expected { + t.Fatalf("bytes=%q, error=%v, expected=%q", body, err, expected) + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/codex_document.go b/install/integrationctl/agentplugins/adapters/nativeconfig/codex_document.go new file mode 100644 index 000000000..f9c003dcc --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/codex_document.go @@ -0,0 +1,112 @@ +package nativeconfig + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + + "github.com/tailscale/hujson" +) + +// CodexDocument retains concrete syntax outside the contract dependency closure. +// Implementations return independent entry/output byte slices. +type CodexDocument interface { + MCPEntries() (map[string]json.RawMessage, error) + RenderMCPEntries(map[string]json.RawMessage) ([]byte, error) +} + +type CodexDocumentParser func([]byte) (CodexDocument, error) + +// WithCodexDocumentParser retains this Kernel's exact FileIO and lock authority. +func (kernel Kernel) WithCodexDocumentParser(parser CodexDocumentParser) Kernel { + kernel.codexParser = parser + return kernel +} + +func (kernel Kernel) requireDocumentParser(codec Codec) error { + if codec == CodecCodex && kernel.codexParser == nil { + return fmt.Errorf("codex TOML document parser is required") + } + return nil +} + +func (kernel Kernel) parseDocument(body []byte, jsonc bool, codec Codec) (*document, error) { + if codec != CodecCodex { + return parseCodecDocument(body, jsonc, codec) + } + if err := kernel.requireDocumentParser(codec); err != nil { + return nil, err + } + if jsonc { + return nil, fmt.Errorf("%w: Codex requires one TOML config path", ErrMalformed) + } + native, err := kernel.codexParser(bytes.Clone(body)) + if err != nil { + return nil, err + } + if nilCodexDocument(native) { + return nil, fmt.Errorf("codex TOML parser returned no document") + } + entries, err := native.MCPEntries() + if err != nil { + return nil, err + } + neutral, err := json.Marshal(map[string]any{"mcp_servers": cloneCodexEntries(entries)}) + if err != nil { + return nil, err + } + doc, err := parseDocument(neutral, false) + if err != nil { + return nil, err + } + doc.codexDocument = native + return doc, nil +} + +func nilCodexDocument(value CodexDocument) bool { + if value == nil { + return true + } + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} + +func cloneCodexEntries(entries map[string]json.RawMessage) map[string]json.RawMessage { + result := make(map[string]json.RawMessage, len(entries)) + for name, body := range entries { + result[name] = bytes.Clone(body) + } + return result +} + +func (doc *document) codexEntries() (map[string]json.RawMessage, error) { + entries, err := codecCollection(doc, CodecCodex, false) + if err != nil || entries == nil { + return nil, err + } + result := make(map[string]json.RawMessage, len(entries.Members)) + for i := range entries.Members { + member := &entries.Members[i] + body, err := entryCanonical(member) + if err != nil { + return nil, err + } + result[member.Name.Value.(hujson.Literal).String()] = bytes.Clone(body) + } + return result, nil +} + +func (doc *document) renderCodexDocument() ([]byte, error) { + entries, err := doc.codexEntries() + if err != nil { + return nil, err + } + body, err := doc.codexDocument.RenderMCPEntries(cloneCodexEntries(entries)) + return bytes.Clone(body), err +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/codex_document_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/codex_document_test.go new file mode 100644 index 000000000..101b9604b --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/codex_document_test.go @@ -0,0 +1,116 @@ +package nativeconfig + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +type codexParserTestIO struct { + body []byte + reads, writes int +} + +func (files *codexParserTestIO) ReadNoFollow(string) ([]byte, os.FileMode, bool, error) { + files.reads++ + return files.body, 0600, true, nil +} +func (files *codexParserTestIO) WriteAtomic(_ string, body []byte, _ os.FileMode) error { + files.writes++ + files.body = bytes.Clone(body) + return nil +} +func (*codexParserTestIO) RemoveNoFollow(string) error { return nil } + +type codexParserTestDocument struct{ entries map[string]json.RawMessage } + +func (doc *codexParserTestDocument) MCPEntries() (map[string]json.RawMessage, error) { + return doc.entries, nil +} +func (*codexParserTestDocument) RenderMCPEntries(entries map[string]json.RawMessage) ([]byte, error) { + return json.Marshal(entries) +} + +func codexParserRequest(t *testing.T) Request { + t.Helper() + return Request{Paths: Paths{JSON: filepath.Join(t.TempDir(), "config.toml")}, Codec: CodecCodex, + Action: ActionAdd, Name: "managed", Server: Server{Type: "remote", URL: "https://inert.test/mcp"}} +} + +func TestProjectCodexMissingParserRefusesBeforeIOOrLocks(t *testing.T) { + files := &codexParserTestIO{body: []byte("original")} + locks := 0 + kernel := NewWithFileIO(files) + kernel.acquireLocks = func(Paths, Codec) (func() error, error) { locks++; return func() error { return nil }, nil } + request := codexParserRequest(t) + if _, err := kernel.Apply(request); err == nil { + t.Fatal("missing parser admitted mutation") + } + if _, _, err := kernel.Inspect(request.Paths, request.Codec, request.Name, nil); err == nil { + t.Fatal("missing parser admitted inspection") + } + if _, err := DesiredReceipt(request.Paths.JSON, request.Codec, request.Name, request.Server, Placeholders{}); err != nil { + t.Fatal("pure receipt gained parser requirement", err) + } + if files.reads != 0 || files.writes != 0 || locks != 0 { + t.Fatalf("missing parser effects reads=%d writes=%d locks=%d", files.reads, files.writes, locks) + } +} + +func TestProjectCodexParserCopyPreservesInjectedAuthority(t *testing.T) { + files := &codexParserTestIO{body: []byte("original")} + locks, releases := 0, 0 + original := NewWithFileIO(files) + original.acquireLocks = func(Paths, Codec) (func() error, error) { locks++; return func() error { releases++; return nil }, nil } + kernel := original.WithCodexDocumentParser(func([]byte) (CodexDocument, error) { + return &codexParserTestDocument{entries: map[string]json.RawMessage{}}, nil + }) + if _, err := kernel.Apply(codexParserRequest(t)); err != nil { + t.Fatal(err) + } + if original.codexParser != nil || kernel.files != files || files.reads == 0 || files.writes != 1 || locks != 1 || releases != 1 { + t.Fatalf("configured copy lost authority reads=%d writes=%d locks=%d releases=%d", files.reads, files.writes, locks, releases) + } +} + +func TestProjectCodexNilDocumentsCannotMutateSnapshotsOrLeakLocks(t *testing.T) { + for _, typed := range []bool{false, true} { + t.Run(map[bool]string{false: "literal", true: "typed"}[typed], func(t *testing.T) { + files := &codexParserTestIO{body: []byte("original snapshot")} + locks, releases := 0, 0 + kernel := NewWithFileIO(files) + kernel.acquireLocks = func(Paths, Codec) (func() error, error) { locks++; return func() error { releases++; return nil }, nil } + kernel = kernel.WithCodexDocumentParser(func(body []byte) (CodexDocument, error) { + body[0] = '!' + if typed { + var doc *codexParserTestDocument + return doc, nil + } + return nil, nil + }) + if _, err := kernel.Apply(codexParserRequest(t)); err == nil { + t.Fatal("nil document admitted") + } + if string(files.body) != "original snapshot" || files.writes != 0 || locks != 1 || releases != 1 { + t.Fatalf("parser aliased snapshot or leaked lock: body=%q writes=%d lock=%d release=%d", files.body, files.writes, locks, releases) + } + }) + } +} + +func TestProjectCodexParserRawEntriesDoNotAliasNeutralDocument(t *testing.T) { + raw := json.RawMessage(`{"url":"https://inert.test/mcp","large":9007199254740993}`) + parser := &codexParserTestDocument{entries: map[string]json.RawMessage{"foreign": raw}} + kernel := New().WithCodexDocumentParser(func([]byte) (CodexDocument, error) { return parser, nil }) + doc, err := kernel.parseDocument(nil, false, CodecCodex) + if err != nil { + t.Fatal(err) + } + raw[0] = '!' + entries, err := doc.codexEntries() + if err != nil || !bytes.Contains(entries["foreign"], []byte("9007199254740993")) || !json.Valid(entries["foreign"]) { + t.Fatalf("raw parser alias/number loss: %q %v", entries["foreign"], err) + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/codex_server.go b/install/integrationctl/agentplugins/adapters/nativeconfig/codex_server.go new file mode 100644 index 000000000..2419ab390 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/codex_server.go @@ -0,0 +1,35 @@ +package nativeconfig + +import ( + "fmt" + "strings" +) + +func projectCodexServer(server Server, placeholders Placeholders) (map[string]any, error) { + if server.OpenCodeV2 != nil { + return nil, fmt.Errorf("codex does not accept OpenCode V2 options") + } + if strings.EqualFold(strings.TrimSpace(server.Type), "remote") && server.RemoteTransport == "streamable-http" { + server.RemoteTransport = "" + } + projected, err := projectBaseServer(CodecMCPServers, server, placeholders) + if err != nil { + return nil, err + } + if headers, ok := projected["headers"]; ok { + delete(projected, "headers") + projected["http_headers"] = headers + } + if server.CodexAuth != nil { + if server.Type != "remote" { + return nil, fmt.Errorf("codex stdio servers do not accept remote authentication options") + } + if server.CodexAuth.BearerTokenEnvVar != "" { + projected["bearer_token_env_var"] = server.CodexAuth.BearerTokenEnvVar + } + if server.CodexAuth.OAuthResource != "" { + projected["oauth_resource"] = server.CodexAuth.OAuthResource + } + } + return projected, nil +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/copilot_project.go b/install/integrationctl/agentplugins/adapters/nativeconfig/copilot_project.go new file mode 100644 index 000000000..d4ab5de35 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/copilot_project.go @@ -0,0 +1,32 @@ +package nativeconfig + +import ( + "fmt" + "strings" +) + +// Copilot repository configuration uses explicit stdio/http/sse transport tags. +// The adapter supplies a managed launcher instead of an unqualified native cwd. +func projectCopilotServer(server Server, placeholders Placeholders) (map[string]any, error) { + nativeType := "stdio" + if strings.EqualFold(strings.TrimSpace(server.Type), "remote") { + switch server.RemoteTransport { + case "streamable-http": + nativeType = "http" + case "sse": + nativeType = "sse" + default: + return nil, fmt.Errorf("copilot remote MCP server requires streamable-http or sse transport") + } + server.RemoteTransport = "" + } + if server.CWD != "" { + return nil, fmt.Errorf("copilot project stdio requires managed launcher cwd binding") + } + entry, err := projectBaseServer(CodecMCPServers, server, placeholders) + if err != nil { + return nil, err + } + entry["type"], entry["tools"] = nativeType, []string{"*"} + return entry, nil +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/copilot_project_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/copilot_project_test.go new file mode 100644 index 000000000..2ebe3966f --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/copilot_project_test.go @@ -0,0 +1,67 @@ +package nativeconfig + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestProjectCopilotCodecPreservesExplicitTransportsAndForeignMembers(t *testing.T) { + path := filepath.Join(t.TempDir(), ".mcp.json") + const foreign = `{"mcpServers":{"foreign":{"type":"http","url":"https://foreign.test/mcp","disabled":true}},"foreignPreference":"keep"}` + mustWrite(t, path, foreign) + kernel := New() + request := Request{Paths: Paths{JSON: path}, Codec: CodecCopilot, Action: ActionAdd, Name: "managed", + Server: Server{Type: "remote", RemoteTransport: "streamable-http", URL: "https://managed.test/mcp", Headers: map[string]string{"X-Fixture": "inert"}}} + receipt, err := kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + for _, transport := range []struct { + native string + server Server + }{{"http", request.Server}, {"sse", Server{Type: "remote", RemoteTransport: "sse", URL: "https://managed.test/events"}}, {"stdio", Server{Type: "stdio", Command: "TEST-never-executed", Args: []string{"inert"}}}} { + request.Action, request.Owned, request.Server = ActionUpdate, &receipt, transport.server + receipt, err = kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + present, owned, err := kernel.Inspect(request.Paths, request.Codec, request.Name, &receipt) + body, readErr := os.ReadFile(path) + var observed struct { + Servers map[string]struct{ Type string } `json:"mcpServers"` + } + decodeErr := json.Unmarshal(body, &observed) + if err != nil || readErr != nil || decodeErr != nil || !present || !owned || observed.Servers["managed"].Type != transport.native || !strings.Contains(string(body), "foreign.test") || !strings.Contains(string(body), "foreignPreference") { + t.Fatalf("Copilot native transport %s = %s, %v, %v", transport.native, body, err, readErr) + } + } + request.Action, request.Owned = ActionRemove, &receipt + if _, err := kernel.Apply(request); err != nil { + t.Fatal(err) + } + if present, _, err := kernel.Inspect(request.Paths, request.Codec, request.Name, nil); err != nil || present { + t.Fatalf("Copilot exact removal = %t, %v", present, err) + } + request.Action, request.Name, request.Owned = ActionAdd, "foreign", nil + if _, err := kernel.Apply(request); !errors.Is(err, ErrCollision) { + t.Fatalf("disabled foreign Copilot server was adopted: %v", err) + } +} + +func TestProjectCopilotCodecRejectsUnqualifiedTransportAndCWD(t *testing.T) { + path := filepath.Join(t.TempDir(), ".mcp.json") + for _, server := range []Server{ + {Type: "remote", URL: "https://managed.test/mcp"}, + {Type: "remote", URL: "https://managed.test/mcp", RemoteTransport: "unknown"}, + {Type: "stdio", Command: "TEST-never-executed", CWD: filepath.Dir(path)}, + {Type: "stdio", Command: "TEST-never-executed", RemoteTransport: "sse"}, + } { + if _, err := DesiredReceipt(path, CodecCopilot, "managed", server, Placeholders{}); err == nil { + t.Fatalf("unqualified native contract admitted: %+v", server) + } + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/devin.go b/install/integrationctl/agentplugins/adapters/nativeconfig/devin.go new file mode 100644 index 000000000..0e4f5f617 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/devin.go @@ -0,0 +1,31 @@ +package nativeconfig + +import "fmt" + +// Current Devin uses command/args/env without cwd, and url/headers with an +// explicit http/sse transport. It is separate from legacy Cascade serverUrl. +func projectDevinServer(server Server, placeholders Placeholders) (map[string]any, error) { + if server.CWD != "" { + return nil, fmt.Errorf("devin stdio MCP does not accept cwd; use the managed launcher") + } + transport := "" + if server.Type == "remote" { + switch server.RemoteTransport { + case "streamable-http": + transport = "http" + case "sse": + transport = "sse" + default: + return nil, fmt.Errorf("devin remote MCP requires streamable-http or sse transport") + } + server.RemoteTransport = "" + } + projected, err := projectBaseServer(CodecMCPServers, server, placeholders) + if err != nil { + return nil, err + } + if transport != "" { + projected["transport"] = transport + } + return projected, nil +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/devin_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/devin_test.go new file mode 100644 index 000000000..665dd0e9e --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/devin_test.go @@ -0,0 +1,103 @@ +package nativeconfig + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDevinCodecPreservesExactCurrentTransportAndForeignPolicy(t *testing.T) { + path := filepath.Join(t.TempDir(), "mcp_config.json") + original := `{"policy":"keep","mcpServers":{"disabled":{"url":"https://foreign.test","disabled":true}}}` + if err := os.WriteFile(path, []byte(original), 0600); err != nil { + t.Fatal(err) + } + kernel := New() + for _, transport := range []string{"streamable-http", "sse"} { + server := Server{Type: "remote", URL: "https://managed.test/mcp", Headers: map[string]string{"X-Token": "literal"}, RemoteTransport: transport} + desired, err := DesiredReceipt(path, CodecDevin, "owned", server, Placeholders{}) + if err != nil { + t.Fatal(err) + } + receipt, err := kernel.Apply(Request{Paths: Paths{JSON: path}, Codec: CodecDevin, Name: "owned", Action: ActionAdd, Server: server}) + if err != nil || receipt != desired { + t.Fatalf("current transport=%+v,%v", receipt, err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var document map[string]any + if err := json.Unmarshal(body, &document); err != nil { + t.Fatal(err) + } + entries := document["mcpServers"].(map[string]any) + managed := entries["owned"].(map[string]any) + expected := "http" + if transport == "sse" { + expected = "sse" + } + if managed["url"] != server.URL || managed["transport"] != expected || managed["serverUrl"] != nil || entries["disabled"].(map[string]any)["disabled"] != true || document["policy"] != "keep" { + t.Fatalf("native Devin shape=%s", body) + } + if _, err := kernel.Apply(Request{Paths: Paths{JSON: path}, Codec: CodecDevin, Name: "owned", Action: ActionRemove, Owned: &receipt}); err != nil { + t.Fatal(err) + } + } +} + +func TestDevinCodecRejectsUnsupportedNativeIntent(t *testing.T) { + for _, server := range []Server{ + {Type: "stdio", Command: "node", CWD: "/tmp"}, + {Type: "stdio", Command: "node", RemoteTransport: "sse"}, + {Type: "remote", URL: "https://managed.test"}, + {Type: "remote", URL: "https://managed.test", RemoteTransport: "websocket"}, + {Type: "remote", URL: "https://managed.test", RemoteTransport: "sse", OpenCodeV2: &OpenCodeV2Options{}}, + } { + if _, err := DesiredReceipt(filepath.Join(t.TempDir(), "mcp_config.json"), CodecDevin, "x", server, Placeholders{}); err == nil { + t.Fatalf("unsupported native intent accepted: %+v", server) + } + } +} + +func TestDevinNativeCommentsPreserveForeignBytesWithoutRelaxingLegacy(t *testing.T) { + path := filepath.Join(t.TempDir(), "mcp_config.json") + const original = `{ // target foreign comment + "mcpServers": {"foreign": { "url": "https://foreign.test", "disabled": true }}, + "policy": "keep" /* target policy comment */ +}` + if err := os.WriteFile(path, []byte(original), 0600); err != nil { + t.Fatal(err) + } + kernel := New() + server := Server{Type: "remote", URL: "https://managed.test", RemoteTransport: "streamable-http"} + receipt, err := kernel.Apply(Request{Paths: Paths{JSON: path}, Codec: CodecDevin, Name: "owned", Action: ActionAdd, Server: server}) + if err != nil { + t.Fatal(err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + for _, foreign := range []string{"// target foreign comment", `"foreign": { "url": "https://foreign.test", "disabled": true }`, `"policy": "keep" /* target policy comment */`} { + if !strings.Contains(string(body), foreign) { + t.Fatalf("comment/foreign bytes lost: %s", body) + } + } + if _, err := kernel.Apply(Request{Paths: Paths{JSON: path}, Codec: CodecDevin, Name: "owned", Action: ActionRemove, Owned: &receipt}); err != nil { + t.Fatal(err) + } + for _, invalid := range []string{`{"mcpServers":{},}`, `{"mcpServers":{"x":{},"x":{}}}`} { + if _, err := parseCodecDocument([]byte(invalid), false, CodecDevin); err == nil { + t.Fatalf("invalid native grammar accepted: %s", invalid) + } + if _, err := StandardizeCommentedJSON([]byte(invalid)); err == nil { + t.Fatalf("invalid policy grammar accepted: %s", invalid) + } + } + if _, err := parseCodecDocument([]byte(original), false, CodecWindsurf); err == nil { + t.Fatal("legacy Cascade codec grammar broadened") + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/document.go b/install/integrationctl/agentplugins/adapters/nativeconfig/document.go index e62abb7cb..895719597 100644 --- a/install/integrationctl/agentplugins/adapters/nativeconfig/document.go +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/document.go @@ -12,9 +12,11 @@ import ( ) type document struct { - ast hujson.Value - root *hujson.Object - jsonc bool + ast hujson.Value + root *hujson.Object + jsonc bool + commentsOnly bool + codexDocument CodexDocument } func parseDocument(body []byte, jsonc bool) (*document, error) { @@ -35,7 +37,12 @@ func parseDocument(body []byte, jsonc bool) (*document, error) { return &document{ast: ast, root: root, jsonc: jsonc}, nil } -func newDocument(jsonc bool) (*document, error) { return parseDocument([]byte("{}\n"), jsonc) } +func (kernel Kernel) newDocument(jsonc bool, codec Codec) (*document, error) { + if codec == CodecCodex { + return kernel.parseDocument(nil, jsonc, codec) + } + return kernel.parseDocument([]byte("{}\n"), jsonc, codec) +} func rejectDuplicateKeys(value *hujson.Value, path string) error { switch node := value.Value.(type) { @@ -146,11 +153,23 @@ func removeEntry(obj *hujson.Object, name string) bool { } func (doc *document) render() ([]byte, error) { + if doc.codexDocument != nil { + return doc.renderCodexDocument() + } + if doc.commentsOnly { + geminiRemoveTrailingCommas(&doc.ast) + } body := doc.ast.Pack() if !doc.jsonc && !json.Valid(body) { return nil, fmt.Errorf("rendered native config is not strict JSON") } - if _, err := parseDocument(body, doc.jsonc); err != nil { + var err error + if doc.commentsOnly { + _, err = parseCodecDocument(body, false, CodecGemini) + } else { + _, err = parseDocument(body, doc.jsonc) + } + if err != nil { return nil, fmt.Errorf("validate rendered native config: %w", err) } return body, nil diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/gemini_comments_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/gemini_comments_test.go new file mode 100644 index 000000000..e028d7e02 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/gemini_comments_test.go @@ -0,0 +1,78 @@ +package nativeconfig + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestProjectGeminiCommentedSettingsExactLifecycle(t *testing.T) { + path := filepath.Join(t.TempDir(), "settings.json") + before := "{\n // retain policy comment\n \"skills\":{\"disabled\":[\"foreign\"]},\n \"mcpServers\":{ /* retain foreign entry */ \"foreign\":{\"url\":\"https://foreign.test/mcp\"}}\n}\n" + mustWrite(t, path, before) + kernel := New() + request := Request{Paths: Paths{JSON: path}, Codec: CodecGemini, Action: ActionAdd, Name: "managed", Server: Server{Type: "remote", URL: "https://managed.test/mcp", RemoteTransport: "streamable-http"}} + receipt, err := kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + if present, owned, err := kernel.Inspect(request.Paths, request.Codec, request.Name, &receipt); err != nil || !present || !owned { + t.Fatalf("commented exact inspection = %v/%v, %v", present, owned, err) + } + request.Action, request.Owned = ActionUpdate, &receipt + request.Server.URL = "https://updated.test/mcp" + receipt, err = kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + // A foreign writer may add trivia before the preceding member's comma. + // Removing our last entry must not create a native-invalid trailing comma. + updated, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + foreign := `"foreign":{"url":"https://foreign.test/mcp"}` + mustWrite(t, path, strings.Replace(string(updated), foreign, foreign+" /* keep new foreign trivia */", 1)) + request.Action, request.Owned = ActionRemove, &receipt + if _, err := kernel.Apply(request); err != nil { + t.Fatal(err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + for _, preserved := range []string{"// retain policy comment", "/* retain foreign entry */", "/* keep new foreign trivia */", `"disabled":["foreign"]`, `"foreign":{"url":"https://foreign.test/mcp"}`} { + if !strings.Contains(string(body), preserved) { + t.Fatalf("lost %q in %s", preserved, body) + } + } + if present, _, err := kernel.Inspect(request.Paths, request.Codec, request.Name, nil); err != nil || present { + t.Fatalf("removed project entry still present = %v, %v", present, err) + } +} + +func TestProjectGeminiCommentsDoNotBroadenInvalidJSON(t *testing.T) { + for name, body := range map[string]string{ + "root comma": `{"mcpServers":{},}`, + "nested comma": `{"mcpServers":{"foreign":{"args":["x",]}}}`, + "duplicate": `{"mcpServers":{},"mcpServers":{}}`, + "malformed comment": "{/* incomplete", + } { + t.Run(name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "settings.json") + mustWrite(t, path, body) + request := Request{Paths: Paths{JSON: path}, Codec: CodecGemini, Action: ActionAdd, Name: "managed", Server: Server{Type: "stdio", Command: "TEST-never-executed"}} + if _, err := New().Apply(request); !errors.Is(err, ErrMalformed) { + t.Fatalf("invalid settings accepted: %v", err) + } + assertBytes(t, path, body) + }) + } + path := filepath.Join(t.TempDir(), "settings.json") + mustWrite(t, path, "{// comment\n\"mcpServers\":{}}") + if _, _, err := New().Inspect(Paths{JSON: path}, CodecMCPServers, "managed", nil); !errors.Is(err, ErrMalformed) { + t.Fatalf("generic JSON codec was broadened: %v", err) + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/gemini_document.go b/install/integrationctl/agentplugins/adapters/nativeconfig/gemini_document.go new file mode 100644 index 000000000..0814c3e00 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/gemini_document.go @@ -0,0 +1,86 @@ +package nativeconfig + +import ( + "fmt" + + "github.com/tailscale/hujson" +) + +// Gemini v0.65.0 and Devin3000.11.205 accept native JSON comments. Keep +// the original AST so mutations preserve comments and unrelated configuration; +// neither this admission nor a read-only policy check accepts trailing commas. +func parseCodecDocument(body []byte, jsonc bool, codec Codec) (*document, error) { + if codec == CodecCodex { + return nil, fmt.Errorf("codex TOML document parser is required") + } + // VS Code's actual mcp.json accepts comments and trailing commas. It has + // no alternative JSONC filename; preserve its native AST in that one file. + if codec == CodecVSCode { + return parseDocument(body, true) + } + if (codec != CodecGemini && codec != CodecDevin) || jsonc { + return parseDocument(body, jsonc) + } + label := "Gemini" + if codec == CodecDevin { + label = "Devin" + } + return parseCommentsOnlyDocument(body, label) +} + +func parseCommentsOnlyDocument(body []byte, label string) (*document, error) { + doc, err := parseDocument(body, true) + if err != nil { + return nil, err + } + for value := range doc.ast.All() { + if !geminiWithoutTrailingComma(value) { + return nil, fmt.Errorf("%w: %s settings do not support trailing commas", ErrMalformed, label) + } + } + doc.commentsOnly = true + return doc, nil +} + +func geminiWithoutTrailingComma(value *hujson.Value) bool { + switch node := value.Value.(type) { + case *hujson.Object: + return len(node.Members) == 0 || node.Members[len(node.Members)-1].Value.AfterExtra == nil + case *hujson.Array: + return len(node.Elements) == 0 || node.Elements[len(node.Elements)-1].AfterExtra == nil + default: + return true + } +} + +// Deleting a last member can turn the preceding member's comma into a trailing +// comma. Input validation already rejected native-invalid commas; keep the +// surviving foreign trivia while removing only commas created by mutation. +func geminiRemoveTrailingCommas(ast *hujson.Value) { + for value := range ast.All() { + switch node := value.Value.(type) { + case *hujson.Object: + if len(node.Members) > 0 { + last := &node.Members[len(node.Members)-1].Value + node.AfterExtra = append(last.AfterExtra, node.AfterExtra...) + last.AfterExtra = nil + } + case *hujson.Array: + if len(node.Elements) > 0 { + last := &node.Elements[len(node.Elements)-1] + node.AfterExtra = append(last.AfterExtra, node.AfterExtra...) + last.AfterExtra = nil + } + } + } +} + +// StandardizeCommentedJSON normalizes native JSON-with-comments grammar for +// read-only policy inspection. Duplicate keys and trailing commas refuse; +// callers retain original file bytes for mutation and ownership checks. +func StandardizeCommentedJSON(body []byte) ([]byte, error) { + if _, err := parseCommentsOnlyDocument(body, "native config"); err != nil { + return nil, err + } + return hujson.Standardize(body) +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/kernel.go b/install/integrationctl/agentplugins/adapters/nativeconfig/kernel.go index 02cfa33f9..9c585f511 100644 --- a/install/integrationctl/agentplugins/adapters/nativeconfig/kernel.go +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/kernel.go @@ -60,6 +60,9 @@ func (kernel Kernel) ApplyBatch(requests []Request) (receipts []Receipt, err err return nil, fmt.Errorf("batched native config requests must share paths and codec") } } + if err := kernel.requireDocumentParser(requests[0].Codec); err != nil { + return nil, err + } acquireLocks := kernel.acquireLocks if acquireLocks == nil { acquireLocks = kernel.acquireCandidateLocks @@ -92,9 +95,11 @@ func (kernel Kernel) ApplyBatch(requests []Request) (receipts []Receipt, err err if err != nil { return nil, err } - doc, err := newDocument(file.jsonc) + var doc *document if file.exists { - doc, err = parseDocument(file.body, file.jsonc) + doc, err = kernel.parseDocument(file.body, file.jsonc, requests[0].Codec) + } else { + doc, err = kernel.newDocument(file.jsonc, requests[0].Codec) } if err != nil { return nil, err @@ -247,11 +252,14 @@ func (kernel Kernel) Inspect(paths Paths, codec Codec, name string, owned *Recei if err := validateRequest(probe); err != nil { return false, false, err } + if err := kernel.requireDocumentParser(codec); err != nil { + return false, false, err + } file, err := kernel.resolve(paths) if err != nil || !file.exists { return false, false, err } - doc, err := parseDocument(file.body, file.jsonc) + doc, err := kernel.parseDocument(file.body, file.jsonc, codec) if err != nil { return false, false, err } diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/kimi_project.go b/install/integrationctl/agentplugins/adapters/nativeconfig/kimi_project.go new file mode 100644 index 000000000..3cfa0778e --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/kimi_project.go @@ -0,0 +1,29 @@ +package nativeconfig + +import ( + "fmt" + "strings" +) + +// Current Kimi Code project MCP uses transport:stdio/http/sse. Keep this +// project-only dialect separate from the preserved user plugin manifest. +func projectKimiServer(server Server, placeholders Placeholders) (map[string]any, error) { + transport := "stdio" + if strings.EqualFold(strings.TrimSpace(server.Type), "remote") { + switch server.RemoteTransport { + case "streamable-http": + transport = "http" + case "sse": + transport = "sse" + default: + return nil, fmt.Errorf("kimi project remote MCP requires streamable-http or sse transport") + } + server.RemoteTransport = "" + } + entry, err := projectBaseServer(CodecMCPServers, server, placeholders) + if err != nil { + return nil, err + } + entry["transport"] = transport + return entry, nil +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/kimi_project_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/kimi_project_test.go new file mode 100644 index 000000000..1984c9f8a --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/kimi_project_test.go @@ -0,0 +1,66 @@ +package nativeconfig + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestProjectKimiCodecPreservesExplicitTransportsAndForeignMembers(t *testing.T) { + path := filepath.Join(t.TempDir(), ".mcp.json") + const foreign = `{"mcpServers":{"foreign":{"transport":"http","url":"https://foreign.test/mcp","enabled":false}},"foreignPreference":"keep"}` + mustWrite(t, path, foreign) + kernel := New() + request := Request{Paths: Paths{JSON: path}, Codec: CodecKimi, Action: ActionAdd, Name: "managed", + Server: Server{Type: "remote", RemoteTransport: "streamable-http", URL: "https://managed.test/mcp", Headers: map[string]string{"X-Fixture": "inert"}}} + receipt, err := kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + for _, transport := range []struct { + native string + server Server + }{{"http", request.Server}, {"sse", Server{Type: "remote", RemoteTransport: "sse", URL: "https://managed.test/events"}}, {"stdio", Server{Type: "stdio", Command: "TEST-never-executed", Args: []string{"inert"}}}} { + request.Action, request.Owned, request.Server = ActionUpdate, &receipt, transport.server + receipt, err = kernel.Apply(request) + if err != nil { + t.Fatal(err) + } + present, owned, err := kernel.Inspect(request.Paths, request.Codec, request.Name, &receipt) + body, readErr := os.ReadFile(path) + var observed struct { + Servers map[string]struct{ Transport string } `json:"mcpServers"` + } + decodeErr := json.Unmarshal(body, &observed) + if err != nil || readErr != nil || decodeErr != nil || !present || !owned || observed.Servers["managed"].Transport != transport.native || !strings.Contains(string(body), "foreign.test") || !strings.Contains(string(body), "foreignPreference") { + t.Fatalf("Kimi native transport %s = %s, %v, %v", transport.native, body, err, readErr) + } + } + request.Action, request.Owned = ActionRemove, &receipt + if _, err := kernel.Apply(request); err != nil { + t.Fatal(err) + } + if present, _, err := kernel.Inspect(request.Paths, request.Codec, request.Name, nil); err != nil || present { + t.Fatalf("Kimi exact removal = %t, %v", present, err) + } + request.Action, request.Name, request.Owned = ActionAdd, "foreign", nil + if _, err := kernel.Apply(request); !errors.Is(err, ErrCollision) { + t.Fatalf("disabled foreign Kimi server was adopted: %v", err) + } +} + +func TestProjectKimiCodecRejectsUnknownRemoteTransport(t *testing.T) { + path := filepath.Join(t.TempDir(), "mcp.json") + for _, server := range []Server{ + {Type: "remote", URL: "https://managed.test/mcp"}, + {Type: "remote", URL: "https://managed.test/mcp", RemoteTransport: "unknown"}, + {Type: "stdio", Command: "TEST-never-executed", RemoteTransport: "sse"}, + } { + if _, err := DesiredReceipt(path, CodecKimi, "managed", server, Placeholders{}); err == nil { + t.Fatalf("unqualified native transport admitted: %+v", server) + } + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/project.go b/install/integrationctl/agentplugins/adapters/nativeconfig/project.go index b19769f80..9425cc107 100644 --- a/install/integrationctl/agentplugins/adapters/nativeconfig/project.go +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/project.go @@ -37,15 +37,33 @@ func DesiredReceipt(resolvedPath string, codec Codec, name string, server Server } func projectServer(codec Codec, server Server, placeholders Placeholders) (map[string]any, error) { + if codec != CodecCodex && server.CodexAuth != nil { + return nil, fmt.Errorf("%s does not accept Codex authentication options", codec) + } if !supportedCodec(codec) { return nil, fmt.Errorf("unsupported native config codec %q", codec) } if codec == CodecOpenCodeV2 { return projectOpenCodeV2Server(server, placeholders) } + if codec == CodecCodex { + return projectCodexServer(server, placeholders) + } if server.OpenCodeV2 != nil { return nil, fmt.Errorf("%s does not accept OpenCode V2 options", codec) } + if codec == CodecKimi { + return projectKimiServer(server, placeholders) + } + if codec == CodecDevin { + return projectDevinServer(server, placeholders) + } + if codec == CodecCopilot { + return projectCopilotServer(server, placeholders) + } + if codec == CodecVSCode { + return projectVSCodeServer(server, placeholders) + } return projectBaseServer(codec, server, placeholders) } diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/types.go b/install/integrationctl/agentplugins/adapters/nativeconfig/types.go index 52a07375a..3307dfada 100644 --- a/install/integrationctl/agentplugins/adapters/nativeconfig/types.go +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/types.go @@ -17,10 +17,15 @@ const ( // command/args/env/cwd and remote uses url/headers with no transport tag. // Clients with different native transport keys require their own codec. CodecMCPServers Codec = "mcpServers" + CodecCopilot Codec = "copilot-mcpServers" + CodecVSCode Codec = "vscode-project-servers" + CodecKimi Codec = "kimi-project-mcpServers" CodecGemini Codec = "gemini-mcpServers" + CodecCodex Codec = "codex-mcp-servers" CodecOpenCode Codec = "opencode-mcp" CodecOpenCodeV2 Codec = "opencode-v2-mcp" CodecWindsurf Codec = "windsurf-mcpServers" + CodecDevin Codec = "devin-mcpServers" CodecCline Codec = "cline-mcpServers" ) @@ -84,6 +89,13 @@ type Server struct { // OpenCodeV2 carries only the pinned V2 disabled/timeout contract. Other // codecs reject it rather than silently discard dialect-specific intent. OpenCodeV2 *OpenCodeV2Options `json:"opencode_v2,omitempty"` + // CodexAuth contains only nonsecret references from compatibility hints. + CodexAuth *CodexAuthOptions `json:"codex_auth,omitempty"` +} + +type CodexAuthOptions struct { + BearerTokenEnvVar string `json:"bearer_token_env_var,omitempty"` + OAuthResource string `json:"oauth_resource,omitempty"` } // OpenCodeV2Options is a closed subset of the pinned native MCP entry schema. @@ -167,6 +179,7 @@ type lockAcquirer func(Paths, Codec) (func() error, error) type Kernel struct { files FileIO acquireLocks lockAcquirer + codexParser CodexDocumentParser } func New() Kernel { return Kernel{files: conditionalOSFiles{}} } @@ -178,6 +191,12 @@ func NewWithLockAcquirer(acquireLocks func(Paths, Codec) (func() error, error)) } func validateRequest(req Request) error { + if req.Codec == CodecCodex && req.Paths.JSONC != "" { + return fmt.Errorf("codex requires one TOML config path") + } + if req.Codec == CodecVSCode && req.Paths.JSONC != "" { + return fmt.Errorf("VS Code requires one mcp.json config path with native JSONC syntax") + } if err := validateExactPath(req.Paths.JSON, "JSON native config path"); err != nil { return err } @@ -208,10 +227,16 @@ func validateRequest(req Request) error { } func supportedCodec(codec Codec) bool { - return codec == CodecMCPServers || codec == CodecGemini || codec == CodecOpenCode || codec == CodecOpenCodeV2 || codec == CodecWindsurf || codec == CodecCline + return codec == CodecMCPServers || codec == CodecCopilot || codec == CodecVSCode || codec == CodecKimi || codec == CodecGemini || codec == CodecCodex || codec == CodecOpenCode || codec == CodecOpenCodeV2 || codec == CodecWindsurf || codec == CodecDevin || codec == CodecCline } func codecCollectionKey(codec Codec) string { + if codec == CodecVSCode { + return "servers" + } + if codec == CodecCodex { + return "mcp_servers" + } if codec == CodecOpenCode || codec == CodecOpenCodeV2 { return "mcp" } diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project.go b/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project.go new file mode 100644 index 000000000..71a00da68 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project.go @@ -0,0 +1,29 @@ +package nativeconfig + +import ( + "fmt" + "strings" +) + +// VS Code's project servers use type:stdio/http/sse. This is independent of +// Copilot's mcpServers dialect: cwd is supported and no tools policy is added. +func projectVSCodeServer(server Server, placeholders Placeholders) (map[string]any, error) { + nativeType := "stdio" + if strings.EqualFold(strings.TrimSpace(server.Type), "remote") { + switch server.RemoteTransport { + case "streamable-http": + nativeType = "http" + case "sse": + nativeType = "sse" + default: + return nil, fmt.Errorf("VS Code project remote MCP requires streamable-http or sse transport") + } + server.RemoteTransport = "" + } + entry, err := projectBaseServer(CodecMCPServers, server, placeholders) + if err != nil { + return nil, err + } + entry["type"] = nativeType + return entry, nil +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project_admission_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project_admission_test.go new file mode 100644 index 000000000..410741296 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project_admission_test.go @@ -0,0 +1,100 @@ +package nativeconfig + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestVSCodeProjectCodecRefusesForeignAndModifiedMembers(t *testing.T) { + path := filepath.Join(t.TempDir(), "mcp.json") + mustWrite(t, path, vscodeForeignDocument) + req := Request{Paths: Paths{JSON: path}, Codec: CodecVSCode, Action: ActionAdd, Name: "foreign", + Server: Server{Type: "stdio", Command: "TEST-never-executed"}} + kernel := New() + if _, err := kernel.Apply(req); !errors.Is(err, ErrCollision) { + t.Fatalf("foreign numeric-env/disabled member adopted: %v", err) + } + req.Name = "managed" + receipt, err := kernel.Apply(req) + if err != nil { + t.Fatal(err) + } + wrong := receipt + wrong.Codec = CodecCopilot + req.Action, req.Owned = ActionUpdate, &wrong + if _, err := kernel.Apply(req); !errors.Is(err, ErrNotOwned) { + t.Fatalf("Copilot receipt admitted to VS Code member: %v", err) + } + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + changed := strings.Replace(string(body), `"command":"TEST-never-executed"`, `"command":"TEST-never-executed","foreignMember":true`, 1) + if changed == string(body) { + t.Fatal("test did not modify the managed entry") + } + mustWrite(t, path, changed) + req.Owned = &receipt + for _, action := range []Action{ActionUpdate, ActionRemove} { + req.Action = action + if _, err := kernel.Apply(req); !errors.Is(err, ErrNotOwned) { + t.Fatalf("changed complete entry accepted for %s: %v", action, err) + } + current, err := os.ReadFile(path) + if err != nil || string(current) != changed { + t.Fatalf("refusal changed native bytes: %v", err) + } + } +} + +func TestVSCodeProjectCodecRejectsUnsupportedDialectAndAlternatePath(t *testing.T) { + path := filepath.Join(t.TempDir(), "mcp.json") + for _, server := range []Server{ + {Type: "remote", URL: "https://inert.test/mcp"}, + {Type: "remote", URL: "https://inert.test/mcp", RemoteTransport: "http"}, + {Type: "remote", URL: "https://inert.test/mcp", RemoteTransport: "sse", CWD: filepath.Dir(path)}, + {Type: "stdio", Command: "inert", RemoteTransport: "sse"}, + {Type: "stdio", Command: "inert", CodexAuth: &CodexAuthOptions{BearerTokenEnvVar: "TOKEN"}}, + {Type: "stdio", Command: "inert", OpenCodeV2: &OpenCodeV2Options{}}, + } { + if _, err := DesiredReceipt(path, CodecVSCode, "managed", server, Placeholders{}); err == nil { + t.Fatalf("unsupported VS Code dialect admitted: %+v", server) + } + } + locks, err := WriterLockPaths(Paths{JSON: path}, CodecVSCode) + if err != nil || len(locks) != 1 || locks[0] != path+".agentplugins.lock" { + t.Fatalf("VS Code exact writer lock = %q, %v", locks, err) + } + paths := Paths{JSON: path, JSONC: path + "c"} + if _, err := WriterLockPaths(paths, CodecVSCode); err == nil { + t.Fatal("invented alternate VS Code config path obtained writer authority") + } + called := false + kernel := NewWithLockAcquirer(func(Paths, Codec) (func() error, error) { + called = true + return func() error { return nil }, nil + }) + req := Request{Paths: paths, Codec: CodecVSCode, Action: ActionAdd, Name: "managed", Server: Server{Type: "stdio", Command: "inert"}} + if _, err := kernel.Apply(req); err == nil || called { + t.Fatalf("alternate path reached mutation authority: %v, locked=%t", err, called) + } + if _, _, err := New().Inspect(paths, CodecVSCode, "managed", nil); err == nil { + t.Fatal("alternate path admitted by read-only inspection") + } + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("refused path created state: %v", err) + } +} + +func TestVSCodeProjectJSONCStillRejectsMalformedOrDuplicateMembers(t *testing.T) { + for _, body := range []string{`{"servers":[]}`, `{"servers":{},"servers":{}}`, `{"servers":{"foreign":{"env":{"PORT":1,"PORT":2}}}}`} { + path := filepath.Join(t.TempDir(), "mcp.json") + mustWrite(t, path, body) + if _, _, err := New().Inspect(Paths{JSON: path}, CodecVSCode, "managed", nil); !errors.Is(err, ErrMalformed) { + t.Fatalf("malformed native JSONC admitted: %v", err) + } + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project_test.go b/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project_test.go new file mode 100644 index 000000000..f57aad426 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/vscode_project_test.go @@ -0,0 +1,102 @@ +package nativeconfig + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/tailscale/hujson" +) + +const vscodeForeignDocument = `{ + // retain this native workspace comment + "inputs": [{"id":"token", "type":"promptString", "description":"Native input",},], + "sandbox": {"enabled":true,}, + "servers": { + "foreign": {"type":"stdio", "command":"foreign", "env":{"PORT":8123}, "disabled":true,}, + }, + "mcpServers": {"copilot-marker":{"command":"keep-separate"}}, +}` + +func readVSCodeDocument(t *testing.T, path string) (map[string]any, string) { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + original := string(body) + standard, err := hujson.Standardize(body) + if err != nil { + t.Fatal(err) + } + var document map[string]any + if err := json.Unmarshal(standard, &document); err != nil { + t.Fatal(err) + } + return document, original +} + +func requireVSCodeForeignPreserved(t *testing.T, path string, before map[string]any) map[string]any { + t.Helper() + document, body := readVSCodeDocument(t, path) + servers := document["servers"].(map[string]any) + managed, _ := servers["managed"].(map[string]any) + delete(servers, "managed") + if !reflect.DeepEqual(document, before) || !strings.Contains(body, "// retain this native workspace comment") { + t.Fatalf("VS Code mutation changed foreign values or comment: %s", body) + } + return managed +} + +func TestVSCodeProjectCodecOwnedLifecyclePreservesNativeJSONC(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "mcp.json") + mustWrite(t, path, vscodeForeignDocument) + before, _ := readVSCodeDocument(t, path) + placeholders := Placeholders{PackageRoot: filepath.Join(root, "package-${PLUGIN_DATA}"), DataRoot: filepath.Join(root, "data")} + stdio := Server{Type: "stdio", Command: "TEST-never-executed", Args: []string{"${PLUGIN_ROOT}", "${PLUGIN_DATA}"}, + Env: map[string]string{"CACHE": "${PLUGIN_DATA}/cache", "TOKEN": "${input:token}"}, CWD: "${PLUGIN_ROOT}"} + req := Request{Paths: Paths{JSON: path}, Codec: CodecVSCode, Action: ActionAdd, Name: "managed", Server: stdio, Placeholders: placeholders} + desired, err := DesiredReceipt(path, CodecVSCode, req.Name, stdio, placeholders) + if err != nil { + t.Fatal(err) + } + req.Desired = &desired + kernel := New() + receipt, err := kernel.Apply(req) + if err != nil || receipt != desired { + t.Fatalf("VS Code staged/apply receipt differs: %+v, %v", receipt, err) + } + managed := requireVSCodeForeignPreserved(t, path, before) + want := map[string]any{"type": "stdio", "command": stdio.Command, "args": []any{placeholders.PackageRoot, placeholders.DataRoot}, + "env": map[string]any{"CACHE": placeholders.DataRoot + "/cache", "TOKEN": "${input:token}"}, "cwd": placeholders.PackageRoot} + if !reflect.DeepEqual(managed, want) { + t.Fatalf("VS Code stdio projection = %#v, want %#v", managed, want) + } + for _, transport := range []struct{ portable, native string }{{"streamable-http", "http"}, {"sse", "sse"}} { + req.Action, req.Owned, req.Desired = ActionUpdate, &receipt, nil + req.Server = Server{Type: "remote", RemoteTransport: transport.portable, URL: "https://inert.test/mcp", Headers: map[string]string{"X-Fixture": "${input:token}"}} + receipt, err = kernel.Apply(req) + if err != nil { + t.Fatal(err) + } + managed = requireVSCodeForeignPreserved(t, path, before) + want = map[string]any{"type": transport.native, "url": req.Server.URL, "headers": map[string]any{"X-Fixture": "${input:token}"}} + if !reflect.DeepEqual(managed, want) { + t.Fatalf("VS Code remote projection = %#v, want %#v", managed, want) + } + if present, owned, err := kernel.Inspect(req.Paths, req.Codec, req.Name, &receipt); err != nil || !present || !owned { + t.Fatalf("native JSONC ownership = %t/%t, %v", present, owned, err) + } + } + req.Action, req.Owned = ActionRemove, &receipt + if _, err := kernel.Apply(req); err != nil { + t.Fatal(err) + } + if managed := requireVSCodeForeignPreserved(t, path, before); managed != nil { + t.Fatal("exact owned member survived removal") + } +} diff --git a/install/integrationctl/agentplugins/adapters/nativeconfig/writer_lock_paths.go b/install/integrationctl/agentplugins/adapters/nativeconfig/writer_lock_paths.go index 467873e8b..d0611f284 100644 --- a/install/integrationctl/agentplugins/adapters/nativeconfig/writer_lock_paths.go +++ b/install/integrationctl/agentplugins/adapters/nativeconfig/writer_lock_paths.go @@ -38,6 +38,9 @@ type writerLockTarget struct { } func writerLockTargets(paths Paths, codec Codec) ([]writerLockTarget, error) { + if codec == CodecVSCode && paths.JSONC != "" { + return nil, fmt.Errorf("VS Code requires one mcp.json config path with native JSONC syntax") + } if err := validateExactPath(paths.JSON, "JSON native config path"); err != nil { return nil, err } diff --git a/install/integrationctl/agentplugins/adapters/securityscan/cache.go b/install/integrationctl/agentplugins/adapters/securityscan/cache.go index e241b1cb6..bb5f7213b 100644 --- a/install/integrationctl/agentplugins/adapters/securityscan/cache.go +++ b/install/integrationctl/agentplugins/adapters/securityscan/cache.go @@ -9,9 +9,9 @@ import ( "io" "os" "path/filepath" - "runtime" "strings" + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/atomicfile" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) @@ -31,7 +31,7 @@ func CacheKey(subject domain.SecuritySubject, requirement domain.SecurityRequire } func (cache FileCache) Load(key string) (domain.SecurityAssessment, bool) { - body, err := os.ReadFile(filepath.Join(cache.Root, key+".json")) + body, err := atomicfile.ReadFile(filepath.Join(cache.Root, key+".json")) if err != nil || len(body) > 1<<20 { return domain.SecurityAssessment{}, false } @@ -81,10 +81,7 @@ func (cache FileCache) Store(key string, assessment domain.SecurityAssessment) e return err } target := filepath.Join(cache.Root, key+".json") - if runtime.GOOS == "windows" { - _ = os.Remove(target) - } - if err := os.Rename(temporaryName, target); err != nil { + if err := atomicfile.Replace(temporaryName, target); err != nil { return fmt.Errorf("commit security cache entry: %w", err) } return nil diff --git a/install/integrationctl/agentplugins/adapters/securityscan/cache_publication_windows_test.go b/install/integrationctl/agentplugins/adapters/securityscan/cache_publication_windows_test.go new file mode 100644 index 000000000..55c0083e9 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/securityscan/cache_publication_windows_test.go @@ -0,0 +1,78 @@ +//go:build windows + +package securityscan + +import ( + "bytes" + "encoding/json" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestWindowsAtomicPublicationSecurityCache(t *testing.T) { + cache := FileCache{Root: filepath.Join(t.TempDir(), "assessments")} + key := strings.Repeat("a", 64) + requirement := DefaultRequirement() + subject := domain.SecuritySubject{TreeDigest: testTreeDigest, ManifestDigest: testManifestDigest} + first := domain.SecurityAssessment{ + SchemaVersion: 1, Subject: subject, Scanner: requirement.Scanner, Policy: requirement.Policy, + Outcome: domain.SecurityNoBlockingFindings, ScannedFiles: 1, ReportDigest: testTreeDigest, + Evidence: domain.SecurityEvidenceLocalScan, + } + if err := first.Validate(requirement, subject); err != nil { + t.Fatal(err) + } + if err := cache.Store(key, first); err != nil { + t.Fatal(err) + } + path := filepath.Join(cache.Root, key+".json") + before, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + root, err := os.OpenRoot(cache.Root) + if err != nil { + t.Fatal(err) + } + reader, openErr := root.Open(filepath.Base(path)) + closeErr := root.Close() + if openErr != nil || closeErr != nil { + if reader != nil { + _ = reader.Close() + } + t.Fatalf("open controlled cache reader: %v %v", openErr, closeErr) + } + t.Cleanup(func() { + if err := reader.Close(); err != nil { + t.Error(err) + } + }) + second := first + second.ScannedFiles = 2 + if err := cache.Store(key, second); err != nil { + t.Fatal(err) + } + old, err := io.ReadAll(reader) + if err != nil || !bytes.Equal(old, before) { + t.Fatal("cache publication changed bytes behind the retained reader") + } + loaded, ok := cache.Load(key) + if !ok || !reflect.DeepEqual(loaded, second) { + t.Fatal("fresh cache reader did not observe the complete new assessment") + } + after, err := os.ReadFile(path) + want, marshalErr := json.Marshal(second) + if err != nil || marshalErr != nil || !bytes.Equal(after, want) { + t.Fatal("cache target is not the complete encoded assessment") + } + entries, err := os.ReadDir(cache.Root) + if err != nil || len(entries) != 1 || entries[0].Name() != key+".json" { + t.Fatal("cache publication leaked a temporary file or removed its target") + } +} diff --git a/install/integrationctl/agentplugins/adapters/sourceacquisition/acquirer.go b/install/integrationctl/agentplugins/adapters/sourceacquisition/acquirer.go index 33f6cb9f9..17d0b17d6 100644 --- a/install/integrationctl/agentplugins/adapters/sourceacquisition/acquirer.go +++ b/install/integrationctl/agentplugins/adapters/sourceacquisition/acquirer.go @@ -86,10 +86,7 @@ func (OSRunner) Run(ctx context.Context, command Command) ([]byte, error) { return nil, fmt.Errorf("git %s failed: %w: %s", strings.Join(command.Args, " "), err, strings.TrimSpace(string(output))) } if result.ExitCode != 0 { - if command.NativeCredentials { - return nil, fmt.Errorf("native Git authentication or transport failed (exit %d); configure Git credentials or SSH access and retry", result.ExitCode) - } - return nil, fmt.Errorf("git %s failed with exit code %d: %s", strings.Join(command.Args, " "), result.ExitCode, strings.TrimSpace(string(output))) + return nil, gitTransportExitFailure(command, result.ExitCode, output) } // Blob callers require byte-exact stdout; diagnostic stderr is never content. return append([]byte(nil), result.Stdout...), nil diff --git a/install/integrationctl/agentplugins/adapters/sourceacquisition/git_exit_error.go b/install/integrationctl/agentplugins/adapters/sourceacquisition/git_exit_error.go new file mode 100644 index 000000000..872be65be --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/sourceacquisition/git_exit_error.go @@ -0,0 +1,27 @@ +package sourceacquisition + +import ( + "fmt" + "strings" +) + +// gitTransportExitError marks a nonzero Git result after process supervision +// itself succeeded. A generic process ExitCode cannot establish that fact. +type gitTransportExitError struct { + cause error + code int +} + +func (err *gitTransportExitError) Error() string { return err.cause.Error() } +func (err *gitTransportExitError) Unwrap() error { return err.cause } +func (err *gitTransportExitError) GitTransportExitCode() int { return err.code } + +func gitTransportExitFailure(command Command, code int, output []byte) error { + var cause error + if command.NativeCredentials { + cause = fmt.Errorf("native Git authentication or transport failed (exit %d); configure Git credentials or SSH access and retry", code) + } else { + cause = fmt.Errorf("git %s failed with exit code %d: %s", strings.Join(command.Args, " "), code, strings.TrimSpace(string(output))) + } + return &gitTransportExitError{cause: cause, code: code} +} diff --git a/install/integrationctl/agentplugins/adapters/sourceacquisition/git_exit_error_test.go b/install/integrationctl/agentplugins/adapters/sourceacquisition/git_exit_error_test.go new file mode 100644 index 000000000..63ae8d59f --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/sourceacquisition/git_exit_error_test.go @@ -0,0 +1,40 @@ +package sourceacquisition + +import ( + "errors" + "fmt" + "reflect" + "testing" +) + +func TestGitTransportExitFailureRetainsExactExistingMessages(t *testing.T) { + for _, tc := range []struct { + native bool + want string + }{ + {false, "git fetch secret-argument failed with exit code 128: secret stream"}, + {true, "native Git authentication or transport failed (exit 128); configure Git credentials or SSH access and retry"}, + } { + err := gitTransportExitFailure(Command{Args: []string{"fetch", "secret-argument"}, NativeCredentials: tc.native}, 128, []byte(" \nsecret stream\n ")) + if err.Error() != tc.want { + t.Fatal("clean Git exit changed its existing message") + } + var status interface{ GitTransportExitCode() int } + if !errors.As(err, &status) || status.GitTransportExitCode() != 128 { + t.Fatal("clean Git exit lost its structural status") + } + } +} + +func TestGitTransportExitCarrierPreservesCauseAndSignedStatus(t *testing.T) { + original := errors.New("inert original cause") + carrier := &gitTransportExitError{cause: original, code: -1} + wrapped := fmt.Errorf("existing outer context: %w", carrier) + if !errors.Is(wrapped, original) || reflect.ValueOf(carrier.Unwrap()) != reflect.ValueOf(original) || carrier.Error() != original.Error() { + t.Fatal("Git exit carrier changed its original cause") + } + var status interface{ GitTransportExitCode() int } + if !errors.As(wrapped, &status) || status.GitTransportExitCode() != -1 { + t.Fatal("wrapped Git status was truncated or changed") + } +} diff --git a/install/integrationctl/agentplugins/adapters/sourceacquisition/git_transport_test.go b/install/integrationctl/agentplugins/adapters/sourceacquisition/git_transport_test.go index e2c45c9d8..193cec764 100644 --- a/install/integrationctl/agentplugins/adapters/sourceacquisition/git_transport_test.go +++ b/install/integrationctl/agentplugins/adapters/sourceacquisition/git_transport_test.go @@ -110,7 +110,10 @@ func TestNativeHTTPSGitPublicAndPrivateCredentialHelper(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", home) endpoint := server.URL + "/repo.git" source := sourcespec.Git{URL: endpoint} - acquirer := Acquirer{TempRoot: t.TempDir()} + diagnostic := &nativeTransportDiagnosticRunner{delegate: OSRunner{}, operation: "resolve", report: func(operation, class, status string) { + t.Logf("native HTTPS Git diagnostic: operation=%s class=%s exit_status=%s", operation, class, status) + }} + acquirer := Acquirer{TempRoot: t.TempDir(), Runner: diagnostic} configureNativeFixtureCredentials(t, home, server.URL, false) if private { _, err := acquirer.ResolveGit(t.Context(), source, "default", false) @@ -126,6 +129,7 @@ func TestNativeHTTPSGitPublicAndPrivateCredentialHelper(t *testing.T) { if resolved.Resolution.Commit != commit { t.Fatalf("resolved %s, expected %s", resolved.Resolution.Commit, commit) } + diagnostic.operation = "acquire" snapshot, err := acquirer.AcquireGit(t.Context(), source, commit, "") if err != nil { t.Fatal(err) diff --git a/install/integrationctl/agentplugins/adapters/sourceacquisition/native_git_diagnostic_contract_test.go b/install/integrationctl/agentplugins/adapters/sourceacquisition/native_git_diagnostic_contract_test.go new file mode 100644 index 000000000..df965d1ad --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/sourceacquisition/native_git_diagnostic_contract_test.go @@ -0,0 +1,87 @@ +package sourceacquisition + +import ( + "context" + "errors" + "os/exec" + "reflect" + "testing" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" +) + +type nativeDiagnosticDelegate func(context.Context, Command) ([]byte, error) + +func (delegate nativeDiagnosticDelegate) Run(ctx context.Context, command Command) ([]byte, error) { + return delegate(ctx, command) +} + +type nativeDiagnosticSecretError struct{} +type nativeDiagnosticContextKey struct{} + +func (*nativeDiagnosticSecretError) Error() string { + panic("native diagnostic must not read error payload") +} + +func TestNativeTransportDiagnosticPreservesIdentityAndFailureBound(t *testing.T) { + ctx := context.WithValue(t.Context(), nativeDiagnosticContextKey{}, "secret context") + command := Command{Dir: "/secret/path", Args: []string{"secret command", "secret argument"}, Stdin: []byte("secret input"), NativeCredentials: true, MaxOutputBytes: 73} + body, original := []byte("secret output"), &nativeDiagnosticSecretError{} + calls, reports := 0, 0 + runner := &nativeTransportDiagnosticRunner{operation: "acquire", delegate: nativeDiagnosticDelegate(func(got context.Context, input Command) ([]byte, error) { + calls++ + if got != ctx || !reflect.DeepEqual(input, command) || &input.Args[0] != &command.Args[0] || &input.Stdin[0] != &command.Stdin[0] { + t.Fatal("native diagnostic changed context/command authority") + } + return body, original + }), report: func(operation, class, status string) { + reports++ + if operation != "acquire" || class != "other_git_or_process" || status != "unavailable" { + t.Fatal("native diagnostic emitted nonfixed payload") + } + }} + for range 6 { + got, err := runner.Run(ctx, command) + if reflect.ValueOf(err) != reflect.ValueOf(original) || &got[0] != &body[0] { + t.Fatal("native diagnostic changed original bytes/error") + } + } + if calls != 6 || reports != 4 || runner.failures.Load() != 6 { + t.Fatal("native diagnostic changed call/failure/log bounds") + } +} + +func TestNativeTransportDiagnosticSuccessAndClosedClasses(t *testing.T) { + runner := &nativeTransportDiagnosticRunner{delegate: nativeDiagnosticDelegate(func(context.Context, Command) ([]byte, error) { + return []byte("success"), nil + }), report: func(string, string, string) { t.Fatal("successful native command logged") }} + if _, err := runner.Run(t.Context(), Command{}); err != nil || runner.failures.Load() != 0 { + t.Fatal("successful native command changed") + } + for _, tc := range []struct { + err error + want string + }{{context.Canceled, "context_canceled"}, {context.DeadlineExceeded, "context_deadline"}, {processadapter.ErrOrdinaryContainmentUnsupported, "ordinary_unsupported"}, {processadapter.ErrStdoutLimitExceeded, "stdout_limit"}, {processadapter.ErrOrdinaryForcedCleanup, "ordinary_forced_cleanup"}, {errors.Join(context.Canceled, processadapter.ErrOrdinaryForcedCleanup), "context_canceled"}, {&exec.ExitError{}, "other_git_or_process"}, {errors.New("opaque native policy failure"), "other_git_or_process"}} { + if class, status := nativeTransportDiagnosticClass(tc.err); class != tc.want || status != "unavailable" { + t.Fatalf("native diagnostic class=%q want=%q", class, tc.want) + } + } +} + +func TestNativeTransportDiagnosticPositiveExitDoesNotReadError(t *testing.T) { + original := errors.Join(&gitTransportExitError{cause: &nativeDiagnosticSecretError{}, code: 128}) + body := []byte("secret output") + reports := 0 + runner := &nativeTransportDiagnosticRunner{operation: "secret operation", delegate: nativeDiagnosticDelegate(func(context.Context, Command) ([]byte, error) { + return body, original + }), report: func(operation, class, status string) { + reports++ + if operation != "unknown" || class != "git_exit" || status != "128" { + t.Fatal("positive Git exit or closed operation classification changed") + } + }} + got, err := runner.Run(t.Context(), Command{Args: []string{"secret command"}}) + if reflect.ValueOf(err) != reflect.ValueOf(original) || &got[0] != &body[0] || reports != 1 { + t.Fatal("positive native exit diagnostic changed delegation") + } +} diff --git a/install/integrationctl/agentplugins/adapters/sourceacquisition/native_git_diagnostic_test.go b/install/integrationctl/agentplugins/adapters/sourceacquisition/native_git_diagnostic_test.go new file mode 100644 index 000000000..2ec013c27 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/sourceacquisition/native_git_diagnostic_test.go @@ -0,0 +1,55 @@ +package sourceacquisition + +import ( + "context" + "errors" + "strconv" + "sync/atomic" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" +) + +// This decorator belongs only to the native transport test. Operation labels +// come from its existing call boundaries, never from command arguments. +type nativeTransportDiagnosticRunner struct { + delegate Runner + operation string + report func(string, string, string) + failures atomic.Uint32 +} + +func (runner *nativeTransportDiagnosticRunner) Run(ctx context.Context, command Command) ([]byte, error) { + body, err := runner.delegate.Run(ctx, command) + if err != nil && runner.failures.Add(1) <= 4 { + operation := runner.operation + if operation != "resolve" && operation != "acquire" { + operation = "unknown" + } + class, status := nativeTransportDiagnosticClass(err) + runner.report(operation, class, status) + } + return body, err +} + +func nativeTransportDiagnosticClass(err error) (string, string) { + switch { + case errors.Is(err, context.Canceled): + return "context_canceled", "unavailable" + case errors.Is(err, context.DeadlineExceeded): + return "context_deadline", "unavailable" + case errors.Is(err, processadapter.ErrOrdinaryContainmentUnsupported): + return "ordinary_unsupported", "unavailable" + case errors.Is(err, processadapter.ErrStdoutLimitExceeded): + return "stdout_limit", "unavailable" + case errors.Is(err, processadapter.ErrOrdinaryForcedCleanup): + return "ordinary_forced_cleanup", "unavailable" + } + if class := processadapter.OrdinarySupervisorFailureClass(err); class != "" { + return class, "unavailable" + } + var exit interface{ GitTransportExitCode() int } + if errors.As(err, &exit) { + return "git_exit", strconv.Itoa(exit.GitTransportExitCode()) + } + return "other_git_or_process", "unavailable" +} diff --git a/install/integrationctl/agentplugins/adapters/statev2/lifecycle_values.go b/install/integrationctl/agentplugins/adapters/statev2/lifecycle_values.go new file mode 100644 index 000000000..2b7895db8 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/statev2/lifecycle_values.go @@ -0,0 +1,69 @@ +package statev2 + +import "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + +func validReceiptPhase(value string) bool { + return value == receiptPhaseStateCommitted || value == receiptPhaseCommitted +} + +func validMaterializationState(value domain.MaterializationState) bool { + switch value { + case domain.MaterializationAbsent, + domain.MaterializationStaged, + domain.MaterializationMaterialized, + domain.MaterializationDegraded: + return true + default: + return false + } +} + +func validActivationState(value domain.ActivationState) bool { + switch value { + case domain.ActivationNotRequired, + domain.ActivationPrepared, + domain.ActivationManual, + domain.ActivationActive, + domain.ActivationFailed: + return true + default: + return false + } +} + +func validAuthenticationState(value domain.AuthenticationState) bool { + switch value { + case domain.AuthenticationNotRequired, + domain.AuthenticationNotChecked, + domain.AuthenticationPending, + domain.AuthenticationComplete, + domain.AuthenticationFailed: + return true + default: + return false + } +} + +func validPolicyState(value domain.PolicyState) bool { + switch value { + case domain.PolicyAllowed, + domain.PolicyBlocked, + domain.PolicyApprovalRequired: + return true + default: + return false + } +} + +func validVerificationState(value domain.VerificationState) bool { + switch value { + case domain.VerificationNotRun, + domain.VerificationPackageValid, + domain.VerificationInstalled, + domain.VerificationRuntime, + domain.VerificationFailed: + return true + default: + return false + } +} diff --git a/install/integrationctl/agentplugins/adapters/statev2/project.go b/install/integrationctl/agentplugins/adapters/statev2/project.go new file mode 100644 index 000000000..7075367e0 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/statev2/project.go @@ -0,0 +1,125 @@ +package statev2 + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Machine receipts are valid only under the selected physical project. Portable +// transfer uses uap.toml/uap.lock; moving .uap is never an authority transfer. +func (store Store) validateProject(state domain.StateFileV2) error { + if store.ProjectRoot == "" { + return nil + } + if err := store.validateProjectPaths(); err != nil { + return err + } + for _, receipt := range state.TransactionReceipts { + if err := store.validateProjectReceipt(receipt); err != nil { + return err + } + } + for _, installation := range state.Installations { + if err := store.validateProjectInstallation(installation); err != nil { + return err + } + } + return nil +} + +func (store Store) validateProjectInstallation(installation domain.Installation) error { + for _, data := range installation.DataReceipts { + if data.Scope != string(domain.ScopeProject) { + return fmt.Errorf("project data receipt has non-project scope") + } + if err := checkProjectLocator(filepath.Join(store.ProjectRoot, ".uap", "plugin-data"), data.Locator); err != nil { + return err + } + } + for _, binding := range installation.Clients { + if err := store.validateProjectBinding(binding); err != nil { + return err + } + } + return nil +} + +func (store Store) validateProjectBinding(binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return fmt.Errorf("project state contains a non-project binding") + } + if err := checkProjectLocator(store.ProjectRoot, binding.TargetLocator); err != nil { + return err + } + nativeBase := store.ProjectRoot + if binding.NativeProfileRoot == store.ProjectRoot { + // Root-native registries (for example a workspace opencode.json) use + // the project directory itself. Only this field permits exact equality; + // validate its spelling and no-follow authority through its parent. + if _, err := domain.NewTargetContext(domain.ScopeProject, store.ProjectRoot, ""); err != nil { + return err + } + nativeBase = filepath.Dir(store.ProjectRoot) + } + if err := checkProjectLocator(nativeBase, binding.NativeProfileRoot); err != nil { + return err + } + if binding.ProfileNamespace != "" && binding.ProfileNamespace != filepath.Join(store.ProjectRoot, ".uap") { + return fmt.Errorf("project binding belongs to a different namespace") + } + for _, object := range binding.NativeObjects { + if err := checkProjectLocator(store.ProjectRoot, object.Path); err != nil { + return err + } + } + for _, receipt := range binding.Receipts { + if err := store.validateProjectReceipt(receipt); err != nil { + return err + } + } + return nil +} + +func (store Store) validateProjectReceipt(receipt domain.MutationReceipt) error { + for _, path := range []string{receipt.ActivePath, receipt.StagingPath, receipt.BackupPath} { + if err := checkProjectLocator(store.ProjectRoot, path); err != nil { + return err + } + } + for _, owner := range receipt.ProfileOwners { + if owner.Namespace != filepath.Join(store.ProjectRoot, ".uap") { + return fmt.Errorf("project receipt belongs to a different namespace") + } + } + return nil +} + +func checkProjectLocator(base, path string) error { + if path == "" { + return nil + } + if !filepath.IsAbs(path) || filepath.Clean(path) != path { + return fmt.Errorf("project machine receipt locator must be absolute and clean") + } + if err := pathpolicy.RequireContainedChild(base, path); err != nil { + return fmt.Errorf("project machine receipt points outside selected namespace: %w", err) + } + return nil +} + +func (store Store) validateProjectPaths() error { + if store.ProjectRoot == "" { + return nil + } + namespace := filepath.Join(store.ProjectRoot, ".uap") + if err := pathpolicy.RequireContainedChild(store.ProjectRoot, namespace); err != nil { + return fmt.Errorf("unsafe project namespace: %w", err) + } + if err := pathpolicy.RequireContainedChild(namespace, store.Path); err != nil { + return fmt.Errorf("unsafe project state path: %w", err) + } + return nil +} diff --git a/install/integrationctl/agentplugins/adapters/statev2/project_registry_test.go b/install/integrationctl/agentplugins/adapters/statev2/project_registry_test.go new file mode 100644 index 000000000..b71f7d106 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/statev2/project_registry_test.go @@ -0,0 +1,94 @@ +package statev2 + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func projectRegistryState(root, native string) domain.StateFileV2 { + installation := validInstallation("00000000-0000-4000-8000-000000000001", "src_project", "demo-000000000001") + for key, binding := range installation.Clients { + delete(installation.Clients, key) + binding.Scope = string(domain.ScopeProject) + binding.TargetLocator = filepath.Join(root, ".uap", "managed", binding.PhysicalArtifact) + binding.NativeProfileRoot = native + binding.ClientBindingID = domain.ComputeClientBindingID(installation.InstallationID, binding.ClientID, binding.Scope, binding.TargetLocator) + installation.Clients[binding.ClientBindingID] = binding + } + return domain.StateFileV2{SchemaVersion: domain.StateSchemaVersion, Installations: []domain.Installation{installation}} +} + +func TestProjectStateAcceptsExactRootNativeRegistry(t *testing.T) { + root := t.TempDir() + store := Store{ProjectRoot: root, Path: filepath.Join(root, ".uap", "state-v2.json")} + if err := store.Save(projectRegistryState(root, root)); err != nil { + t.Fatal(err) + } + loaded, err := store.Load() + if err != nil { + t.Fatal(err) + } + for _, binding := range loaded.Installations[0].Clients { + if binding.NativeProfileRoot != root || binding.TargetLocator == root { + t.Fatalf("root-native registry lost its distinct artifact locator: %+v", binding) + } + } +} + +func TestProjectStateRootNativeRegistryDoesNotBroadenOtherLocators(t *testing.T) { + for _, kind := range []string{"different project", "relative native root", "root artifact"} { + t.Run(kind, func(t *testing.T) { + root := t.TempDir() + state := projectRegistryState(root, root) + installation := &state.Installations[0] + for key, binding := range installation.Clients { + delete(installation.Clients, key) + switch kind { + case "different project": + binding.NativeProfileRoot = t.TempDir() + case "relative native root": + binding.NativeProfileRoot = "." + case "root artifact": + binding.TargetLocator = root + } + binding.ClientBindingID = domain.ComputeClientBindingID(installation.InstallationID, binding.ClientID, binding.Scope, binding.TargetLocator) + installation.Clients[binding.ClientBindingID] = binding + } + assertProjectRegistryRejected(t, root, state) + }) + } +} + +func assertProjectRegistryRejected(t *testing.T, root string, state domain.StateFileV2) { + t.Helper() + store := Store{ProjectRoot: root, Path: filepath.Join(root, ".uap", "state-v2.json")} + if err := store.Save(state); err == nil { + t.Fatal("unsafe project registry/target was persisted") + } + body, err := json.Marshal(state) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(store.Path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(store.Path, body, 0600); err != nil { + t.Fatal(err) + } + if _, err := store.Load(); err == nil { + t.Fatal("unsafe project registry/target was adopted on load") + } +} + +func TestProjectStateRootNativeRegistryRejectsSymlinkAuthority(t *testing.T) { + parent, actual := t.TempDir(), t.TempDir() + alias := filepath.Join(parent, "project-alias") + if err := os.Symlink(actual, alias); err != nil { + t.Skipf("host cannot create disposable symlink: %v", err) + } + assertProjectRegistryRejected(t, alias, projectRegistryState(alias, alias)) +} diff --git a/install/integrationctl/agentplugins/adapters/statev2/project_test.go b/install/integrationctl/agentplugins/adapters/statev2/project_test.go new file mode 100644 index 000000000..5c4f05794 --- /dev/null +++ b/install/integrationctl/agentplugins/adapters/statev2/project_test.go @@ -0,0 +1,92 @@ +package statev2 + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectStateCopyCannotTransferMachineAuthority(t *testing.T) { + a, b := t.TempDir(), t.TempDir() + original := Store{ProjectRoot: a, Path: filepath.Join(a, ".uap", "state-v2.json")} + installation := validInstallation("00000000-0000-4000-8000-000000000001", "src_project", "demo-000000000001") + for key, binding := range installation.Clients { + delete(installation.Clients, key) + binding.Scope = string(domain.ScopeProject) + binding.TargetLocator = filepath.Join(a, ".uap", "managed", "codex", binding.PhysicalArtifact) + binding.NativeProfileRoot = filepath.Join(a, ".codex") + binding.ClientBindingID = domain.ComputeClientBindingID(installation.InstallationID, binding.ClientID, binding.Scope, binding.TargetLocator) + installation.Clients[binding.ClientBindingID] = binding + } + state := domain.StateFileV2{SchemaVersion: domain.StateSchemaVersion, Installations: []domain.Installation{installation}} + if err := original.Save(state); err != nil { + t.Fatal(err) + } + before, err := os.ReadFile(original.Path) + if err != nil { + t.Fatal(err) + } + copied := Store{ProjectRoot: b, Path: filepath.Join(b, ".uap", "state-v2.json")} + if err := os.Mkdir(filepath.Dir(copied.Path), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(copied.Path, before, 0o600); err != nil { + t.Fatal(err) + } + if _, err := copied.Load(); err == nil { + t.Fatal("copied project state adopted original project's target authority") + } + if err := copied.Save(state); err == nil { + t.Fatal("cross-project state save adopted original target authority") + } + after, err := os.ReadFile(original.Path) + if err != nil || !bytes.Equal(before, after) { + t.Fatalf("original state changed: %v", err) + } +} + +func TestProjectStateRejectsUserReceiptsOnLoad(t *testing.T) { + root := t.TempDir() + filename := filepath.Join(root, ".uap", "state-v2.json") + state := domain.StateFileV2{SchemaVersion: domain.StateSchemaVersion, Installations: []domain.Installation{validInstallation("00000000-0000-4000-8000-000000000001", "src_user", "demo-000000000001")}} + if err := (Store{Path: filename}).Save(state); err != nil { + t.Fatal(err) + } + if _, err := (Store{Path: filename, ProjectRoot: root}).Load(); err == nil { + t.Fatal("user receipts were adopted as project state") + } +} + +func TestProjectStateRejectsCwdDependentLocators(t *testing.T) { + root := t.TempDir() + t.Chdir(root) + for _, relativeField := range []string{"target", "native-profile"} { + t.Run(relativeField, func(t *testing.T) { + installation := validInstallation("00000000-0000-4000-8000-000000000001", "src_project", "demo-000000000001") + for key, binding := range installation.Clients { + delete(installation.Clients, key) + binding.Scope = string(domain.ScopeProject) + binding.TargetLocator = filepath.Join(root, ".uap", "managed", binding.PhysicalArtifact) + binding.NativeProfileRoot = filepath.Join(root, ".codex") + if relativeField == "target" { + binding.TargetLocator = filepath.Join(".uap", "managed", binding.PhysicalArtifact) + } else { + binding.NativeProfileRoot = ".codex" + } + binding.ClientBindingID = domain.ComputeClientBindingID(installation.InstallationID, binding.ClientID, binding.Scope, binding.TargetLocator) + installation.Clients[binding.ClientBindingID] = binding + } + store := Store{ProjectRoot: root, Path: filepath.Join(root, ".uap", "state-v2.json")} + state := domain.StateFileV2{SchemaVersion: domain.StateSchemaVersion, Installations: []domain.Installation{installation}} + if err := store.Save(state); err == nil { + t.Fatal("project receipt gained authority from invocation cwd") + } + if _, err := os.Stat(filepath.Dir(store.Path)); !os.IsNotExist(err) { + t.Fatalf("refused state created mutable namespace: %v", err) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/adapters/statev2/store.go b/install/integrationctl/agentplugins/adapters/statev2/store.go index 96c8bdcb8..8be6228dd 100644 --- a/install/integrationctl/agentplugins/adapters/statev2/store.go +++ b/install/integrationctl/agentplugins/adapters/statev2/store.go @@ -24,10 +24,15 @@ const ( var fullLowercaseSHA = regexp.MustCompile(`^[0-9a-f]{40}$`) type Store struct { - Path string + // ProjectRoot is operational context; machine state copied across projects is rejected. + ProjectRoot string `json:"-"` + Path string } func (store Store) SchemaVersion() (int, error) { + if err := store.validateProjectPaths(); err != nil { + return 0, err + } body, err := os.ReadFile(store.Path) if os.IsNotExist(err) { return domain.StateSchemaVersion, nil @@ -71,6 +76,9 @@ func managedBindingDigest(binding domain.ClientBinding) string { } func (store Store) Load() (domain.StateFileV2, error) { + if err := store.validateProjectPaths(); err != nil { + return domain.StateFileV2{}, err + } if strings.TrimSpace(store.Path) == "" { return domain.StateFileV2{}, fmt.Errorf("state v2 path is required") } @@ -113,10 +121,16 @@ func (store Store) Load() (domain.StateFileV2, error) { if err := Validate(state); err != nil { return domain.StateFileV2{}, fmt.Errorf("validate state: %w", err) } + if err := store.validateProject(state); err != nil { + return domain.StateFileV2{}, err + } return state, nil } func (store Store) Save(state domain.StateFileV2) error { + if err := store.validateProjectPaths(); err != nil { + return err + } if strings.TrimSpace(store.Path) == "" { return fmt.Errorf("state v2 path is required") } @@ -127,6 +141,9 @@ func (store Store) Save(state domain.StateFileV2) error { if err := Validate(state); err != nil { return fmt.Errorf("refuse invalid state: %w", err) } + if err := store.validateProject(state); err != nil { + return err + } state.Installations = append([]domain.Installation(nil), state.Installations...) sort.Slice(state.Installations, func(i, j int) bool { return state.Installations[i].InstallationID < state.Installations[j].InstallationID @@ -343,7 +360,9 @@ func Validate(state domain.StateFileV2) error { } clientBindingIDs[client.ClientBindingID] = struct{}{} if client.InstallIntent == domain.InstallIntentPrepare && client.Scope != string(domain.ScopeUser) { - return fmt.Errorf("prepare binding requires user scope") + if client.Scope != string(domain.ScopeProject) || !(domain.SelectedDelivery{}).EffectiveTraitsForScope(domain.ClientID(client.ClientID), domain.ScopeProject).Allows(domain.InstallIntentPrepare) { + return fmt.Errorf("prepare binding requires a supported scope") + } } if err := client.InstallIntent.Validate(domain.ClientID(client.ClientID)); err != nil { return err @@ -449,69 +468,3 @@ func Validate(state domain.StateFileV2) error { } return nil } - -func validReceiptPhase(value string) bool { - return value == receiptPhaseStateCommitted || value == receiptPhaseCommitted -} - -func validMaterializationState(value domain.MaterializationState) bool { - switch value { - case domain.MaterializationAbsent, - domain.MaterializationStaged, - domain.MaterializationMaterialized, - domain.MaterializationDegraded: - return true - default: - return false - } -} - -func validActivationState(value domain.ActivationState) bool { - switch value { - case domain.ActivationNotRequired, - domain.ActivationPrepared, - domain.ActivationManual, - domain.ActivationActive, - domain.ActivationFailed: - return true - default: - return false - } -} - -func validAuthenticationState(value domain.AuthenticationState) bool { - switch value { - case domain.AuthenticationNotRequired, - domain.AuthenticationNotChecked, - domain.AuthenticationPending, - domain.AuthenticationComplete, - domain.AuthenticationFailed: - return true - default: - return false - } -} - -func validPolicyState(value domain.PolicyState) bool { - switch value { - case domain.PolicyAllowed, - domain.PolicyBlocked, - domain.PolicyApprovalRequired: - return true - default: - return false - } -} - -func validVerificationState(value domain.VerificationState) bool { - switch value { - case domain.VerificationNotRun, - domain.VerificationPackageValid, - domain.VerificationInstalled, - domain.VerificationRuntime, - domain.VerificationFailed: - return true - default: - return false - } -} diff --git a/install/integrationctl/agentplugins/clients/all/project_registry_test.go b/install/integrationctl/agentplugins/clients/all/project_registry_test.go new file mode 100644 index 000000000..f0a558c77 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/all/project_registry_test.go @@ -0,0 +1,47 @@ +package all_test + +import ( + "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/clients/vscode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectRegistrySelectsDistinctVSCodeBackend(t *testing.T) { + user, ok := all.Default().Lookup(domain.ClientVSCode) + if !ok { + t.Fatal("user VS Code adapter missing") + } + if _, ok := user.(*vscode.Adapter); !ok { + t.Fatal("default VS Code backend changed") + } + project, ok := all.Project().Lookup(domain.ClientVSCode) + if !ok { + t.Fatal("project VS Code adapter missing") + } + if _, ok := project.(*vscode.ProjectAdapter); !ok { + t.Fatal("project registry reused user or Local adapter") + } + if clients.SupportsScope(all.Default(), domain.ClientVSCode, domain.ScopeProject) || !clients.SupportsScope(all.Project(), domain.ClientVSCode, domain.ScopeProject) { + t.Fatal("static scope declaration bypassed explicit project composition") + } + inspector, ok := project.(clients.RegistryInspector) + if !ok || inspector.UsesNativeRegistryExecutable() { + t.Fatal("project registry requires a vendor/Copilot command") + } + if domain.SameClientBackendForScope(domain.ClientVSCode, domain.ClientCopilot, domain.ScopeProject) || !domain.SameClientBackendForScope(domain.ClientVSCode, domain.ClientCopilot, domain.ScopeUser) { + t.Fatal("workspace composition changed user backend sharing") + } + for _, id := range domain.SupportedClientIDs() { + if id == domain.ClientVSCode { + continue + } + original, _ := all.Default().Lookup(id) + selected, _ := all.Project().Lookup(id) + if original.ID() != selected.ID() { + t.Fatal("project registry lost another supported client", id) + } + } +} diff --git a/install/integrationctl/agentplugins/clients/all/registry.go b/install/integrationctl/agentplugins/clients/all/registry.go index 9bdee69b9..d455c41db 100644 --- a/install/integrationctl/agentplugins/clients/all/registry.go +++ b/install/integrationctl/agentplugins/clients/all/registry.go @@ -29,18 +29,30 @@ import ( // immutable once built, so one instance is shared. func Default() *clients.Registry { return defaultRegistry() } +// Project selects explicit workspace adapters while retaining every other +// client's own project admission. SDK hosts use it with a project-scoped engine. +// Default retains VS Code's user/Copilot backend and refuses its project scope. +func Project() *clients.Registry { return projectRegistry() } + // A failure here is a duplicate or an unknown client id, which is a mistake in // the list below rather than a runtime condition, so it panics instead of // handing back an error every caller would have to translate into the same // crash. It is built on first use rather than in init, so importing the // composition root of one command does not pay for it. -var defaultRegistry = sync.OnceValue(func() *clients.Registry { +var defaultRegistry = sync.OnceValue(func() *clients.Registry { return buildRegistry(false) }) +var projectRegistry = sync.OnceValue(func() *clients.Registry { return buildRegistry(true) }) + +func buildRegistry(project bool) *clients.Registry { + var workspace clients.Adapter = vscode.New() + if project { + workspace = vscode.NewProject() + } registry, err := clients.NewRegistry( codex.New(), chatgpt.New(), cursor.New(), copilot.New(), - vscode.New(), + workspace, kiro.New(), claude.New(), gemini.New(), @@ -55,4 +67,4 @@ var defaultRegistry = sync.OnceValue(func() *clients.Registry { panic(fmt.Sprintf("build the default client registry: %v", err)) } return registry -}) +} diff --git a/install/integrationctl/agentplugins/clients/chatgpt/lifecycle.go b/install/integrationctl/agentplugins/clients/chatgpt/lifecycle.go index 28a7e3a19..17c416b45 100644 --- a/install/integrationctl/agentplugins/clients/chatgpt/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/chatgpt/lifecycle.go @@ -17,6 +17,9 @@ var ( // PreflightActivation rejects a prepare request that has not presented a // validated personal mapping. Other intents have nothing extra to check. func (*Adapter) PreflightActivation(_ clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject || request.Plan.TargetContext.ProjectRoot != "" { + return fmt.Errorf("chatgpt does not support project scope") + } if request.Plan.InstallIntent != domain.InstallIntentPrepare { return nil } @@ -56,5 +59,8 @@ func (*Adapter) Activate(_ context.Context, env clients.Env, request domain.Acti // Deactivate requires the operator to uninstall in ChatGPT Plugins first. func (*Adapter) Deactivate(_ context.Context, _ clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject || request.TargetContext.ProjectRoot != "" { + return domain.DeactivationOutcome{}, fmt.Errorf("chatgpt does not support project scope") + } return shared.RequireExternalUninstall(shared.StartedDeactivation(), request.ExternalUninstalled, "uninstall the plugin in ChatGPT Plugins, then rerun remove with `--external-uninstalled` (also use the flag if it was never activated)"), nil } diff --git a/install/integrationctl/agentplugins/clients/chatgpt/project_scope_test.go b/install/integrationctl/agentplugins/clients/chatgpt/project_scope_test.go new file mode 100644 index 000000000..280f5c8c6 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/chatgpt/project_scope_test.go @@ -0,0 +1,68 @@ +package chatgpt + +import ( + "context" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectChatGPTDoesNotAdvertiseNativeFilesystemScope(t *testing.T) { + registry, err := clients.NewRegistry(New()) + if err != nil { + t.Fatal(err) + } + if !clients.SupportsScope(registry, domain.ClientChatGPT, domain.ScopeUser) || clients.SupportsScope(registry, domain.ClientChatGPT, domain.ScopeProject) { + t.Fatal("ChatGPT scope qualification changed its user or project boundary") + } + if _, ok := clients.As[clients.ProjectTargetLayout](registry, domain.ClientChatGPT); ok { + t.Fatal("ChatGPT exposed a fake project artifact installation layout") + } + if _, ok := clients.As[clients.NativeRegistryLayout](registry, domain.ClientChatGPT); ok { + t.Fatal("ChatGPT exposed an unobservable project native registry") + } +} + +func TestProjectChatGPTDirectLifecycleRefusesPreparationAndAttestation(t *testing.T) { + adapter := New() + for _, intent := range []domain.InstallIntent{domain.InstallIntentAutomatic, domain.InstallIntentPrepare} { + for _, target := range []domain.TargetContext{{Scope: domain.ScopeProject}, {Scope: domain.ScopeUser, ProjectRoot: t.TempDir()}} { + request := domain.ActivationRequest{Client: domain.DetectedClient{ClientID: domain.ClientChatGPT}, + Plan: domain.DeliveryPlan{ClientID: domain.ClientChatGPT, Scope: domain.ScopeProject, TargetContext: target, + InstallIntent: intent, PersonalChatGPTPreparation: true}, + Delivery: domain.StagedDelivery{ClientID: domain.ClientChatGPT}, ActivationComplete: true} + if _, err := adapter.Activate(context.Background(), clients.Env{}, request); err == nil || !strings.Contains(err.Error(), "does not support project scope") { + t.Fatalf("project direct activation/attestation = %v", err) + } + // Target context alone cannot bypass the guard through an inconsistent + // legacy plan scope or a project root with a user scope label. + request.Plan.Scope = domain.ScopeUser + if err := adapter.PreflightActivation(clients.Env{}, request); err == nil { + t.Fatal("project context bypassed direct activation preflight") + } + remove := domain.DeactivationRequest{TargetContext: target, Confirmed: true, ExternalUninstalled: true} + if outcome, err := adapter.Deactivate(context.Background(), clients.Env{}, remove); err == nil || outcome.ExternalRemovalComplete { + t.Fatalf("project external uninstall attested unsupported removal: %+v, %v", outcome, err) + } + } + } +} + +func TestProjectChatGPTLeafPreservesExistingUserManualPreparation(t *testing.T) { + for _, personal := range []bool{false, true} { + intent, activation := domain.InstallIntentAutomatic, domain.ActivationManual + if personal { + intent, activation = domain.InstallIntentPrepare, domain.ActivationPrepared + } + request := domain.ActivationRequest{Client: domain.DetectedClient{ClientID: domain.ClientChatGPT}, + Plan: domain.DeliveryPlan{ClientID: domain.ClientChatGPT, Scope: domain.ScopeUser, InstallIntent: intent, + PersonalChatGPTPreparation: personal, Verification: domain.VerificationPackageValid}, + Delivery: domain.StagedDelivery{ClientID: domain.ClientChatGPT, ActivePath: t.TempDir()}, DeclaredName: "user-preparation"} + outcome, err := New().Activate(context.Background(), clients.Env{}, request) + if err != nil || outcome.Activation != activation || outcome.ActivationAttested || outcome.Verification == domain.VerificationInstalled { + t.Fatalf("user manual/prepared boundary changed: %+v, %v", outcome, err) + } + } +} diff --git a/install/integrationctl/agentplugins/clients/claude/identity.go b/install/integrationctl/agentplugins/clients/claude/identity.go index ac22fa538..71de73ec4 100644 --- a/install/integrationctl/agentplugins/clients/claude/identity.go +++ b/install/integrationctl/agentplugins/clients/claude/identity.go @@ -39,7 +39,7 @@ func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, clie if configRoot == "" && strings.TrimSpace(plan.TargetRoot) != "" { configRoot = filepath.Dir(filepath.Clean(plan.TargetRoot)) } - command, err := ClaudeListCommand(plan.NativeRegistryExecutable, configRoot, plan.ActivePath) + command, err := claudeRegistryListCommand(plan, client, configRoot) if err != nil { return clients.RegistryIndeterminate, err } @@ -53,7 +53,10 @@ func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, clie if result.ExitCode != 0 { return clients.RegistryIndeterminate, fmt.Errorf("the Claude Code plugin registry command failed with exit code %d", result.ExitCode) } - switch PluginStatusFromList(result.Stdout, plan.DeclaredName, plan.ActivePath) { + if plan.Scope == domain.ScopeProject { + return claudeProjectRegistryFinding(result.Stdout, client, plan, managed), nil + } + switch PluginStatusFromListForScope(result.Stdout, plan.DeclaredName, plan.ActivePath, claudeListingScope(plan.Scope)) { case StatusInstalled: if managed == nil { return clients.RegistryCollision, nil diff --git a/install/integrationctl/agentplugins/clients/claude/lifecycle.go b/install/integrationctl/agentplugins/clients/claude/lifecycle.go index dba685622..9088bfd88 100644 --- a/install/integrationctl/agentplugins/clients/claude/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/claude/lifecycle.go @@ -30,6 +30,9 @@ func (*Adapter) VerifierAvailable(_ domain.DetectedClient, _ domain.DeliveryPlan // PreflightActivation rejects a request whose Claude path layout cannot be // probed through the trusted CLI. func (*Adapter) PreflightActivation(_ clients.Env, request domain.ActivationRequest) error { + if (request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject) && request.ActivationComplete { + return fmt.Errorf("claude project native discovery cannot be established by activation attestation; retry exact project listing") + } _, err := PrepareClaudeActivationProbe(request) return err } @@ -47,6 +50,9 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac if !shared.HasClientCLI(env, request.BackendExecutable) { return shared.FailedActivation(outcome, "install Claude Code CLI and retry exact @skills-dir verification", fmt.Errorf("trusted Claude Code CLI is required")) } + if request.Plan.Scope == domain.ScopeProject { + return activateClaudeProject(ctx, env, request) + } if err := verifyClaudePlugin(ctx, env, request); err != nil { return shared.FailedActivation(outcome, fmt.Sprintf("verify with `%s plugin list --json`", request.BackendExecutable), err) } @@ -66,7 +72,7 @@ func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain. if !shared.HasClientCLI(env, request.BackendExecutable) { return outcome, fmt.Errorf("trusted Claude Code CLI is required for exact removal verification") } - listed, err := runClaudeList(ctx, env, request.BackendExecutable, request.Client.ConfigRoot, request.ManagedArtifactPath) + listed, err := listClaudeDeactivation(ctx, env, request) if err != nil { return outcome, fmt.Errorf("verify Claude Code plugin before removal: %w", err) } @@ -74,7 +80,10 @@ func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain. } func finishClaudeDeactivation(outcome domain.DeactivationOutcome, stdout []byte, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { - switch PluginStatusFromList(stdout, request.DeclaredName, request.ManagedArtifactPath) { + if request.TargetContext.Scope == domain.ScopeProject { + return finishClaudeProjectDeactivation(outcome, stdout, request) + } + switch PluginStatusFromListForScope(stdout, request.DeclaredName, request.ManagedArtifactPath, claudeListingScope(request.TargetContext.Scope)) { case StatusInstalled: case StatusAbsent: if request.CurrentActivation == domain.ActivationActive { @@ -96,7 +105,7 @@ func verifyClaudePlugin(ctx context.Context, env clients.Env, request domain.Act if err != nil { return fmt.Errorf("verify Claude Code plugin listing: %w", err) } - switch PluginStatusFromList(listed.Stdout, request.DeclaredName, probe.ActivePath) { + switch PluginStatusFromListForScope(listed.Stdout, request.DeclaredName, probe.ActivePath, claudeListingScope(request.Plan.Scope)) { case StatusInstalled: return nil case StatusAbsent, StatusCollision: diff --git a/install/integrationctl/agentplugins/clients/claude/list.go b/install/integrationctl/agentplugins/clients/claude/list.go index 7664677f0..335455f0e 100644 --- a/install/integrationctl/agentplugins/clients/claude/list.go +++ b/install/integrationctl/agentplugins/clients/claude/list.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) // ListStatus is the recognized shape of a Claude Code plugin listing. @@ -23,6 +24,15 @@ const ( // PluginStatusFromList classifies a `plugin list --json` document for the // exact managed @skills-dir identity. func PluginStatusFromList(body []byte, name, activePath string) ListStatus { + return PluginStatusFromListForScope(body, name, activePath, domain.ScopeUser) +} + +// PluginStatusFromListForScope admits only the selected user/project listing; +// the existing user-only entry point retains its strict original contract. +func PluginStatusFromListForScope(body []byte, name, activePath string, scope domain.InstallScope) ListStatus { + if scope != domain.ScopeUser && scope != domain.ScopeProject { + return StatusUnknown + } decoder := json.NewDecoder(strings.NewReader(string(body))) decoder.UseNumber() parsed, err := shared.DecodeUniqueJSONValue(decoder) @@ -36,14 +46,14 @@ func PluginStatusFromList(body []byte, name, activePath string) ListStatus { if !ok { return StatusUnknown } - return classifyClaudeEntries(entries, name+"@skills-dir", filepath.Clean(activePath)) + return classifyClaudeEntries(entries, name+"@skills-dir", filepath.Clean(activePath), string(scope)) } -func classifyClaudeEntries(entries []any, expectedID, expectedPath string) ListStatus { +func classifyClaudeEntries(entries []any, expectedID, expectedPath, expectedScope string) ListStatus { seen := map[string]struct{}{} found := false for _, value := range entries { - status, matched, ok := classifyClaudeEntry(value, expectedID, expectedPath, seen) + status, matched, ok := classifyClaudeEntry(value, expectedID, expectedPath, expectedScope, seen) if !ok { return status } @@ -61,7 +71,7 @@ func classifyClaudeEntries(entries []any, expectedID, expectedPath string) ListS return StatusAbsent } -func classifyClaudeEntry(value any, expectedID, expectedPath string, seen map[string]struct{}) (ListStatus, bool, bool) { +func classifyClaudeEntry(value any, expectedID, expectedPath, expectedScope string, seen map[string]struct{}) (ListStatus, bool, bool) { entry, ok := value.(map[string]any) if !ok { return StatusUnknown, false, false @@ -81,8 +91,28 @@ func classifyClaudeEntry(value any, expectedID, expectedPath string, seen map[st if id != expectedID { return StatusUnknown, false, true } - if filepath.Clean(installPath) != expectedPath || scope != "user" { + if filepath.Clean(installPath) != expectedPath || scope != expectedScope { return StatusCollision, false, false } + if expectedScope == "project" && !claudeProjectEntryLoaded(entry, expectedPath) { + return StatusUnknown, false, false + } return StatusUnknown, enabled, true } + +func claudeProjectEntryLoaded(entry map[string]any, expectedPath string) bool { + if installPath, _ := entry["installPath"].(string); installPath != expectedPath { + return false + } + if project, exists := entry["projectPath"]; exists { + path, ok := project.(string) + if !ok || !filepath.IsAbs(path) || path != filepath.Dir(filepath.Dir(filepath.Dir(expectedPath))) { + return false + } + } + if reported, exists := entry["errors"]; exists { + loadErrors, ok := reported.([]any) + return ok && len(loadErrors) == 0 + } + return true +} diff --git a/install/integrationctl/agentplugins/clients/claude/plan.go b/install/integrationctl/agentplugins/clients/claude/plan.go index 679625813..530c75e06 100644 --- a/install/integrationctl/agentplugins/clients/claude/plan.go +++ b/install/integrationctl/agentplugins/clients/claude/plan.go @@ -32,6 +32,11 @@ func (*Adapter) TargetRoot(client domain.DetectedClient, mode domain.PackageMode // CheckPlanPrecondition refuses to plan without the CLI: the exact @skills-dir // identity of a delivered package can only be confirmed through it. func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject { + if _, err := claudeProjectRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot); err != nil { + return err + } + } if strings.TrimSpace(in.Client.ExecutablePath) != "" { return nil } @@ -42,7 +47,12 @@ func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.Deliver return nil } -func (*Adapter) RefinePlan(_ context.Context, _ clients.PlanInput, plan *domain.DeliveryPlan) error { +func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject { + if _, err := claudePlanProjectRoot(in.Client, *plan); err != nil { + return err + } + } plan.UserActions = shared.AppendUnique(plan.UserActions, "start a new Claude Code session or run /reload-plugins") return nil } diff --git a/install/integrationctl/agentplugins/clients/claude/probe.go b/install/integrationctl/agentplugins/clients/claude/probe.go index 9f182391f..94c7598a5 100644 --- a/install/integrationctl/agentplugins/clients/claude/probe.go +++ b/install/integrationctl/agentplugins/clients/claude/probe.go @@ -33,6 +33,9 @@ type ClaudeActivationProbe struct { } func PrepareClaudeActivationProbe(request domain.ActivationRequest) (ClaudeActivationProbe, error) { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return prepareClaudeProjectActivationProbe(request) + } configRoot := filepath.Clean(strings.TrimSpace(request.Client.ConfigRoot)) if !filepath.IsAbs(configRoot) { return ClaudeActivationProbe{}, fmt.Errorf("absolute Claude Code config root is required") @@ -72,6 +75,10 @@ func RunClaudeListCommand(ctx context.Context, runner ports.CommandRunner, comma } func ClaudeListCommand(executable, configRoot, activePath string) (legacyports.Command, error) { + return claudeListCommand(executable, configRoot, activePath, filepath.Join(filepath.Clean(configRoot), "skills"), "") +} + +func claudeListCommand(executable, configRoot, activePath, skillsRoot, workingDir string) (legacyports.Command, error) { executable = strings.TrimSpace(executable) if executable == "" { return legacyports.Command{}, fmt.Errorf("Claude Code executable is required") @@ -82,7 +89,7 @@ func ClaudeListCommand(executable, configRoot, activePath string) (legacyports.C } configRoot = filepath.Clean(configRoot) activePath = filepath.Clean(strings.TrimSpace(activePath)) - if !filepath.IsAbs(activePath) || filepath.Dir(activePath) != filepath.Join(configRoot, "skills") { + if !filepath.IsAbs(activePath) || filepath.Dir(activePath) != skillsRoot { return legacyports.Command{}, fmt.Errorf("the Claude Code managed plugin path is not an exact child of the configured skills root") } environment, home, err := boundedClaudeProbeEnvironment() @@ -97,10 +104,13 @@ func ClaudeListCommand(executable, configRoot, activePath string) (legacyports.C if environmentSize(environment) > claudeProbeMaxEnvironmentTotal { return legacyports.Command{}, fmt.Errorf("Claude Code probe environment exceeds the bounded size") } + if workingDir == "" { + workingDir = home + } return legacyports.Command{ Argv: []string{executable, "plugin", "list", "--json"}, Env: environment, - Dir: home, + Dir: workingDir, }, nil } diff --git a/install/integrationctl/agentplugins/clients/claude/project.go b/install/integrationctl/agentplugins/clients/claude/project.go index aff9d3b94..d63a55a1c 100644 --- a/install/integrationctl/agentplugins/clients/claude/project.go +++ b/install/integrationctl/agentplugins/clients/claude/project.go @@ -32,6 +32,10 @@ func (*Adapter) StagingBase(plan domain.DeliveryPlan) string { // ValidateTargetLayout requires the planned target root to be the exact // configured skills directory under the Claude config root. func (*Adapter) ValidateTargetLayout(plan domain.DeliveryPlan) error { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + _, err := claudePlanProjectRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan) + return err + } if filepath.Clean(plan.TargetRoot) != filepath.Join(filepath.Clean(plan.TargetAnchor), "skills") { return fmt.Errorf("the Claude Code delivery target root must be the exact configured skills directory") } diff --git a/install/integrationctl/agentplugins/clients/claude/project_deactivate.go b/install/integrationctl/agentplugins/clients/claude/project_deactivate.go new file mode 100644 index 000000000..7017cee79 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_deactivate.go @@ -0,0 +1,20 @@ +package claude + +import ( + "context" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +func listClaudeDeactivation(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (legacyports.CommandResult, error) { + if request.TargetContext.Scope != domain.ScopeProject { + return runClaudeList(ctx, env, request.BackendExecutable, request.Client.ConfigRoot, request.ManagedArtifactPath) + } + command, err := claudeProjectListCommand(request.BackendExecutable, request.Client, request.TargetContext, request.NativeRegistryRoot, request.ManagedArtifactPath) + if err != nil { + return legacyports.CommandResult{}, err + } + return runPreparedClaudeList(ctx, env, command) +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_identity.go b/install/integrationctl/agentplugins/clients/claude/project_identity.go new file mode 100644 index 000000000..c15031865 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_identity.go @@ -0,0 +1,17 @@ +package claude + +import ( + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +func claudeRegistryListCommand(plan domain.DeliveryPlan, client domain.DetectedClient, userRoot string) (legacyports.Command, error) { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + root, err := claudePlanProjectRoot(client, plan) + if err != nil { + return legacyports.Command{}, err + } + return claudeProjectListCommand(plan.NativeRegistryExecutable, client, plan.TargetContext, root, plan.ActivePath) + } + return ClaudeListCommand(plan.NativeRegistryExecutable, userRoot, plan.ActivePath) +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_lifecycle.go b/install/integrationctl/agentplugins/clients/claude/project_lifecycle.go new file mode 100644 index 000000000..7aac9f3ef --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_lifecycle.go @@ -0,0 +1,86 @@ +package claude + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func activateClaudeProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + outcome := shared.StartedActivation(request) + probe, err := PrepareClaudeActivationProbe(request) + if err != nil { + return shared.FailedActivation(outcome, "retry exact Claude Code project verification", err) + } + listed, err := runPreparedClaudeList(ctx, env, probe.Command) + if err != nil { + return shared.FailedActivation(outcome, "retry exact Claude Code project listing", err) + } + observation := observeClaudeProjectList(listed.Stdout, request.DeclaredName, probe.ActivePath, request.Plan.TargetContext.ProjectRoot, request.Client.ConfigRoot) + switch observation.state { + case projectListEnabled: + outcome.Activation, outcome.Verification = domain.ActivationActive, domain.VerificationInstalled + return outcome, nil + case projectListSuppressed, projectListDisabled, projectListOtherSource: + // Delivery ownership is established by the verified package transaction, + // never by a sentinel or a same-name record from another source. + if request.Delivery.ArtifactDigest == "" { + return shared.FailedActivation(outcome, "verify the owned project package", fmt.Errorf("the Claude project delivery has no verified artifact digest")) + } + outcome.Activation, outcome.Verification = domain.ActivationPrepared, domain.VerificationInstalled + outcome.UserActions = []string{observation.action()} + if observation.state == projectListSuppressed && observation.otherSource { + outcome.UserActions = append(outcome.UserActions, (projectListObservation{state: projectListOtherSource}).action()) + } + return outcome, nil + case projectListAbsent, projectListForeign: + return shared.FailedActivation(outcome, "retry exact Claude Code project verification", fmt.Errorf("%w: the selected Claude Code project plugin is absent or claimed at another project path", shared.ErrRecognizedNegativeEvidence)) + default: + return shared.FailedActivation(outcome, "inspect Claude Code project list output", fmt.Errorf("the Claude Code project plugin list output is not recognized")) + } +} + +func claudeProjectRegistryFinding(body []byte, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) clients.RegistryFinding { + observation := observeClaudeProjectList(body, plan.DeclaredName, plan.ActivePath, plan.TargetContext.ProjectRoot, client.ConfigRoot) + switch observation.state { + case projectListEnabled: + if managed == nil { + return clients.RegistryCollision + } + return clients.RegistryExpected + case projectListDisabled: + if managed == nil { + return clients.RegistryCollision + } + return clients.RegistryClear + case projectListAbsent, projectListSuppressed, projectListOtherSource: + // Clear applies only to this scoped native namespace. The prepared scan and + // generic receipt/digest verification independently establish file ownership. + // This is deliberately not Expected/native-discovery reconciliation. + return clients.RegistryClear + case projectListForeign: + return clients.RegistryCollision + default: + return clients.RegistryIndeterminate + } +} + +func finishClaudeProjectDeactivation(outcome domain.DeactivationOutcome, body []byte, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + observation := observeClaudeProjectList(body, request.DeclaredName, request.ManagedArtifactPath, request.TargetContext.ProjectRoot, request.Client.ConfigRoot) + switch observation.state { + case projectListEnabled: + case projectListAbsent, projectListSuppressed, projectListDisabled, projectListOtherSource: + if request.CurrentActivation == domain.ActivationActive { + return outcome, fmt.Errorf("%w: active Claude Code project identity is no longer confirmed; reobserve the owned installation before removal", shared.ErrRecognizedNegativeEvidence) + } + default: + return outcome, fmt.Errorf("the Claude Code project plugin identity is not exact before removal") + } + // Skills-directory plugins have no external uninstall registration. The + // ordinary removal kernel still verifies and deletes only the owned directory. + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_list_test.go b/install/integrationctl/agentplugins/clients/claude/project_list_test.go new file mode 100644 index 000000000..4df96f404 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_list_test.go @@ -0,0 +1,86 @@ +package claude + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectListingRequiresExactScopePathAndNoLoadErrors(t *testing.T) { + project := t.TempDir() + active := filepath.Join(project, ".claude", "skills", "demo-managed") + for _, change := range []string{"exact", "user-scope", "other-path", "other-project", "nonclean-path", "nonclean-project", "disabled", "load-errors", "malformed-errors", "duplicate"} { + t.Run(change, func(t *testing.T) { + entry := map[string]any{"id": "demo@skills-dir", "scope": "project", "enabled": true, "installPath": active, "projectPath": project} + switch change { + case "user-scope": + entry["scope"] = "user" + case "other-path": + entry["installPath"] = active + "-other" + case "other-project": + entry["projectPath"] = project + "-other" + case "nonclean-path": + entry["installPath"] = filepath.Dir(active) + string(filepath.Separator) + "alias" + string(filepath.Separator) + ".." + string(filepath.Separator) + filepath.Base(active) + case "nonclean-project": + entry["projectPath"] = project + string(filepath.Separator) + "alias" + string(filepath.Separator) + ".." + case "disabled": + entry["enabled"] = false + case "load-errors": + entry["errors"] = []string{"managed policy blocked load"} + case "malformed-errors": + entry["errors"] = "unrecognized" + } + entries := []map[string]any{entry} + if change == "duplicate" { + entries = append(entries, entry) + } + body, err := json.Marshal(entries) + if err != nil { + t.Fatal(err) + } + got := PluginStatusFromListForScope(body, "demo", active, domain.ScopeProject) + if (got == StatusInstalled) != (change == "exact") { + t.Fatalf("%s listing status=%v", change, got) + } + if change == "exact" && PluginStatusFromList(body, "demo", active) == StatusInstalled { + t.Fatal("project listing weakened user-only caller") + } + }) + } +} + +func TestProjectListingRejectsSymlinkTraversalSpelling(t *testing.T) { + project, other := t.TempDir(), t.TempDir() + for _, path := range []*string{&project, &other} { + physical, err := filepath.EvalSymlinks(*path) + if err != nil { + t.Fatal(err) + } + *path = physical + } + skills := filepath.Join(project, ".claude", "skills") + for _, path := range []string{skills, filepath.Join(other, "sub"), filepath.Join(other, "demo-managed")} { + if err := os.MkdirAll(path, 0700); err != nil { + t.Fatal(err) + } + } + if err := os.Symlink(filepath.Join(other, "sub"), filepath.Join(skills, "alias")); err != nil { + t.Skipf("directory symlink unavailable: %v", err) + } + active := filepath.Join(skills, "demo-managed") + listingPath := filepath.Join(skills, "alias") + string(filepath.Separator) + ".." + string(filepath.Separator) + filepath.Base(active) + resolved, err := filepath.EvalSymlinks(listingPath) + if err != nil || resolved != filepath.Join(other, "demo-managed") || filepath.Clean(listingPath) != active { + t.Fatalf("traversal fixture=%q,%v", resolved, err) + } + body, err := json.Marshal([]map[string]any{{"id": "demo@skills-dir", "scope": "project", "enabled": true, "installPath": listingPath, "projectPath": project}}) + if err != nil { + t.Fatal(err) + } + if PluginStatusFromListForScope(body, "demo", active, domain.ScopeProject) == StatusInstalled { + t.Fatal("native traversal spelling manufactured project identity") + } +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_observation.go b/install/integrationctl/agentplugins/clients/claude/project_observation.go new file mode 100644 index 000000000..d3e779286 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_observation.go @@ -0,0 +1,252 @@ +package claude + +import ( + "encoding/json" + "errors" + "io" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" +) + +// Project discovery and scoped filesystem ownership are different observations. +// In particular, suppression does not identify any particular installed package. +type projectListState uint8 + +const ( + projectListUnknown projectListState = iota + projectListAbsent + projectListEnabled + projectListDisabled + projectListSuppressed + projectListOtherSource + projectListForeign +) + +type projectListObservation struct { + state projectListState + untrusted bool + otherSource bool +} + +const claudeUntrustedProjectNote = "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." + +type projectListQuery struct { + name, active, project, user string +} + +func (q projectListQuery) valid(body []byte) bool { + return len(body) <= 1<<20 && q.name != "" && exactAbsolute(q.active) && exactAbsolute(q.project) && exactAbsolute(q.user) && filepath.Dir(q.active) == filepath.Join(q.project, ".claude", "skills") +} + +type projectListRow struct { + scope, path string + enabled bool +} + +type projectListScan struct { + query projectListQuery + observation projectListObservation + seen map[string]bool + selected, other, suppressed, foreign bool +} + +func observeClaudeProjectList(body []byte, name, active, project, user string) projectListObservation { + query := projectListQuery{name: name, active: active, project: project, user: user} + if !query.valid(body) { + return projectListObservation{} + } + entries, ok := decodeProjectList(body) + if !ok { + return projectListObservation{} + } + scan := projectListScan{query: query, observation: projectListObservation{state: projectListAbsent}, seen: map[string]bool{}} + for _, value := range entries { + entry, ok := value.(map[string]any) + if !ok || !scan.record(entry) { + return projectListObservation{} + } + } + return scan.result() +} + +func decodeProjectList(body []byte) ([]any, bool) { + decoder := json.NewDecoder(strings.NewReader(string(body))) + decoder.UseNumber() + parsed, err := shared.DecodeUniqueJSONValue(decoder) + if err != nil { + return nil, false + } + if _, err := decoder.Token(); !errors.Is(err, io.EOF) { + return nil, false + } + entries, ok := parsed.([]any) + return entries, ok +} + +func (s *projectListScan) record(entry map[string]any) bool { + id, ok := entry["id"].(string) + if !ok || id == "" { + return false + } + if id == "(suppressed)@skills-dir" { + trust, valid := projectSuppression(entry) + if !valid || s.suppressed { + return false + } + s.suppressed, s.observation.untrusted = true, trust + return true + } + row, valid := parseProjectListRow(entry, s.query.project) + if !valid { + return false + } + identity := id + "\x00" + row.scope + "\x00" + row.path + if s.seen[identity] { + return false + } + s.seen[identity] = true + if id != s.query.name+"@skills-dir" { + return true + } + return s.recordCandidate(entry, row) +} + +func parseProjectListRow(entry map[string]any, project string) (projectListRow, bool) { + scope, scopeOK := entry["scope"].(string) + enabled, enabledOK := entry["enabled"].(bool) + path, pathOK := entry["installPath"].(string) + if !scopeOK || !enabledOK || !pathOK || !exactAbsolute(path) || !knownClaudeScope(scope) || !validProjectListMetadata(entry, project) { + return projectListRow{}, false + } + return projectListRow{scope: scope, path: path, enabled: enabled}, true +} + +func (s *projectListScan) recordCandidate(entry map[string]any, row projectListRow) bool { + switch row.scope { + case "user": + if filepath.Dir(row.path) != filepath.Join(s.query.user, "skills") || s.other { + return false + } + s.other = true + case "project": + if filepath.Dir(row.path) != filepath.Join(s.query.project, ".claude", "skills") { + return false + } + if row.path != s.query.active { + s.foreign = true + return true + } + if s.selected { + return false + } + s.selected = true + for _, key := range []string{"errors", "notes"} { + if reported, exists := entry[key]; exists && len(reported.([]any)) != 0 { + return false + } + } + if row.enabled { + s.observation.state = projectListEnabled + } else { + s.observation.state = projectListDisabled + } + default: + // Skills-directory plugins have only the observed user/project origins. + return false + } + return true +} + +func (s *projectListScan) result() projectListObservation { + if s.selected && (s.suppressed || s.other || s.foreign) { + return projectListObservation{} + } + result := s.observation + if s.foreign { + result.state = projectListForeign + return result + } + if s.suppressed { + result.state, result.otherSource = projectListSuppressed, s.other + return result + } + if s.other { + result.state, result.otherSource = projectListOtherSource, true + } + return result +} + +func exactAbsolute(path string) bool { return filepath.IsAbs(path) && filepath.Clean(path) == path } + +func knownClaudeScope(scope string) bool { + switch scope { + case "user", "project", "local", "managed": + return true + } + return false +} + +func boundedProjectStrings(value any, required bool) bool { + values, ok := value.([]any) + if !ok || len(values) > 64 || required && len(values) == 0 { + return false + } + total := 0 + for _, item := range values { + text, ok := item.(string) + if !ok || strings.TrimSpace(text) == "" || len(text) > 4096 { + return false + } + total += len(text) + } + return total <= 16384 +} + +func validProjectListMetadata(entry map[string]any, project string) bool { + if version, exists := entry["version"]; exists { + if text, ok := version.(string); !ok || text == "" { + return false + } + } + if path, exists := entry["projectPath"]; exists { + if text, ok := path.(string); !ok || !exactAbsolute(text) || text != project { + return false + } + } + for _, key := range []string{"errors", "notes"} { + if value, exists := entry[key]; exists && !boundedProjectStrings(value, false) { + return false + } + } + return true +} + +func projectSuppression(entry map[string]any) (bool, bool) { + if len(entry) != 6 || entry["version"] != "unknown" || entry["scope"] != "project" || entry["enabled"] != false || entry["installPath"] != "" || !boundedProjectStrings(entry["notes"], true) { + return false, false + } + trust := false + for _, note := range entry["notes"].([]any) { + if note == claudeUntrustedProjectNote { + trust = true + } + } + return trust, true +} + +func (o projectListObservation) action() string { + switch o.state { + case projectListSuppressed: + if o.untrusted { + return "Claude Code skipped project plugins because the selected folder is not trusted; review its trust dialog yourself, then start Claude Code from the project root or run /reload-plugins and retry verification" + } + return "Claude Code suppressed project plugin discovery; inspect /plugin in the selected project and retry verification from its root" + case projectListDisabled: + return "the exact Claude Code project plugin is disabled; review its existing enablement settings and retry verification from the project root" + case projectListOtherSource: + return "Claude Code lists a same-name user plugin instead of the selected project copy; preserve that installation and review native name precedence before retrying project verification" + } + return "start Claude Code from the selected project root and retry exact project plugin verification" +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_observation_mixed_test.go b/install/integrationctl/agentplugins/clients/claude/project_observation_mixed_test.go new file mode 100644 index 000000000..0e48d0861 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_observation_mixed_test.go @@ -0,0 +1,95 @@ +package claude + +import ( + "encoding/json" + "fmt" + "maps" + "path/filepath" + "testing" +) + +func TestProjectObservationPreservesCompleteMixedRowResults(t *testing.T) { + for _, test := range []struct { + name string + rows []string + want projectListObservation + }{ + {"user-trust", []string{"user", "suppressed"}, projectListObservation{state: projectListSuppressed, untrusted: true, otherSource: true}}, + {"user-opaque", []string{"user", "opaque"}, projectListObservation{state: projectListSuppressed, otherSource: true}}, + {"foreign-trust", []string{"foreign", "suppressed"}, projectListObservation{state: projectListForeign, untrusted: true}}, + {"foreign-user", []string{"foreign", "user"}, projectListObservation{state: projectListForeign}}, + {"foreign-trust-user", []string{"foreign", "suppressed", "user"}, projectListObservation{state: projectListForeign, untrusted: true}}, + {"foreign-opaque-user", []string{"foreign", "opaque", "user"}, projectListObservation{state: projectListForeign}}, + {"selected-trust", []string{"selected", "suppressed"}, projectListObservation{}}, + {"selected-user", []string{"selected", "user"}, projectListObservation{}}, + {"selected-foreign", []string{"selected", "foreign"}, projectListObservation{}}, + {"duplicate-unrelated", []string{"local", "local"}, projectListObservation{}}, + {"two-user-paths", []string{"user", "second-user"}, projectListObservation{}}, + {"suppressed-malformed-unrelated", []string{"suppressed", "malformed"}, projectListObservation{}}, + {"unrelated-local", []string{"local"}, projectListObservation{state: projectListAbsent}}, + {"unrelated-managed", []string{"managed"}, projectListObservation{state: projectListAbsent}}, + {"selected-unrelated", []string{"selected", "local", "managed"}, projectListObservation{state: projectListEnabled}}, + {"same-name-local", []string{"same-local"}, projectListObservation{}}, + {"same-name-managed", []string{"same-managed"}, projectListObservation{}}, + {"disabled-foreign", []string{"disabled-foreign"}, projectListObservation{state: projectListForeign}}, + } { + t.Run(test.name, func(t *testing.T) { + root, user, active, fixtures := mixedProjectListFixtures(t) + // Rotations and reversals cover all orders for the two/three-row cases. + for start := range len(test.rows) { + for _, reverse := range []bool{false, true} { + t.Run(fmt.Sprintf("start-%d-reverse-%t", start, reverse), func(t *testing.T) { + rows := mixedProjectListOrder(test.rows, fixtures, start, reverse) + body, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + if got := observeClaudeProjectList(body, "demo", active, root, user); got != test.want { + t.Fatalf("observation=%+v,want=%+v", got, test.want) + } + }) + } + } + }) + } +} + +func mixedProjectListFixtures(t *testing.T) (string, string, string, map[string]map[string]any) { + t.Helper() + fixture := projectListFixture(t) + root, user, active := fixture.root, fixture.user, fixture.active + selected, other, suppressed := fixture.selected, fixture.other, fixture.suppressed + foreign := maps.Clone(selected) + foreign["installPath"] = active + "-foreign" + disabledForeign := maps.Clone(foreign) + disabledForeign["enabled"] = false + opaque := maps.Clone(suppressed) + opaque["notes"] = []string{"A future native suppression reason."} + secondUser := maps.Clone(other) + secondUser["installPath"] = other["installPath"].(string) + "-second" + local := maps.Clone(selected) + local["id"], local["scope"], local["installPath"] = "unrelated", "local", filepath.Join(root, "unrelated") + managed := maps.Clone(local) + managed["scope"] = "managed" + malformed := maps.Clone(local) + malformed["notes"] = "unrecognized" + sameLocal, sameManaged := maps.Clone(local), maps.Clone(managed) + sameLocal["id"], sameManaged["id"] = "demo@skills-dir", "demo@skills-dir" + return root, user, active, map[string]map[string]any{ + "selected": selected, "user": other, "suppressed": suppressed, "opaque": opaque, + "foreign": foreign, "disabled-foreign": disabledForeign, "second-user": secondUser, + "local": local, "managed": managed, "malformed": malformed, "same-local": sameLocal, "same-managed": sameManaged, + } +} + +func mixedProjectListOrder(names []string, fixtures map[string]map[string]any, start int, reverse bool) []map[string]any { + rows := make([]map[string]any, 0, len(names)) + for step := range names { + index := (start + step) % len(names) + if reverse { + index = (start + len(names) - step) % len(names) + } + rows = append(rows, fixtures[names[index]]) + } + return rows +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_observation_test.go b/install/integrationctl/agentplugins/clients/claude/project_observation_test.go new file mode 100644 index 000000000..edd3e8d1b --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_observation_test.go @@ -0,0 +1,155 @@ +package claude + +import ( + "encoding/json" + "path/filepath" + "testing" +) + +type claudeProjectListFixture struct { + root, user, active string + selected, other, suppressed map[string]any +} + +func projectListFixture(t *testing.T) claudeProjectListFixture { + t.Helper() + base := t.TempDir() + root := filepath.Join(base, "A") + user := filepath.Join(base, "user", ".claude") + active := filepath.Join(root, ".claude", "skills", "demo-managed") + selected := map[string]any{"id": "demo@skills-dir", "scope": "project", "enabled": true, "installPath": active, "projectPath": root} + other := map[string]any{"id": "demo@skills-dir", "scope": "user", "enabled": true, "installPath": filepath.Join(user, "skills", "demo-user")} + suppressed := map[string]any{"id": "(suppressed)@skills-dir", "version": "unknown", "scope": "project", "enabled": false, "installPath": "", "notes": []string{claudeUntrustedProjectNote}} + return claudeProjectListFixture{root: root, user: user, active: active, selected: selected, other: other, suppressed: suppressed} +} + +func TestProjectObservationSeparatesBlockedDiscoveryFromIdentity(t *testing.T) { + for _, test := range []struct { + name string + want projectListState + }{ + {"absent", projectListAbsent}, {"exact", projectListEnabled}, {"disabled", projectListDisabled}, + {"suppressed", projectListSuppressed}, {"opaque-suppression", projectListSuppressed}, {"user-source", projectListOtherSource}, + {"user-and-suppressed", projectListSuppressed}, {"suppressed-and-user", projectListSuppressed}, + {"exact-and-user", projectListUnknown}, {"user-and-exact", projectListUnknown}, {"exact-and-suppressed", projectListUnknown}, + {"suppressed-and-exact", projectListUnknown}, {"duplicate-suppression", projectListUnknown}, {"duplicate-exact", projectListUnknown}, + {"foreign-project", projectListForeign}, {"other-project", projectListUnknown}, {"load-errors", projectListUnknown}, {"load-notes", projectListUnknown}, + } { + t.Run(test.name, func(t *testing.T) { + fixture := projectListFixture(t) + root, user, active := fixture.root, fixture.user, fixture.active + selected, other, suppressed := fixture.selected, fixture.other, fixture.suppressed + rows := []map[string]any{} + switch test.name { + case "exact": + rows = append(rows, selected) + case "disabled": + selected["enabled"] = false + rows = append(rows, selected) + case "suppressed": + rows = append(rows, suppressed) + case "opaque-suppression": + suppressed["notes"] = []string{"A future native suppression reason."} + rows = append(rows, suppressed) + case "user-source": + rows = append(rows, other) + case "user-and-suppressed": + rows = append(rows, other, suppressed) + case "suppressed-and-user": + rows = append(rows, suppressed, other) + case "exact-and-user": + rows = append(rows, selected, other) + case "user-and-exact": + rows = append(rows, other, selected) + case "exact-and-suppressed": + rows = append(rows, selected, suppressed) + case "suppressed-and-exact": + rows = append(rows, suppressed, selected) + case "duplicate-suppression": + rows = append(rows, suppressed, suppressed) + case "duplicate-exact": + rows = append(rows, selected, selected) + case "foreign-project": + selected["installPath"] = active + "-foreign" + rows = append(rows, selected) + case "other-project": + selected["projectPath"] = filepath.Join(filepath.Dir(root), "B") + rows = append(rows, selected) + case "load-errors", "load-notes": + key := "errors" + if test.name == "load-notes" { + key = "notes" + } + selected[key] = []string{"Not loaded"} + rows = append(rows, selected) + } + body, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + got := observeClaudeProjectList(body, "demo", active, root, user) + if got.state != test.want { + t.Fatalf("observation=%+v,want=%v", got, test.want) + } + if test.name == "opaque-suppression" && got.untrusted { + t.Fatal("unknown notes manufactured trust evidence") + } + if test.name == "suppressed" && !got.untrusted { + t.Fatal("known trust note lost") + } + if PluginStatusFromList(body, "demo", active) == StatusInstalled { + t.Fatal("project observer weakened strict user contract") + } + }) + } +} + +func TestProjectSuppressionRejectsMalformedNearMisses(t *testing.T) { + for _, change := range []string{"id", "scope", "version", "enabled", "path", "notes-string", "notes-empty", "notes-blank", "notes-type", "notes-long", "extra", "mixed-malformed", "duplicate-key", "trailing"} { + t.Run(change, func(t *testing.T) { + fixture := projectListFixture(t) + root, user, active, sentinel := fixture.root, fixture.user, fixture.active, fixture.suppressed + switch change { + case "id": + sentinel["id"] = "suppressed@skills-dir" + case "scope": + sentinel["scope"] = "user" + case "version": + sentinel["version"] = "1.0.0" + case "enabled": + sentinel["enabled"] = true + case "path": + sentinel["installPath"] = active + case "notes-string": + sentinel["notes"] = "trust" + case "notes-empty": + sentinel["notes"] = []string{} + case "notes-blank": + sentinel["notes"] = []string{" "} + case "notes-type": + sentinel["notes"] = []any{true} + case "notes-long": + sentinel["notes"] = []string{string(make([]byte, 4097))} + case "extra": + sentinel["errors"] = []string{} + } + rows := []map[string]any{sentinel} + if change == "mixed-malformed" { + rows = append(rows, map[string]any{"id": "unrelated", "scope": "user", "enabled": true, "installPath": "relative"}) + } + body, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + if change == "duplicate-key" { + body = []byte(`[{"id":"(suppressed)@skills-dir","id":"(suppressed)@skills-dir"}]`) + } + if change == "trailing" { + body = append(body, []byte(` {}`)...) + } + if got := observeClaudeProjectList(body, "demo", active, root, user); got.state != projectListUnknown { + t.Fatalf("malformed listing became %+v", got) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/clients/claude/project_probe.go b/install/integrationctl/agentplugins/clients/claude/project_probe.go new file mode 100644 index 000000000..2ccbdf033 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/project_probe.go @@ -0,0 +1,36 @@ +package claude + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +func prepareClaudeProjectActivationProbe(request domain.ActivationRequest) (ClaudeActivationProbe, error) { + root, err := claudePlanProjectRoot(request.Client, request.Plan) + if err != nil { + return ClaudeActivationProbe{}, err + } + if request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath { + return ClaudeActivationProbe{}, fmt.Errorf("claude project activation path differs from selected delivery") + } + command, err := claudeProjectListCommand(request.BackendExecutable, request.Client, request.Plan.TargetContext, root, request.Plan.ActivePath) + return ClaudeActivationProbe{Command: command, ConfigRoot: request.Client.ConfigRoot, ActivePath: request.Plan.ActivePath}, err +} + +func claudeProjectListCommand(executable string, client domain.DetectedClient, target domain.TargetContext, registryRoot, activePath string) (legacyports.Command, error) { + root, err := claudeProjectRoot(client, target, registryRoot) + if err != nil { + return legacyports.Command{}, err + } + if filepath.Clean(activePath) != activePath || filepath.Dir(activePath) != filepath.Join(root, "skills") { + return legacyports.Command{}, fmt.Errorf("claude project artifact differs from exact skills-directory layout") + } + if err := pathpolicy.RequireContainedChild(root, activePath); err != nil { + return legacyports.Command{}, err + } + return claudeListCommand(executable, client.ConfigRoot, activePath, filepath.Join(root, "skills"), target.ProjectRoot) +} diff --git a/install/integrationctl/agentplugins/clients/claude/scope.go b/install/integrationctl/agentplugins/clients/claude/scope.go new file mode 100644 index 000000000..8481938d4 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/claude/scope.go @@ -0,0 +1,72 @@ +package claude + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".claude"), in.Client.ExecutablePath + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, _ string, target domain.TargetContext) (string, string, error) { + if mode != domain.PackageProjection { + return "", "", fmt.Errorf("claude project skills-directory installation requires projected delivery") + } + root, err := claudeProjectRoot(client, target, "") + if err != nil { + return "", "", err + } + return root, filepath.Join(root, "skills"), nil +} + +func claudeProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.Scope != domain.ScopeProject || validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("claude project context differs from selected scope or original user configuration") + } + root := filepath.Join(validated.ProjectRoot, ".claude") + if planned != "" && planned != root { + return "", fmt.Errorf("claude native registry differs from selected project") + } + if err := pathpolicy.RequireContainedChild(validated.ProjectRoot, root); err != nil { + return "", err + } + return root, nil +} + +func claudePlanProjectRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return "", fmt.Errorf("claude project lifecycle requires matching project scope") + } + root, err := claudeProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if plan.TargetAnchor != root || plan.TargetRoot != filepath.Join(root, "skills") || filepath.Dir(plan.ActivePath) != plan.TargetRoot { + return "", fmt.Errorf("claude project delivery differs from exact skills-directory layout") + } + return root, nil +} + +func claudeListingScope(scope domain.InstallScope) domain.InstallScope { + if scope == domain.ScopeProject { + return scope + } + return domain.ScopeUser +} diff --git a/install/integrationctl/agentplugins/clients/cline/activate.go b/install/integrationctl/agentplugins/clients/cline/activate.go index b4b557090..3512ee65b 100644 --- a/install/integrationctl/agentplugins/clients/cline/activate.go +++ b/install/integrationctl/agentplugins/clients/cline/activate.go @@ -11,6 +11,9 @@ import ( // AutomaticallyActivates reports that Activate will write managed Cline native // objects for this exact request. func (*Adapter) AutomaticallyActivates(_ clients.Env, request domain.ActivationRequest) bool { + if clineProjectSelected(request.Plan) { + return request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components) + } return request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.HasNativeConfigRoot(request) } @@ -23,6 +26,9 @@ func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryP // Activate installs or verifies managed Cline skills and MCP servers through // the native config kernel. func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if clineProjectSelected(request.Plan) { + return activateClineProject(ctx, env, request) + } if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } @@ -44,6 +50,9 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac // Deactivate removes managed Cline native objects. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject || request.TargetContext.ProjectRoot != "" { + return deactivateClineProject(ctx, env, request) + } outcome := shared.StartedDeactivation() if len(ClineObjects(request.NativeObjects)) == 0 { action := "remove any Cline skills and MCP entries for this plugin, " + diff --git a/install/integrationctl/agentplugins/clients/cline/identity.go b/install/integrationctl/agentplugins/clients/cline/identity.go index 1a1738d15..21c227553 100644 --- a/install/integrationctl/agentplugins/clients/cline/identity.go +++ b/install/integrationctl/agentplugins/clients/cline/identity.go @@ -16,10 +16,13 @@ var _ clients.RegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return false } -func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, _ domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + if clineProjectSelected(plan) { + return inspectClineProject(client, plan, managed, env.NativeConfig) + } return InspectClineRegistry(plan, managed, env.NativeConfig) } diff --git a/install/integrationctl/agentplugins/clients/cline/plan.go b/install/integrationctl/agentplugins/clients/cline/plan.go index 08e37b89a..30dc8e77c 100644 --- a/install/integrationctl/agentplugins/clients/cline/plan.go +++ b/install/integrationctl/agentplugins/clients/cline/plan.go @@ -11,6 +11,9 @@ import ( var _ clients.PlanRefiner = (*Adapter)(nil) func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if clineProjectSelected(*plan) || in.TargetContext.ProjectRoot != "" { + return refineClineProjectPlan(in, plan) + } shared.PromoteNativeReady(plan, in.Client.ConfigRoot, shared.OnlyNativeComponents(plan.Components)) plan.UserActions = shared.AppendUnique(plan.UserActions, "agentplugins will install and verify Cline skills and MCP servers automatically; VS Code reloads MCP settings, while Cline CLI reads them on its next process") return nil diff --git a/install/integrationctl/agentplugins/clients/cline/project.go b/install/integrationctl/agentplugins/clients/cline/project.go index ff26821e1..4e9bbc3ca 100644 --- a/install/integrationctl/agentplugins/clients/cline/project.go +++ b/install/integrationctl/agentplugins/clients/cline/project.go @@ -4,20 +4,44 @@ import ( "context" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) var _ clients.Projector = (*Adapter)(nil) var _ clients.ActiveNativeProjector = (*Adapter)(nil) -func (*Adapter) ProjectActiveNative(_ context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { + if clineProjectSelected(plan) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := validateClineProjection(root, plan, true); err != nil { + return nil, err + } + return clineProjectObjects(root, envelope, plan) + } return BuildClineNativeObjects(root, envelope, plan) } // Project writes Cline's native projection and records the objects it owns. -func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if clineProjectSelected(in.Plan) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := validateClineProjection(in.StagingPath, in.Plan, false); err != nil { + return nil, err + } + if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { + return nil, err + } + } if err := ProjectClineNative(in.StagingPath, in.Envelope, in.Plan, in.PluginDataPath); err != nil { return nil, err } + if clineProjectSelected(in.Plan) { + return clineProjectObjects(in.StagingPath, in.Envelope, in.Plan) + } return BuildClineNativeObjects(in.StagingPath, in.Envelope, in.Plan) } diff --git a/install/integrationctl/agentplugins/clients/cline/project_components.go b/install/integrationctl/agentplugins/clients/cline/project_components.go new file mode 100644 index 000000000..b5e59077a --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cline/project_components.go @@ -0,0 +1,117 @@ +package cline + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const ( + clineProjectSkillKind = "cline_project_skill_directory" + clineProjectMCPKind = "cline_project_mcp_server" +) + +func clineProjectComponents(root string) shared.ProjectComponents { + return shared.ProjectComponents{ProjectRoot: root, SkillsRoot: filepath.Join(root, ".cline", "skills"), + ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, ".uap", "clients", "cline", "mcp.json")}, + Codec: nativeconfig.CodecCline, SkillKind: clineProjectSkillKind, MCPKind: clineProjectMCPKind} +} + +func clineProjectOwned(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + selected := []domain.NativeObjectOwnership{} + for _, object := range objects { + if object.Kind != "managed_package_directory" { + selected = append(selected, object) + } + } + return selected +} + +func clineProjectObjects(staging string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + layout := clineProjectComponents(plan.TargetContext.ProjectRoot) + objects := []domain.NativeObjectOwnership{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + var object domain.NativeObjectOwnership + var err error + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return nil, fmt.Errorf("planned Cline project skill is missing") + } + relative := filepath.ToSlash(filepath.Dir(filepath.FromSlash(skill.RelativePath))) + if skill.RelativePath == "" { + relative = "skills/" + component.Name + } + object, err = layout.SkillObject(component.Name, relative, staging) + case domain.ComponentMCPServer: + var projection ClineProjection + projection, err = ReadClineProjection(staging) + if err == nil { + server, ok := projection.Servers[component.Name] + if !ok { + return nil, fmt.Errorf("projected Cline MCP server is missing") + } + object, err = layout.MCPObject(component.Name, server, layout.ConfigPaths.JSON) + } + default: + return nil, fmt.Errorf("unsupported Cline project component %q", component.Kind) + } + if err != nil { + return nil, err + } + objects = append(objects, object) + } + sort.Slice(objects, func(i, j int) bool { return objects[i].ObjectID < objects[j].ObjectID }) + return objects, layout.ValidateObjects(objects) +} + +func clineProjectServers(active string, objects []domain.NativeObjectOwnership) (map[string]nativeconfig.Server, error) { + servers := map[string]nativeconfig.Server{} + for _, object := range objects { + if object.Kind != clineProjectMCPKind { + continue + } + projection, err := ReadClineProjection(active) + if err != nil { + return nil, err + } + server, ok := projection.Servers[object.LogicalName] + if !ok { + return nil, fmt.Errorf("projected Cline MCP server is missing") + } + servers[object.LogicalName] = server + } + return servers, nil +} + +func validateClineProjectSelection(plan domain.DeliveryPlan, objects []domain.NativeObjectOwnership) error { + selected := map[string]bool{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + kind := clineProjectSkillKind + if component.Kind == domain.ComponentMCPServer { + kind = clineProjectMCPKind + } + selected[kind+":"+component.Name] = true + } + owned := clineProjectOwned(objects) + if len(owned) != len(selected) { + return fmt.Errorf("cline delivered component selection is incomplete") + } + for _, object := range owned { + if !selected[object.ObjectID] { + return fmt.Errorf("cline delivered component differs from selection") + } + } + return clineProjectComponents(plan.TargetContext.ProjectRoot).ValidateObjects(owned) +} diff --git a/install/integrationctl/agentplugins/clients/cline/project_identity.go b/install/integrationctl/agentplugins/clients/cline/project_identity.go new file mode 100644 index 000000000..1ae36bd90 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cline/project_identity.go @@ -0,0 +1,46 @@ +package cline + +import ( + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.PreparedRegistryInspector = (*Adapter)(nil) + +func inspectClineProject(client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding, kernel nativeconfig.Kernel) (clients.RegistryFinding, error) { + root, err := clineProjectPlanRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + if managed.Scope != "project" || managed.ClientID != "cline" || managed.PhysicalArtifact != plan.PhysicalArtifactID || managed.TargetLocator != plan.ActivePath { + return clients.RegistryIndeterminate, fmt.Errorf("cline project binding differs from selected artifact") + } + if err := New().ValidateBindingProfile(root, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + selected := *managed + selected.NativeObjects = clineProjectOwned(managed.NativeObjects) + managed = &selected + } + return clineProjectComponents(root).Inspect(plan.Components, managed, kernel) +} + +func (*Adapter) InspectPreparedRegistry(plan domain.DeliveryPlan, name string, owned bool) (clients.RegistryFinding, error) { + if !clineProjectSelected(plan) { + return shared.InspectUnqualifiedPluginRoot(plan.TargetRoot, name, plan.ActivePath, owned) + } + if _, err := clineProjectPlanRoot(domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return clients.RegistryIndeterminate, err + } + if name != plan.DeclaredName { + return clients.RegistryIndeterminate, fmt.Errorf("cline project prepared name differs") + } + // Native file targets are inspected by InspectNativeRegistry with complete + // receipts; the core independently verifies the managed package digest. + return clients.RegistryClear, nil +} diff --git a/install/integrationctl/agentplugins/clients/cline/project_lifecycle.go b/install/integrationctl/agentplugins/clients/cline/project_lifecycle.go new file mode 100644 index 000000000..69752b6ea --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cline/project_lifecycle.go @@ -0,0 +1,108 @@ +package cline + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if !clineProjectSelected(request.Plan) { + return nil + } + root, err := clineProjectPlanRoot(request.Client, request.Plan) + if err != nil { + return err + } + if request.DeclaredName != "" && request.DeclaredName != request.Plan.DeclaredName { + return fmt.Errorf("cline delivered name differs") + } + if request.ActivationComplete { + return fmt.Errorf("cline project session discovery cannot be established by activation attestation") + } + if request.Plan.Status == domain.PlanUnsupported || !shared.OnlyNativeComponents(request.Plan.Components) { + return fmt.Errorf("cline project requires supported skills and MCP components") + } + if request.Delivery.ClientID != "" && request.Delivery.ClientID != domain.ClientCline { + return fmt.Errorf("cline delivered client differs") + } + if request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath { + return fmt.Errorf("cline delivered package differs") + } + if request.Delivery.OwnedBase != "" && request.Delivery.OwnedBase != request.Plan.TargetRoot { + return fmt.Errorf("cline delivered base differs") + } + layout := clineProjectComponents(root) + if err := layout.ValidateObjects(clineProjectOwned(request.PreviousNativeObjects)); err != nil { + return err + } + if err := layout.ValidateObjects(clineProjectOwned(request.Delivery.NativeObjects)); err != nil { + return err + } + return env.NativeConfig.RequireFileIO() +} + +func activateClineProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if err := shared.ActivationIdentityMismatch(request); err != nil { + return domain.ActivationOutcome{}, err + } + if err := New().PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + if request.Delivery.ActivePath != request.Plan.ActivePath || request.Delivery.OwnedBase != request.Plan.TargetRoot { + return domain.ActivationOutcome{}, fmt.Errorf("cline project delivery is incomplete or differs from plan") + } + if request.DeclaredName != request.Plan.DeclaredName { + return domain.ActivationOutcome{}, fmt.Errorf("cline delivered name differs") + } + if err := validateClineProjectSelection(request.Plan, request.Delivery.NativeObjects); err != nil { + return domain.ActivationOutcome{}, err + } + layout := clineProjectComponents(request.Plan.TargetContext.ProjectRoot) + out, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ + Automatic: request.Plan.InstallIntent == domain.InstallIntentAutomatic, + UnavailableAction: "select supported Cline project components", RepairAction: "repair exact owned Cline project components", RetryAction: "retry Cline project component preparation", CompletedAction: clineProjectAction(request.Plan), + Verify: func() error { + return layout.Verify(clineProjectOwned(request.Delivery.NativeObjects), false, env.NativeConfig) + }, + Activate: func(ctx context.Context, r domain.ActivationRequest) error { + objects := clineProjectOwned(r.Delivery.NativeObjects) + servers, err := clineProjectServers(r.Delivery.ActivePath, objects) + if err != nil { + return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} + } + return (shared.ProjectComponentMutation{Layout: layout, ActivePath: r.Delivery.ActivePath, Previous: clineProjectOwned(r.PreviousNativeObjects), Desired: objects, Servers: servers, Kernel: env.NativeConfig}).Apply(ctx) + }, + }) + if err == nil && out.Verification == domain.VerificationInstalled { + out.Activation, out.ActivationAttested = domain.ActivationPrepared, false + } + return out, err +} + +func deactivateClineProject(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + out := shared.StartedDeactivation() + root, err := clineProjectRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return out, err + } + binding := domain.ClientBinding{ClientID: "cline", Scope: "project", NativeProfileRoot: root, PhysicalArtifact: request.PhysicalArtifactID, TargetLocator: request.ManagedArtifactPath, NativeObjects: request.NativeObjects} + if err := New().ValidateBindingProfile(root, binding); err != nil { + return out, err + } + if !request.Confirmed { + out.UserActions = []string{"agentplugins will remove only exact owned Cline project skill directories and prepared MCP members"} + return out, nil + } + if err := (shared.ProjectComponentMutation{Layout: clineProjectComponents(root), Previous: clineProjectOwned(request.NativeObjects), Kernel: env.NativeConfig}).Apply(ctx); err != nil && !shared.CommittedNativeDeactivationCleanup(&out, err) { + return out, err + } + out.ExternalRemovalComplete = true + out.UserActions = []string{"Start a fresh Cline CLI process to observe removal; existing sessions may retain previously selected MCP configuration"} + return out, nil +} diff --git a/install/integrationctl/agentplugins/clients/cline/project_validation.go b/install/integrationctl/agentplugins/clients/cline/project_validation.go new file mode 100644 index 000000000..d109acf46 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cline/project_validation.go @@ -0,0 +1,79 @@ +package cline + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ProfileBindingValidator = (*Adapter)(nil) + +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil + } + if !filepath.IsAbs(root) || filepath.Clean(root) != root || binding.ClientID != "cline" || binding.NativeProfileRoot != root { + return fmt.Errorf("cline project binding scope or namespace differs") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + expected := filepath.Join(root, ".uap", "managed", "clients", "cline", binding.PhysicalArtifact) + if binding.TargetLocator != expected { + return fmt.Errorf("cline project binding artifact differs") + } + if err := pathpolicy.RequireContainedChild(root, expected); err != nil { + return err + } + for _, object := range binding.NativeObjects { + if object.Kind == "managed_package_directory" && (object.Path != expected || object.ManagedDigest == "") { + return fmt.Errorf("cline project package ownership differs") + } + } + return clineProjectComponents(root).ValidateObjects(clineProjectOwned(binding.NativeObjects)) +} + +func validateClineProjection(root string, plan domain.DeliveryPlan, active bool) error { + if _, err := clineProjectPlanRoot(domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return err + } + if active { + if root != plan.ActivePath { + return fmt.Errorf("cline active package differs from plan") + } + } else if filepath.Dir(root) != plan.TargetRoot || !strings.HasPrefix(filepath.Base(root), ".agentplugins-staging-") { + return fmt.Errorf("cline staging differs from exact selected target") + } + if !filepath.IsAbs(root) || filepath.Clean(root) != root { + return fmt.Errorf("cline package path must be absolute and clean") + } + if err := pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, root); err != nil { + return err + } + info, err := os.Lstat(root) + if err != nil { + return err + } + if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("cline package must be a real directory") + } + return nil +} + +func clineProjectAction(plan domain.DeliveryPlan) string { + project, _ := json.Marshal(plan.TargetContext.ProjectRoot) + action := fmt.Sprintf("After Cline project component files are installed, start Cline from working directory %s and inspect skills. Native session discovery, IDE behavior, trust and MCP initialization remain unobserved", project) + if !shared.ComponentKindPresent(plan.Components, domain.ComponentMCPServer) { + return action + } + path, _ := json.Marshal(clineProjectComponents(plan.TargetContext.ProjectRoot).ConfigPaths.JSON) + args, _ := json.Marshal([]string{"--auto-approve", "false", "--cwd", plan.TargetContext.ProjectRoot}) + return action + fmt.Sprintf(". For one Cline CLI process, set environment CLINE_MCP_SETTINGS_PATH to %s and CLINE_NO_AUTO_UPDATE to 1, and use arguments %s. This selects the whole MCP source; original user MCP entries are not merged or copied, and a later ordinary process returns to its user source", path, args) +} diff --git a/install/integrationctl/agentplugins/clients/cline/scope.go b/install/integrationctl/agentplugins/clients/cline/scope.go new file mode 100644 index 000000000..6dc3c1c4b --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cline/scope.go @@ -0,0 +1,111 @@ +package cline + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) +) + +func clineProjectSelected(plan domain.DeliveryPlan) bool { + return plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" +} + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject || in.TargetContext.ProjectRoot != "" { + return in.TargetContext.ProjectRoot, "" + } + return in.Client.ConfigRoot, "" +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managed string, target domain.TargetContext) (string, string, error) { + root, err := clineProjectRoot(client, target, "") + if err != nil { + return "", "", err + } + if managed != filepath.Join(root, ".uap", "managed") { + return "", "", fmt.Errorf("cline project managed root differs from selected project") + } + return shared.ManagedTargetRoot(client, mode, managed) +} + +func clineProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + selected, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if client.ClientID != domain.ClientCline || selected.Scope != domain.ScopeProject || selected.UserConfigRoot != client.ConfigRoot || client.ConfigRoot == "" { + return "", fmt.Errorf("cline project requires matching scope and original user profile") + } + root := selected.ProjectRoot + if clineRootsOverlap(root, client.ConfigRoot) { + return "", fmt.Errorf("cline project must not overlap the original user profile") + } + if err := pathpolicy.RequireContainedChild(filepath.Dir(client.ConfigRoot), client.ConfigRoot); err != nil { + return "", err + } + if planned != "" && planned != root { + return "", fmt.Errorf("cline project namespace differs from selected project") + } + for _, child := range []string{filepath.Join(root, ".cline", "skills"), filepath.Join(root, ".uap", "clients", "cline", "mcp.json")} { + if err := pathpolicy.RequireContainedChild(root, child); err != nil { + return "", err + } + } + return root, nil +} + +func clineProjectPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.ClientID != domain.ClientCline || plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject || plan.NativeRegistryExecutable != "" { + return "", fmt.Errorf("cline project requires exact client, scope and file-only namespace") + } + root, err := clineProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if plan.NativeRegistryRoot != root { + return "", fmt.Errorf("cline project namespace is missing") + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + managed := filepath.Join(root, ".uap", "managed") + target := filepath.Join(managed, "clients", "cline") + if plan.TargetAnchor != managed || plan.TargetRoot != target || plan.ActivePath != filepath.Join(target, plan.PhysicalArtifactID) { + return "", fmt.Errorf("cline managed package differs from selected project") + } + return root, pathpolicy.RequireContainedChild(root, plan.ActivePath) +} + +func refineClineProjectPlan(in clients.PlanInput, plan *domain.DeliveryPlan) error { + root, err := clineProjectPlanRoot(in.Client, *plan) + if err != nil { + return err + } + if plan.Status == domain.PlanUnsupported || !shared.OnlyNativeComponents(plan.Components) { + return nil + } + shared.PromoteNativeReady(plan, root, true) + plan.Activation, plan.Verification = domain.ActivationPrepared, domain.VerificationPackageValid + plan.UserActions = shared.AppendUnique(plan.UserActions, clineProjectAction(*plan)) + return nil +} + +func clineRootsOverlap(first, second string) bool { + for _, pair := range [][2]string{{first, second}, {second, first}} { + relative, err := filepath.Rel(pair[0], pair[1]) + if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return true + } + } + return false +} diff --git a/install/integrationctl/agentplugins/clients/codex/identity.go b/install/integrationctl/agentplugins/clients/codex/identity.go index f42aab7c3..1d9cb222f 100644 --- a/install/integrationctl/agentplugins/clients/codex/identity.go +++ b/install/integrationctl/agentplugins/clients/codex/identity.go @@ -27,6 +27,9 @@ func (*Adapter) UsesNativeRegistryExecutable() bool { return true } // by its CLI; scanning every sibling directory during a dry-run would turn an // unrelated fixture or installation into a false indeterminate result. func (*Adapter) InspectPreparedRegistry(plan domain.DeliveryPlan, name string, owned bool) (clients.RegistryFinding, error) { + if plan.Scope == domain.ScopeProject { + return inspectPreparedCodexProject(plan, owned) + } if strings.TrimSpace(plan.ActivePath) == "" { return clients.RegistryClear, nil } @@ -82,6 +85,12 @@ func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, clie if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if _, err := codexProjectPlanRoot(client, plan); err != nil { + return clients.RegistryIndeterminate, err + } + return inspectCodexProject(plan, managed, env.NativeConfig) + } profileRoot := client.ConfigRoot if profileRoot == "" { profileRoot = plan.NativeRegistryRoot diff --git a/install/integrationctl/agentplugins/clients/codex/lifecycle.go b/install/integrationctl/agentplugins/clients/codex/lifecycle.go index 39c72fa7b..7afd88112 100644 --- a/install/integrationctl/agentplugins/clients/codex/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/codex/lifecycle.go @@ -21,11 +21,17 @@ var ( // AutomaticallyActivates reports that Activate will drive the Codex CLI. func (*Adapter) AutomaticallyActivates(env clients.Env, request domain.ActivationRequest) bool { + if request.Plan.Scope == domain.ScopeProject { + return request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components) + } return shared.HasClientCLI(env, request.BackendExecutable) } // VerifierAvailable reports that an exact Codex listing can be observed. -func (*Adapter) VerifierAvailable(_ domain.DetectedClient, _ domain.DeliveryPlan, backendExecutable string) bool { +func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, backendExecutable string) bool { + if plan.Scope == domain.ScopeProject { + return shared.OnlyNativeComponents(plan.Components) + } return shared.HasListingCLI(backendExecutable) } @@ -35,6 +41,9 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return activateCodexProject(ctx, env, request) + } profileRoot := request.Client.ConfigRoot if profileRoot == "" { profileRoot = request.Plan.NativeRegistryRoot diff --git a/install/integrationctl/agentplugins/clients/codex/lifecycle_deactivate.go b/install/integrationctl/agentplugins/clients/codex/lifecycle_deactivate.go index 83a7b5519..13ac6ab36 100644 --- a/install/integrationctl/agentplugins/clients/codex/lifecycle_deactivate.go +++ b/install/integrationctl/agentplugins/clients/codex/lifecycle_deactivate.go @@ -13,6 +13,9 @@ import ( // Deactivate removes the managed Codex plugin and marketplace after the // operator confirms, or records the manual steps when the CLI is missing. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject { + return deactivateCodexProject(ctx, env, request) + } outcome := shared.StartedDeactivation() if err := validateProfile(request.Client.ConfigRoot, ""); err != nil { return outcome, err diff --git a/install/integrationctl/agentplugins/clients/codex/plan.go b/install/integrationctl/agentplugins/clients/codex/plan.go index 8c699c7bb..0155514d2 100644 --- a/install/integrationctl/agentplugins/clients/codex/plan.go +++ b/install/integrationctl/agentplugins/clients/codex/plan.go @@ -14,10 +14,17 @@ var ( ) func (*Adapter) CheckPlanPrecondition(input clients.PlanInput, plan *domain.DeliveryPlan) error { + if plan.Scope == domain.ScopeProject || input.TargetContext.Scope == domain.ScopeProject { + _, err := codexProjectPlanRoot(input.Client, *plan) + return err + } return validateProfile(input.Client.ConfigRoot, plan.NativeRegistryRoot) } -func (*Adapter) RefinePlan(_ context.Context, _ clients.PlanInput, plan *domain.DeliveryPlan) error { +func (*Adapter) RefinePlan(_ context.Context, input clients.PlanInput, plan *domain.DeliveryPlan) error { + if plan.Scope == domain.ScopeProject { + return refineCodexProjectPlan(input, plan) + } if plan.Status != domain.PlanUnsupported && shared.HasListingCLI(plan.NativeRegistryExecutable) { plan.Status = domain.PlanReady plan.Activation = domain.ActivationActive diff --git a/install/integrationctl/agentplugins/clients/codex/profile.go b/install/integrationctl/agentplugins/clients/codex/profile.go index 2487a1724..b62234e0a 100644 --- a/install/integrationctl/agentplugins/clients/codex/profile.go +++ b/install/integrationctl/agentplugins/clients/codex/profile.go @@ -88,6 +88,12 @@ func ValidateBindingProfile(root string, binding domain.ClientBinding) error { if err := validateProfile(root, ""); err != nil { return err } + if binding.Scope == string(domain.ScopeProject) { + if binding.NativeProfileRoot != root { + return fmt.Errorf("recorded Codex project root differs from selected root") + } + return validateCodexProjectObjects(root, binding.NativeObjects) + } if binding.NativeProfileRoot != "" { if err := validateProfile(binding.NativeProfileRoot, root); err != nil { return fmt.Errorf("recorded Codex profile mismatch: %w", err) diff --git a/install/integrationctl/agentplugins/clients/codex/project.go b/install/integrationctl/agentplugins/clients/codex/project.go index 9f4dfc068..92de4214f 100644 --- a/install/integrationctl/agentplugins/clients/codex/project.go +++ b/install/integrationctl/agentplugins/clients/codex/project.go @@ -11,8 +11,9 @@ import ( ) var ( - _ clients.Projector = (*Adapter)(nil) - _ clients.SelectionReader = (*Adapter)(nil) + _ clients.Projector = (*Adapter)(nil) + _ clients.SelectionReader = (*Adapter)(nil) + _ clients.ActiveNativeProjector = (*Adapter)(nil) ) // ManagedMCPSelection reports Codex's OpenAI-shaped `.mcp.json`. @@ -22,6 +23,9 @@ func (*Adapter) ManagedMCPSelection() clients.SelectionLayout { // Project renders the OpenAI compatibility layout and the managed marketplace. func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if in.Plan.Scope == domain.ScopeProject { + return projectCodexComponents(in) + } if in.Plan.PackageMode != domain.PackageProjection { return nil, nil } @@ -34,6 +38,16 @@ func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain return nil, nil } +func (*Adapter) ProjectActiveNative(_ context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { + if plan.Scope != domain.ScopeProject { + return nil, nil + } + if err := validateCodexProjectStdio(root, plan, dataPath); err != nil { + return nil, err + } + return codexProjectObjects(root, envelope, plan) +} + func projectOpenAI(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, hints domain.CompatibilityHints, dataPath string) error { manifest, err := shared.ProjectedOpenAIManifest(envelope) if err != nil { diff --git a/install/integrationctl/agentplugins/clients/codex/project_identity.go b/install/integrationctl/agentplugins/clients/codex/project_identity.go new file mode 100644 index 000000000..a88b71e32 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/project_identity.go @@ -0,0 +1,43 @@ +package codex + +import ( + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/codextoml" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func inspectPreparedCodexProject(plan domain.DeliveryPlan, owned bool) (clients.RegistryFinding, error) { + var managed *domain.ClientBinding + if owned { + managed = &domain.ClientBinding{Scope: string(domain.ScopeProject), NativeProfileRoot: plan.NativeRegistryRoot, NativeObjects: plan.PreviousNativeObjects} + } + return inspectCodexProject(plan, managed, nativeconfig.New()) +} + +func inspectCodexProject(plan domain.DeliveryPlan, managed *domain.ClientBinding, kernel nativeconfig.Kernel) (clients.RegistryFinding, error) { + if managed != nil { + if managed.PhysicalArtifact != "" && managed.PhysicalArtifact != plan.PhysicalArtifactID { + return clients.RegistryIndeterminate, fmt.Errorf("codex project binding artifact differs from plan") + } + if managed.Scope != string(domain.ScopeProject) { + return clients.RegistryIndeterminate, fmt.Errorf("codex project binding scope differs from plan") + } + if err := ValidateBindingProfile(plan.NativeRegistryRoot, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + selected := *managed + selected.NativeObjects = codexProjectOwned(managed.NativeObjects) + managed = &selected + } + return codexProjectLayout(plan.NativeRegistryRoot).Inspect(plan.Components, managed, kernel.WithCodexDocumentParser(codextoml.Parse)) +} + +func verifyCodexProjectObjects(root string, objects []domain.NativeObjectOwnership, allowMissing bool, kernel nativeconfig.Kernel) error { + if err := validateCodexProjectObjects(root, objects); err != nil { + return err + } + return codexProjectLayout(root).Verify(codexProjectOwned(objects), allowMissing, kernel.WithCodexDocumentParser(codextoml.Parse)) +} diff --git a/install/integrationctl/agentplugins/clients/codex/project_lifecycle.go b/install/integrationctl/agentplugins/clients/codex/project_lifecycle.go new file mode 100644 index 000000000..6ce3cecd2 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/project_lifecycle.go @@ -0,0 +1,88 @@ +package codex + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/codextoml" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func activateCodexProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + root, err := codexProjectPlanRoot(request.Client, request.Plan) + if err != nil { + return domain.ActivationOutcome{}, err + } + if err := New().PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + outcome, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ + Automatic: request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components), + UnavailableAction: "prepare the supported Codex project skills and MCP components", + RepairAction: "repair the exact managed Codex project components", + RetryAction: "retry Codex project component installation", + CompletedAction: codexProjectDiscoveryAction, + Verify: func() error { + return verifyCodexProjectObjects(root, request.Delivery.NativeObjects, false, env.NativeConfig) + }, + Activate: func(ctx context.Context, request domain.ActivationRequest) error { + return mutateCodexProject(ctx, env, root, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects) + }, + }) + if err != nil { + return outcome, err + } + outcome.Activation, outcome.ActivationAttested = domain.ActivationPrepared, false + conditions, err := codexProjectConditions(env.NativeConfig, request) + if err != nil { + return outcome, fmt.Errorf("codex project configuration was installed but restrictions could not be inspected: %w", err) + } + for _, condition := range conditions { + outcome.UserActions = shared.AppendUnique(outcome.UserActions, condition) + } + return outcome, nil +} + +func mutateCodexProject(ctx context.Context, env clients.Env, root, active string, previous, desired []domain.NativeObjectOwnership) error { + if err := validateCodexProjectObjects(root, previous); err != nil { + return err + } + if err := validateCodexProjectObjects(root, desired); err != nil { + return err + } + servers := make(map[string]nativeconfig.Server) + for _, object := range codexProjectOwned(desired) { + if object.Kind != codexProjectMCPKind { + continue + } + server, err := readCodexProjectServer(active, object.LogicalName) + if err != nil { + return err + } + servers[object.LogicalName] = server + } + return (shared.ProjectComponentMutation{Layout: codexProjectLayout(root), ActivePath: active, + Previous: codexProjectOwned(previous), Desired: codexProjectOwned(desired), Servers: servers, Kernel: env.NativeConfig.WithCodexDocumentParser(codextoml.Parse)}).Apply(ctx) +} + +func deactivateCodexProject(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + root, err := codexProjectRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return outcome, err + } + if !request.Confirmed { + outcome.UserActions = append(outcome.UserActions, "agentplugins will remove only its exact managed Codex project skill directories and MCP entries") + return outcome, nil + } + if err := mutateCodexProject(ctx, env, root, "", request.NativeObjects, nil); err != nil { + if !shared.CommittedNativeDeactivationCleanup(&outcome, err) { + return outcome, err + } + } + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/codex/project_objects.go b/install/integrationctl/agentplugins/clients/codex/project_objects.go new file mode 100644 index 000000000..397dd9b33 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/project_objects.go @@ -0,0 +1,100 @@ +package codex + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const ( + codexProjectSkillKind = "codex_project_skill_directory" + codexProjectMCPKind = "codex_project_mcp_server" +) + +func projectCodexComponents(in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if _, err := codexProjectPlanRoot(domain.DetectedClient{ConfigRoot: in.Plan.TargetContext.UserConfigRoot}, in.Plan); err != nil { + return nil, err + } + if err := shared.ProjectOpenAIMCP(in.StagingPath, in.Envelope, shared.SupportedMCPNames(in.Plan), in.Hints, in.Plan.ActivePath, in.PluginDataPath); err != nil { + return nil, err + } + return codexProjectObjects(in.StagingPath, in.Envelope, in.Plan) +} + +func codexProjectObjects(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + if _, err := codexProjectPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return nil, err + } + objects := make([]domain.NativeObjectOwnership, 0, len(plan.Components)) + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + object, err := codexProjectObject(root, envelope, plan, component) + if err != nil { + return nil, err + } + objects = append(objects, object) + } + sort.Slice(objects, func(i, j int) bool { return objects[i].ObjectID < objects[j].ObjectID }) + return objects, validateCodexProjectObjects(plan.NativeRegistryRoot, objects) +} + +func codexProjectObject(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, component domain.ComponentDecision) (domain.NativeObjectOwnership, error) { + layout := codexProjectLayout(plan.NativeRegistryRoot) + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return domain.NativeObjectOwnership{}, fmt.Errorf("planned Codex skill is missing") + } + relative := skill.RelativePath + if relative == "" { + relative = filepath.ToSlash(filepath.Join("skills", component.Name, "SKILL.md")) + } + return layout.SkillObject(component.Name, filepath.ToSlash(filepath.Dir(filepath.FromSlash(relative))), root) + case domain.ComponentMCPServer: + server, err := readCodexProjectServer(root, component.Name) + if err != nil { + return domain.NativeObjectOwnership{}, err + } + return layout.MCPObject(component.Name, server, codexProjectConfigPath(plan.NativeRegistryRoot)) + default: + return domain.NativeObjectOwnership{}, fmt.Errorf("unsupported Codex project component kind %q", component.Kind) + } +} + +func codexProjectLayout(root string) shared.ProjectComponents { + return shared.ProjectComponents{ProjectRoot: root, SkillsRoot: filepath.Join(root, ".agents", "skills"), + ConfigPaths: nativeconfig.Paths{JSON: codexProjectConfigPath(root)}, Codec: nativeconfig.CodecCodex, + SkillKind: codexProjectSkillKind, MCPKind: codexProjectMCPKind} +} + +func codexProjectConfigPath(root string) string { return filepath.Join(root, ".codex", "config.toml") } +func codexProjectSkillPath(root, name string) string { + return filepath.Join(root, ".agents", "skills", name) +} + +func codexProjectOwned(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + var owned []domain.NativeObjectOwnership + for _, object := range objects { + if object.Kind == codexProjectSkillKind || object.Kind == codexProjectMCPKind { + owned = append(owned, object) + } + } + return owned +} + +func validateCodexProjectObjects(root string, objects []domain.NativeObjectOwnership) error { + for _, object := range objects { + if object.Kind != codexProjectSkillKind && object.Kind != codexProjectMCPKind && object.Kind != "managed_package_directory" { + return fmt.Errorf("codex project binding contains an unexpected native object kind") + } + } + return codexProjectLayout(root).ValidateObjects(codexProjectOwned(objects)) +} diff --git a/install/integrationctl/agentplugins/clients/codex/project_server.go b/install/integrationctl/agentplugins/clients/codex/project_server.go new file mode 100644 index 000000000..04a4364f0 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/project_server.go @@ -0,0 +1,78 @@ +package codex + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Read the existing OpenAI projection so managed stdio paths/data and accepted +// compatibility auth references are materialized once by the shared projector. +func readCodexProjectServer(root, name string) (nativeconfig.Server, error) { + var result nativeconfig.Server + path := filepath.Join(root, ".mcp.json") + if err := pathpolicy.RequireContainedChild(root, path); err != nil { + return result, err + } + file, err := nativeconfig.New().ReadExactFile(path) + if err != nil { + return result, err + } + if !file.Exists || len(file.Body) > 1<<20 { + return result, fmt.Errorf("codex projected MCP document is absent or exceeds inspection limit") + } + body := file.Body + doc, err := shared.DecodeStrictJSONObject(body) + if err != nil { + return result, err + } + servers, ok := doc["mcpServers"].(map[string]any) + if !ok { + return result, fmt.Errorf("codex projection has no MCP server collection") + } + value, ok := servers[name].(map[string]any) + if !ok { + return result, fmt.Errorf("codex projected MCP server is missing") + } + body, err = json.Marshal(value) + if err != nil { + return result, err + } + if err := json.Unmarshal(body, &result); err != nil { + return result, fmt.Errorf("codex projected MCP server has invalid fields") + } + switch result.Type { + case "", "stdio": + result.Type, result.StdioValuesResolved, result.CWDResolved = "stdio", true, true + case "http": + result.Type, result.RemoteTransport = "remote", "streamable-http" + default: + return result, fmt.Errorf("codex project MCP requires stdio or streamable-http") + } + var auth nativeconfig.CodexAuthOptions + if err := json.Unmarshal(body, &auth); err != nil { + return result, fmt.Errorf("codex projected authentication references are invalid") + } + if auth != (nativeconfig.CodexAuthOptions{}) { + result.CodexAuth = &auth + } + return result, nil +} + +func validateCodexProjectStdio(root string, plan domain.DeliveryPlan, dataPath string) error { + for _, name := range shared.SupportedMCPNames(plan) { + server, err := readCodexProjectServer(root, name) + if err != nil { + return err + } + if server.Type == "stdio" && (server.Env["PLUGIN_ROOT"] != plan.ActivePath || dataPath != "" && server.Env["PLUGIN_DATA"] != dataPath) { + return fmt.Errorf("codex projected stdio package/data environment differs from bound locators") + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/codex/project_skill_settings.go b/install/integrationctl/agentplugins/clients/codex/project_skill_settings.go new file mode 100644 index 000000000..41d6e824e --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/project_skill_settings.go @@ -0,0 +1,47 @@ +package codex + +import ( + "path/filepath" + "strings" +) + +type codexSkillSelector struct { + Name *string `toml:"name"` + Path *string `toml:"path"` + Enabled *bool `toml:"enabled"` +} + +// The pinned client accepts skill selectors only from user/session layers, not +// project layers. This checks the visible user file's ordered rules only; it +// does not infer the selected native profile or session's effective policy. +func codexUserSkillDisabled(selectors []codexSkillSelector, name, document string) bool { + // Native SkillsConfig decoding is all-or-nothing for the selected layer. + // Missing enabled or a nonabsolute typed path invalidates the whole section. + for _, selector := range selectors { + if selector.Enabled == nil || selector.Path != nil && !filepath.IsAbs(*selector.Path) { + return false + } + } + disabled := false + document = codexSkillDocumentIdentity(document) + for _, selector := range selectors { + matches := false + switch { + case selector.Name != nil && selector.Path == nil: + matches = strings.TrimSpace(*selector.Name) == name + case selector.Path != nil && selector.Name == nil: + matches = codexSkillDocumentIdentity(*selector.Path) == document + } + if matches { + disabled = !*selector.Enabled + } + } + return disabled +} + +func codexSkillDocumentIdentity(path string) string { + if canonical, err := filepath.EvalSymlinks(path); err == nil { + return canonical + } + return filepath.Clean(path) +} diff --git a/install/integrationctl/agentplugins/clients/codex/project_status.go b/install/integrationctl/agentplugins/clients/codex/project_status.go new file mode 100644 index 000000000..e156715aa --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/project_status.go @@ -0,0 +1,121 @@ +package codex + +import ( + "fmt" + "path/filepath" + + "github.com/pelletier/go-toml/v2" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/codextoml" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject && request.Plan.TargetContext.Scope != domain.ScopeProject { + return nil + } + if _, err := codexProjectPlanRoot(request.Client, request.Plan); err != nil { + return err + } + if _, err := codexProjectConditions(env.NativeConfig, request); err != nil { + return err + } + if request.ActivationComplete { + return fmt.Errorf("codex project native discovery cannot be established by activation attestation; inspect skills and MCP in a fresh trusted project session") + } + return nil +} + +func refineCodexProjectPlan(input clients.PlanInput, plan *domain.DeliveryPlan) error { + if _, err := codexProjectPlanRoot(input.Client, *plan); err != nil { + return err + } + for i := range plan.Components { + component := &plan.Components[i] + if component.Kind == domain.ComponentMCPServer && input.Envelope.MCP.Servers[component.Name].Type == "sse" { + component.Support = domain.SupportUnsupported + component.Reason = "Codex project MCP supports stdio and streamable-http; SSE is unsupported" + } + } + if !shared.OnlyNativeComponents(plan.Components) { + plan.Status, plan.Activation = domain.PlanUnsupported, domain.ActivationNotRequired + return nil + } + shared.PromoteNativeReady(plan, plan.NativeRegistryRoot, true) + plan.UserActions = shared.AppendUnique(plan.UserActions, codexProjectDiscoveryAction) + return nil +} + +const codexProjectDiscoveryAction = "Codex project components are configured; native discovery, project trust, effective managed/session policy and MCP initialization remain unobserved; start Codex in the selected project and inspect skills and `codex mcp list --json`" + +type codexProjectSettings struct { + MCPServers map[string]struct { + Enabled *bool `toml:"enabled"` + } `toml:"mcp_servers"` + Skills struct { + IncludeInstructions *bool `toml:"include_instructions"` + Config []codexSkillSelector `toml:"config"` + } `toml:"skills"` +} + +func readCodexProjectSettings(kernel nativeconfig.Kernel, path string) (codexProjectSettings, error) { + kernel = kernel.WithCodexDocumentParser(codextoml.Parse) + var settings codexProjectSettings + if _, _, err := kernel.Inspect(nativeconfig.Paths{JSON: path}, nativeconfig.CodecCodex, "agentplugins-policy-inspection", nil); err != nil { + return settings, err + } + file, err := kernel.ReadExactFile(path) + if err != nil || !file.Exists { + return settings, err + } + if len(file.Body) > 1<<20 { + return settings, fmt.Errorf("codex config exceeds component inspection limit") + } + if err := toml.Unmarshal(file.Body, &settings); err != nil { + return settings, fmt.Errorf("codex component settings are invalid") + } + return settings, nil +} + +// These preserve visible user/project restrictions. They deliberately do not +// reconstruct native profile, trust, managed, ancestor or session precedence. +func codexProjectConditions(kernel nativeconfig.Kernel, request domain.ActivationRequest) ([]string, error) { + var conditions []string + paths := []string{filepath.Join(request.Client.ConfigRoot, "config.toml"), codexProjectConfigPath(request.Plan.NativeRegistryRoot)} + for i, path := range paths { + settings, err := readCodexProjectSettings(kernel, path) + if err != nil { + return nil, err + } + conditions = append(conditions, codexConfiguredRestrictions(settings, request, i == 0)...) + } + return conditions, nil +} + +func codexConfiguredRestrictions(settings codexProjectSettings, request domain.ActivationRequest, userLayer bool) []string { + var conditions []string + for _, component := range request.Plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + if component.Kind == domain.ComponentMCPServer { + if enabled := settings.MCPServers[component.Name].Enabled; enabled != nil && !*enabled { + conditions = append(conditions, fmt.Sprintf("preserved Codex settings disable MCP server %q; configuration does not prove native discovery", component.Name)) + } + } + if component.Kind == domain.ComponentSkill { + if settings.Skills.IncludeInstructions != nil && !*settings.Skills.IncludeInstructions { + conditions = append(conditions, "preserved Codex settings disable automatic skill instructions; explicit native discovery remains unobserved") + } + if userLayer && codexUserSkillDisabled(settings.Skills.Config, component.Name, filepath.Join(codexProjectSkillPath(request.Plan.NativeRegistryRoot, component.Name), "SKILL.md")) { + conditions = append(conditions, fmt.Sprintf("preserved Codex user settings disable skill %q; session overrides and native effective enablement remain unobserved", component.Name)) + } + } + } + return conditions +} diff --git a/install/integrationctl/agentplugins/clients/codex/scope.go b/install/integrationctl/agentplugins/clients/codex/scope.go new file mode 100644 index 000000000..a7444607a --- /dev/null +++ b/install/integrationctl/agentplugins/clients/codex/scope.go @@ -0,0 +1,65 @@ +package codex + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return in.TargetContext.ProjectRoot, in.Client.ExecutablePath + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if target.Scope != domain.ScopeProject { + return "", "", fmt.Errorf("codex project layout requires project scope") + } + if _, err := codexProjectRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", fmt.Errorf("codex prepared package escapes selected project: %w", err) + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func codexProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + if target.Scope != domain.ScopeProject { + return "", fmt.Errorf("codex component delivery requires project target context") + } + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("codex original user profile differs from target context") + } + if err := validateProfile(validated.ProjectRoot, planned); err != nil { + return "", err + } + for _, relative := range []string{".codex", filepath.Join(".agents", "skills")} { + if err := pathpolicy.RequireContainedChild(validated.ProjectRoot, filepath.Join(validated.ProjectRoot, relative)); err != nil { + return "", err + } + } + return validated.ProjectRoot, nil +} + +func codexProjectPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject { + return "", fmt.Errorf("codex plan scope differs from project context") + } + return codexProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) +} diff --git a/install/integrationctl/agentplugins/clients/copilot/identity.go b/install/integrationctl/agentplugins/clients/copilot/identity.go index be2262aa3..a882cc2f7 100644 --- a/install/integrationctl/agentplugins/clients/copilot/identity.go +++ b/install/integrationctl/agentplugins/clients/copilot/identity.go @@ -3,15 +3,41 @@ package copilot import ( "context" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) var _ clients.RegistryInspector = (*Adapter)(nil) +var _ clients.PreparedRegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return true } -func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, _ domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { + if err := ctx.Err(); err != nil { + return clients.RegistryIndeterminate, err + } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if _, err := copilotPlanRoot(client, plan); err != nil { + return clients.RegistryIndeterminate, err + } + return inspectCopilotProject(plan, managed, env.NativeConfig) + } return shared.InspectCopilotRegistry(ctx, env, plan, managed) } + +func (*Adapter) InspectPreparedRegistry(plan domain.DeliveryPlan, name string, owned bool) (clients.RegistryFinding, error) { + if plan.Scope != domain.ScopeProject && plan.TargetContext.Scope != domain.ScopeProject { + return shared.InspectUnqualifiedPluginRoot(plan.TargetRoot, name, plan.ActivePath, owned) + } + if _, err := copilotPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return clients.RegistryIndeterminate, err + } + var managed *domain.ClientBinding + if owned { + managed = &domain.ClientBinding{Scope: string(domain.ScopeProject), NativeProfileRoot: plan.NativeRegistryRoot, + PhysicalArtifact: plan.PhysicalArtifactID, TargetLocator: plan.ActivePath, NativeObjects: plan.PreviousNativeObjects} + } + return inspectCopilotProject(plan, managed, nativeconfig.New()) +} diff --git a/install/integrationctl/agentplugins/clients/copilot/lifecycle.go b/install/integrationctl/agentplugins/clients/copilot/lifecycle.go index 7c7e3a05e..a4a14d1a2 100644 --- a/install/integrationctl/agentplugins/clients/copilot/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/copilot/lifecycle.go @@ -15,13 +15,19 @@ var ( _ clients.ReadOnlyVerifier = (*Adapter)(nil) ) -// AutomaticallyActivates reports that Activate will drive the Copilot CLI. +// AutomaticallyActivates selects repository files or the existing user CLI lane. func (*Adapter) AutomaticallyActivates(env clients.Env, request domain.ActivationRequest) bool { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components) + } return shared.CopilotAutomaticallyActivates(env, request) } -// VerifierAvailable reports that an exact Copilot listing can be observed. -func (*Adapter) VerifierAvailable(_ domain.DetectedClient, _ domain.DeliveryPlan, backendExecutable string) bool { +// VerifierAvailable reports exact repository configuration or user CLI inspection. +func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, backendExecutable string) bool { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + return shared.OnlyNativeComponents(plan.Components) + } return shared.CopilotVerifierAvailable(backendExecutable) } @@ -31,6 +37,9 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return activateCopilotProject(ctx, env, request) + } outcome := shared.StartedActivation(request) if !shared.HasClientCLI(env, request.BackendExecutable) { outcome.Activation = domain.ActivationManual @@ -43,6 +52,9 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac // Deactivate uninstalls through the Copilot CLI after confirmation. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject { + return deactivateCopilotProject(ctx, env, request) + } action := fmt.Sprintf("run `copilot plugin uninstall %s`, then rerun remove with `--external-uninstalled`", request.DeclaredName) return shared.CompleteCopilotDeactivation(ctx, env, request, action) } diff --git a/install/integrationctl/agentplugins/clients/copilot/plan.go b/install/integrationctl/agentplugins/clients/copilot/plan.go index 7f0c019c6..d126cd884 100644 --- a/install/integrationctl/agentplugins/clients/copilot/plan.go +++ b/install/integrationctl/agentplugins/clients/copilot/plan.go @@ -11,10 +11,20 @@ import ( var _ clients.PlanRefiner = (*Adapter)(nil) -// RefinePlan promotes the plan when the Copilot CLI that performs the install -// is present. The CLI is this client's own executable; VS Code reaches the same -// backend through its sibling. -func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { +// RefinePlan keeps repository configuration separate from user CLI activation. +func (*Adapter) RefinePlan(ctx context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if err := ctx.Err(); err != nil { + return err + } + if in.TargetContext.Scope == domain.ScopeProject || plan.Scope == domain.ScopeProject { + if _, err := copilotPlanRoot(in.Client, *plan); err != nil { + return err + } + shared.PromoteNativeReady(plan, plan.NativeRegistryRoot, shared.OnlyNativeComponents(plan.Components)) + plan.Activation = domain.ActivationPrepared + plan.UserActions = shared.AppendUnique(plan.UserActions, "repository skills and MCP entries can be configured automatically; Copilot native discovery, trust, enablement and session policy remain unobserved") + return nil + } executable := in.Client.ExecutablePath shared.PromoteBackendReady(plan, executable) action := "GitHub Copilot CLI is required for automatic activation" diff --git a/install/integrationctl/agentplugins/clients/copilot/project.go b/install/integrationctl/agentplugins/clients/copilot/project.go index 659c533f9..53e59fe04 100644 --- a/install/integrationctl/agentplugins/clients/copilot/project.go +++ b/install/integrationctl/agentplugins/clients/copilot/project.go @@ -2,6 +2,7 @@ package copilot import ( "context" + "fmt" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" @@ -9,11 +10,50 @@ import ( ) var _ clients.Projector = (*Adapter)(nil) +var _ clients.ActiveNativeProjector = (*Adapter)(nil) -// Project writes the managed GitHub Copilot marketplace that VS Code shares. -func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { +// Project writes repository component receipts or the existing user marketplace. +func (*Adapter) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if in.Plan.Scope == domain.ScopeProject || in.Plan.TargetContext.Scope == domain.ScopeProject { + if _, err := copilotPlanRoot(domain.DetectedClient{ConfigRoot: in.Plan.TargetContext.UserConfigRoot}, in.Plan); err != nil { + return nil, err + } + if err := shared.ProjectMCPServers(shared.MCPProjection{Root: in.StagingPath, Envelope: in.Envelope, + Names: shared.SupportedMCPNames(in.Plan), Dialect: shared.MCPDialectCursor, PluginRoot: in.Plan.ActivePath, DataPath: in.PluginDataPath}); err != nil { + return nil, err + } + if err := projectCopilotStdio(in); err != nil { + return nil, err + } + return copilotProjectObjects(in.StagingPath, in.Envelope, in.Plan) + } if err := shared.ProjectCopilotMarketplace(in.StagingPath, in.Envelope, in.Plan); err != nil { return nil, err } return nil, nil } + +func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if plan.ClientID != domain.ClientCopilot || root == "" || root != plan.ActivePath { + return nil, fmt.Errorf("copilot active projection client or root differs") + } + if err := plan.SelectedDelivery.ValidatePlan(plan, envelope.TreeDigest); err != nil { + return nil, err + } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if _, err := copilotPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return nil, err + } + if err := validateCopilotProjectStdio(root, envelope, plan, dataPath); err != nil { + return nil, err + } + return copilotProjectObjects(root, envelope, plan) + } + return nil, nil +} diff --git a/install/integrationctl/agentplugins/clients/copilot/project_active.go b/install/integrationctl/agentplugins/clients/copilot/project_active.go new file mode 100644 index 000000000..ee9bfccd0 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/project_active.go @@ -0,0 +1,45 @@ +package copilot + +import ( + "fmt" + "path/filepath" + "reflect" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Reuse validates the complete immutable stdio projection against the selected +// package and data receipt. It never rewrites old locators into fresh authority. +func validateCopilotProjectStdio(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) error { + for _, name := range shared.SupportedMCPNames(plan) { + source := envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + if !filepath.IsAbs(dataPath) || filepath.Clean(dataPath) != dataPath { + return fmt.Errorf("%w: copilot selected stdio data path is not absolute and clean", domain.ErrProjectionRefreshRequired) + } + entry := shared.CloneObject(source.Decoded) + delete(entry, "type") + if err := shared.ApplyStdioDataContract(entry, plan.ActivePath, dataPath, root); err != nil { + return err + } + expected, err := decodeCopilotProjectServer(entry) + if err != nil { + return err + } + expected, err = bindCopilotProjectStdio(expected, source, plan.ActivePath, dataPath) + if err != nil { + return err + } + actual, err := readCopilotProjectServer(root, name) + if err != nil { + return err + } + if !reflect.DeepEqual(actual, expected) { + return fmt.Errorf("%w: copilot immutable stdio package/data/launcher projection differs", domain.ErrProjectionRefreshRequired) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/copilot/project_config.go b/install/integrationctl/agentplugins/clients/copilot/project_config.go new file mode 100644 index 000000000..f0a2d592e --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/project_config.go @@ -0,0 +1,76 @@ +package copilot + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func copilotComponents(root string) (shared.ProjectComponents, error) { + path := filepath.Join(root, ".github", "mcp.json") + workspace := filepath.Join(root, ".mcp.json") + if info, err := os.Lstat(workspace); err == nil { + if !info.Mode().IsRegular() { + return shared.ProjectComponents{}, fmt.Errorf("copilot workspace MCP config must be a regular file") + } + path = workspace + } else if !os.IsNotExist(err) { + return shared.ProjectComponents{}, err + } + if err := pathpolicy.RequireContainedChild(root, path); err != nil { + return shared.ProjectComponents{}, err + } + return shared.ProjectComponents{ProjectRoot: root, SkillsRoot: filepath.Join(root, ".github", "skills"), + ConfigPaths: nativeconfig.Paths{JSON: path}, Codec: nativeconfig.CodecCopilot, + SkillKind: copilotProjectSkillKind, MCPKind: copilotProjectMCPKind}, nil +} + +func validateCopilotConfig(kernel nativeconfig.Kernel, path string) error { + file, err := kernel.ReadExactFile(path) + if err != nil || !file.Exists { + return err + } + if len(file.Body) > 1<<20 { + return fmt.Errorf("copilot project MCP document exceeds inspection limit") + } + document, err := shared.DecodeStrictJSONObject(file.Body) + if err != nil { + return err + } + if len(document) == 0 { + return nil + } + if _, ok := document["mcpServers"].(map[string]any); !ok { + return fmt.Errorf("copilot project MCP requires a nested mcpServers object; existing bare maps are preserved without mutation") + } + return nil +} + +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil // Preserve the existing user CLI-registry admission contract. + } + if binding.NativeProfileRoot != root { + return fmt.Errorf("copilot recorded project registry differs from selected root") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + expected := filepath.Join(root, ".uap", "managed", "clients", "copilot", binding.PhysicalArtifact) + if binding.TargetLocator != expected { + return fmt.Errorf("copilot recorded package differs from selected project") + } + if err := pathpolicy.RequireContainedChild(root, expected); err != nil { + return err + } + layout, err := copilotComponents(root) + if err != nil { + return err + } + return layout.ValidateObjects(copilotNativeObjects(binding.NativeObjects)) +} diff --git a/install/integrationctl/agentplugins/clients/copilot/project_identity.go b/install/integrationctl/agentplugins/clients/copilot/project_identity.go new file mode 100644 index 000000000..984538a19 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/project_identity.go @@ -0,0 +1,43 @@ +package copilot + +import ( + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func inspectCopilotProject(plan domain.DeliveryPlan, managed *domain.ClientBinding, kernel nativeconfig.Kernel) (clients.RegistryFinding, error) { + layout, err := copilotComponents(plan.NativeRegistryRoot) + if err != nil { + return clients.RegistryIndeterminate, err + } + if shared.ComponentKindPresent(plan.Components, domain.ComponentMCPServer) || copilotHasOwnedMCP(plan.PreviousNativeObjects) { + if err := validateCopilotConfig(kernel, layout.ConfigPaths.JSON); err != nil { + return clients.RegistryIndeterminate, err + } + } + if managed != nil { + if managed.Scope != string(domain.ScopeProject) || managed.PhysicalArtifact != plan.PhysicalArtifactID || managed.TargetLocator != plan.ActivePath { + return clients.RegistryIndeterminate, fmt.Errorf("copilot project binding scope, artifact or locator differs from plan") + } + if err := New().ValidateBindingProfile(plan.NativeRegistryRoot, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + selected := *managed + selected.NativeObjects = copilotNativeObjects(managed.NativeObjects) + managed = &selected + } + return layout.Inspect(plan.Components, managed, kernel) +} + +func copilotHasOwnedMCP(objects []domain.NativeObjectOwnership) bool { + for _, object := range objects { + if object.Kind == copilotProjectMCPKind { + return true + } + } + return false +} diff --git a/install/integrationctl/agentplugins/clients/copilot/project_lifecycle.go b/install/integrationctl/agentplugins/clients/copilot/project_lifecycle.go new file mode 100644 index 000000000..f4e298096 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/project_lifecycle.go @@ -0,0 +1,105 @@ +package copilot + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject && request.Plan.TargetContext.Scope != domain.ScopeProject { + return nil + } + root, err := copilotPlanRoot(request.Client, request.Plan) + if err != nil { + return err + } + if request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath { + return fmt.Errorf("copilot delivered package differs from selected project artifact") + } + if request.ActivationComplete { + return fmt.Errorf("copilot project native discovery cannot be established by activation attestation; inspect the selected trusted project") + } + if err := env.NativeConfig.RequireFileIO(); err != nil { + return err + } + if shared.ComponentKindPresent(request.Plan.Components, domain.ComponentMCPServer) { + layout, err := copilotComponents(root) + if err != nil { + return err + } + return validateCopilotConfig(env.NativeConfig, layout.ConfigPaths.JSON) + } + return nil +} + +func activateCopilotProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + root, err := copilotPlanRoot(request.Client, request.Plan) + if err != nil { + return domain.ActivationOutcome{}, err + } + if request.Delivery.ActivePath != request.Plan.ActivePath { + return domain.ActivationOutcome{}, fmt.Errorf("copilot delivered package differs from selected project artifact") + } + if err := New().PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + layout, err := copilotComponents(root) + if err != nil { + return domain.ActivationOutcome{}, err + } + outcome, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ + Automatic: request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components), + UnavailableAction: "select supported Copilot project skills and MCP components", + RepairAction: "repair exact managed Copilot project components", RetryAction: "retry Copilot project component installation", + CompletedAction: "Copilot project components are configured; native discovery, folder trust, plugin/session/admin precedence, disabled selection and MCP initialization remain unobserved", + Verify: func() error { + return layout.Verify(copilotNativeObjects(request.Delivery.NativeObjects), false, env.NativeConfig) + }, + Activate: func(ctx context.Context, request domain.ActivationRequest) error { + servers := map[string]nativeconfig.Server{} + for _, object := range copilotNativeObjects(request.Delivery.NativeObjects) { + if object.Kind == copilotProjectMCPKind { + server, err := readCopilotProjectServer(request.Delivery.ActivePath, object.LogicalName) + if err != nil { + return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} + } + servers[object.LogicalName] = server + } + } + return (shared.ProjectComponentMutation{Layout: layout, ActivePath: request.Delivery.ActivePath, + Previous: copilotNativeObjects(request.PreviousNativeObjects), Desired: copilotNativeObjects(request.Delivery.NativeObjects), Servers: servers, Kernel: env.NativeConfig}).Apply(ctx) + }, + }) + if err == nil && outcome.Verification == domain.VerificationInstalled { + outcome.Activation, outcome.ActivationAttested = domain.ActivationPrepared, false + } + return outcome, err +} + +func deactivateCopilotProject(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + root, err := copilotProjectRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return outcome, err + } + if !request.Confirmed { + outcome.UserActions = append(outcome.UserActions, "agentplugins will remove only exact owned Copilot repository skills and MCP members") + return outcome, nil + } + layout, err := copilotComponents(root) + if err != nil { + return outcome, err + } + if err := (shared.ProjectComponentMutation{Layout: layout, Previous: copilotNativeObjects(request.NativeObjects), Kernel: env.NativeConfig}).Apply(ctx); err != nil && !shared.CommittedNativeDeactivationCleanup(&outcome, err) { + return outcome, err + } + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/copilot/project_mcp.go b/install/integrationctl/agentplugins/clients/copilot/project_mcp.go new file mode 100644 index 000000000..83488e7b2 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/project_mcp.go @@ -0,0 +1,127 @@ +package copilot + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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 readCopilotProjectMCP(root string) (map[string]any, map[string]any, error) { + path := filepath.Join(root, "mcp.json") + if err := pathpolicy.RequireContainedChild(root, path); err != nil { + return nil, nil, err + } + file, err := nativeconfig.New().ReadExactFile(path) + if err != nil { + return nil, nil, err + } + if !file.Exists || len(file.Body) > 1<<20 { + return nil, nil, fmt.Errorf("copilot package MCP document is missing or exceeds inspection limit") + } + document, err := shared.DecodeStrictJSONObject(file.Body) + if err != nil { + return nil, nil, err + } + servers, ok := document["mcpServers"].(map[string]any) + if !ok { + return nil, nil, fmt.Errorf("copilot package MCP collection is missing") + } + return document, servers, nil +} + +func readCopilotProjectServer(root, name string) (nativeconfig.Server, error) { + _, servers, err := readCopilotProjectMCP(root) + if err != nil { + return nativeconfig.Server{}, err + } + entry, ok := servers[name].(map[string]any) + if !ok { + return nativeconfig.Server{}, fmt.Errorf("copilot selected MCP server is missing") + } + return decodeCopilotProjectServer(entry) +} + +func decodeCopilotProjectServer(entry map[string]any) (nativeconfig.Server, error) { + body, err := json.Marshal(entry) + if err != nil { + return nativeconfig.Server{}, err + } + var server nativeconfig.Server + if err := json.Unmarshal(body, &server); err != nil { + return server, err + } + switch server.Type { + case "", "stdio": + server.Type = "stdio" + case "http": + server.Type, server.RemoteTransport = "remote", "streamable-http" + case "sse": + server.Type, server.RemoteTransport = "remote", "sse" + default: + return server, fmt.Errorf("copilot projected MCP transport is unsupported") + } + server.StdioValuesResolved, server.CWDResolved = true, true + return server, nil +} + +func projectCopilotStdio(in clients.ProjectionInput) error { + if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { + return err + } + if len(shared.SupportedMCPNames(in.Plan)) == 0 { + return nil + } + document, entries, err := readCopilotProjectMCP(in.StagingPath) + if err != nil { + return err + } + for _, name := range shared.SupportedMCPNames(in.Plan) { + source := in.Envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + server, err := readCopilotProjectServer(in.StagingPath, name) + if err != nil { + return err + } + server, err = bindCopilotProjectStdio(server, source, in.Plan.ActivePath, in.PluginDataPath) + if err != nil { + return err + } + body, err := json.Marshal(server) + if err != nil { + return err + } + var native map[string]any + if err := json.Unmarshal(body, &native); err != nil { + return err + } + entries[name] = native + } + return shared.WriteJSON(filepath.Join(in.StagingPath, "mcp.json"), document) +} + +func bindCopilotProjectStdio(server nativeconfig.Server, source domain.MCPServer, activePath, dataPath string) (nativeconfig.Server, error) { + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, activePath, dataPath) + if err != nil { + return server, err + } + command, _ := source.Decoded["command"].(string) + if strings.HasPrefix(command, "${package.root}/") { + command = "./" + strings.TrimPrefix(command, "${package.root}/") + } + server.Args = managedstdio.Arguments(activePath, dataPath, server.CWD, cwd.Anchor, command, server.Args) + server.Command = filepath.Join(activePath, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + server.CWD = "" + return server, nil +} diff --git a/install/integrationctl/agentplugins/clients/copilot/project_objects.go b/install/integrationctl/agentplugins/clients/copilot/project_objects.go new file mode 100644 index 000000000..f114cc086 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/project_objects.go @@ -0,0 +1,76 @@ +package copilot + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const ( + copilotProjectSkillKind = "copilot_project_skill_directory" + copilotProjectMCPKind = "copilot_project_mcp_server" +) + +func copilotProjectObjects(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + project, err := copilotPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan) + if err != nil { + return nil, err + } + if err := pathpolicy.RequireContainedChild(project, root); err != nil { + return nil, err + } + layout, err := copilotComponents(project) + if err != nil { + return nil, err + } + objects := make([]domain.NativeObjectOwnership, 0, len(plan.Components)) + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + object, err := copilotProjectObject(layout, root, envelope, component) + if err != nil { + return nil, err + } + objects = append(objects, object) + } + sort.Slice(objects, func(i, j int) bool { return objects[i].ObjectID < objects[j].ObjectID }) + return objects, layout.ValidateObjects(objects) +} + +func copilotProjectObject(layout shared.ProjectComponents, root string, envelope domain.PackageEnvelope, component domain.ComponentDecision) (domain.NativeObjectOwnership, error) { + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return domain.NativeObjectOwnership{}, fmt.Errorf("copilot planned skill is missing") + } + relative := filepath.ToSlash(filepath.Dir(filepath.FromSlash(skill.RelativePath))) + if skill.RelativePath == "" { + relative = "skills/" + component.Name + } + return layout.SkillObject(component.Name, relative, root) + case domain.ComponentMCPServer: + server, err := readCopilotProjectServer(root, component.Name) + if err != nil { + return domain.NativeObjectOwnership{}, err + } + return layout.MCPObject(component.Name, server, layout.ConfigPaths.JSON) + default: + return domain.NativeObjectOwnership{}, fmt.Errorf("copilot project component %q is unsupported", component.Kind) + } +} + +func copilotNativeObjects(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + selected := make([]domain.NativeObjectOwnership, 0, len(objects)) + for _, object := range objects { + if object.Kind != "managed_package_directory" { + selected = append(selected, object) + } + } + return selected +} diff --git a/install/integrationctl/agentplugins/clients/copilot/scope.go b/install/integrationctl/agentplugins/clients/copilot/scope.go new file mode 100644 index 000000000..cc3c25c1c --- /dev/null +++ b/install/integrationctl/agentplugins/clients/copilot/scope.go @@ -0,0 +1,84 @@ +package copilot + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) + _ clients.ProfileBindingValidator = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return in.TargetContext.ProjectRoot, "" + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if _, err := copilotProjectRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", err + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject || plan.Scope == domain.ScopeProject { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("copilot project plan requires matching project scope") + } + // The generic planner resolves the physical artifact after this check. + _, err := copilotProjectRoot(in.Client, plan.TargetContext, plan.NativeRegistryRoot) + return err + } + return nil +} + +func copilotPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return "", fmt.Errorf("copilot project plan requires matching project scope") + } + root, err := copilotProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + expected := filepath.Join(root, ".uap", "managed", "clients", "copilot", plan.PhysicalArtifactID) + if plan.ActivePath != expected { + return "", fmt.Errorf("copilot managed package differs from selected physical artifact") + } + return root, pathpolicy.RequireContainedChild(root, expected) +} + +func copilotProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + selected, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if selected.Scope != domain.ScopeProject || selected.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("copilot project scope or original user profile differs from selected target") + } + if planned != "" && planned != selected.ProjectRoot { + return "", fmt.Errorf("copilot native registry differs from selected project") + } + for _, relative := range []string{".mcp.json", filepath.Join(".github", "mcp.json"), filepath.Join(".github", "skills")} { + if err := pathpolicy.RequireContainedChild(selected.ProjectRoot, filepath.Join(selected.ProjectRoot, relative)); err != nil { + return "", err + } + } + return selected.ProjectRoot, nil +} diff --git a/install/integrationctl/agentplugins/clients/cursor/README.md b/install/integrationctl/agentplugins/clients/cursor/README.md index 979cfcfa5..71ff06df0 100644 --- a/install/integrationctl/agentplugins/clients/cursor/README.md +++ b/install/integrationctl/agentplugins/clients/cursor/README.md @@ -1,6 +1,35 @@ -# Cursor adapter: selected editor package preparation +# Cursor adapter: project components and user package preparation -This adapter retains the manual skill/MCP package workflow. It prepares an +## Project component lane + +Project requests own exact directories at `.cursor/skills/` and exact +MCP entries in `.cursor/mcp.json`. Prepared artifacts stay under the selected +project's `.uap/managed/clients/cursor/`. Detected user ConfigRoot remains user +authority and is never replaced by the workspace root. Native config ownership +uses the existing kernel's whole-entry receipts, conditional writes and locks; +skill directories stage, verify and swap with rollback around one MCP batch. +Foreign same-name or disabled entries collide, foreign hooks/settings remain +untouched, and persistent application data survives component removal. + +Project stdio uses the existing trusted managed launcher with explicit package, +data and cwd bindings. It does not rely on plugin placeholder expansion or +implicit native cwd behavior. No client, package, model or MCP code runs during +installation. Successful configuration reports `prepared` activation with +`installation_verified`; discovery, aliases, enablement, approvals, policy and +MCP initialization remain unobserved. Project activation attestation is refused +before generic provider shortcuts and on direct adapter reuse. + +The composed contract suite covers loader/planner/command rendering/staging, +remote and stdio MCP projection, repeat/update/repair/component removal, +user/project A/B isolation, malformed/disabled/foreign collisions, writer +failure rollback, exact root/context authority and contained alias refusal. +These tests do not qualify native runtime discovery. Current official docs and +pinned CLI help evidence are recorded in +`docs/plans/project-install-e2e/cursor-implementation-evidence.md`. + +## Preserved user package preparation + +The user lane retains the manual skill/MCP package workflow. It prepares an editor compatibility manifest at `.cursor-plugin/plugin.json`, selected skills, and one package-local `mcp.json`. Native plugin discovery, agent CLI plugin or skill loading, and genuine native Stop are unqualified. Plan results say @@ -13,7 +42,7 @@ installation_verified without adapter dispatch. That operator assertion does not qualify native discovery, genuine Stop, or automatic activation. The single MCP strategy in this contract is **plugin package projection**. -It never writes user or project `mcp.json`, enables a disabled server, or +The user lane never writes user or project `mcp.json`, enables a disabled server, or registers a second copy. CLI user-config MCP lifecycle is the separate conditional UC4 slice. It cannot be inferred from this editor package contract. @@ -47,17 +76,15 @@ under changed ambient HOME/USERPROFILE/cwd. Foreign disabled user MCP and hooks remain byte-identical; the sibling namespace and prepared manifest survive. No client, helper, MCP process, model or hook is executed by these tests. -`ValidateBindingProfile` exposes the existing optional client contract for an -exact recorded artifact locator and, if present, `NativeProfileRoot`. -The current shared facade does not dispatch this interface or persist a Cursor -physical directory identity. Its `ReserveIdentity` can return an existing -binding without validating a supplied profile; update/repair still require -explicit roots. UC-P must bind those operations to recorded profile authority, -expose that authority in a public roots/facts view, and refuse mismatches or -replaced directories before effects. This adapter adds no parallel state store. -The existing registry inspector's clear finding covers absence of an additional -managed executable registration only; the prepared-directory walk still owns -namespace collisions. It is not an exhaustive native registry observation. +`ValidateBindingProfile` checks the selected native registry and recorded +artifact locator. Its user branch retains the exact `plugins/local` preparation +layout. Its project branch requires exact `.cursor` authority, the selected +`.uap/managed/clients/cursor/` artifact and exact native object +paths. Generic lifecycle checks independently validate scope, selected project +namespace and recorded authority; this adapter adds no parallel state store. +The user registry inspector retains its limited clear finding for executable +registration; the project inspector checks only the exact configured component +namespace. Neither is exhaustive native discovery evidence. Source evidence: macOS editor 3.22.7 bundle SHA256 `a9157bf9054d3f27a2a6910c855e028bd5e38896fa74405ca2f766abff590cb8`; diff --git a/install/integrationctl/agentplugins/clients/cursor/identity.go b/install/integrationctl/agentplugins/clients/cursor/identity.go index 31b7093d7..e19624525 100644 --- a/install/integrationctl/agentplugins/clients/cursor/identity.go +++ b/install/integrationctl/agentplugins/clients/cursor/identity.go @@ -11,10 +11,22 @@ var _ clients.RegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return false } -func (*Adapter) InspectNativeRegistry(ctx context.Context, _ clients.Env, _ domain.DetectedClient, _ domain.DeliveryPlan, _ *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + if plan.Scope == domain.ScopeProject { + root, err := cursorPlanRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + selected := *managed + selected.NativeObjects = cursorNativeObjects(managed.NativeObjects) + managed = &selected + } + return cursorComponents(root).Inspect(plan.Components, managed, env.NativeConfig) + } // This manual preparation contract has no managed executable registration. // Clear leaves the existing prepared-directory collision walk in charge; // UsesNativeRegistryExecutable=false means no native discovery was attempted. diff --git a/install/integrationctl/agentplugins/clients/cursor/lifecycle.go b/install/integrationctl/agentplugins/clients/cursor/lifecycle.go index ade065c9e..73f16e0f1 100644 --- a/install/integrationctl/agentplugins/clients/cursor/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/cursor/lifecycle.go @@ -15,13 +15,19 @@ var ( // Activate reports manual preparation. A reload or operator attestation cannot // establish native discovery, CLI capability, or genuine Stop qualification. -func (*Adapter) Activate(ctx context.Context, _ clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { +func (adapter *Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { if err := ctx.Err(); err != nil { return domain.ActivationOutcome{}, err } if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } + if err := adapter.PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + if request.Plan.Scope == domain.ScopeProject { + return adapter.activateProject(ctx, env, request) + } outcome := shared.StartedActivation(request) outcome.Activation = domain.ActivationManual if request.VerifyOnly { @@ -35,9 +41,12 @@ func (*Adapter) Activate(ctx context.Context, _ clients.Env, request domain.Acti } // Deactivate has nothing to unwind: Cursor never received a managed CLI entry. -func (*Adapter) Deactivate(ctx context.Context, _ clients.Env, _ domain.DeactivationRequest) (domain.DeactivationOutcome, error) { +func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { if err := ctx.Err(); err != nil { return domain.DeactivationOutcome{}, err } + if request.TargetContext.Scope == domain.ScopeProject { + return deactivateCursorProject(ctx, env, request) + } return shared.StartedDeactivation(), nil } diff --git a/install/integrationctl/agentplugins/clients/cursor/plan.go b/install/integrationctl/agentplugins/clients/cursor/plan.go index 33350510e..65f9db23f 100644 --- a/install/integrationctl/agentplugins/clients/cursor/plan.go +++ b/install/integrationctl/agentplugins/clients/cursor/plan.go @@ -27,13 +27,19 @@ func (adapter *Adapter) TargetRoot(client domain.DetectedClient, mode domain.Pac return root, filepath.Join(root, "plugins", "local"), nil } -func (*Adapter) RefinePlan(ctx context.Context, _ clients.PlanInput, plan *domain.DeliveryPlan) error { +func (*Adapter) RefinePlan(ctx context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { if err := ctx.Err(); err != nil { return err } if plan.Status == domain.PlanUnsupported { return nil } + if in.TargetContext.Scope == domain.ScopeProject { + shared.PromoteNativeReady(plan, plan.NativeRegistryRoot, shared.OnlyNativeComponents(plan.Components)) + plan.Activation = domain.ActivationPrepared + plan.UserActions = shared.AppendUnique(plan.UserActions, "project skill directories and MCP entries can be configured automatically; discovery, enablement, approval and policy require observation in Cursor") + return nil + } plan.Status = domain.PlanManualActivationRequired // PackageMode describes the retained manifest/layout contract. Readiness // and selected components carry preparation status independently of it. diff --git a/install/integrationctl/agentplugins/clients/cursor/profile.go b/install/integrationctl/agentplugins/clients/cursor/profile.go index 4713acf44..92f33d227 100644 --- a/install/integrationctl/agentplugins/clients/cursor/profile.go +++ b/install/integrationctl/agentplugins/clients/cursor/profile.go @@ -63,6 +63,9 @@ func detectedProfileRoot(host clients.Host) (string, error) { // only an exact artifact path can establish its filesystem profile authority. // Directory replacement identity still requires the shared UC-P seam. func (adapter *Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope == string(domain.ScopeProject) { + return validateProjectBinding(root, binding) + } selected, err := adapter.ResolveProfileRoot(root) if err != nil { return err @@ -79,3 +82,24 @@ func (adapter *Adapter) ValidateBindingProfile(root string, binding domain.Clien } return nil } + +func validateProjectBinding(root string, binding domain.ClientBinding) error { + if err := profileSpelling(root); err != nil { + return err + } + if filepath.Base(root) != ".cursor" || binding.NativeProfileRoot != root { + return fmt.Errorf("recorded Cursor project registry differs from selected root") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + project := filepath.Dir(root) + expected := filepath.Join(project, ".uap", "managed", "clients", "cursor", binding.PhysicalArtifact) + if binding.TargetLocator != expected { + return fmt.Errorf("recorded Cursor artifact differs from selected project") + } + if err := pathpolicy.RequireContainedChild(project, expected); err != nil { + return err + } + return cursorComponents(root).ValidateObjects(cursorNativeObjects(binding.NativeObjects)) +} diff --git a/install/integrationctl/agentplugins/clients/cursor/project.go b/install/integrationctl/agentplugins/clients/cursor/project.go index 7e82d6c63..290c7f252 100644 --- a/install/integrationctl/agentplugins/clients/cursor/project.go +++ b/install/integrationctl/agentplugins/clients/cursor/project.go @@ -16,7 +16,7 @@ var _ clients.ActiveNativeProjector = (*Adapter)(nil) // ProjectActiveNative validates the verified package's requested projection. // Cursor's package layout owns no profile entries. The lifecycle retains any // separately acknowledged Stop receipt; a planned receipt is not ownership. -func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { if err := ctx.Err(); err != nil { return nil, err } @@ -26,6 +26,12 @@ func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope d if err := plan.SelectedDelivery.ValidatePlan(plan, envelope.TreeDigest); err != nil { return nil, err } + if plan.Scope == domain.ScopeProject { + if err := validateCursorProjectStdio(root, envelope, plan, dataPath); err != nil { + return nil, err + } + return cursorProjectObjects(root, envelope, plan) + } return nil, nil } @@ -37,6 +43,12 @@ func (*Adapter) Project(ctx context.Context, in clients.ProjectionInput) ([]doma if err := projectCursor(in.StagingPath, in.Envelope, in.Plan, in.PluginDataPath); err != nil { return nil, err } + if in.Plan.Scope == domain.ScopeProject { + if err := projectCursorStdio(in); err != nil { + return nil, err + } + return cursorProjectObjects(in.StagingPath, in.Envelope, in.Plan) + } return nil, nil } diff --git a/install/integrationctl/agentplugins/clients/cursor/project_lifecycle.go b/install/integrationctl/agentplugins/clients/cursor/project_lifecycle.go new file mode 100644 index 000000000..4a5d514e6 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cursor/project_lifecycle.go @@ -0,0 +1,89 @@ +package cursor + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.AutomaticActivator = (*Adapter)(nil) + _ clients.ReadOnlyVerifier = (*Adapter)(nil) + _ clients.ActivationPreflighter = (*Adapter)(nil) +) + +func (*Adapter) AutomaticallyActivates(_ clients.Env, request domain.ActivationRequest) bool { + return request.Plan.Scope == domain.ScopeProject && request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components) +} + +func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, _ string) bool { + return plan.Scope == domain.ScopeProject && len(plan.Components) > 0 +} + +func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject && request.Plan.TargetContext.Scope != domain.ScopeProject { + return nil + } + if _, err := cursorPlanRoot(request.Client, request.Plan); err != nil { + return err + } + if request.ActivationComplete { + return fmt.Errorf("cursor project native discovery cannot be established by activation attestation; inspect skills and MCP in the selected project") + } + return env.NativeConfig.RequireFileIO() +} + +func (adapter *Adapter) activateProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + root, err := cursorPlanRoot(request.Client, request.Plan) + if err != nil { + return domain.ActivationOutcome{}, err + } + if err := adapter.PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + layout := cursorComponents(root) + outcome, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ + Automatic: adapter.AutomaticallyActivates(env, request), + UnavailableAction: "select only supported Cursor project skills and MCP servers", + RepairAction: "repair managed Cursor project skills and MCP entries", + RetryAction: "retry managed Cursor project component installation", + CompletedAction: "open the selected project in Cursor and inspect skills and MCP servers", + Verify: func() error { + return layout.Verify(cursorNativeObjects(request.Delivery.NativeObjects), false, env.NativeConfig) + }, + Activate: func(ctx context.Context, request domain.ActivationRequest) error { + servers, err := cursorDesiredServers(request.Delivery.ActivePath, cursorNativeObjects(request.Delivery.NativeObjects)) + if err != nil { + return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} + } + return (shared.ProjectComponentMutation{Layout: layout, ActivePath: request.Delivery.ActivePath, + Previous: cursorNativeObjects(request.PreviousNativeObjects), Desired: cursorNativeObjects(request.Delivery.NativeObjects), Servers: servers, Kernel: env.NativeConfig}).Apply(ctx) + }, + }) + if err == nil && outcome.Verification == domain.VerificationInstalled { + outcome.Activation, outcome.ActivationAttested = domain.ActivationPrepared, false + outcome.UserActions = shared.AppendUnique(outcome.UserActions, "Cursor project configuration is installed; native discovery, aliases, enablement, approvals, team policy and MCP initialization remain unobserved") + } + return outcome, err +} + +func deactivateCursorProject(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + root, err := cursorNativeRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return outcome, err + } + if !request.Confirmed { + outcome.UserActions = append(outcome.UserActions, "agentplugins will remove its exact owned Cursor project skill directories and MCP members") + return outcome, nil + } + err = (shared.ProjectComponentMutation{Layout: cursorComponents(root), Previous: cursorNativeObjects(request.NativeObjects), Kernel: env.NativeConfig}).Apply(ctx) + if err != nil && !shared.CommittedNativeDeactivationCleanup(&outcome, err) { + return outcome, err + } + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/cursor/project_native.go b/install/integrationctl/agentplugins/clients/cursor/project_native.go new file mode 100644 index 000000000..ac18798ec --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cursor/project_native.go @@ -0,0 +1,143 @@ +package cursor + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const ( + CursorProjectSkillKind = "cursor_project_skill_directory" + CursorProjectMCPKind = "cursor_project_mcp_server" +) + +func cursorComponents(root string) shared.ProjectComponents { + return shared.ProjectComponents{ProjectRoot: filepath.Dir(root), SkillsRoot: filepath.Join(root, "skills"), + ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, "mcp.json")}, Codec: nativeconfig.CodecMCPServers, + SkillKind: CursorProjectSkillKind, MCPKind: CursorProjectMCPKind} +} + +func cursorProjectObjects(stagingRoot string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + target, err := domain.NewTargetContext(plan.TargetContext.Scope, plan.TargetContext.ProjectRoot, plan.TargetContext.UserConfigRoot) + if err != nil { + return nil, err + } + if plan.Scope != domain.ScopeProject || target.Scope != domain.ScopeProject || plan.NativeRegistryRoot != filepath.Join(target.ProjectRoot, ".cursor") { + return nil, fmt.Errorf("cursor projection differs from selected project") + } + if err := profileSpelling(stagingRoot); err != nil { + return nil, err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, stagingRoot); err != nil { + return nil, err + } + layout := cursorComponents(plan.NativeRegistryRoot) + objects := []domain.NativeObjectOwnership{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + object, err := cursorProjectObject(layout, stagingRoot, envelope, component) + if err != nil { + return nil, err + } + if object != nil { + objects = append(objects, *object) + } + } + return objects, layout.ValidateObjects(objects) +} + +func cursorProjectObject(layout shared.ProjectComponents, stagingRoot string, envelope domain.PackageEnvelope, component domain.ComponentDecision) (*domain.NativeObjectOwnership, error) { + if err := pathpolicy.ValidateLeafID(component.Name); err != nil { + return nil, err + } + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return nil, fmt.Errorf("planned Cursor skill %q is missing", component.Name) + } + relative := filepath.Dir(filepath.FromSlash(skill.RelativePath)) + if skill.RelativePath == "" { + relative = filepath.Join("skills", component.Name) + } + object, err := layout.SkillObject(component.Name, filepath.ToSlash(relative), stagingRoot) + return &object, err + case domain.ComponentMCPServer: + server, err := cursorProjectedServer(stagingRoot, component.Name) + if err != nil { + return nil, err + } + object, err := layout.MCPObject(component.Name, server, layout.ConfigPaths.JSON) + return &object, err + default: + return nil, fmt.Errorf("unsupported Cursor project component kind %q", component.Kind) + } +} + +func cursorProjectedServer(activeRoot, name string) (nativeconfig.Server, error) { + file, err := nativeconfig.New().ReadExactFile(filepath.Join(activeRoot, "mcp.json")) + if err != nil { + return nativeconfig.Server{}, err + } + document, err := shared.DecodeStrictJSONObject(file.Body) + if err != nil { + return nativeconfig.Server{}, err + } + entries, ok := document["mcpServers"].(map[string]any) + if !ok { + return nativeconfig.Server{}, fmt.Errorf("cursor package has no mcpServers object") + } + entry, ok := entries[name].(map[string]any) + if !ok { + return nativeconfig.Server{}, fmt.Errorf("cursor projected MCP server %q is missing", name) + } + body, err := json.Marshal(entry) + if err != nil { + return nativeconfig.Server{}, err + } + server := nativeconfig.Server{} + if err := json.Unmarshal(body, &server); err != nil { + return server, err + } + server.StdioValuesResolved, server.CWDResolved = true, true + if server.Command != "" { + server.Type = "stdio" + } else { + server.Type = "remote" + } + return server, nil +} + +func cursorDesiredServers(activeRoot string, objects []domain.NativeObjectOwnership) (map[string]nativeconfig.Server, error) { + servers := map[string]nativeconfig.Server{} + for _, object := range objects { + if object.Kind != CursorProjectMCPKind { + continue + } + server, err := cursorProjectedServer(activeRoot, object.LogicalName) + if err != nil { + return nil, err + } + servers[object.LogicalName] = server + } + return servers, nil +} + +// Package directory ownership belongs to the shared artifact lifecycle, not the +// native component writer. Other unknown kinds remain visible and fail closed. +func cursorNativeObjects(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + result := []domain.NativeObjectOwnership{} + for _, object := range objects { + if object.Kind != "managed_package_directory" { + result = append(result, object) + } + } + return result +} diff --git a/install/integrationctl/agentplugins/clients/cursor/project_stdio.go b/install/integrationctl/agentplugins/clients/cursor/project_stdio.go new file mode 100644 index 000000000..dbaae5e92 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cursor/project_stdio.go @@ -0,0 +1,83 @@ +package cursor + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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" +) + +// The project lane uses the existing trusted launcher to establish cwd rather +// than assuming Cursor interprets the portable cwd field or plugin placeholders. +func projectCursorStdio(in clients.ProjectionInput) error { + if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { + return err + } + if len(shared.SupportedMCPNames(in.Plan)) == 0 { + return nil + } + file, err := nativeconfig.New().ReadExactFile(filepath.Join(in.StagingPath, "mcp.json")) + if err != nil { + return err + } + document, err := shared.DecodeStrictJSONObject(file.Body) + if err != nil { + return err + } + servers, ok := document["mcpServers"].(map[string]any) + if !ok { + return fmt.Errorf("cursor package has no MCP collection") + } + for _, name := range shared.SupportedMCPNames(in.Plan) { + server := in.Envelope.MCP.Servers[name] + if server.Type != "stdio" { + continue + } + entry, ok := servers[name].(map[string]any) + if !ok { + return fmt.Errorf("cursor projected stdio entry is missing") + } + if err := bindCursorStdio(entry, server, in.Plan.ActivePath, in.PluginDataPath); err != nil { + return err + } + } + return shared.WriteJSON(filepath.Join(in.StagingPath, "mcp.json"), document) +} + +func bindCursorStdio(entry map[string]any, server domain.MCPServer, activePath, dataPath string) error { + if !filepath.IsAbs(activePath) || !filepath.IsAbs(dataPath) { + return fmt.Errorf("cursor managed stdio roots must be absolute") + } + rawCWD, _ := server.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, activePath, dataPath) + if err != nil { + return err + } + absoluteCWD, ok := entry["cwd"].(string) + if !ok { + return fmt.Errorf("cursor projected stdio cwd is missing") + } + rawArgs, _ := entry["args"].([]any) + args := make([]string, 0, len(rawArgs)) + for _, value := range rawArgs { + text, ok := value.(string) + if !ok { + return fmt.Errorf("cursor stdio arguments must be strings") + } + args = append(args, text) + } + command, _ := server.Decoded["command"].(string) + if strings.HasPrefix(command, "${package.root}/") { + 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) + delete(entry, "cwd") + return nil +} diff --git a/install/integrationctl/agentplugins/clients/cursor/project_stdio_verify.go b/install/integrationctl/agentplugins/clients/cursor/project_stdio_verify.go new file mode 100644 index 000000000..d5f81ec14 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cursor/project_stdio_verify.go @@ -0,0 +1,45 @@ +package cursor + +import ( + "fmt" + "path/filepath" + "slices" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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" +) + +// Active package bytes are immutable. A different selected data receipt needs +// a fresh projection; old launcher/environment locators cannot authorize it. +func validateCursorProjectStdio(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) error { + for _, name := range shared.SupportedMCPNames(plan) { + source := envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + server, err := cursorProjectedServer(root, name) + if err != nil { + return err + } + launcher := filepath.Join(plan.ActivePath, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + if !filepath.IsAbs(dataPath) || server.Type != "stdio" || server.Command != launcher || server.CWD != "" || server.Env["PLUGIN_ROOT"] != plan.ActivePath || server.Env["PLUGIN_DATA"] != dataPath { + return fmt.Errorf("%w: Cursor stdio package/data binding differs from selected locators", domain.ErrProjectionRefreshRequired) + } + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, plan.ActivePath, dataPath) + if err != nil { + return err + } + _, resolvedCWD, err := shared.ResolveStdioPaths("", rawCWD, plan.ActivePath, dataPath, root) + 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) { + return fmt.Errorf("%w: Cursor launcher package/data/cwd arguments differ from selected locators", domain.ErrProjectionRefreshRequired) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/cursor/scope.go b/install/integrationctl/agentplugins/clients/cursor/scope.go new file mode 100644 index 000000000..d3fb198a9 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/cursor/scope.go @@ -0,0 +1,74 @@ +package cursor + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) +) + +// NativeRegistry keeps the detected user profile distinct from workspace files. +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".cursor"), "" + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if target.Scope != domain.ScopeProject { + return "", "", fmt.Errorf("cursor project layout requires project scope") + } + if _, err := cursorNativeRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", err + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope != domain.ScopeProject { + return nil + } + _, err := cursorNativeRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot) + return err +} + +func cursorPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return "", fmt.Errorf("cursor component lifecycle requires matching project scope") + } + return cursorNativeRoot(client, plan.TargetContext, plan.NativeRegistryRoot) +} + +func cursorNativeRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.Scope != domain.ScopeProject { + return "", fmt.Errorf("cursor native components require project scope") + } + if validated.UserConfigRoot != "" && validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("cursor detected user profile differs from selected target") + } + root := filepath.Join(validated.ProjectRoot, ".cursor") + if planned != "" && planned != root { + return "", fmt.Errorf("cursor native registry differs from selected project") + } + if err := pathpolicy.RequireContainedChild(validated.ProjectRoot, root); err != nil { + return "", err + } + return root, nil +} diff --git a/install/integrationctl/agentplugins/clients/detection.go b/install/integrationctl/agentplugins/clients/detection.go index de09f3aab..33ba60663 100644 --- a/install/integrationctl/agentplugins/clients/detection.go +++ b/install/integrationctl/agentplugins/clients/detection.go @@ -1,6 +1,7 @@ package clients import ( + "context" "io/fs" "os" @@ -84,3 +85,9 @@ type ProfileBindingValidator interface { // PhysicalProfileAuthority is the single optional port, shared with the planner. type PhysicalProfileAuthority = ports.PhysicalProfileAuthority + +// ProjectPhysicalAuthority captures the existing authority token for the real +// project registry, without replacing the detected user's ConfigRoot. +type ProjectPhysicalAuthority interface { + CaptureProjectAuthority(context.Context, domain.DetectedClient, domain.TargetContext, string) (domain.ProfileAuthority, error) +} diff --git a/install/integrationctl/agentplugins/clients/gemini/activate.go b/install/integrationctl/agentplugins/clients/gemini/activate.go index 744aa7e81..938649656 100644 --- a/install/integrationctl/agentplugins/clients/gemini/activate.go +++ b/install/integrationctl/agentplugins/clients/gemini/activate.go @@ -22,33 +22,41 @@ func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryP // Activate installs or verifies managed Gemini CLI skills and MCP servers // through the native config kernel. -func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { +func (adapter *Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } - if err := validateProfile(request.Client.ConfigRoot, request.Plan.NativeRegistryRoot); err != nil { + root, err := geminiPlanRoot(request.Client, request.Plan) + if err != nil { + return domain.ActivationOutcome{}, err + } + if err := adapter.PreflightActivation(env, request); err != nil { return domain.ActivationOutcome{}, err } automatic := request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.HasNativeConfigRoot(request) - return shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ + outcome, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ Automatic: automatic, UnavailableAction: "install Gemini CLI and rerun add with an isolated writable Gemini config root", RepairAction: "repair the managed Gemini CLI skills and MCP configuration", RetryAction: "retry the managed Gemini CLI native installation", CompletedAction: "in a running Gemini CLI session use `/mcp reload` and `/skills reload`, or restart Gemini CLI", Verify: func() error { - return VerifyGeminiNativeObjects(request.Client.ConfigRoot, request.Delivery.NativeObjects, false, env.NativeConfig) + return VerifyGeminiNativeObjects(root, request.Delivery.NativeObjects, false, env.NativeConfig) }, Activate: func(ctx context.Context, request domain.ActivationRequest) error { return ActivateGeminiNativeWithKernel(ctx, request, env.NativeConfig) }, }) + if err != nil { + return outcome, err + } + return geminiProjectStatus(env, request, outcome) } // Deactivate removes managed Gemini native objects. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { outcome := shared.StartedDeactivation() - if err := validateProfile(request.Client.ConfigRoot, ""); err != nil { + if _, err := geminiNativeRoot(request.Client, request.TargetContext, request.NativeRegistryRoot); err != nil { return outcome, err } if !request.Confirmed { diff --git a/install/integrationctl/agentplugins/clients/gemini/identity.go b/install/integrationctl/agentplugins/clients/gemini/identity.go index 3c565f53e..1f7e54c8d 100644 --- a/install/integrationctl/agentplugins/clients/gemini/identity.go +++ b/install/integrationctl/agentplugins/clients/gemini/identity.go @@ -15,16 +15,24 @@ func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, clie if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + root, err := geminiInspectionRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + plan.NativeRegistryRoot = root + return InspectGeminiRegistry(plan, managed, env.NativeConfig) +} + +func geminiInspectionRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope == domain.ScopeProject { + return geminiPlanRoot(client, plan) + } root := client.ConfigRoot if root == "" { root = plan.NativeRegistryRoot } if root == "" { - return clients.RegistryIndeterminate, nil - } - if err := validateProfile(root, plan.NativeRegistryRoot); err != nil { - return clients.RegistryIndeterminate, err + return "", nil } - plan.NativeRegistryRoot = root - return InspectGeminiRegistry(plan, managed, env.NativeConfig) + return root, validateProfile(root, plan.NativeRegistryRoot) } diff --git a/install/integrationctl/agentplugins/clients/gemini/native.go b/install/integrationctl/agentplugins/clients/gemini/native.go index a0f7b0ce4..d35daeaf8 100644 --- a/install/integrationctl/agentplugins/clients/gemini/native.go +++ b/install/integrationctl/agentplugins/clients/gemini/native.go @@ -28,14 +28,22 @@ func ActivateGeminiNativeWithKernel(ctx context.Context, request domain.Activati if err := ctx.Err(); err != nil { return err } - return applyGeminiNativeMutationWithKernel(request.Client.ConfigRoot, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, kernel) + root, err := geminiPlanRoot(request.Client, request.Plan) + if err != nil { + return err + } + return applyGeminiNativeMutationWithKernel(root, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, kernel) } func DeactivateGeminiNativeWithKernel(ctx context.Context, request domain.DeactivationRequest, kernel nativeconfig.Kernel) error { if err := ctx.Err(); err != nil { return err } - return applyGeminiNativeMutationWithKernel(request.Client.ConfigRoot, "", request.NativeObjects, nil, kernel) + root, err := geminiNativeRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return err + } + return applyGeminiNativeMutationWithKernel(root, "", request.NativeObjects, nil, kernel) } func VerifyGeminiNativeObjects(configRoot string, objects []domain.NativeObjectOwnership, allowMissing bool, kernel nativeconfig.Kernel) error { diff --git a/install/integrationctl/agentplugins/clients/gemini/plan.go b/install/integrationctl/agentplugins/clients/gemini/plan.go index dce45f036..3c29068ed 100644 --- a/install/integrationctl/agentplugins/clients/gemini/plan.go +++ b/install/integrationctl/agentplugins/clients/gemini/plan.go @@ -14,11 +14,21 @@ var ( ) func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { - return validateProfile(in.Client.ConfigRoot, plan.NativeRegistryRoot) + _, err := geminiNativeRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot) + return err } func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { - shared.PromoteNativeReady(plan, in.Client.ConfigRoot, shared.OnlyNativeComponents(plan.Components)) + root := in.Client.ConfigRoot + if in.TargetContext.Scope == domain.ScopeProject { + root = plan.NativeRegistryRoot + } + shared.PromoteNativeReady(plan, root, shared.OnlyNativeComponents(plan.Components)) + if in.TargetContext.Scope == domain.ScopeProject { + plan.Activation = domain.ActivationPrepared + plan.UserActions = shared.AppendUnique(plan.UserActions, "project skills and MCP settings can be configured automatically; native discovery, workspace trust and system policy require observation in Gemini CLI") + return nil + } plan.UserActions = shared.AppendUnique(plan.UserActions, "agentplugins will install and verify Gemini CLI skills and MCP servers automatically; reload them in a running session or restart Gemini CLI") return nil } diff --git a/install/integrationctl/agentplugins/clients/gemini/project_status.go b/install/integrationctl/agentplugins/clients/gemini/project_status.go new file mode 100644 index 000000000..84caaf56f --- /dev/null +++ b/install/integrationctl/agentplugins/clients/gemini/project_status.go @@ -0,0 +1,161 @@ +package gemini + +import ( + "encoding/json" + "fmt" + "path/filepath" + "slices" + + "github.com/tailscale/hujson" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +// PreflightActivation runs before the provider's generic attestation shortcut. +// Native configuration ownership cannot prove discovery in a Gemini session. +func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject { + return nil + } + if _, err := geminiPlanRoot(request.Client, request.Plan); err != nil { + return err + } + if _, err := geminiProjectConditions(env.NativeConfig, request); err != nil { + return err + } + if request.ActivationComplete { + return fmt.Errorf("gemini project native discovery cannot be established by activation attestation; inspect skills and MCP servers in a fresh trusted session") + } + return nil +} + +func geminiProjectStatus(env clients.Env, request domain.ActivationRequest, outcome domain.ActivationOutcome) (domain.ActivationOutcome, error) { + if request.Plan.Scope != domain.ScopeProject || outcome.Verification != domain.VerificationInstalled { + return outcome, nil + } + outcome.Activation = domain.ActivationPrepared + outcome.ActivationAttested = false + outcome.UserActions = shared.AppendUnique(outcome.UserActions, "Gemini project configuration is installed; native discovery, workspace trust, system policy and MCP initialization remain unobserved; start Gemini CLI in the selected project and inspect skills list and mcp list") + conditions, err := geminiProjectConditions(env.NativeConfig, request) + if err != nil { + return outcome, fmt.Errorf("gemini project configuration was installed but effective settings could not be inspected: %w", err) + } + for _, condition := range conditions { + outcome.UserActions = shared.AppendUnique(outcome.UserActions, condition) + } + return outcome, nil +} + +type geminiProjectSettings struct { + Skills struct { + Enabled *bool `json:"enabled"` + Disabled []string `json:"disabled"` + } `json:"skills"` + MCP struct { + Allowed *[]string `json:"allowed"` + Excluded *[]string `json:"excluded"` + } `json:"mcp"` + Admin struct { + Skills struct { + Enabled *bool `json:"enabled"` + } `json:"skills"` + MCP struct { + Enabled *bool `json:"enabled"` + } `json:"mcp"` + } `json:"admin"` +} + +func readGeminiProjectSettings(kernel nativeconfig.Kernel, root string) (geminiProjectSettings, error) { + settings := geminiProjectSettings{} + // Inspect applies the same comment-only parser and uniqueness rules as the + // mutation path without creating a file or claiming ownership of this name. + if _, _, err := kernel.Inspect(GeminiConfigPaths(root), nativeconfig.CodecGemini, "agentplugins-policy-inspection", nil); err != nil { + return settings, err + } + file, err := kernel.ReadExactFile(filepath.Join(root, "settings.json")) + if err != nil || !file.Exists { + return settings, err + } + if len(file.Body) > 1<<20 { + return settings, fmt.Errorf("gemini settings exceed the inspection limit") + } + standard, err := hujson.Standardize(file.Body) + if err != nil { + return settings, err + } + if _, err := shared.DecodeStrictJSONObject(standard); err != nil { + return settings, err + } + if err := json.Unmarshal(standard, &settings); err != nil { + return settings, fmt.Errorf("invalid Gemini component settings: %w", err) + } + return settings, nil +} + +// These are configured user/workspace restrictions, not a second Gemini +// settings engine. Higher system policy and session trust remain unobserved. +func geminiProjectConditions(kernel nativeconfig.Kernel, request domain.ActivationRequest) ([]string, error) { + user, err := readGeminiProjectSettings(kernel, request.Client.ConfigRoot) + if err != nil { + return nil, err + } + workspace, err := readGeminiProjectSettings(kernel, request.Plan.NativeRegistryRoot) + if err != nil { + return nil, err + } + conditions := geminiConfiguredRestrictions(user, workspace, request.Plan.Components) + for _, component := range request.Plan.Components { + if component.Kind != domain.ComponentSkill { + continue + } + if err := pathpolicy.ValidateLeafID(component.Name); err != nil { + return nil, err + } + alias := filepath.Join(request.Plan.TargetContext.ProjectRoot, ".agents", "skills", component.Name, "SKILL.md") + file, err := kernel.ReadExactFile(alias) + if err != nil { + return nil, fmt.Errorf("inspect Gemini workspace skill alias: %w", err) + } + if file.Exists { + conditions = append(conditions, fmt.Sprintf("workspace .agents/skills/%s/SKILL.md may shadow the managed .gemini skill; native discovery remains pending", component.Name)) + } + } + return conditions, nil +} + +func geminiConfiguredRestrictions(user, workspace geminiProjectSettings, components []domain.ComponentDecision) []string { + conditions := []string{} + skills := geminiConfiguredEnabled(user.Skills.Enabled, workspace.Skills.Enabled) + adminSkills := geminiConfiguredEnabled(user.Admin.Skills.Enabled, workspace.Admin.Skills.Enabled) + adminMCP := geminiConfiguredEnabled(user.Admin.MCP.Enabled, workspace.Admin.MCP.Enabled) + allowed, excluded := user.MCP.Allowed, user.MCP.Excluded + if workspace.MCP.Allowed != nil { + allowed = workspace.MCP.Allowed + } + if workspace.MCP.Excluded != nil { + excluded = workspace.MCP.Excluded + } + disabled := append(slices.Clone(user.Skills.Disabled), workspace.Skills.Disabled...) + for _, component := range components { + if component.Kind == domain.ComponentSkill && (!skills || !adminSkills || slices.Contains(disabled, component.Name)) { + conditions = append(conditions, fmt.Sprintf("preserved Gemini user/workspace settings disable skill %q; it remains configured pending native discovery", component.Name)) + } + if component.Kind == domain.ComponentMCPServer && (!adminMCP || allowed != nil && !slices.Contains(*allowed, component.Name) || excluded != nil && slices.Contains(*excluded, component.Name)) { + conditions = append(conditions, fmt.Sprintf("preserved Gemini user/workspace settings restrict MCP server %q; it remains configured pending native discovery", component.Name)) + } + } + return conditions +} + +func geminiConfiguredEnabled(user, workspace *bool) bool { + if workspace != nil { + return *workspace + } + return user == nil || *user +} diff --git a/install/integrationctl/agentplugins/clients/gemini/scope.go b/install/integrationctl/agentplugins/clients/gemini/scope.go new file mode 100644 index 000000000..d3d1928d0 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/gemini/scope.go @@ -0,0 +1,63 @@ +package gemini + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) +) + +// NativeRegistry selects actual workspace configuration without replacing the +// detected user profile or changing Gemini's native home environment. +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".gemini"), in.Client.ExecutablePath + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if target.Scope != domain.ScopeProject { + return "", "", fmt.Errorf("gemini project layout requires project scope") + } + if _, err := geminiNativeRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", fmt.Errorf("gemini prepared package must stay in the selected project: %w", err) + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func geminiPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if (plan.Scope == domain.ScopeProject) != (plan.TargetContext.Scope == domain.ScopeProject) { + return "", fmt.Errorf("gemini plan scope differs from its target context") + } + return geminiNativeRoot(client, plan.TargetContext, plan.NativeRegistryRoot) +} + +func geminiNativeRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.UserConfigRoot != "" && validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("gemini detected user profile differs from the selected target context") + } + root := client.ConfigRoot + if validated.Scope == domain.ScopeProject { + root = filepath.Join(validated.ProjectRoot, ".gemini") + } + if err := validateProfile(root, planned); err != nil { + return "", err + } + return root, nil +} diff --git a/install/integrationctl/agentplugins/clients/grok/detect.go b/install/integrationctl/agentplugins/clients/grok/detect.go index fd3e2235e..2f3a3cbdc 100644 --- a/install/integrationctl/agentplugins/clients/grok/detect.go +++ b/install/integrationctl/agentplugins/clients/grok/detect.go @@ -25,11 +25,15 @@ func (*Adapter) DetectSurfaces(host clients.Host) clients.Detection { if configured := strings.TrimSpace(host.Env("GROK_HOME")); configured != "" { root = configured } - return clients.Detection{ + detection := clients.Detection{ ConfigRoot: root, ExecutablePath: host.LookPath("grok"), Surfaces: []domain.ClientSurface{ host.BinarySurface("grok_cli", "grok"), host.DirectorySurface("grok_config", root), }, } + if strings.TrimSpace(host.Env("GROK_HOME")) != "" { + detection.Surfaces = append(detection.Surfaces, domain.ClientSurface{ID: "grok_custom_home", Detected: true}) + } + return detection } diff --git a/install/integrationctl/agentplugins/clients/grok/lifecycle.go b/install/integrationctl/agentplugins/clients/grok/lifecycle.go index 2d71e5b26..bdcfef881 100644 --- a/install/integrationctl/agentplugins/clients/grok/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/grok/lifecycle.go @@ -17,14 +17,26 @@ var ( ) func (*Adapter) AutomaticallyActivates(env clients.Env, request domain.ActivationRequest) bool { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject || request.Plan.TargetContext.ProjectRoot != "" { + return false + } return shared.HasClientCLI(env, request.BackendExecutable) } -func (*Adapter) VerifierAvailable(_ domain.DetectedClient, _ domain.DeliveryPlan, backendExecutable string) bool { +func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, backendExecutable string) bool { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" { + return false + } return shared.HasListingCLI(backendExecutable) } func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject || request.Plan.TargetContext.ProjectRoot != "" { + return activateProject(ctx, env, request) + } + return activateUser(ctx, env, request) +} +func activateUser(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } @@ -112,6 +124,9 @@ func verifyAbsent(ctx context.Context, env clients.Env, executable, name string) } func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject || request.TargetContext.ProjectRoot != "" { + return deactivateProject(ctx, env, request) + } outcome := shared.StartedDeactivation() if request.ExternalUninstalled { outcome.ExternalRemovalComplete = true diff --git a/install/integrationctl/agentplugins/clients/grok/plan.go b/install/integrationctl/agentplugins/clients/grok/plan.go index 663be4f04..977e2776d 100644 --- a/install/integrationctl/agentplugins/clients/grok/plan.go +++ b/install/integrationctl/agentplugins/clients/grok/plan.go @@ -27,6 +27,18 @@ func (*Adapter) TargetRoot(client domain.DetectedClient, mode domain.PackageMode } func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if plan.Scope == domain.ScopeProject || in.TargetContext.Scope == domain.ScopeProject || in.TargetContext.ProjectRoot != "" { + if _, err := grokPlanRoot(in.Client, *plan); err != nil { + return err + } + if plan.Status != domain.PlanUnsupported && shared.OnlyNativeComponents(plan.Components) { + plan.Status = domain.PlanReady + plan.Activation = domain.ActivationPrepared + plan.Verification = domain.VerificationPackageValid + plan.UserActions = shared.AppendUnique(plan.UserActions, projectPreparationAction) + } + return nil + } shared.PromoteNativeReady(plan, in.Client.ConfigRoot, shared.OnlyNativeComponents(plan.Components)) if plan.Status == domain.PlanUnsupported { return nil diff --git a/install/integrationctl/agentplugins/clients/grok/project.go b/install/integrationctl/agentplugins/clients/grok/project.go index 68512e8c9..2a9d86d3d 100644 --- a/install/integrationctl/agentplugins/clients/grok/project.go +++ b/install/integrationctl/agentplugins/clients/grok/project.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "path/filepath" + "strings" + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" @@ -13,6 +15,22 @@ import ( var _ clients.Projector = (*Adapter)(nil) func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if in.Plan.Scope == domain.ScopeProject || in.Plan.TargetContext.Scope == domain.ScopeProject || in.Plan.TargetContext.ProjectRoot != "" { + if _, err := grokPlanRoot(domain.DetectedClient{ClientID: domain.ClientGrok, ConfigRoot: in.Plan.TargetContext.UserConfigRoot}, in.Plan); err != nil { + return nil, err + } + if !filepath.IsAbs(in.StagingPath) || filepath.Clean(in.StagingPath) != in.StagingPath || filepath.Dir(in.StagingPath) != in.Plan.TargetAnchor || !strings.HasPrefix(filepath.Base(in.StagingPath), ".agentplugins-staging-") { + return nil, fmt.Errorf("grok project staging differs from exact selected anchor") + } + if err := pathpolicy.RequireContainedChild(in.Plan.TargetAnchor, in.StagingPath); err != nil { + return nil, err + } + for _, component := range in.Plan.Components { + if component.Support != domain.SupportUnsupported && component.Kind != domain.ComponentSkill { + return nil, fmt.Errorf("grok project component is unqualified") + } + } + } manifest := shared.ManifestFromEnvelope(in.Envelope, shared.WithAuthorNameEmail()) if shared.ComponentKindPresent(in.Plan.Components, domain.ComponentSkill) { manifest["skills"] = "./skills/" diff --git a/install/integrationctl/agentplugins/clients/grok/project_collision.go b/install/integrationctl/agentplugins/clients/grok/project_collision.go new file mode 100644 index 000000000..4c709f93e --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_collision.go @@ -0,0 +1,185 @@ +package grok + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const projectManifestLimit = 64 << 10 +const projectDiscoveryLimit = 1 << 20 +const projectDirectoryLimit = 128 +const projectAncestorLimit = 64 + +type projectFileScanner struct { + remaining int + plan domain.DeliveryPlan + owned bool + expected bool +} + +func inspectProjectFiles(plan domain.DeliveryPlan, owned bool) (clients.RegistryFinding, error) { + scanner := projectFileScanner{remaining: projectDiscoveryLimit, plan: plan, owned: owned} + roots := []string{plan.TargetContext.UserConfigRoot, filepath.Join(filepath.Dir(plan.TargetContext.UserConfigRoot), ".claude")} + current := plan.TargetContext.ProjectRoot + for count := 0; ; count++ { + if count >= projectAncestorLimit { + return clients.RegistryIndeterminate, fmt.Errorf("grok project ancestor discovery exceeds finite bound") + } + roots = append(roots, filepath.Join(current, ".grok"), filepath.Join(current, ".claude")) + parent := filepath.Dir(current) + if parent == current { + break + } + current = parent + } + seen := map[string]bool{} + for _, root := range roots { + if seen[root] { + continue + } + seen[root] = true + collision, err := scanner.scanRoot(root) + if err != nil { + return clients.RegistryIndeterminate, err + } + if collision { + return clients.RegistryCollision, nil + } + } + if scanner.expected { + return clients.RegistryExpected, nil + } + return clients.RegistryClear, nil +} +func (scanner *projectFileScanner) scanRoot(root string) (bool, error) { + skills, err := projectDirectoryEntries(filepath.Join(root, "skills"), nil) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return false, err + } + for _, entry := range skills { + if scanner.selectedSkillName(entry.Name()) { + return true, nil + } + } + entries, err := projectDirectoryEntries(filepath.Join(root, "plugins"), nil) + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + if err != nil { + return false, err + } + for _, entry := range entries { + if !entry.IsDir() || entry.Type()&os.ModeSymlink != 0 { + return false, fmt.Errorf("grok plugin discovery contains an unqualified entry") + } + path := filepath.Join(root, "plugins", entry.Name()) + name, err := scanner.pluginName(path) + if err != nil { + return false, err + } + if name != scanner.plan.DeclaredName { + continue + } + if scanner.owned && path == scanner.plan.ActivePath { + scanner.expected = true + continue + } + return true, nil + } + return false, nil +} +func (scanner *projectFileScanner) pluginName(path string) (string, error) { + selected := "" + for _, layout := range []string{".grok-plugin", ".claude-plugin"} { + manifest := filepath.Join(path, layout, "plugin.json") + if _, err := os.Lstat(manifest); errors.Is(err, os.ErrNotExist) { + continue + } else if err != nil { + return "", err + } + if selected != "" { + return "", fmt.Errorf("grok plugin contains competing native manifests") + } + selected = manifest + } + if selected == "" { + return "", fmt.Errorf("grok plugin discovery cannot exhaust an unknown plugin layout") + } + body, err := scanner.readProjectManifest(selected, nil) + if err != nil { + return "", err + } + object, err := shared.DecodeStrictJSONObject(body) + if err != nil || !exactProjectFields(object, []string{"name"}) { + return "", fmt.Errorf("grok plugin manifest identity is indeterminate") + } + name, ok := object["name"].(string) + if !ok || pathpolicy.ValidateLeafID(name) != nil { + return "", fmt.Errorf("grok plugin manifest name is missing") + } + return name, nil +} +func (scanner *projectFileScanner) readProjectManifest(path string, beforeOpen func()) ([]byte, error) { + file, info, err := openProjectPath(path, false, beforeOpen) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + limit := projectManifestLimit + if scanner.remaining < limit { + limit = scanner.remaining + } + if limit <= 0 || info.Size() > int64(limit) { + return nil, fmt.Errorf("grok plugin manifest exceeds bounded discovery budget") + } + body, err := io.ReadAll(io.LimitReader(file, int64(limit)+1)) + scanner.remaining -= len(body) + if err != nil { + return nil, err + } + if len(body) > limit { + return nil, fmt.Errorf("grok plugin manifest exceeds bounded discovery budget") + } + if err := stableProjectPath(path, file, info); err != nil { + return nil, err + } + return body, nil +} +func projectDirectoryEntries(path string, beforeOpen func()) ([]os.DirEntry, error) { + file, info, err := openProjectPath(path, true, beforeOpen) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + entries, err := file.ReadDir(projectDirectoryLimit + 1) + if err != nil && !errors.Is(err, io.EOF) { + return nil, err + } + if len(entries) > projectDirectoryLimit { + return nil, fmt.Errorf("grok plugin directory exceeds bounded discovery entries") + } + if err := stableProjectPath(path, file, info); err != nil { + return nil, err + } + return entries, nil +} + +func (scanner *projectFileScanner) selectedSkillName(name string) bool { + if name == scanner.plan.DeclaredName { + return true + } + for _, component := range scanner.plan.Components { + if component.Kind == domain.ComponentSkill && component.Support != domain.SupportUnsupported && component.Name == name { + return true + } + } + return false +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_contract_test.go b/install/integrationctl/agentplugins/clients/grok/project_contract_test.go new file mode 100644 index 000000000..c9c230a44 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_contract_test.go @@ -0,0 +1,310 @@ +package grok + +import ( + "context" + "encoding/json" + "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/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +func projectTestRoot(t *testing.T) string { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + return root +} +func projectTestWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} +func projectTestRequest(t *testing.T) domain.ActivationRequest { + t.Helper() + base := projectTestRoot(t) + project, user := filepath.Join(base, "A"), filepath.Join(base, "home", ".grok") + if err := os.MkdirAll(project, 0700); err != nil { + t.Fatal(err) + } + projectTestWrite(t, filepath.Join(user, "keep.txt"), "original user") + target, err := domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + native := filepath.Join(project, ".grok") + active := filepath.Join(native, "plugins", "demo-0123456789ab") + client := domain.DetectedClient{ClientID: domain.ClientGrok, ConfigRoot: user, ExecutablePath: filepath.Join(base, "grok-never-executed")} + return domain.ActivationRequest{Client: client, DeclaredName: "demo", BackendExecutable: client.ExecutablePath, Plan: domain.DeliveryPlan{ClientID: domain.ClientGrok, Scope: domain.ScopeProject, TargetContext: target, Status: domain.PlanReady, DeclaredName: "demo", DeclaredVersion: "1.0.0", NativeRegistryRoot: native, NativeRegistryExecutable: client.ExecutablePath, TargetAnchor: native, TargetRoot: filepath.Dir(active), ActivePath: active, PhysicalArtifactID: filepath.Base(active), Components: []domain.ComponentDecision{{Kind: domain.ComponentSkill, Name: "docs", Support: domain.SupportNative}}}, Delivery: domain.StagedDelivery{ClientID: domain.ClientGrok, ActivePath: active, OwnedBase: filepath.Dir(active)}} +} + +type projectTestRunner struct { + request domain.ActivationRequest + calls int + enabled bool + bad string +} + +func (runner *projectTestRunner) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + runner.calls++ + plan := runner.request.Plan + if !reflect.DeepEqual(command.Argv, []string{runner.request.Client.ExecutablePath, "inspect", "--json"}) || command.Dir != plan.TargetContext.ProjectRoot || command.StdoutLimitBytes != 1<<20 { + return legacyports.CommandResult{}, os.ErrPermission + } + body, _ := json.Marshal(map[string]any{"grokVersion": "1.0.67", "cwd": command.Dir, "projectRoot": command.Dir + string(filepath.Separator), "projectTrusted": false, "skills": []any{}, "plugins": []any{map[string]any{"name": "demo", "scope": "project", "path": plan.ActivePath, "enabled": runner.enabled}}}) + if runner.bad != "" { + body = []byte(runner.bad) + } + return legacyports.CommandResult{Stdout: body}, nil +} +func TestProjectGrokPreparedForeverAndNoGlobalCommand(t *testing.T) { + request := projectTestRequest(t) + projectTestWrite(t, filepath.Join(request.Plan.ActivePath, ".grok-plugin", "plugin.json"), `{"name":"demo"}`) + for _, enabled := range []bool{false, true} { + runner := &projectTestRunner{request: request, enabled: enabled} + env := clients.Env{Runner: runner} + out, err := New().Activate(t.Context(), env, request) + if err != nil || out.Activation != domain.ActivationPrepared || out.Verification != domain.VerificationPackageValid || out.AuthoritativeObservation || runner.calls != 1 { + t.Fatalf("source evidence promoted enabled%t: %+v %v calls%d", enabled, out, err, runner.calls) + } + if New().AutomaticallyActivates(env, request) || New().VerifierAvailable(request.Client, request.Plan, request.BackendExecutable) { + t.Fatal("prepared source claimed activation verifier") + } + } + request.ActivationComplete = true + runner := &projectTestRunner{request: request} + if _, err := New().Activate(t.Context(), clients.Env{Runner: runner}, request); err == nil || runner.calls != 0 { + t.Fatal("attestation reached native process or promoted prepared source") + } + early := request + early.ActivationComplete = false + early.Delivery = domain.StagedDelivery{} + if err := New().PreflightActivation(clients.Env{}, early); err != nil { + t.Fatal("unstaged preflight refused", err) + } + if body, _ := os.ReadFile(filepath.Join(request.Client.ConfigRoot, "keep.txt")); string(body) != "original user" { + t.Fatal("original user changed") + } +} +func TestProjectGrokStrictObservedInspectShape(t *testing.T) { + request := projectTestRequest(t) + runner := &projectTestRunner{request: request} + command := legacyports.Command{Argv: []string{request.Client.ExecutablePath, "inspect", "--json"}, Dir: request.Plan.TargetContext.ProjectRoot, StdoutLimitBytes: 1 << 20} + result, err := runner.Run(t.Context(), command) + if err != nil { + t.Fatal(err) + } + body := string(result.Stdout) + project := request.Plan.TargetContext.ProjectRoot + replaceRoot := func(path string) string { + t.Helper() + before, _ := json.Marshal(project + string(filepath.Separator)) + after, _ := json.Marshal(path) + field := `"projectRoot":` + string(before) + if strings.Count(body, field) != 1 { + t.Fatal("fixture root field is not unique") + } + changed := strings.Replace(body, field, `"projectRoot":`+string(after), 1) + if changed == body { + t.Fatal("fixture root replacement did not change JSON") + } + return changed + } + for _, candidate := range []string{body, replaceRoot(project)} { + if _, err := parseProjectInspect([]byte(candidate), project); err != nil { + t.Fatal("observed root spelling refused", err) + } + } + invalid := make([]string, 0, 10) + invalid = append(invalid, strings.Replace(body, `"grokVersion":"1.0.67"`, `"grokVersion":"1.0.68"`, 1), strings.Replace(body, `"cwd":`, `"Cwd":`, 1), strings.Replace(body, `"projectRoot":`, `"ProjectRoot":`, 1), strings.Replace(body, `"enabled":false`, `"enabled":null`, 1), body+` {}`, strings.Replace(body, `"skills":[]`, `"skills":null`, 1), strings.Replace(body, `"name":"demo"`, `"name":"demo","Name":"demo"`, 1)) + for _, suffix := range []string{"//", "/./", "/../A/"} { + invalid = append(invalid, replaceRoot(project+suffix)) + } + for _, candidate := range invalid { + if candidate == body { + t.Fatal("invalid fixture is unchanged valid JSON") + } + if _, err := parseProjectInspect([]byte(candidate), project); err == nil { + t.Fatalf("ambiguous native source accepted: %s", candidate) + } + } +} +func TestProjectGrokMalformedContextCannotReachUserLifecycle(t *testing.T) { + for _, name := range []string{"both user labels", "other native", "other artifact", "other active", "delivery client", "attestation", "custom home"} { + t.Run(name, func(t *testing.T) { + request := projectTestRequest(t) + switch name { + case "both user labels": + request.Plan.Scope = domain.ScopeUser + request.Plan.TargetContext.Scope = domain.ScopeUser + case "other native": + request.Plan.NativeRegistryRoot = request.Client.ConfigRoot + case "other artifact": + request.Plan.PhysicalArtifactID = "foreign" + case "other active": + request.Plan.ActivePath = filepath.Join(request.Client.ConfigRoot, "plugins", "foreign") + case "delivery client": + request.Delivery.ClientID = domain.ClientClaude + case "attestation": + request.ActivationComplete = true + case "custom home": + request.Client.Surfaces = []domain.ClientSurface{{ID: "grok_custom_home", Detected: true}} + } + runner := &projectTestRunner{request: request} + if _, err := New().Activate(t.Context(), clients.Env{Runner: runner}, request); err == nil || runner.calls != 0 { + t.Fatalf("malformed project reached lifecycle: %v calls%d", err, runner.calls) + } + }) + } +} +func TestProjectGrokFiniteCollisionSourcesAndBudgets(t *testing.T) { + for _, name := range []string{"user grok", "user claude", "ancestor grok", "selected claude", "direct skill", "unknown layout", "folded name", "oversized", "aggregate", "entries"} { + t.Run(name, func(t *testing.T) { + request := projectTestRequest(t) + root := request.Plan.NativeRegistryRoot + switch name { + case "user grok": + root = request.Client.ConfigRoot + case "user claude": + root = filepath.Join(filepath.Dir(request.Client.ConfigRoot), ".claude") + case "ancestor grok": + root = filepath.Join(filepath.Dir(request.Plan.TargetContext.ProjectRoot), ".grok") + case "selected claude": + root = filepath.Join(request.Plan.TargetContext.ProjectRoot, ".claude") + } + manifest := filepath.Join(root, "plugins", "foreign", ".grok-plugin", "plugin.json") + body := `{"name":"demo"}` + switch name { + case "direct skill": + projectTestWrite(t, filepath.Join(root, "skills", "demo", "SKILL.md"), "inert") + case "unknown layout": + projectTestWrite(t, filepath.Join(root, "plugins", "foreign", "unknown.txt"), "inert") + case "folded name": + projectTestWrite(t, manifest, `{"Name":"demo"}`) + case "oversized": + projectTestWrite(t, manifest, strings.Repeat(" ", projectManifestLimit+1)) + case "aggregate": + for i := range 17 { + projectTestWrite(t, filepath.Join(root, "plugins", string(rune('a'+i)), ".grok-plugin", "plugin.json"), `{"name":"other"}`+strings.Repeat(" ", projectManifestLimit-len(`{"name":"other"}`))) + } + case "entries": + for i := range 129 { + projectTestWrite(t, filepath.Join(root, "plugins", fmt.Sprintf("entry-%03d", i), ".grok-plugin", "plugin.json"), `{"name":"other"}`) + } + entries, err := os.ReadDir(filepath.Join(root, "plugins")) + if err != nil || len(entries) != 129 { + t.Fatalf("entry-budget fixture has %d entries: %v", len(entries), err) + } + default: + projectTestWrite(t, manifest, body) + } + finding, err := inspectProjectFiles(request.Plan, false) + if name == "entries" && (err == nil || !strings.Contains(err.Error(), "grok plugin directory exceeds bounded discovery entries")) { + t.Fatalf("entry-budget case failed for another reason: %v", err) + } + collision := name == "user grok" || name == "user claude" || name == "ancestor grok" || name == "selected claude" || name == "direct skill" + if collision { + if err != nil || finding != clients.RegistryCollision { + t.Fatalf("foreign source overlooked: %s %v %v", name, finding, err) + } + } else if err == nil || finding != clients.RegistryIndeterminate { + t.Fatalf("incomplete bounded discovery became available: %s %v %v", name, finding, err) + } + }) + } +} +func TestProjectGrokProjectionRejectsForeignStagingBeforeWrites(t *testing.T) { + for _, kind := range []string{"user", "other", "outside", "nonclean"} { + t.Run(kind, func(t *testing.T) { + request := projectTestRequest(t) + if err := os.MkdirAll(request.Plan.TargetAnchor, 0700); err != nil { + t.Fatal(err) + } + stage := filepath.Join(request.Plan.TargetAnchor, ".agentplugins-staging-fixture") + if kind == "nonclean" { + stage = request.Plan.TargetAnchor + string(filepath.Separator) + "x" + string(filepath.Separator) + ".." + string(filepath.Separator) + ".agentplugins-staging-fixture" + } else { + target := projectTestRoot(t) + if kind == "user" { + target = request.Client.ConfigRoot + } + if err := os.Symlink(target, stage); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + } + if _, err := New().Project(t.Context(), clients.ProjectionInput{Plan: request.Plan, StagingPath: stage}); err == nil { + t.Fatal("foreign staging admitted") + } + if body, _ := os.ReadFile(filepath.Join(request.Client.ConfigRoot, "keep.txt")); string(body) != "original user" { + t.Fatal("user mutated") + } + }) + } +} + +func TestProjectGrokMissingLeafCannotHideAncestorAlias(t *testing.T) { + request := projectTestRequest(t) + alias := filepath.Join(filepath.Dir(request.Plan.TargetContext.ProjectRoot), ".claude") + if err := os.Symlink(projectTestRoot(t), alias); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if finding, err := inspectProjectFiles(request.Plan, false); err == nil || finding != clients.RegistryIndeterminate { + t.Fatalf("missing leaf under alias became absence proof: %v %v", finding, err) + } +} +func TestProjectGrokManifestSparseCapAndExactLimit(t *testing.T) { + path := filepath.Join(projectTestRoot(t), "manifest.json") + body := `{"name":"other"}` + projectTestWrite(t, path, body+strings.Repeat(" ", projectManifestLimit-len(body))) + scanner := projectFileScanner{remaining: projectDiscoveryLimit} + if got, err := scanner.readProjectManifest(path, nil); err != nil || len(got) != projectManifestLimit { + t.Fatal("exact limit refused", err) + } + file, err := os.OpenFile(path, os.O_WRONLY, 0600) + if err != nil { + t.Fatal(err) + } + if err := file.Truncate(1 << 30); err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + if _, err := scanner.readProjectManifest(path, nil); err == nil { + t.Fatal("sparse overlimit manifest admitted") + } +} + +func TestProjectGrokSelectedSkillNameCollidesAcrossNativeSources(t *testing.T) { + for _, surface := range []string{".grok", ".claude"} { + for _, scope := range []string{"user", "ancestor", "selected"} { + t.Run(scope+surface, func(t *testing.T) { + request := projectTestRequest(t) + base := request.Plan.TargetContext.ProjectRoot + switch scope { + case "user": + base = filepath.Dir(request.Client.ConfigRoot) + case "ancestor": + base = filepath.Dir(base) + } + projectTestWrite(t, filepath.Join(base, surface, "skills", "docs", "SKILL.md"), "foreign selected skill") + finding, err := inspectProjectFiles(request.Plan, false) + if err != nil || finding != clients.RegistryCollision { + t.Fatalf("selected skill docs under plugin demo missed: %v %v", finding, err) + } + }) + } + } +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_inspect.go b/install/integrationctl/agentplugins/clients/grok/project_inspect.go new file mode 100644 index 000000000..b2a518122 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_inspect.go @@ -0,0 +1,118 @@ +package grok + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type projectPlugin struct { + name, scope, path string + enabled bool +} +type projectInspection struct{ plugins []projectPlugin } + +func parseProjectInspect(body []byte, project string) (projectInspection, error) { + fail := func() (projectInspection, error) { + return projectInspection{}, fmt.Errorf("grok 1.0.67 project inspect contract is not recognized") + } + value, err := shared.DecodeStrictJSONObject(body) + if err != nil { + return fail() + } + if !exactProjectFields(value, []string{"grokVersion", "cwd", "projectRoot", "projectTrusted", "plugins", "skills"}) { + return fail() + } + if value["grokVersion"] != "1.0.67" || value["cwd"] != project || (value["projectRoot"] != project && value["projectRoot"] != project+string(filepath.Separator)) { + return fail() + } + if _, ok := value["projectTrusted"].(bool); !ok { + return fail() + } + if _, ok := value["skills"].([]any); !ok { + return fail() + } + entries, ok := value["plugins"].([]any) + if !ok { + return fail() + } + out := projectInspection{} + seen := map[string]bool{} + for _, entry := range entries { + parsed, err := parseProjectPlugin(entry) + if err != nil || seen[parsed.name] { + return fail() + } + seen[parsed.name] = true + out.plugins = append(out.plugins, parsed) + } + return out, nil +} +func exactProjectFields(object map[string]any, fields []string) bool { + for key := range object { + for _, field := range fields { + if shared.FoldJSONKey(key) == shared.FoldJSONKey(field) && key != field { + return false + } + } + } + return true +} +func parseProjectPlugin(entry any) (projectPlugin, error) { + object, ok := entry.(map[string]any) + if !ok || !exactProjectFields(object, []string{"name", "scope", "path", "enabled"}) { + return projectPlugin{}, fmt.Errorf("grok plugin inspect identity is not recognized") + } + name, n := object["name"].(string) + scope, s := object["scope"].(string) + path, p := object["path"].(string) + enabled, e := object["enabled"].(bool) + if !n || !s || !p || !e || strings.TrimSpace(name) == "" || !filepath.IsAbs(path) || filepath.Clean(path) != path || (scope != "project" && scope != "user") { + return projectPlugin{}, fmt.Errorf("grok plugin inspect identity is not recognized") + } + return projectPlugin{name, scope, path, enabled}, nil +} +func inspectProject(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, owned bool) (clients.RegistryFinding, error) { + root, err := grokPlanRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if env.Runner == nil || plan.NativeRegistryExecutable == "" || plan.NativeRegistryExecutable != client.ExecutablePath { + return clients.RegistryIndeterminate, fmt.Errorf("pinned Grok project inspect CLI unavailable or differs from detected client") + } + finding, err := inspectProjectFiles(plan, owned) + if err != nil || finding == clients.RegistryCollision { + return finding, err + } + bounded, cancel := context.WithTimeout(ctx, grokListTimeout) + defer cancel() + result, err := shared.RunNativeRegistry(bounded, env.Runner, legacyports.Command{Argv: []string{plan.NativeRegistryExecutable, "inspect", "--json"}, Dir: filepath.Dir(root), StdoutLimitBytes: grokListStdoutLimitBytes}) + if err != nil { + return clients.RegistryIndeterminate, err + } + if result.ExitCode != 0 || len(result.Stderr) != 0 { + return clients.RegistryIndeterminate, fmt.Errorf("grok project inspect failed or emitted stderr") + } + native, err := parseProjectInspect(result.Stdout, filepath.Dir(root)) + if err != nil { + return clients.RegistryIndeterminate, err + } + for _, entry := range native.plugins { + if entry.name == plan.DeclaredName { + if owned && entry.scope == "project" && entry.path == plan.ActivePath { + if finding != clients.RegistryExpected { + return clients.RegistryIndeterminate, fmt.Errorf("grok project source lacks its exact prepared manifest") + } + return clients.RegistryExpected, nil + } + return clients.RegistryCollision, nil + } + } + return finding, nil +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_lifecycle.go b/install/integrationctl/agentplugins/clients/grok/project_lifecycle.go new file mode 100644 index 000000000..7c8eeda0b --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_lifecycle.go @@ -0,0 +1,84 @@ +package grok + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +const projectPreparationAction = "Grok project plugin is prepared; review the selected project and plugin in Grok, make any trust decision there, and separately verify native discovery; skills execution and bundled MCP remain unqualified" + +func (*Adapter) PreflightActivation(_ clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject && request.Plan.TargetContext.Scope != domain.ScopeProject && request.Plan.TargetContext.ProjectRoot == "" { + return nil + } + if _, err := grokPlanRoot(request.Client, request.Plan); err != nil { + return err + } + if request.Plan.Status == domain.PlanUnsupported { + return fmt.Errorf("grok project package is unsupported") + } + if request.Delivery.ClientID != "" && request.Delivery.ClientID != domain.ClientGrok { + return fmt.Errorf("grok project delivery client differs from plan") + } + if request.ActivationComplete { + return fmt.Errorf("grok project preparation cannot be promoted by activation attestation") + } + if (request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath) || (request.Delivery.OwnedBase != "" && request.Delivery.OwnedBase != request.Plan.TargetRoot) || (request.DeclaredName != "" && request.DeclaredName != request.Plan.DeclaredName) { + return fmt.Errorf("grok project delivered identity differs from plan") + } + if !shared.ComponentKindPresent(request.Plan.Components, domain.ComponentSkill) { + return fmt.Errorf("grok project preparation requires a supported skill") + } + for _, component := range request.Plan.Components { + if component.Support != domain.SupportUnsupported && component.Kind != domain.ComponentSkill { + return fmt.Errorf("grok project component is unqualified") + } + } + return nil +} +func activateProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if err := New().PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + if request.Delivery.ClientID != domain.ClientGrok || request.Delivery.ActivePath != request.Plan.ActivePath || request.Delivery.OwnedBase != request.Plan.TargetRoot || request.DeclaredName != request.Plan.DeclaredName { + return domain.ActivationOutcome{}, fmt.Errorf("grok project delivered identity differs from plan") + } + finding, err := inspectProject(ctx, env, request.Client, request.Plan, true) + if err != nil { + return domain.ActivationOutcome{}, err + } + if finding != clients.RegistryExpected { + return domain.ActivationOutcome{}, fmt.Errorf("grok prepared project plugin source is absent, foreign or indeterminate") + } + outcome := shared.StartedActivation(request) + outcome.Activation = domain.ActivationPrepared + outcome.UserActions = []string{projectPreparationAction} + return outcome, nil +} +func deactivateProject(_ context.Context, _ clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + root, err := grokProjectRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return outcome, err + } + binding := domain.ClientBinding{ClientID: "grok", Scope: "project", NativeProfileRoot: root, PhysicalArtifact: request.PhysicalArtifactID, TargetLocator: request.ManagedArtifactPath, NativeObjects: request.NativeObjects} + if err := New().ValidateBindingProfile(root, binding); err != nil { + return outcome, err + } + if filepath.Dir(request.ManagedArtifactPath) != filepath.Join(root, "plugins") { + return outcome, fmt.Errorf("grok project removal path differs from owned directory") + } + if !request.Confirmed { + outcome.UserActions = []string{"agentplugins will remove only its exact prepared Grok project plugin directory"} + return outcome, nil + } + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_open.go b/install/integrationctl/agentplugins/clients/grok/project_open.go new file mode 100644 index 000000000..84c82e62d --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_open.go @@ -0,0 +1,66 @@ +package grok + +import ( + "fmt" + "os" + "path/filepath" +) + +// The hook is supplied only by inert race regressions. Production opens are +// anchored, non-following and nonblocking before inspecting the actual handle. +func openProjectPath(path string, directory bool, beforeOpen func()) (*os.File, os.FileInfo, error) { + if !filepath.IsAbs(path) || filepath.Clean(path) != path { + return nil, nil, fmt.Errorf("grok discovery path is not absolute and clean") + } + before, err := os.Lstat(path) + if err != nil { + // Even an absent leaf must not hide an existing alias in its ancestors. + probe, openErr := openProjectHandle(path, directory) + if probe != nil { + _ = probe.Close() + return nil, nil, fmt.Errorf("grok discovery path appeared during open") + } + return nil, nil, openErr + } + if before.Mode()&os.ModeSymlink != 0 || before.IsDir() != directory || (!directory && !before.Mode().IsRegular()) { + return nil, nil, fmt.Errorf("grok discovery requires a real typed path") + } + if beforeOpen != nil { + beforeOpen() + } + file, err := openProjectHandle(path, directory) + if err != nil { + return nil, nil, err + } + actual, err := file.Stat() + if err != nil { + _ = file.Close() + return nil, nil, err + } + if !os.SameFile(before, actual) || actual.IsDir() != directory || (!directory && !actual.Mode().IsRegular()) { + _ = file.Close() + return nil, nil, fmt.Errorf("grok discovery path changed before open") + } + return file, actual, nil +} +func stableProjectPath(path string, file *os.File, before os.FileInfo) error { + // Re-open through the same anchored no-reparse walk, so a same-object leaf + // reached through a newly introduced ancestor alias cannot pass SameFile. + probe, _, err := openProjectPath(path, before.IsDir(), nil) + if err != nil { + return err + } + _ = probe.Close() + actual, err := file.Stat() + if err != nil { + return err + } + after, err := os.Lstat(path) + if err != nil { + return err + } + if !os.SameFile(before, actual) || !os.SameFile(actual, after) || after.Mode()&os.ModeSymlink != 0 || before.Size() != actual.Size() || actual.Size() != after.Size() || !before.ModTime().Equal(actual.ModTime()) || !actual.ModTime().Equal(after.ModTime()) { + return fmt.Errorf("grok discovery path changed during bounded read") + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_open_unix.go b/install/integrationctl/agentplugins/clients/grok/project_open_unix.go new file mode 100644 index 000000000..7746fb49d --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_open_unix.go @@ -0,0 +1,38 @@ +//go:build !windows + +package grok + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/unix" +) + +func openProjectHandle(path string, directory bool) (*os.File, error) { + flags := unix.O_RDONLY | unix.O_CLOEXEC | unix.O_NOFOLLOW | unix.O_NONBLOCK + fd, err := unix.Open(string(filepath.Separator), flags|unix.O_DIRECTORY, 0) + if err != nil { + return nil, err + } + parts := strings.Split(strings.TrimPrefix(path, string(filepath.Separator)), string(filepath.Separator)) + for i, part := range parts { + if part == "" { + _ = unix.Close(fd) + return nil, fmt.Errorf("grok discovery path has an empty component") + } + selected := flags + if i < len(parts)-1 || directory { + selected |= unix.O_DIRECTORY + } + next, openErr := unix.Openat(fd, part, selected, 0) + _ = unix.Close(fd) + if openErr != nil { + return nil, openErr + } + fd = next + } + return os.NewFile(uintptr(fd), path), nil +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_open_unix_test.go b/install/integrationctl/agentplugins/clients/grok/project_open_unix_test.go new file mode 100644 index 000000000..0e31e495e --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_open_unix_test.go @@ -0,0 +1,60 @@ +//go:build !windows + +package grok + +import ( + "os" + "path/filepath" + "testing" + + "golang.org/x/sys/unix" +) + +func TestProjectGrokAnchoredOpenRefusesLeafAndAncestorSwaps(t *testing.T) { + for _, directory := range []bool{false, true} { + for _, kind := range []string{"fifo", "symlink", "ancestor"} { + t.Run(map[bool]string{false: "manifest", true: "directory"}[directory]+"/"+kind, func(t *testing.T) { + root := projectTestRoot(t) + parent := filepath.Join(root, "parent") + path := filepath.Join(parent, "leaf") + if directory { + if err := os.MkdirAll(path, 0700); err != nil { + t.Fatal(err) + } + } else { + projectTestWrite(t, path, `{"name":"demo"}`) + } + target := projectTestRoot(t) + projectTestWrite(t, filepath.Join(target, "leaf"), "foreign") + hook := func() { + if kind == "ancestor" { + if err := os.Rename(parent, parent+"-old"); err != nil { + t.Fatal(err) + } + if err := os.Symlink(target, parent); err != nil { + t.Fatal(err) + } + return + } + if err := os.RemoveAll(path); err != nil { + t.Fatal(err) + } + if kind == "fifo" { + if err := unix.Mkfifo(path, 0600); err != nil { + t.Fatal(err) + } + } else if err := os.Symlink(target, path); err != nil { + t.Fatal(err) + } + } + file, _, err := openProjectPath(path, directory, hook) + if file != nil { + _ = file.Close() + } + if err == nil { + t.Fatal("typed nofollow handle admitted path swap") + } + }) + } + } +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_open_windows.go b/install/integrationctl/agentplugins/clients/grok/project_open_windows.go new file mode 100644 index 000000000..3e6968c90 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_open_windows.go @@ -0,0 +1,120 @@ +//go:build windows + +package grok + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "unsafe" + + "golang.org/x/sys/windows" +) + +func openProjectHandle(path string, directory bool) (*os.File, error) { + return openProjectWindows(path, directory, nil) +} + +// Every child is opened relative to its held directory handle. Reparse refusal +// is applied by the object manager at each hop, before a leaf can be followed. +func openProjectWindows(path string, directory bool, beforeLeaf func()) (*os.File, error) { + volume := filepath.VolumeName(path) + root := volume + string(filepath.Separator) + pointer, err := windows.UTF16PtrFromString(root) + if err != nil { + return nil, err + } + parent, err := windows.CreateFile(pointer, windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OPEN_REPARSE_POINT|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + if err != nil { + return nil, err + } + if err := projectWindowsHandleType(parent, true); err != nil { + _ = windows.CloseHandle(parent) + return nil, err + } + parts := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator)) + for index, part := range parts { + if part == "" { + _ = windows.CloseHandle(parent) + return nil, fmt.Errorf("grok discovery has an empty path component") + } + leaf := index == len(parts)-1 + if leaf && beforeLeaf != nil { + beforeLeaf() + } + child, openErr := openProjectWindowsChild(parent, part, !leaf || directory) + _ = windows.CloseHandle(parent) + if openErr != nil { + return nil, openErr + } + parent = child + } + if err := projectWindowsAncestors(path); err != nil { + _ = windows.CloseHandle(parent) + return nil, err + } + return os.NewFile(uintptr(parent), path), nil +} +func openProjectWindowsChild(parent windows.Handle, name string, directory bool) (windows.Handle, error) { + nativeName, err := windows.NewNTUnicodeString(name) + if err != nil { + return 0, err + } + attributes := windows.OBJECT_ATTRIBUTES{Length: uint32(unsafe.Sizeof(windows.OBJECT_ATTRIBUTES{})), RootDirectory: parent, ObjectName: nativeName, Attributes: windows.OBJ_CASE_INSENSITIVE | windows.OBJ_DONT_REPARSE} + options := uint32(windows.FILE_OPEN_REPARSE_POINT | windows.FILE_SYNCHRONOUS_IO_NONALERT | windows.FILE_NON_DIRECTORY_FILE) + if directory { + options = windows.FILE_OPEN_REPARSE_POINT | windows.FILE_SYNCHRONOUS_IO_NONALERT | windows.FILE_DIRECTORY_FILE + } + var handle windows.Handle + var status windows.IO_STATUS_BLOCK + err = windows.NtCreateFile(&handle, windows.GENERIC_READ|windows.SYNCHRONIZE, &attributes, &status, nil, 0, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, windows.FILE_OPEN, options, 0, 0) + if err != nil { + if native, ok := err.(windows.NTStatus); ok { + return 0, native.Errno() + } + return 0, err + } + if err := projectWindowsHandleType(handle, directory); err != nil { + _ = windows.CloseHandle(handle) + return 0, err + } + return handle, nil +} +func projectWindowsHandleType(handle windows.Handle, directory bool) error { + kind, err := windows.GetFileType(handle) + if err != nil { + return err + } + if kind != windows.FILE_TYPE_DISK { + return fmt.Errorf("grok discovery requires a disk handle") + } + var info windows.ByHandleFileInformation + if err := windows.GetFileInformationByHandle(handle, &info); err != nil { + return err + } + if info.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || (info.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0) != directory { + return fmt.Errorf("grok discovery handle type is unqualified") + } + return nil +} +func projectWindowsAncestors(path string) error { + current := filepath.VolumeName(path) + string(filepath.Separator) + for _, part := range append([]string{""}, strings.Split(strings.TrimPrefix(path, current), string(filepath.Separator))...) { + if part != "" { + current = filepath.Join(current, part) + } + pointer, err := windows.UTF16PtrFromString(current) + if err != nil { + return err + } + attributes, err := windows.GetFileAttributes(pointer) + if err != nil { + return err + } + if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return fmt.Errorf("grok discovery reparse point is unqualified") + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/grok/project_open_windows_test.go b/install/integrationctl/agentplugins/clients/grok/project_open_windows_test.go new file mode 100644 index 000000000..fedc619ec --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/project_open_windows_test.go @@ -0,0 +1,95 @@ +//go:build windows + +package grok + +import ( + "os" + "path/filepath" + "testing" +) + +func TestProjectGrokWindowsAnchoredOpenRejectsSameObjectAncestorAlias(t *testing.T) { + for _, directory := range []bool{false, true} { + t.Run(map[bool]string{false: "manifest", true: "directory"}[directory], func(t *testing.T) { + base := projectTestRoot(t) + parent := filepath.Join(base, "parent") + path := filepath.Join(parent, "leaf") + if directory { + if err := os.MkdirAll(path, 0700); err != nil { + t.Fatal(err) + } + } else { + projectTestWrite(t, path, `{"name":"demo"}`) + } + // Check symlink capability before introducing the deterministic swap. + probe := filepath.Join(base, "probe") + if err := os.Symlink(parent, probe); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if err := os.Remove(probe); err != nil { + t.Fatal(err) + } + before, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + file, err := openProjectWindows(path, directory, func() { + if err := os.Rename(parent, parent+"-old"); err != nil { + t.Fatal(err) + } + if err := os.Symlink(parent+"-old", parent); err != nil { + t.Fatal(err) + } + }) + if file != nil { + _ = file.Close() + } + if err == nil { + t.Fatal("same-object ancestor alias was admitted") + } + after, err := os.Stat(path) + if err != nil || !os.SameFile(before, after) { + t.Fatal("fixture failed to preserve exact same leaf object", err) + } + }) + } +} + +func TestProjectGrokWindowsReadRecheckRefusesSameObjectAncestorAlias(t *testing.T) { + base := projectTestRoot(t) + parent := filepath.Join(base, "parent") + path := filepath.Join(parent, "manifest.json") + projectTestWrite(t, path, `{"name":"demo"}`) + file, info, err := openProjectPath(path, false, nil) + if err != nil { + t.Fatal(err) + } + defer func() { _ = file.Close() }() + if err := stableProjectPath(path, file, info); err != nil { + t.Fatal("canonical open-file recheck failed", err) + } + alias := filepath.Join(base, "alias") + if err := os.Symlink(parent, alias); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + aliasPath := filepath.Join(alias, "manifest.json") + aliasInfo, err := os.Stat(aliasPath) + if err != nil || !os.SameFile(info, aliasInfo) { + t.Fatal("alias does not name the still-open leaf", err) + } + probe, anchorErr := openProjectHandle(aliasPath, false) + if probe != nil { + _ = probe.Close() + } + if anchorErr == nil { + t.Fatal("anchored open admitted the ancestor alias") + } + // Local handle-type refusals are fresh diagnostic errors, not sentinels. + // Pin the same anchored refusal while keeping the held-file identity proof. + if err := stableProjectPath(aliasPath, file, info); err == nil || err.Error() != anchorErr.Error() { + t.Fatalf("recheck did not return the anchored alias refusal: %v; want %v", err, anchorErr) + } + if err := stableProjectPath(path, file, info); err != nil { + t.Fatal("canonical descriptor changed during alias check", err) + } +} diff --git a/install/integrationctl/agentplugins/clients/grok/registry.go b/install/integrationctl/agentplugins/clients/grok/registry.go index 2ef01bf7d..88cc143bc 100644 --- a/install/integrationctl/agentplugins/clients/grok/registry.go +++ b/install/integrationctl/agentplugins/clients/grok/registry.go @@ -122,7 +122,25 @@ var _ clients.RegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return true } -func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, _ domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" { + root, err := grokPlanRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + if managed.Scope != string(domain.ScopeProject) { + return clients.RegistryIndeterminate, fmt.Errorf("grok project ownership requires a project receipt") + } + if err := New().ValidateBindingProfile(root, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + if managed.PhysicalArtifact != plan.PhysicalArtifactID || managed.TargetLocator != plan.ActivePath { + return clients.RegistryIndeterminate, fmt.Errorf("grok binding differs from planned project artifact") + } + } + return inspectProject(ctx, env, client, plan, managed != nil) + } if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } @@ -147,3 +165,19 @@ func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, _ do } return clients.RegistryCollision, nil } + +var _ clients.PreparedRegistryInspector = (*Adapter)(nil) + +// Project discovery replaces generic prepared scanning with finite native roots. +func (*Adapter) InspectPreparedRegistry(plan domain.DeliveryPlan, name string, owned bool) (clients.RegistryFinding, error) { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" { + if _, err := grokPlanRoot(domain.DetectedClient{ClientID: domain.ClientGrok, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return clients.RegistryIndeterminate, err + } + if name != plan.DeclaredName { + return clients.RegistryIndeterminate, fmt.Errorf("grok prepared name differs from plan") + } + return inspectProjectFiles(plan, owned) + } + return shared.InspectUnqualifiedPluginRoot(plan.TargetRoot, name, plan.ActivePath, owned) +} diff --git a/install/integrationctl/agentplugins/clients/grok/scope.go b/install/integrationctl/agentplugins/clients/grok/scope.go new file mode 100644 index 000000000..54326d16e --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/scope.go @@ -0,0 +1,135 @@ +package grok + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) + _ clients.PlanQualifier = (*Adapter)(nil) + _ clients.ProfileBindingValidator = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".grok"), in.Client.ExecutablePath + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, _ string, target domain.TargetContext) (string, string, error) { + if mode != domain.PackageNative { + return "", "", fmt.Errorf("grok project preparation requires native plugin-directory delivery") + } + root, err := grokProjectRoot(client, target, "") + if err != nil { + return "", "", err + } + return root, filepath.Join(root, "plugins"), nil +} +func grokProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + for _, surface := range client.Surfaces { + if surface.ID == "grok_custom_home" && surface.Detected { + return "", fmt.Errorf("grok project preparation cannot exhaust Claude-compatible sources with custom GROK_HOME") + } + } + selected, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if client.ClientID != domain.ClientGrok || selected.Scope != domain.ScopeProject || selected.UserConfigRoot != client.ConfigRoot || filepath.Base(client.ConfigRoot) != ".grok" { + return "", fmt.Errorf("grok project requires matching scope and original ordinary .grok user profile; custom GROK_HOME compatibility sources are unqualified") + } + root := filepath.Join(selected.ProjectRoot, ".grok") + if root == client.ConfigRoot || strings.HasPrefix(root, client.ConfigRoot+string(filepath.Separator)) || strings.HasPrefix(client.ConfigRoot, root+string(filepath.Separator)) { + return "", fmt.Errorf("grok project registry must not overlap the original user profile") + } + if planned != "" && planned != root { + return "", fmt.Errorf("grok native registry differs from selected project") + } + if err := pathpolicy.RequireContainedChild(selected.ProjectRoot, filepath.Join(root, "plugins")); err != nil { + return "", err + } + return root, nil +} +func grokPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.ClientID != domain.ClientGrok || plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return "", fmt.Errorf("grok project lifecycle requires matching client and project scope") + } + root, err := grokProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if err := pathpolicy.ValidateLeafID(plan.DeclaredName); err != nil { + return "", err + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + if plan.TargetAnchor != root || plan.TargetRoot != filepath.Join(root, "plugins") || plan.ActivePath != filepath.Join(root, "plugins", plan.PhysicalArtifactID) { + return "", fmt.Errorf("grok project artifact differs from exact selected plugin directory") + } + return root, pathpolicy.RequireContainedChild(root, plan.ActivePath) +} +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if plan.Scope != domain.ScopeProject && in.TargetContext.Scope != domain.ScopeProject && in.TargetContext.ProjectRoot == "" { + return nil + } + if plan.Scope != domain.ScopeProject || in.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("grok project plan scope mismatch") + } + if _, err := grokProjectRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot); err != nil { + return err + } + if in.Client.ExecutablePath == "" { + plan.Status = domain.PlanUnsupported + plan.Warnings = append(plan.Warnings, "pinned_grok_inspect_required") + plan.UserActions = append(plan.UserActions, "Grok Build 1.0.67 CLI is required for read-only project source checks") + } + return nil +} +func (*Adapter) QualifyPlan(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if plan.Scope != domain.ScopeProject && in.TargetContext.Scope != domain.ScopeProject && in.TargetContext.ProjectRoot == "" { + return nil + } + if plan.Scope != domain.ScopeProject || in.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("grok project plan scope mismatch") + } + if _, err := grokProjectRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot); err != nil { + return err + } + for i := range plan.Components { + if plan.Components[i].Kind == domain.ComponentMCPServer { + plan.Components[i].Support = domain.SupportUnsupported + plan.Components[i].Reason = "Grok project bundled MCP initialization and tools are unqualified at pinned 1.0.67" + } + } + return nil +} +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil + } + if binding.ClientID != "grok" || binding.NativeProfileRoot != root || filepath.Base(root) != ".grok" { + return fmt.Errorf("grok project binding native profile differs from selected project") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + if binding.TargetLocator != filepath.Join(root, "plugins", binding.PhysicalArtifact) { + return fmt.Errorf("grok project binding locator differs from physical artifact") + } + for _, object := range binding.NativeObjects { + if object.Kind != "managed_package_directory" || object.Path != binding.TargetLocator { + return fmt.Errorf("grok project binding contains foreign native ownership") + } + } + return pathpolicy.RequireContainedChild(filepath.Dir(root), binding.TargetLocator) +} diff --git a/install/integrationctl/agentplugins/clients/grok/staging.go b/install/integrationctl/agentplugins/clients/grok/staging.go new file mode 100644 index 000000000..a7f012bc8 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/grok/staging.go @@ -0,0 +1,26 @@ +package grok + +import ( + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.StagingLayout = (*Adapter)(nil) + +// Keep unpublished bytes beside the watched plugins root. Discard may carry +// only TargetRoot, whose parent identifies the same deterministic anchor. +func (*Adapter) StagingBase(plan domain.DeliveryPlan) string { + if plan.TargetAnchor != "" { + return plan.TargetAnchor + } + return filepath.Dir(plan.TargetRoot) +} +func (*Adapter) ValidateTargetLayout(plan domain.DeliveryPlan) error { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" { + _, err := grokPlanRoot(domain.DetectedClient{ClientID: domain.ClientGrok, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan) + return err + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/kimi/adapter.go b/install/integrationctl/agentplugins/clients/kimi/adapter.go index 4944fb49f..41bb15641 100644 --- a/install/integrationctl/agentplugins/clients/kimi/adapter.go +++ b/install/integrationctl/agentplugins/clients/kimi/adapter.go @@ -48,6 +48,9 @@ func (*Adapter) TargetRoot(client domain.DetectedClient, _ domain.PackageMode, _ return client.ConfigRoot, filepath.Join(client.ConfigRoot, "plugins", "managed"), nil } func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject { + return refineKimiProject(in, plan) + } shared.PromoteNativeReady(plan, in.Client.ConfigRoot, shared.OnlyNativeComponents(plan.Components)) plan.UserActions = shared.AppendUnique(plan.UserActions, reloadAction) return nil diff --git a/install/integrationctl/agentplugins/clients/kimi/identity.go b/install/integrationctl/agentplugins/clients/kimi/identity.go index c020e2032..87dce2747 100644 --- a/install/integrationctl/agentplugins/clients/kimi/identity.go +++ b/install/integrationctl/agentplugins/clients/kimi/identity.go @@ -2,6 +2,7 @@ package kimi import ( "context" + "fmt" "os" "path/filepath" "strings" @@ -13,10 +14,28 @@ import ( ) func (*Adapter) UsesNativeRegistryExecutable() bool { return false } -func (*Adapter) InspectNativeRegistry(ctx context.Context, _ clients.Env, _ domain.DetectedClient, p domain.DeliveryPlan, owned *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, p domain.DeliveryPlan, owned *domain.ClientBinding) (clients.RegistryFinding, error) { if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + if p.Scope == domain.ScopeProject || p.TargetContext.Scope == domain.ScopeProject { + root, err := kimiPlanRoot(client, p) + if err != nil { + return clients.RegistryIndeterminate, err + } + if owned != nil { + if owned.ClientID != "kimi" || owned.Scope != string(domain.ScopeProject) || owned.PhysicalArtifact != p.PhysicalArtifactID || owned.TargetLocator != p.ActivePath { + return clients.RegistryIndeterminate, fmt.Errorf("kimi project binding scope/artifact/path differs") + } + if err := New().ValidateBindingProfile(root, *owned); err != nil { + return clients.RegistryIndeterminate, err + } + selected := *owned + selected.NativeObjects = kimiNativeObjects(owned.NativeObjects) + owned = &selected + } + return kimiComponents(root).Inspect(p.Components, owned, env.NativeConfig) + } if err := validateIdentity(p.NativeRegistryRoot, p.DeclaredName, p.ActivePath); err != nil { return clients.RegistryIndeterminate, err } @@ -38,6 +57,13 @@ func (*Adapter) InspectNativeRegistry(ctx context.Context, _ clients.Env, _ doma return clients.RegistryCollision, nil } func (*Adapter) InspectPreparedRegistry(p domain.DeliveryPlan, name string, owned bool) (clients.RegistryFinding, error) { + if p.Scope == domain.ScopeProject || p.TargetContext.Scope == domain.ScopeProject { + _, err := kimiPlanRoot(domain.DetectedClient{ConfigRoot: p.TargetContext.UserConfigRoot}, p) + if err != nil { + return clients.RegistryIndeterminate, err + } + return clients.RegistryClear, nil + } if err := pathpolicy.RequireContainedChild(p.NativeRegistryRoot, p.TargetRoot); err != nil { return clients.RegistryIndeterminate, err } diff --git a/install/integrationctl/agentplugins/clients/kimi/lifecycle.go b/install/integrationctl/agentplugins/clients/kimi/lifecycle.go index 9c9e288c1..2a1b4dc3b 100644 --- a/install/integrationctl/agentplugins/clients/kimi/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/kimi/lifecycle.go @@ -14,12 +14,18 @@ import ( ) func (*Adapter) AutomaticallyActivates(_ clients.Env, r domain.ActivationRequest) bool { + if r.Plan.Scope == domain.ScopeProject { + return r.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(r.Plan.Components) + } return r.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.HasNativeConfigRoot(r) } func (*Adapter) VerifierAvailable(_ domain.DetectedClient, p domain.DeliveryPlan, _ string) bool { return p.NativeRegistryRoot != "" } -func (*Adapter) PreflightActivation(_ clients.Env, r domain.ActivationRequest) error { +func (*Adapter) PreflightActivation(env clients.Env, r domain.ActivationRequest) error { + if r.Plan.Scope == domain.ScopeProject || r.Plan.TargetContext.Scope == domain.ScopeProject { + return preflightKimiProject(env, r) + } // Generic preflight runs before staging, so Delivery and DeclaredName are // still empty there. Validate those fields only once they are supplied. if r.Client.ClientID != domain.ClientKimi || r.Plan.ClientID != domain.ClientKimi || @@ -45,6 +51,9 @@ func (*Adapter) PreflightActivation(_ clients.Env, r domain.ActivationRequest) e return err } func (a *Adapter) Activate(ctx context.Context, env clients.Env, r domain.ActivationRequest) (domain.ActivationOutcome, error) { + if r.Plan.Scope == domain.ScopeProject || r.Plan.TargetContext.Scope == domain.ScopeProject { + return activateKimiProject(ctx, env, r) + } out := shared.StartedActivation(r) // Native-config lifecycle persists an attempt before dispatch. Report its // effect explicitly so verified success can clear that marker; failures @@ -94,7 +103,10 @@ func (a *Adapter) Activate(ctx context.Context, env clients.Env, r domain.Activa out.UserActions = []string{reloadAction} return out, nil } -func (*Adapter) Deactivate(ctx context.Context, _ clients.Env, r domain.DeactivationRequest) (domain.DeactivationOutcome, error) { +func (*Adapter) Deactivate(ctx context.Context, env clients.Env, r domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if r.TargetContext.Scope == domain.ScopeProject { + return deactivateKimiProject(ctx, env, r) + } out := shared.StartedDeactivation() if err := ctx.Err(); err != nil { return out, err diff --git a/install/integrationctl/agentplugins/clients/kimi/project.go b/install/integrationctl/agentplugins/clients/kimi/project.go index 4f107dba2..cac12edc2 100644 --- a/install/integrationctl/agentplugins/clients/kimi/project.go +++ b/install/integrationctl/agentplugins/clients/kimi/project.go @@ -19,10 +19,16 @@ var _ clients.ActiveNativeProjector = (*Adapter)(nil) // writing. Everything Kimi owns — the projected manifest included — lives // inside the managed package directory the stager already digest-verified, so // after the manifest identity check there is no external ownership to rebuild. -func (*Adapter) ProjectActiveNative(ctx context.Context, root string, _ domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, data string) ([]domain.NativeObjectOwnership, error) { if err := ctx.Err(); err != nil { return nil, err } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if err := validateKimiActive(root, envelope, plan, data); err != nil { + return nil, err + } + return kimiProjectObjects(root, envelope, plan) + } if err := verifyManifest(root, plan.DeclaredName); err != nil { return nil, err } @@ -30,6 +36,9 @@ func (*Adapter) ProjectActiveNative(ctx context.Context, root string, _ domain.P } func (*Adapter) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if in.Plan.Scope == domain.ScopeProject || in.Plan.TargetContext.Scope == domain.ScopeProject { + return projectKimiComponents(ctx, in) + } if err := ctx.Err(); err != nil { return nil, err } diff --git a/install/integrationctl/agentplugins/clients/kimi/project_active.go b/install/integrationctl/agentplugins/clients/kimi/project_active.go new file mode 100644 index 000000000..cdf77efe1 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kimi/project_active.go @@ -0,0 +1,49 @@ +package kimi + +import ( + "encoding/json" + "fmt" + "path/filepath" + "reflect" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func validateKimiActive(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, data string) error { + if _, err := kimiPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return err + } + if root != plan.ActivePath { + return fmt.Errorf("kimi active package differs from selected plan") + } + if err := plan.SelectedDelivery.ValidatePlan(plan, envelope.TreeDigest); err != nil { + return err + } + for _, name := range shared.SupportedMCPNames(plan) { + if envelope.MCP.Servers[name].Type == "stdio" && (!filepath.IsAbs(data) || filepath.Clean(data) != data) { + return fmt.Errorf("%w: kimi selected stdio data root must be absolute and clean", domain.ErrProjectionRefreshRequired) + } + expected, err := renderKimiProjectServer(envelope.MCP.Servers[name], plan.ActivePath, data, root) + if err != nil { + return err + } + actual, err := readKimiProjectEntry(root, name) + if err != nil { + return err + } + // JSON normalization equates []string projected in memory with decoded []any. + body, err := json.Marshal(expected) + if err != nil { + return err + } + var normalized map[string]any + if err := json.Unmarshal(body, &normalized); err != nil { + return err + } + if !reflect.DeepEqual(actual, normalized) { + return fmt.Errorf("%w: kimi immutable package/data/launcher projection differs", domain.ErrProjectionRefreshRequired) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/kimi/project_components.go b/install/integrationctl/agentplugins/clients/kimi/project_components.go new file mode 100644 index 000000000..9eb025e65 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kimi/project_components.go @@ -0,0 +1,90 @@ +package kimi + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const ( + kimiProjectSkillKind = "kimi_project_skill_directory" + kimiProjectMCPKind = "kimi_project_mcp_server" +) + +func kimiComponents(root string) shared.ProjectComponents { + return shared.ProjectComponents{ProjectRoot: filepath.Dir(root), SkillsRoot: filepath.Join(root, "skills"), ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, "mcp.json")}, Codec: nativeconfig.CodecKimi, SkillKind: kimiProjectSkillKind, MCPKind: kimiProjectMCPKind} +} + +func kimiProjectObjects(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + native, err := kimiPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan) + if err != nil { + return nil, err + } + if err := pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, root); err != nil { + return nil, err + } + layout := kimiComponents(native) + objects := []domain.NativeObjectOwnership{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + var object domain.NativeObjectOwnership + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return nil, fmt.Errorf("kimi planned skill is missing") + } + relative := filepath.ToSlash(filepath.Dir(filepath.FromSlash(skill.RelativePath))) + if skill.RelativePath == "" { + relative = "skills/" + component.Name + } + object, err = layout.SkillObject(component.Name, relative, root) + case domain.ComponentMCPServer: + var server nativeconfig.Server + server, err = kimiProjectServer(root, component.Name) + if err == nil { + object, err = layout.MCPObject(component.Name, server, layout.ConfigPaths.JSON) + } + default: + return nil, fmt.Errorf("unsupported Kimi project component %q", component.Kind) + } + if err != nil { + return nil, err + } + objects = append(objects, object) + } + sort.Slice(objects, func(i, j int) bool { return objects[i].ObjectID < objects[j].ObjectID }) + return objects, layout.ValidateObjects(objects) +} + +func kimiDesiredServers(root string, objects []domain.NativeObjectOwnership) (map[string]nativeconfig.Server, error) { + servers := map[string]nativeconfig.Server{} + for _, object := range objects { + if object.Kind != kimiProjectMCPKind { + continue + } + server, err := kimiProjectServer(root, object.LogicalName) + if err != nil { + return nil, err + } + servers[object.LogicalName] = server + } + return servers, nil +} + +func kimiNativeObjects(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + selected := []domain.NativeObjectOwnership{} + for _, object := range objects { + if object.Kind != "managed_package_directory" { + selected = append(selected, object) + } + } + return selected +} diff --git a/install/integrationctl/agentplugins/clients/kimi/project_lifecycle.go b/install/integrationctl/agentplugins/clients/kimi/project_lifecycle.go new file mode 100644 index 000000000..47bb168a5 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kimi/project_lifecycle.go @@ -0,0 +1,118 @@ +package kimi + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const KimiProjectAction = "Kimi Code project components are configured. Review folder trust and the selected working directory, start a fresh session and inspect skills/MCP. Effective extra skill roots, disabled/session selection, native discovery, MCP initialization and authentication remain unobserved; existing sessions retain removed MCP tools until restarted." + +func preflightKimiProject(env clients.Env, r domain.ActivationRequest) error { + root, err := kimiPlanRoot(r.Client, r.Plan) + if err != nil { + return err + } + if r.Client.ClientID != domain.ClientKimi || (r.Delivery.ClientID != "" && r.Delivery.ClientID != domain.ClientKimi) { + return fmt.Errorf("kimi activation client mismatch") + } + if r.Delivery.ActivePath != "" && r.Delivery.ActivePath != r.Plan.ActivePath { + return fmt.Errorf("kimi delivery differs from selected artifact") + } + if r.ActivationComplete { + return fmt.Errorf("kimi project trust and native discovery cannot be established by activation attestation") + } + if err := currentKimi(r.Client); err != nil { + return err + } + if shared.ComponentKindPresent(r.Plan.Components, domain.ComponentSkill) { + if err := kimiSkillRoot(r.Plan.TargetContext.ProjectRoot); err != nil { + return err + } + } + if !shared.OnlyNativeComponents(r.Plan.Components) { + return fmt.Errorf("kimi project delivery requires supported skills and MCP only") + } + layout := kimiComponents(root) + if err := layout.ValidateObjects(kimiNativeObjects(r.PreviousNativeObjects)); err != nil { + return err + } + if err := layout.ValidateObjects(kimiNativeObjects(r.Delivery.NativeObjects)); err != nil { + return err + } + if err := env.NativeConfig.RequireFileIO(); err != nil { + return err + } + if shared.ComponentKindPresent(r.Plan.Components, domain.ComponentMCPServer) { + _, err := kimiConfigNames(filepath.Join(root, "mcp.json")) + return err + } + return nil +} + +func activateKimiProject(ctx context.Context, env clients.Env, r domain.ActivationRequest) (domain.ActivationOutcome, error) { + if err := ctx.Err(); err != nil { + return domain.ActivationOutcome{}, err + } + if err := shared.ActivationIdentityMismatch(r); err != nil { + return domain.ActivationOutcome{}, err + } + if r.Delivery.ActivePath != r.Plan.ActivePath { + return domain.ActivationOutcome{}, fmt.Errorf("kimi delivery differs from selected artifact") + } + if err := preflightKimiProject(env, r); err != nil { + return domain.ActivationOutcome{}, err + } + layout := kimiComponents(r.Plan.NativeRegistryRoot) + out, err := shared.CompleteNativeConfigActivation(ctx, r, shared.NativeConfigActivation{ + Automatic: r.Plan.InstallIntent == domain.InstallIntentAutomatic, + UnavailableAction: "select supported Kimi project components", RepairAction: "repair exact Kimi project entries", RetryAction: "retry Kimi project component installation", CompletedAction: KimiProjectAction, + Verify: func() error { + return layout.Verify(kimiNativeObjects(r.Delivery.NativeObjects), false, env.NativeConfig) + }, + Activate: func(ctx context.Context, request domain.ActivationRequest) error { + servers, err := kimiDesiredServers(request.Delivery.ActivePath, kimiNativeObjects(request.Delivery.NativeObjects)) + if err != nil { + return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} + } + return (shared.ProjectComponentMutation{Layout: layout, ActivePath: request.Delivery.ActivePath, Previous: kimiNativeObjects(request.PreviousNativeObjects), Desired: kimiNativeObjects(request.Delivery.NativeObjects), Servers: servers, Kernel: env.NativeConfig}).Apply(ctx) + }, + }) + if err == nil && out.Verification == domain.VerificationInstalled { + out.Activation, out.ActivationAttested = domain.ActivationPrepared, false + } + return out, err +} + +func deactivateKimiProject(ctx context.Context, env clients.Env, r domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + out := shared.StartedDeactivation() + if err := ctx.Err(); err != nil { + return out, err + } + plan := domain.DeliveryPlan{Scope: domain.ScopeProject, TargetContext: r.TargetContext, ClientID: domain.ClientKimi, PhysicalArtifactID: r.PhysicalArtifactID, ActivePath: r.ManagedArtifactPath, NativeRegistryRoot: r.NativeRegistryRoot} + root, err := kimiPlanRoot(r.Client, plan) + if err != nil { + return out, err + } + if r.Client.ClientID != domain.ClientKimi { + return out, fmt.Errorf("kimi deactivation client mismatch") + } + layout := kimiComponents(root) + if err := layout.ValidateObjects(kimiNativeObjects(r.NativeObjects)); err != nil { + return out, err + } + if !r.Confirmed { + out.UserActions = []string{"agentplugins will remove only exact owned Kimi project skill directories and MCP members"} + return out, nil + } + if err := (shared.ProjectComponentMutation{Layout: layout, Previous: kimiNativeObjects(r.NativeObjects), Kernel: env.NativeConfig}).Apply(ctx); err != nil && !shared.CommittedNativeDeactivationCleanup(&out, err) { + return out, err + } + out.ExternalRemovalComplete = true + out.UserActions = []string{"Start a fresh Kimi session to observe removal; already-open sessions can retain removed MCP tools"} + return out, nil +} diff --git a/install/integrationctl/agentplugins/clients/kimi/project_mcp.go b/install/integrationctl/agentplugins/clients/kimi/project_mcp.go new file mode 100644 index 000000000..c64fe30b6 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kimi/project_mcp.go @@ -0,0 +1,167 @@ +package kimi + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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 projectKimiComponents(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if _, err := kimiPlanRoot(domain.DetectedClient{ConfigRoot: in.Plan.TargetContext.UserConfigRoot}, in.Plan); err != nil { + return nil, err + } + if err := pathpolicy.RequireContainedChild(in.Plan.TargetContext.ProjectRoot, in.StagingPath); err != nil { + return nil, err + } + if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { + return nil, err + } + entries := map[string]any{} + for _, name := range shared.SupportedMCPNames(in.Plan) { + entry, err := renderKimiProjectServer(in.Envelope.MCP.Servers[name], in.Plan.ActivePath, in.PluginDataPath, in.StagingPath) + if err != nil { + return nil, err + } + entries[name] = entry + } + if len(entries) > 0 { + if err := shared.WriteJSON(filepath.Join(in.StagingPath, "mcp.json"), map[string]any{"mcpServers": entries}); err != nil { + return nil, err + } + } + return kimiProjectObjects(in.StagingPath, in.Envelope, in.Plan) +} + +func renderKimiProjectServer(source domain.MCPServer, active, data, staging string) (map[string]any, error) { + entry := shared.CloneObject(source.Decoded) + delete(entry, "type") + switch source.Type { + case "stdio": + if !filepath.IsAbs(data) || filepath.Clean(data) != data { + return nil, fmt.Errorf("kimi selected stdio data path must be absolute and clean") + } + if err := shared.ApplyStdioDataContract(entry, active, data, staging); err != nil { + return nil, err + } + if err := bindKimiStdio(entry, source, active, data); err != nil { + return nil, err + } + entry["transport"] = "stdio" + case "streamable-http": + entry["transport"] = "http" + case "sse": + entry["transport"] = "sse" + default: + return nil, fmt.Errorf("unsupported Kimi project MCP transport %q", source.Type) + } + return entry, nil +} + +func bindKimiStdio(entry map[string]any, source domain.MCPServer, active, data string) error { + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, active, data) + if err != nil { + return err + } + absolute, ok := entry["cwd"].(string) + if !ok { + return fmt.Errorf("kimi projected stdio cwd is missing") + } + args := []string{} + if raw, present := entry["args"]; present { + values, ok := raw.([]any) + if !ok { + return fmt.Errorf("kimi stdio arguments must be strings") + } + for _, value := range values { + text, ok := value.(string) + if !ok { + return fmt.Errorf("kimi stdio argument must be string") + } + args = append(args, text) + } + } + command, _ := source.Decoded["command"].(string) + if strings.HasPrefix(command, "${package.root}/") { + command = "./" + strings.TrimPrefix(command, "${package.root}/") + } + entry["command"] = filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + entry["args"] = managedstdio.Arguments(active, data, absolute, cwd.Anchor, command, args) + delete(entry, "cwd") + return nil +} + +func readKimiFile(path string) ([]byte, error) { + file, err := nativeconfig.New().ReadExactFile(path) + if err != nil { + return nil, err + } + if len(file.Body) > 1<<20 { + return nil, fmt.Errorf("kimi MCP file exceeds inspection limit") + } + return file.Body, nil +} + +func readKimiProjectEntry(root, name string) (map[string]any, error) { + path := filepath.Join(root, "mcp.json") + if err := pathpolicy.RequireContainedChild(root, path); err != nil { + return nil, err + } + body, err := readKimiFile(path) + if err != nil { + return nil, err + } + document, err := shared.DecodeStrictJSONObject(body) + if err != nil { + return nil, err + } + entries, ok := document["mcpServers"].(map[string]any) + if !ok { + return nil, fmt.Errorf("kimi projected MCP collection is missing") + } + entry, ok := entries[name].(map[string]any) + if !ok { + return nil, fmt.Errorf("kimi selected MCP entry is missing") + } + return entry, nil +} + +func kimiProjectServer(root, name string) (nativeconfig.Server, error) { + entry, err := readKimiProjectEntry(root, name) + if err != nil { + return nativeconfig.Server{}, err + } + body, err := json.Marshal(entry) + if err != nil { + return nativeconfig.Server{}, err + } + var server nativeconfig.Server + if err := json.Unmarshal(body, &server); err != nil { + return server, err + } + switch entry["transport"] { + case "stdio": + server.Type = "stdio" + case "http": + server.Type, server.RemoteTransport = "remote", "streamable-http" + case "sse": + server.Type, server.RemoteTransport = "remote", "sse" + default: + return server, fmt.Errorf("kimi native transport is unsupported") + } + server.StdioValuesResolved, server.CWDResolved = true, true + return server, nil +} diff --git a/install/integrationctl/agentplugins/clients/kimi/scope.go b/install/integrationctl/agentplugins/clients/kimi/scope.go new file mode 100644 index 000000000..f051ef4e7 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kimi/scope.go @@ -0,0 +1,235 @@ +package kimi + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanQualifier = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) + _ clients.ProfileBindingValidator = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".kimi-code"), "" + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if _, err := kimiProjectRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", err + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope != domain.ScopeProject { + return nil + } + if _, err := kimiProjectRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot); err != nil { + return err + } + if err := currentKimi(in.Client); err != nil { + return err + } + if len(in.Envelope.Skills) > 0 || len(in.Envelope.Commands) > 0 { + return kimiSkillRoot(in.TargetContext.ProjectRoot) + } + return nil +} + +func currentKimi(client domain.DetectedClient) error { + name := filepath.Base(client.ExecutablePath) + if (name != "kimi" && name != "kimi.exe") || !filepath.IsAbs(client.ExecutablePath) || filepath.Clean(client.ExecutablePath) != client.ExecutablePath || client.Version != "2.3.1" { + return fmt.Errorf("kimi project components require the explicitly selected current Kimi Code 2.3.1 executable/version; archived or unknown harnesses are unqualified") + } + return nil +} + +func kimiProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.Scope != domain.ScopeProject { + return "", fmt.Errorf("kimi components require project scope") + } + if validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("kimi original user profile differs from selected target") + } + root := filepath.Join(validated.ProjectRoot, ".kimi-code") + if planned != "" && planned != root { + return "", fmt.Errorf("kimi native registry differs from selected project") + } + return root, pathpolicy.RequireContainedChild(validated.ProjectRoot, root) +} + +func kimiPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject || plan.ClientID != domain.ClientKimi { + return "", fmt.Errorf("kimi project plan scope/client mismatch") + } + root, err := kimiProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + expected := filepath.Join(plan.TargetContext.ProjectRoot, ".uap", "managed", "clients", "kimi", plan.PhysicalArtifactID) + if plan.ActivePath != expected { + return "", fmt.Errorf("kimi managed package differs from selected project/artifact") + } + return root, pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, expected) +} + +// Native skill discovery chooses the nearest existing .git marker. Never move +// selected skills into a different ancestor, or qualify an unknown fallback. +func kimiSkillRoot(project string) error { + root, err := kimiNearestGitRoot(project) + if err != nil { + return err + } + if root == "" { + return fmt.Errorf("kimi project skill discovery requires an observed .git marker at the selected project root") + } + if root != project { + return fmt.Errorf("kimi skills resolve at nearest Git ancestor %s, which differs from selected project", root) + } + return nil +} + +func kimiNearestGitRoot(project string) (string, error) { + for directory := project; ; directory = filepath.Dir(directory) { + marker := filepath.Join(directory, ".git") + info, err := os.Lstat(marker) + if err == nil { + if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) { + return "", fmt.Errorf("kimi nearest Git marker must be a real directory or regular gitfile") + } + return directory, nil + } + if !os.IsNotExist(err) { + return "", err + } + if filepath.Dir(directory) == directory { + return "", nil + } + } +} + +func refineKimiProject(in clients.PlanInput, plan *domain.DeliveryPlan) error { + root, err := kimiPlanRoot(in.Client, *plan) + if err != nil { + return err + } + plan.NativeRegistryRoot, plan.NativeRegistryExecutable = root, "" + if plan.Status == domain.PlanUnsupported || !shared.OnlyNativeComponents(plan.Components) { + return nil + } + plan.Status, plan.Activation, plan.Verification = domain.PlanReady, domain.ActivationPrepared, domain.VerificationPackageValid + plan.UserActions = []string{KimiProjectAction} + compatibilityRoot, err := kimiNearestGitRoot(in.TargetContext.ProjectRoot) + if err != nil { + return err + } + if compatibilityRoot == "" { + compatibilityRoot = in.TargetContext.ProjectRoot + } + for _, path := range []string{filepath.Join(in.Client.ConfigRoot, "mcp.json"), filepath.Join(compatibilityRoot, ".mcp.json")} { + if in.Client.ConfigRoot == "" && !filepath.IsAbs(path) { + continue + } + names, err := kimiConfigNames(path) + if err != nil { + return err + } + for _, name := range shared.SupportedMCPNames(*plan) { + if names[name] { + plan.Warnings = shared.AppendUnique(plan.Warnings, "Kimi selected project MCP overrides a same-name lower-priority user or compatibility entry; effective trust/session loading remains unobserved") + } + } + } + return nil +} + +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil + } + if binding.ClientID != "kimi" { + return fmt.Errorf("kimi recorded project client differs") + } + if binding.NativeProfileRoot != root || filepath.Base(root) != ".kimi-code" { + return fmt.Errorf("kimi recorded project registry differs") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + expected := filepath.Join(filepath.Dir(root), ".uap", "managed", "clients", "kimi", binding.PhysicalArtifact) + if binding.TargetLocator != expected { + return fmt.Errorf("kimi recorded package differs from project/artifact") + } + if err := pathpolicy.RequireContainedChild(filepath.Dir(root), expected); err != nil { + return err + } + return kimiComponents(root).ValidateObjects(kimiNativeObjects(binding.NativeObjects)) +} + +func kimiConfigNames(path string) (map[string]bool, error) { + file, err := readKimiFile(path) + if err != nil { + return nil, err + } + if strings.TrimSpace(string(file)) == "" { + return map[string]bool{}, nil + } + doc, err := shared.DecodeStrictJSONObject(file) + if err != nil { + return nil, err + } + names := map[string]bool{} + raw, present := doc["mcpServers"] + if !present { + return names, nil + } + entries, ok := raw.(map[string]any) + if !ok { + return nil, fmt.Errorf("kimi MCP collection must be an object") + } + for name := range entries { + names[name] = true + } + return names, nil +} + +// Current pinned Kimi supports SSE in the project file. Its preserved user +// plugin capability remains unsupported; qualify only this observed lane. +func (*Adapter) QualifyPlan(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope != domain.ScopeProject { + return nil + } + if err := currentKimi(in.Client); err != nil { + return err + } + for i, component := range plan.Components { + if component.Kind == domain.ComponentMCPServer && in.Envelope.MCP.Servers[component.Name].Type == "sse" { + plan.Components[i].Support, plan.Components[i].Reason = domain.SupportNative, "" + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/kiro/activate.go b/install/integrationctl/agentplugins/clients/kiro/activate.go index 442e95516..2fce10a45 100644 --- a/install/integrationctl/agentplugins/clients/kiro/activate.go +++ b/install/integrationctl/agentplugins/clients/kiro/activate.go @@ -22,6 +22,9 @@ func (*Adapter) AutomaticallyActivates(_ clients.Env, request domain.ActivationR } func automaticallyActivates(request domain.ActivationRequest) bool { + if request.Plan.Scope == domain.ScopeProject { + return request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components) + } if request.Plan.InstallIntent != domain.InstallIntentAutomatic { return false } @@ -36,6 +39,9 @@ func automaticallyActivates(request domain.ActivationRequest) bool { // made for this plan. The executable name is part of the evidence: a backend // that does not look like Kiro cannot prove Kiro state. func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, backendExecutable string) bool { + if plan.Scope == domain.ScopeProject { + return shared.OnlyNativeComponents(plan.Components) + } if strings.TrimSpace(backendExecutable) == "" { return false } @@ -56,6 +62,9 @@ func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryP // PreflightActivation rejects a prepare request that cannot own native objects, // and rejects automatic MCP activation whose runner cannot speak ACP duplex. func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return preflightKiroProject(env, request) + } if request.Plan.InstallIntent == domain.InstallIntentPrepare { if request.Plan.Scope != domain.ScopeUser || strings.TrimSpace(request.Client.ConfigRoot) == "" || !shared.OnlyNativeComponents(request.Plan.Components) { return kiroErrorf("Kiro preparation requires native configuration and supported components") @@ -78,6 +87,9 @@ func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRe // Activate prepares, verifies or installs managed Kiro native objects, then // observes MCP through ACP when the package selected any. func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return activateKiroProject(ctx, env, request) + } outcome := shared.StartedActivation(request) outcome.NativeEffect = domain.NativeEffectUnchanged outcome.NativeObjects = append([]domain.NativeObjectOwnership(nil), request.PreviousNativeObjects...) @@ -184,6 +196,9 @@ func manualKiroVerification(outcome domain.ActivationOutcome, request domain.Act // Deactivate removes managed Kiro native objects, or asks the operator to // finish a leftover custom Power first. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject { + return deactivateKiroProject(ctx, env, request) + } outcome := shared.StartedDeactivation() if len(NativeObjects(request.NativeObjects)) == 0 { return shared.RequireExternalUninstall(outcome, request.ExternalUninstalled, "remove the legacy custom Power in Kiro, then rerun remove with `--external-uninstalled`"), nil diff --git a/install/integrationctl/agentplugins/clients/kiro/identity.go b/install/integrationctl/agentplugins/clients/kiro/identity.go index 1516ae124..f52c27a87 100644 --- a/install/integrationctl/agentplugins/clients/kiro/identity.go +++ b/install/integrationctl/agentplugins/clients/kiro/identity.go @@ -17,10 +17,20 @@ var _ clients.RegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return false } -func (*Adapter) InspectNativeRegistry(ctx context.Context, _ clients.Env, _ domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, _ clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if _, err := kiroProjectPlanRoot(client, plan); err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + if err := validateKiroProjectBinding(plan.NativeRegistryRoot, plan.PhysicalArtifactID, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + } + } return inspectKiroRegistry(plan, managed) } diff --git a/install/integrationctl/agentplugins/clients/kiro/plan.go b/install/integrationctl/agentplugins/clients/kiro/plan.go index 70c52e58d..abc0ddd8d 100644 --- a/install/integrationctl/agentplugins/clients/kiro/plan.go +++ b/install/integrationctl/agentplugins/clients/kiro/plan.go @@ -20,6 +20,9 @@ var ( ) func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject { + return refineProjectPlan(in, plan) + } eligible := shared.OnlyNativeComponents(plan.Components) needsCLI := shared.HasSupportedMCP(plan.Components) hasConfigRoot := strings.TrimSpace(in.Client.ConfigRoot) != "" @@ -39,6 +42,24 @@ func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain // mutating it, which needs a native registry root and a selection Kiro delivers // whole. func (*Adapter) RefinePreparation(plan *domain.DeliveryPlan) error { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" { + if plan.ClientID != domain.ClientKiro { + return fmt.Errorf("kiro project preparation client identity mismatch") + } + if _, err := kiroProjectPlanRoot(domain.DetectedClient{ClientID: domain.ClientKiro, ConfigRoot: plan.TargetContext.UserConfigRoot}, *plan); err != nil { + return err + } + if plan.Status == domain.PlanUnsupported { + return nil + } + if !shared.OnlyNativeComponents(plan.Components) { + return fmt.Errorf("kiro project preparation requires supported native components") + } + plan.Status, plan.Activation, plan.Verification = domain.PlanReady, domain.ActivationPrepared, domain.VerificationPackageValid + plan.UserActions = []string{KiroProjectAction} + return nil + } + if strings.TrimSpace(plan.NativeRegistryRoot) == "" || !shared.OnlyNativeComponents(plan.Components) { return fmt.Errorf("a Kiro preparation requires a native config root and supported skills or MCP servers") } diff --git a/install/integrationctl/agentplugins/clients/kiro/project.go b/install/integrationctl/agentplugins/clients/kiro/project.go index 744772af3..0a3e9861f 100644 --- a/install/integrationctl/agentplugins/clients/kiro/project.go +++ b/install/integrationctl/agentplugins/clients/kiro/project.go @@ -17,12 +17,22 @@ import ( var _ clients.Projector = (*Adapter)(nil) var _ clients.ActiveNativeProjector = (*Adapter)(nil) -func (*Adapter) ProjectActiveNative(_ context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) ProjectActiveNative(_ context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { + if plan.Scope == domain.ScopeProject { + if err := validateKiroProjectStdio(root, envelope, plan, dataPath); err != nil { + return nil, err + } + } return BuildNativeObjects(root, envelope, plan) } // Project writes Kiro's MCP document and records the native objects it owns. func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if in.Plan.Scope == domain.ScopeProject { + if err := validateKiroProjectionRoot(in.StagingPath, in.Plan); err != nil { + return nil, err + } + } if err := shared.ProjectMCPServers(shared.MCPProjection{ Root: in.StagingPath, Envelope: in.Envelope, @@ -33,10 +43,20 @@ func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain }); err != nil { return nil, err } + if in.Plan.Scope == domain.ScopeProject { + if err := projectKiroStdio(in); err != nil { + return nil, err + } + } return BuildNativeObjects(in.StagingPath, in.Envelope, in.Plan) } func BuildNativeObjects(stagingRoot string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + if plan.Scope == domain.ScopeProject { + if err := validateKiroProjectionRoot(stagingRoot, plan); err != nil { + return nil, err + } + } configRoot := strings.TrimSpace(plan.NativeRegistryRoot) if configRoot == "" { return nil, kiroErrorf("Kiro config root is unavailable") @@ -62,6 +82,9 @@ func BuildNativeObjects(stagingRoot string, envelope domain.PackageEnvelope, pla } } sort.Slice(objects, func(i, j int) bool { return objects[i].ObjectID < objects[j].ObjectID }) + if plan.Scope == domain.ScopeProject { + return objects, validateKiroProjectObjects(configRoot, objects) + } return objects, nil } diff --git a/install/integrationctl/agentplugins/clients/kiro/project_effect_test.go b/install/integrationctl/agentplugins/clients/kiro/project_effect_test.go new file mode 100644 index 000000000..9c7d629e8 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kiro/project_effect_test.go @@ -0,0 +1,89 @@ +package kiro + +import ( + "bytes" + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/atomicfile" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func kiroProjectEffectRequest(t *testing.T) domain.ActivationRequest { + t.Helper() + base := t.TempDir() + project, user := filepath.Join(base, "project"), filepath.Join(base, "user", ".kiro") + root := filepath.Join(project, ".kiro") + active, objects := kiroNativeFixture(t, root, "new", "https://managed.test") + target := filepath.Join(project, ".uap", "managed", "clients", "kiro", "demo-0123456789ab") + if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(active, target); err != nil { + t.Fatal(err) + } + request := kiroActivationRequest(root, target, nil, objects) + request.Client.ConfigRoot = user + request.Plan.Scope = domain.ScopeProject + request.Plan.NativeRegistryRoot = root + request.Plan.ActivePath = target + request.Plan.PhysicalArtifactID = "demo-0123456789ab" + var err error + request.Plan.TargetContext, err = domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + return request +} + +func TestProjectKiroVisibleNativeWriteFailureRollsBackAndKeepsOwnership(t *testing.T) { + request := kiroProjectEffectRequest(t) + path := filepath.Join(request.Plan.NativeRegistryRoot, "settings", "mcp.json") + original := []byte(`{"mcpServers":{"foreign":{"url":"https://foreign.test","disabled":true}}}`) + writeTestFile(t, path, string(original)) + writeErr := errors.New("inert visible write failure") + files := &kiroFaultFiles{write: func(path string, body []byte, mode os.FileMode, count int) error { + if err := atomicfile.Write(path, body, mode); err != nil { + return err + } + if count == 1 { + return writeErr + } + return nil + }} + outcome, err := New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.NewWithFileIO(files)}, request) + if !errors.Is(err, writeErr) || outcome.NativeEffect != domain.NativeEffectUnchanged || len(outcome.NativeObjects) != 0 { + t.Fatalf("project rollback=%+v,%v", outcome, err) + } + body, err := os.ReadFile(path) + if err != nil || !bytes.Equal(body, original) || files.writes != 2 { + t.Fatalf("project rollback bytes=%q,%v writes=%d", body, err, files.writes) + } + if _, err := os.Stat(filepath.Join(request.Plan.NativeRegistryRoot, "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("project skill survived failed MCP write: %v", err) + } + if _, err := os.Stat(request.Client.ConfigRoot); !os.IsNotExist(err) { + t.Fatalf("project rollback touched global config: %v", err) + } +} + +func TestProjectKiroCommittedCleanupRetainsPreparedReceipts(t *testing.T) { + request := kiroProjectEffectRequest(t) + releaseErr := errors.New("inert lock cleanup failure") + kernel := nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return func() error { return releaseErr }, nil + }) + outcome, err := New().Activate(context.Background(), clients.Env{NativeConfig: kernel}, request) + if err != nil || outcome.NativeEffect != domain.NativeEffectCommitted || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || len(outcome.NativeObjects) != 2 || !strings.Contains(strings.Join(outcome.UserActions, " "), "cleanup degraded") { + t.Fatalf("project committed cleanup=%+v,%v", outcome, err) + } + if err := VerifyNativeObjects(request.Plan.NativeRegistryRoot, outcome.NativeObjects, false); err != nil { + t.Fatal(err) + } +} diff --git a/install/integrationctl/agentplugins/clients/kiro/project_lifecycle.go b/install/integrationctl/agentplugins/clients/kiro/project_lifecycle.go new file mode 100644 index 000000000..7c9f257b0 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kiro/project_lifecycle.go @@ -0,0 +1,121 @@ +package kiro + +import ( + "context" + "fmt" + "os" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const KiroProjectAction = "Open the selected Kiro workspace, review workspace trust and the active custom agent's includeMcpJson/resources, then inspect skills and MCP connections. Configuration delivery alone does not establish native discovery, IDE behavior, permissions or MCP initialization." + +func preflightKiroProject(env clients.Env, request domain.ActivationRequest) error { + root, err := kiroProjectPlanRoot(request.Client, request.Plan) + if err != nil { + return err + } + if request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath { + return fmt.Errorf("kiro delivered package differs from selected project") + } + if request.Plan.ClientID != request.Client.ClientID { + return fmt.Errorf("kiro project plan client identity mismatch") + } + if request.Delivery.ClientID != "" { + if err := shared.ActivationIdentityMismatch(request); err != nil { + return err + } + } + if request.ActivationComplete { + return fmt.Errorf("kiro project native discovery cannot be established by activation attestation") + } + if !shared.OnlyNativeComponents(request.Plan.Components) { + return fmt.Errorf("kiro project delivery requires supported skills and MCP servers only") + } + if err := validateKiroProjectObjects(root, request.PreviousNativeObjects); err != nil { + return err + } + if err := validateKiroProjectObjects(root, request.Delivery.NativeObjects); err != nil { + return err + } + return env.NativeConfig.RequireFileIO() +} + +func activateKiroProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + outcome := shared.StartedActivation(request) + if err := shared.ActivationIdentityMismatch(request); err != nil { + return outcome, err + } + if request.Delivery.ActivePath != request.Plan.ActivePath { + return outcome, fmt.Errorf("kiro delivered package differs from selected project") + } + outcome.NativeEffect, outcome.NativeObjects = domain.NativeEffectUnchanged, append([]domain.NativeObjectOwnership(nil), request.PreviousNativeObjects...) + if err := preflightKiroProject(env, request); err != nil { + return outcome, err + } + if err := ctx.Err(); err != nil { + return outcome, err + } + root := request.Plan.NativeRegistryRoot + if request.VerifyOnly { + if err := VerifyNativeObjects(root, request.Delivery.NativeObjects, false); err != nil { + return shared.FailedActivation(outcome, "repair managed Kiro workspace configuration", err) + } + outcome.NativeObjects = append([]domain.NativeObjectOwnership(nil), request.Delivery.NativeObjects...) + } else { + effect, err := applyKiroNativeMutationWithKernelAndOps(root, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, env.NativeConfig, shared.RenameDirectoryExclusive, os.RemoveAll) + outcome.NativeEffect = effect + outcome.NativeObjects = kiroProjectEffectObjects(root, effect, request) + if err != nil && !shared.CommittedNativeCleanup(&outcome, err) { + return shared.FailedActivation(outcome, "repair managed Kiro workspace configuration", err) + } + } + outcome.Activation, outcome.Verification = domain.ActivationPrepared, domain.VerificationInstalled + outcome.UserActions = shared.AppendUnique(outcome.UserActions, KiroProjectAction) + return outcome, nil +} + +func kiroProjectEffectObjects(root string, effect domain.NativeEffectState, request domain.ActivationRequest) []domain.NativeObjectOwnership { + if effect == domain.NativeEffectCommitted { + return append([]domain.NativeObjectOwnership(nil), request.Delivery.NativeObjects...) + } + if effect == domain.NativeEffectUnchanged { + return append([]domain.NativeObjectOwnership(nil), request.PreviousNativeObjects...) + } + var retained []domain.NativeObjectOwnership + for _, object := range NativeObjects(request.PreviousNativeObjects) { + if VerifyNativeObjects(root, []domain.NativeObjectOwnership{object}, false) == nil { + retained = append(retained, object) + } + } + return retained +} + +func deactivateKiroProject(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + root, err := kiroProjectRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return outcome, err + } + if err := validateKiroProjectObjects(root, request.NativeObjects); err != nil { + return outcome, err + } + if err := env.NativeConfig.RequireFileIO(); err != nil { + return outcome, err + } + if !request.Confirmed { + outcome.UserActions = []string{"agentplugins will remove its exact owned Kiro workspace skill directories and MCP entries"} + return outcome, nil + } + if err := ctx.Err(); err != nil { + return outcome, err + } + _, err = applyKiroNativeMutationWithKernelAndOps(root, "", request.NativeObjects, nil, env.NativeConfig, shared.RenameDirectoryExclusive, os.RemoveAll) + if err != nil && !shared.CommittedNativeDeactivationCleanup(&outcome, err) { + return outcome, err + } + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/kiro/project_stdio.go b/install/integrationctl/agentplugins/clients/kiro/project_stdio.go new file mode 100644 index 000000000..7ef5bb863 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kiro/project_stdio.go @@ -0,0 +1,87 @@ +package kiro + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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" +) + +// The workspace JSON does not need an undocumented cwd field: the trusted +// launcher establishes the selected package/data cwd before starting stdio. +func projectKiroStdio(in clients.ProjectionInput) error { + if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { + return err + } + servers := map[string]any{} + for _, name := range shared.SupportedMCPNames(in.Plan) { + entry, err := projectedKiroMCPServer(in.StagingPath, name) + if err != nil { + return err + } + delete(entry, "type") // Kiro selects local/remote from command/url, not portable transport tags. + source := in.Envelope.MCP.Servers[name] + if source.Type == "stdio" { + if err := bindKiroProjectStdio(entry, source, in.Plan.ActivePath, in.PluginDataPath); err != nil { + return err + } + } + servers[name] = entry + } + if len(servers) == 0 { + return nil + } + return shared.WriteJSON(filepath.Join(in.StagingPath, "mcp.json"), map[string]any{"mcpServers": servers}) +} + +func bindKiroProjectStdio(entry map[string]any, source domain.MCPServer, packageRoot, dataRoot string) error { + if !filepath.IsAbs(packageRoot) || !filepath.IsAbs(dataRoot) { + return fmt.Errorf("kiro stdio requires absolute package/data roots") + } + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, packageRoot, dataRoot) + if err != nil { + return err + } + absoluteCWD, ok := entry["cwd"].(string) + if !ok { + return fmt.Errorf("kiro projected stdio cwd is missing") + } + args, err := kiroProjectArguments(entry) + if err != nil { + return err + } + command, _ := source.Decoded["command"].(string) + if strings.HasPrefix(command, "${package.root}/") { + command = "./" + strings.TrimPrefix(command, "${package.root}/") + } + entry["command"] = filepath.Join(packageRoot, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + entry["args"] = managedstdio.Arguments(packageRoot, dataRoot, absoluteCWD, cwd.Anchor, command, args) + delete(entry, "cwd") + return nil +} + +func kiroProjectArguments(entry map[string]any) ([]string, error) { + raw, present := entry["args"] + if !present { + return nil, nil + } + array, ok := raw.([]any) + if !ok { + return nil, fmt.Errorf("kiro projected stdio args must be an array") + } + result := make([]string, len(array)) + for i, value := range array { + text, ok := value.(string) + if !ok { + return nil, fmt.Errorf("kiro stdio args must contain strings") + } + result[i] = text + } + return result, nil +} diff --git a/install/integrationctl/agentplugins/clients/kiro/project_stdio_verify.go b/install/integrationctl/agentplugins/clients/kiro/project_stdio_verify.go new file mode 100644 index 000000000..e56e00947 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kiro/project_stdio_verify.go @@ -0,0 +1,67 @@ +package kiro + +import ( + "fmt" + "path/filepath" + "slices" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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 validateKiroProjectStdio(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) error { + if root != plan.ActivePath { + return fmt.Errorf("kiro active reprojection differs from selected package") + } + if err := validateKiroProjectionRoot(root, plan); err != nil { + return err + } + for _, name := range shared.SupportedMCPNames(plan) { + source := envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + if !filepath.IsAbs(dataPath) || filepath.Clean(dataPath) != dataPath { + return fmt.Errorf("%w: Kiro selected data locator must be clean and absolute", domain.ErrProjectionRefreshRequired) + } + entry, err := projectedKiroMCPServer(root, name) + if err != nil { + return err + } + if err := verifyKiroStdioBinding(root, entry, source, plan.ActivePath, dataPath); err != nil { + return err + } + } + return nil +} + +func verifyKiroStdioBinding(observed string, entry map[string]any, source domain.MCPServer, packageRoot, dataRoot string) error { + expected := filepath.Join(packageRoot, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + env, ok := entry["env"].(map[string]any) + if !ok || env["PLUGIN_ROOT"] != packageRoot || env["PLUGIN_DATA"] != dataRoot || entry["command"] != expected { + return fmt.Errorf("%w: Kiro stdio package/data binding differs", domain.ErrProjectionRefreshRequired) + } + if _, present := entry["cwd"]; present { + return fmt.Errorf("%w: Kiro launcher cannot depend on a native cwd field", domain.ErrProjectionRefreshRequired) + } + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, packageRoot, dataRoot) + if err != nil { + return err + } + _, resolved, err := shared.ResolveStdioPaths("", rawCWD, packageRoot, dataRoot, observed) + if err != nil { + return err + } + args, err := kiroProjectArguments(entry) + if err != nil { + return err + } + prefix := managedstdio.Arguments(packageRoot, dataRoot, resolved, cwd.Anchor, "", nil)[:6] + if len(args) < 7 || !slices.Equal(args[:6], prefix) { + return fmt.Errorf("%w: Kiro launcher roots or cwd differ", domain.ErrProjectionRefreshRequired) + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/kiro/project_validation.go b/install/integrationctl/agentplugins/clients/kiro/project_validation.go new file mode 100644 index 000000000..a9bc5130d --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kiro/project_validation.go @@ -0,0 +1,83 @@ +package kiro + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ProfileBindingValidator = (*Adapter)(nil) + +// The user lane retains its existing registry rules. Project ownership is +// explicit and cannot be inferred from a detected global profile. +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil + } + return validateKiroProjectBinding(root, binding.PhysicalArtifact, binding) +} + +func validateKiroProjectionRoot(root string, plan domain.DeliveryPlan) error { + if _, err := kiroProjectPlanRoot(domain.DetectedClient{ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return err + } + if !filepath.IsAbs(root) || filepath.Clean(root) != root || strings.TrimSpace(root) != root { + return fmt.Errorf("kiro project package path must be clean and absolute") + } + return pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, root) +} + +func validateKiroProjectBinding(root, artifact string, binding domain.ClientBinding) error { + if !filepath.IsAbs(root) || filepath.Clean(root) != root || filepath.Base(root) != ".kiro" { + return fmt.Errorf("kiro project registry must be an exact absolute root") + } + if binding.Scope != string(domain.ScopeProject) || binding.NativeProfileRoot != root || binding.PhysicalArtifact != artifact { + return fmt.Errorf("kiro binding scope, artifact or native root differs from selected project") + } + if err := pathpolicy.ValidateLeafID(artifact); err != nil { + return err + } + project := filepath.Dir(root) + expected := filepath.Join(project, ".uap", "managed", "clients", "kiro", artifact) + if binding.TargetLocator != expected { + return fmt.Errorf("kiro binding package differs from selected project") + } + if err := pathpolicy.RequireContainedChild(project, expected); err != nil { + return err + } + return validateKiroProjectObjects(root, binding.NativeObjects) +} + +func validateKiroProjectObjects(root string, objects []domain.NativeObjectOwnership) error { + // Reuse portable path/duplicate/digest validation without changing Kiro's + // existing persisted receipt IDs or its exact JSONC transaction codec. + normalized := make([]domain.NativeObjectOwnership, 0, len(objects)) + for _, object := range objects { + var prefix string + switch object.Kind { + case SkillObjectKind: + prefix = "kiro-skill:" + case MCPObjectKind: + prefix = "kiro-mcp:" + case "managed_package_directory": + continue + default: + return fmt.Errorf("unexpected Kiro project native object kind %q", object.Kind) + } + if object.ObjectID != prefix+object.LogicalName { + return fmt.Errorf("kiro project receipt identity differs from its logical name") + } + object.ObjectID = object.Kind + ":" + object.LogicalName + normalized = append(normalized, object) + } + layout := shared.ProjectComponents{ProjectRoot: filepath.Dir(root), SkillsRoot: filepath.Join(root, "skills"), + ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, "settings", "mcp.json")}, Codec: nativeconfig.CodecMCPServers, + SkillKind: SkillObjectKind, MCPKind: MCPObjectKind} + return layout.ValidateObjects(normalized) +} diff --git a/install/integrationctl/agentplugins/clients/kiro/scope.go b/install/integrationctl/agentplugins/clients/kiro/scope.go new file mode 100644 index 000000000..6706bec9a --- /dev/null +++ b/install/integrationctl/agentplugins/clients/kiro/scope.go @@ -0,0 +1,92 @@ +package kiro + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".kiro"), "" + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if _, err := kiroProjectRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", err + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope != domain.ScopeProject { + return nil + } + _, err := kiroProjectRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot) + return err +} + +func kiroProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.Scope != domain.ScopeProject { + return "", fmt.Errorf("kiro workspace configuration requires project scope") + } + if validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("kiro detected user profile differs from selected target") + } + root := filepath.Join(validated.ProjectRoot, ".kiro") + if planned != "" && planned != root { + return "", fmt.Errorf("kiro native registry differs from selected project") + } + return root, pathpolicy.RequireContainedChild(validated.ProjectRoot, root) +} + +func kiroProjectPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return "", fmt.Errorf("kiro project lifecycle requires matching project scope") + } + root, err := kiroProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + expected := filepath.Join(plan.TargetContext.ProjectRoot, ".uap", "managed", "clients", "kiro", plan.PhysicalArtifactID) + if plan.ActivePath != expected { + return "", fmt.Errorf("kiro managed package differs from selected project") + } + return root, pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, expected) +} + +func refineProjectPlan(in clients.PlanInput, plan *domain.DeliveryPlan) error { + root, err := kiroProjectPlanRoot(in.Client, *plan) + if err != nil { + return err + } + plan.NativeRegistryRoot, plan.NativeRegistryExecutable = root, "" + if plan.Status == domain.PlanUnsupported || !shared.OnlyNativeComponents(plan.Components) { + return nil + } + plan.Status, plan.Activation, plan.Verification = domain.PlanReady, domain.ActivationPrepared, domain.VerificationPackageValid + plan.UserActions = []string{KiroProjectAction} + return nil +} diff --git a/install/integrationctl/agentplugins/clients/opencode/activate.go b/install/integrationctl/agentplugins/clients/opencode/activate.go index ad3fa28e8..ae4f04225 100644 --- a/install/integrationctl/agentplugins/clients/opencode/activate.go +++ b/install/integrationctl/agentplugins/clients/opencode/activate.go @@ -17,8 +17,18 @@ func (*Adapter) AutomaticallyActivates(_ clients.Env, request domain.ActivationR func automaticallyActivatesOpenCode(request domain.ActivationRequest) bool { // Removing the final declarations still reconciles owned native objects. // Keep empty first installs manual and preserve non-native plan restrictions. - cleanup := request.Client.ConfigRoot != "" && len(request.Plan.Components) == 0 && len(OpenCodeObjects(request.PreviousNativeObjects)) > 0 - return request.Plan.InstallIntent == domain.InstallIntentAutomatic && (shared.HasNativeConfigRoot(request) || cleanup) + root := request.Client.ConfigRoot + native := shared.HasNativeConfigRoot(request) + if request.Plan.Scope == domain.ScopeProject { + layout, err := openCodeActivationLayout(request) + if err != nil { + return false + } + root = layout.Root + native = root != "" && shared.OnlyNativeComponents(request.Plan.Components) + } + cleanup := root != "" && len(request.Plan.Components) == 0 && len(OpenCodeObjects(request.PreviousNativeObjects)) > 0 + return request.Plan.InstallIntent == domain.InstallIntentAutomatic && (native || cleanup) } // VerifierAvailable reports that an exact native configuration listing can be @@ -29,7 +39,7 @@ func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryP // Activate installs or verifies managed OpenCode skills and MCP servers // through the native config kernel. -func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { +func (adapter *Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { if !request.VerifyOnly && request.Client.OpenCodeHost != nil { if err := request.Client.OpenCodeHost.ValidateNative(false, nil); err != nil { return domain.ActivationOutcome{}, err @@ -38,6 +48,13 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } + layout, err := openCodeActivationLayout(request) + if err != nil { + return domain.ActivationOutcome{}, err + } + if err := adapter.PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } automatic := automaticallyActivatesOpenCode(request) outcome, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ Automatic: automatic, @@ -46,7 +63,7 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac RetryAction: "retry the managed OpenCode native installation", CompletedAction: "restart OpenCode to load the installed plugin", Verify: func() error { - return VerifyOpenCodeNativeObjects(request.Client.ConfigRoot, request.Delivery.ActivePath, request.Delivery.NativeObjects, env.NativeConfig, false) + return verifyOpenCodeNativeLayout(layout, request.Delivery.ActivePath, request.Delivery.NativeObjects, env.NativeConfig, false) }, Activate: func(ctx context.Context, request domain.ActivationRequest) error { if crossOpenCodeDialect(request.PreviousNativeObjects, request.Delivery.NativeObjects) { @@ -55,12 +72,18 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac return ActivateOpenCodeNativeWithKernel(ctx, request, env.NativeConfig) }, }) - return outcome, err + if err != nil { + return outcome, err + } + return openCodeProjectStatus(request, outcome), nil } // Deactivate removes managed OpenCode native objects. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { outcome := shared.StartedDeactivation() + if _, err := openCodeTargetLayout(request.Client, request.TargetContext, request.NativeRegistryRoot); err != nil { + return outcome, err + } if !request.Confirmed { outcome.UserActions = append(outcome.UserActions, "agentplugins will remove its managed OpenCode skills and MCP entries automatically") return outcome, nil diff --git a/install/integrationctl/agentplugins/clients/opencode/identity.go b/install/integrationctl/agentplugins/clients/opencode/identity.go index 1666721f5..b6da07c1b 100644 --- a/install/integrationctl/agentplugins/clients/opencode/identity.go +++ b/install/integrationctl/agentplugins/clients/opencode/identity.go @@ -2,6 +2,7 @@ package opencode import ( "context" + "fmt" "os" "path/filepath" "strings" @@ -27,10 +28,16 @@ func InspectOpenCodeRegistry(plan domain.DeliveryPlan, managed *domain.ClientBin if managed == nil && !hasPlannedOpenCodeNative(plan) { return clients.RegistryClear, nil } - root := strings.TrimSpace(plan.NativeRegistryRoot) - if root == "" { + if strings.TrimSpace(plan.NativeRegistryRoot) == "" { return clients.RegistryIndeterminate, nil } + layout, err := openCodePlanLayout(plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if err := validateOpenCodeManagedContext(layout, managed); err != nil { + return clients.RegistryIndeterminate, err + } codec, err := observedOpenCodeCodec(plan, managed) if err != nil { return clients.RegistryIndeterminate, err @@ -40,7 +47,7 @@ func InspectOpenCodeRegistry(plan domain.DeliveryPlan, managed *domain.ClientBin if component.Support == domain.SupportUnsupported { continue } - exists, owned, err := inspectOpenCodeComponent(root, managed, component, kernel, codec) + exists, owned, err := inspectOpenCodeComponent(layout, managed, component, kernel, codec) if err != nil { return clients.RegistryIndeterminate, err } @@ -84,22 +91,23 @@ func observedOpenCodeCodec(plan domain.DeliveryPlan, managed *domain.ClientBindi return nativeconfig.CodecOpenCode, nil } -func inspectOpenCodeComponent(root string, managed *domain.ClientBinding, component domain.ComponentDecision, kernel nativeconfig.Kernel, codec nativeconfig.Codec) (bool, bool, error) { +func inspectOpenCodeComponent(layout openCodeNativeLayout, managed *domain.ClientBinding, component domain.ComponentDecision, kernel nativeconfig.Kernel, codec nativeconfig.Codec) (bool, bool, error) { switch component.Kind { case domain.ComponentSkill: - return inspectOpenCodeSkillComponent(root, managed, component.Name) + return inspectOpenCodeSkillComponent(layout, managed, component.Name) case domain.ComponentMCPServer: - return inspectOpenCodeMCPComponent(root, managed, component.Name, kernel, codec) + return inspectOpenCodeMCPComponent(layout, managed, component.Name, kernel, codec) default: return false, false, nil } } -func inspectOpenCodeSkillComponent(root string, managed *domain.ClientBinding, name string) (bool, bool, error) { +func inspectOpenCodeSkillComponent(layout openCodeNativeLayout, managed *domain.ClientBinding, name string) (bool, bool, error) { if err := pathpolicy.ValidateLeafID(name); err != nil { return false, false, err } - path := filepath.Join(root, "skills", name) + root := layout.Root + path := filepath.Join(layout.Skills, name) if err := pathpolicy.RequireContainedChild(root, path); err != nil { return false, false, err } @@ -111,7 +119,8 @@ func inspectOpenCodeSkillComponent(root string, managed *domain.ClientBinding, n return err == nil, owned, nil } -func inspectOpenCodeMCPComponent(root string, managed *domain.ClientBinding, name string, kernel nativeconfig.Kernel, codec nativeconfig.Codec) (bool, bool, error) { +func inspectOpenCodeMCPComponent(layout openCodeNativeLayout, managed *domain.ClientBinding, name string, kernel nativeconfig.Kernel, codec nativeconfig.Codec) (bool, bool, error) { + root := layout.Root paths := nativeconfig.Paths{JSON: filepath.Join(root, "opencode.json"), JSONC: filepath.Join(root, "opencode.jsonc")} if err := pathpolicy.RequireContainedChild(root, paths.JSON); err != nil { return false, false, err @@ -127,7 +136,7 @@ func inspectOpenCodeMCPComponent(root string, managed *domain.ClientBinding, nam return false, false, err } if mcp && object.LogicalName == name { - if err := validateOpenCodeObject(root, OpenCodeProjection{}, object); err != nil { + if err := validateOpenCodeObject(layout, OpenCodeProjection{}, object); err != nil { return false, false, err } codec = stored @@ -150,3 +159,18 @@ func managedOpenCodeObjectExists(objects []domain.NativeObjectOwnership, kind, n } return false } + +func validateOpenCodeManagedContext(layout openCodeNativeLayout, managed *domain.ClientBinding) error { + if managed == nil || layout.Scope != domain.ScopeProject { + return nil + } + if managed.Scope != string(domain.ScopeProject) || managed.NativeProfileRoot != layout.Root { + return fmt.Errorf("opencode ownership differs from the selected project") + } + for _, object := range OpenCodeObjects(managed.NativeObjects) { + if err := validateOpenCodeObject(layout, OpenCodeProjection{}, object); err != nil { + return err + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/opencode/native_apply.go b/install/integrationctl/agentplugins/clients/opencode/native_apply.go index d81a8b8da..c7c49993b 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_apply.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_apply.go @@ -18,14 +18,22 @@ func ActivateOpenCodeNativeWithKernel(ctx context.Context, request domain.Activa if err := ctx.Err(); err != nil { return err } - return applyOpenCodeNativeWithKernel(request.Client.ConfigRoot, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, kernel) + layout, err := openCodeActivationLayout(request) + if err != nil { + return err + } + return applyOpenCodeNativeLayoutWithKernel(layout, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, kernel) } func DeactivateOpenCodeNativeWithKernel(ctx context.Context, request domain.DeactivationRequest, kernel nativeconfig.Kernel) error { if err := ctx.Err(); err != nil { return err } - return applyOpenCodeNativeWithKernel(request.Client.ConfigRoot, "", request.NativeObjects, nil, kernel) + layout, err := openCodeTargetLayout(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return err + } + return applyOpenCodeNativeLayoutWithKernel(layout, "", request.NativeObjects, nil, kernel) } func ApplyOpenCodeNative(configRoot, activePath string, previous, desired []domain.NativeObjectOwnership) error { @@ -33,7 +41,11 @@ func ApplyOpenCodeNative(configRoot, activePath string, previous, desired []doma } func applyOpenCodeNativeWithKernel(configRoot, activePath string, previous, desired []domain.NativeObjectOwnership, kernel nativeconfig.Kernel) error { - return applyOpenCodeNativeWithKernelAndOps(configRoot, activePath, previous, desired, kernel, shared.RenameDirectoryExclusive, os.RemoveAll) + return applyOpenCodeNativeLayoutWithKernel(userOpenCodeLayout(configRoot), activePath, previous, desired, kernel) +} + +func applyOpenCodeNativeLayoutWithKernel(layout openCodeNativeLayout, activePath string, previous, desired []domain.NativeObjectOwnership, kernel nativeconfig.Kernel) error { + return applyOpenCodeNativeLayoutWithOps(layout, activePath, previous, desired, kernel, shared.RenameDirectoryExclusive, os.RemoveAll) } func ApplyOpenCodeNativeWithRename(configRoot, activePath string, previous, desired []domain.NativeObjectOwnership, rename openCodeRenameFunc) (resultErr error) { @@ -45,7 +57,11 @@ func ApplyOpenCodeNativeWithOps(configRoot, activePath string, previous, desired } func applyOpenCodeNativeWithKernelAndOps(configRoot, activePath string, previous, desired []domain.NativeObjectOwnership, kernel nativeconfig.Kernel, rename openCodeRenameFunc, removeAll func(string) error) (resultErr error) { - prepared, err := prepareOpenCodeNativeApply(configRoot, activePath, previous, desired, kernel, rename, removeAll) + return applyOpenCodeNativeLayoutWithOps(userOpenCodeLayout(configRoot), activePath, previous, desired, kernel, rename, removeAll) +} + +func applyOpenCodeNativeLayoutWithOps(layout openCodeNativeLayout, activePath string, previous, desired []domain.NativeObjectOwnership, kernel nativeconfig.Kernel, rename openCodeRenameFunc, removeAll func(string) error) (resultErr error) { + prepared, err := prepareOpenCodeNativeApplyLayout(layout, activePath, previous, desired, kernel, rename, removeAll) if err != nil { return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} } @@ -53,7 +69,7 @@ func applyOpenCodeNativeWithKernelAndOps(configRoot, activePath string, previous if err != nil { return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} } - skills, err := installOpenCodeSkillsWithOps(configRoot, activePath, prepared.previous, prepared.desired, prepared.rename, prepared.removeAll) + skills, err := installOpenCodeSkillsWithLayout(layout, activePath, prepared.previous, prepared.desired, prepared.rename, prepared.removeAll) if err != nil { return err } @@ -77,6 +93,7 @@ func knownOpenCodePreWriteError(err error) bool { } type openCodeNativeApply struct { + layout openCodeNativeLayout rename openCodeRenameFunc removeAll func(string) error kernel nativeconfig.Kernel @@ -86,6 +103,14 @@ type openCodeNativeApply struct { } func prepareOpenCodeNativeApply(configRoot, activePath string, previous, desired []domain.NativeObjectOwnership, kernel nativeconfig.Kernel, rename openCodeRenameFunc, removeAll func(string) error) (*openCodeNativeApply, error) { + return prepareOpenCodeNativeApplyLayout(userOpenCodeLayout(configRoot), activePath, previous, desired, kernel, rename, removeAll) +} + +func prepareOpenCodeNativeApplyLayout(layout openCodeNativeLayout, activePath string, previous, desired []domain.NativeObjectOwnership, kernel nativeconfig.Kernel, rename openCodeRenameFunc, removeAll func(string) error) (*openCodeNativeApply, error) { + configRoot := layout.Root + if err := layout.validate(); err != nil { + return nil, err + } if err := kernel.RequireFileIO(); err != nil { return nil, err } @@ -106,20 +131,20 @@ func prepareOpenCodeNativeApply(configRoot, activePath string, previous, desired if err != nil { return nil, err } - if err := validateOpenCodeProjection(configRoot, activePath, projection); err != nil { + if err := validateOpenCodeProjection(layout, activePath, projection); err != nil { return nil, err } } - if err := preflightOpenCodeNativeApply(configRoot, activePath, projection, previous, desired); err != nil { + if err := preflightOpenCodeNativeApply(layout, activePath, projection, previous, desired); err != nil { return nil, err } if err := confirmOpenCodeConfigSelection(configRoot, projection, previous, desired); err != nil { return nil, err } - return &openCodeNativeApply{rename: rename, removeAll: removeAll, kernel: kernel, projection: projection, previous: previous, desired: desired}, nil + return &openCodeNativeApply{layout: layout, rename: rename, removeAll: removeAll, kernel: kernel, projection: projection, previous: previous, desired: desired}, nil } -func preflightOpenCodeNativeApply(configRoot, activePath string, projection OpenCodeProjection, previous, desired []domain.NativeObjectOwnership) error { +func preflightOpenCodeNativeApply(layout openCodeNativeLayout, activePath string, projection OpenCodeProjection, previous, desired []domain.NativeObjectOwnership) error { if crossOpenCodeDialect(previous, desired) { candidate := &openCodeNativeApply{projection: projection, previous: previous, desired: desired} if _, err := openCodeTransitionRequest(candidate); err != nil { @@ -127,15 +152,15 @@ func preflightOpenCodeNativeApply(configRoot, activePath string, projection Open } for _, objects := range [][]domain.NativeObjectOwnership{previous, desired} { for _, object := range objects { - if err := validateOpenCodeObject(configRoot, projection, object); err != nil { + if err := validateOpenCodeObject(layout, projection, object); err != nil { return err } } } - if err := preflightDesiredOpenCodeObjects(configRoot, projection, shared.ObjectMap(previous), shared.ObjectMap(desired)); err != nil { + if err := preflightDesiredOpenCodeObjects(layout, projection, shared.ObjectMap(previous), shared.ObjectMap(desired)); err != nil { return err } - } else if err := preflightOpenCodeObjects(configRoot, activePath, projection, previous, desired); err != nil { + } else if err := preflightOpenCodeObjects(layout, activePath, projection, previous, desired); err != nil { return err } return nil diff --git a/install/integrationctl/agentplugins/clients/opencode/native_mcp.go b/install/integrationctl/agentplugins/clients/opencode/native_mcp.go index 14dc18c09..54b79eefd 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_mcp.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_mcp.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "sort" + "strings" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" @@ -15,6 +16,14 @@ import ( ) func VerifyOpenCodeNativeObjects(configRoot, activePath string, objects []domain.NativeObjectOwnership, kernel nativeconfig.Kernel, allowMissing bool) error { + return verifyOpenCodeNativeLayout(userOpenCodeLayout(configRoot), activePath, objects, kernel, allowMissing) +} + +func verifyOpenCodeNativeLayout(layout openCodeNativeLayout, activePath string, objects []domain.NativeObjectOwnership, kernel nativeconfig.Kernel, allowMissing bool) error { + configRoot := layout.Root + if err := layout.validate(); err != nil { + return err + } if err := kernel.RequireFileIO(); err != nil { return err } @@ -25,20 +34,20 @@ func VerifyOpenCodeNativeObjects(configRoot, activePath string, objects []domain if err != nil { return err } - if err := validateOpenCodeProjection(configRoot, activePath, projection); err != nil { + if err := validateOpenCodeProjection(layout, activePath, projection); err != nil { return err } } for _, object := range OpenCodeObjects(objects) { - if err := verifyOpenCodeObject(configRoot, projection, object, kernel, allowMissing); err != nil { + if err := verifyOpenCodeObject(layout, projection, object, kernel, allowMissing); err != nil { return err } } return nil } -func verifyOpenCodeObject(configRoot string, projection OpenCodeProjection, object domain.NativeObjectOwnership, kernel nativeconfig.Kernel, allowMissing bool) error { - if err := validateOpenCodeObject(configRoot, projection, object); err != nil { +func verifyOpenCodeObject(layout openCodeNativeLayout, projection OpenCodeProjection, object domain.NativeObjectOwnership, kernel nativeconfig.Kernel, allowMissing bool) error { + if err := validateOpenCodeObject(layout, projection, object); err != nil { return err } if object.Kind == openCodeSkillKind { @@ -76,7 +85,8 @@ func verifyOpenCodeMCP(projection OpenCodeProjection, object domain.NativeObject return nil } -func validateOpenCodeProjection(configRoot, activePath string, projection OpenCodeProjection) error { +func validateOpenCodeProjection(layout openCodeNativeLayout, activePath string, projection OpenCodeProjection) error { + configRoot := layout.Root if !shared.SameCleanPath(projection.ConfigJSON, filepath.Join(configRoot, "opencode.json")) || !shared.SameCleanPath(projection.ConfigJSONC, filepath.Join(configRoot, "opencode.jsonc")) || !shared.SameCleanPath(projection.PackageRoot, activePath) { @@ -243,24 +253,47 @@ func openCodeMCPDesiredRequests(kernel nativeconfig.Kernel, paths nativeconfig.P return result, nil } -func validateOpenCodeObject(configRoot string, projection OpenCodeProjection, object domain.NativeObjectOwnership) error { +func validateOpenCodeObject(layout openCodeNativeLayout, projection OpenCodeProjection, object domain.NativeObjectOwnership) error { if object.LogicalName == "" || object.ManagedDigest == "" { return fmt.Errorf("OpenCode native object is incomplete") } if _, _, err := nativeconfig.OpenCodeCodecForKind(object.Kind); err != nil { return err } - expected := expectedOpenCodeObjectPath(configRoot, projection, object) + if layout.Scope == domain.ScopeProject { + if err := validateProjectOpenCodeObjectIdentity(object); err != nil { + return err + } + } + expected := expectedOpenCodeObjectPath(layout, projection, object) if expected == "" { return fmt.Errorf("unsupported OpenCode native object kind %q", object.Kind) } if !shared.SameCleanPath(expected, object.Path) { return fmt.Errorf("OpenCode native object %q has an untrusted path", object.LogicalName) } - return pathpolicy.RequireContainedChild(configRoot, object.Path) + return pathpolicy.RequireContainedChild(layout.Root, object.Path) +} + +func validateProjectOpenCodeObjectIdentity(object domain.NativeObjectOwnership) error { + prefix := "opencode-mcp:" + if object.Kind == openCodeSkillKind { + if object.LogicalName != strings.TrimSpace(object.LogicalName) { + return fmt.Errorf("opencode project skill name is padded") + } + if err := pathpolicy.ValidateLeafID(object.LogicalName); err != nil { + return err + } + prefix = "opencode-skill:" + } + if object.ObjectID != prefix+object.LogicalName || !filepath.IsAbs(object.Path) || filepath.Clean(object.Path) != object.Path { + return fmt.Errorf("opencode project object identity is not exact") + } + return nil } -func expectedOpenCodeObjectPath(configRoot string, projection OpenCodeProjection, object domain.NativeObjectOwnership) string { +func expectedOpenCodeObjectPath(layout openCodeNativeLayout, projection OpenCodeProjection, object domain.NativeObjectOwnership) string { + configRoot := layout.Root switch object.Kind { case OpenCodeMCPObjectKind, OpenCodeV2MCPObjectKind: if projection.ConfigPath != "" { @@ -271,32 +304,32 @@ func expectedOpenCodeObjectPath(configRoot string, projection OpenCodeProjection } return "" case openCodeSkillKind: - return filepath.Join(configRoot, "skills", object.LogicalName) + return filepath.Join(layout.Skills, object.LogicalName) default: return "" } } -func preflightOpenCodeObjects(configRoot, activePath string, projection OpenCodeProjection, previous, desired []domain.NativeObjectOwnership) error { +func preflightOpenCodeObjects(layout openCodeNativeLayout, activePath string, projection OpenCodeProjection, previous, desired []domain.NativeObjectOwnership) error { if err := preflightOpenCodeCodecs(projection, previous, desired); err != nil { return err } previousByID, desiredByID := shared.ObjectMap(previous), shared.ObjectMap(desired) for _, object := range previous { - if err := validateOpenCodeObject(configRoot, projection, object); err != nil { + if err := validateOpenCodeObject(layout, projection, object); err != nil { return err } } - if err := preflightDesiredOpenCodeObjects(configRoot, projection, previousByID, desiredByID); err != nil { + if err := preflightDesiredOpenCodeObjects(layout, projection, previousByID, desiredByID); err != nil { return err } _ = activePath return nil } -func preflightDesiredOpenCodeObjects(configRoot string, projection OpenCodeProjection, previousByID, desiredByID map[string]domain.NativeObjectOwnership) error { +func preflightDesiredOpenCodeObjects(layout openCodeNativeLayout, projection OpenCodeProjection, previousByID, desiredByID map[string]domain.NativeObjectOwnership) error { for id, object := range desiredByID { - if err := validateOpenCodeObject(configRoot, projection, object); err != nil { + if err := validateOpenCodeObject(layout, projection, object); err != nil { return err } if prior, replacing := previousByID[id]; replacing { diff --git a/install/integrationctl/agentplugins/clients/opencode/native_project.go b/install/integrationctl/agentplugins/clients/opencode/native_project.go index adaca6657..42518b599 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_project.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_project.go @@ -26,7 +26,11 @@ func ProjectOpenCodeNative(root string, envelope domain.PackageEnvelope, plan do if empty { return rejectOpenCodeProjectionCollision(root) } - configRoot := strings.TrimSpace(plan.NativeRegistryRoot) + layout, err := openCodePlanLayout(plan) + if err != nil { + return err + } + configRoot := layout.Root if configRoot == "" || !filepath.IsAbs(configRoot) { return fmt.Errorf("OpenCode config root is unavailable") } @@ -327,7 +331,11 @@ func openCodeSkillOwnership(stagingRoot string, envelope domain.PackageEnvelope, if err != nil { return domain.NativeObjectOwnership{}, err } - target := filepath.Join(plan.NativeRegistryRoot, "skills", component.Name) + layout, err := openCodePlanLayout(plan) + if err != nil { + return domain.NativeObjectOwnership{}, err + } + target := filepath.Join(layout.Skills, component.Name) if err := pathpolicy.RequireContainedChild(plan.NativeRegistryRoot, target); err != nil { return domain.NativeObjectOwnership{}, err } diff --git a/install/integrationctl/agentplugins/clients/opencode/native_skills.go b/install/integrationctl/agentplugins/clients/opencode/native_skills.go index f190c1bbc..194df4c41 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_skills.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_skills.go @@ -33,7 +33,7 @@ func renameOpenCodeDirectoryNoReplace(oldPath, newPath string, rename openCodeRe return rename(oldPath, newPath) } -func installOpenCodeSkillsWithOps(configRoot, activePath string, previous, desired []domain.NativeObjectOwnership, rename openCodeRenameFunc, removeAll func(string) error) (*openCodeSkillTxn, error) { +func installOpenCodeSkillsWithLayout(layout openCodeNativeLayout, activePath string, previous, desired []domain.NativeObjectOwnership, rename openCodeRenameFunc, removeAll func(string) error) (*openCodeSkillTxn, error) { if rename == nil { return nil, fmt.Errorf("OpenCode rename operation is unavailable") } @@ -44,7 +44,7 @@ func installOpenCodeSkillsWithOps(configRoot, activePath string, previous, desir if !containsOpenCodeSkill(previous) && !containsOpenCodeSkill(desired) { return txn, nil } - if err := txn.createRoot(configRoot); err != nil { + if err := txn.createLayoutRoot(layout); err != nil { return nil, err } if err := txn.stageAndSwap(activePath, previous, desired); err != nil { @@ -54,7 +54,11 @@ func installOpenCodeSkillsWithOps(configRoot, activePath string, previous, desir } func (txn *openCodeSkillTxn) createRoot(configRoot string) error { - skillsRoot := filepath.Join(configRoot, "skills") + return txn.createLayoutRoot(userOpenCodeLayout(configRoot)) +} + +func (txn *openCodeSkillTxn) createLayoutRoot(layout openCodeNativeLayout) error { + configRoot, skillsRoot := layout.Root, layout.Skills if err := pathpolicy.RequireContainedChild(configRoot, skillsRoot); err != nil { return err } @@ -67,7 +71,12 @@ func (txn *openCodeSkillTxn) createRoot(configRoot string) error { } txn.root = root if txn.durable { - if err := atomicfile.SyncDirectory(configRoot); err != nil { + if layout.Scope == domain.ScopeProject { + if err := atomicfile.SyncDirectory(configRoot); err != nil { + return err + } + } + if err := atomicfile.SyncDirectory(filepath.Dir(skillsRoot)); err != nil { return err } return atomicfile.SyncDirectory(skillsRoot) diff --git a/install/integrationctl/agentplugins/clients/opencode/native_transition.go b/install/integrationctl/agentplugins/clients/opencode/native_transition.go index 6a56fd211..e9976a8ca 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_transition.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_transition.go @@ -6,7 +6,6 @@ import ( "fmt" "io/fs" "os" - "path/filepath" "runtime" "sort" @@ -135,7 +134,11 @@ func applyOpenCodeTransition(ctx context.Context, request domain.ActivationReque if recorder == nil || request.NativeAttempt.OperationID == "" { return unchanged(fmt.Errorf("durable native transition authority is required")) } - p, err := prepareOpenCodeNativeApply(request.Client.ConfigRoot, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, kernel, shared.RenameDirectoryExclusive, os.RemoveAll) + layout, err := openCodeActivationLayout(request) + if err != nil { + return unchanged(err) + } + p, err := prepareOpenCodeNativeApplyLayout(layout, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, kernel, shared.RenameDirectoryExclusive, os.RemoveAll) if err != nil { return unchanged(err) } @@ -155,7 +158,7 @@ func applyOpenCodeTransition(ctx context.Context, request domain.ActivationReque if err := recorder.PersistPrepared(ctx, request.NativeAttempt, txn.root, transitionDTO(prepared), request.PreviousNativeObjects, request.Delivery.NativeObjects); err != nil { return err } - return commitOpenCodeTransitionSkills(request.Client.ConfigRoot, txn, p, staged) + return commitOpenCodeTransitionSkillsLayout(p.layout, txn, p, staged) } receipts, applyErr := kernel.ApplyDialectTransition(req) if !persisted { @@ -170,7 +173,7 @@ func applyOpenCodeTransition(ctx context.Context, request domain.ActivationReque func stageOpenCodeTransitionSkills(request domain.ActivationRequest, p *openCodeNativeApply) (*openCodeSkillTxn, map[string]string, error) { txn := &openCodeSkillTxn{durable: true, backups: map[string]openCodeBackup{}, installed: map[string]domain.NativeObjectOwnership{}, rename: p.rename, removeAll: p.removeAll} - if err := txn.createRoot(request.Client.ConfigRoot); err != nil { + if err := txn.createLayoutRoot(p.layout); err != nil { return nil, nil, err } staged, err := txn.stageDesired(request.Delivery.ActivePath, shared.ObjectMap(p.desired)) @@ -185,10 +188,14 @@ func stageOpenCodeTransitionSkills(request domain.ActivationRequest, p *openCode } func commitOpenCodeTransitionSkills(configRoot string, txn *openCodeSkillTxn, p *openCodeNativeApply, staged map[string]string) error { + return commitOpenCodeTransitionSkillsLayout(userOpenCodeLayout(configRoot), txn, p, staged) +} + +func commitOpenCodeTransitionSkillsLayout(layout openCodeNativeLayout, txn *openCodeSkillTxn, p *openCodeNativeApply, staged map[string]string) error { if err := txn.backupPrevious(shared.ObjectMap(p.previous)); err != nil { return err } - if err := atomicfile.SyncDirectory(filepath.Join(configRoot, "skills")); err != nil { + if err := atomicfile.SyncDirectory(layout.Skills); err != nil { return err } if err := atomicfile.SyncDirectory(txn.root); err != nil { @@ -197,7 +204,7 @@ func commitOpenCodeTransitionSkills(configRoot string, txn *openCodeSkillTxn, p if err := txn.installStaged(staged, shared.ObjectMap(p.desired)); err != nil { return err } - if err := atomicfile.SyncDirectory(filepath.Join(configRoot, "skills")); err != nil { + if err := atomicfile.SyncDirectory(layout.Skills); err != nil { return err } return atomicfile.SyncDirectory(txn.root) diff --git a/install/integrationctl/agentplugins/clients/opencode/native_transition_journal.go b/install/integrationctl/agentplugins/clients/opencode/native_transition_journal.go index bb045c039..340adf9d4 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_transition_journal.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_transition_journal.go @@ -42,6 +42,9 @@ type transitionRecord struct { // NativeTransitions consumes only stored roots and receipt/state authority. It // never resolves a host and never acquires the outer operation lock itself. type NativeTransitions struct { + // ProjectRoot binds discovery and reconciliation to the selected namespace. + // Empty preserves user-only recovery, including legacy user receipts. + ProjectRoot string `json:"-"` PackageVerifier interface { Verify(context.Context, string, string) error } @@ -159,6 +162,9 @@ func (t NativeTransitions) prepareTransitionAuthority(ctx context.Context, id do if !reflect.DeepEqual(OpenCodeObjects(binding.NativeObjects), OpenCodeObjects(previous)) { return before, binding, fmt.Errorf("native source ownership changed") } + if _, err := t.selectedBindingLayout(binding); err != nil { + return before, binding, err + } return before, binding, nil } @@ -180,6 +186,9 @@ func transitionTargetState(before domain.StateFileV2, id domain.NativeAttemptIde binding.NativeObjects = objects binding.NativeActivationAttempt = "" binding.Activation = domain.ActivationActive + if binding.Scope == string(domain.ScopeProject) { + binding.Activation = domain.ActivationPrepared + } binding.Verification = domain.VerificationInstalled binding.Materialization = domain.MaterializationMaterialized binding.Policy = domain.PolicyAllowed @@ -294,7 +303,7 @@ func readTransitionFile(root, path string, limit int64) ([]byte, error) { } func readTransitionRecord(root string) (transitionRecord, error) { var r transitionRecord - if err := validateTransitionRoot(filepath.Dir(filepath.Dir(root)), root); err != nil { + if err := validatePrivateTransitionRoot(root); err != nil { return r, err } path := filepath.Join(root, transitionRecordFile) @@ -331,6 +340,9 @@ func readTransitionRecord(root string) (transitionRecord, error) { } func storedTransitionRoot(b domain.ClientBinding) string { + if b.Scope == string(domain.ScopeProject) { + return b.NativeProfileRoot + } root := b.NativeProfileRoot for _, o := range OpenCodeObjects(b.NativeObjects) { codec, mcp, err := nativeconfig.OpenCodeCodecForKind(o.Kind) diff --git a/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery.go b/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery.go index ecd154416..23f4ead5e 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery.go @@ -26,10 +26,13 @@ func (t NativeTransitions) Pending() ([]PendingNativeTransition, error) { if err != nil { return nil, err } - roots, attempts := pendingTransitionAuthority(state) + roots, attempts, err := t.pendingTransitionAuthority(state) + if err != nil { + return nil, err + } var pending []PendingNativeTransition - for root := range roots { - if err := appendPendingTransitionRoot(root, attempts, &pending); err != nil { + for _, layout := range roots { + if err := appendPendingTransitionRoot(layout, attempts, &pending); err != nil { return nil, err } } @@ -45,27 +48,34 @@ func (t NativeTransitions) Pending() ([]PendingNativeTransition, error) { return pending, nil } -func pendingTransitionAuthority(state domain.StateFileV2) (map[string]bool, map[string]PendingNativeTransition) { - roots := map[string]bool{} +func (t NativeTransitions) pendingTransitionAuthority(state domain.StateFileV2) (map[string]openCodeNativeLayout, map[string]PendingNativeTransition, error) { + roots := map[string]openCodeNativeLayout{} attempts := map[string]PendingNativeTransition{} for _, installation := range state.Installations { for _, b := range installation.Clients { if b.ClientID != "opencode" { continue } + if (b.Scope == string(domain.ScopeProject) || t.ProjectRoot != "") && b.NativeProfileRoot == "" { + return nil, nil, fmt.Errorf("opencode project binding has no recorded native root") + } if root := storedTransitionRoot(b); root != "" { - roots[root] = true + layout, err := t.selectedBindingLayout(b) + if err != nil { + return nil, nil, err + } + roots[root] = layout } if b.NativeActivationAttempt != "" { attempts[b.NativeActivationAttempt] = PendingNativeTransition{OperationID: b.NativeActivationAttempt, InstallationID: installation.InstallationID, BindingID: b.ClientBindingID, TargetPath: b.TargetLocator, Phase: "native_attempt"} } } } - return roots, attempts + return roots, attempts, nil } -func appendPendingTransitionRoot(root string, attempts map[string]PendingNativeTransition, pending *[]PendingNativeTransition) error { - skills := filepath.Join(root, "skills") +func appendPendingTransitionRoot(layout openCodeNativeLayout, attempts map[string]PendingNativeTransition, pending *[]PendingNativeTransition) error { + root, skills := layout.Root, layout.Skills if err := pathpolicy.RequireContainedChild(root, skills); err != nil { return err } @@ -80,7 +90,7 @@ func appendPendingTransitionRoot(root string, attempts map[string]PendingNativeT if !strings.HasPrefix(entry.Name(), ".agentplugins-native-") { continue } - item, err := pendingTransitionEntry(root, skills, entry.Name()) + item, err := pendingTransitionEntry(layout, entry.Name()) if err != nil { return err } @@ -96,8 +106,9 @@ func appendPendingTransitionRoot(root string, attempts map[string]PendingNativeT return nil } -func pendingTransitionEntry(root, skills, name string) (*PendingNativeTransition, error) { - dir := filepath.Join(skills, name) +func pendingTransitionEntry(layout openCodeNativeLayout, name string) (*PendingNativeTransition, error) { + root := layout.Root + dir := filepath.Join(layout.Skills, name) if err := pathpolicy.RequireContainedChild(root, dir); err != nil { return nil, err } @@ -110,7 +121,8 @@ func pendingTransitionEntry(root, skills, name string) (*PendingNativeTransition if err != nil { return nil, err } - if r.Identity.NativeRoot != root { + recordLayout, layoutErr := recordOpenCodeLayout(r) + if layoutErr != nil || recordLayout != layout || r.Identity.NativeRoot != root { return nil, fmt.Errorf("transition root identity mismatch") } _, b, err := transitionBinding(r.OldState, r.Identity) @@ -152,6 +164,9 @@ func (t NativeTransitions) Reconcile(ctx context.Context, root string) (domain.N if err != nil { return domain.NativeEffectUncertain, err } + if err := t.validateRecordContext(r); err != nil { + return domain.NativeEffectUncertain, err + } q := transitionReconciler{transitions: t, ctx: ctx, record: r, effect: domain.NativeEffectUncertain} observation, reconcileErr := t.Kernel.ReconcileDialectTransition(nativeconfig.TransitionRecoveryRequest{ Paths: transitionPaths(r.Identity.NativeRoot), Prepared: nativePrepared(r.Native), diff --git a/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery_test.go b/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery_test.go index 08b8e7679..5de521e55 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery_test.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_transition_recovery_test.go @@ -37,9 +37,19 @@ func transitionTestWrite(t *testing.T, path string, body []byte) { } } func providerTransitionFixture(t *testing.T) transitionFixtureData { + return providerTransitionFixtureScope(t, domain.ScopeUser) +} +func providerTransitionFixtureScope(t *testing.T, scope domain.InstallScope) transitionFixtureData { t.Helper() root := t.TempDir() config, active := filepath.Join(root, "config"), filepath.Join(root, "package") + user, project := config, "" + layout := userOpenCodeLayout(config) + if scope == domain.ScopeProject { + config, project = filepath.Join(root, "project"), filepath.Join(root, "project") + active = filepath.Join(project, ".uap", "managed", "package") + layout = projectOpenCodeLayout(project) + } paths := transitionPaths(config) transitionTestWrite(t, paths.JSON, []byte("{ // keep user trivia\n\"theme\":\"fixture\",\"mcp\":{}}")) // JSONC keeps exact user comments, whitespace and original permissions. @@ -69,7 +79,7 @@ func providerTransitionFixture(t *testing.T) transitionFixtureData { next := old next.Kind = OpenCodeV2MCPObjectKind next.ManagedDigest = desired.Digest - oldSkill := domain.NativeObjectOwnership{ObjectID: "opencode-skill:skill", Kind: openCodeSkillKind, LogicalName: "skill", Path: filepath.Join(config, "skills", "skill"), SourceRelative: "skills/skill", ProtectionClass: "managed"} + oldSkill := domain.NativeObjectOwnership{ObjectID: "opencode-skill:skill", Kind: openCodeSkillKind, LogicalName: "skill", Path: filepath.Join(layout.Skills, "skill"), SourceRelative: "skills/skill", ProtectionClass: "managed"} transitionTestWrite(t, filepath.Join(oldSkill.Path, "SKILL.md"), []byte("source skill\n")) oldSkill.ManagedDigest, err = shared.DigestSkillDirectory(oldSkill.Path) if err != nil { @@ -81,8 +91,8 @@ func providerTransitionFixture(t *testing.T) transitionFixtureData { if err != nil { t.Fatal(err) } - id := domain.NativeAttemptIdentity{OperationID: "TEST-attempt", InstallationID: "TEST-installation", BindingID: domain.ComputeClientBindingID("TEST-installation", "opencode", "user", active), NativeRoot: config} - b := domain.ClientBinding{ClientBindingID: id.BindingID, ClientID: "opencode", Scope: "user", TargetLocator: active, PhysicalArtifact: domain.ComputePhysicalArtifactID("demo", id.InstallationID), Materialization: domain.MaterializationMaterialized, Activation: domain.ActivationPrepared, Authentication: domain.AuthenticationNotRequired, Verification: domain.VerificationPackageValid, Policy: domain.PolicyAllowed, NativeActivationAttempt: id.OperationID, NativeObjects: []domain.NativeObjectOwnership{old, oldSkill}, PackageRevision: &domain.ClientPackageRevision{TreeDigest: "sha256:tree", ManifestDigest: "sha256:manifest"}} + id := domain.NativeAttemptIdentity{OperationID: "TEST-attempt", InstallationID: "TEST-installation", BindingID: domain.ComputeClientBindingID("TEST-installation", "opencode", string(scope), active), NativeRoot: config} + b := domain.ClientBinding{ClientBindingID: id.BindingID, ClientID: "opencode", Scope: string(scope), NativeProfileRoot: config, TargetLocator: active, PhysicalArtifact: domain.ComputePhysicalArtifactID("demo", id.InstallationID), Materialization: domain.MaterializationMaterialized, Activation: domain.ActivationPrepared, Authentication: domain.AuthenticationNotRequired, Verification: domain.VerificationPackageValid, Policy: domain.PolicyAllowed, NativeActivationAttempt: id.OperationID, NativeObjects: []domain.NativeObjectOwnership{old, oldSkill}, PackageRevision: &domain.ClientPackageRevision{TreeDigest: "sha256:tree", ManifestDigest: "sha256:manifest"}} artifact, err := (packagesnapshot.Builder{}).Build(context.Background(), active) if err != nil { t.Fatal(err) @@ -95,27 +105,39 @@ func providerTransitionFixture(t *testing.T) transitionFixtureData { b.NativeObjects = append(b.NativeObjects, packageObject) state := domain.StateFileV2{SchemaVersion: domain.StateSchemaVersion, Installations: []domain.Installation{{InstallationID: id.InstallationID, DeclaredName: "demo", Source: domain.SourceBinding{SourceBindingID: "src_demo", RequestedSource: "demo", CanonicalSource: "https://fixture.invalid/demo", ResolvedRevision: "abc", TreeDigest: "sha256:tree"}, Package: domain.PackageBinding{LoaderKind: domain.LoaderKindAgentPlugins, FormatID: domain.FormatIDAgentPluginsV1, SchemaURI: domain.PluginSchemaV1, DeclaredName: "demo", ManifestDigest: "sha256:manifest"}, Clients: map[string]domain.ClientBinding{id.BindingID: b}}}} store := statev2.Store{Path: filepath.Join(root, "state.json")} + if project != "" { + store.Path, store.ProjectRoot = filepath.Join(project, ".uap", "state.json"), project + } if err := store.Save(state); err != nil { t.Fatal(err) } - return transitionFixtureData{request: domain.ActivationRequest{NativeAttempt: id, Client: domain.DetectedClient{ClientID: domain.ClientOpenCode, ConfigRoot: config}, Delivery: domain.StagedDelivery{ActivePath: active, NativeObjects: []domain.NativeObjectOwnership{next, newSkill, packageObject}}, PreviousNativeObjects: b.NativeObjects}, store: store, source: source} + return transitionFixtureData{request: domain.ActivationRequest{NativeAttempt: id, Client: domain.DetectedClient{ClientID: domain.ClientOpenCode, ConfigRoot: user}, Plan: domain.DeliveryPlan{Scope: scope, NativeRegistryRoot: config, TargetContext: domain.TargetContext{Scope: scope, ProjectRoot: project, UserConfigRoot: user}}, Delivery: domain.StagedDelivery{ActivePath: active, NativeObjects: []domain.NativeObjectOwnership{next, newSkill, packageObject}}, PreviousNativeObjects: b.NativeObjects}, store: store, source: source} } func freshTransitions(f transitionFixtureData) NativeTransitions { - return NativeTransitions{PackageVerifier: transitionTestPackageVerifier{}, State: transaction.Kernel{StateStore: statev2.Store{Path: f.store.Path}}, Kernel: nativeconfig.New()} + return NativeTransitions{ProjectRoot: f.store.ProjectRoot, PackageVerifier: transitionTestPackageVerifier{}, State: transaction.Kernel{StateStore: f.store}, Kernel: nativeconfig.New()} } // Reopened recovery must infer bytes/state, never trust a phase. Each crash // occurs after real persisted preimage/rename/write/state/cleanup effects. func TestNativeTransitionReopensEveryDurableBoundary(t *testing.T) { + testTransitionDurableBoundaries(t, domain.ScopeUser) +} + +func TestProjectOpenCodeTransitionReopensEveryDurableBoundary(t *testing.T) { + testTransitionDurableBoundaries(t, domain.ScopeProject) +} + +func testTransitionDurableBoundaries(t *testing.T, scope domain.InstallScope) { + t.Helper() for _, crash := range []string{"prepared", "backup", "skill", "native", "state", "cleanup"} { t.Run(crash, func(t *testing.T) { - f := providerTransitionFixture(t) + f := providerTransitionFixtureScope(t, scope) t0 := freshTransitions(f) executable, err := os.Executable() if err != nil { t.Fatal(err) } - wire := transitionCrashWire{Identity: f.request.NativeAttempt, ActivePath: f.request.Delivery.ActivePath, Previous: f.request.PreviousNativeObjects, Desired: f.request.Delivery.NativeObjects, StatePath: f.store.Path} + wire := transitionCrashWire{Identity: f.request.NativeAttempt, ActivePath: f.request.Delivery.ActivePath, Previous: f.request.PreviousNativeObjects, Desired: f.request.Delivery.NativeObjects, StatePath: f.store.Path, ProjectRoot: f.store.ProjectRoot, UserRoot: f.request.Client.ConfigRoot} body, _ := json.Marshal(wire) fixture := filepath.Join(filepath.Dir(f.store.Path), "TEST-child.json") transitionTestWrite(t, fixture, body) @@ -160,15 +182,19 @@ func TestNativeTransitionReopensEveryDurableBoundary(t *testing.T) { if codec != want { t.Fatalf("confirmed receipt codec %s, want %s", codec, want) } - if err := VerifyOpenCodeNativeObjects(f.request.Client.ConfigRoot, "", b.NativeObjects, nativeconfig.New(), false); err != nil { + layout, err := openCodeActivationLayout(f.request) + if err != nil { + t.Fatal(err) + } + if err := verifyOpenCodeNativeLayout(layout, "", b.NativeObjects, nativeconfig.New(), false); err != nil { t.Fatal(err) } - content, err := os.ReadFile(filepath.Join(f.request.Client.ConfigRoot, "skills", "skill", "SKILL.md")) + content, err := os.ReadFile(filepath.Join(layout.Skills, "skill", "SKILL.md")) if err != nil || string(content) != skill { t.Fatal("wrong recovered skill", err) } if want == nativeconfig.CodecOpenCode { - body, _ := os.ReadFile(transitionPaths(f.request.Client.ConfigRoot).JSONC) + body, _ := os.ReadFile(transitionPaths(layout.Root).JSONC) if !bytes.Equal(body, f.source) { t.Fatal("source recovery changed exact foreign bytes") } @@ -181,9 +207,9 @@ func TestNativeTransitionReopensEveryDurableBoundary(t *testing.T) { } type transitionCrashWire struct { - Identity domain.NativeAttemptIdentity - ActivePath, StatePath string - Previous, Desired []domain.NativeObjectOwnership + Identity domain.NativeAttemptIdentity + ActivePath, StatePath, ProjectRoot, UserRoot string + Previous, Desired []domain.NativeObjectOwnership } // This inert compiled TEST child is always exercised by the parent matrix; @@ -203,9 +229,17 @@ func TestNativeTransitionCrashChild(t *testing.T) { } crash := args[1] request := domain.ActivationRequest{NativeAttempt: wire.Identity, Client: domain.DetectedClient{ClientID: domain.ClientOpenCode, ConfigRoot: wire.Identity.NativeRoot}, Delivery: domain.StagedDelivery{ActivePath: wire.ActivePath, NativeObjects: wire.Desired}, PreviousNativeObjects: wire.Previous} - f := transitionFixtureData{request: request, store: statev2.Store{Path: wire.StatePath}} + if wire.ProjectRoot != "" { + request.Client.ConfigRoot = wire.UserRoot + request.Plan = domain.DeliveryPlan{Scope: domain.ScopeProject, NativeRegistryRoot: wire.ProjectRoot, TargetContext: domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: wire.ProjectRoot, UserConfigRoot: wire.UserRoot}} + } + layout, err := openCodeActivationLayout(request) + if err != nil { + t.Fatal(err) + } + f := transitionFixtureData{request: request, store: statev2.Store{Path: wire.StatePath, ProjectRoot: wire.ProjectRoot}} transitions := freshTransitions(f) - p, err := prepareOpenCodeNativeApply(request.Client.ConfigRoot, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, nativeconfig.New(), shared.RenameDirectoryExclusive, os.RemoveAll) + p, err := prepareOpenCodeNativeApplyLayout(layout, request.Delivery.ActivePath, request.PreviousNativeObjects, request.Delivery.NativeObjects, nativeconfig.New(), shared.RenameDirectoryExclusive, os.RemoveAll) if err != nil { t.Fatal(err) } @@ -214,7 +248,7 @@ func TestNativeTransitionCrashChild(t *testing.T) { t.Fatal(err) } txn := &openCodeSkillTxn{durable: true, rename: shared.RenameDirectoryExclusive, removeAll: os.RemoveAll, backups: map[string]openCodeBackup{}, installed: map[string]domain.NativeObjectOwnership{}} - if err := txn.createRoot(request.Client.ConfigRoot); err != nil { + if err := txn.createLayoutRoot(layout); err != nil { t.Fatal(err) } staged, err := txn.stageDesired(request.Delivery.ActivePath, shared.ObjectMap(p.desired)) diff --git a/install/integrationctl/agentplugins/clients/opencode/native_transition_scope.go b/install/integrationctl/agentplugins/clients/opencode/native_transition_scope.go new file mode 100644 index 000000000..14fa31575 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/opencode/native_transition_scope.go @@ -0,0 +1,73 @@ +package opencode + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Project scope is stored binding authority, never a heuristic over package +// paths. Legacy user bindings retain the existing receipt-root fallback. +func transitionBindingLayout(binding domain.ClientBinding) (openCodeNativeLayout, error) { + root := storedTransitionRoot(binding) + if root == "" || !filepath.IsAbs(root) || filepath.Clean(root) != root { + return openCodeNativeLayout{}, fmt.Errorf("opencode stored native root is unavailable") + } + layout := userOpenCodeLayout(root) + if binding.Scope == string(domain.ScopeProject) { + if _, err := domain.NewTargetContext(domain.ScopeProject, root, ""); err != nil { + return openCodeNativeLayout{}, err + } + layout = projectOpenCodeLayout(root) + } else if binding.Scope != "" && binding.Scope != string(domain.ScopeUser) { + return openCodeNativeLayout{}, fmt.Errorf("unsupported opencode binding scope") + } + return layout, layout.validate() +} + +func recordOpenCodeLayout(record transitionRecord) (openCodeNativeLayout, error) { + _, binding, err := transitionBinding(record.OldState, record.Identity) + if err != nil { + return openCodeNativeLayout{}, err + } + return transitionBindingLayout(binding) +} + +func (t NativeTransitions) selectedBindingLayout(binding domain.ClientBinding) (openCodeNativeLayout, error) { + layout, err := transitionBindingLayout(binding) + if err != nil { + return layout, err + } + if layout.Scope == domain.ScopeProject { + if t.ProjectRoot == "" || t.ProjectRoot != layout.Root { + return layout, fmt.Errorf("opencode transition differs from the selected project") + } + } else if t.ProjectRoot != "" { + return layout, fmt.Errorf("opencode user transition is outside the selected project") + } + return layout, nil +} + +func (t NativeTransitions) validateRecordContext(record transitionRecord) error { + current, err := t.State.Load() + if err != nil { + return err + } + _, binding, err := transitionBinding(current, record.Identity) + if err != nil { + return err + } + selected, err := t.selectedBindingLayout(binding) + if err != nil { + return err + } + bound, err := recordOpenCodeLayout(record) + if err != nil { + return err + } + if selected != bound { + return fmt.Errorf("opencode transition scope or native layout changed") + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/opencode/native_transition_validation.go b/install/integrationctl/agentplugins/clients/opencode/native_transition_validation.go index 8c4c90c54..35ec2a820 100644 --- a/install/integrationctl/agentplugins/clients/opencode/native_transition_validation.go +++ b/install/integrationctl/agentplugins/clients/opencode/native_transition_validation.go @@ -56,14 +56,33 @@ func validateTransitionEnvelope(record transitionRecord, root string) error { return err } } - return validateTransitionRoot(record.Identity.NativeRoot, root) + layout, err := recordOpenCodeLayout(record) + if err != nil { + return err + } + return validateTransitionLayoutRoot(layout, root) } -func validateTransitionRoot(nativeRoot, root string) error { - if !filepath.IsAbs(nativeRoot) || filepath.Clean(nativeRoot) != nativeRoot || filepath.Dir(root) != filepath.Join(nativeRoot, "skills") || !strings.HasPrefix(filepath.Base(root), ".agentplugins-native-") { +func validateTransitionLayoutRoot(layout openCodeNativeLayout, root string) error { + if !filepath.IsAbs(layout.Root) || filepath.Clean(layout.Root) != layout.Root || filepath.Dir(root) != layout.Skills { return fmt.Errorf("invalid transition root") } - if err := pathpolicy.RequireContainedChild(nativeRoot, root); err != nil { + if err := layout.validate(); err != nil { + return err + } + if err := pathpolicy.RequireContainedChild(layout.Root, root); err != nil { + return err + } + return validatePrivateTransitionRoot(root) +} + +// Before decoding the bounded private record, validate only its local container. +// The decoded binding subsequently supplies the exact user/project native layout. +func validatePrivateTransitionRoot(root string) error { + if !filepath.IsAbs(root) || filepath.Clean(root) != root || !strings.HasPrefix(filepath.Base(root), ".agentplugins-native-") { + return fmt.Errorf("invalid transition root") + } + if err := pathpolicy.RequireContainedChild(filepath.Dir(root), root); err != nil { return err } info, err := os.Lstat(root) @@ -145,6 +164,10 @@ func validateTransitionReceipts(record transitionRecord, old, target domain.Clie } func validateTransitionReceiptSide(record transitionRecord, side transitionReceiptSide) error { + layout, err := recordOpenCodeLayout(record) + if err != nil { + return err + } objects := shared.ObjectMap(OpenCodeObjects(side.objects)) mcpCount := 0 for _, object := range objects { @@ -152,7 +175,7 @@ func validateTransitionReceiptSide(record transitionRecord, side transitionRecei if err != nil { return err } - if err := validateOpenCodeObject(record.Identity.NativeRoot, OpenCodeProjection{}, object); err != nil { + if err := validateOpenCodeObject(layout, OpenCodeProjection{}, object); err != nil { return err } if mcp { @@ -220,10 +243,14 @@ func transitionSkillIntentCount(old, next map[string]domain.NativeObjectOwnershi } func validateTransitionSkillIntent(record transitionRecord, index int, skill transitionSkill) error { + layout, err := recordOpenCodeLayout(record) + if err != nil { + return err + } if err := pathpolicy.ValidateLeafID(skill.Name); err != nil { return err } - if skill.ID != "opencode-skill:"+skill.Name || index > 0 && record.Skills[index-1].ID >= skill.ID || skill.Target != filepath.Join(record.Identity.NativeRoot, "skills", skill.Name) || skill.Backup != filepath.Join(record.Root, "old-"+skill.Name) || skill.Old == nil && skill.New == nil || skill.Old == nil && skill.OldExists { + if skill.ID != "opencode-skill:"+skill.Name || index > 0 && record.Skills[index-1].ID >= skill.ID || skill.Target != filepath.Join(layout.Skills, skill.Name) || skill.Backup != filepath.Join(record.Root, "old-"+skill.Name) || skill.Old == nil && skill.New == nil || skill.Old == nil && skill.OldExists { return fmt.Errorf("invalid skill intent") } return nil diff --git a/install/integrationctl/agentplugins/clients/opencode/plan.go b/install/integrationctl/agentplugins/clients/opencode/plan.go index 8ab7ab3d4..374fcd7ce 100644 --- a/install/integrationctl/agentplugins/clients/opencode/plan.go +++ b/install/integrationctl/agentplugins/clients/opencode/plan.go @@ -38,7 +38,15 @@ func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain } plan.OpenCodeHost = in.Client.OpenCodeHost } - shared.PromoteNativeReady(plan, in.Client.ConfigRoot, shared.OnlyNativeComponents(plan.Components)) + root := in.Client.ConfigRoot + if in.TargetContext.Scope == domain.ScopeProject { + layout, err := openCodeTargetLayout(in.Client, in.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return err + } + root = layout.Root + } + shared.PromoteNativeReady(plan, root, shared.OnlyNativeComponents(plan.Components)) plan.UserActions = shared.AppendUnique(plan.UserActions, "agentplugins will install the package's skills and MCP servers; restart OpenCode when complete") return nil } diff --git a/install/integrationctl/agentplugins/clients/opencode/project.go b/install/integrationctl/agentplugins/clients/opencode/project.go index 6c92a0d9c..793e0f269 100644 --- a/install/integrationctl/agentplugins/clients/opencode/project.go +++ b/install/integrationctl/agentplugins/clients/opencode/project.go @@ -10,7 +10,15 @@ import ( var _ clients.Projector = (*Adapter)(nil) var _ clients.ActiveNativeProjector = (*Adapter)(nil) -func (*Adapter) ProjectActiveNative(_ context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if err := validateOpenCodeProjectActiveProjection(root, envelope, plan, dataPath); err != nil { + return nil, err + } + } return BuildOpenCodeNativeObjects(root, envelope, plan) } diff --git a/install/integrationctl/agentplugins/clients/opencode/project_active.go b/install/integrationctl/agentplugins/clients/opencode/project_active.go new file mode 100644 index 000000000..c708fda24 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/opencode/project_active.go @@ -0,0 +1,44 @@ +package opencode + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Existing immutable projection bytes cannot silently retain a different data +// locator from the selected receipt. Reprojection reads and refuses; it does +// not rewrite the digest-covered package to adopt a new authority. +func validateOpenCodeProjectActiveProjection(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) error { + layout, err := openCodePlanLayout(plan) + if err != nil { + return err + } + if root != plan.ActivePath { + return fmt.Errorf("%w: OpenCode active root differs from selected package", domain.ErrProjectionRefreshRequired) + } + if err := pathpolicy.RequireContainedChild(layout.Root, root); err != nil { + return err + } + if empty, err := emptyOpenCodeNativePlan(envelope, plan); err != nil || empty { + return err + } + projection, err := ReadOpenCodeProjection(root) + if err != nil { + return err + } + if err := validateOpenCodeProjection(layout, root, projection); err != nil { + return err + } + if projection.PackageRoot != plan.ActivePath { + return fmt.Errorf("%w: OpenCode projection package root differs from selected package", domain.ErrProjectionRefreshRequired) + } + for _, server := range projection.MCPServers { + if server.Type == "stdio" && (dataPath == "" || !filepath.IsAbs(dataPath) || filepath.Clean(dataPath) != dataPath || projection.DataRoot != dataPath) { + return fmt.Errorf("%w: OpenCode projection data root differs from selected receipt", domain.ErrProjectionRefreshRequired) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/opencode/project_status.go b/install/integrationctl/agentplugins/clients/opencode/project_status.go new file mode 100644 index 000000000..c49b48daf --- /dev/null +++ b/install/integrationctl/agentplugins/clients/opencode/project_status.go @@ -0,0 +1,37 @@ +package opencode + +import ( + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +// The provider calls this before its generic attestation shortcut. A native +// config receipt proves exact installed bytes, not the effective session config. +func (*Adapter) PreflightActivation(_ clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject && request.Plan.TargetContext.Scope != domain.ScopeProject { + return nil + } + if _, err := openCodeActivationLayout(request); err != nil { + return err + } + if request.ActivationComplete { + return fmt.Errorf("opencode project discovery cannot be established by activation attestation; inspect effective config, skills and MCP servers in a fresh session in the selected project") + } + return nil +} + +func openCodeProjectStatus(request domain.ActivationRequest, outcome domain.ActivationOutcome) domain.ActivationOutcome { + if request.Plan.Scope != domain.ScopeProject || outcome.Verification != domain.VerificationInstalled { + return outcome + } + outcome.Activation = domain.ActivationPrepared + outcome.ActivationAttested = false + outcome.UserActions = shared.AppendUnique(outcome.UserActions, "OpenCode project configuration is installed; effective config, native skill discovery, permissions and MCP initialization remain unobserved; start OpenCode in the selected project and inspect debug config, debug skill and mcp list") + outcome.UserActions = shared.AppendUnique(outcome.UserActions, "OpenCode merges ancestor, user, custom-directory, inline and managed configuration; preserved higher-priority settings or same-name skills can shadow the managed project components") + return outcome +} diff --git a/install/integrationctl/agentplugins/clients/opencode/project_transition_test.go b/install/integrationctl/agentplugins/clients/opencode/project_transition_test.go new file mode 100644 index 000000000..70f5ae844 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/opencode/project_transition_test.go @@ -0,0 +1,80 @@ +package opencode + +import ( + "bytes" + "context" + "errors" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func projectPreparedTransition(t *testing.T) (transitionFixtureData, string) { + t.Helper() + f := providerTransitionFixtureScope(t, domain.ScopeProject) + layout, err := openCodeActivationLayout(f.request) + if err != nil { + t.Fatal(err) + } + p, err := prepareOpenCodeNativeApplyLayout(layout, f.request.Delivery.ActivePath, f.request.PreviousNativeObjects, f.request.Delivery.NativeObjects, nativeconfig.New(), shared.RenameDirectoryExclusive, os.RemoveAll) + if err != nil { + t.Fatal(err) + } + req, err := openCodeTransitionRequest(p) + if err != nil { + t.Fatal(err) + } + txn, _, err := stageOpenCodeTransitionSkills(f.request, p) + if err != nil { + t.Fatal(err) + } + stop := errors.New("TEST stop after durable preparation") + req.PersistPrepared = func(prepared nativeconfig.PreparedTransition) error { + if err := freshTransitions(f).PersistPrepared(context.Background(), f.request.NativeAttempt, txn.root, transitionDTO(prepared), f.request.PreviousNativeObjects, f.request.Delivery.NativeObjects); err != nil { + return err + } + return stop + } + if _, err := nativeconfig.New().ApplyDialectTransition(req); !errors.Is(err, stop) { + t.Fatalf("preparation fence: %v", err) + } + return f, txn.root +} + +func TestProjectOpenCodeTransitionRequiresSelectedContextBeforeRecovery(t *testing.T) { + f, root := projectPreparedTransition(t) + stateBefore, err := os.ReadFile(f.store.Path) + if err != nil { + t.Fatal(err) + } + configBefore, err := os.ReadFile(transitionPaths(f.store.ProjectRoot).JSONC) + if err != nil { + t.Fatal(err) + } + for _, selected := range []string{"", filepath.Join(t.TempDir(), "B")} { + transitions := freshTransitions(f) + transitions.ProjectRoot = selected + if _, err := transitions.Pending(); err == nil { + t.Fatalf("discovery accepted mismatched project %q", selected) + } + if _, err := transitions.Reconcile(context.Background(), root); err == nil { + t.Fatalf("recovery accepted mismatched project %q", selected) + } + } + stateAfter, _ := os.ReadFile(f.store.Path) + configAfter, _ := os.ReadFile(transitionPaths(f.store.ProjectRoot).JSONC) + if !bytes.Equal(stateBefore, stateAfter) || !bytes.Equal(configBefore, configAfter) { + t.Fatal("refused recovery changed state or native config") + } + pending, err := freshTransitions(f).Pending() + if err != nil || len(pending) != 1 || pending[0].Root != root { + t.Fatalf("selected project lost pending transition: %+v, %v", pending, err) + } + if err := freshTransitions(f).Recover(context.Background()); err != nil { + t.Fatal(err) + } +} diff --git a/install/integrationctl/agentplugins/clients/opencode/scope.go b/install/integrationctl/agentplugins/clients/opencode/scope.go new file mode 100644 index 000000000..9a25bde0c --- /dev/null +++ b/install/integrationctl/agentplugins/clients/opencode/scope.go @@ -0,0 +1,114 @@ +package opencode + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) +) + +// OpenCode's project config and skills have distinct native roots. This layout +// is operational authority; it is never inferred from a staged package. +type openCodeNativeLayout struct { + Root, Skills string + Scope domain.InstallScope +} + +func userOpenCodeLayout(root string) openCodeNativeLayout { + return openCodeNativeLayout{Root: root, Skills: filepath.Join(root, "skills"), Scope: domain.ScopeUser} +} + +func projectOpenCodeLayout(root string) openCodeNativeLayout { + return openCodeNativeLayout{Root: root, Skills: filepath.Join(root, ".opencode", "skills"), Scope: domain.ScopeProject} +} + +func (layout openCodeNativeLayout) validate() error { + if layout.Scope != domain.ScopeProject { + // Legacy user callers retain their existing per-object preconditions. + return nil + } + if _, err := domain.NewTargetContext(domain.ScopeProject, layout.Root, ""); err != nil { + return err + } + for _, path := range []string{layout.Skills, filepath.Join(layout.Root, "opencode.json"), filepath.Join(layout.Root, "opencode.jsonc")} { + if err := pathpolicy.RequireContainedChild(layout.Root, path); err != nil { + return err + } + } + return nil +} + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return in.TargetContext.ProjectRoot, in.Client.ExecutablePath + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if target.Scope != domain.ScopeProject { + return "", "", fmt.Errorf("opencode project layout requires project scope") + } + if _, err := openCodeTargetLayout(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", fmt.Errorf("opencode prepared package must stay in the selected project: %w", err) + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func openCodeTargetLayout(client domain.DetectedClient, target domain.TargetContext, planned string) (openCodeNativeLayout, error) { + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return openCodeNativeLayout{}, err + } + if validated.UserConfigRoot != "" && validated.UserConfigRoot != client.ConfigRoot { + return openCodeNativeLayout{}, fmt.Errorf("opencode detected user profile differs from the selected context") + } + layout := userOpenCodeLayout(client.ConfigRoot) + if validated.Scope == domain.ScopeProject { + layout = projectOpenCodeLayout(validated.ProjectRoot) + } + if planned != "" && planned != layout.Root { + return openCodeNativeLayout{}, fmt.Errorf("opencode native root differs from the selected context") + } + if layout.Root == "" && layout.Scope != domain.ScopeProject { + return layout, nil + } + return layout, layout.validate() +} + +func openCodePlanLayout(plan domain.DeliveryPlan) (openCodeNativeLayout, error) { + if (plan.Scope == domain.ScopeProject) != (plan.TargetContext.Scope == domain.ScopeProject) { + return openCodeNativeLayout{}, fmt.Errorf("opencode plan scope differs from its target context") + } + if plan.Scope == domain.ScopeProject { + target, err := domain.NewTargetContext(plan.TargetContext.Scope, plan.TargetContext.ProjectRoot, plan.TargetContext.UserConfigRoot) + if err != nil { + return openCodeNativeLayout{}, err + } + if plan.NativeRegistryRoot != target.ProjectRoot { + return openCodeNativeLayout{}, fmt.Errorf("opencode native root differs from the selected project") + } + layout := projectOpenCodeLayout(target.ProjectRoot) + return layout, layout.validate() + } + layout := userOpenCodeLayout(plan.NativeRegistryRoot) + return layout, layout.validate() +} + +func openCodeActivationLayout(request domain.ActivationRequest) (openCodeNativeLayout, error) { + if (request.Plan.Scope == domain.ScopeProject) != (request.Plan.TargetContext.Scope == domain.ScopeProject) { + return openCodeNativeLayout{}, fmt.Errorf("opencode plan scope differs from its target context") + } + return openCodeTargetLayout(request.Client, request.Plan.TargetContext, request.Plan.NativeRegistryRoot) +} diff --git a/install/integrationctl/agentplugins/clients/planning.go b/install/integrationctl/agentplugins/clients/planning.go index bd01e6186..a1f7f4149 100644 --- a/install/integrationctl/agentplugins/clients/planning.go +++ b/install/integrationctl/agentplugins/clients/planning.go @@ -29,6 +29,12 @@ type TargetLayout interface { TargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string) (anchor, root string, err error) } +// ProjectTargetLayout is an explicit project admission and layout contract. +// There is no fallback to user TargetLayout for project requests. +type ProjectTargetLayout interface { + ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (anchor, root string, err error) +} + // NativeRegistryLayout overrides which native client registry a delivery is // recorded against. VS Code is installed through the Copilot CLI it shares a // backend with, so its plan points at the sibling's locators rather than its @@ -83,6 +89,7 @@ type PreparationRefiner interface { // PlanInput is everything a refiner is allowed to see. type PlanInput struct { + TargetContext domain.TargetContext `json:"-"` PreviousNativeObjects []domain.NativeObjectOwnership `json:"-"` LocalEntryObservation *domain.LocalEntryObservation `json:"-"` Envelope domain.PackageEnvelope diff --git a/install/integrationctl/agentplugins/clients/qoder/lifecycle.go b/install/integrationctl/agentplugins/clients/qoder/lifecycle.go index 89521acad..008c5ae93 100644 --- a/install/integrationctl/agentplugins/clients/qoder/lifecycle.go +++ b/install/integrationctl/agentplugins/clients/qoder/lifecycle.go @@ -17,14 +17,23 @@ var ( ) func (*Adapter) AutomaticallyActivates(env clients.Env, request domain.ActivationRequest) bool { + if projectRequested(request.Plan) { + return false + } return shared.HasClientCLI(env, request.BackendExecutable) } -func (*Adapter) VerifierAvailable(_ domain.DetectedClient, _ domain.DeliveryPlan, backendExecutable string) bool { +func (*Adapter) VerifierAvailable(client domain.DetectedClient, plan domain.DeliveryPlan, backendExecutable string) bool { + if projectRequested(plan) { + return validateProjectPlan(client, plan) == nil + } return shared.HasListingCLI(backendExecutable) } func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if projectRequested(request.Plan) { + return activateProject(ctx, env, request) + } if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } @@ -138,6 +147,9 @@ func verifyAbsent(ctx context.Context, env clients.Env, executable, name string) } func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject || request.TargetContext.ProjectRoot != "" { + return deactivateProject(ctx, request) + } outcome := shared.StartedDeactivation() if request.ExternalUninstalled { outcome.ExternalRemovalComplete = true diff --git a/install/integrationctl/agentplugins/clients/qoder/plan.go b/install/integrationctl/agentplugins/clients/qoder/plan.go index 1202e052c..5b0bebb2e 100644 --- a/install/integrationctl/agentplugins/clients/qoder/plan.go +++ b/install/integrationctl/agentplugins/clients/qoder/plan.go @@ -2,6 +2,7 @@ package qoder import ( "context" + "fmt" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" @@ -17,7 +18,14 @@ var ( // "name@local" id Qoder registers plugins by. An '@' in the name would make // every registration, verification, and uninstall addressable to a different // plugin, so such packages never reach target resolution. -func (*Adapter) CheckPlanPrecondition(_ clients.PlanInput, plan *domain.DeliveryPlan) error { +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if projectRequested(*plan) || in.TargetContext.Scope == domain.ScopeProject || in.TargetContext.ProjectRoot != "" { + if plan.Scope != domain.ScopeProject || in.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("qoder project plan scope mismatch") + } + _, err := projectRoot(in.Client, in.TargetContext) + return err + } if qualifiedPluginName(plan.DeclaredName) { return nil } @@ -31,6 +39,18 @@ func (*Adapter) CheckPlanPrecondition(_ clients.PlanInput, plan *domain.Delivery // RefinePlan promotes on the Qoder CLI: the package lands in the managed root, // so readiness is the CLI's ability to register it, not a config root to write. func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if projectRequested(*plan) || in.TargetContext.Scope == domain.ScopeProject || in.TargetContext.ProjectRoot != "" { + if err := validateProjectPlan(in.Client, *plan); err != nil { + return err + } + if plan.Status != domain.PlanUnsupported { + plan.Status, plan.Activation = domain.PlanReady, domain.ActivationPrepared + for _, action := range projectActions(*plan) { + plan.UserActions = shared.AppendUnique(plan.UserActions, action) + } + } + return nil + } shared.PromoteNativeReady(plan, in.Client.ExecutablePath, shared.OnlyNativeComponents(plan.Components)) if plan.Status == domain.PlanUnsupported { return nil diff --git a/install/integrationctl/agentplugins/clients/qoder/project.go b/install/integrationctl/agentplugins/clients/qoder/project.go index cfba98162..b677c751d 100644 --- a/install/integrationctl/agentplugins/clients/qoder/project.go +++ b/install/integrationctl/agentplugins/clients/qoder/project.go @@ -20,7 +20,10 @@ func (*Adapter) ManagedMCPSelection() clients.SelectionLayout { return clients.SelectionLayout{File: ".mcp.json", Nested: true} } -func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if projectRequested(in.Plan) { + return projectBundle(ctx, in) + } manifest := shared.ManifestFromEnvelope(in.Envelope, shared.WithAuthorNameEmail()) if shared.ComponentKindPresent(in.Plan.Components, domain.ComponentSkill) { manifest["skills"] = "./skills/" diff --git a/install/integrationctl/agentplugins/clients/qoder/project_bundle.go b/install/integrationctl/agentplugins/clients/qoder/project_bundle.go new file mode 100644 index 000000000..823ad53b8 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/qoder/project_bundle.go @@ -0,0 +1,212 @@ +package qoder + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/filetree" + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/packagesnapshot" +) + +const projectNativeView = ".agentplugins-qoder-native" + +func selectedProjectSkills(plan domain.DeliveryPlan) (map[string]bool, error) { + names := map[string]bool{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + if component.Kind != domain.ComponentSkill || (component.Support != domain.SupportNative && (component.Support != domain.SupportProjected || component.Source != domain.ComponentSourceCommand)) { + return nil, fmt.Errorf("qoder project component is unqualified") + } + if err := pathpolicy.ValidateLeafID(component.Name); err != nil { + return nil, err + } + if names[component.Name] { + return nil, fmt.Errorf("qoder project skill identity is duplicated") + } + names[component.Name] = true + } + if len(names) == 0 { + return nil, fmt.Errorf("qoder project preparation requires a supported skill") + } + return names, nil +} + +func projectBundle(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := validateProjectPlan(domain.DetectedClient{ClientID: domain.ClientQoder, ConfigRoot: in.Plan.TargetContext.UserConfigRoot}, in.Plan); err != nil { + return nil, err + } + if !filepath.IsAbs(in.StagingPath) || filepath.Clean(in.StagingPath) != in.StagingPath || filepath.Dir(in.StagingPath) != in.Plan.TargetRoot || !strings.HasPrefix(filepath.Base(in.StagingPath), ".agentplugins-staging-") { + return nil, fmt.Errorf("qoder project staging differs from exact selected managed base") + } + if err := pathpolicy.RequireContainedChild(in.Plan.TargetContext.ProjectRoot, in.StagingPath); err != nil { + return nil, err + } + names, err := selectedProjectSkills(in.Plan) + if err != nil { + return nil, err + } + view := filepath.Join(in.StagingPath, projectNativeView) + if _, err := os.Lstat(view); err == nil { + return nil, fmt.Errorf("source contains reserved Qoder native view") + } else if !os.IsNotExist(err) { + return nil, err + } + if in.Envelope.Manifest.Name != in.Plan.DeclaredName || in.Envelope.Manifest.Version != in.Plan.DeclaredVersion { + return nil, fmt.Errorf("qoder projected manifest differs from selected identity") + } + for name := range names { + if err := ctx.Err(); err != nil { + return nil, err + } + skill, ok := in.Envelope.Skills[name] + if !ok || filepath.ToSlash(skill.RelativePath) != "skills/"+name+"/SKILL.md" { + return nil, fmt.Errorf("qoder project skill has no exact portable directory: %s", name) + } + source := filepath.Join(in.StagingPath, "skills", name) + if err := pathpolicy.RequireContainedChild(in.StagingPath, source); err != nil { + return nil, err + } + if err := filetree.CopyDir(source, filepath.Join(view, "skills", name)); err != nil { + return nil, err + } + } + manifest := shared.ManifestFromEnvelope(in.Envelope, shared.WithAuthorNameEmail()) + manifest["skills"] = "./skills/" + if err := shared.WriteJSON(filepath.Join(view, ".qoder-plugin", "plugin.json"), manifest); err != nil { + return nil, err + } + return nil, verifyProjectView(in.StagingPath, in.Plan) +} + +// verifyProjectView checks the complete importable surface. Raw source payload +// remains outside this directory and cannot acquire native discovery semantics. +func verifyProjectView(active string, plan domain.DeliveryPlan) error { + names, err := selectedProjectSkills(plan) + if err != nil { + return err + } + view := filepath.Join(active, projectNativeView) + if err := pathpolicy.RequireContainedChild(active, view); err != nil { + return err + } + if err := verifyProjectViewPaths(active, view, names); err != nil { + return err + } + manifest, err := readProjectManifest(filepath.Join(view, ".qoder-plugin", "plugin.json")) + if err != nil { + return err + } + if manifest["name"] != plan.DeclaredName || manifest["skills"] != "./skills/" || (plan.DeclaredVersion != "" && manifest["version"] != plan.DeclaredVersion) { + return fmt.Errorf("qoder native view manifest differs from selected identity") + } + allowed := map[string]bool{"name": true, "version": true, "description": true, "homepage": true, "repository": true, "license": true, "author": true, "keywords": true, "skills": true} + for key := range manifest { + if !allowed[key] { + return fmt.Errorf("qoder native view manifest contains an unqualified native resource") + } + } + return nil +} + +func verifyProjectViewPaths(active, view string, names map[string]bool) error { + for root, expected := range map[string]map[string]bool{ + view: {".qoder-plugin": true, "skills": true}, + filepath.Join(view, ".qoder-plugin"): {"plugin.json": true}, + filepath.Join(view, "skills"): names, + } { + entries, err := os.ReadDir(root) + if err != nil { + return err + } + if len(entries) != len(expected) { + return fmt.Errorf("qoder native view has an unexpected path set") + } + for _, entry := range entries { + if !expected[entry.Name()] { + return fmt.Errorf("qoder native view contains an unselected path") + } + if err := pathpolicy.RequireContainedChild(active, filepath.Join(root, entry.Name())); err != nil { + return err + } + } + } + for name := range names { + skill := filepath.Join(view, "skills", name, "SKILL.md") + if err := pathpolicy.RequireContainedChild(active, skill); err != nil { + return err + } + info, err := os.Lstat(skill) + if err != nil { + return err + } + if !info.Mode().IsRegular() { + return fmt.Errorf("qoder projected skill is not a regular file") + } + } + return nil +} + +func readProjectManifest(path string) (map[string]any, error) { + file, err := openProjectManifest(path) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + info, err := file.Stat() + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() || info.Size() > 1<<20 { + return nil, fmt.Errorf("qoder native manifest is not a bounded regular file") + } + body, err := io.ReadAll(io.LimitReader(file, (1<<20)+1)) + if err != nil { + return nil, err + } + if len(body) > 1<<20 { + return nil, fmt.Errorf("qoder native manifest exceeds byte limit") + } + return shared.DecodeStrictJSONObject(body) +} + +func verifyProjectBundle(ctx context.Context, request domain.ActivationRequest) error { + if len(request.Delivery.NativeObjects) != 1 { + return fmt.Errorf("qoder project delivery requires its exact managed package receipt") + } + object := request.Delivery.NativeObjects[0] + if object.Kind != "managed_package_directory" || object.ObjectID != "package:qoder:"+request.Plan.PhysicalArtifactID || object.Path != request.Plan.ActivePath || object.LogicalName != request.DeclaredName || object.ManagedDigest == "" || object.ManagedDigest != request.Delivery.ArtifactDigest { + return fmt.Errorf("qoder project managed receipt differs from delivered identity") + } + digest, err := (packagesnapshot.Builder{}).Digest(ctx, request.Plan.ActivePath) + if err != nil { + return err + } + if digest != object.ManagedDigest { + return fmt.Errorf("qoder project package digest differs from its managed receipt") + } + if err := verifyProjectView(request.Plan.ActivePath, request.Plan); err != nil { + return err + } + // The closed-view reads cannot adopt a package changed during verification. + digest, err = (packagesnapshot.Builder{}).Digest(ctx, request.Plan.ActivePath) + if err != nil { + return err + } + if digest != object.ManagedDigest { + return fmt.Errorf("qoder project package changed during verification") + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/qoder/project_lifecycle.go b/install/integrationctl/agentplugins/clients/qoder/project_lifecycle.go new file mode 100644 index 000000000..92d6193ea --- /dev/null +++ b/install/integrationctl/agentplugins/clients/qoder/project_lifecycle.go @@ -0,0 +1,109 @@ +package qoder + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ActivationPreflighter = (*Adapter)(nil) + _ clients.PreparedRegistryInspector = (*Adapter)(nil) +) + +func projectActions(plan domain.DeliveryPlan) []string { + view := filepath.Join(plan.ActivePath, projectNativeView) + command, _ := json.Marshal(struct { + WorkingDirectory string `json:"working_directory"` + Argv []string `json:"argv"` + }{WorkingDirectory: plan.TargetContext.ProjectRoot, Argv: []string{"qoder", "plugins", "install", view, "--scope", "project", "--json"}}) + return []string{ + "Qoder project skill bundle is prepared at " + view + "; native discovery, loaded selection, trust, authentication and MCP are not established.", + "Optional separate native import (UAP does not execute this command): " + string(command), + "Review existing native registrations and cache first: native import writes the user's shared native registry/cache, and same-name/same-version replacement remains unqualified.", + "A manually imported native cached copy is independent of this UAP bundle. UAP removal removes only its project bundle; it does not unregister or delete that native copy.", + } +} + +func (*Adapter) PreflightActivation(_ clients.Env, request domain.ActivationRequest) error { + if !projectRequested(request.Plan) { + return nil + } + if err := validateProjectPlan(request.Client, request.Plan); err != nil { + return err + } + if request.Plan.Status == domain.PlanUnsupported { + return fmt.Errorf("qoder project package is unsupported") + } + if request.ActivationComplete { + return fmt.Errorf("qoder project preparation cannot be promoted by activation attestation") + } + if request.Delivery.ClientID != "" && request.Delivery.ClientID != domain.ClientQoder { + return fmt.Errorf("qoder project delivery client differs from plan") + } + if (request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath) || (request.Delivery.OwnedBase != "" && request.Delivery.OwnedBase != request.Plan.TargetRoot) || (request.DeclaredName != "" && request.DeclaredName != request.Plan.DeclaredName) { + return fmt.Errorf("qoder project delivered identity differs from plan") + } + _, err := selectedProjectSkills(request.Plan) + return err +} + +func activateProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if err := New().PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + if request.Delivery.ClientID != domain.ClientQoder || request.Delivery.ActivePath != request.Plan.ActivePath || request.Delivery.OwnedBase != request.Plan.TargetRoot || request.DeclaredName != request.Plan.DeclaredName { + return domain.ActivationOutcome{}, fmt.Errorf("qoder project delivered identity differs from plan") + } + if err := verifyProjectBundle(ctx, request); err != nil { + return domain.ActivationOutcome{}, err + } + outcome := shared.StartedActivation(request) + outcome.Activation, outcome.Verification = domain.ActivationPrepared, domain.VerificationInstalled + outcome.NativeEffect = domain.NativeEffectUnchanged + outcome.ActivationAttested = false + outcome.UserActions = projectActions(request.Plan) + return outcome, nil +} + +func deactivateProject(ctx context.Context, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + if err := ctx.Err(); err != nil { + return outcome, err + } + root, err := projectRoot(request.Client, request.TargetContext) + if err != nil { + return outcome, err + } + if request.NativeRegistryRoot != root || !request.SelectedDelivery.IsZero() { + return outcome, fmt.Errorf("qoder project removal profile differs from selected namespace") + } + binding := domain.ClientBinding{ClientID: "qoder", Scope: "project", NativeProfileRoot: root, PhysicalArtifact: request.PhysicalArtifactID, TargetLocator: request.ManagedArtifactPath, NativeObjects: request.NativeObjects} + if err := New().ValidateBindingProfile(root, binding); err != nil { + return outcome, err + } + if !request.Confirmed { + outcome.UserActions = []string{"agentplugins will remove only its exact prepared Qoder project bundle"} + return outcome, nil + } + outcome.ExternalRemovalComplete = true + outcome.UserActions = []string{"Any separately imported Qoder cached copy remains native state; UAP did not unregister or delete it."} + return outcome, nil +} + +func (*Adapter) InspectPreparedRegistry(plan domain.DeliveryPlan, name string, owned bool) (clients.RegistryFinding, error) { + if projectRequested(plan) { + if err := validateProjectPlan(domain.DetectedClient{ClientID: domain.ClientQoder, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return clients.RegistryIndeterminate, err + } + if name != plan.DeclaredName { + return clients.RegistryIndeterminate, fmt.Errorf("qoder prepared name differs from plan") + } + } + return shared.InspectUnqualifiedPluginRoot(plan.TargetRoot, name, plan.ActivePath, owned) +} diff --git a/install/integrationctl/agentplugins/clients/qoder/project_open_unix.go b/install/integrationctl/agentplugins/clients/qoder/project_open_unix.go new file mode 100644 index 000000000..eff554c49 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/qoder/project_open_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package qoder + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func openProjectManifest(path string) (*os.File, error) { + return os.OpenFile(path, os.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) +} diff --git a/install/integrationctl/agentplugins/clients/qoder/project_open_windows.go b/install/integrationctl/agentplugins/clients/qoder/project_open_windows.go new file mode 100644 index 000000000..5db971f8b --- /dev/null +++ b/install/integrationctl/agentplugins/clients/qoder/project_open_windows.go @@ -0,0 +1,29 @@ +//go:build windows + +package qoder + +import ( + "fmt" + "os" + + "golang.org/x/sys/windows" +) + +func openProjectManifest(path string) (*os.File, error) { + pointer, err := windows.UTF16PtrFromString(path) + if err != nil { + return nil, err + } + handle, err := windows.CreateFile(pointer, windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OPEN_REPARSE_POINT, 0) + if err != nil { + return nil, err + } + var info windows.ByHandleFileInformation + kind, kindErr := windows.GetFileType(handle) + infoErr := windows.GetFileInformationByHandle(handle, &info) + if kindErr != nil || infoErr != nil || kind != windows.FILE_TYPE_DISK || info.FileAttributes&(windows.FILE_ATTRIBUTE_REPARSE_POINT|windows.FILE_ATTRIBUTE_DIRECTORY) != 0 { + _ = windows.CloseHandle(handle) + return nil, fmt.Errorf("qoder native manifest requires an ordinary disk file") + } + return os.NewFile(uintptr(handle), path), nil +} diff --git a/install/integrationctl/agentplugins/clients/qoder/registry.go b/install/integrationctl/agentplugins/clients/qoder/registry.go index 28f2b140e..b910eb6ec 100644 --- a/install/integrationctl/agentplugins/clients/qoder/registry.go +++ b/install/integrationctl/agentplugins/clients/qoder/registry.go @@ -160,7 +160,28 @@ var _ clients.RegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return true } -func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, _ domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { + if projectRequested(plan) { + if err := ctx.Err(); err != nil { + return clients.RegistryIndeterminate, err + } + if err := validateProjectPlan(client, plan); err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + if managed.Scope != "project" { + return clients.RegistryIndeterminate, fmt.Errorf("qoder project inspection requires a project binding") + } + if err := New().ValidateBindingProfile(plan.NativeRegistryRoot, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + if managed.TargetLocator != plan.ActivePath || managed.PhysicalArtifact != plan.PhysicalArtifactID { + return clients.RegistryIndeterminate, fmt.Errorf("qoder project native inspection received another binding") + } + } + // Local prepared receipts never prove a native Qoder registration. + return clients.RegistryClear, nil + } if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } diff --git a/install/integrationctl/agentplugins/clients/qoder/registry_test.go b/install/integrationctl/agentplugins/clients/qoder/registry_test.go index 6b3166769..413af0a05 100644 --- a/install/integrationctl/agentplugins/clients/qoder/registry_test.go +++ b/install/integrationctl/agentplugins/clients/qoder/registry_test.go @@ -174,3 +174,23 @@ func TestQoderListingLocalDeadlineStopsOSProcess(t *testing.T) { t.Fatalf("stalled producer completed after deadline: %v", err) } } + +// Genuine unchanged bytes from run40135008690/source2b0ef181, the preserved +// B-installed-project-A-list stream. Global inventory has repeated ids with +// different scope/projectPath/version; legacy user mutation must not collapse it. +func TestQoderGenuineScopedGlobalInventoryCannotBecomeUniqueUserOwnership(t *testing.T) { + body, err := os.ReadFile("testdata/qoder-1.1.78-scoped-global-list.json") + if err != nil { + t.Fatal(err) + } + var rows []map[string]any + if err := json.Unmarshal(body, &rows); err != nil || len(rows) != 3 { + t.Fatalf("raw fixture: %v %v", rows, err) + } + if rows[0]["scope"] != "user" || rows[1]["scope"] != "project" || rows[2]["scope"] != "project" || rows[1]["projectPath"] == rows[2]["projectPath"] || rows[0]["id"] != rows[2]["id"] { + t.Fatal("genuine inventory fixture lost global scoped shape") + } + if _, err := parseList(body); !errors.Is(err, errUnknownList) { + t.Fatalf("global repeated ids became one owned user entry: %v", err) + } +} diff --git a/install/integrationctl/agentplugins/clients/qoder/scope.go b/install/integrationctl/agentplugins/clients/qoder/scope.go new file mode 100644 index 000000000..510de6573 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/qoder/scope.go @@ -0,0 +1,138 @@ +package qoder + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanQualifier = (*Adapter)(nil) + _ clients.ProfileBindingValidator = (*Adapter)(nil) +) + +func projectRequested(plan domain.DeliveryPlan) bool { + return plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" +} + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject || in.TargetContext.ProjectRoot != "" { + return filepath.Join(in.TargetContext.ProjectRoot, ".uap", "managed", "clients", "qoder"), "" + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func projectRoot(client domain.DetectedClient, target domain.TargetContext) (string, error) { + selected, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if client.ClientID != domain.ClientQoder || selected.Scope != domain.ScopeProject || selected.UserConfigRoot == "" || selected.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("qoder project requires explicit project scope and the original user profile") + } + canonical, err := filepath.EvalSymlinks(selected.ProjectRoot) + if err != nil { + return "", err + } + info, err := os.Lstat(selected.ProjectRoot) + if err != nil { + return "", err + } + if canonical != selected.ProjectRoot || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return "", fmt.Errorf("qoder project root must remain an existing canonical directory") + } + root := filepath.Join(selected.ProjectRoot, ".uap", "managed", "clients", "qoder") + if pathsOverlap(selected.ProjectRoot, client.ConfigRoot) { + return "", fmt.Errorf("qoder project and original user profile must not overlap") + } + if err := pathpolicy.RequireContainedChild(selected.ProjectRoot, root); err != nil { + return "", err + } + return root, nil +} + +func pathsOverlap(left, right string) bool { + return left == right || strings.HasPrefix(left, right+string(filepath.Separator)) || strings.HasPrefix(right, left+string(filepath.Separator)) +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, _ string, target domain.TargetContext) (string, string, error) { + if mode != domain.PackageNative { + return "", "", fmt.Errorf("qoder project preparation requires native package delivery") + } + root, err := projectRoot(client, target) + if err != nil { + return "", "", err + } + return filepath.Join(target.ProjectRoot, ".uap", "managed"), root, nil +} + +func validateProjectPlan(client domain.DetectedClient, plan domain.DeliveryPlan) error { + if plan.ClientID != domain.ClientQoder || plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject || !plan.SelectedDelivery.IsZero() { + return fmt.Errorf("qoder project lifecycle requires matching client, scope and delivery") + } + root, err := projectRoot(client, plan.TargetContext) + if err != nil { + return err + } + if err := pathpolicy.ValidateLeafID(plan.DeclaredName); err != nil { + return err + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return err + } + if plan.NativeRegistryRoot != root || plan.NativeRegistryExecutable != "" || plan.TargetAnchor != filepath.Join(plan.TargetContext.ProjectRoot, ".uap", "managed") || plan.TargetRoot != root || plan.ActivePath != filepath.Join(root, plan.PhysicalArtifactID) { + return fmt.Errorf("qoder project artifact differs from the exact selected managed namespace") + } + return pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, plan.ActivePath) +} + +func (*Adapter) QualifyPlan(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if !projectRequested(*plan) && in.TargetContext.ProjectRoot == "" && in.TargetContext.Scope != domain.ScopeProject { + return nil + } + if plan.Scope != domain.ScopeProject || in.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("qoder project plan scope mismatch") + } + if _, err := projectRoot(in.Client, in.TargetContext); err != nil { + return err + } + for i := range plan.Components { + if plan.Components[i].Kind != domain.ComponentSkill { + plan.Components[i].Support = domain.SupportUnsupported + plan.Components[i].Reason = "Qoder project preparation supports skill bundles only; native MCP, hooks and other resources are unqualified" + } + } + return nil +} + +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil + } + project := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(root)))) + if binding.ClientID != "qoder" || binding.NativeProfileRoot != root || root != filepath.Join(project, ".uap", "managed", "clients", "qoder") || !binding.SelectedDelivery.IsZero() { + return fmt.Errorf("qoder project binding profile differs from the selected namespace") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + if binding.TargetLocator != filepath.Join(root, binding.PhysicalArtifact) { + return fmt.Errorf("qoder project binding locator differs from physical artifact") + } + if len(binding.NativeObjects) != 1 { + return fmt.Errorf("qoder project binding requires its exact managed package receipt") + } + for _, object := range binding.NativeObjects { + if object.Kind != "managed_package_directory" || object.Path != binding.TargetLocator || object.ObjectID != "package:qoder:"+binding.PhysicalArtifact || object.ManagedDigest == "" { + return fmt.Errorf("qoder project binding contains foreign native ownership") + } + } + return pathpolicy.RequireContainedChild(project, binding.TargetLocator) +} diff --git a/install/integrationctl/agentplugins/clients/qoder/testdata/qoder-1.1.78-scoped-global-list.json b/install/integrationctl/agentplugins/clients/qoder/testdata/qoder-1.1.78-scoped-global-list.json new file mode 100644 index 000000000..4aa9f7f96 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/qoder/testdata/qoder-1.1.78-scoped-global-list.json @@ -0,0 +1,76 @@ +[ + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "0.9.0", + "scope": "user", + "enabled": true, + "canDisable": true, + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/0.9.0", + "installedAt": "2026-10-24T22:08:07.501Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert USER research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.0.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-A", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.0.0", + "installedAt": "2026-10-24T22:08:10.193Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert A research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + }, + { + "id": "uap-scope-research@local", + "name": "uap-scope-research", + "source": "uap-scope-research@local", + "version": "1.1.0", + "scope": "project", + "enabled": true, + "canDisable": true, + "projectPath": "/tmp/qoder-populated-w490rb05/project-B", + "installPath": "/tmp/qoder-populated-w490rb05/home/.qoder/plugins/cache/local/uap-scope-research/1.1.0", + "installedAt": "2026-10-24T22:08:12.859Z", + "description": "Inert scoped research fixture", + "resources": { + "skills": [ + { + "name": "marker", + "description": "Inert B research marker" + } + ], + "agents": [], + "mcpServers": [], + "commands": [], + "hooks": [] + } + } +] diff --git a/install/integrationctl/agentplugins/clients/scope.go b/install/integrationctl/agentplugins/clients/scope.go new file mode 100644 index 000000000..3ad03e2b8 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/scope.go @@ -0,0 +1,31 @@ +package clients + +import "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + +// SupportsScope checks declared capability and concrete project hooks without +// loading package bytes or touching native configuration. +func SupportsScope(registry *Registry, id domain.ClientID, scope domain.InstallScope) bool { + if registry == nil { + return false + } + if _, ok := registry.Lookup(id); !ok { + return false + } + definition, ok := domain.ClientDefinitionFor(id) + if !ok { + return false + } + declared := false + for _, candidate := range definition.Capabilities.Scopes { + declared = declared || candidate == scope + } + if !declared || scope != domain.ScopeProject { + return declared + } + if !definition.ProjectPolicy.Valid() || definition.PlansWithoutHostPresence || definition.Traits.RequiresPersonalMappingForPrepare { + return false + } + _, layout := As[ProjectTargetLayout](registry, id) + _, native := As[NativeRegistryLayout](registry, id) + return layout && native +} diff --git a/install/integrationctl/agentplugins/clients/shared/project_components.go b/install/integrationctl/agentplugins/clients/shared/project_components.go new file mode 100644 index 000000000..c9796bbea --- /dev/null +++ b/install/integrationctl/agentplugins/clients/shared/project_components.go @@ -0,0 +1,261 @@ +package shared + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "unicode" + "unicode/utf8" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// ProjectComponents names one adapter's already-selected native file layout. +// It supplies ownership mechanics, not client discovery or settings policy. +type ProjectComponents struct { + ProjectRoot, SkillsRoot string + ConfigPaths nativeconfig.Paths + Codec nativeconfig.Codec + SkillKind, MCPKind string +} + +func (layout ProjectComponents) validate() error { + if !cleanComponentPath(layout.ProjectRoot) || layout.SkillKind == "" || layout.MCPKind == "" || layout.SkillKind == layout.MCPKind { + return fmt.Errorf("invalid project component layout") + } + paths := []string{layout.SkillsRoot, layout.ConfigPaths.JSON} + if layout.ConfigPaths.JSONC != "" { + paths = append(paths, layout.ConfigPaths.JSONC) + } + for _, path := range paths { + if !cleanComponentPath(path) { + return fmt.Errorf("project component path must be clean and absolute") + } + if err := pathpolicy.RequireContainedChild(layout.ProjectRoot, path); err != nil { + return err + } + } + return nil +} + +func cleanComponentPath(path string) bool { + return utf8.ValidString(path) && filepath.IsAbs(path) && filepath.Clean(path) == path && filepath.Dir(path) != path && strings.TrimSpace(path) == path && strings.IndexFunc(path, unicode.IsControl) < 0 +} + +func (layout ProjectComponents) ValidateObjects(objects []domain.NativeObjectOwnership) error { + if err := layout.validate(); err != nil { + return err + } + seen := map[string]bool{} + for _, object := range objects { + if strings.TrimSpace(object.LogicalName) != object.LogicalName { + return fmt.Errorf("project component name must not be padded") + } + if err := pathpolicy.ValidateLeafID(object.LogicalName); err != nil { + return err + } + if object.ObjectID != object.Kind+":"+object.LogicalName || seen[object.ObjectID] || object.ManagedDigest == "" { + return fmt.Errorf("invalid or duplicate project component ownership") + } + seen[object.ObjectID] = true + if err := layout.validateObjectPath(object); err != nil { + return err + } + } + return nil +} + +func (layout ProjectComponents) validateObjectPath(object domain.NativeObjectOwnership) error { + switch object.Kind { + case layout.SkillKind: + if object.Path != filepath.Join(layout.SkillsRoot, object.LogicalName) { + return fmt.Errorf("project skill path differs from selected layout") + } + if err := componentRelative(object.SourceRelative); err != nil { + return err + } + case layout.MCPKind: + if object.Path != layout.ConfigPaths.JSON && (layout.ConfigPaths.JSONC == "" || object.Path != layout.ConfigPaths.JSONC) { + return fmt.Errorf("project MCP path differs from selected layout") + } + default: + return fmt.Errorf("unknown project component ownership kind %q", object.Kind) + } + if !cleanComponentPath(object.Path) { + return fmt.Errorf("invalid project native object path") + } + if err := pathpolicy.RequireContainedChild(layout.ProjectRoot, object.Path); err != nil { + return err + } + return nil +} + +func componentRelative(path string) error { + if path == "" || strings.ContainsAny(path, "\\:") || strings.IndexFunc(path, unicode.IsControl) >= 0 { + return fmt.Errorf("invalid portable skill source directory") + } + for _, part := range strings.Split(path, "/") { + if err := pathpolicy.ValidatePortablePathSegment(part); err != nil { + return fmt.Errorf("invalid portable skill source segment: %w", err) + } + } + return nil +} + +func (layout ProjectComponents) SkillObject(name, sourceRelative, stagingRoot string) (domain.NativeObjectOwnership, error) { + object := domain.NativeObjectOwnership{ObjectID: layout.SkillKind + ":" + name, Kind: layout.SkillKind, LogicalName: name, + Path: filepath.Join(layout.SkillsRoot, name), SourceRelative: sourceRelative, ProtectionClass: "managed"} + if err := layout.packagePath(stagingRoot); err != nil { + return object, err + } + if err := componentRelative(sourceRelative); err != nil { + return object, err + } + source := filepath.Join(stagingRoot, filepath.FromSlash(sourceRelative)) + if err := pathpolicy.RequireContainedChild(stagingRoot, source); err != nil { + return object, err + } + digest, err := DigestSkillDirectory(source) + if err != nil { + return object, err + } + object.ManagedDigest = digest + return object, layout.ValidateObjects([]domain.NativeObjectOwnership{object}) +} + +func (layout ProjectComponents) packagePath(root string) error { + if !cleanComponentPath(root) { + return fmt.Errorf("project package root must be clean and absolute") + } + if err := pathpolicy.RequireContainedChild(layout.ProjectRoot, root); err != nil { + return err + } + info, err := os.Lstat(root) + if err != nil { + return err + } + if !info.IsDir() { + return fmt.Errorf("project package root must be a real directory") + } + return nil +} + +func (layout ProjectComponents) MCPObject(name string, server nativeconfig.Server, resolvedPath string) (domain.NativeObjectOwnership, error) { + receipt, err := nativeconfig.DesiredReceipt(resolvedPath, layout.Codec, name, server, nativeconfig.Placeholders{}) + object := domain.NativeObjectOwnership{ObjectID: layout.MCPKind + ":" + name, Kind: layout.MCPKind, LogicalName: name, + Path: resolvedPath, ManagedDigest: receipt.Digest, ProtectionClass: "managed"} + if err != nil { + return object, err + } + return object, layout.ValidateObjects([]domain.NativeObjectOwnership{object}) +} + +func (layout ProjectComponents) receipt(object domain.NativeObjectOwnership) *nativeconfig.Receipt { + return &nativeconfig.Receipt{Version: "1", Path: object.Path, Codec: layout.Codec, Name: object.LogicalName, Digest: object.ManagedDigest} +} + +func verifyComponentSkill(object domain.NativeObjectOwnership, allowMissing bool) error { + info, err := os.Lstat(object.Path) + if os.IsNotExist(err) && allowMissing { + return nil + } + if err != nil { + return err + } + if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("managed project skill is not a real directory") + } + digest, err := DigestSkillDirectory(object.Path) + if err != nil { + return err + } + if digest != object.ManagedDigest { + return fmt.Errorf("managed project skill %q changed outside agentplugins", object.LogicalName) + } + return nil +} + +func (layout ProjectComponents) Verify(objects []domain.NativeObjectOwnership, allowMissing bool, kernel nativeconfig.Kernel) error { + if err := layout.ValidateObjects(objects); err != nil { + return err + } + if err := kernel.RequireFileIO(); err != nil { + return err + } + for _, object := range objects { + if object.Kind == layout.SkillKind { + if err := verifyComponentSkill(object, allowMissing); err != nil { + return err + } + continue + } + present, owned, err := kernel.Inspect(layout.ConfigPaths, layout.Codec, object.LogicalName, layout.receipt(object)) + if err != nil { + return err + } + if !present && allowMissing { + continue + } + if !present || !owned { + return fmt.Errorf("managed project MCP server %q changed outside agentplugins", object.LogicalName) + } + } + return nil +} + +func (layout ProjectComponents) Inspect(components []domain.ComponentDecision, managed *domain.ClientBinding, kernel nativeconfig.Kernel) (clients.RegistryFinding, error) { + prior := []domain.NativeObjectOwnership{} + if managed != nil { + prior = managed.NativeObjects + } + if err := layout.Verify(prior, true, kernel); err != nil { + return clients.RegistryIndeterminate, err + } + ownership := ObjectMap(prior) + finding := clients.RegistryClear + for _, component := range components { + if component.Support == domain.SupportUnsupported { + continue + } + present, owned, err := layout.inspectComponent(component, ownership, kernel) + if err != nil { + return clients.RegistryIndeterminate, err + } + if present && !owned { + return clients.RegistryCollision, nil + } + if present { + finding = clients.RegistryExpected + } + } + return finding, nil +} + +func (layout ProjectComponents) inspectComponent(component domain.ComponentDecision, ownership map[string]domain.NativeObjectOwnership, kernel nativeconfig.Kernel) (bool, bool, error) { + if strings.TrimSpace(component.Name) != component.Name { + return false, false, fmt.Errorf("project component name must not be padded") + } + if err := pathpolicy.ValidateLeafID(component.Name); err != nil { + return false, false, err + } + if component.Kind == domain.ComponentSkill { + _, err := os.Lstat(filepath.Join(layout.SkillsRoot, component.Name)) + if err != nil && !os.IsNotExist(err) { + return false, false, err + } + _, owned := ownership[layout.SkillKind+":"+component.Name] + return err == nil, owned, nil + } + if component.Kind == domain.ComponentMCPServer { + var receipt *nativeconfig.Receipt + if object, ok := ownership[layout.MCPKind+":"+component.Name]; ok { + receipt = layout.receipt(object) + } + return kernel.Inspect(layout.ConfigPaths, layout.Codec, component.Name, receipt) + } + return false, false, fmt.Errorf("unsupported native project component kind %q", component.Kind) +} diff --git a/install/integrationctl/agentplugins/clients/shared/project_components_apply.go b/install/integrationctl/agentplugins/clients/shared/project_components_apply.go new file mode 100644 index 000000000..096cd735a --- /dev/null +++ b/install/integrationctl/agentplugins/clients/shared/project_components_apply.go @@ -0,0 +1,69 @@ +package shared + +import ( + "context" + "errors" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func componentUnchanged(err error) error { + return &NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} +} + +func (mutation ProjectComponentMutation) Apply(ctx context.Context) error { + requests, err := mutation.prepare(ctx) + if err != nil { + return componentUnchanged(err) + } + txn := newProjectSkillSwap(mutation) + if err := txn.stage(ctx); err != nil { + return componentUnchanged(errors.Join(err, txn.cleanup())) + } + return mutation.commit(ctx, requests, txn) +} + +func (mutation ProjectComponentMutation) commit(ctx context.Context, requests []nativeconfig.Request, txn *projectSkillSwap) error { + if err := txn.swap(ctx); err != nil { + return rollbackProjectSkills(txn, err, true) + } + if err := mutation.Layout.Verify(projectSkillObjects(mutation), false, mutation.Kernel); err != nil { + return rollbackProjectSkills(txn, err, true) + } + if err := ctx.Err(); err != nil { + return rollbackProjectSkills(txn, err, true) + } + _, err := mutation.Kernel.ApplyBatch(requests) + if err != nil && !nativeconfig.IsCommittedCleanup(err) { + certain := errors.Is(err, nativeconfig.ErrCollision) || errors.Is(err, nativeconfig.ErrNotOwned) + return rollbackProjectSkills(txn, err, certain) + } + if cleanupErr := txn.cleanup(); cleanupErr != nil { + return &nativeconfig.CommittedCleanupError{Err: errors.Join(err, cleanupErr)} + } + return err +} + +func projectSkillObjects(mutation ProjectComponentMutation) []domain.NativeObjectOwnership { + objects := []domain.NativeObjectOwnership{} + for _, object := range mutation.Desired { + if object.Kind == mutation.Layout.SkillKind { + objects = append(objects, object) + } + } + return objects +} + +func rollbackProjectSkills(txn *projectSkillSwap, cause error, unchanged bool) error { + if err := txn.restore(); err != nil { + return fmt.Errorf("%w; project skill rollback refused: %w; recovery retained at %q", cause, err, txn.root) + } + cleanupErr := txn.cleanup() + result := errors.Join(cause, cleanupErr) + if unchanged { + return componentUnchanged(result) + } + return result +} diff --git a/install/integrationctl/agentplugins/clients/shared/project_components_recovery_test.go b/install/integrationctl/agentplugins/clients/shared/project_components_recovery_test.go new file mode 100644 index 000000000..106ccf4ee --- /dev/null +++ b/install/integrationctl/agentplugins/clients/shared/project_components_recovery_test.go @@ -0,0 +1,122 @@ +package shared + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" +) + +func componentAssertBody(t *testing.T, path, want string) { + t.Helper() + body, err := os.ReadFile(path) + if err != nil || string(body) != want { + t.Fatalf("component bytes = %q, %v; want %q", body, err, want) + } +} + +func componentRecovery(t *testing.T, mutation ProjectComponentMutation) string { + t.Helper() + roots, err := filepath.Glob(filepath.Join(mutation.Layout.SkillsRoot, ".agentplugins-native-*")) + if err != nil || len(roots) != 1 { + t.Fatalf("retained recovery roots = %v, %v", roots, err) + } + return roots[0] +} + +func componentInstalled(t *testing.T) ProjectComponentMutation { + t.Helper() + mutation := componentFixture(t) + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + return mutation +} + +func TestProjectComponentsMCPFailureRestoresExactSkill(t *testing.T) { + mutation := componentNext(t, componentInstalled(t)) + before, _ := os.ReadFile(mutation.Layout.ConfigPaths.JSON) + mutation.Kernel = nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + componentAssertBody(t, filepath.Join(mutation.Layout.SkillsRoot, "docs", "SKILL.md"), "inert version two\n") + return nil, fmt.Errorf("inert fixture lock refusal") + }) + if err := mutation.Apply(context.Background()); err == nil { + t.Fatal("MCP failure not surfaced") + } + componentAssertBody(t, filepath.Join(mutation.Layout.SkillsRoot, "docs", "SKILL.md"), "inert version one\n") + componentAssertBody(t, mutation.Layout.ConfigPaths.JSON, string(before)) + roots, _ := filepath.Glob(filepath.Join(mutation.Layout.SkillsRoot, ".agentplugins-native-*")) + if len(roots) != 0 { + t.Fatalf("successful rollback retained recoveries: %v", roots) + } +} + +func TestProjectComponentsRollbackPreservesExternallyChangedInstall(t *testing.T) { + mutation := componentNext(t, componentInstalled(t)) + mutation.Kernel = nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + componentWrite(t, filepath.Join(mutation.Layout.SkillsRoot, "docs", "SKILL.md"), "external changed install\n") + return nil, fmt.Errorf("inert fixture lock refusal") + }) + err := mutation.Apply(context.Background()) + if err == nil || !strings.Contains(err.Error(), "recovery retained") { + t.Fatalf("lost rollback recovery: %v", err) + } + componentAssertBody(t, filepath.Join(mutation.Layout.SkillsRoot, "docs", "SKILL.md"), "external changed install\n") + recovery := componentRecovery(t, mutation) + componentAssertBody(t, filepath.Join(recovery, "old-docs", "SKILL.md"), "inert version one\n") + var effect *NativeEffectError + if errors.As(err, &effect) { + t.Fatalf("uncertain rollback claimed unchanged: %v", err) + } +} + +func TestProjectComponentsCommittedCleanupRefusesChangedRecovery(t *testing.T) { + for _, drift := range []string{"backup", "unknown"} { + t.Run(drift, func(t *testing.T) { + mutation := componentNext(t, componentInstalled(t)) + mutation.Kernel = nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return func() error { + root := componentRecovery(t, mutation) + if drift == "backup" { + componentWrite(t, filepath.Join(root, "old-docs", "SKILL.md"), "foreign recovery change\n") + } else { + componentWrite(t, filepath.Join(root, "foreign.txt"), "foreign recovery\n") + } + return nil + }, nil + }) + err := mutation.Apply(context.Background()) + if !nativeconfig.IsCommittedCleanup(err) { + t.Fatalf("committed cleanup lost authoritative effect: %v", err) + } + if err := mutation.Layout.Verify(mutation.Desired, false, mutation.Kernel); err != nil { + t.Fatal(err) + } + root := componentRecovery(t, mutation) + if drift == "backup" { + componentAssertBody(t, filepath.Join(root, "old-docs", "SKILL.md"), "foreign recovery change\n") + } else { + componentAssertBody(t, filepath.Join(root, "foreign.txt"), "foreign recovery\n") + } + }) + } +} + +func TestProjectComponentsCancelledBeforeMutation(t *testing.T) { + mutation := componentFixture(t) + before, _ := os.ReadFile(mutation.Layout.ConfigPaths.JSON) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if err := mutation.Apply(ctx); !errors.Is(err, context.Canceled) { + t.Fatalf("cancel error = %v", err) + } + componentAssertBody(t, mutation.Layout.ConfigPaths.JSON, string(before)) + if _, err := os.Lstat(mutation.Desired[0].Path); !os.IsNotExist(err) { + t.Fatalf("canceled request installed skill: %v", err) + } +} diff --git a/install/integrationctl/agentplugins/clients/shared/project_components_requests.go b/install/integrationctl/agentplugins/clients/shared/project_components_requests.go new file mode 100644 index 000000000..528393eac --- /dev/null +++ b/install/integrationctl/agentplugins/clients/shared/project_components_requests.go @@ -0,0 +1,148 @@ +package shared + +import ( + "context" + "fmt" + "os" + "sort" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// ProjectComponentMutation swaps exact skill directories around one kernel +// batch. The adapter owns scope selection and prepares all native server values. +type ProjectComponentMutation struct { + Layout ProjectComponents + ActivePath string + Previous, Desired []domain.NativeObjectOwnership + Servers map[string]nativeconfig.Server + Kernel nativeconfig.Kernel +} + +func (mutation ProjectComponentMutation) prepare(ctx context.Context) ([]nativeconfig.Request, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := mutation.Layout.ValidateObjects(mutation.Desired); err != nil { + return nil, err + } + if err := mutation.Layout.Verify(mutation.Previous, true, mutation.Kernel); err != nil { + return nil, err + } + if len(mutation.Desired) > 0 { + if err := mutation.Layout.packagePath(mutation.ActivePath); err != nil { + return nil, err + } + } + previous := ObjectMap(mutation.Previous) + desiredMCP := 0 + for _, next := range mutation.Desired { + if prior, ok := previous[next.ObjectID]; ok { + if prior.Path != next.Path || prior.Kind != next.Kind || prior.LogicalName != next.LogicalName { + return nil, fmt.Errorf("project component identity changed") + } + } else if err := mutation.requireAbsent(next); err != nil { + return nil, err + } + if next.Kind == mutation.Layout.MCPKind { + desiredMCP++ + } + } + if len(mutation.Servers) != desiredMCP { + return nil, fmt.Errorf("prepared MCP server selection differs from desired ownership") + } + return mutation.requests(previous, ObjectMap(mutation.Desired)) +} + +func (mutation ProjectComponentMutation) requireAbsent(object domain.NativeObjectOwnership) error { + if object.Kind == mutation.Layout.SkillKind { + _, err := os.Lstat(object.Path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + return fmt.Errorf("project skill %q exists without ownership", object.LogicalName) + } + present, _, err := mutation.Kernel.Inspect(mutation.Layout.ConfigPaths, mutation.Layout.Codec, object.LogicalName, nil) + if err != nil { + return err + } + if present { + return fmt.Errorf("project MCP server %q exists without ownership", object.LogicalName) + } + return nil +} + +func (mutation ProjectComponentMutation) requests(previous, desired map[string]domain.NativeObjectOwnership) ([]nativeconfig.Request, error) { + ids := map[string]bool{} + for id := range previous { + ids[id] = true + } + for id := range desired { + ids[id] = true + } + ordered := make([]string, 0, len(ids)) + for id := range ids { + ordered = append(ordered, id) + } + sort.Strings(ordered) + requests := []nativeconfig.Request{} + for _, id := range ordered { + prior, hadPrior := previous[id] + next, hasNext := desired[id] + if prior.Kind != mutation.Layout.MCPKind && next.Kind != mutation.Layout.MCPKind { + continue + } + request, include, err := mutation.request(prior, next, hadPrior, hasNext) + if err != nil { + return nil, err + } + if include { + requests = append(requests, request) + } + } + return requests, nil +} + +func (mutation ProjectComponentMutation) request(prior, next domain.NativeObjectOwnership, hadPrior, hasNext bool) (nativeconfig.Request, bool, error) { + name := prior.LogicalName + if hasNext { + name = next.LogicalName + } + var owned *nativeconfig.Receipt + if hadPrior { + owned = mutation.Layout.receipt(prior) + } + present, valid, err := mutation.Kernel.Inspect(mutation.Layout.ConfigPaths, mutation.Layout.Codec, name, owned) + if err != nil { + return nativeconfig.Request{}, false, err + } + request := nativeconfig.Request{Paths: mutation.Layout.ConfigPaths, Codec: mutation.Layout.Codec, Name: name, Owned: owned} + if !hasNext { + request.Action = nativeconfig.ActionRemove + return request, present, nil + } + server, ok := mutation.Servers[name] + if !ok { + return request, false, fmt.Errorf("missing prepared MCP server %q", name) + } + expected, err := mutation.Layout.MCPObject(name, server, next.Path) + if err != nil { + return request, false, err + } + if expected.ManagedDigest != next.ManagedDigest { + return request, false, fmt.Errorf("prepared MCP server %q differs from staged receipt", name) + } + request.Server, request.Desired = server, mutation.Layout.receipt(next) + request.Action = nativeconfig.ActionAdd + if present { + if !valid { + return request, false, fmt.Errorf("project MCP server %q is no longer owned", name) + } + request.Action = nativeconfig.ActionUpdate + } + return request, true, nil +} diff --git a/install/integrationctl/agentplugins/clients/shared/project_components_skills.go b/install/integrationctl/agentplugins/clients/shared/project_components_skills.go new file mode 100644 index 000000000..97df95c1a --- /dev/null +++ b/install/integrationctl/agentplugins/clients/shared/project_components_skills.go @@ -0,0 +1,261 @@ +package shared + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/filetree" + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type projectSkillSwap struct { + mutation ProjectComponentMutation + root string + identity os.FileInfo + known map[string]domain.NativeObjectOwnership + staged, backups map[string]string + installed map[string]domain.NativeObjectOwnership + unchanged map[string]bool +} + +func newProjectSkillSwap(mutation ProjectComponentMutation) *projectSkillSwap { + return &projectSkillSwap{mutation: mutation, known: map[string]domain.NativeObjectOwnership{}, + staged: map[string]string{}, backups: map[string]string{}, installed: map[string]domain.NativeObjectOwnership{}, unchanged: map[string]bool{}} +} + +func (txn *projectSkillSwap) create() error { + if txn.root != "" { + return txn.ownedRoot() + } + layout := txn.mutation.Layout + if err := pathpolicy.RequireContainedChild(layout.ProjectRoot, layout.SkillsRoot); err != nil { + return err + } + if err := os.MkdirAll(layout.SkillsRoot, 0700); err != nil { + return err + } + root, err := os.MkdirTemp(layout.SkillsRoot, ".agentplugins-native-") + if err != nil { + return err + } + txn.root = root + txn.identity, err = os.Lstat(root) + return err +} + +func (txn *projectSkillSwap) stage(ctx context.Context) error { + previous := ObjectMap(txn.mutation.Previous) + for _, object := range projectSkillObjects(txn.mutation) { + if err := ctx.Err(); err != nil { + return err + } + if err := txn.stageSkill(object, previous); err != nil { + return err + } + } + for _, object := range txn.mutation.Previous { + if object.Kind == txn.mutation.Layout.SkillKind && !txn.unchanged[object.ObjectID] { + if err := txn.create(); err != nil { + return err + } + } + } + return nil +} + +func (txn *projectSkillSwap) stageSkill(object domain.NativeObjectOwnership, previous map[string]domain.NativeObjectOwnership) error { + source := filepath.Join(txn.mutation.ActivePath, filepath.FromSlash(object.SourceRelative)) + if err := pathpolicy.RequireContainedChild(txn.mutation.ActivePath, source); err != nil { + return err + } + sourceObject := object + sourceObject.Path = source + if err := verifyComponentSkill(sourceObject, false); err != nil { + return err + } + if prior, ok := previous[object.ObjectID]; ok && prior.ManagedDigest == object.ManagedDigest { + if _, err := os.Lstat(object.Path); err == nil { + if err := verifyComponentSkill(object, false); err != nil { + return err + } + txn.unchanged[object.ObjectID] = true + return nil + } else if !os.IsNotExist(err) { + return err + } + } + if err := txn.create(); err != nil { + return err + } + name := "new-" + object.LogicalName + target := filepath.Join(txn.root, name) + txn.known[name] = object + if err := filetree.CopyDir(source, target); err != nil { + return err + } + staged := object + staged.Path = target + if err := verifyComponentSkill(staged, false); err != nil { + return err + } + txn.staged[object.ObjectID] = name + return nil +} + +func (txn *projectSkillSwap) swap(ctx context.Context) error { + if txn.root != "" { + if err := txn.ownedRoot(); err != nil { + return err + } + } + for _, object := range txn.mutation.Previous { + if object.Kind != txn.mutation.Layout.SkillKind || txn.unchanged[object.ObjectID] { + continue + } + if err := ctx.Err(); err != nil { + return err + } + if _, err := os.Lstat(object.Path); os.IsNotExist(err) { + continue + } else if err != nil { + return err + } + name := "old-" + object.LogicalName + if err := pathpolicy.RequireContainedChild(txn.mutation.Layout.ProjectRoot, object.Path); err != nil { + return err + } + if err := RenameDirectoryExclusive(object.Path, filepath.Join(txn.root, name)); err != nil { + return err + } + txn.backups[object.ObjectID], txn.known[name] = name, object + isolated := object + isolated.Path = filepath.Join(txn.root, name) + if err := verifyComponentSkill(isolated, false); err != nil { + return err + } + } + desired := ObjectMap(txn.mutation.Desired) + for id, name := range txn.staged { + if err := ctx.Err(); err != nil { + return err + } + object := desired[id] + if err := pathpolicy.RequireContainedChild(txn.mutation.Layout.ProjectRoot, object.Path); err != nil { + return err + } + if err := RenameDirectoryExclusive(filepath.Join(txn.root, name), object.Path); err != nil { + return err + } + delete(txn.known, name) + txn.installed[id] = object + } + return nil +} + +func (txn *projectSkillSwap) restore() error { + if txn.root != "" { + if err := txn.ownedRoot(); err != nil { + return err + } + } + var failures []error + for _, object := range txn.installed { + if err := txn.removeInstalled(object); err != nil { + failures = append(failures, err) + } + } + previous := ObjectMap(txn.mutation.Previous) + for id, name := range txn.backups { + object := previous[id] + isolated := object + isolated.Path = filepath.Join(txn.root, name) + if err := txn.ownedRoot(); err != nil { + failures = append(failures, err) + continue + } + if err := verifyComponentSkill(isolated, false); err != nil { + failures = append(failures, err) + continue + } + if err := pathpolicy.RequireContainedChild(txn.mutation.Layout.ProjectRoot, object.Path); err != nil { + failures = append(failures, err) + continue + } + if err := RenameDirectoryExclusive(isolated.Path, object.Path); err != nil { + failures = append(failures, err) + continue + } + delete(txn.known, name) + } + return errors.Join(failures...) +} + +func (txn *projectSkillSwap) removeInstalled(object domain.NativeObjectOwnership) error { + if _, err := os.Lstat(object.Path); os.IsNotExist(err) { + return nil + } + if err := pathpolicy.RequireContainedChild(txn.mutation.Layout.ProjectRoot, object.Path); err != nil { + return err + } + if err := verifyComponentSkill(object, false); err != nil { + return err + } + return os.RemoveAll(object.Path) +} + +func (txn *projectSkillSwap) ownedRoot() error { + if err := pathpolicy.RequireContainedChild(txn.mutation.Layout.ProjectRoot, txn.root); err != nil { + return err + } + observed, err := os.Lstat(txn.root) + if err != nil { + return err + } + if txn.identity == nil || !os.SameFile(txn.identity, observed) { + return fmt.Errorf("private project skill transaction identity changed") + } + return nil +} + +func (txn *projectSkillSwap) cleanup() error { + if txn.root == "" { + return nil + } + if err := txn.validateCleanup(); err != nil { + return fmt.Errorf("preserved project skill recovery at %q: %w", txn.root, err) + } + for name := range txn.known { + if err := os.RemoveAll(filepath.Join(txn.root, name)); err != nil { + return fmt.Errorf("cleanup project skill recovery at %q: %w", txn.root, err) + } + } + return os.Remove(txn.root) +} + +func (txn *projectSkillSwap) validateCleanup() error { + if err := txn.ownedRoot(); err != nil { + return err + } + entries, err := os.ReadDir(txn.root) + if err != nil { + return err + } + if len(entries) != len(txn.known) { + return fmt.Errorf("private project skill transaction contents changed") + } + for _, entry := range entries { + object, ok := txn.known[entry.Name()] + if !ok { + return fmt.Errorf("unknown private project skill transaction entry") + } + object.Path = filepath.Join(txn.root, entry.Name()) + if err := verifyComponentSkill(object, false); err != nil { + return err + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/shared/project_components_test.go b/install/integrationctl/agentplugins/clients/shared/project_components_test.go new file mode 100644 index 000000000..cc7b615aa --- /dev/null +++ b/install/integrationctl/agentplugins/clients/shared/project_components_test.go @@ -0,0 +1,212 @@ +package shared + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func componentWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func componentFixture(t *testing.T) ProjectComponentMutation { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + active := filepath.Join(root, ".uap", "managed", "demo") + layout := ProjectComponents{ProjectRoot: root, SkillsRoot: filepath.Join(root, ".agents", "skills"), + ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, ".cursor", "mcp.json")}, + Codec: nativeconfig.CodecMCPServers, SkillKind: "fixture_project_skill", MCPKind: "fixture_project_mcp"} + componentWrite(t, layout.ConfigPaths.JSON, `{"mcpServers":{"foreign":{"url":"https://foreign.test/mcp","disabled":true}},"policy":"keep"}`) + componentWrite(t, filepath.Join(active, "skills", "docs", "SKILL.md"), "inert version one\n") + skill, err := layout.SkillObject("docs", "skills/docs", active) + if err != nil { + t.Fatal(err) + } + server := nativeconfig.Server{Type: "remote", URL: "https://managed.test/v1"} + mcp, err := layout.MCPObject("docs", server, layout.ConfigPaths.JSON) + if err != nil { + t.Fatal(err) + } + return ProjectComponentMutation{Layout: layout, ActivePath: active, Desired: []domain.NativeObjectOwnership{skill, mcp}, Servers: map[string]nativeconfig.Server{"docs": server}, Kernel: nativeconfig.New()} +} + +func componentNext(t *testing.T, mutation ProjectComponentMutation) ProjectComponentMutation { + t.Helper() + mutation.Previous = append([]domain.NativeObjectOwnership(nil), mutation.Desired...) + componentWrite(t, filepath.Join(mutation.ActivePath, "skills", "docs", "SKILL.md"), "inert version two\n") + skill, err := mutation.Layout.SkillObject("docs", "skills/docs", mutation.ActivePath) + if err != nil { + t.Fatal(err) + } + server := nativeconfig.Server{Type: "remote", URL: "https://managed.test/v2"} + mcp, err := mutation.Layout.MCPObject("docs", server, mutation.Layout.ConfigPaths.JSON) + if err != nil { + t.Fatal(err) + } + mutation.Desired, mutation.Servers = []domain.NativeObjectOwnership{skill, mcp}, map[string]nativeconfig.Server{"docs": server} + return mutation +} + +func TestProjectComponentsLifecycleAndMissingSamePinRepair(t *testing.T) { + mutation := componentFixture(t) + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + mutation.Previous = mutation.Desired + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(mutation.Desired[0].Path); err != nil { + t.Fatal(err) + } + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + if err := mutation.Layout.Verify(mutation.Desired, false, mutation.Kernel); err != nil { + t.Fatal(err) + } + mutation = componentNext(t, mutation) + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + if err := mutation.Layout.Verify(mutation.Desired, false, mutation.Kernel); err != nil { + t.Fatal(err) + } + mutation.Previous, mutation.Desired, mutation.Servers = mutation.Desired, nil, nil + for range 2 { + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + } + body, err := os.ReadFile(mutation.Layout.ConfigPaths.JSON) + if err != nil || strings.Contains(string(body), "managed.test") || !strings.Contains(string(body), `"disabled":true`) || !strings.Contains(string(body), `"policy":"keep"`) { + t.Fatalf("remove lost foreign data: %s, %v", body, err) + } + if _, err := os.Stat(filepath.Join(mutation.Layout.SkillsRoot, "docs")); !os.IsNotExist(err) { + t.Fatalf("skill survived remove: %v", err) + } +} + +func TestProjectComponentsPrewriteTamperRefuses(t *testing.T) { + for _, test := range []string{"extra-server", "missing-server", "source-content", "source-relative", "outside-active", "receipt"} { + t.Run(test, func(t *testing.T) { + mutation := componentFixture(t) + before, _ := os.ReadFile(mutation.Layout.ConfigPaths.JSON) + switch test { + case "extra-server": + mutation.Servers["extra"] = mutation.Servers["docs"] + case "missing-server": + delete(mutation.Servers, "docs") + case "source-content": + componentWrite(t, filepath.Join(mutation.ActivePath, "skills", "docs", "SKILL.md"), "foreign drift") + case "source-relative": + mutation.Desired[0].SourceRelative = "skills/../docs" + case "outside-active": + mutation.ActivePath = t.TempDir() + case "receipt": + mutation.Desired[1].ManagedDigest = "sha256:foreign" + } + if err := mutation.Apply(context.Background()); err == nil { + t.Fatal("tampered projection admitted") + } + actual, _ := os.ReadFile(mutation.Layout.ConfigPaths.JSON) + if string(actual) != string(before) { + t.Fatal("prewrite refusal changed MCP config") + } + if _, err := os.Lstat(mutation.Desired[0].Path); !os.IsNotExist(err) { + t.Fatalf("prewrite refusal installed skill: %v", err) + } + }) + } +} + +func TestProjectComponentsContainedAliasesRefuse(t *testing.T) { + for _, target := range []string{"skills", "config"} { + t.Run(target, func(t *testing.T) { + mutation := componentFixture(t) + destination := filepath.Join(mutation.Layout.ProjectRoot, "real-"+target) + if err := os.Mkdir(destination, 0700); err != nil { + t.Fatal(err) + } + alias := filepath.Join(mutation.Layout.ProjectRoot, "alias-"+target) + if err := os.Symlink(destination, alias); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if target == "skills" { + mutation.Layout.SkillsRoot = alias + } else { + mutation.Layout.ConfigPaths.JSON = filepath.Join(alias, "mcp.json") + } + if err := mutation.Layout.ValidateObjects(nil); err == nil { + t.Fatal("contained native alias admitted") + } + }) + } +} + +func TestProjectComponentsOwnershipSpellingRefuses(t *testing.T) { + for _, relative := range []string{"skills/docs.", "skills/CON", "skills/a?b", "skills/\xff", "skills/cafe\u0301", "skills//docs", "skills/./docs", "skills/../docs"} { + t.Run(relative, func(t *testing.T) { + mutation := componentFixture(t) + mutation.Desired[0].SourceRelative = relative + if err := mutation.Layout.ValidateObjects(mutation.Desired); err == nil { + t.Fatalf("nonportable source accepted: %q", relative) + } + }) + } + mutation := componentFixture(t) + object := &mutation.Desired[0] + object.LogicalName = " docs " + object.ObjectID = object.Kind + ":" + object.LogicalName + object.Path = filepath.Join(mutation.Layout.SkillsRoot, object.LogicalName) + if err := mutation.Layout.ValidateObjects(mutation.Desired); err == nil { + t.Fatal("padded logical name admitted") + } +} + +func TestProjectComponentsMCPOnlyDoesNotCreateSkillTransaction(t *testing.T) { + mutation := componentFixture(t) + mutation.Desired = mutation.Desired[1:] + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + mutation.Previous = mutation.Desired + server := nativeconfig.Server{Type: "remote", URL: "https://managed.test/mcp-only-v2"} + object, err := mutation.Layout.MCPObject("docs", server, mutation.Layout.ConfigPaths.JSON) + if err != nil { + t.Fatal(err) + } + mutation.Desired, mutation.Servers = []domain.NativeObjectOwnership{object}, map[string]nativeconfig.Server{"docs": server} + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + if err := mutation.Layout.Verify(mutation.Desired, false, mutation.Kernel); err != nil { + t.Fatal(err) + } + mutation.Previous, mutation.Desired, mutation.Servers = mutation.Desired, nil, nil + if err := mutation.Apply(context.Background()); err != nil { + t.Fatal(err) + } + body, err := os.ReadFile(mutation.Layout.ConfigPaths.JSON) + if err != nil || strings.Contains(string(body), "managed.test") || !strings.Contains(string(body), `"disabled":true`) { + t.Fatalf("MCP-only foreign preservation failed: %s, %v", body, err) + } + if _, err := os.Lstat(mutation.Layout.SkillsRoot); !os.IsNotExist(err) { + t.Fatalf("MCP-only request created skill transaction/root: %v", err) + } +} diff --git a/install/integrationctl/agentplugins/clients/vscode/plan.go b/install/integrationctl/agentplugins/clients/vscode/plan.go index e73cb5bb2..406f34ad7 100644 --- a/install/integrationctl/agentplugins/clients/vscode/plan.go +++ b/install/integrationctl/agentplugins/clients/vscode/plan.go @@ -2,6 +2,7 @@ package vscode import ( "context" + "fmt" "strings" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" @@ -12,8 +13,19 @@ import ( var ( _ clients.NativeRegistryLayout = (*Adapter)(nil) _ clients.PlanRefiner = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) ) +// CheckPlanPrecondition keeps the retained user/Copilot backend separate from +// the explicitly composed workspace adapter, even if static metadata declares +// project support. +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject || plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + return fmt.Errorf("VS Code project installation requires vscode.NewProject or clients/all.Project composition") + } + return nil +} + // NativeRegistry points at the Copilot installation VS Code discovers: the // package is registered through the CLI of the sibling this client shares the // github-copilot backend with, so the plan records that registry, not its own. diff --git a/install/integrationctl/agentplugins/clients/vscode/project_active.go b/install/integrationctl/agentplugins/clients/vscode/project_active.go new file mode 100644 index 000000000..c61b2cc15 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/vscode/project_active.go @@ -0,0 +1,45 @@ +package vscode + +import ( + "fmt" + "path/filepath" + "reflect" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Reuse validates the complete immutable stdio projection against the selected +// package and data receipt. It never rewrites old locators into fresh authority. +func validateWorkspaceStdio(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) error { + for _, name := range shared.SupportedMCPNames(plan) { + source := envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + if !filepath.IsAbs(dataPath) || filepath.Clean(dataPath) != dataPath { + return fmt.Errorf("%w: VS Code workspace selected stdio data path is not absolute and clean", domain.ErrProjectionRefreshRequired) + } + entry := shared.CloneObject(source.Decoded) + delete(entry, "type") + if err := shared.ApplyStdioDataContract(entry, plan.ActivePath, dataPath, root); err != nil { + return err + } + expected, err := decodeWorkspaceServer(entry) + if err != nil { + return err + } + expected, err = bindWorkspaceStdio(expected, source, plan.ActivePath, dataPath) + if err != nil { + return err + } + actual, err := readWorkspaceServer(root, name) + if err != nil { + return err + } + if !reflect.DeepEqual(actual, expected) { + return fmt.Errorf("%w: VS Code workspace immutable stdio package/data/launcher projection differs", domain.ErrProjectionRefreshRequired) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/vscode/project_adapter.go b/install/integrationctl/agentplugins/clients/vscode/project_adapter.go new file mode 100644 index 000000000..83ddea79f --- /dev/null +++ b/install/integrationctl/agentplugins/clients/vscode/project_adapter.go @@ -0,0 +1,162 @@ +package vscode + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// ProjectAdapter prepares one selected local workspace's owned skills and MCP +// entries. It deliberately embeds neither the user Copilot adapter nor Local. +type ProjectAdapter struct{} + +func NewProject() *ProjectAdapter { return &ProjectAdapter{} } + +var ( + _ clients.Adapter = (*ProjectAdapter)(nil) + _ clients.HostDetector = (*ProjectAdapter)(nil) + _ clients.ProjectTargetLayout = (*ProjectAdapter)(nil) + _ clients.NativeRegistryLayout = (*ProjectAdapter)(nil) + _ clients.PlanPrecondition = (*ProjectAdapter)(nil) + _ clients.PlanRefiner = (*ProjectAdapter)(nil) + _ clients.ProfileBindingValidator = (*ProjectAdapter)(nil) + _ clients.CompatibilityLimiter = (*ProjectAdapter)(nil) +) + +func (*ProjectAdapter) ID() domain.ClientID { return domain.ClientVSCode } + +func (*ProjectAdapter) DetectSurfaces(host clients.Host) clients.Detection { + // This detector probes code and the original VS Code profile. It does not + // use the Copilot sibling to establish presence or workspace authority. + return New().DetectSurfaces(host) +} + +func (*ProjectAdapter) NativeRegistry(in clients.PlanInput) (string, string) { + return filepath.Join(in.TargetContext.ProjectRoot, ".vscode"), "" +} + +func (*ProjectAdapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managed string, target domain.TargetContext) (string, string, error) { + root, err := workspaceRoot(client, target, "") + if err != nil { + return "", "", err + } + if managed != filepath.Join(root, ".uap", "managed") { + return "", "", fmt.Errorf("VS Code managed root differs from selected project") + } + return shared.ManagedTargetRoot(client, mode, managed) +} + +func (*ProjectAdapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("VS Code project adapter requires explicit project composition and scope") + } + _, err := workspaceRoot(in.Client, plan.TargetContext, plan.NativeRegistryRoot) + return err +} + +func (*ProjectAdapter) RefinePlan(ctx context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if err := ctx.Err(); err != nil { + return err + } + root, err := workspacePlanRoot(in.Client, *plan) + if err != nil { + return err + } + if plan.Status == domain.PlanUnsupported { + return nil + } + shared.PromoteNativeReady(plan, filepath.Join(root, ".vscode"), shared.OnlyNativeComponents(plan.Components)) + plan.Activation = domain.ActivationPrepared + plan.UserActions = shared.AppendUnique(plan.UserActions, workspaceAction(root)) + return nil +} + +func (*ProjectAdapter) ClientLimitations(domain.PackageEnvelope) []string { return nil } + +func (*ProjectAdapter) ComponentLimitations(_ domain.PackageEnvelope, item domain.ComponentDecision) ([]string, []string) { + if item.Kind != domain.ComponentSkill && item.Kind != domain.ComponentMCPServer { + return []string{"vscode_project_component_unsupported"}, nil + } + return nil, []string{"vscode_project_prepared_files_only"} +} + +func workspaceRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + selected, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if client.ClientID != domain.ClientVSCode || selected.Scope != domain.ScopeProject || selected.UserConfigRoot == "" || selected.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("VS Code workspace requires matching project scope and original user profile") + } + root := selected.ProjectRoot + if planned != "" && planned != filepath.Join(root, ".vscode") { + return "", fmt.Errorf("VS Code native registry differs from selected workspace") + } + for _, pair := range [][2]string{{root, client.ConfigRoot}, {client.ConfigRoot, root}} { + rel, err := filepath.Rel(pair[0], pair[1]) + if err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("VS Code project overlaps the original user profile") + } + } + for _, path := range []string{filepath.Join(root, ".github", "skills"), filepath.Join(root, ".vscode", "mcp.json")} { + if err := pathpolicy.RequireContainedChild(root, path); err != nil { + return "", err + } + } + return root, nil +} + +func workspacePlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.ClientID != domain.ClientVSCode || plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject || plan.NativeRegistryExecutable != "" { + return "", fmt.Errorf("VS Code project plan requires matching file-only workspace authority") + } + root, err := workspaceRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if plan.NativeRegistryRoot != filepath.Join(root, ".vscode") { + return "", fmt.Errorf("VS Code project native registry is missing") + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + managed := filepath.Join(root, ".uap", "managed") + target := filepath.Join(managed, "clients", "vscode") + if plan.TargetAnchor != managed || plan.TargetRoot != target || plan.ActivePath != filepath.Join(target, plan.PhysicalArtifactID) { + return "", fmt.Errorf("VS Code managed package differs from selected workspace") + } + return root, pathpolicy.RequireContainedChild(root, plan.ActivePath) +} + +func (*ProjectAdapter) ValidateBindingProfile(registry string, binding domain.ClientBinding) error { + if binding.ClientID != string(domain.ClientVSCode) || binding.Scope != string(domain.ScopeProject) || filepath.Base(registry) != ".vscode" || binding.NativeProfileRoot != registry { + return fmt.Errorf("VS Code binding workspace differs") + } + if err := pathpolicy.ValidateLeafID(binding.PhysicalArtifact); err != nil { + return err + } + root := filepath.Dir(registry) + expected := filepath.Join(root, ".uap", "managed", "clients", "vscode", binding.PhysicalArtifact) + if binding.TargetLocator != expected { + return fmt.Errorf("VS Code binding package differs from selected workspace") + } + if err := pathpolicy.RequireContainedChild(root, expected); err != nil { + return err + } + for _, object := range binding.NativeObjects { + if object.Kind == "managed_package_directory" && (object.Path != expected || object.ManagedDigest == "") { + return fmt.Errorf("VS Code package ownership differs from selected workspace") + } + } + return workspaceComponents(root).ValidateObjects(workspaceOwned(binding.NativeObjects)) +} + +func workspaceAction(root string) string { + return fmt.Sprintf("After VS Code project component files are installed for %q, open that folder, review workspace/server trust and inspect skills and MCP. Native discovery, effective Local/Copilot selection, approvals, MCP initialization and active sessions remain unobserved", root) +} diff --git a/install/integrationctl/agentplugins/clients/vscode/project_components.go b/install/integrationctl/agentplugins/clients/vscode/project_components.go new file mode 100644 index 000000000..644cf6a0a --- /dev/null +++ b/install/integrationctl/agentplugins/clients/vscode/project_components.go @@ -0,0 +1,109 @@ +package vscode + +import ( + "fmt" + "path/filepath" + "sort" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const workspaceSkillKind = "vscode_project_skill_directory" +const workspaceMCPKind = "vscode_project_mcp_server" + +func workspaceComponents(root string) shared.ProjectComponents { + return shared.ProjectComponents{ProjectRoot: root, SkillsRoot: filepath.Join(root, ".github", "skills"), + ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, ".vscode", "mcp.json")}, Codec: nativeconfig.CodecVSCode, + SkillKind: workspaceSkillKind, MCPKind: workspaceMCPKind} +} + +func workspaceOwned(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + selected := []domain.NativeObjectOwnership{} + for _, object := range objects { + if object.Kind != "managed_package_directory" { + selected = append(selected, object) + } + } + return selected +} + +func workspaceObjects(staging string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + layout := workspaceComponents(plan.TargetContext.ProjectRoot) + objects := []domain.NativeObjectOwnership{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + object, err := workspaceObject(layout, staging, envelope, component) + if err != nil { + return nil, err + } + objects = append(objects, object) + } + sort.Slice(objects, func(i, j int) bool { return objects[i].ObjectID < objects[j].ObjectID }) + return objects, layout.ValidateObjects(objects) +} + +func workspaceObject(layout shared.ProjectComponents, staging string, envelope domain.PackageEnvelope, component domain.ComponentDecision) (domain.NativeObjectOwnership, error) { + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return domain.NativeObjectOwnership{}, fmt.Errorf("planned VS Code workspace skill is missing") + } + relative := filepath.ToSlash(filepath.Dir(filepath.FromSlash(skill.RelativePath))) + if skill.RelativePath == "" { + relative = "skills/" + component.Name + } + return layout.SkillObject(component.Name, relative, staging) + case domain.ComponentMCPServer: + server, err := readWorkspaceServer(staging, component.Name) + if err != nil { + return domain.NativeObjectOwnership{}, err + } + return layout.MCPObject(component.Name, server, layout.ConfigPaths.JSON) + default: + return domain.NativeObjectOwnership{}, fmt.Errorf("unsupported VS Code project component %q", component.Kind) + } +} + +func workspaceServers(active string, objects []domain.NativeObjectOwnership) (map[string]nativeconfig.Server, error) { + servers := map[string]nativeconfig.Server{} + for _, object := range objects { + if object.Kind != workspaceMCPKind { + continue + } + server, err := readWorkspaceServer(active, object.LogicalName) + if err != nil { + return nil, err + } + servers[object.LogicalName] = server + } + return servers, nil +} + +func workspaceSelection(plan domain.DeliveryPlan, objects []domain.NativeObjectOwnership) error { + selected := map[string]bool{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + kind := workspaceSkillKind + if component.Kind == domain.ComponentMCPServer { + kind = workspaceMCPKind + } + selected[kind+":"+component.Name] = true + } + owned := workspaceOwned(objects) + if len(owned) != len(selected) { + return fmt.Errorf("VS Code delivered component selection is incomplete") + } + for _, object := range owned { + if !selected[object.ObjectID] { + return fmt.Errorf("VS Code delivered component differs from selection") + } + } + return workspaceComponents(plan.TargetContext.ProjectRoot).ValidateObjects(owned) +} diff --git a/install/integrationctl/agentplugins/clients/vscode/project_lifecycle.go b/install/integrationctl/agentplugins/clients/vscode/project_lifecycle.go new file mode 100644 index 000000000..0c57d998c --- /dev/null +++ b/install/integrationctl/agentplugins/clients/vscode/project_lifecycle.go @@ -0,0 +1,148 @@ +package vscode + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.Lifecycle = (*ProjectAdapter)(nil) + _ clients.AutomaticActivator = (*ProjectAdapter)(nil) + _ clients.ReadOnlyVerifier = (*ProjectAdapter)(nil) + _ clients.ActivationPreflighter = (*ProjectAdapter)(nil) + _ clients.RegistryInspector = (*ProjectAdapter)(nil) + _ clients.PreparedRegistryInspector = (*ProjectAdapter)(nil) +) + +func (*ProjectAdapter) AutomaticallyActivates(_ clients.Env, req domain.ActivationRequest) bool { + return req.Plan.Scope == domain.ScopeProject && req.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(req.Plan.Components) +} + +func (*ProjectAdapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, _ string) bool { + return plan.Scope == domain.ScopeProject && len(plan.Components) > 0 +} + +func (*ProjectAdapter) PreflightActivation(env clients.Env, req domain.ActivationRequest) error { + root, err := workspacePlanRoot(req.Client, req.Plan) + if err != nil { + return err + } + if req.ActivationComplete { + return fmt.Errorf("VS Code workspace discovery cannot be established by activation attestation") + } + if req.Plan.Status == domain.PlanUnsupported || !shared.OnlyNativeComponents(req.Plan.Components) { + return fmt.Errorf("VS Code project delivery requires supported skills and MCP components") + } + if req.DeclaredName != "" && req.DeclaredName != req.Plan.DeclaredName || req.Delivery.ClientID != "" && req.Delivery.ClientID != domain.ClientVSCode || req.Delivery.ActivePath != "" && req.Delivery.ActivePath != req.Plan.ActivePath || req.Delivery.OwnedBase != "" && req.Delivery.OwnedBase != req.Plan.TargetRoot { + return fmt.Errorf("VS Code delivered identity differs from workspace plan") + } + layout := workspaceComponents(root) + if err := layout.ValidateObjects(workspaceOwned(req.PreviousNativeObjects)); err != nil { + return err + } + if err := layout.ValidateObjects(workspaceOwned(req.Delivery.NativeObjects)); err != nil { + return err + } + return env.NativeConfig.RequireFileIO() +} + +func (adapter *ProjectAdapter) Activate(ctx context.Context, env clients.Env, req domain.ActivationRequest) (domain.ActivationOutcome, error) { + if err := shared.ActivationIdentityMismatch(req); err != nil { + return domain.ActivationOutcome{}, err + } + if err := adapter.PreflightActivation(env, req); err != nil { + return domain.ActivationOutcome{}, err + } + if req.Delivery.ActivePath != req.Plan.ActivePath || req.Delivery.OwnedBase != req.Plan.TargetRoot || req.DeclaredName != req.Plan.DeclaredName { + return domain.ActivationOutcome{}, fmt.Errorf("VS Code workspace delivery is incomplete") + } + if err := workspaceSelection(req.Plan, req.Delivery.NativeObjects); err != nil { + return domain.ActivationOutcome{}, err + } + layout := workspaceComponents(req.Plan.TargetContext.ProjectRoot) + out, err := shared.CompleteNativeConfigActivation(ctx, req, shared.NativeConfigActivation{ + Automatic: adapter.AutomaticallyActivates(env, req), + UnavailableAction: "select supported VS Code project components", RepairAction: "repair exact owned VS Code project components", + RetryAction: "retry VS Code workspace component preparation", CompletedAction: workspaceAction(req.Plan.TargetContext.ProjectRoot), + Verify: func() error { + return layout.Verify(workspaceOwned(req.Delivery.NativeObjects), false, env.NativeConfig) + }, + Activate: func(ctx context.Context, r domain.ActivationRequest) error { + objects := workspaceOwned(r.Delivery.NativeObjects) + servers, err := workspaceServers(r.Delivery.ActivePath, objects) + if err != nil { + return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} + } + return (shared.ProjectComponentMutation{Layout: layout, ActivePath: r.Delivery.ActivePath, Previous: workspaceOwned(r.PreviousNativeObjects), Desired: objects, Servers: servers, Kernel: env.NativeConfig}).Apply(ctx) + }, + }) + if err == nil && out.Verification == domain.VerificationInstalled { + out.Activation, out.ActivationAttested = domain.ActivationPrepared, false + } + return out, err +} + +func (adapter *ProjectAdapter) Deactivate(ctx context.Context, env clients.Env, req domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + out := shared.StartedDeactivation() + root, err := workspaceRoot(req.Client, req.TargetContext, req.NativeRegistryRoot) + if err != nil { + return out, err + } + if req.NativeRegistryRoot != filepath.Join(root, ".vscode") { + return out, fmt.Errorf("VS Code removal native workspace authority is missing") + } + binding := domain.ClientBinding{ClientID: "vscode", Scope: "project", NativeProfileRoot: filepath.Join(root, ".vscode"), PhysicalArtifact: req.PhysicalArtifactID, TargetLocator: req.ManagedArtifactPath, NativeObjects: req.NativeObjects} + if err := adapter.ValidateBindingProfile(filepath.Join(root, ".vscode"), binding); err != nil { + return out, err + } + if !req.Confirmed { + out.UserActions = []string{"agentplugins will remove only exact owned VS Code workspace skills and MCP members"} + return out, nil + } + err = (shared.ProjectComponentMutation{Layout: workspaceComponents(root), Previous: workspaceOwned(req.NativeObjects), Kernel: env.NativeConfig}).Apply(ctx) + if err != nil && !shared.CommittedNativeDeactivationCleanup(&out, err) { + return out, err + } + out.ExternalRemovalComplete = true + return out, nil +} + +func (*ProjectAdapter) UsesNativeRegistryExecutable() bool { return false } + +func (adapter *ProjectAdapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { + if err := ctx.Err(); err != nil { + return clients.RegistryIndeterminate, err + } + root, err := workspacePlanRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + if managed.PhysicalArtifact != plan.PhysicalArtifactID || managed.TargetLocator != plan.ActivePath { + return clients.RegistryIndeterminate, fmt.Errorf("VS Code binding differs from selected physical artifact") + } + if err := adapter.ValidateBindingProfile(filepath.Join(root, ".vscode"), *managed); err != nil { + return clients.RegistryIndeterminate, err + } + selected := *managed + selected.NativeObjects = workspaceOwned(managed.NativeObjects) + managed = &selected + } + return workspaceComponents(root).Inspect(plan.Components, managed, env.NativeConfig) +} + +func (*ProjectAdapter) InspectPreparedRegistry(plan domain.DeliveryPlan, name string, _ bool) (clients.RegistryFinding, error) { + _, err := workspacePlanRoot(domain.DetectedClient{ClientID: domain.ClientVSCode, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if name != plan.DeclaredName { + return clients.RegistryIndeterminate, fmt.Errorf("VS Code prepared workspace identity differs") + } + return clients.RegistryClear, nil +} diff --git a/install/integrationctl/agentplugins/clients/vscode/project_mcp.go b/install/integrationctl/agentplugins/clients/vscode/project_mcp.go new file mode 100644 index 000000000..8459e643c --- /dev/null +++ b/install/integrationctl/agentplugins/clients/vscode/project_mcp.go @@ -0,0 +1,134 @@ +package vscode + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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 readWorkspaceMCP(root string) (map[string]any, map[string]any, error) { + path := filepath.Join(root, "mcp.json") + if err := pathpolicy.RequireContainedChild(root, path); err != nil { + return nil, nil, err + } + file, err := nativeconfig.New().ReadExactFile(path) + if err != nil { + return nil, nil, err + } + if !file.Exists || len(file.Body) > 1<<20 { + return nil, nil, fmt.Errorf("VS Code workspace package MCP document is missing or exceeds inspection limit") + } + document, err := shared.DecodeStrictJSONObject(file.Body) + if err != nil { + return nil, nil, err + } + servers, ok := document["mcpServers"].(map[string]any) + if !ok { + return nil, nil, fmt.Errorf("VS Code workspace package MCP collection is missing") + } + return document, servers, nil +} + +func readWorkspaceServer(root, name string) (nativeconfig.Server, error) { + _, servers, err := readWorkspaceMCP(root) + if err != nil { + return nativeconfig.Server{}, err + } + entry, ok := servers[name].(map[string]any) + if !ok { + return nativeconfig.Server{}, fmt.Errorf("VS Code workspace selected MCP server is missing") + } + return decodeWorkspaceServer(entry) +} + +func decodeWorkspaceServer(entry map[string]any) (nativeconfig.Server, error) { + for key := range entry { + switch key { + case "type", "command", "args", "env", "cwd", "url", "headers": + default: + return nativeconfig.Server{}, fmt.Errorf("unsupported VS Code projected MCP field %q", key) + } + } + body, err := json.Marshal(entry) + if err != nil { + return nativeconfig.Server{}, err + } + var server nativeconfig.Server + if err := json.Unmarshal(body, &server); err != nil { + return server, err + } + switch server.Type { + case "", "stdio": + server.Type = "stdio" + case "http": + server.Type, server.RemoteTransport = "remote", "streamable-http" + case "sse": + server.Type, server.RemoteTransport = "remote", "sse" + default: + return server, fmt.Errorf("VS Code workspace projected MCP transport is unsupported") + } + server.StdioValuesResolved, server.CWDResolved = true, true + return server, nil +} + +func projectWorkspaceStdio(in clients.ProjectionInput) error { + if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { + return err + } + if len(shared.SupportedMCPNames(in.Plan)) == 0 { + return nil + } + document, entries, err := readWorkspaceMCP(in.StagingPath) + if err != nil { + return err + } + for _, name := range shared.SupportedMCPNames(in.Plan) { + source := in.Envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + server, err := readWorkspaceServer(in.StagingPath, name) + if err != nil { + return err + } + server, err = bindWorkspaceStdio(server, source, in.Plan.ActivePath, in.PluginDataPath) + if err != nil { + return err + } + body, err := json.Marshal(server) + if err != nil { + return err + } + var native map[string]any + if err := json.Unmarshal(body, &native); err != nil { + return err + } + entries[name] = native + } + return shared.WriteJSON(filepath.Join(in.StagingPath, "mcp.json"), document) +} + +func bindWorkspaceStdio(server nativeconfig.Server, source domain.MCPServer, activePath, dataPath string) (nativeconfig.Server, error) { + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, activePath, dataPath) + if err != nil { + return server, err + } + command, _ := source.Decoded["command"].(string) + if strings.HasPrefix(command, "${package.root}/") { + command = "./" + strings.TrimPrefix(command, "${package.root}/") + } + server.Args = managedstdio.Arguments(activePath, dataPath, server.CWD, cwd.Anchor, command, server.Args) + server.Command = filepath.Join(activePath, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + server.CWD = "" + return server, nil +} diff --git a/install/integrationctl/agentplugins/clients/vscode/project_projection.go b/install/integrationctl/agentplugins/clients/vscode/project_projection.go new file mode 100644 index 000000000..872791f71 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/vscode/project_projection.go @@ -0,0 +1,74 @@ +package vscode + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.Projector = (*ProjectAdapter)(nil) + _ clients.ActiveNativeProjector = (*ProjectAdapter)(nil) +) + +func (*ProjectAdapter) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := validateWorkspaceProjection(in.StagingPath, in.Plan, false); err != nil { + return nil, err + } + // This is an internal portable staging document. The native destination is + // independently encoded by CodecVSCode into .vscode/mcp.json's servers. + if err := shared.ProjectMCPServers(shared.MCPProjection{Root: in.StagingPath, Envelope: in.Envelope, + Names: shared.SupportedMCPNames(in.Plan), Dialect: shared.MCPDialectCursor, PluginRoot: in.Plan.ActivePath, DataPath: in.PluginDataPath}); err != nil { + return nil, err + } + if err := projectWorkspaceStdio(in); err != nil { + return nil, err + } + return workspaceObjects(in.StagingPath, in.Envelope, in.Plan) +} + +func (*ProjectAdapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := plan.SelectedDelivery.ValidatePlan(plan, envelope.TreeDigest); err != nil { + return nil, err + } + if err := validateWorkspaceProjection(root, plan, true); err != nil { + return nil, err + } + if err := validateWorkspaceStdio(root, envelope, plan, dataPath); err != nil { + return nil, err + } + return workspaceObjects(root, envelope, plan) +} + +func validateWorkspaceProjection(root string, plan domain.DeliveryPlan, active bool) error { + if _, err := workspacePlanRoot(domain.DetectedClient{ClientID: domain.ClientVSCode, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return err + } + if active && root != plan.ActivePath || !active && (filepath.Dir(root) != plan.TargetRoot || !strings.HasPrefix(filepath.Base(root), ".agentplugins-staging-")) { + return fmt.Errorf("VS Code package path differs from selected staging/active workspace") + } + if err := pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, root); err != nil { + return err + } + info, err := os.Lstat(root) + if err != nil { + return err + } + if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("VS Code package must be an ordinary workspace directory") + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/windsurf/README.md b/install/integrationctl/agentplugins/clients/windsurf/README.md new file mode 100644 index 000000000..1442bd7c2 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/README.md @@ -0,0 +1,46 @@ +# Windsurf and current Devin project delivery + +The user lane preserves legacy Cascade channel selection and its exact owned +MCP configuration. User skills remain prepared as before. Detection ordering, +Devin/Windsurf coexistence selection and cloud configuration are unchanged. + +The project lane requires the explicitly selected primary `devin`/`devin.exe` +with host-observed normalized exact version `3000.11.205`. Other/unknown versions +and a primary legacy Windsurf executable refuse before native effects. The +optional SDK `Request.ClientVersion` carries the embedding host's observation; +SDK Discover/Prepare and this adapter perform no version probe. This assertion +qualifies only the configuration format, not the executable's provenance. + +| Delivery | Selected project location | +| --- | --- | +| Managed package, state, receipts and persistent data | `.uap` | +| Skill directories | `.devin/skills/` | +| MCP members | `.devin/mcp_config.json` under `mcpServers` | + +Current Devin uses command/args/env for stdio without a native cwd field. The +existing trusted managed launcher carries the package/data/cwd contract. Remote +servers use url/headers and explicit transport http or sse through a separate +native codec; legacy Cascade serverUrl behavior is preserved. Active projection +is read-only and rejects stored launcher/env locators that disagree with the +selected package/data receipt instead of silently accepting old data authority. + +A current-only installation can legitimately have no legacy Cascade user root. +That absence is allowed only in the validated project SDK context; nonempty +roots and stricter adapter profile resolvers retain their existing validation. +Exact original user context, selected project/native root, artifact path, +binding and every native receipt remain checked. Sibling facts keep their +existing strict root/executable/binding requirements. + +UAP refuses project main-config legacy mcpServers requiring native migration +and same-name higher local MCP overrides. It preserves local overrides, disabled +foreign members, skills, hooks, imported roots, trust and permissions. The shared +native kernel and component transaction supply exact owned updates/removal and +skill rollback. Recorded exact removal is version independent and launches no +native command; Devin's default-local `mcp remove` is never used. + +Installed files report prepared activation with installation verification. +Generic activation attestation cannot claim native discovery. Effective aliases, +imports, local/user/cloud/team policy, trust, account state, Desktop UI, MCP +initialization and runtime remain unobserved. Genuine pinned Linux version/help +outputs and artifact hashes are recorded in the project evidence document; +composed inert UAP tests do not qualify native discovery or execution. diff --git a/install/integrationctl/agentplugins/clients/windsurf/activate.go b/install/integrationctl/agentplugins/clients/windsurf/activate.go index 558df001d..84b6e7183 100644 --- a/install/integrationctl/agentplugins/clients/windsurf/activate.go +++ b/install/integrationctl/agentplugins/clients/windsurf/activate.go @@ -14,6 +14,9 @@ import ( // AutomaticallyActivates reports that Activate will write owned Windsurf MCP // entries for this exact request. func (*Adapter) AutomaticallyActivates(_ clients.Env, request domain.ActivationRequest) bool { + if request.Plan.Scope == domain.ScopeProject { + return request.Plan.InstallIntent == domain.InstallIntentAutomatic && shared.OnlyNativeComponents(request.Plan.Components) + } return request.Plan.InstallIntent == domain.InstallIntentAutomatic && strings.TrimSpace(request.Client.ConfigRoot) != "" && len(WindsurfObjects(request.Delivery.NativeObjects)) > 0 @@ -22,12 +25,18 @@ func (*Adapter) AutomaticallyActivates(_ clients.Env, request domain.ActivationR // VerifierAvailable reports that an exact native configuration listing can be // observed. It does not need a client executable. func (*Adapter) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, _ string) bool { + if plan.Scope == domain.ScopeProject { + return len(plan.Components) > 0 + } return len(plan.Components) > 0 } // Activate installs or verifies owned Windsurf MCP entries, or leaves a // channel-selection follow-up when automatic installation is unavailable. -func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { +func (adapter *Adapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if request.Plan.Scope == domain.ScopeProject || request.Plan.TargetContext.Scope == domain.ScopeProject { + return adapter.activateDevinProject(ctx, env, request) + } if err := shared.ActivationIdentityMismatch(request); err != nil { return domain.ActivationOutcome{}, err } @@ -73,6 +82,9 @@ func (*Adapter) Activate(ctx context.Context, env clients.Env, request domain.Ac // Deactivate removes owned Windsurf MCP entries, or asks the operator to // finish a leftover channel first. func (*Adapter) Deactivate(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + if request.TargetContext.Scope == domain.ScopeProject { + return deactivateDevinProject(ctx, env, request) + } outcome := shared.StartedDeactivation() if len(WindsurfObjects(request.NativeObjects)) == 0 { return shared.RequireExternalUninstall(outcome, request.ExternalUninstalled, "remove the prepared package manually, then rerun remove with `--external-uninstalled`"), nil diff --git a/install/integrationctl/agentplugins/clients/windsurf/identity.go b/install/integrationctl/agentplugins/clients/windsurf/identity.go index 1b20afdf2..e81b0de39 100644 --- a/install/integrationctl/agentplugins/clients/windsurf/identity.go +++ b/install/integrationctl/agentplugins/clients/windsurf/identity.go @@ -11,9 +11,24 @@ var _ clients.RegistryInspector = (*Adapter)(nil) func (*Adapter) UsesNativeRegistryExecutable() bool { return false } -func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, _ domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { +func (*Adapter) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { if err := ctx.Err(); err != nil { return clients.RegistryIndeterminate, err } + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + root, err := devinProjectPlanRoot(client, plan) + if err != nil { + return clients.RegistryIndeterminate, err + } + if managed != nil { + if err := validateDevinProjectBinding(root, plan.PhysicalArtifactID, *managed); err != nil { + return clients.RegistryIndeterminate, err + } + selected := *managed + selected.NativeObjects = devinNativeObjects(managed.NativeObjects) + managed = &selected + } + return devinComponents(root).Inspect(plan.Components, managed, env.NativeConfig) + } return InspectWindsurfRegistry(plan, managed, env.NativeConfig) } diff --git a/install/integrationctl/agentplugins/clients/windsurf/plan.go b/install/integrationctl/agentplugins/clients/windsurf/plan.go index 4ad5ef996..7db4558bf 100644 --- a/install/integrationctl/agentplugins/clients/windsurf/plan.go +++ b/install/integrationctl/agentplugins/clients/windsurf/plan.go @@ -18,6 +18,9 @@ var ( // takes MCP servers through its local configuration, while skills stay in the // prepared package and are never reported as activated. func (*Adapter) RefinePlan(_ context.Context, in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope == domain.ScopeProject || plan.Scope == domain.ScopeProject { + return refineProjectPlan(in, plan) + } hasMCP := shared.ComponentKindPresent(plan.Components, domain.ComponentMCPServer) shared.PromoteNativeReady(plan, in.Client.ConfigRoot, hasMCP) action := "select exactly one legacy Windsurf channel and import the prepared MCP configuration manually" diff --git a/install/integrationctl/agentplugins/clients/windsurf/project.go b/install/integrationctl/agentplugins/clients/windsurf/project.go index 53260838c..dfe4054fa 100644 --- a/install/integrationctl/agentplugins/clients/windsurf/project.go +++ b/install/integrationctl/agentplugins/clients/windsurf/project.go @@ -2,6 +2,7 @@ package windsurf import ( "context" + "fmt" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" @@ -11,18 +12,50 @@ import ( var _ clients.Projector = (*Adapter)(nil) var _ clients.ActiveNativeProjector = (*Adapter)(nil) -func (*Adapter) ProjectActiveNative(_ context.Context, root string, _ domain.PackageEnvelope, plan domain.DeliveryPlan, _ string) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) ProjectActiveNative(ctx context.Context, root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) ([]domain.NativeObjectOwnership, error) { + if plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject { + if err := ctx.Err(); err != nil { + return nil, err + } + if plan.ClientID != domain.ClientWindsurf || root != plan.ActivePath { + return nil, fmt.Errorf("devin active package identity differs") + } + if err := plan.SelectedDelivery.ValidatePlan(plan, envelope.TreeDigest); err != nil { + return nil, err + } + if err := validateDevinProjectionRoot(root, plan); err != nil { + return nil, err + } + if err := validateDevinProjectStdio(root, envelope, plan, dataPath); err != nil { + return nil, err + } + return devinProjectObjects(root, envelope, plan) + } return BuildWindsurfNativeObjects(root, plan) } // Project copies the managed stdio launcher when needed, writes Windsurf MCP, // and records the objects it owns. -func (*Adapter) Project(_ context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { +func (*Adapter) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + if in.Plan.Scope == domain.ScopeProject || in.Plan.TargetContext.Scope == domain.ScopeProject { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := validateDevinProjectionRoot(in.StagingPath, in.Plan); err != nil { + return nil, err + } + if err := in.Plan.SelectedDelivery.ValidatePlan(in.Plan, in.Envelope.TreeDigest); err != nil { + return nil, err + } + } if err := shared.DeliverManagedStdio(in.DeliverLauncher(), in.StagingPath, in.Envelope, in.Plan); err != nil { return nil, err } if err := ProjectWindsurfMCP(in.StagingPath, in.Envelope, in.Plan, in.PluginDataPath); err != nil { return nil, err } + if in.Plan.Scope == domain.ScopeProject { + return devinProjectObjects(in.StagingPath, in.Envelope, in.Plan) + } return BuildWindsurfNativeObjects(in.StagingPath, in.Plan) } diff --git a/install/integrationctl/agentplugins/clients/windsurf/project_lifecycle.go b/install/integrationctl/agentplugins/clients/windsurf/project_lifecycle.go new file mode 100644 index 000000000..078d851bc --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/project_lifecycle.go @@ -0,0 +1,113 @@ +package windsurf + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ActivationPreflighter = (*Adapter)(nil) + +func (*Adapter) PreflightActivation(env clients.Env, request domain.ActivationRequest) error { + if request.Plan.Scope != domain.ScopeProject && request.Plan.TargetContext.Scope != domain.ScopeProject { + return nil + } + root, err := devinProjectPlanRoot(request.Client, request.Plan) + if err != nil { + return err + } + if err := requireCurrentDevin(request.Client); err != nil { + return err + } + if request.Delivery.ActivePath != "" && request.Delivery.ActivePath != request.Plan.ActivePath { + return fmt.Errorf("devin delivered package differs from selected project") + } + if request.Plan.ClientID != request.Client.ClientID { + return fmt.Errorf("devin project plan client identity mismatch") + } + if request.Delivery.ClientID != "" { + if err := shared.ActivationIdentityMismatch(request); err != nil { + return err + } + } + if request.ActivationComplete { + return fmt.Errorf("devin project native discovery cannot be established by activation attestation") + } + if !shared.OnlyNativeComponents(request.Plan.Components) { + return fmt.Errorf("devin project delivery requires supported skills and MCP servers only") + } + layout := devinComponents(root) + if err := layout.ValidateObjects(devinNativeObjects(request.PreviousNativeObjects)); err != nil { + return err + } + if err := layout.ValidateObjects(devinNativeObjects(request.Delivery.NativeObjects)); err != nil { + return err + } + return checkDevinProjectPolicy(root, request.Plan.Components, env.NativeConfig) +} + +func (adapter *Adapter) activateDevinProject(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + if err := shared.ActivationIdentityMismatch(request); err != nil { + return domain.ActivationOutcome{}, err + } + if request.Delivery.ActivePath != request.Plan.ActivePath { + return domain.ActivationOutcome{}, fmt.Errorf("devin delivered package differs from selected project") + } + if err := adapter.PreflightActivation(env, request); err != nil { + return domain.ActivationOutcome{}, err + } + layout := devinComponents(request.Plan.NativeRegistryRoot) + outcome, err := shared.CompleteNativeConfigActivation(ctx, request, shared.NativeConfigActivation{ + Automatic: adapter.AutomaticallyActivates(env, request), + UnavailableAction: "select only supported Devin project skills and MCP servers", + RepairAction: "repair managed Devin project skills and MCP entries", + RetryAction: "retry managed Devin project component installation", + CompletedAction: DevinProjectAction, + Verify: func() error { + return layout.Verify(devinNativeObjects(request.Delivery.NativeObjects), false, env.NativeConfig) + }, + Activate: func(ctx context.Context, request domain.ActivationRequest) error { + servers, err := devinDesiredServers(request.Delivery.ActivePath, devinNativeObjects(request.Delivery.NativeObjects)) + if err != nil { + return &shared.NativeEffectError{Effect: domain.NativeEffectUnchanged, Err: err} + } + return (shared.ProjectComponentMutation{Layout: layout, ActivePath: request.Delivery.ActivePath, + Previous: devinNativeObjects(request.PreviousNativeObjects), Desired: devinNativeObjects(request.Delivery.NativeObjects), Servers: servers, Kernel: env.NativeConfig}).Apply(ctx) + }, + }) + if err == nil && outcome.Verification == domain.VerificationInstalled { + outcome.Activation, outcome.ActivationAttested = domain.ActivationPrepared, false + outcome.UserActions = shared.AppendUnique(outcome.UserActions, DevinProjectAction) + } + return outcome, err +} + +// Exact recorded removal has no current executable/version prerequisite and +// never invokes native CLI remove (whose default scope is local). +func deactivateDevinProject(ctx context.Context, env clients.Env, request domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + outcome := shared.StartedDeactivation() + root, err := devinProjectRoot(request.Client, request.TargetContext, request.NativeRegistryRoot) + if err != nil { + return outcome, err + } + layout := devinComponents(root) + if err := layout.ValidateObjects(devinNativeObjects(request.NativeObjects)); err != nil { + return outcome, err + } + if err := env.NativeConfig.RequireFileIO(); err != nil { + return outcome, err + } + if !request.Confirmed { + outcome.UserActions = []string{"agentplugins will remove its exact owned Devin project skill directories and MCP members"} + return outcome, nil + } + err = (shared.ProjectComponentMutation{Layout: layout, Previous: devinNativeObjects(request.NativeObjects), Kernel: env.NativeConfig}).Apply(ctx) + if err != nil && !shared.CommittedNativeDeactivationCleanup(&outcome, err) { + return outcome, err + } + outcome.ExternalRemovalComplete = true + return outcome, nil +} diff --git a/install/integrationctl/agentplugins/clients/windsurf/project_native.go b/install/integrationctl/agentplugins/clients/windsurf/project_native.go new file mode 100644 index 000000000..a9015d265 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/project_native.go @@ -0,0 +1,138 @@ +package windsurf + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const ( + DevinProjectSkillKind = "devin_project_skill_directory" + DevinProjectMCPKind = "devin_project_mcp_server" +) + +func devinComponents(root string) shared.ProjectComponents { + return shared.ProjectComponents{ProjectRoot: filepath.Dir(root), SkillsRoot: filepath.Join(root, "skills"), + ConfigPaths: nativeconfig.Paths{JSON: filepath.Join(root, "mcp_config.json")}, Codec: nativeconfig.CodecDevin, + SkillKind: DevinProjectSkillKind, MCPKind: DevinProjectMCPKind} +} + +func devinProjectObjects(stagingRoot string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan) ([]domain.NativeObjectOwnership, error) { + target, err := domain.NewTargetContext(plan.TargetContext.Scope, plan.TargetContext.ProjectRoot, plan.TargetContext.UserConfigRoot) + if err != nil { + return nil, err + } + if plan.Scope != domain.ScopeProject || target.Scope != domain.ScopeProject || plan.NativeRegistryRoot != filepath.Join(target.ProjectRoot, ".devin") { + return nil, fmt.Errorf("devin projection differs from selected project") + } + if err := validateDevinProjectionRoot(stagingRoot, plan); err != nil { + return nil, err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, stagingRoot); err != nil { + return nil, err + } + layout := devinComponents(plan.NativeRegistryRoot) + objects := []domain.NativeObjectOwnership{} + for _, component := range plan.Components { + if component.Support == domain.SupportUnsupported { + continue + } + object, err := devinProjectObject(layout, stagingRoot, envelope, component) + if err != nil { + return nil, err + } + if object != nil { + objects = append(objects, *object) + } + } + return objects, layout.ValidateObjects(objects) +} + +func devinProjectObject(layout shared.ProjectComponents, stagingRoot string, envelope domain.PackageEnvelope, component domain.ComponentDecision) (*domain.NativeObjectOwnership, error) { + if err := pathpolicy.ValidateLeafID(component.Name); err != nil { + return nil, err + } + switch component.Kind { + case domain.ComponentSkill: + skill, ok := envelope.Skills[component.Name] + if !ok { + return nil, fmt.Errorf("planned Devin skill %q is missing", component.Name) + } + relative := filepath.Dir(filepath.FromSlash(skill.RelativePath)) + if skill.RelativePath == "" { + relative = filepath.Join("skills", component.Name) + } + object, err := layout.SkillObject(component.Name, filepath.ToSlash(relative), stagingRoot) + return &object, err + case domain.ComponentMCPServer: + server, err := devinProjectedServer(stagingRoot, component.Name) + if err != nil { + return nil, err + } + object, err := layout.MCPObject(component.Name, server, layout.ConfigPaths.JSON) + return &object, err + default: + return nil, fmt.Errorf("unsupported Devin project component kind %q", component.Kind) + } +} + +func devinProjectedServer(activeRoot, name string) (nativeconfig.Server, error) { + file, err := nativeconfig.New().ReadExactFile(filepath.Join(activeRoot, "mcp.json")) + if err != nil { + return nativeconfig.Server{}, err + } + document, err := shared.DecodeStrictJSONObject(file.Body) + if err != nil { + return nativeconfig.Server{}, err + } + entries, ok := document["mcpServers"].(map[string]any) + if !ok { + return nativeconfig.Server{}, fmt.Errorf("devin package has no mcpServers object") + } + entry, ok := entries[name].(map[string]any) + if !ok { + return nativeconfig.Server{}, fmt.Errorf("devin projected MCP server %q is missing", name) + } + body, err := json.Marshal(entry) + if err != nil { + return nativeconfig.Server{}, err + } + server, err := decodeProjectedWindsurfServer(body) + if err != nil { + return server, err + } + server.StdioValuesResolved, server.CWDResolved = true, true + return server, nil +} + +func devinDesiredServers(activeRoot string, objects []domain.NativeObjectOwnership) (map[string]nativeconfig.Server, error) { + servers := map[string]nativeconfig.Server{} + for _, object := range objects { + if object.Kind != DevinProjectMCPKind { + continue + } + server, err := devinProjectedServer(activeRoot, object.LogicalName) + if err != nil { + return nil, err + } + servers[object.LogicalName] = server + } + return servers, nil +} + +// Package directory ownership belongs to the shared artifact lifecycle, not the +// native component writer. Other unknown kinds remain visible and fail closed. +func devinNativeObjects(objects []domain.NativeObjectOwnership) []domain.NativeObjectOwnership { + result := []domain.NativeObjectOwnership{} + for _, object := range objects { + if object.Kind != "managed_package_directory" { + result = append(result, object) + } + } + return result +} diff --git a/install/integrationctl/agentplugins/clients/windsurf/project_policy.go b/install/integrationctl/agentplugins/clients/windsurf/project_policy.go new file mode 100644 index 000000000..febff6386 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/project_policy.go @@ -0,0 +1,63 @@ +package windsurf + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// This observes only selected project files. It never initiates Devin's native +// migration, edits a local override, or interprets unknown imported policy. +func checkDevinProjectPolicy(root string, components []domain.ComponentDecision, kernel nativeconfig.Kernel) error { + for _, name := range []string{"config.json", "config.local.json"} { + document, err := readDevinProjectDocument(filepath.Join(root, name), kernel) + if err != nil { + return err + } + if raw, exists := document["mcpServers"]; exists { + entries, ok := raw.(map[string]any) + if !ok || len(entries) > 0 { + return fmt.Errorf("%w: Devin project main config has legacy mcpServers; migrate separately with the selected native client before automatic installation", nativeconfig.ErrNativeMigrationRequired) + } + } + } + document, err := readDevinProjectDocument(filepath.Join(root, "mcp_config.local.json"), kernel) + if err != nil { + return err + } + raw, exists := document["mcpServers"] + if !exists { + return nil + } + entries, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("devin local MCP override must contain an object mcpServers") + } + for _, component := range components { + if component.Kind != domain.ComponentMCPServer || component.Support == domain.SupportUnsupported { + continue + } + if _, shadowed := entries[component.Name]; shadowed { + return fmt.Errorf("devin local MCP override shadows selected server %q; resolve policy separately", component.Name) + } + } + return nil +} + +func readDevinProjectDocument(path string, kernel nativeconfig.Kernel) (map[string]any, error) { + file, err := kernel.ReadExactFile(path) + if err != nil { + return nil, err + } + if !file.Exists { + return map[string]any{}, nil + } + standard, err := nativeconfig.StandardizeCommentedJSON(file.Body) + if err != nil { + return nil, err + } + return shared.DecodeStrictJSONObject(standard) +} diff --git a/install/integrationctl/agentplugins/clients/windsurf/project_stdio_verify.go b/install/integrationctl/agentplugins/clients/windsurf/project_stdio_verify.go new file mode 100644 index 000000000..e646a9e59 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/project_stdio_verify.go @@ -0,0 +1,45 @@ +package windsurf + +import ( + "fmt" + "path/filepath" + "slices" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "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" +) + +// Active package bytes are immutable. A different selected data receipt needs +// a fresh projection; old launcher/environment locators cannot authorize it. +func validateDevinProjectStdio(root string, envelope domain.PackageEnvelope, plan domain.DeliveryPlan, dataPath string) error { + for _, name := range shared.SupportedMCPNames(plan) { + source := envelope.MCP.Servers[name] + if source.Type != "stdio" { + continue + } + server, err := devinProjectedServer(root, name) + if err != nil { + return err + } + launcher := filepath.Join(plan.ActivePath, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) + if !filepath.IsAbs(dataPath) || filepath.Clean(dataPath) != dataPath || server.Type != "stdio" || server.Command != launcher || server.CWD != "" || server.Env["PLUGIN_ROOT"] != plan.ActivePath || server.Env["PLUGIN_DATA"] != dataPath { + return fmt.Errorf("%w: Devin stdio package/data binding differs from selected locators", domain.ErrProjectionRefreshRequired) + } + rawCWD, _ := source.Decoded["cwd"].(string) + cwd, err := pathcontract.ExpandCWD(rawCWD, plan.ActivePath, dataPath) + if err != nil { + return err + } + _, resolvedCWD, err := shared.ResolveStdioPaths("", rawCWD, plan.ActivePath, dataPath, root) + 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) { + return fmt.Errorf("%w: Devin launcher package/data/cwd arguments differ from selected locators", domain.ErrProjectionRefreshRequired) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/clients/windsurf/project_validation.go b/install/integrationctl/agentplugins/clients/windsurf/project_validation.go new file mode 100644 index 000000000..73ae7de28 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/project_validation.go @@ -0,0 +1,61 @@ +package windsurf + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var _ clients.ProfileBindingValidator = (*Adapter)(nil) + +const DevinProjectAction = "Devin project configuration is installed; open the selected project and inspect skills and MCP servers. Native discovery, imported aliases, enablement, workspace trust, permissions, cloud/team policy and MCP initialization remain unobserved" + +func requireCurrentDevin(client domain.DetectedClient) error { + name := strings.ToLower(filepath.Base(client.ExecutablePath)) + if client.ClientID != domain.ClientWindsurf || !filepath.IsAbs(client.ExecutablePath) || filepath.Clean(client.ExecutablePath) != client.ExecutablePath || (name != "devin" && name != "devin.exe") || client.Version != "3000.11.205" { + return fmt.Errorf("devin project configuration requires the explicitly selected primary devin executable with observed exact version 3000.11.205; legacy Windsurf, unknown or other versions require separate qualification") + } + return nil +} + +func (*Adapter) ValidateBindingProfile(root string, binding domain.ClientBinding) error { + if binding.Scope != string(domain.ScopeProject) { + return nil + } + return validateDevinProjectBinding(root, binding.PhysicalArtifact, binding) +} + +func validateDevinProjectionRoot(root string, plan domain.DeliveryPlan) error { + if _, err := devinProjectPlanRoot(domain.DetectedClient{ClientID: domain.ClientWindsurf, ConfigRoot: plan.TargetContext.UserConfigRoot}, plan); err != nil { + return err + } + if !filepath.IsAbs(root) || filepath.Clean(root) != root || strings.TrimSpace(root) != root { + return fmt.Errorf("devin package path must be clean and absolute") + } + return pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, root) +} + +func validateDevinProjectBinding(root, artifact string, binding domain.ClientBinding) error { + if !filepath.IsAbs(root) || filepath.Clean(root) != root || filepath.Base(root) != ".devin" { + return fmt.Errorf("devin project registry must be an exact absolute root") + } + if binding.ClientID != string(domain.ClientWindsurf) || binding.Scope != string(domain.ScopeProject) || binding.NativeProfileRoot != root || binding.PhysicalArtifact != artifact { + return fmt.Errorf("devin binding scope, artifact or native root differs from selected project") + } + if err := pathpolicy.ValidateLeafID(artifact); err != nil { + return err + } + project := filepath.Dir(root) + expected := filepath.Join(project, ".uap", "managed", "clients", "windsurf", artifact) + if binding.TargetLocator != expected { + return fmt.Errorf("devin binding package differs from selected project") + } + if err := pathpolicy.RequireContainedChild(project, expected); err != nil { + return err + } + return devinComponents(root).ValidateObjects(devinNativeObjects(binding.NativeObjects)) +} diff --git a/install/integrationctl/agentplugins/clients/windsurf/scope.go b/install/integrationctl/agentplugins/clients/windsurf/scope.go new file mode 100644 index 000000000..d5db70858 --- /dev/null +++ b/install/integrationctl/agentplugins/clients/windsurf/scope.go @@ -0,0 +1,109 @@ +package windsurf + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +var ( + _ clients.ProjectTargetLayout = (*Adapter)(nil) + _ clients.NativeRegistryLayout = (*Adapter)(nil) + _ clients.PlanPrecondition = (*Adapter)(nil) +) + +func (*Adapter) NativeRegistry(in clients.PlanInput) (string, string) { + if in.TargetContext.Scope == domain.ScopeProject { + return filepath.Join(in.TargetContext.ProjectRoot, ".devin"), "" + } + return in.Client.ConfigRoot, in.Client.ExecutablePath +} + +func (*Adapter) ProjectTargetRoot(client domain.DetectedClient, mode domain.PackageMode, managedRoot string, target domain.TargetContext) (string, string, error) { + if _, err := devinProjectRoot(client, target, ""); err != nil { + return "", "", err + } + if err := pathpolicy.RequireContainedChild(target.ProjectRoot, managedRoot); err != nil { + return "", "", err + } + return shared.ManagedTargetRoot(client, mode, managedRoot) +} + +func (*Adapter) CheckPlanPrecondition(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if in.TargetContext.Scope != domain.ScopeProject { + return nil + } + root, err := devinProjectRoot(in.Client, in.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return err + } + if err := requireCurrentDevin(in.Client); err != nil { + return err + } + return checkDevinProjectPolicy(root, plan.Components, nativeconfig.New()) +} + +func devinProjectRoot(client domain.DetectedClient, target domain.TargetContext, planned string) (string, error) { + if client.ClientID != domain.ClientWindsurf { + return "", fmt.Errorf("devin selected client identity differs") + } + validated, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot) + if err != nil { + return "", err + } + if validated.Scope != domain.ScopeProject { + return "", fmt.Errorf("devin workspace configuration requires project scope") + } + if validated.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("devin detected user profile differs from selected target") + } + root := filepath.Join(validated.ProjectRoot, ".devin") + if planned != "" && planned != root { + return "", fmt.Errorf("devin native registry differs from selected project") + } + return root, pathpolicy.RequireContainedChild(validated.ProjectRoot, root) +} + +func devinProjectPlanRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.ClientID != domain.ClientWindsurf || plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject || plan.NativeRegistryRoot == "" || plan.NativeRegistryExecutable != "" { + return "", fmt.Errorf("devin project lifecycle requires matching project scope") + } + root, err := devinProjectRoot(client, plan.TargetContext, plan.NativeRegistryRoot) + if err != nil { + return "", err + } + if err := pathpolicy.ValidateLeafID(plan.PhysicalArtifactID); err != nil { + return "", err + } + expected := filepath.Join(plan.TargetContext.ProjectRoot, ".uap", "managed", "clients", "windsurf", plan.PhysicalArtifactID) + if plan.ActivePath != expected { + return "", fmt.Errorf("devin managed package differs from selected project") + } + return root, pathpolicy.RequireContainedChild(plan.TargetContext.ProjectRoot, expected) +} + +func refineProjectPlan(in clients.PlanInput, plan *domain.DeliveryPlan) error { + if err := requireCurrentDevin(in.Client); err != nil { + return err + } + root, err := devinProjectPlanRoot(in.Client, *plan) + if err != nil { + return err + } + if err := checkDevinProjectPolicy(root, plan.Components, nativeconfig.New()); err != nil { + return err + } + plan.NativeRegistryRoot, plan.NativeRegistryExecutable = root, "" + if plan.Status == domain.PlanUnsupported || !shared.OnlyNativeComponents(plan.Components) { + return nil + } + plan.Status, plan.Activation, plan.Verification = domain.PlanReady, domain.ActivationPrepared, domain.VerificationPackageValid + plan.UserActions = []string{DevinProjectAction} + plan.Warnings = shared.AppendUnique(plan.Warnings, "devin_project_effective_policy_unobserved") + return nil +} diff --git a/install/integrationctl/agentplugins/domain/clients.go b/install/integrationctl/agentplugins/domain/clients.go index d36d8549d..e3626a6fb 100644 --- a/install/integrationctl/agentplugins/domain/clients.go +++ b/install/integrationctl/agentplugins/domain/clients.go @@ -64,6 +64,7 @@ const ( // mutation remains in client-specific adapters and is intentionally not part // of this registry. type ClientDefinition struct { + ProjectPolicy ProjectPolicy `json:"-"` ID ClientID DisplayName string BackendFamily string @@ -91,14 +92,14 @@ type ClientDefinition struct { } var clientDefinitions = []ClientDefinition{ - clientDefinition(ClientCodex, "OpenAI Codex", "codex", "managed", "projected", true, PackageProjection, SupportProjected, SupportProjected, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ + withProjectScope(clientDefinition(ClientCodex, "OpenAI Codex", "codex", "managed", "projected", true, PackageProjection, SupportProjected, SupportProjected, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ QualifiedLocalInstallerFacade: true, InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, HonorsOpenAIMCPAuthHints: true, SupportsPreparedRecovery: true, BindsNativeProfileRoot: true, - }), + }), LifecycleNativeConfig), withoutHostPresence(withDirectoryPreparation( clientDefinition(ClientChatGPT, "ChatGPT", "chatgpt", "manual_activation", "projected", false, PackageProjection, SupportProjected, SupportUnsupported, SupportUnsupported, SupportProjected, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic, InstallIntentPrepare}, @@ -106,63 +107,63 @@ var clientDefinitions = []ClientDefinition{ RequiresPersonalMappingForPrepare: true, }), DirectoryResolveContext7ChatGPTPreparation)), - clientDefinition(ClientCursor, "Cursor", "cursor", "managed", "native", true, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportNative, ClientTraits{ + withProjectScope(clientDefinition(ClientCursor, "Cursor", "cursor", "managed", "native", true, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportNative, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, - }), - withoutCommands(clientDefinition(ClientCopilot, "GitHub Copilot CLI", "github-copilot", "managed", "native", true, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportNative, ClientTraits{ + }), LifecycleNativeConfig), + withProjectScope(withoutCommands(clientDefinition(ClientCopilot, "GitHub Copilot CLI", "github-copilot", "managed", "native", true, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportNative, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, - })), - withoutCommands(clientDefinition(ClientVSCode, "Visual Studio Code", "github-copilot", "prepared", "prepared", true, PackagePrepared, SupportPrepared, SupportPrepared, SupportPrepared, SupportUnsupported, SupportPrepared, ClientTraits{ + })), LifecycleNativeConfig), + withProjectScope(withoutCommands(clientDefinition(ClientVSCode, "Visual Studio Code", "github-copilot", "prepared", "prepared", true, PackagePrepared, SupportPrepared, SupportPrepared, SupportPrepared, SupportUnsupported, SupportPrepared, ClientTraits{ SavedDependenciesDeferred: true, InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, - })), - clientDefinition(ClientKiro, "Kiro", "kiro", "managed", "native", true, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, ClientTraits{ + })), LifecycleNativeConfig), + withProjectScope(clientDefinition(ClientKiro, "Kiro", "kiro", "managed", "native", true, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic, InstallIntentPrepare}, LifecycleKind: LifecycleCLIRegistry, TracksNativeEffects: true, - }), - withActivation(clientDefinition(ClientClaude, "Claude Code", "claude", "managed", "projected", false, PackageProjection, SupportProjected, SupportProjected, SupportProjected, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), LifecycleNativeConfig), + withProjectScope(withActivation(clientDefinition(ClientClaude, "Claude Code", "claude", "managed", "projected", false, PackageProjection, SupportProjected, SupportProjected, SupportProjected, SupportUnsupported, SupportUnsupported, ClientTraits{ QualifiedLocalInstallerFacade: true, InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, UsesManagedStdioLauncher: true, - }), ActivationAutomatic), - clientDefinition(ClientGemini, "Gemini CLI", "gemini", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), ActivationAutomatic), LifecycleCLIRegistry), + withProjectScope(clientDefinition(ClientGemini, "Gemini CLI", "gemini", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleNativeConfig, BindsNativeProfileRoot: true, - }), - withActivation(clientDefinition(ClientOpenCode, "OpenCode", "opencode", "managed", "prepared", false, PackagePrepared, SupportPrepared, SupportPrepared, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), LifecycleNativeConfig), + withProjectScope(withActivation(clientDefinition(ClientOpenCode, "OpenCode", "opencode", "managed", "prepared", false, PackagePrepared, SupportPrepared, SupportPrepared, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleNativeConfig, ReportsMCPToolNamespaceCollision: true, - }), ActivationAutomatic), - withActivation(clientDefinition(ClientCline, "Cline", "cline", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), ActivationAutomatic), LifecycleNativeConfig), + withProjectScope(withActivation(clientDefinition(ClientCline, "Cline", "cline", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleNativeConfig, - }), ActivationAutomatic), - clientDefinition(ClientWindsurf, "Windsurf / Devin", "windsurf", "prepared", "prepared", false, PackagePrepared, SupportPrepared, SupportPrepared, SupportPrepared, SupportUnsupported, SupportPrepared, ClientTraits{ + }), ActivationAutomatic), LifecycleNativeConfig), + withProjectScope(clientDefinition(ClientWindsurf, "Windsurf / Devin", "windsurf", "prepared", "prepared", false, PackagePrepared, SupportPrepared, SupportPrepared, SupportPrepared, SupportUnsupported, SupportPrepared, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleNativeConfig, UsesManagedStdioLauncher: true, - }), - clientDefinition(ClientGrok, "Grok Build", "grok", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), LifecycleNativeConfig), + withProjectScope(clientDefinition(ClientGrok, "Grok Build", "grok", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, - }), - clientDefinition(ClientKimi, "Kimi Code", "kimi", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), LifecyclePrepared), + withProjectScope(clientDefinition(ClientKimi, "Kimi Code", "kimi", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleNativeConfig, - }), - clientDefinition(ClientQoder, "Qoder", "qoder", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ + }), LifecycleNativeConfig), + withProjectScope(clientDefinition(ClientQoder, "Qoder", "qoder", "managed", "native", false, PackageNative, SupportNative, SupportNative, SupportUnsupported, SupportUnsupported, SupportUnsupported, ClientTraits{ InstallIntents: []InstallIntent{InstallIntentAutomatic}, LifecycleKind: LifecycleCLIRegistry, DiscoversNativeHooks: true, HonorsOpenAIMCPAuthHints: true, - }), + }), LifecyclePrepared), } func withActivation(definition ClientDefinition, activation ActivationMode) ClientDefinition { @@ -356,6 +357,7 @@ type ComponentDecision struct { } type DeliveryPlan struct { + TargetContext TargetContext `json:"-"` profileAuthority *ProfileAuthority `json:"-"` profileNamespace string `json:"-"` PreviousNativeObjects []NativeObjectOwnership `json:"-"` @@ -404,11 +406,13 @@ type DeliveryPlan struct { // configured client roots. Persisted state must be checked against this value // before any destructive operation. type DeliveryTarget struct { - profileAuthority *ProfileAuthority `json:"-"` - profileNamespace string `json:"-"` - TargetAnchor string `json:"-"` - TargetRoot string `json:"-"` - ActivePath string `json:"-"` + TargetContext TargetContext `json:"-"` + NativeRegistryRoot string `json:"-"` + profileAuthority *ProfileAuthority `json:"-"` + profileNamespace string `json:"-"` + TargetAnchor string `json:"-"` + TargetRoot string `json:"-"` + ActivePath string `json:"-"` } type OpenAIMCPAuthHint struct { @@ -475,6 +479,8 @@ type ActivationOutcome struct { } type DeactivationRequest struct { + TargetContext TargetContext `json:"-"` + NativeRegistryRoot string `json:"-"` LocalEntryObservation *LocalEntryObservation `json:"-"` RemoveOwnedEntry bool `json:"-"` SelectedDelivery SelectedDelivery `json:"-"` diff --git a/install/integrationctl/agentplugins/domain/clients_test.go b/install/integrationctl/agentplugins/domain/clients_test.go index 27fcdd9cd..b289fad63 100644 --- a/install/integrationctl/agentplugins/domain/clients_test.go +++ b/install/integrationctl/agentplugins/domain/clients_test.go @@ -82,7 +82,7 @@ func TestQoderClientDefinition(t *testing.T) { if capabilities.AppSupport != SupportUnsupported || capabilities.ExtensionSupport != SupportUnsupported { t.Fatalf("Qoder app/extension support = %+v", capabilities) } - if want := []InstallScope{ScopeUser}; !reflect.DeepEqual(capabilities.Scopes, want) { + if want := []InstallScope{ScopeUser, ScopeProject}; !reflect.DeepEqual(capabilities.Scopes, want) { t.Fatalf("Qoder scopes = %v, want %v", capabilities.Scopes, want) } if definition.Traits.LifecycleKind != LifecycleCLIRegistry || !definition.Traits.Allows(InstallIntentAutomatic) { diff --git a/install/integrationctl/agentplugins/domain/planning.go b/install/integrationctl/agentplugins/domain/planning.go index fefeb60c3..44295685b 100644 --- a/install/integrationctl/agentplugins/domain/planning.go +++ b/install/integrationctl/agentplugins/domain/planning.go @@ -5,6 +5,7 @@ package domain // logical client; a nil map means the planner falls back to the one its // composition root configured. type PlanRequest struct { + ProjectRoot string `json:"-"` PreviousNativeObjects []NativeObjectOwnership `json:"-"` LocalEntryObservation *LocalEntryObservation `json:"-"` Envelope PackageEnvelope diff --git a/install/integrationctl/agentplugins/domain/project_copilot_policy_test.go b/install/integrationctl/agentplugins/domain/project_copilot_policy_test.go new file mode 100644 index 000000000..9d55e0a9e --- /dev/null +++ b/install/integrationctl/agentplugins/domain/project_copilot_policy_test.go @@ -0,0 +1,20 @@ +package domain + +import "testing" + +func TestProjectCopilotUsesSeparateProjectBackendAndPreservesUserPolicy(t *testing.T) { + definition, ok := ClientDefinitionFor(ClientCopilot) + if !ok || !definition.ProjectPolicy.Valid() || definition.CommandSupport != SupportUnsupported { + t.Fatalf("Copilot project declaration changed command policy: %+v", definition) + } + if !SameClientBackendForScope(ClientCopilot, ClientVSCode, ScopeUser) || SameClientBackendForScope(ClientCopilot, ClientVSCode, ScopeProject) { + t.Fatal("Copilot project borrowed the shared user editor backend") + } + delivery := SelectedDelivery{} + if delivery.EffectiveTraitsForScope(ClientCopilot, ScopeUser).LifecycleKind != LifecycleCLIRegistry || delivery.EffectiveTraitsForScope(ClientCopilot, ScopeProject).LifecycleKind != LifecycleNativeConfig { + t.Fatal("Copilot project/user lifecycle separation changed") + } + if delivery.SharesBackendForScope(ClientCopilot, ScopeProject) || BackendFamilyForScope(ClientCopilot, ScopeProject) != "copilot" { + t.Fatal("Copilot repository components have an ambiguous physical owner") + } +} diff --git a/install/integrationctl/agentplugins/domain/project_policy.go b/install/integrationctl/agentplugins/domain/project_policy.go new file mode 100644 index 000000000..d3390f8a7 --- /dev/null +++ b/install/integrationctl/agentplugins/domain/project_policy.go @@ -0,0 +1,87 @@ +package domain + +// ProjectPolicy is a qualified client's scoped lifecycle declaration. Its zero +// value enables nothing. Client stacks add it together with project layouts; +// user traits and physical backend sharing remain unchanged. +type ProjectPolicy struct { + LifecycleKind LifecycleKind `json:"-"` + BackendFamily string `json:"-"` +} + +func (policy ProjectPolicy) Valid() bool { + return policy.BackendFamily != "" && (policy.LifecycleKind == LifecycleNativeConfig || + policy.LifecycleKind == LifecycleCLIRegistry || policy.LifecycleKind == LifecyclePrepared) +} + +// withProjectScope is used only by individually qualified client table rows. +// Projects have independent physical registries unless explicitly declared. +func withProjectScope(definition ClientDefinition, kind LifecycleKind) ClientDefinition { + definition.ProjectPolicy = ProjectPolicy{LifecycleKind: kind, BackendFamily: string(definition.ID)} + definition.Capabilities.Scopes = append(definition.Capabilities.Scopes, ScopeProject) + return definition +} + +// EffectiveTraitsForScope never borrows a user policy for project delivery. +// Admission must reject an absent project declaration before mutation. +func (delivery SelectedDelivery) EffectiveTraitsForScope(id ClientID, scope InstallScope) ClientTraits { + if scope != ScopeProject { + return delivery.EffectiveTraits(id) + } + definition, ok := ClientDefinitionFor(id) + if !ok || !definition.ProjectPolicy.Valid() || !delivery.IsZero() { + return ClientTraits{} + } + traits := definition.Traits + traits.LifecycleKind = definition.ProjectPolicy.LifecycleKind + traits.SavedDependenciesDeferred = false + traits.BindsNativeProfileRoot = true + if traits.LifecycleKind == LifecycleNativeConfig { + traits.TracksNativeEffects = true + traits.BindsNativeProfileRoot = true + traits.SupportsPreparedRecovery = true + traits.UsesManagedStdioLauncher = true + } + return traits +} + +func BackendFamilyForScope(id ClientID, scope InstallScope) string { + definition, ok := ClientDefinitionFor(id) + if !ok { + return "" + } + if scope == ScopeProject { + if !definition.ProjectPolicy.Valid() { + return "" + } + return definition.ProjectPolicy.BackendFamily + } + return definition.BackendFamily +} + +func SameClientBackendForScope(first, second ClientID, scope InstallScope) bool { + if first == second { + return true + } + left := BackendFamilyForScope(first, scope) + return left != "" && left == BackendFamilyForScope(second, scope) +} + +func BackendSiblingsForScope(id ClientID, scope InstallScope) []ClientID { + if scope != ScopeProject { + return BackendSiblings(id) + } + var siblings []ClientID + for _, candidate := range clientDefinitions { + if candidate.ID != id && SameClientBackendForScope(id, candidate.ID, scope) { + siblings = append(siblings, candidate.ID) + } + } + return siblings +} + +func (delivery SelectedDelivery) SharesBackendForScope(id ClientID, scope InstallScope) bool { + if scope != ScopeProject { + return delivery.SharesBackend(id) + } + return delivery.IsZero() && len(BackendSiblingsForScope(id, scope)) > 0 +} diff --git a/install/integrationctl/agentplugins/domain/target_context.go b/install/integrationctl/agentplugins/domain/target_context.go new file mode 100644 index 000000000..226c149d8 --- /dev/null +++ b/install/integrationctl/agentplugins/domain/target_context.go @@ -0,0 +1,45 @@ +package domain + +import ( + "fmt" + "path/filepath" + "strings" +) + +// TargetContext separates the selected project from the original user profile. +// These operational locators never contribute to portable source identity. +type TargetContext struct { + Scope InstallScope `json:"-"` + ProjectRoot string `json:"-"` + UserConfigRoot string `json:"-"` +} + +// NewTargetContext validates explicit locators without observing or creating +// directories. The composition boundary canonicalizes the selected directory. +func NewTargetContext(scope InstallScope, projectRoot, userConfigRoot string) (TargetContext, error) { + if scope == "" { + scope = ScopeUser + } + target := TargetContext{Scope: scope, ProjectRoot: projectRoot, UserConfigRoot: userConfigRoot} + if scope != ScopeUser && scope != ScopeProject && scope != ScopeLocal { + return TargetContext{}, fmt.Errorf("unsupported installation scope %q", scope) + } + if userConfigRoot != "" && !cleanTargetRoot(userConfigRoot) { + return TargetContext{}, fmt.Errorf("user configuration root must be absolute and clean") + } + if scope != ScopeProject { + if projectRoot != "" { + return TargetContext{}, fmt.Errorf("project root requires project scope") + } + return target, nil + } + if !cleanTargetRoot(projectRoot) || filepath.Dir(projectRoot) == projectRoot { + return TargetContext{}, fmt.Errorf("project scope requires an explicit absolute clean non-root directory") + } + return target, nil +} + +func cleanTargetRoot(root string) bool { + return root != "" && strings.TrimSpace(root) == root && filepath.IsAbs(root) && + filepath.Clean(root) == root && !strings.ContainsAny(root, "\x00\r\n") +} diff --git a/install/integrationctl/agentplugins/domain/target_context_test.go b/install/integrationctl/agentplugins/domain/target_context_test.go new file mode 100644 index 000000000..32c06f04e --- /dev/null +++ b/install/integrationctl/agentplugins/domain/target_context_test.go @@ -0,0 +1,56 @@ +package domain + +import ( + "encoding/json" + "path/filepath" + "strings" + "testing" +) + +func TestTargetContextKeepsOperationalRootsPrivate(t *testing.T) { + root := t.TempDir() + target, err := NewTargetContext(ScopeProject, root, filepath.Join(root, "user-profile")) + if err != nil { + t.Fatal(err) + } + for _, value := range []any{target, DeliveryPlan{Scope: ScopeProject, TargetContext: target}, + PlanRequest{ProjectRoot: root}, DeactivationRequest{TargetContext: target}} { + body, err := json.Marshal(value) + if err != nil || strings.Contains(string(body), root) { + t.Fatalf("operational root escaped private projection: %s, %v", body, err) + } + } +} + +func TestTargetContextRejectsAmbiguousScopeRoots(t *testing.T) { + root := t.TempDir() + for _, test := range []struct { + scope InstallScope + root string + }{{ScopeUser, root}, {ScopeProject, ""}, {ScopeProject, "."}, {ScopeProject, filepath.VolumeName(root) + string(filepath.Separator)}, {"unknown", root}} { + if _, err := NewTargetContext(test.scope, test.root, ""); err == nil { + t.Fatalf("accepted scope %q with root %q", test.scope, test.root) + } + } + if target, err := NewTargetContext("", "", ""); err != nil || target.Scope != ScopeUser { + t.Fatalf("historical empty scope did not retain user default: %+v, %v", target, err) + } +} + +func TestProjectPolicyRequiresQualifiedDeclaration(t *testing.T) { + selected := SelectedDelivery{} + if got := selected.EffectiveTraitsForScope(ClientCodex, ScopeUser); got.LifecycleKind != ClientTraitsFor(ClientCodex).LifecycleKind { + t.Fatal("user lifecycle policy changed") + } + if got := selected.EffectiveTraitsForScope(ClientID("unqualified-fixture"), ScopeProject); got.LifecycleKind != "" { + t.Fatal("unqualified project borrowed user lifecycle policy") + } + if SameClientBackendForScope(ClientCopilot, ClientVSCode, ScopeProject) || + !SameClientBackendForScope(ClientCopilot, ClientVSCode, ScopeUser) { + t.Fatal("project sibling isolation or historical user sharing changed") + } + definition := withProjectScope(ClientDefinition{ID: "fixture", Capabilities: ClientCapabilities{Scopes: []InstallScope{ScopeUser}}}, LifecycleNativeConfig) + if !definition.ProjectPolicy.Valid() || definition.ProjectPolicy.BackendFamily != "fixture" || len(definition.Capabilities.Scopes) != 2 { + t.Fatalf("qualified table helper lost policy: %+v", definition) + } +} diff --git a/install/integrationctl/agentplugins/domain/traits.go b/install/integrationctl/agentplugins/domain/traits.go index a66a7db50..919b4153c 100644 --- a/install/integrationctl/agentplugins/domain/traits.go +++ b/install/integrationctl/agentplugins/domain/traits.go @@ -69,6 +69,14 @@ func RequiresNativeProjector(id ClientID) bool { return id == ClientVSCode } +func RequiresNativeProjectorForScope(id ClientID, scope InstallScope) bool { + if scope != ScopeProject { + return RequiresNativeProjector(id) + } + definition, ok := ClientDefinitionFor(id) + return ok && definition.ProjectPolicy.Valid() +} + // Allows reports whether this client's table lists the intent. Validate still // accepts historical empty automatic intent even when the slice omits it; // callers that need that exception must go through Validate. @@ -95,7 +103,11 @@ func SharesBackend(first, second ClientID) bool { // not ChatGPT-style personal mapping when the intent is prepare or the // executable names the client. func ShouldReadOnlyVerify(id ClientID, executable string, intent InstallIntent) bool { - traits := ClientTraitsFor(id) + return ShouldReadOnlyVerifyForScope(id, ScopeUser, executable, intent) +} + +func ShouldReadOnlyVerifyForScope(id ClientID, scope InstallScope, executable string, intent InstallIntent) bool { + traits := (SelectedDelivery{}).EffectiveTraitsForScope(id, scope) switch { case traits.LifecycleKind == LifecycleNativeConfig: return true @@ -103,7 +115,7 @@ func ShouldReadOnlyVerify(id ClientID, executable string, intent InstallIntent) return strings.TrimSpace(executable) != "" case traits.HonorsOpenAIMCPAuthHints: return strings.TrimSpace(executable) != "" - case len(BackendSiblings(id)) > 0: + case len(BackendSiblingsForScope(id, scope)) > 0: return strings.TrimSpace(executable) != "" case traits.Allows(InstallIntentPrepare) && !traits.RequiresPersonalMappingForPrepare: if intent == InstallIntentPrepare { diff --git a/install/integrationctl/agentplugins/environment/coordinator.go b/install/integrationctl/agentplugins/environment/coordinator.go index 1bb3e4fad..8fb459bb8 100644 --- a/install/integrationctl/agentplugins/environment/coordinator.go +++ b/install/integrationctl/agentplugins/environment/coordinator.go @@ -26,8 +26,16 @@ type BindingMutator interface { } type Coordinator struct { - Resolver Resolver - Mutator BindingMutator + EnvironmentID string + Resolver Resolver + Mutator BindingMutator +} + +func (coordinator Coordinator) environmentID() string { + if coordinator.EnvironmentID == "" { + return EnvironmentUserDefault + } + return coordinator.EnvironmentID } func (coordinator Coordinator) Resolve(ctx context.Context, requirements Requirements, previous Lock, updateAlias string) (Lock, error) { @@ -117,7 +125,7 @@ func (coordinator Coordinator) prepareSync(requirements Requirements, lock Lock, if err := lock.Validate(requirements); err != nil { return PreparedSync{}, err } - plan := PreparedSync{EnvironmentID: EnvironmentUserDefault, ManifestDigest: lock.ManifestDigest} + plan := PreparedSync{EnvironmentID: coordinator.environmentID(), ManifestDigest: lock.ManifestDigest} byAliasTarget := map[string]BindingObservation{} for _, item := range observed { byAliasTarget[item.Alias+"\x00"+item.ClientID] = item @@ -143,7 +151,7 @@ func (coordinator Coordinator) prepareSync(requirements Requirements, lock Lock, if current.Pending { return PreparedSync{}, installer.ErrRecoveryRequired } - plan.Bindings = append(plan.Bindings, PreparedBinding{Alias: current.Alias, ClientID: current.ClientID, InstallationID: current.InstallationID, BindingID: current.BindingID, Action: ActionRemove, Authority: bindingAuthority(current.Alias, current.ClientID, current.SourceKey, ActionRemove, current)}) + plan.Bindings = append(plan.Bindings, PreparedBinding{Alias: current.Alias, ClientID: current.ClientID, InstallationID: current.InstallationID, BindingID: current.BindingID, Action: ActionRemove, Authority: bindingAuthority(plan.EnvironmentID, current.Alias, current.ClientID, current.SourceKey, ActionRemove, current)}) } } if err := validatePlannedAttestation(plan, attestation); err != nil { @@ -199,8 +207,8 @@ func (coordinator Coordinator) ApplySync(ctx context.Context, plan PreparedSync, return results, nil } -func bindingAuthority(alias, target, source string, action DesiredAction, observed BindingObservation) *domain.EnvironmentAuthority { - out := &domain.EnvironmentAuthority{EnvironmentID: EnvironmentUserDefault, Alias: alias, ClientID: target, InstallationID: observed.InstallationID, ClientBindingID: observed.BindingID, SourceKey: source, ExpectedAction: string(action)} +func bindingAuthority(environmentID, alias, target, source string, action DesiredAction, observed BindingObservation) *domain.EnvironmentAuthority { + out := &domain.EnvironmentAuthority{EnvironmentID: environmentID, Alias: alias, ClientID: target, InstallationID: observed.InstallationID, ClientBindingID: observed.BindingID, SourceKey: source, ExpectedAction: string(action)} if observed.Receipt != nil { out.ReceiptID = observed.Receipt.ReceiptID } @@ -288,7 +296,7 @@ func appendDesiredBindings(plan *PreparedSync, lock Lock, observed []BindingObse ActivationComplete: activation, AuthComplete: auth, Alias: pkg.Alias, ClientID: target, InstallationID: current.InstallationID, BindingID: current.BindingID, Action: action, Package: &pkg, Reason: reason, - Authority: bindingAuthority(pkg.Alias, target, pkg.SourceKey, action, current), + Authority: bindingAuthority(plan.EnvironmentID, pkg.Alias, target, pkg.SourceKey, action, current), }) } } diff --git a/install/integrationctl/agentplugins/environment/model.go b/install/integrationctl/agentplugins/environment/model.go index 7adafbff0..35575454f 100644 --- a/install/integrationctl/agentplugins/environment/model.go +++ b/install/integrationctl/agentplugins/environment/model.go @@ -19,10 +19,11 @@ import ( ) const ( - ManifestSchemaVersion = 1 - LockVersion = 1 - EnvironmentUserDefault = "user-default" - TreeDigestAlgorithm = "agentplugins-tree-sha256-v1" + ManifestSchemaVersion = 1 + LockVersion = 1 + EnvironmentUserDefault = "user-default" + EnvironmentProjectDefault = "project-default" + TreeDigestAlgorithm = "agentplugins-tree-sha256-v1" ) var ( diff --git a/install/integrationctl/agentplugins/environment/observe.go b/install/integrationctl/agentplugins/environment/observe.go index 67fd3389b..c065ccf6d 100644 --- a/install/integrationctl/agentplugins/environment/observe.go +++ b/install/integrationctl/agentplugins/environment/observe.go @@ -41,6 +41,13 @@ func LifecycleFacts(binding installer.InspectedBinding) (retry bool, facts []str // receipts. Unmanaged siblings remain visible so planning cannot adopt their // shared installation through a different target. func Observe(view installer.Inspection) ([]BindingObservation, error) { + return ObserveForEnvironment(view, EnvironmentUserDefault) +} + +// ObserveForEnvironment uses the selected namespace's explicit receipt owner. +// Other ownership stays visible as foreign and cannot be adopted by alias. +func ObserveForEnvironment(view installer.Inspection, environmentID string) ([]BindingObservation, error) { + environmentID = (Coordinator{EnvironmentID: environmentID}).environmentID() if view.Recovery.Required { return nil, installer.ErrRecoveryRequired } @@ -54,7 +61,7 @@ func Observe(view installer.Inspection) ([]BindingObservation, error) { continue } live[receipt.ReceiptID] = true - if receipt.EnvironmentID != EnvironmentUserDefault { + if receipt.EnvironmentID != environmentID { continue } if receipt.SourceKey != installation.SourceKey || receipt.PackageDigest != binding.TreeDigest { @@ -70,7 +77,7 @@ func Observe(view installer.Inspection) ([]BindingObservation, error) { } } for _, receipt := range view.EnvironmentReceipts { - if receipt.EnvironmentID == EnvironmentUserDefault && !live[receipt.ReceiptID] { + if receipt.EnvironmentID == environmentID && !live[receipt.ReceiptID] { return nil, errors.New("partial: environment receipt has no live binding") } } diff --git a/install/integrationctl/agentplugins/environment/project_test.go b/install/integrationctl/agentplugins/environment/project_test.go new file mode 100644 index 000000000..2f00309b4 --- /dev/null +++ b/install/integrationctl/agentplugins/environment/project_test.go @@ -0,0 +1,33 @@ +package environment + +import ( + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +func TestProjectObservationCannotAdoptAnotherEnvironmentReceipt(t *testing.T) { + receipt := domain.EnvironmentReceipt{ReceiptID: "project-receipt", EnvironmentID: EnvironmentProjectDefault, + Alias: "tools", ClientID: "codex", InstallationID: "installed", ClientBindingID: "binding", SourceKey: "github:acme/tools:", PackageDigest: "digest"} + view := installer.Inspection{EnvironmentReceipts: []domain.EnvironmentReceipt{receipt}, Installations: []installer.InspectedInstallation{{ + InstallationID: receipt.InstallationID, SourceKey: receipt.SourceKey, + Bindings: []installer.InspectedBinding{{BindingID: receipt.ClientBindingID, ClientID: receipt.ClientID, + TreeDigest: receipt.PackageDigest, Materialization: "materialized", LifecycleConverged: true}}, + }}} + user, err := Observe(view) + if err != nil || len(user) != 1 || user[0].Managed { + t.Fatalf("user observation adopted project ownership: %+v, %v", user, err) + } + project, err := ObserveForEnvironment(view, EnvironmentProjectDefault) + if err != nil || len(project) != 1 || !project[0].Managed || project[0].Receipt.EnvironmentID != EnvironmentProjectDefault { + t.Fatalf("project ownership lost: %+v, %v", project, err) + } + requirements := Requirements{SchemaVersion: ManifestSchemaVersion, Dependencies: map[string]Requirement{}, Targets: map[string][]string{}} + lock := Lock{LockVersion: LockVersion, ManifestDigest: mustDigest(t, requirements)} + plan, err := (Coordinator{EnvironmentID: EnvironmentProjectDefault}).PrepareSync(requirements, lock, project) + if err != nil || len(plan.Bindings) != 1 || plan.Bindings[0].Action != ActionRemove || + plan.Bindings[0].Authority.EnvironmentID != EnvironmentProjectDefault { + t.Fatalf("removal borrowed user authority: %+v, %v", plan, err) + } +} diff --git a/install/integrationctl/agentplugins/go.mod b/install/integrationctl/agentplugins/go.mod index 5ec36800a..b90e6674f 100644 --- a/install/integrationctl/agentplugins/go.mod +++ b/install/integrationctl/agentplugins/go.mod @@ -8,6 +8,7 @@ replace github.com/777genius/plugin-kit-ai/install/integrationctl => ../ require ( github.com/777genius/plugin-kit-ai/install/integrationctl v0.0.0-20260919103549-148794ac5947 + github.com/creachadair/tomledit v0.0.29 github.com/dlclark/regexp2 v1.12.0 github.com/pelletier/go-toml/v2 v2.3.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 diff --git a/install/integrationctl/agentplugins/go.sum b/install/integrationctl/agentplugins/go.sum index ffc0218c1..bef534eba 100644 --- a/install/integrationctl/agentplugins/go.sum +++ b/install/integrationctl/agentplugins/go.sum @@ -1,3 +1,5 @@ +github.com/creachadair/tomledit v0.0.29 h1:dB5CbdwJMpn/fmfAPTAAleXF/KJwY0Ggc1eL/zvZRgk= +github.com/creachadair/tomledit v0.0.29/go.mod h1:4SoTXxzHgvzHRMIJPw+o6zK/yXii4VjLrb6/3gCQnyA= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= diff --git a/install/integrationctl/agentplugins/installer/apply.go b/install/integrationctl/agentplugins/installer/apply.go index d3d3d2a51..fbb271668 100644 --- a/install/integrationctl/agentplugins/installer/apply.go +++ b/install/integrationctl/agentplugins/installer/apply.go @@ -24,6 +24,9 @@ func (e *Engine) Apply(ctx context.Context, prepared *PreparedOperation, decisio return Result{}, err } defer func() { + if result.Binding.ClientID != "" { + result.Binding.TargetContext = preparedTargetContext(prepared, result.Binding.ClientID) + } prepared.mu.Lock() prepared.busy = false if err == nil { @@ -188,7 +191,7 @@ func (e *Engine) applyMutatingPackage(ctx context.Context, prepared *PreparedOpe return Result{Operation: prepared.req.Operation, Outcome: OutcomeConflict, Reason: "plan_changed"}, err } added, err := call(svc, usecase.AddInput{ - Envelope: prepared.envelope, Client: prepared.client, Scope: domain.ScopeUser, Confirmed: true, + Envelope: prepared.envelope, Client: prepared.client, Scope: e.scope(), Confirmed: true, InstallationID: firstNonEmpty(prepared.req.InstallationID, prepared.plan.InstallationID), OperationID: prepared.req.OperationID, BackendExecutable: prepared.req.ClientExecutable, OriginMode: prepared.snapshot.OriginMode, DirectoryResolution: cloneAcquiredDirectory(prepared.snapshot.Directory), @@ -294,7 +297,7 @@ func (e *Engine) applyRemove(ctx context.Context, prepared *PreparedOperation) ( if !ok { return Result{Operation: OpRemove, Outcome: OutcomeIncomplete, Reason: "client is not installed"}, fmt.Errorf("%w: client %s is not installed", ErrInvalidRequest, prepared.client.ClientID) } - if err := e.removalPreflight(ctx, prepared.client, binding, receipt); err != nil { + if err := e.removalPreflight(ctx, prepared.client, installation.DeclaredName, binding, receipt); err != nil { return Result{Operation: OpRemove, Outcome: OutcomeIncomplete, Reason: err.Error()}, err } helper, _ := e.helper() @@ -304,7 +307,7 @@ func (e *Engine) applyRemove(ctx context.Context, prepared *PreparedOperation) ( return removalConflict(Result{Operation: OpRemove, Binding: prepared.facts}, err) } removed, err := svc.Remove(ctx, usecase.RemoveInput{ - Selector: prepared.plan.InstallationID, Client: prepared.client, Scope: domain.ScopeUser, + Selector: prepared.plan.InstallationID, Client: prepared.client, Scope: e.scope(), Confirmed: true, OperationID: prepared.req.OperationID, BackendExecutable: prepared.req.ClientExecutable, ExternalUninstalled: prepared.req.ExternalUninstalled, SelectedDelivery: prepared.plan.SelectedDelivery, EnvironmentAuthority: prepared.req.EnvironmentAuthority, diff --git a/install/integrationctl/agentplugins/installer/apply_confirm.go b/install/integrationctl/agentplugins/installer/apply_confirm.go index 32b22e796..bd8f2dae3 100644 --- a/install/integrationctl/agentplugins/installer/apply_confirm.go +++ b/install/integrationctl/agentplugins/installer/apply_confirm.go @@ -52,7 +52,7 @@ func (e *Engine) confirmInstallTarget(ctx context.Context, prepared *PreparedOpe return err } if prepared.artifact != "" { - target, err := e.planner().ResolveTarget(ctx, prepared.client, domain.ScopeUser, prepared.artifact) + target, err := e.planner().ResolveTarget(ctx, prepared.client, e.scope(), prepared.artifact) if err != nil { return fmt.Errorf("%w: %w", ErrPlanChanged, err) } diff --git a/install/integrationctl/agentplugins/installer/config.go b/install/integrationctl/agentplugins/installer/config.go index 357fc7111..9339827d3 100644 --- a/install/integrationctl/agentplugins/installer/config.go +++ b/install/integrationctl/agentplugins/installer/config.go @@ -11,11 +11,14 @@ import ( "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/clientdetect" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/ports" ) // Config is copied by New. Later mutation of the caller's value is ignored. type Config struct { + temporaryRoot bool + ProjectRoot string `json:"-"` // OpenCodeProbe defaults to the explicit-target facade. New captures and pins // the allowlisted environment once; it never executes a probe. OpenCodeProbe OpenCodeProbe @@ -28,9 +31,10 @@ type Config struct { // Registry is the explicit set of client adapters supported by this // executable. Nil is rejected rather than silently enabling every client. Registry *clients.Registry - // EnableNativeObserver composes the namespace-aware client observer for - // repair and recovery. Callers that provide a real command runner should - // enable it; leaving it off retains the filesystem-only compatibility path. + // EnableNativeObserver enables live client discovery for repair and recovery + // when Runner is supplied. Project filesystem ownership checks remain + // mandatory when it is disabled or no runner is available. User scope keeps + // its existing filesystem-only compatibility path when disabled. EnableNativeObserver bool Runner ports.CommandRunner // ServerName selects the declared MCP server whose args the host may replace. @@ -60,6 +64,11 @@ type Config struct { func (c Config) resolved() (Config, error) { out := c + if out.ProjectRoot != "" { + if _, err := domain.NewTargetContext(domain.ScopeProject, out.ProjectRoot, ""); err != nil { + return Config{}, fmt.Errorf("%w: %w", ErrInvalidConfig, err) + } + } if !validRoot(out.StateRoot) { return Config{}, fmt.Errorf("%w: StateRoot must be an explicit absolute clean path", ErrInvalidConfig) } diff --git a/install/integrationctl/agentplugins/installer/discover.go b/install/integrationctl/agentplugins/installer/discover.go index ab37a3189..40843d7d2 100644 --- a/install/integrationctl/agentplugins/installer/discover.go +++ b/install/integrationctl/agentplugins/installer/discover.go @@ -5,6 +5,7 @@ import ( "os/exec" "path/filepath" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) @@ -33,6 +34,14 @@ func (e *Engine) clientMetadata(id string, bindings []InspectedBinding) ClientMe meta := ClientMetadata{ClientID: id, Scopes: []string{"user"}, Bindings: append([]InspectedBinding(nil), bindings...)} definition, _ := domain.ClientDefinitionFor(domain.ClientID(id)) meta.Capabilities = definition.Capabilities + meta.Scopes = nil + meta.Capabilities.Scopes = nil + for _, scope := range definition.Capabilities.Scopes { + if clients.SupportsScope(e.cfg.Registry, definition.ID, scope) { + meta.Scopes = append(meta.Scopes, string(scope)) + meta.Capabilities.Scopes = append(meta.Capabilities.Scopes, scope) + } + } explicit := "" if e.cfg.ClientExecutables != nil { explicit = e.cfg.ClientExecutables[id] diff --git a/install/integrationctl/agentplugins/installer/engine.go b/install/integrationctl/agentplugins/installer/engine.go index ef2861f61..d345cbbe2 100644 --- a/install/integrationctl/agentplugins/installer/engine.go +++ b/install/integrationctl/agentplugins/installer/engine.go @@ -27,8 +27,9 @@ import ( // Engine is the process-local installer. It does not export Store or Kernel. type Engine struct { - cfg Config - store transaction.StateStore + projectGuard *projectNamespaceGuard + cfg Config + store transaction.StateStore // persistObservations enables the §5.4 PersistAuthoritativeObservations // seam. Prepare still uses DryRun and does not persist. persistObservations bool @@ -41,7 +42,11 @@ func New(cfg Config) (*Engine, error) { if err != nil { return nil, err } - return &Engine{cfg: resolved, store: statev2.Store{Path: resolved.StateFile}}, nil + engine := &Engine{cfg: resolved, store: statev2.Store{Path: resolved.StateFile, ProjectRoot: resolved.ProjectRoot}, projectGuard: &projectNamespaceGuard{}} + if err := engine.CheckProjectNamespace(); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInvalidConfig, err) + } + return engine, nil } // ClientSupported reports saved-dependency qualification for an explicit @@ -50,6 +55,23 @@ func ClientSupported(registry *clients.Registry, clientID string) bool { return ClientSupportExclusionReason(registry, clientID) == "" } +// ClientScopeSupported is the read-only saved-admission gate before source +// acquisition. A qualified project declaration does not reopen deferred user +// dependency support or admit remote/manual-only clients. +func ClientScopeSupported(registry *clients.Registry, clientID string, scope domain.InstallScope) bool { + if scope == "" { + scope = domain.ScopeUser + } + if !clients.SupportsScope(registry, domain.ClientID(clientID), scope) { + return false + } + if scope != domain.ScopeProject { + return ClientSupported(registry, clientID) + } + definition, _ := domain.ClientDefinitionFor(domain.ClientID(clientID)) + return !definition.PlansWithoutHostPresence && !definition.Traits.RequiresPersonalMappingForPrepare +} + // ClientSupportExclusionReason is the shared qualification policy for the SDK, // CLI and generated public metadata. It never removes a registered adapter. func ClientSupportExclusionReason(registry *clients.Registry, clientID string) string { @@ -85,6 +107,9 @@ func (e *Engine) lifecycle(helper *managedstdio.Source, facts BindingFacts, dete nativeKernel := nativeconfig.New() frozenProfiles := profileClients(detected) profileCheck := func(ctx context.Context) error { + if err := e.CheckProjectNamespace(); err != nil { + return err + } for _, c := range frozenProfiles { token := domain.ProfileAuthority{} if c.ProfileAuthority != nil { @@ -106,16 +131,19 @@ func (e *Engine) lifecycle(helper *managedstdio.Source, facts BindingFacts, dete projectArgs: e.cfg.ProjectArgs, facts: facts, } - transitions := opencode.NativeTransitions{PackageVerifier: stager, State: transaction.Kernel{StateStore: e.store}, Kernel: nativeKernel} + transitions := opencode.NativeTransitions{ProjectRoot: e.cfg.ProjectRoot, PackageVerifier: stager, State: transaction.Kernel{StateStore: e.store}, Kernel: nativeKernel} inner := providers.Activator{OpenCodeTransitions: transitions, Runner: e.cfg.Runner, Registry: e.cfg.Registry, NativeConfig: &nativeKernel} var observer usecase.NativeIdentityObserver + identity := providers.NativeIdentityObserver{Stager: stager, NativeConfig: &nativeKernel, Registry: e.cfg.Registry} if e.cfg.EnableNativeObserver && e.cfg.Runner != nil { - observer = providers.NativeIdentityObserver{ - Stager: stager, Runner: e.cfg.Runner, NativeConfig: &nativeKernel, Registry: e.cfg.Registry, - } + identity.Runner = e.cfg.Runner + observer = identity + } else if e.cfg.ProjectRoot != "" { + observer = preparedProjectIdentityObserver{identity: identity} } return usecase.Service{ - StateStore: e.store, Paths: paths, Planner: plan, Targets: plan, Stager: stager, + CheckNamespace: e.CheckProjectNamespace, + StateStore: e.store, Paths: paths, Planner: plan, Targets: plan, Stager: stager, ProjectRoot: e.cfg.ProjectRoot, Detected: detected, Activator: seamActivator{profileCheck: profileCheck, inner: inner, onCommitted: e.cfg.OnCommittedBinding, store: e.store, facts: facts}, PluginData: providers.PluginDataManager{Base: e.cfg.PluginDataBase}, @@ -129,6 +157,7 @@ func (e *Engine) lifecycle(helper *managedstdio.Source, facts BindingFacts, dete func (e *Engine) planner() planner.Planner { return planner.Planner{ + ProjectRoot: e.cfg.ProjectRoot, ManagedRoot: e.cfg.ManagedRoot, Paths: pathpolicy.Policy{}, Registry: e.cfg.Registry, @@ -166,6 +195,9 @@ func (e *Engine) helperIdentity() (version, digest string) { } func (e *Engine) ensureDirs() error { + if err := e.CheckProjectNamespace(); err != nil { + return err + } for _, dir := range []string{ filepath.Dir(e.cfg.StateFile), filepath.Dir(e.cfg.LockFile), e.cfg.OperationsDir, e.cfg.PluginDataBase, e.cfg.ManagedRoot, e.cfg.TempRoot, @@ -174,7 +206,7 @@ func (e *Engine) ensureDirs() error { return err } } - return nil + return e.CheckProjectNamespace() } func newLoader() (loader.Loader, error) { @@ -269,6 +301,9 @@ func (e *Engine) checkRequestProfiles(ctx context.Context, req Request) error { return e.prevalidatePhysical(ctx) } func (e *Engine) prevalidatePhysical(ctx context.Context) error { + if err := e.CheckProjectNamespace(); err != nil { + return err + } if err := (transaction.Kernel{Namespace: e.cfg.StateRoot, PhysicalAuthority: e.planner(), StateStore: e.store, Directory: dirswap.Manager{Namespace: e.cfg.StateRoot, JournalDir: e.cfg.OperationsDir}}).PrevalidateRecovery(ctx); err != nil { return fmt.Errorf("read installation state and pending authority: %w", err) } @@ -291,7 +326,7 @@ func physicalDetected(req Request, detected map[domain.ClientID]domain.DetectedC func (e *Engine) captureClientProfile(ctx context.Context, installation domain.Installation, c domain.DetectedClient) (domain.DetectedClient, error) { var recorded *domain.ClientBinding for key, b := range installation.Clients { - if b.ClientID != string(c.ClientID) || b.Scope != string(domain.ScopeUser) { + if b.ClientID != string(c.ClientID) || b.Scope != string(e.scope()) { continue } if recorded != nil || key != b.ClientBindingID { @@ -317,11 +352,17 @@ func (e *Engine) captureClientProfile(ctx context.Context, installation domain.I } } if c.ProfileAuthority != nil { - canonical, err := filepath.EvalSymlinks(c.ConfigRoot) + expected, err := e.nativeRegistryRoot(c) + if err != nil { + return c, err + } + canonical, err := filepath.EvalSymlinks(expected) if err != nil || canonical != c.ProfileAuthority.Facts().CanonicalRoot { return c, fmt.Errorf("selected alias differs from frozen physical profile") } - c.ConfigRoot = canonical + if e.scope() == domain.ScopeUser { + c.ConfigRoot = canonical + } } return c, nil } @@ -345,10 +386,17 @@ func (e *Engine) preparedClients(req Request) (domain.DetectedClient, map[domain func (e *Engine) WithTemporaryRoot(root string) (*Engine, error) { cfg := e.cfg cfg.TempRoot = root + cfg.temporaryRoot = true derived, err := New(cfg) if err != nil { return nil, err } + // Derived preparation observes the same selected physical namespace. A new + // staging directory must not reset the original engine's identity pin. + derived.projectGuard = e.projectGuard + if err := derived.CheckProjectNamespace(); err != nil { + return nil, err + } // Prepare always uses DryRun; preserve the observation policy for an // eventual confirmed Apply without enabling writes during preparation. derived.persistObservations = e.persistObservations diff --git a/install/integrationctl/agentplugins/installer/engine_test.go b/install/integrationctl/agentplugins/installer/engine_test.go index 675c69341..27bd40c96 100644 --- a/install/integrationctl/agentplugins/installer/engine_test.go +++ b/install/integrationctl/agentplugins/installer/engine_test.go @@ -6306,7 +6306,7 @@ func TestDiscoverDoesNotCreateStateOrRunHelper(t *testing.T) { t.Fatalf("discover: %+v", got) } claude := discoveredClient(t, got, "claude") - if len(claude.Scopes) != 1 || claude.Scopes[0] != "user" { + if len(claude.Scopes) != 2 || claude.Scopes[0] != "user" || claude.Scopes[1] != "project" { t.Fatalf("scopes: %+v", got) } if runner.n != 0 { diff --git a/install/integrationctl/agentplugins/installer/group.go b/install/integrationctl/agentplugins/installer/group.go index 116d37645..b4b70fe70 100644 --- a/install/integrationctl/agentplugins/installer/group.go +++ b/install/integrationctl/agentplugins/installer/group.go @@ -90,7 +90,7 @@ func (e *Engine) groupAddInputs(req Request, envelopes []domain.PackageEnvelope, return nil, nil, err } inputs = append(inputs, usecase.AddInput{ - Envelope: envelopes[i], Client: client, Scope: domain.ScopeUser, DryRun: dry, Confirmed: confirmed, + Envelope: envelopes[i], Client: client, Scope: e.scope(), DryRun: dry, Confirmed: confirmed, PersistAuthoritativeObservations: e.persistObservations, InstallationID: req.InstallationID, OperationID: req.OperationID, BackendExecutable: client.ExecutablePath, diff --git a/install/integrationctl/agentplugins/installer/group_apply.go b/install/integrationctl/agentplugins/installer/group_apply.go index 251986882..7987826d9 100644 --- a/install/integrationctl/agentplugins/installer/group_apply.go +++ b/install/integrationctl/agentplugins/installer/group_apply.go @@ -49,10 +49,11 @@ func (e *Engine) applyGroup(ctx context.Context, prepared *PreparedOperation) (R } func (e *Engine) finishGroup(prepared *PreparedOperation, result Result, pending bool, got usecase.GroupResult, err error) (Result, error) { + result.Mutated = result.Mutated || got.Mutated err = wrapLifecycleError(err) result.InstallationID = firstNonEmpty(got.InstallationID, prepared.plan.InstallationID) state, loadErr := e.store.Load() - if loadErr != nil && err == nil && !got.Mutated && !prepared.plan.NoChange { + if prepared.req.Scope != domain.ScopeProject && loadErr != nil && err == nil && !got.Mutated && !prepared.plan.NoChange { result.Outcome = OutcomeIncomplete result.Reason = "committed state is unknown" return result, loadErr @@ -64,28 +65,25 @@ func (e *Engine) finishGroup(prepared *PreparedOperation, result Result, pending } attachGroupAttemptResult(&result, got) if err != nil { - result.Outcome = OutcomeIncomplete - result.Reason = err.Error() - if errors.Is(err, ErrPlanChanged) { - result.Outcome = OutcomeConflict - result.Reason = "plan_changed" - } - if strings.Contains(err.Error(), "run update separately") || errors.Is(err, usecase.ErrProjectionRefreshRequired) { - result.Outcome = OutcomeConflict - result.Reason = "update_required" + return failedGroupResult(result, err) + } + if prepared.req.Scope == domain.ScopeProject { + if loadErr != nil { + result.Outcome, result.Reason = OutcomeIncomplete, "committed project state is unknown" + return result, loadErr } - if errors.Is(err, usecase.ErrProjectionPinChangeRequired) { - result.Outcome = OutcomeConflict - result.Reason = "pin_change_required" + if completeErr := completedProjectGroup(prepared, got, state); completeErr != nil { + result.Outcome, result.Reason = OutcomeIncomplete, completeErr.Error() + return result, completeErr } - return result, err + pending = false } if pending && e.anyGroupHostHandoffPending(prepared) { result.Outcome = OutcomeIncomplete result.Reason = "committed binding is not activated" return result, fmt.Errorf("%s", result.Reason) } - if !pending && (prepared.plan.NoChange || (!got.Mutated && groupPreviewUnchanged(got))) { + if !result.Mutated && !pending && (prepared.plan.NoChange || (!got.Mutated && groupPreviewUnchanged(got))) { result.Outcome = OutcomeUnchanged result.NoChange = true return result, nil @@ -98,8 +96,29 @@ func (e *Engine) finishGroup(prepared *PreparedOperation, result Result, pending return result, nil } +func failedGroupResult(result Result, err error) (Result, error) { + result.Outcome = OutcomeIncomplete + result.Reason = err.Error() + if errors.Is(err, ErrPlanChanged) { + result.Outcome = OutcomeConflict + result.Reason = "plan_changed" + } + if strings.Contains(err.Error(), "run update separately") || errors.Is(err, usecase.ErrProjectionRefreshRequired) { + result.Outcome = OutcomeConflict + result.Reason = "update_required" + } + if errors.Is(err, usecase.ErrProjectionPinChangeRequired) { + result.Outcome = OutcomeConflict + result.Reason = "pin_change_required" + } + return result, err +} + func attachGroupAttemptResult(result *Result, got usecase.GroupResult) { for _, target := range got.Targets { + if target.Plan.Scope == domain.ScopeProject { + result.ManualActions = append(result.ManualActions, target.Activation.UserActions...) + } if target.Plan.ClientID == "" || target.GroupPhase == "" { continue } diff --git a/install/integrationctl/agentplugins/installer/group_handoff.go b/install/integrationctl/agentplugins/installer/group_handoff.go index a2df649d2..e08cd0a18 100644 --- a/install/integrationctl/agentplugins/installer/group_handoff.go +++ b/install/integrationctl/agentplugins/installer/group_handoff.go @@ -33,6 +33,7 @@ func (e *Engine) reconcileGroupHostHandoff(ctx context.Context, prepared *Prepar continue } facts := BindingFacts{ProfileAuthority: domain.CloneProfileAuthority(binding.ProfileAuthority), + TargetContext: preparedTargetContext(prepared, binding.ClientID), InstallationID: firstNonEmpty(installation.InstallationID, installationID), ClientID: binding.ClientID, BindingID: binding.ClientBindingID, @@ -146,7 +147,7 @@ func (e *Engine) confirmMutatingGroupTarget(ctx context.Context, prepared *Prepa } if i == 0 && prepared.artifact != "" && plan.Operation == OpInstall { client := prepared.clients[i] - resolved, resolveErr := e.planner().ResolveTarget(ctx, client, domain.ScopeUser, prepared.artifact) + resolved, resolveErr := e.planner().ResolveTarget(ctx, client, e.scope(), prepared.artifact) if resolveErr != nil { return fmt.Errorf("%w: %w", ErrPlanChanged, resolveErr) } diff --git a/install/integrationctl/agentplugins/installer/group_mutation_result_test.go b/install/integrationctl/agentplugins/installer/group_mutation_result_test.go new file mode 100644 index 000000000..1cc6a6065 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/group_mutation_result_test.go @@ -0,0 +1,67 @@ +package installer + +import ( + "errors" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/usecase" +) + +type groupReportingStore struct { + state domain.StateFileV2 + err error +} + +func (s groupReportingStore) Load() (domain.StateFileV2, error) { return s.state, s.err } +func (groupReportingStore) Save(domain.StateFileV2) error { panic("reporting must not persist state") } + +// Reporting cannot lose a known effect just because fresh inspection or the +// separate Prepared host handoff is incomplete. This table has no mutation seam. +func TestFinishGroupPreservesActualMutationFacts(t *testing.T) { + cause := errors.New("fixture activation failure") + loadFailure := errors.New("fixture state read failure") + for _, tc := range []struct { + name string + prior, core, preview, pending bool + loadErr, activationErr error + wantOutcome Outcome + wantErr bool + }{ + {name: "core effect", core: true, wantOutcome: OutcomeCompleted}, + {name: "prior host effect", prior: true, wantOutcome: OutcomeCompleted}, + {name: "stale preview with core effect", core: true, preview: true, wantOutcome: OutcomeCompleted}, + {name: "stale preview with prior effect", prior: true, preview: true, wantOutcome: OutcomeCompleted}, + {name: "read only unchanged preview", preview: true, wantOutcome: OutcomeUnchanged}, + {name: "read only unchanged core", wantOutcome: OutcomeUnchanged}, + {name: "effect before activation failure", core: true, activationErr: cause, wantOutcome: OutcomeIncomplete, wantErr: true}, + {name: "effect before prepared handoff", core: true, pending: true, wantOutcome: OutcomeIncomplete, wantErr: true}, + {name: "core effect with unavailable inspection", core: true, loadErr: loadFailure, wantOutcome: OutcomeCompleted}, + {name: "prior effect with unavailable inspection", prior: true, loadErr: loadFailure, wantOutcome: OutcomeIncomplete, wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + state := domain.StateFileV2{Installations: []domain.Installation{{InstallationID: "reporting-fixture", Clients: map[string]domain.ClientBinding{ + "binding": {ClientBindingID: "binding", ClientID: "claude", Scope: "project", Materialization: domain.MaterializationMaterialized, Activation: domain.ActivationPrepared, Verification: domain.VerificationInstalled}, + }}}} + engine := Engine{store: groupReportingStore{state: state, err: tc.loadErr}} + prepared := &PreparedOperation{req: Request{Operation: OpRepair}, plan: Plan{InstallationID: "reporting-fixture", NoChange: tc.preview}, clients: []domain.DetectedClient{{ClientID: domain.ClientClaude}}} + got := usecase.GroupResult{InstallationID: "reporting-fixture", Mutated: tc.core, Targets: []usecase.AddResult{{NoChange: true}}} + result, err := engine.finishGroup(prepared, Result{Mutated: tc.prior}, tc.pending, got, tc.activationErr) + if result.Mutated != (tc.prior || tc.core) || result.Outcome != tc.wantOutcome || (err != nil) != tc.wantErr { + t.Fatalf("reported=%+v err=%v", result, err) + } + if result.Mutated && (result.NoChange || result.Outcome == OutcomeUnchanged) { + t.Fatalf("actual effect called unchanged=%+v", result) + } + if !result.Mutated && tc.wantOutcome == OutcomeUnchanged && !result.NoChange { + t.Fatalf("read only result lost unchanged=%+v", result) + } + if tc.pending && (result.Reason != "committed binding is not activated" || result.Client.Activation != "prepared" || result.Client.Verification != "installation_verified") { + t.Fatalf("prepared handoff contract changed=%+v", result) + } + if tc.activationErr != nil && !errors.Is(err, tc.activationErr) { + t.Fatalf("activation error lost=%v", err) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/installer/group_project_completion.go b/install/integrationctl/agentplugins/installer/group_project_completion.go new file mode 100644 index 000000000..f662c412f --- /dev/null +++ b/install/integrationctl/agentplugins/installer/group_project_completion.go @@ -0,0 +1,170 @@ +package installer + +import ( + "fmt" + "reflect" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/usecase" +) + +// completedProjectGroup checks the existing fresh operation and saved identity +// before reporting file completion. It neither activates clients nor observes +// files: the core already performed those operations and retained their facts. +func completedProjectGroup(prepared *PreparedOperation, got usecase.GroupResult, state domain.StateFileV2) error { + if got.Phase != usecase.GroupPhaseCompleted || got.InstallationID == "" || + got.InstallationID != prepared.plan.InstallationID || got.InstallationID != prepared.req.InstallationID || prepared.req.ProjectRoot == "" { + return fmt.Errorf("%w: project group operation is not complete", ErrIncomplete) + } + size := len(prepared.plan.Targets) + if size == 0 || len(got.Targets) != size || len(prepared.clients) != size || len(prepared.req.Targets) != size { + return fmt.Errorf("%w: project group target set is inconsistent", ErrIncomplete) + } + installation, err := completedGroupInstallation(state, got.InstallationID) + if err != nil { + return err + } + targets := make(map[string]usecase.AddResult, size) + for _, target := range got.Targets { + id := string(target.Plan.ClientID) + if _, duplicate := targets[id]; duplicate || id == "" { + return fmt.Errorf("%w: project group target identity is ambiguous", ErrIncomplete) + } + targets[id] = target + } + for i, frozen := range prepared.plan.Targets { + target, ok := targets[frozen.ClientID] + if !ok || frozen.ClientID != string(prepared.clients[i].ClientID) || frozen.ClientID != prepared.req.Targets[i].ClientID { + return fmt.Errorf("%w: project group target differs from preparation", ErrIncomplete) + } + delete(targets, frozen.ClientID) + if err := completedProjectTarget(prepared, frozen, target, installation); err != nil { + return err + } + } + return nil +} + +func completedGroupInstallation(state domain.StateFileV2, id string) (domain.Installation, error) { + var found domain.Installation + count := 0 + for _, installation := range state.Installations { + if installation.InstallationID == id { + found, count = installation, count+1 + } + } + if count != 1 { + return domain.Installation{}, fmt.Errorf("%w: committed project installation is missing or ambiguous", ErrIncomplete) + } + return found, nil +} + +func completedProjectTarget(prepared *PreparedOperation, frozen PlanTarget, target usecase.AddResult, installation domain.Installation) error { + if target.InstallationID != installation.InstallationID || target.GroupPhase != usecase.GroupTargetExternalCompleted || target.Failure != nil { + return fmt.Errorf("%w: project target operation is not complete", ErrIncomplete) + } + if err := completedProjectPlan(prepared, frozen, target.Plan); err != nil { + return err + } + binding, ok := installation.Clients[frozen.BindingID] + if !ok || !completedProjectBindingIdentity(frozen, target.Plan, binding) { + return fmt.Errorf("%w: committed project binding differs from completed target", ErrIncomplete) + } + for key, other := range installation.Clients { + if other.ClientID == frozen.ClientID && key != frozen.BindingID { + return fmt.Errorf("%w: committed project client has ambiguous bindings", ErrIncomplete) + } + } + if binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil { + return fmt.Errorf("%w: committed project binding retains an unresolved native attempt", ErrIncomplete) + } + if !completedProjectOutcome(target.Plan, target.Activation) || !completedProjectLifecycle(binding, target.Activation) { + return fmt.Errorf("%w: project target lacks a verified completed operation", ErrIncomplete) + } + return nil +} + +func completedProjectPlan(prepared *PreparedOperation, frozen PlanTarget, plan domain.DeliveryPlan) error { + context := preparedTargetContext(prepared, frozen.ClientID) + bindingID := domain.ComputeClientBindingID(prepared.plan.InstallationID, frozen.ClientID, string(domain.ScopeProject), plan.ActivePath) + if plan.Scope != domain.ScopeProject || plan.TargetContext != context || context.Scope != domain.ScopeProject || + context.UserConfigRoot != frozen.ConfigRoot || frozen.BindingID == "" || frozen.BindingID != bindingID || + plan.ActivePath == "" || plan.ActivePath != frozen.TargetPath || plan.PhysicalArtifactID == "" { + return fmt.Errorf("%w: project result differs from the prepared context", ErrIncomplete) + } + if !reflect.DeepEqual(frozen.SelectedDelivery, plan.SelectedDelivery) || !domain.SameProfileAuthority(frozen.ProfileAuthority, plan.ProfileAuthority()) { + return fmt.Errorf("%w: project result differs from the prepared authority", ErrIncomplete) + } + matched := false + for _, delivery := range prepared.deliveries { + if string(delivery.plan.ClientID) != frozen.ClientID { + continue + } + matched = true + if !completedProjectDeliveryIdentity(delivery.plan, plan) { + return fmt.Errorf("%w: project result differs from the frozen delivery", ErrIncomplete) + } + } + if !matched { + return fmt.Errorf("%w: project result has no frozen delivery", ErrIncomplete) + } + return nil +} + +func completedProjectDeliveryIdentity(frozen, current domain.DeliveryPlan) bool { + return frozen.ClientID == current.ClientID && frozen.Scope == current.Scope && frozen.TargetContext == current.TargetContext && + frozen.PhysicalArtifactID == current.PhysicalArtifactID && frozen.ActivePath == current.ActivePath && + frozen.NativeRegistryRoot == current.NativeRegistryRoot && frozen.ProfileNamespace() == current.ProfileNamespace() && + domain.SameProfileAuthority(frozen.ProfileAuthority(), current.ProfileAuthority()) && reflect.DeepEqual(frozen.SelectedDelivery, current.SelectedDelivery) +} + +func completedProjectBindingIdentity(frozen PlanTarget, plan domain.DeliveryPlan, binding domain.ClientBinding) bool { + root := plan.NativeRegistryRoot + if authority := plan.ProfileAuthority(); authority != nil { + root = authority.Facts().CanonicalRoot + } + return binding.ClientBindingID == frozen.BindingID && binding.ClientID == frozen.ClientID && binding.Scope == string(domain.ScopeProject) && + binding.TargetLocator == plan.ActivePath && binding.PhysicalArtifact == plan.PhysicalArtifactID && binding.NativeProfileRoot == root && + binding.ProfileNamespace == plan.ProfileNamespace() && domain.SameProfileAuthority(binding.ProfileAuthority, plan.ProfileAuthority()) && + reflect.DeepEqual(binding.SelectedDelivery, plan.SelectedDelivery) +} + +func completedProjectLifecycle(binding domain.ClientBinding, outcome domain.ActivationOutcome) bool { + return binding.Materialization == domain.MaterializationMaterialized && binding.Activation == outcome.Activation && + binding.Authentication == outcome.Authentication && binding.Policy == outcome.Policy && binding.Verification == outcome.Verification +} + +func completedProjectOutcome(plan domain.DeliveryPlan, outcome domain.ActivationOutcome) bool { + if outcome.Policy != domain.PolicyAllowed || !completedProjectAuthentication(outcome.Authentication) || !completedProjectEffect(plan, outcome.NativeEffect) { + return false + } + switch outcome.Activation { + case domain.ActivationPrepared: + return outcome.Verification == domain.VerificationInstalled && !outcome.ActivationAttested && len(outcome.UserActions) != 0 + case domain.ActivationActive, domain.ActivationNotRequired: + return outcome.Verification == domain.VerificationInstalled || outcome.Verification == domain.VerificationRuntime + default: + return false + } +} + +func completedProjectAuthentication(authentication domain.AuthenticationState) bool { + switch authentication { + case domain.AuthenticationNotRequired, domain.AuthenticationNotChecked, domain.AuthenticationPending, domain.AuthenticationComplete: + return true + default: + return false + } +} + +func completedProjectEffect(plan domain.DeliveryPlan, effect domain.NativeEffectState) bool { + switch effect { + case domain.NativeEffectUnchanged, domain.NativeEffectCommitted: + return true + case "": + traits := plan.SelectedDelivery.EffectiveTraitsForScope(plan.ClientID, plan.Scope) + return !traits.TracksNativeEffects && (traits.LifecycleKind == domain.LifecycleCLIRegistry || traits.LifecycleKind == domain.LifecyclePrepared) + default: + return false + } +} diff --git a/install/integrationctl/agentplugins/installer/group_project_completion_test.go b/install/integrationctl/agentplugins/installer/group_project_completion_test.go new file mode 100644 index 000000000..c307a2f48 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/group_project_completion_test.go @@ -0,0 +1,270 @@ +package installer + +import ( + "encoding/json" + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/usecase" +) + +type projectGroupReportFixture struct { + prepared *PreparedOperation + got usecase.GroupResult + state domain.StateFileV2 +} + +func newProjectGroupReportFixture(t *testing.T) projectGroupReportFixture { + t.Helper() + project := filepath.Join(t.TempDir(), "project") + id := "project-group-report" + prepared := &PreparedOperation{req: Request{Operation: OpRepair, Scope: domain.ScopeProject, ProjectRoot: project, InstallationID: id}, + plan: Plan{InstallationID: id}, detected: map[domain.ClientID]domain.DetectedClient{}} + got := usecase.GroupResult{InstallationID: id, Phase: usecase.GroupPhaseCompleted} + installation := domain.Installation{InstallationID: id, Clients: map[string]domain.ClientBinding{}} + for _, clientID := range []domain.ClientID{domain.ClientCline, domain.ClientClaude} { + client := domain.DetectedClient{ClientID: clientID, ConfigRoot: filepath.Join(filepath.Dir(project), string(clientID))} + active := filepath.Join(project, "managed", string(clientID)) + target := domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: project, UserConfigRoot: client.ConfigRoot} + plan := domain.DeliveryPlan{ClientID: clientID, Scope: domain.ScopeProject, TargetContext: target, ActivePath: active, + PhysicalArtifactID: "artifact-" + string(clientID), NativeRegistryRoot: project} + bindingID := domain.ComputeClientBindingID(id, string(clientID), "project", active) + prepared.req.Targets = append(prepared.req.Targets, ClientTarget{ClientID: string(clientID), ClientConfigRoot: client.ConfigRoot}) + prepared.clients = append(prepared.clients, client) + prepared.detected[clientID] = client + prepared.plan.Targets = append(prepared.plan.Targets, PlanTarget{ClientID: string(clientID), ConfigRoot: client.ConfigRoot, TargetPath: active, BindingID: bindingID}) + prepared.deliveries = append(prepared.deliveries, preparedDeliveryPlan{plan: plan}) + effect := domain.NativeEffectUnchanged + if clientID == domain.ClientClaude { + effect = "" + } + outcome := domain.ActivationOutcome{Activation: domain.ActivationPrepared, Authentication: domain.AuthenticationNotChecked, + Policy: domain.PolicyAllowed, Verification: domain.VerificationInstalled, NativeEffect: effect, + UserActions: []string{`{"command":"inspect ` + string(clientID) + `"}`, "load the prepared project"}} + got.Targets = append(got.Targets, usecase.AddResult{InstallationID: id, Plan: plan, Activation: outcome, + GroupPhase: usecase.GroupTargetExternalCompleted, NoChange: true}) + installation.Clients[bindingID] = domain.ClientBinding{ClientBindingID: bindingID, ClientID: string(clientID), Scope: "project", + TargetLocator: active, PhysicalArtifact: plan.PhysicalArtifactID, NativeProfileRoot: project, + Materialization: domain.MaterializationMaterialized, Activation: outcome.Activation, Authentication: outcome.Authentication, + Policy: outcome.Policy, Verification: outcome.Verification} + } + return projectGroupReportFixture{prepared: prepared, got: got, state: domain.StateFileV2{Installations: []domain.Installation{installation}}} +} + +func (f *projectGroupReportFixture) changeBinding(change func(*domain.ClientBinding)) { + key := f.prepared.plan.Targets[0].BindingID + binding := f.state.Installations[0].Clients[key] + change(&binding) + f.state.Installations[0].Clients[key] = binding +} + +func TestFinishProjectGroupRetainsPreparedActionsAndActualMutation(t *testing.T) { + for _, mutation := range []bool{false, true} { + t.Run(map[bool]string{false: "unchanged", true: "actual effect"}[mutation], func(t *testing.T) { + f := newProjectGroupReportFixture(t) + f.prepared.plan.NoChange = true // A stale preview cannot override the actual operation. + f.got.Mutated = mutation + engine := Engine{store: groupReportingStore{state: f.state}} + result, err := engine.finishGroup(f.prepared, Result{}, true, f.got, nil) + want := OutcomeUnchanged + if mutation { + want = OutcomeCompleted + } + if err != nil || result.Outcome != want || result.Mutated != mutation || result.NoChange == mutation { + t.Fatalf("project completion=%+v error=%v", result, err) + } + actions := append(append([]string{}, f.got.Targets[0].Activation.UserActions...), f.got.Targets[1].Activation.UserActions...) + if !reflect.DeepEqual(actions, result.ManualActions) { + t.Fatalf("fresh actions changed: %q", result.ManualActions) + } + for _, target := range result.Targets { + if target.Activation != "prepared" || target.Verification != "installation_verified" || target.Authentication != "not_checked" { + t.Fatalf("presentation promoted lifecycle %+v", target) + } + } + }) + } +} + +func TestFinishProjectGroupRejectsInconsistentCompletion(t *testing.T) { + for _, tc := range []struct { + name string + change func(*projectGroupReportFixture) + }{ + {"group phase", func(f *projectGroupReportFixture) { f.got.Phase = usecase.GroupPhaseManagedCommitted }}, + {"group installation", func(f *projectGroupReportFixture) { f.got.InstallationID = "another" }}, + {"target installation", func(f *projectGroupReportFixture) { f.got.Targets[0].InstallationID = "another" }}, + {"target failure", func(f *projectGroupReportFixture) { + f.got.Targets[0].Failure = &usecase.GroupTargetFailure{Stage: "persist"} + }}, + {"target phase", func(f *projectGroupReportFixture) { + f.got.Targets[0].GroupPhase = usecase.GroupTargetExternalNotAttempted + }}, + {"missing target", func(f *projectGroupReportFixture) { f.got.Targets = f.got.Targets[:1] }}, + {"duplicate target", func(f *projectGroupReportFixture) { f.got.Targets[1] = f.got.Targets[0] }}, + {"wrong client", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.ClientID = domain.ClientCodex }}, + {"mixed scope", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.Scope = domain.ScopeUser }}, + {"project context", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.TargetContext.ProjectRoot += "-other" }}, + {"original user context", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.TargetContext.UserConfigRoot += "-other" }}, + {"physical artifact", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.PhysicalArtifactID = "another" }}, + {"active locator", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.ActivePath += "-other" }}, + {"native root", func(f *projectGroupReportFixture) { f.got.Targets[0].Plan.NativeRegistryRoot += "-other" }}, + {"namespace", func(f *projectGroupReportFixture) { + f.got.Targets[0].Plan = f.got.Targets[0].Plan.WithProfileAuthority(nil, "another") + }}, + {"no frozen delivery", func(f *projectGroupReportFixture) { f.prepared.deliveries = f.prepared.deliveries[1:] }}, + {"missing installation", func(f *projectGroupReportFixture) { f.state.Installations = nil }}, + {"duplicate installation", func(f *projectGroupReportFixture) { + f.state.Installations = append(f.state.Installations, f.state.Installations[0]) + }}, + {"missing binding", func(f *projectGroupReportFixture) { + delete(f.state.Installations[0].Clients, f.prepared.plan.Targets[0].BindingID) + }}, + {"another binding of same client", func(f *projectGroupReportFixture) { + key := f.prepared.plan.Targets[0].BindingID + binding := f.state.Installations[0].Clients[key] + binding.ClientBindingID = "other-binding" + f.state.Installations[0].Clients[binding.ClientBindingID] = binding + }}, + {"wrong binding id", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.ClientBindingID = "other" }) + }}, + {"wrong saved artifact", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.PhysicalArtifact = "other" }) + }}, + {"wrong saved profile", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.NativeProfileRoot += "-other" }) + }}, + {"wrong saved namespace", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.ProfileNamespace = "other" }) + }}, + {"pending attempt", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.NativeActivationAttempt = "unacknowledged" }) + }}, + {"pending intent", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.PendingNativeIntent = &domain.PendingNativeIntent{} }) + }}, + {"unmaterialized", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.Materialization = domain.MaterializationStaged }) + }}, + {"changed live lifecycle", func(f *projectGroupReportFixture) { + f.changeBinding(func(b *domain.ClientBinding) { b.Verification = domain.VerificationRuntime }) + }}, + {"prepared attestation", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.ActivationAttested = true }}, + {"missing fresh actions", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.UserActions = nil }}, + {"tracked empty effect", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.NativeEffect = "" }}, + {"legacy unknown effect", func(f *projectGroupReportFixture) { f.got.Targets[1].Activation.NativeEffect = "unknown" }}, + {"uncertain effect", func(f *projectGroupReportFixture) { + f.got.Targets[0].Activation.NativeEffect = domain.NativeEffectUncertain + }}, + {"manual activation", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Activation = domain.ActivationManual }}, + {"failed activation", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Activation = domain.ActivationFailed }}, + {"unknown activation", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Activation = "future" }}, + {"failed authentication", func(f *projectGroupReportFixture) { + f.got.Targets[0].Activation.Authentication = domain.AuthenticationFailed + }}, + {"unknown authentication", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Authentication = "future" }}, + {"blocked policy", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Policy = domain.PolicyBlocked }}, + {"unknown policy", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Policy = "future" }}, + {"unverified files", func(f *projectGroupReportFixture) { + f.got.Targets[0].Activation.Verification = domain.VerificationPackageValid + }}, + {"unknown verification", func(f *projectGroupReportFixture) { f.got.Targets[0].Activation.Verification = "future" }}, + } { + t.Run(tc.name, func(t *testing.T) { + f := newProjectGroupReportFixture(t) + tc.change(&f) + f.got.Mutated = true + engine := Engine{store: groupReportingStore{state: f.state}} + result, err := engine.finishGroup(f.prepared, Result{}, true, f.got, nil) + if err == nil || result.Outcome != OutcomeIncomplete || !result.Mutated || result.NoChange { + t.Fatalf("invalid completion %+v %v", result, err) + } + }) + } +} + +func TestFinishProjectGroupRejectsChangedDeliveryOrAuthority(t *testing.T) { + for _, saved := range []bool{false, true} { + f := newProjectGroupReportFixture(t) + var selected domain.SelectedDelivery + if err := json.Unmarshal([]byte(`{"mode":"unknown-project-mode"}`), &selected); err != nil { + t.Fatal(err) + } + if saved { + f.changeBinding(func(b *domain.ClientBinding) { b.SelectedDelivery = selected }) + } else { + f.got.Targets[0].Plan.SelectedDelivery = selected + } + if err := completedProjectGroup(f.prepared, f.got, f.state); err == nil { + t.Fatal("changed selected delivery accepted") + } + } + f := newProjectGroupReportFixture(t) + // A zero token is still structurally distinct from the absent authority. + f.changeBinding(func(b *domain.ClientBinding) { b.ProfileAuthority = &domain.ProfileAuthority{} }) + if err := completedProjectGroup(f.prepared, f.got, f.state); err == nil { + t.Fatal("changed profile authority accepted") + } +} + +func TestFinishProjectGroupStateFailureCannotHideObservedEffects(t *testing.T) { + loadErr := errors.New("fixture reload failure") + coreErr := errors.New("fixture core failure") + for _, mutation := range []bool{false, true} { + for _, failure := range []error{nil, coreErr} { + f := newProjectGroupReportFixture(t) + f.got.Mutated = mutation + engine := Engine{store: groupReportingStore{err: loadErr}} + result, err := engine.finishGroup(f.prepared, Result{}, true, f.got, failure) + expected := loadErr + if failure != nil { + expected = failure + } + if !errors.Is(err, expected) || result.Outcome != OutcomeIncomplete || result.Mutated != mutation || result.NoChange { + t.Fatalf("state failure %+v %v", result, err) + } + if len(result.ManualActions) != 4 { + t.Fatalf("fresh actions lost on inspection failure %+v", result) + } + } + } +} + +func TestFinishGroupFailurePrecedenceRetainsObservedFacts(t *testing.T) { + for _, tc := range []struct { + name string + cause error + reason string + outcome Outcome + }{ + {"ordinary", errors.New("activation failed"), "activation failed", OutcomeIncomplete}, + {"plan", ErrPlanChanged, "plan_changed", OutcomeConflict}, + {"refresh", errors.Join(ErrPlanChanged, usecase.ErrProjectionRefreshRequired), "update_required", OutcomeConflict}, + {"text", errors.Join(ErrPlanChanged, errors.New("run update separately")), "update_required", OutcomeConflict}, + {"pin", errors.Join(ErrPlanChanged, usecase.ErrProjectionPinChangeRequired), "pin_change_required", OutcomeConflict}, + {"all", errors.Join(ErrPlanChanged, usecase.ErrProjectionRefreshRequired, usecase.ErrProjectionPinChangeRequired), "pin_change_required", OutcomeConflict}, + } { + t.Run(tc.name, func(t *testing.T) { + f := newProjectGroupReportFixture(t) + f.got.Mutated = true + f.got.Targets[0].Failure = &usecase.GroupTargetFailure{Stage: "activation", Message: "observed failure"} + engine := Engine{store: groupReportingStore{state: f.state}} + initial := Result{Binding: BindingFacts{BindingID: "host-fact"}, ManualActions: []string{"host action"}} + got, err := engine.finishGroup(f.prepared, initial, true, f.got, tc.cause) + if !errors.Is(err, tc.cause) || err.Error() != wrapLifecycleError(tc.cause).Error() || got.Outcome != tc.outcome || got.Reason != tc.reason || !got.Mutated || got.NoChange { + t.Fatalf("failure classification %+v %v", got, err) + } + wantActions := []string{"host action"} + for _, target := range f.got.Targets { + wantActions = append(wantActions, target.Activation.UserActions...) + } + if got.Binding.BindingID != f.prepared.plan.Targets[0].BindingID || !reflect.DeepEqual(got.ManualActions, wantActions) || len(got.Targets) != 2 || got.Targets[0].Failure == nil || got.Targets[0].Failure.Message != "observed failure" { + t.Fatalf("observed facts lost: binding=%q actions=%v targets=%+v", got.Binding.BindingID, got.ManualActions, got.Targets) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/installer/group_project_lifecycle_test.go b/install/integrationctl/agentplugins/installer/group_project_lifecycle_test.go new file mode 100644 index 000000000..5022cc2d0 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/group_project_lifecycle_test.go @@ -0,0 +1,170 @@ +package installer + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cline" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type observedGroupCline struct { + *cline.Adapter + observations int + activations []domain.ActivationRequest +} + +func (a *observedGroupCline) InspectNativeRegistry(ctx context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, binding *domain.ClientBinding) (clients.RegistryFinding, error) { + a.observations++ + return a.Adapter.InspectNativeRegistry(ctx, env, client, plan, binding) +} + +func (a *observedGroupCline) Activate(ctx context.Context, env clients.Env, req domain.ActivationRequest) (domain.ActivationOutcome, error) { + outcome, err := a.Adapter.Activate(ctx, env, req) + a.activations = append(a.activations, req) + return outcome, err +} + +func TestProjectPreparedGroupPublicLifecycleAndHandoff(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + codexUser := filepath.Join(f.base, "user", ".codex") + deathWrite(t, filepath.Join(codexUser, "keep.txt"), "original Codex profile") + adapter := &observedGroupCline{Adapter: cline.New()} + registry, err := clients.NewRegistry(adapter, codex.New()) + if err != nil { + t.Fatal(err) + } + f.cfg.Registry = registry + f.req.Targets = []ClientTarget{{ClientID: "cline", ClientConfigRoot: f.user}, {ClientID: "codex", ClientConfigRoot: codexUser}} + f.reopen(t) + untouched := f.untouched(t) + installed := f.apply(t, OpInstall) + if installed.Outcome != OutcomeCompleted || !installed.Mutated || len(installed.Targets) != 2 || len(installed.ManualActions) == 0 { + t.Fatalf("install %+v", installed) + } + nativePaths := []string{f.skill, f.config, filepath.Join(f.project, ".agents", "skills", "docs", "SKILL.md"), filepath.Join(f.project, ".codex", "config.toml")} + native := func() map[string]string { + out := map[string]string{} + for _, path := range nativePaths { + out[path] = deathRead(t, path) + } + return out + } + beforeNative := native() + for _, operation := range []Operation{OpInstall, OpUpdate} { + beforeState, observations, activations := f.state(t), adapter.observations, len(adapter.activations) + f.reopen(t) + result := f.apply(t, operation) + if result.Outcome != OutcomeUnchanged || result.Mutated || !result.NoChange || len(result.ManualActions) == 0 { + t.Fatalf("repeat %s mutated=%v unchanged=%v stateEqual=%v nativeEqual=%v", operation, result.Mutated, result.NoChange, f.state(t) == beforeState, reflect.DeepEqual(native(), beforeNative)) + } + if f.state(t) != beforeState || !reflect.DeepEqual(native(), beforeNative) || adapter.observations <= observations || len(adapter.activations) <= activations || !adapter.activations[len(adapter.activations)-1].VerifyOnly { + t.Fatalf("repeat %s did not freshly observe identical files/state", operation) + } + f.assertUntouched(t, untouched) + } + // Group repair deliberately restages managed packages. Preserve that actual + // mutation instead of calling it unchanged because the native bytes match. + beforeRepair := f.state(t) + intact := f.apply(t, OpRepair) + if intact.Outcome != OutcomeCompleted || !intact.Mutated || intact.NoChange || f.state(t) == beforeRepair || !reflect.DeepEqual(native(), beforeNative) { + t.Fatalf("intact group repair %+v", intact) + } + // A successful host callback does not falsely promote Prepared or skip + // the subsequent real adapter observation. + callbacks := 0 + f.cfg.OnCommittedBinding = func(context.Context, BindingFacts) error { callbacks++; return nil } + f.reopen(t) + observations := adapter.observations + successful := f.apply(t, OpInstall) + if successful.Outcome != OutcomeUnchanged || callbacks < 2 || adapter.observations <= observations { + t.Fatalf("successful callback skipped fresh core %+v callbacks=%d", successful, callbacks) + } + for _, target := range successful.Targets { + if target.Activation != "prepared" || target.Verification != "installation_verified" { + t.Fatalf("promoted prepared client %+v", target) + } + } + sentinel := errors.New("host callback refused handoff") + f.cfg.OnCommittedBinding = func(context.Context, BindingFacts) error { return sentinel } + f.reopen(t) + before := f.state(t) + refused, err := f.execute(t, OpInstall) + if !errors.Is(err, sentinel) || refused.Outcome != OutcomeIncomplete || refused.Mutated || f.state(t) != before || !reflect.DeepEqual(native(), beforeNative) { + t.Fatalf("host refusal %+v %v", refused, err) + } + f.cfg.OnCommittedBinding = nil + f.reopen(t) + changed := deathSkill("docs", "new group revision") + deathWrite(t, filepath.Join(f.pkg, "skills", "docs", "SKILL.md"), changed) + updated := f.apply(t, OpUpdate) + if updated.Outcome != OutcomeCompleted || !updated.Mutated || updated.NoChange { + t.Fatalf("changed source %+v", updated) + } + for _, path := range []string{f.skill, nativePaths[2]} { + if deathRead(t, path) != changed { + t.Fatal("updated skill missing", path) + } + } + beforeNative = native() + if err := os.Remove(f.skill); err != nil { + t.Fatal(err) + } + // A partially altered directory is not proof that the whole owned native + // object is absent. Preserve that refusal before testing typed absence. + beforePartial := f.state(t) + partial, partialErr := f.execute(t, OpRepair) + if partialErr == nil || partial.Mutated || f.state(t) != beforePartial { + t.Fatalf("partial object accepted %+v %v", partial, partialErr) + } + if err := os.RemoveAll(filepath.Dir(f.skill)); err != nil { + t.Fatal(err) + } + repaired := f.apply(t, OpRepair) + if repaired.Outcome != OutcomeCompleted || !repaired.Mutated || !reflect.DeepEqual(native(), beforeNative) { + t.Fatalf("native repair %+v", repaired) + } + f.reopen(t) + removed := f.apply(t, OpRemove) + if removed.Outcome != OutcomeCompleted || !removed.Mutated { + t.Fatalf("remove %+v", removed) + } + for _, path := range []string{f.skill, nativePaths[2]} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatal("owned skill survived remove", path, err) + } + } + f.assertUntouched(t, untouched) +} + +func TestProjectPreparedSingleReplayFreshlyVerifies(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + adapter := &observedGroupCline{Adapter: cline.New()} + registry, err := clients.NewRegistry(adapter) + if err != nil { + t.Fatal(err) + } + f.cfg.Registry = registry + f.reopen(t) + other := f.untouched(t) + f.apply(t, OpInstall) + before, skill, config := f.state(t), deathRead(t, f.skill), deathRead(t, f.config) + for _, operation := range []Operation{OpInstall, OpUpdate} { + f.reopen(t) + observations, activations := adapter.observations, len(adapter.activations) + result := f.apply(t, operation) + if result.Outcome != OutcomeCompleted && result.Outcome != OutcomeUnchanged || result.Mutated || f.state(t) != before || deathRead(t, f.skill) != skill || deathRead(t, f.config) != config { + t.Fatalf("single repeat %s: %+v", operation, result) + } + if adapter.observations <= observations || len(adapter.activations) <= activations || !adapter.activations[len(adapter.activations)-1].VerifyOnly { + t.Fatal("single replay skipped fresh read-only adapter verification") + } + f.assertUntouched(t, other) + } +} diff --git a/install/integrationctl/agentplugins/installer/group_project_remove_test.go b/install/integrationctl/agentplugins/installer/group_project_remove_test.go new file mode 100644 index 000000000..438b177f0 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/group_project_remove_test.go @@ -0,0 +1,138 @@ +package installer + +import ( + "errors" + "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/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func projectRemovalGroup(t *testing.T) *projectFilesFixture { + t.Helper() + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + codexUser := filepath.Join(f.base, "user", ".codex") + deathWrite(t, filepath.Join(codexUser, "keep.txt"), "original Codex profile") + f.req.Targets = []ClientTarget{{ClientID: "cline", ClientConfigRoot: f.user}, {ClientID: "codex", ClientConfigRoot: codexUser}} + f.reopen(t) + f.apply(t, OpInstall) + return f +} + +func TestProjectGroupRemovalAbsentTargetsAreReadOnly(t *testing.T) { + for _, firstAbsent := range []bool{false, true} { + t.Run(map[bool]string{false: "both present", true: "one absent"}[firstAbsent], func(t *testing.T) { + f := projectRemovalGroup(t) + other := f.untouched(t) + if firstAbsent { + targets := f.req.Targets + f.req.Targets = nil + f.apply(t, OpRemove) + f.req.Targets = targets + } + f.reopen(t) + removed := f.apply(t, OpRemove) + if removed.Outcome != OutcomeCompleted || !removed.Mutated || len(removed.Targets) != 2 { + t.Fatalf("remove %+v", removed) + } + if firstAbsent && removed.Targets[0].Materialization != string(domain.MaterializationAbsent) { + t.Fatal("absent target was not preserved", removed.Targets) + } + before := deathTree(t, f.cfg.StateRoot) + repeated := f.apply(t, OpRemove) + if repeated.Outcome != OutcomeUnchanged || repeated.Mutated || !repeated.NoChange || repeated.Reason != "already_absent" || !reflect.DeepEqual(before, deathTree(t, f.cfg.StateRoot)) { + t.Fatalf("absent group mutated state/journal %+v", repeated) + } + f.assertUntouched(t, other) + }) + } +} + +func TestProjectGroupRemovalRefusalsPreserveAuthority(t *testing.T) { + for _, kind := range []string{"managed drift", "foreign native", "other project", "mixed scope", "missing project", "unsupported target", "missing project hooks"} { + t.Run(kind, func(t *testing.T) { + f := projectRemovalGroup(t) + switch kind { + case "managed drift": + deathWrite(t, filepath.Join(f.plan.ActivePath, "skills", "docs", "SKILL.md"), "changed managed package") + case "foreign native": + deathWrite(t, f.skill, "foreign native bytes") + case "other project": + f.req.ProjectRoot = filepath.Join(f.base, "B") + case "mixed scope": + f.req.Scope = domain.ScopeUser + case "missing project": + f.req.ProjectRoot = "" + case "unsupported target": + f.req.Targets[1].ClientID = "not-a-client" + case "missing project hooks": + registry, err := clients.NewRegistry(&handoffCaptureAdapter{id: domain.ClientCline}, codex.New()) + if err != nil { + t.Fatal(err) + } + f.cfg.Registry = registry + f.reopen(t) + } + before, skill, other := f.state(t), deathRead(t, f.skill), f.untouched(t) + removed, err := f.execute(t, OpRemove) + if err == nil || removed.Mutated || f.state(t) != before || deathRead(t, f.skill) != skill { + t.Fatalf("refusal %+v %v", removed, err) + } + f.assertUntouched(t, other) + }) + } +} + +func TestProjectGroupRemovalRechecksChangedBinding(t *testing.T) { + f := projectRemovalGroup(t) + req := f.req + req.Operation, req.OperationID = OpRemove, "frozen-project-group-removal" + handle, err := f.engine.Prepare(t.Context(), req) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + state, err := f.engine.store.Load() + if err != nil { + t.Fatal(err) + } + for key, binding := range state.Installations[0].Clients { + if binding.ClientID == "cline" { + binding.NativeProfileRoot = filepath.Join(f.project, "foreign-profile") + state.Installations[0].Clients[key] = binding + } + } + if err := f.engine.store.Save(state); err != nil { + t.Fatal(err) + } + before, skill, other := f.state(t), deathRead(t, f.skill), f.untouched(t) + removed, err := f.engine.Apply(t.Context(), handle, Decision{Confirmed: true}) + if err == nil || removed.Mutated || f.state(t) != before || deathRead(t, f.skill) != skill { + t.Fatalf("changed binding removed %+v %v", removed, err) + } + f.assertUntouched(t, other) +} + +func TestProjectGroupRemovalKeepsHistoricalUserGuard(t *testing.T) { + f := projectRemovalGroup(t) + // Exercise the original non-project planning branch with a retained + // installation. A missing target must not fabricate removal authority. + engine := *f.engine + engine.cfg.ProjectRoot = "" + req := f.req + req.Scope, req.ProjectRoot, req.Operation = domain.ScopeUser, "", OpRemove + before, other := f.state(t), f.untouched(t) + _, err := engine.planRemoveGroup(t.Context(), &PreparedOperation{req: req}, domain.Installation{InstallationID: "retained"}) + if !errors.Is(err, ErrUnsupported) || !strings.Contains(err.Error(), "historical group removal requires Claude/Codex") || f.state(t) != before { + t.Fatalf("user guard changed %v", err) + } + f.assertUntouched(t, other) +} diff --git a/install/integrationctl/agentplugins/installer/group_remove.go b/install/integrationctl/agentplugins/installer/group_remove.go index 5cb5f10bc..6d1cd67d5 100644 --- a/install/integrationctl/agentplugins/installer/group_remove.go +++ b/install/integrationctl/agentplugins/installer/group_remove.go @@ -114,12 +114,14 @@ func (e *Engine) planRemoveGroup(ctx context.Context, handle *PreparedOperation, // An absent selected target has no authority to dispatch. Keep its zero // selection and report absence; a present historical zero stays historical. if (found && binding.SelectedDelivery.IsZero() || !found && !selectedCapable) && target.ClientID != "claude" && target.ClientID != "codex" { - return nil, fmt.Errorf("%w: historical group removal requires Claude/Codex", ErrUnsupported) + if !e.projectGroupRemovalSupported(req, target.ClientID) { + return nil, fmt.Errorf("%w: historical group removal requires Claude/Codex", ErrUnsupported) + } } if found { item.SelectedDelivery = binding.SelectedDelivery item.TreeDigest = recordedBindingDigest(binding, installation.Source.TreeDigest) - if err := e.removalPreflight(ctx, client, binding, receipt); err != nil { + if err := e.removalPreflight(ctx, client, installation.DeclaredName, binding, receipt); err != nil { return nil, err } item.TargetPath = binding.TargetLocator @@ -137,6 +139,13 @@ func (e *Engine) planRemoveGroup(ctx context.Context, handle *PreparedOperation, return handle, nil } +// Project clients retain the same per-binding preflight and frozen removal +// authority below. This only narrows the historical SDK admission restriction. +func (e *Engine) projectGroupRemovalSupported(req Request, clientID string) bool { + return req.Scope == domain.ScopeProject && req.ProjectRoot != "" && req.ProjectRoot == e.cfg.ProjectRoot && + ClientScopeSupported(e.cfg.Registry, clientID, domain.ScopeProject) +} + func setRemoveGroupPrimary(handle *PreparedOperation, client domain.DetectedClient, item PlanTarget, binding domain.ClientBinding, receipt domain.DataReceipt) { handle.plan.Client = liveClientResult(binding, handle.req.RequiredComponents, item.TreeDigest) handle.plan.ProfileAuthority = domain.CloneProfileAuthority(binding.ProfileAuthority) @@ -168,7 +177,7 @@ func removeGroupInputs(prepared *PreparedOperation) ([]usecase.RemoveInput, []Cl } client := prepared.clients[i] live = append(live, usecase.RemoveInput{ - Selector: prepared.plan.InstallationID, Client: client, Scope: domain.ScopeUser, SelectedDelivery: target.SelectedDelivery, + Selector: prepared.plan.InstallationID, Client: client, Scope: prepared.req.Scope, SelectedDelivery: target.SelectedDelivery, Confirmed: true, OperationID: prepared.req.OperationID, BackendExecutable: client.ExecutablePath, ExternalUninstalled: prepared.req.Targets[i].ExternalUninstalled || prepared.req.ExternalUninstalled, }) @@ -197,7 +206,7 @@ func (e *Engine) preflightRemoveGroup(ctx context.Context, prepared *PreparedOpe err := fmt.Errorf("%w: client %s is not installed", ErrInvalidRequest, client.ClientID) return err } - if err := e.removalPreflight(ctx, client, binding, receipt); err != nil { + if err := e.removalPreflight(ctx, client, installation.DeclaredName, binding, receipt); err != nil { return err } } diff --git a/install/integrationctl/agentplugins/installer/identity.go b/install/integrationctl/agentplugins/installer/identity.go index af2a41948..595679b75 100644 --- a/install/integrationctl/agentplugins/installer/identity.go +++ b/install/integrationctl/agentplugins/installer/identity.go @@ -35,7 +35,7 @@ func (e *Engine) ReserveIdentity(req IdentityRequest) (IdentityReservation, erro if req.ClientID != "" && !e.SupportsClient(req.ClientID) { return IdentityReservation{}, fmt.Errorf("%w: client %q is not registered for this installer", ErrUnsupported, req.ClientID) } - out := IdentityReservation{Scope: string(domain.ScopeUser), InstallationID: req.InstallationID} + out := IdentityReservation{Scope: string(e.scope()), InstallationID: req.InstallationID} state, err := e.store.Load() if err != nil { return IdentityReservation{}, err @@ -72,7 +72,7 @@ func (e *Engine) reserveNewBinding(out *IdentityReservation, req IdentityRequest } physicalID := domain.ComputePhysicalArtifactID(req.DeclaredName, out.InstallationID) client := domain.DetectedClient{ClientID: domain.ClientID(req.ClientID), Status: domain.DetectionDetected, ConfigRoot: req.ClientConfigRoot} - target, err := e.planner().ResolveTarget(context.Background(), client, domain.ScopeUser, physicalID) + target, err := e.planner().ResolveTarget(context.Background(), client, e.scope(), physicalID) if err != nil { return } diff --git a/install/integrationctl/agentplugins/installer/inspect.go b/install/integrationctl/agentplugins/installer/inspect.go index dc998f2bb..6c151a0e0 100644 --- a/install/integrationctl/agentplugins/installer/inspect.go +++ b/install/integrationctl/agentplugins/installer/inspect.go @@ -47,6 +47,9 @@ func (e *Engine) Inspect(ctx context.Context) (Inspection, error) { func (e *Engine) observe() (Inspection, error) { out := Inspection{StateRoot: e.cfg.StateRoot} + if err := e.CheckProjectNamespace(); err != nil { + return out, err + } state, err := e.store.Load() if err != nil { out.Recovery.Required = true @@ -87,19 +90,11 @@ func (e *Engine) observe() (Inspection, error) { }) } out.Recovery.Receipts = unfinishedReceipts(state, openIDs, bindingIndex) - pending, err := (opencode.NativeTransitions{State: transaction.Kernel{StateStore: e.store}}).Pending() - if err != nil { + if err := e.appendPendingTransitions(&out); err != nil { out.Recovery.Required = true out.Recovery.Reason = err.Error() return out, err } - for _, p := range pending { - if p.Root == "" { - out.Recovery.Receipts = append(out.Recovery.Receipts, PendingReceipt{OperationID: p.OperationID, InstallationID: p.InstallationID, BindingID: p.BindingID, TargetPath: p.TargetPath, Phase: p.Phase}) - } else { - out.Recovery.Journals = append(out.Recovery.Journals, PendingJournal{OperationID: p.OperationID, InstallationID: p.InstallationID, BindingID: p.BindingID, TargetPath: p.TargetPath, Phase: p.Phase, Digest: p.Digest}) - } - } sort.Slice(out.Recovery.Journals, func(i, j int) bool { return out.Recovery.Journals[i].OperationID < out.Recovery.Journals[j].OperationID }) @@ -513,3 +508,18 @@ func populateInspectedBinding(inspected *InspectedBinding, binding domain.Client } } } + +func (e *Engine) appendPendingTransitions(out *Inspection) error { + pending, err := (opencode.NativeTransitions{ProjectRoot: e.cfg.ProjectRoot, State: transaction.Kernel{StateStore: e.store}}).Pending() + if err != nil { + return err + } + for _, p := range pending { + if p.Root == "" { + out.Recovery.Receipts = append(out.Recovery.Receipts, PendingReceipt{OperationID: p.OperationID, InstallationID: p.InstallationID, BindingID: p.BindingID, TargetPath: p.TargetPath, Phase: p.Phase}) + } else { + out.Recovery.Journals = append(out.Recovery.Journals, PendingJournal{OperationID: p.OperationID, InstallationID: p.InstallationID, BindingID: p.BindingID, TargetPath: p.TargetPath, Phase: p.Phase, Digest: p.Digest}) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/installer/opencode_host_test.go b/install/integrationctl/agentplugins/installer/opencode_host_test.go index adc4317e7..d9d316b2a 100644 --- a/install/integrationctl/agentplugins/installer/opencode_host_test.go +++ b/install/integrationctl/agentplugins/installer/opencode_host_test.go @@ -34,6 +34,20 @@ func openCodeTestRoot(t *testing.T) string { t.Setenv("TMPDIR", root) return root } + +// Replace disposable executable inodes instead of truncating bytes that a +// completed probe may still have mapped (ETXTBSY on Linux, signing cache on macOS). +func replaceOpenCodeFixture(t *testing.T, executable string, body []byte) { + t.Helper() + next := executable + ".next" + if err := os.WriteFile(next, body, 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(next, executable); err != nil { + t.Fatal(err) + } +} + func buildOpenCodeTarget(t *testing.T, root, version, mode string) string { t.Helper() if err := os.MkdirAll(root, 0700); err != nil { @@ -337,9 +351,7 @@ func TestOpenCodeApplyRevalidatesBeforeEffects(t *testing.T) { if err != nil { t.Fatal(err) } - if err := os.WriteFile(executable, body, 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, body) case "symlink": if err := os.Remove(selected); err != nil { t.Fatal(err) @@ -365,9 +377,7 @@ func TestOpenCodeApplyRevalidatesBeforeEffects(t *testing.T) { case "failed": fail = true case "wrapper", "nested_wrapper": - if err := os.WriteFile(executable, []byte("#!/bin/sh\necho executed > \""+filepath.Join(root, "wrapper-ran")+"\"\nexec \""+replacement+"\" --version\n"), 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, []byte("#!/bin/sh\necho executed > \""+filepath.Join(root, "wrapper-ran")+"\"\nexec \""+replacement+"\" --version\n")) } result, err := engine.Apply(testCtx(t), handle, Decision{Confirmed: true}) if !errors.Is(err, ErrPlanChanged) || result.Outcome != OutcomeConflict || result.Reason != "plan_changed" || effects != 0 { @@ -442,9 +452,7 @@ func TestOpenCodePendingHandoffRevalidates(t *testing.T) { if err != nil { t.Fatal(err) } - if err := os.WriteFile(executable, body, 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, body) result, err = engine.Apply(testCtx(t), retry, Decision{Confirmed: true}) if !errors.Is(err, ErrPlanChanged) || result.Reason != "plan_changed" || handoffs != 1 { t.Fatalf("pending boundary: %+v handoffs=%d %v", result, handoffs, err) @@ -511,17 +519,13 @@ func TestOpenCodeExistingMutationsRequireAndRevalidateTarget(t *testing.T) { t.Fatal(err) } defer func() { _ = handle.Close() }() - if err := os.WriteFile(executable, changed, 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, changed) effects = 0 result, err := engine.Apply(testCtx(t), handle, Decision{Confirmed: true}) if !errors.Is(err, ErrPlanChanged) || result.Reason != "plan_changed" || effects != 0 { t.Fatalf("changed target: %+v effects=%d %v", result, effects, err) } - if err := os.WriteFile(executable, original, 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, original) }) } } @@ -604,9 +608,7 @@ func TestOpenCodeMetadataUpdateFencesPriorNativeEffects(t *testing.T) { if err != nil { t.Fatal(err) } - if err := os.WriteFile(executable, body, 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, body) case "root": other := filepath.Join(root, "config-b") if err := os.Mkdir(other, 0700); err != nil { @@ -667,9 +669,7 @@ func TestOpenCodeMetadataUpdateFencesPriorNativeEffects(t *testing.T) { if err != nil { t.Fatal(err) } - if err := os.WriteFile(executable, changed, 0700); err != nil { - t.Fatal(err) - } + replaceOpenCodeFixture(t, executable, changed) effects = 0 result, err = engine.Apply(testCtx(t), pending, Decision{Confirmed: true}) if !errors.Is(err, ErrPlanChanged) || result.Reason != "plan_changed" || effects != 0 { diff --git a/install/integrationctl/agentplugins/installer/opencode_recovery.go b/install/integrationctl/agentplugins/installer/opencode_recovery.go index 59e283428..7f7f2e6f9 100644 --- a/install/integrationctl/agentplugins/installer/opencode_recovery.go +++ b/install/integrationctl/agentplugins/installer/opencode_recovery.go @@ -16,7 +16,7 @@ import ( // OpenCode recorder owns native classification, state publication and cleanup; // this guard permits only its current journal and binding to advance. func (e *Engine) recoverObservedOpenCode(ctx context.Context, svc usecase.Service, observed RecoveryObservation) error { - t := opencode.NativeTransitions{PackageVerifier: svc.Stager, State: transaction.Kernel{StateStore: e.store}, Kernel: nativeconfig.New()} + t := opencode.NativeTransitions{ProjectRoot: e.cfg.ProjectRoot, PackageVerifier: svc.Stager, State: transaction.Kernel{StateStore: e.store}, Kernel: nativeconfig.New()} pending, err := t.Pending() if err != nil { return err @@ -61,7 +61,7 @@ func (s *openCodeRecoveryState) confirm(completed bool) error { if err != nil { return fmt.Errorf("%w: %w", ErrPlanChanged, err) } - pending, err := (opencode.NativeTransitions{State: transaction.Kernel{StateStore: s.engine.store}}).Pending() + pending, err := (opencode.NativeTransitions{ProjectRoot: s.engine.cfg.ProjectRoot, State: transaction.Kernel{StateStore: s.engine.store}}).Pending() if err != nil { return fmt.Errorf("%w: %w", ErrPlanChanged, err) } diff --git a/install/integrationctl/agentplugins/installer/package.go b/install/integrationctl/agentplugins/installer/package.go index 5000095b9..2b2994aef 100644 --- a/install/integrationctl/agentplugins/installer/package.go +++ b/install/integrationctl/agentplugins/installer/package.go @@ -176,10 +176,10 @@ func (e *Engine) detectedClient(req Request) (domain.DetectedClient, error) { if !e.SupportsClient(req.ClientID) { return domain.DetectedClient{}, fmt.Errorf("%w: client %q is not registered for this installer", ErrUnsupported, req.ClientID) } - if req.ClientConfigRoot == "" || !validRoot(req.ClientConfigRoot) { + if (req.ClientConfigRoot == "" && e.scope() != domain.ScopeProject) || (req.ClientConfigRoot != "" && !validRoot(req.ClientConfigRoot)) { return domain.DetectedClient{}, fmt.Errorf("%w: ClientConfigRoot must be an explicit absolute clean path", ErrInvalidRequest) } - if (req.ClientExecutable != "" && !validRoot(req.ClientExecutable)) || (req.Operation == OpInstall && requiresExecutable(e.cfg.Registry, id) && req.ClientExecutable == "") { + if (req.ClientExecutable != "" && !validRoot(req.ClientExecutable)) || (req.Operation == OpInstall && requiresExecutable(e.cfg.Registry, id, e.scope()) && req.ClientExecutable == "") { return domain.DetectedClient{}, fmt.Errorf("%w: ClientExecutable must be an explicit absolute clean path", ErrInvalidRequest) } root := req.ClientConfigRoot @@ -190,7 +190,7 @@ func (e *Engine) detectedClient(req Request) (domain.DetectedClient, error) { return domain.DetectedClient{}, fmt.Errorf("%w: invalid client profile: %w", ErrInvalidRequest, err) } } - return domain.DetectedClient{ClientID: id, Status: domain.DetectionDetected, ConfigRoot: root, ExecutablePath: req.ClientExecutable}, nil + return domain.DetectedClient{ClientID: id, Status: domain.DetectionDetected, ConfigRoot: root, ExecutablePath: req.ClientExecutable, Version: req.ClientVersion}, nil } func missingRequired(envelope domain.PackageEnvelope, required []string) []string { @@ -421,8 +421,12 @@ func validateAcquiredExecutables(root string, files []string) error { // Native configuration adapters need only an explicit profile. CLI registries // and host-profile probes also require the caller's executable authority. -func requiresExecutable(registry *clients.Registry, id domain.ClientID) bool { - if domain.ClientTraitsFor(id).LifecycleKind == domain.LifecycleCLIRegistry { +func requiresExecutable(registry *clients.Registry, id domain.ClientID, scopes ...domain.InstallScope) bool { + scope := domain.ScopeUser + if len(scopes) > 0 && scopes[0] != "" { + scope = scopes[0] + } + if (domain.SelectedDelivery{}).EffectiveTraitsForScope(id, scope).LifecycleKind == domain.LifecycleCLIRegistry { return true } consumer, ok := clients.As[clients.OpenCodeHostProfileConsumer](registry, id) diff --git a/install/integrationctl/agentplugins/installer/prepare.go b/install/integrationctl/agentplugins/installer/prepare.go index 048248d2c..84c3190ac 100644 --- a/install/integrationctl/agentplugins/installer/prepare.go +++ b/install/integrationctl/agentplugins/installer/prepare.go @@ -40,6 +40,8 @@ type PreparedOperation struct { func (p *PreparedOperation) Plan() Plan { out := p.plan + out.Scope = p.req.Scope + out.TargetContext = domain.TargetContext{Scope: p.req.Scope, ProjectRoot: p.req.ProjectRoot, UserConfigRoot: p.client.ConfigRoot} out.ProfileAuthority = domain.CloneProfileAuthority(p.plan.ProfileAuthority) out.Client.ProfileAuthority = domain.CloneProfileAuthority(p.plan.Client.ProfileAuthority) if p.plan.OpenCodeProfile != nil { @@ -95,14 +97,14 @@ func (p *PreparedOperation) closeLocked() error { // Prepare captures a sealed snapshot for install or inspects owned state for remove. // It does not mutate installed client config or the Notifications runtime ledger. func (e *Engine) Prepare(ctx context.Context, req Request) (*PreparedOperation, error) { - if (req.ActivationComplete || req.AuthComplete) && (req.Operation == OpRemove || len(req.Targets) > 0) { - return nil, fmt.Errorf("%w: completion attestation requires a single mutating target", ErrInvalidRequest) + if err := validatePrepareInput(ctx, req); err != nil { + return nil, err } - if ctx == nil { - return nil, fmt.Errorf("%w: context is required", ErrInvalidRequest) + if req.Scope == "" { + req.Scope = domain.ScopeUser } - if req.ExecutableFiles != nil && len(req.Targets) > 0 { - return nil, fmt.Errorf("%w: explicit snapshot executable inventory requires a single target", ErrInvalidRequest) + if err := e.validatePrepareContext(req); err != nil { + return nil, err } copied := clonePrepareRequest(req) if len(copied.Targets) == 1 { @@ -247,7 +249,7 @@ func (e *Engine) prepareMutatingPackage(ctx context.Context, req Request, op Ope hints = req.AcquiredSnapshot.Compatibility } preview, err := dry(svc, usecase.AddInput{ - Envelope: handle.envelope, Client: handle.client, Scope: domain.ScopeUser, DryRun: true, Confirmed: false, + Envelope: handle.envelope, Client: handle.client, Scope: e.scope(), DryRun: true, Confirmed: false, PersistAuthoritativeObservations: e.persistObservations, InstallationID: req.InstallationID, OperationID: req.OperationID, BackendExecutable: req.ClientExecutable, OriginMode: snapshot.OriginMode, DirectoryResolution: cloneAcquiredDirectory(snapshot.Directory), Hints: hints, @@ -294,7 +296,7 @@ func (e *Engine) prepareRemove(ctx context.Context, req Request) (*PreparedOpera handle := &PreparedOperation{engine: e, req: req, client: client, detected: detected, recorded: state} return e.prepareAbsentRemoval(handle, installation.InstallationID), nil } - if err := e.removalPreflight(ctx, client, binding, receipt); err != nil { + if err := e.removalPreflight(ctx, client, installation.DeclaredName, binding, receipt); err != nil { return nil, err } handle := &PreparedOperation{engine: e, req: req, client: client, detected: detected, recorded: state} @@ -336,7 +338,7 @@ func (e *Engine) prepareAbsentRemoval(handle *PreparedOperation, installationID // managed artifact digest, and owned PLUGIN_DATA. It does not lock, recover, // EnsureData, invoke a helper, or deactivate the client. Apply repeats it // before UAP Remove. -func (e *Engine) removalPreflight(ctx context.Context, client domain.DetectedClient, binding domain.ClientBinding, receipt domain.DataReceipt) error { +func (e *Engine) removalPreflight(ctx context.Context, client domain.DetectedClient, declaredName string, binding domain.ClientBinding, receipt domain.DataReceipt) error { if err := e.validateRemovalBinding(client, binding); err != nil { return err } @@ -347,7 +349,7 @@ func (e *Engine) removalPreflight(ctx context.Context, client domain.DetectedCli if digest == "" { return fmt.Errorf("%w: managed package digest is missing; refusing removal", ErrInvalidRequest) } - target, err := e.planner().ResolveTarget(ctx, client, domain.ScopeUser, binding.PhysicalArtifact) + target, err := e.planner().ResolveTarget(ctx, client, e.scope(), binding.PhysicalArtifact) if err != nil { return fmt.Errorf("%w: resolve managed removal target: %w", ErrInvalidRequest, err) } @@ -357,6 +359,9 @@ func (e *Engine) removalPreflight(ctx context.Context, client domain.DetectedCli if err := (providers.Stager{}).Verify(ctx, binding.TargetLocator, digest); err != nil { return fmt.Errorf("%w: managed package was changed or is missing; refusing silent removal: %w", ErrInvalidRequest, err) } + if err := e.confirmProjectRemovalFiles(ctx, client, declaredName, binding, target, digest); err != nil { + return err + } if receipt.DataReceiptID == "" { return nil } @@ -488,3 +493,17 @@ func clonePrepareRequest(req Request) Request { } return copied } + +func validatePrepareInput(ctx context.Context, req Request) error { + if (req.ActivationComplete || req.AuthComplete) && (req.Operation == OpRemove || len(req.Targets) > 0) { + return fmt.Errorf("%w: completion attestation requires a single mutating target", ErrInvalidRequest) + } + if ctx == nil { + return fmt.Errorf("%w: context is required", ErrInvalidRequest) + } + if req.ExecutableFiles != nil && len(req.Targets) > 0 { + return fmt.Errorf("%w: explicit snapshot executable inventory requires a single target", ErrInvalidRequest) + } + + return nil +} diff --git a/install/integrationctl/agentplugins/installer/project_claude_fixture_test.go b/install/integrationctl/agentplugins/installer/project_claude_fixture_test.go new file mode 100644 index 000000000..fbfab5a5d --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_claude_fixture_test.go @@ -0,0 +1,196 @@ +package installer_test + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/claude" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type claudeSDKFixture struct { + project, state, user, packageRoot, executable string + dataRoot string + failAfterCommit bool + engine *installer.Engine + runner *claudeSDKRunner + request installer.Request +} + +func claudeSDKWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func newClaudeSDKFixture(t *testing.T, base, project, user, pkg string) *claudeSDKFixture { + t.Helper() + f := &claudeSDKFixture{project: project, user: user, packageRoot: pkg, state: filepath.Join(base, "user-uap"), executable: filepath.Join(base, "inert-claude")} + scope := domain.ScopeUser + if project != "" { + f.state = filepath.Join(project, ".uap") + scope = domain.ScopeProject + } + claudeSDKWrite(t, f.executable, "inert test artifact; never executed\n") + if err := os.Chmod(f.executable, 0700); err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(claude.New()) + if err != nil { + t.Fatal(err) + } + f.runner = &claudeSDKRunner{t: t, user: user, project: project, executable: f.executable} + f.engine, err = installer.New(installer.Config{ProjectRoot: project, StateRoot: f.state, Registry: registry, Runner: f.runner, HelperExecutable: f.executable, EnableNativeObserver: true, TrustedLocalPackages: true, + OnCommittedBinding: func(_ context.Context, facts installer.BindingFacts) error { + if f.failAfterCommit { + body, err := os.ReadFile(filepath.Join(facts.TargetPath, claude.ClaudeRuntimeDirectory, "skills", "docs", "SKILL.md")) + if err == nil && strings.Contains(string(body), "Moving source bytes.") { + f.runner.fail = true + } + } + return nil + }, + }) + if err != nil { + t.Fatal(err) + } + f.request = installer.Request{Scope: scope, ProjectRoot: project, Operation: installer.OpInstall, PackageRoot: pkg, ClientID: "claude", ClientConfigRoot: user, ClientExecutable: f.executable, InstallationID: "00000000-0000-4000-8000-000000000263", OperationID: "claude-project-sdk-install", RequiredComponents: []string{"skills", "mcp"}} + return f +} + +func (f *claudeSDKFixture) apply(t *testing.T, operation installer.Operation, label string) installer.Result { + t.Helper() + f.request.Operation, f.request.OperationID = operation, label + handle, err := f.engine.Prepare(t.Context(), f.request) + if err != nil { + t.Fatalf("prepare SDK %s (%s): %v", operation, label, err) + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + result, err := f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) + if err != nil || result.Outcome != installer.OutcomeCompleted && result.Outcome != installer.OutcomeUnchanged { + t.Fatalf("SDK %s=%+v,%v", operation, result, err) + } + if result.Binding.DataRoot != "" { + if f.dataRoot != "" && f.dataRoot != result.Binding.DataRoot { + t.Fatal("SDK lifecycle changed persistent data authority") + } + f.dataRoot = result.Binding.DataRoot + } + return result +} + +func (f *claudeSDKFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := (statev2.Store{Path: filepath.Join(f.state, "state-v2.json"), ProjectRoot: f.project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("SDK state=%+v,%v", state, err) + } + for _, binding := range state.Installations[0].Clients { + return binding + } + t.Fatal("SDK binding absent") + return domain.ClientBinding{} +} + +// This inert runner discovers fixture manifests from the production-selected +// directory. It is a command contract boundary, not genuine Claude evidence. +type claudeSDKRunner struct { + t *testing.T + user, project, executable string + fail bool + trusted bool + calls int +} + +func (r *claudeSDKRunner) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.t.Helper() + if !reflect.DeepEqual(command.Argv, []string{r.executable, "plugin", "list", "--json"}) { + r.t.Fatalf("unexpected SDK native command=%+v", command) + } + root, scope := r.user, "user" + if r.project != "" { + root, scope = filepath.Join(r.project, ".claude"), "project" + if command.Dir != r.project { + r.t.Fatalf("SDK project cwd=%q", command.Dir) + } + } + want := "CLAUDE_CONFIG_DIR=" + r.user + found := false + for _, entry := range command.Env { + if entry == want { + found = true + } + } + if !found { + r.t.Fatalf("SDK replaced original user config=%q", command.Env) + } + if r.fail { + return legacyports.CommandResult{}, fmt.Errorf("inert unavailable native listing") + } + r.calls++ + listing := claudeSDKListing(r.t, r.user, "user") + if scope == "project" { + projectRows := claudeSDKListing(r.t, root, "project") + if len(projectRows) > 0 && !r.trusted { + listing = append(listing, map[string]any{"id": "(suppressed)@skills-dir", "version": "unknown", "scope": "project", "enabled": false, "installPath": "", "notes": []string{"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies."}}) + } else { + // Fixture model of the documented same-name user precedence. This + // is deliberately not represented as genuine trusted native evidence. + userIDs := map[any]bool{} + for _, row := range listing { + userIDs[row["id"]] = true + } + for _, row := range projectRows { + if !userIDs[row["id"]] { + listing = append(listing, row) + } + } + } + } + + body, err := json.Marshal(listing) + return legacyports.CommandResult{Stdout: body}, err +} + +func claudeSDKListing(t *testing.T, root, scope string) []map[string]any { + t.Helper() + entries, err := os.ReadDir(filepath.Join(root, "skills")) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + listing := []map[string]any{} + for _, entry := range entries { + path := filepath.Join(root, "skills", entry.Name()) + body, err := os.ReadFile(filepath.Join(path, ".claude-plugin", "plugin.json")) + if os.IsNotExist(err) { + continue + } + if err != nil { + t.Fatal(err) + } + var manifest map[string]any + if err := json.Unmarshal(body, &manifest); err != nil { + t.Fatal(err) + } + listing = append(listing, map[string]any{"id": fmt.Sprint(manifest["name"]) + "@skills-dir", "version": manifest["version"], "scope": scope, "enabled": true, "installPath": path}) + } + return listing +} diff --git a/install/integrationctl/agentplugins/installer/project_claude_lifecycle_test.go b/install/integrationctl/agentplugins/installer/project_claude_lifecycle_test.go new file mode 100644 index 000000000..13159834d --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_claude_lifecycle_test.go @@ -0,0 +1,187 @@ +package installer_test + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/claude" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +func claudeSDKBytes(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} + +func TestProjectClaudePublicInstallerLifecycleSeparatesUserAndProjects(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + user, pkg := filepath.Join(base, "user", ".claude"), filepath.Join(base, "package") + for _, root := range []string{user, filepath.Join(base, "A", ".claude"), filepath.Join(base, "B", ".claude")} { + claudeSDKWrite(t, filepath.Join(root, "settings.json"), `{"foreign":true}`) + claudeSDKWrite(t, filepath.Join(root, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + claudeSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-claude","version":"1.0.0"}`) + const original = "---\nname: docs\ndescription: Inert SDK fixture\n---\nOriginal pinned bytes.\n" + claudeSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), original) + claudeSDKWrite(t, filepath.Join(pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js"],"env":{"DATA":"${PLUGIN_DATA}"}}}}`) + claudeSDKWrite(t, filepath.Join(pkg, "server.js"), "// Inert SDK fixture; never executed.\n") + fixtures := make([]*claudeSDKFixture, 0, 3) + for _, project := range []string{"", filepath.Join(base, "A"), filepath.Join(base, "B")} { + f := newClaudeSDKFixture(t, base, project, user, pkg) + f.apply(t, installer.OpInstall, "install") + if f.dataRoot == "" { + t.Fatal("SDK omitted persistent data locator") + } + claudeSDKWrite(t, filepath.Join(f.dataRoot, "keep.txt"), "retained scoped data\n") + binding := f.binding(t) + wantActivation := domain.ActivationActive + if project != "" { + wantActivation = domain.ActivationPrepared + } + if binding.Activation != wantActivation || binding.Scope != string(f.request.Scope) { + t.Fatalf("SDK scoped binding=%+v", binding) + } + fixtures = append(fixtures, f) + } + claudeSDKUpdateRepairRemove(t, fixtures, original) +} + +func claudeSDKUpdateRepairRemove(t *testing.T, fixtures []*claudeSDKFixture, original string) { + t.Helper() + f := fixtures[1] + userBinding, bBinding := fixtures[0].binding(t), fixtures[2].binding(t) + beforeUser, beforeB := claudeSDKBytes(t, filepath.Join(fixtures[0].state, "state-v2.json")), claudeSDKBytes(t, filepath.Join(fixtures[2].state, "state-v2.json")) + binding := f.binding(t) + path := filepath.Join(binding.TargetLocator, claude.ClaudeRuntimeDirectory, "skills", "docs", "SKILL.md") + if result := f.apply(t, installer.OpUpdate, "unchanged"); result.Outcome != installer.OutcomeUnchanged { + t.Fatalf("unchanged SDK update=%+v", result) + } + claudeSDKWrite(t, path, "external drift\n") + claudeSDKWrite(t, filepath.Join(f.packageRoot, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Advanced SDK fixture\n---\nMoving source bytes.\n") + claudeSDKRepairPinned(t, f, original) + if claudeSDKBytes(t, path) != original { + t.Fatal("repair followed moving source") + } + claudeSDKInterruptedUpdateAndRestart(t, f) + if claudeSDKBytes(t, path) == original { + t.Fatal("update did not advance selected project") + } + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required { + t.Fatalf("SDK status=%+v,%v", view, err) + } + f.apply(t, installer.OpRemove, "remove-selected-project") + if _, err := os.Stat(binding.TargetLocator); !os.IsNotExist(err) { + t.Fatalf("SDK artifact survived remove: %v", err) + } + if !reflect.DeepEqual(userBinding, fixtures[0].binding(t)) || !reflect.DeepEqual(bBinding, fixtures[2].binding(t)) || beforeUser != claudeSDKBytes(t, filepath.Join(fixtures[0].state, "state-v2.json")) || beforeB != claudeSDKBytes(t, filepath.Join(fixtures[2].state, "state-v2.json")) { + t.Fatal("project SDK lifecycle changed user/project B ownership") + } + for _, other := range []domain.ClientBinding{userBinding, bBinding} { + if claudeSDKBytes(t, filepath.Join(other.TargetLocator, claude.ClaudeRuntimeDirectory, "skills", "docs", "SKILL.md")) != original { + t.Fatal("project SDK lifecycle changed another native package") + } + } + if claudeSDKBytes(t, filepath.Join(f.project, ".claude", "settings.json")) != `{"foreign":true}` { + t.Fatal("SDK lifecycle modified foreign settings") + } + for _, selected := range fixtures { + if claudeSDKBytes(t, filepath.Join(selected.dataRoot, "keep.txt")) != "retained scoped data\n" { + t.Fatal("project SDK lifecycle consumed scoped data") + } + } +} + +func claudeSDKInterruptedUpdateAndRestart(t *testing.T, f *claudeSDKFixture) { + t.Helper() + f.failAfterCommit = true + f.request.Operation, f.request.OperationID = installer.OpUpdate, "update-listing-unavailable-after-commit" + handle, err := f.engine.Prepare(t.Context(), f.request) + if err != nil { + t.Fatal(err) + } + result, applyErr := f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) + if err := handle.Close(); err != nil { + t.Fatal(err) + } + if applyErr == nil || result.Outcome != installer.OutcomeIncomplete || !result.Mutated { + t.Fatalf("postcommit listing failure=%+v,%v", result, applyErr) + } + if binding := f.binding(t); binding.Activation == domain.ActivationActive || binding.TargetLocator != result.Binding.TargetPath { + t.Fatalf("failed native observation manufactured active or lost binding=%+v", binding) + } + fresh := newClaudeSDKFixture(t, filepath.Dir(f.executable), f.project, f.user, f.packageRoot) + fresh.dataRoot = f.dataRoot + *f = *fresh + f.apply(t, installer.OpRepair, "repair-after-process-restart") + if binding := f.binding(t); binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("fresh public SDK did not preserve blocked project discovery=%+v", binding) + } +} + +func claudeSDKRepairPinned(t *testing.T, f *claudeSDKFixture, original string) { + t.Helper() + pinned := t.TempDir() + for _, name := range []string{"plugin.json", "mcp.json", "server.js"} { + claudeSDKWrite(t, filepath.Join(pinned, name), claudeSDKBytes(t, filepath.Join(f.packageRoot, name))) + } + claudeSDKWrite(t, filepath.Join(pinned, "skills", "docs", "SKILL.md"), original) + f.request.PackageRoot, f.request.SourceRoot = pinned, f.packageRoot + f.apply(t, installer.OpRepair, "repair-installed-pin") + f.request.PackageRoot, f.request.SourceRoot = f.packageRoot, "" +} + +func TestProjectClaudePreparedInitialAddReplayStatusAndChangedBytes(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + project := filepath.Join(base, "A") + user := filepath.Join(base, "user", ".claude") + pkg := filepath.Join(base, "package") + claudeSDKWrite(t, filepath.Join(user, "settings.json"), `{"foreign":true}`) + claudeSDKWrite(t, filepath.Join(project, ".claude", "settings.json"), `{"foreign":true}`) + claudeSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"prepared-initial","version":"1.0.0"}`) + sourceSkill := filepath.Join(pkg, "skills", "docs", "SKILL.md") + claudeSDKWrite(t, sourceSkill, "---\nname: docs\ndescription: Inert project marker\n---\nVersion one.\n") + f := newClaudeSDKFixture(t, base, project, user, pkg) + f.request.RequiredComponents = []string{"skills"} + f.apply(t, installer.OpInstall, "initial-untrusted-no-receipt") + if binding := f.binding(t); binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("initial untrusted binding=%+v", binding) + } + calls := f.runner.calls + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required || f.runner.calls != calls { + t.Fatalf("host-free status=%+v,%v calls=%d/%d", view, err, f.runner.calls, calls) + } + replay := f.apply(t, installer.OpInstall, "repeat-blocked-add") + if replay.Outcome != installer.OutcomeUnchanged || replay.Mutated || f.runner.calls <= calls { + t.Fatalf("stable prepared replay=%+v calls=%d/%d", replay, f.runner.calls, calls) + } + binding := f.binding(t) + installed := filepath.Join(binding.TargetLocator, claude.ClaudeRuntimeDirectory, "skills", "docs", "SKILL.md") + before := claudeSDKBytes(t, installed) + claudeSDKWrite(t, sourceSkill, "---\nname: docs\ndescription: Updated project marker\n---\nVersion two.\n") + updated := f.apply(t, installer.OpUpdate, "changed-bytes-still-blocked") + if updated.Outcome == installer.OutcomeUnchanged || !updated.Mutated || claudeSDKBytes(t, installed) == before { + t.Fatalf("changed owned bytes called unchanged=%+v", updated) + } + if binding := f.binding(t); binding.Activation != domain.ActivationPrepared { + t.Fatalf("changed bytes promoted discovery=%+v", binding) + } + f.apply(t, installer.OpRemove, "remove-inactive-owned-project") + if _, err := os.Stat(binding.TargetLocator); !os.IsNotExist(err) { + t.Fatalf("inactive owned artifact survived=%v", err) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_cline_test.go b/install/integrationctl/agentplugins/installer/project_cline_test.go new file mode 100644 index 000000000..329b59356 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_cline_test.go @@ -0,0 +1,169 @@ +package installer_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cline" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type clineSDKNoNative struct{ calls int } + +func (r *clineSDKNoNative) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("unexpected native command: %v", command.Argv) +} + +type clineSDKFixture struct { + project, state string + engine *installer.Engine + request installer.Request + runner *clineSDKNoNative +} + +func newClineSDKFixture(t *testing.T, project, user, pkg string, runner *clineSDKNoNative) *clineSDKFixture { + t.Helper() + registry, err := clients.NewRegistry(cline.New()) + if err != nil { + t.Fatal(err) + } + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + f := &clineSDKFixture{project: project, state: filepath.Join(project, ".uap"), runner: runner} + f.engine, err = installer.New(installer.Config{ProjectRoot: project, StateRoot: f.state, Registry: registry, Runner: runner, EnableNativeObserver: true, TrustedLocalPackages: true, HelperExecutable: helper}) + if err != nil { + t.Fatal(err) + } + f.request = installer.Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: installer.OpInstall, PackageRoot: pkg, ClientID: "cline", ClientConfigRoot: user, InstallationID: "00000000-0000-4000-8000-000000000073", OperationID: "cline-project-install", RequiredComponents: []string{"skills", "mcp"}} + return f +} + +func (f *clineSDKFixture) execute(t *testing.T, request installer.Request) (installer.Result, error) { + t.Helper() + handle, err := f.engine.Prepare(t.Context(), request) + if err != nil { + return installer.Result{}, err + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + return f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) +} +func (f *clineSDKFixture) apply(t *testing.T, op installer.Operation) { + t.Helper() + req := f.request + req.Operation, req.OperationID = op, string(op)+"-Cline" + out, err := f.execute(t, req) + if err != nil || out.Outcome != installer.OutcomeCompleted && out.Outcome != installer.OutcomeUnchanged { + t.Fatalf("%s: %+v %v", op, out, err) + } +} +func (f *clineSDKFixture) stateBytes(t *testing.T) string { + t.Helper() + body, err := os.ReadFile(filepath.Join(f.state, "state-v2.json")) + if os.IsNotExist(err) { + return "" + } + if err != nil { + t.Fatal(err) + } + return string(body) +} +func (f *clineSDKFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := (statev2.Store{Path: filepath.Join(f.state, "state-v2.json"), ProjectRoot: f.project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("state: %+v %v", state, err) + } + for _, b := range state.Installations[0].Clients { + return b + } + t.Fatal("missing binding") + return domain.ClientBinding{} +} + +// The real public installer/core, state and file transactions run. The runner +// refuses every command: local Prepared verification must not need Cline. +func TestProjectClinePublicEarlyAttestationHasNoStateOrNativeEffect(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + project, user, pkg := filepath.Join(base, "A"), filepath.Join(base, "home", ".cline"), filepath.Join(base, "package") + if err := os.MkdirAll(project, 0700); err != nil { + t.Fatal(err) + } + claudeSDKWrite(t, filepath.Join(user, "keep"), "original user") + claudeSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-cline","version":"1.0.0"}`) + claudeSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Cline file fixture\n---\nPinned original.\n") + claudeSDKWrite(t, filepath.Join(pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"http://127.0.0.1:9/inert"}}}`) + runner := &clineSDKNoNative{} + f := newClineSDKFixture(t, project, user, pkg, runner) + reject := func(op installer.Operation) { + t.Helper() + before, calls := f.stateBytes(t), runner.calls + req := f.request + req.Operation, req.OperationID, req.ActivationComplete = op, "refuse-"+string(op), true + if out, err := f.execute(t, req); err == nil { + t.Fatalf("attestation accepted: %+v", out) + } + if before != f.stateBytes(t) || runner.calls != calls { + t.Fatal("early refusal changed persisted state or called native client") + } + } + reject(installer.OpInstall) + f.apply(t, installer.OpInstall) + binding := f.binding(t) + if binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled || binding.NativeProfileRoot != project { + t.Fatalf("fabricated native activation: %+v", binding) + } + skill := filepath.Join(project, ".cline", "skills", "docs", "SKILL.md") + original := claudeSDKBytes(t, skill) + config := claudeSDKBytes(t, filepath.Join(project, ".uap", "clients", "cline", "mcp.json")) + for _, op := range []installer.Operation{installer.OpInstall, installer.OpUpdate, installer.OpRepair} { + reject(op) + } + f = newClineSDKFixture(t, project, user, pkg, runner) + for _, op := range []installer.Operation{installer.OpInstall, installer.OpUpdate, installer.OpRepair} { + reject(op) + } + if original != claudeSDKBytes(t, skill) || config != claudeSDKBytes(t, filepath.Join(project, ".uap", "clients", "cline", "mcp.json")) { + t.Fatal("refused public request changed owned file bytes") + } + before := runner.calls + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required || runner.calls != before { + t.Fatalf("status is not host-free: %+v %v", view, err) + } + f.apply(t, installer.OpInstall) + if !reflect.DeepEqual(binding, f.binding(t)) { + t.Fatal("unchanged Prepared replay changed persisted binding") + } + if !strings.Contains(config, "127.0.0.1:9") { + t.Fatal("no actual prepared MCP file") + } + f.apply(t, installer.OpRemove) + if _, err := os.Stat(filepath.Dir(skill)); !os.IsNotExist(err) { + t.Fatal("owned skill survives removal", err) + } + if runner.calls != 0 { + t.Fatal("project lifecycle called native Cline") + } + if claudeSDKBytes(t, filepath.Join(user, "keep")) != "original user" { + t.Fatal("original profile changed") + } +} diff --git a/install/integrationctl/agentplugins/installer/project_codex_replay_test.go b/install/integrationctl/agentplugins/installer/project_codex_replay_test.go new file mode 100644 index 000000000..34c14e3e2 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_codex_replay_test.go @@ -0,0 +1,33 @@ +package installer + +import ( + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestCodexProjectPreparedReplayAndUpdateReportNoChange(t *testing.T) { + f := newProjectFilesFixture(t, "codex", "disabled-with-runner") + f.apply(t, OpInstall) + binding := f.binding(t) + before, foreign := f.state(t), f.untouched(t) + managed := deathTree(t, binding.TargetLocator) + skills := deathTree(t, filepath.Dir(f.skill)) + config := deathRead(t, f.config) + for _, operation := range []Operation{OpInstall, OpUpdate} { + out := f.apply(t, operation) + if !out.NoChange || out.Mutated || f.state(t) != before { + t.Fatalf("%s rewrote or failed to classify verified Prepared replay: %+v", operation, out) + } + current := f.binding(t) + if current.Activation != domain.ActivationPrepared || current.Verification != domain.VerificationInstalled || current.LifecycleConverged() { + t.Fatalf("%s promoted Prepared lifecycle: %+v", operation, current) + } + if !reflect.DeepEqual(managed, deathTree(t, binding.TargetLocator)) || !reflect.DeepEqual(skills, deathTree(t, filepath.Dir(f.skill))) || deathRead(t, f.config) != config { + t.Fatal("unchanged operation changed owned package/native bytes") + } + f.assertUntouched(t, foreign) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_context_test.go b/install/integrationctl/agentplugins/installer/project_context_test.go new file mode 100644 index 000000000..58cd4cbe4 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_context_test.go @@ -0,0 +1,43 @@ +package installer + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestPrepareRejectsCrossEnvironmentContextBeforePackageRead(t *testing.T) { + root := t.TempDir() + for _, test := range []struct { + name, engineRoot string + request Request + }{ + {"project-on-user-engine", "", Request{Scope: domain.ScopeProject, ProjectRoot: root}}, + {"user-on-project-engine", root, Request{Scope: domain.ScopeUser}}, + {"other-project", root, Request{Scope: domain.ScopeProject, ProjectRoot: filepath.Join(root, "other")}}, + {"unknown-scope", "", Request{Scope: "unknown"}}, + } { + t.Run(test.name, func(t *testing.T) { + stateRoot := filepath.Join(root, test.name, ".uap") + if test.engineRoot != "" { + stateRoot = filepath.Join(test.engineRoot, ".uap") + } + engine, err := New(testConfig(t, Config{StateRoot: stateRoot, ProjectRoot: test.engineRoot})) + if err != nil { + t.Fatal(err) + } + test.request.PackageRoot = filepath.Join(root, "missing-package") + test.request.ClientID = "codex" + if _, err := engine.Prepare(context.Background(), test.request); !errors.Is(err, ErrInvalidRequest) { + t.Fatalf("context was not rejected before package read: %v", err) + } + if _, err := os.Lstat(stateRoot); !os.IsNotExist(err) { + t.Fatalf("invalid context created mutable namespace: %v", err) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_devin_admission_test.go b/install/integrationctl/agentplugins/installer/project_devin_admission_test.go new file mode 100644 index 000000000..09d85138b --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_devin_admission_test.go @@ -0,0 +1,65 @@ +package installer_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +func devinSDKRemotePackage(t *testing.T, root string) { + t.Helper() + devinSDKWrite(t, filepath.Join(root, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-devin","version":"1.0.0"}`) + devinSDKWrite(t, filepath.Join(root, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert admission fixture\n---\nText only.\n") + devinSDKWrite(t, filepath.Join(root, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"sse","url":"https://managed.test/events"}}}`) +} + +func TestProjectDevinPublicSDKAbsentLegacyProfileAndVersionAdmission(t *testing.T) { + for _, evidence := range []string{"3000.11.205", "", "3000.3.0", "3000.11.205-alpha", "3000.11.205.1", "devin 3000.11.205 (6dea39440ae3)", "other-primary"} { + t.Run("version="+evidence, func(t *testing.T) { + base := devinSDKTemp(t) + pkg := filepath.Join(base, "package") + devinSDKRemotePackage(t, pkg) + f := newDevinSDKFixture(t, filepath.Join(base, "A"), "", pkg, "") + f.request.ClientVersion = evidence + if evidence == "other-primary" { + f.request.ClientVersion = "3000.11.205" + f.request.ClientExecutable = filepath.Join(base, "windsurf") + } + if evidence == "3000.11.205" { + f.apply(t, installer.OpInstall, "current-only-no-profile") + native := filepath.Join(f.project, ".devin", "mcp_config.json") + if body := devinSDKRead(t, native); !strings.Contains(body, `"transport"`) || !strings.Contains(body, `"sse"`) { + t.Fatalf("SSE transport lost: %s", body) + } + f.request.ClientVersion = "" + f.request.ClientExecutable = "" + f.apply(t, installer.OpRemove, "remove-no-current-harness") + return + } + if _, err := f.engine.Prepare(t.Context(), f.request); err == nil || !strings.Contains(err.Error(), "explicitly selected primary") { + t.Fatalf("unqualified host evidence admitted: %v", err) + } + if _, err := os.Lstat(filepath.Join(f.project, ".devin")); !os.IsNotExist(err) { + t.Fatalf("version refusal changed native files: %v", err) + } + }) + } +} + +func TestProjectDevinPortableManifestCannotSupplyVersionEvidence(t *testing.T) { + base := devinSDKTemp(t) + pkg := filepath.Join(base, "package") + devinSDKRemotePackage(t, pkg) + devinSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-devin","version":"1.0.0","clientVersion":"3000.11.205"}`) + f := newDevinSDKFixture(t, filepath.Join(base, "A"), "", pkg, "") + f.request.ClientVersion = "" + if _, err := f.engine.Prepare(t.Context(), f.request); err == nil { + t.Fatal("portable manifest supplied operational host evidence") + } + if _, err := os.Lstat(filepath.Join(f.project, ".devin")); !os.IsNotExist(err) { + t.Fatalf("injected source changed native files: %v", err) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_devin_fixture_test.go b/install/integrationctl/agentplugins/installer/project_devin_fixture_test.go new file mode 100644 index 000000000..47c958819 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_devin_fixture_test.go @@ -0,0 +1,132 @@ +package installer_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +type devinSDKFixture struct { + project, user, pkg, data string + engine *installer.Engine + request installer.Request +} + +func devinSDKWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func devinSDKRead(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} + +func newDevinSDKFixture(t *testing.T, project, user, pkg, helper string) *devinSDKFixture { + t.Helper() + if err := os.MkdirAll(project, 0700); err != nil { + t.Fatal(err) + } + if helper == "" { + var err error + helper, err = os.Executable() + if err != nil { + t.Fatal(err) + } + } + registry, err := clients.NewRegistry(windsurf.New()) + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{ProjectRoot: project, StateRoot: filepath.Join(project, ".uap"), Registry: registry, HelperExecutable: helper, TrustedLocalPackages: true}) + if err != nil { + t.Fatal(err) + } + return &devinSDKFixture{project: project, user: user, pkg: pkg, engine: engine, request: installer.Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: installer.OpInstall, PackageRoot: pkg, ClientID: "windsurf", ClientConfigRoot: user, ClientExecutable: filepath.Join(filepath.Dir(project), "devin"), ClientVersion: "3000.11.205", + InstallationID: "00000000-0000-4000-8000-000000002331", OperationID: "devin-project-sdk", RequiredComponents: []string{"skills", "mcp"}}} +} + +func (f *devinSDKFixture) apply(t *testing.T, op installer.Operation, label string) installer.Result { + t.Helper() + f.request.Operation, f.request.OperationID = op, label + handle, err := f.engine.Prepare(t.Context(), f.request) + if err != nil { + t.Fatalf("prepare Devin SDK %s: %v", label, err) + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + if op != installer.OpRemove && len(handle.Plan().Delivery.Components) != 2 { + t.Fatalf("Devin SDK omitted supported fixture component: %+v", handle.Plan()) + } + for _, component := range handle.Plan().Delivery.Components { + if component.Support == "unsupported" { + t.Fatalf("Devin SDK unsupported fixture: %+v", handle.Plan()) + } + } + result, err := f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) + if err != nil || result.Outcome != installer.OutcomeCompleted && result.Outcome != installer.OutcomeUnchanged { + t.Fatalf("Devin SDK %s=%+v,%v", label, result, err) + } + if result.Binding.DataRoot != "" { + if f.data != "" && f.data != result.Binding.DataRoot { + t.Fatal("SDK changed selected persistent data authority") + } + f.data = result.Binding.DataRoot + } + return result +} + +func (f *devinSDKFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := (statev2.Store{Path: filepath.Join(f.project, ".uap", "state-v2.json"), ProjectRoot: f.project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("Devin SDK state=%+v,%v", state, err) + } + for _, binding := range state.Installations[0].Clients { + return binding + } + t.Fatal("Devin SDK binding absent") + return domain.ClientBinding{} +} + +func devinSDKConfig(path string) (map[string]any, error) { + body, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var document struct { + Servers map[string]any `json:"mcpServers"` + } + if err := json.Unmarshal(body, &document); err != nil { + return nil, err + } + return document.Servers, nil +} + +func devinSDKTemp(t *testing.T) string { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + return root +} diff --git a/install/integrationctl/agentplugins/installer/project_devin_lifecycle_test.go b/install/integrationctl/agentplugins/installer/project_devin_lifecycle_test.go new file mode 100644 index 000000000..51284e2bb --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_devin_lifecycle_test.go @@ -0,0 +1,123 @@ +package installer_test + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +// Public SDK composition with real filesystem/state and inert stdio helper +// bytes. No runner, native client, launcher or MCP process is executed. +func TestProjectDevinPublicSDKStdioDataRepairRestartAndRemove(t *testing.T) { + base := devinSDKTemp(t) + user, pkg, helper := filepath.Join(base, "user", ".devin"), filepath.Join(base, "package"), filepath.Join(base, "inert-helper") + devinSDKWrite(t, helper, "inert helper bytes; never executed") + if err := os.Chmod(helper, 0700); err != nil { + t.Fatal(err) + } + const original = "---\nname: docs\ndescription: Inert Devin SDK fixture\n---\nOriginal pinned bytes.\n" + devinSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-devin","version":"1.0.0"}`) + devinSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), original) + devinSDKWrite(t, filepath.Join(pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"go","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_DATA}"}}}`) + devinSDKWrite(t, filepath.Join(pkg, "server.js"), "// Inert fixture; never executed.\n") + const foreign = `{"mcpServers":{"foreign":{"url":"https://foreign.test","disabled":true}},"policy":"keep"}` + devinSDKWrite(t, filepath.Join(user, "mcp_config.json"), foreign) + a, b := newDevinSDKFixture(t, filepath.Join(base, "A"), user, pkg, helper), newDevinSDKFixture(t, filepath.Join(base, "B"), user, pkg, helper) + for _, f := range []*devinSDKFixture{a, b} { + devinSDKWrite(t, filepath.Join(f.project, ".devin", "mcp_config.json"), foreign) + f.apply(t, installer.OpInstall, "install") + binding := f.binding(t) + if binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled || f.data == "" { + t.Fatalf("Devin SDK overclaimed/omitted data: %+v", binding) + } + devinSDKWrite(t, filepath.Join(f.data, "keep.txt"), f.project) + devinSDKCheckStdio(t, f) + } + if a.data == b.data { + t.Fatal("project stdio data receipts alias") + } + beforeB := devinSDKRead(t, filepath.Join(b.project, ".uap", "state-v2.json")) + bBinding := b.binding(t) + devinSDKRepairUpdateRemove(t, a, pkg, helper, original) + if beforeB != devinSDKRead(t, filepath.Join(b.project, ".uap", "state-v2.json")) || !reflect.DeepEqual(bBinding, b.binding(t)) { + t.Fatal("Devin SDK changed project B ownership") + } + devinSDKCheckStdio(t, b) + if devinSDKRead(t, filepath.Join(user, "mcp_config.json")) != foreign { + t.Fatal("Devin SDK changed global native configuration") + } + for _, f := range []*devinSDKFixture{a, b} { + if devinSDKRead(t, filepath.Join(f.data, "keep.txt")) != f.project { + t.Fatal("SDK removed scoped persistent data") + } + } +} + +func devinSDKCheckStdio(t *testing.T, f *devinSDKFixture) { + t.Helper() + binding := f.binding(t) + servers, err := devinSDKConfig(filepath.Join(f.project, ".devin", "mcp_config.json")) + if err != nil { + t.Fatal(err) + } + entry := servers["docs"].(map[string]any) + env := entry["env"].(map[string]any) + args := entry["args"].([]any) + if env["PLUGIN_ROOT"] != binding.TargetLocator || env["PLUGIN_DATA"] != f.data || len(args) < 7 || args[1] != binding.TargetLocator || args[2] != f.data || args[3] != f.data || args[4] != "data" || entry["cwd"] != nil || entry["type"] != nil { + t.Fatalf("SDK stdio binding = %+v", entry) + } + if devinSDKRead(t, entry["command"].(string)) != "inert helper bytes; never executed" { + t.Fatal("SDK launcher did not come from injected trusted helper") + } +} + +func devinSDKRepairUpdateRemove(t *testing.T, f *devinSDKFixture, pkg, helper, original string) { + t.Helper() + binding := f.binding(t) + nativeSkill := filepath.Join(f.project, ".devin", "skills", "docs", "SKILL.md") + if got := f.apply(t, installer.OpUpdate, "unchanged"); got.Mutated { + t.Fatalf("unchanged=%+v", got) + } + pinned := t.TempDir() + for _, name := range []string{"plugin.json", "mcp.json", "server.js"} { + devinSDKWrite(t, filepath.Join(pinned, name), devinSDKRead(t, filepath.Join(pkg, name))) + } + devinSDKWrite(t, filepath.Join(pinned, "skills", "docs", "SKILL.md"), original) + devinSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Advanced Devin SDK fixture\n---\nUpdated bytes.\n") + if err := os.RemoveAll(filepath.Dir(nativeSkill)); err != nil { + t.Fatal(err) + } + f.request.PackageRoot, f.request.SourceRoot = pinned, pkg + f.apply(t, installer.OpRepair, "repair-installed-pin") + if devinSDKRead(t, nativeSkill) != original { + t.Fatal("SDK repair followed moving source") + } + fresh := newDevinSDKFixture(t, f.project, f.user, pkg, helper) + fresh.data = f.data + *f = *fresh + f.apply(t, installer.OpUpdate, "update-after-restart") + if devinSDKRead(t, nativeSkill) == original { + t.Fatal("SDK update failed to advance selected project") + } + devinSDKCheckStdio(t, f) + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required { + t.Fatalf("Devin SDK status=%+v,%v", view, err) + } + f.request.ClientVersion, f.request.ClientExecutable = "", "" + f.apply(t, installer.OpRemove, "remove-without-current-version") + if _, err := os.Stat(binding.TargetLocator); !os.IsNotExist(err) { + t.Fatalf("SDK managed artifact survived: %v", err) + } + if _, err := os.Stat(nativeSkill); !os.IsNotExist(err) { + t.Fatalf("SDK native skill survived: %v", err) + } + servers, err := devinSDKConfig(filepath.Join(f.project, ".devin", "mcp_config.json")) + if err != nil || servers["docs"] != nil || servers["foreign"] == nil { + t.Fatalf("SDK remove ownership=%+v,%v", servers, err) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_handoff_test.go b/install/integrationctl/agentplugins/installer/project_handoff_test.go new file mode 100644 index 000000000..794aec8c4 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_handoff_test.go @@ -0,0 +1,128 @@ +package installer + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +type handoffCaptureAdapter struct { + id domain.ClientID + requests []domain.ActivationRequest +} + +func (a *handoffCaptureAdapter) ID() domain.ClientID { return a.id } +func (a *handoffCaptureAdapter) Activate(_ context.Context, _ clients.Env, req domain.ActivationRequest) (domain.ActivationOutcome, error) { + a.requests = append(a.requests, req) + return domain.ActivationOutcome{Activation: domain.ActivationPrepared, Verification: domain.VerificationInstalled}, nil +} +func (*handoffCaptureAdapter) Deactivate(context.Context, clients.Env, domain.DeactivationRequest) (domain.DeactivationOutcome, error) { + panic("handoff test must not deactivate") +} + +// Exercise the actual SDK seam and provider dispatch, retaining identity/path +// checks. The inert leaf observes the request; real file behavior is covered by +// TestProjectPreparedGroupPublicLifecycleAndHandoff and the single replay. +func TestProjectHandoffPreservesOnlyQualifiedReadOnlyRequest(t *testing.T) { + for _, tc := range []struct { + name string + change func(*domain.ActivationRequest) + wantVerify, wantError, profileFails bool + }{ + {name: "project native files", wantVerify: true}, + {name: "acting request", change: func(r *domain.ActivationRequest) { r.VerifyOnly = false }}, + {name: "user handoff", change: func(r *domain.ActivationRequest) { + r.Plan.Scope = domain.ScopeUser + r.Plan.TargetContext = domain.TargetContext{Scope: domain.ScopeUser, UserConfigRoot: r.Client.ConfigRoot} + }}, + {name: "project CLI registry", change: func(r *domain.ActivationRequest) { + r.Client.ClientID = domain.ClientClaude + r.Plan.ClientID = domain.ClientClaude + r.Delivery.ClientID = domain.ClientClaude + }}, + {name: "project prepared", change: func(r *domain.ActivationRequest) { + r.Client.ClientID = domain.ClientGrok + r.Plan.ClientID = domain.ClientGrok + r.Delivery.ClientID = domain.ClientGrok + }}, + {name: "missing project", change: func(r *domain.ActivationRequest) { r.Plan.TargetContext.ProjectRoot = "" }}, + {name: "mixed scope", change: func(r *domain.ActivationRequest) { r.Plan.TargetContext.Scope = domain.ScopeUser }}, + {name: "mixed original user", change: func(r *domain.ActivationRequest) { + r.Plan.TargetContext.UserConfigRoot = filepath.Join(r.Client.ConfigRoot, "other") + }}, + {name: "unclean project", change: func(r *domain.ActivationRequest) { r.Plan.TargetContext.ProjectRoot += string(filepath.Separator) }}, + {name: "client mismatch", change: func(r *domain.ActivationRequest) { r.Plan.ClientID = domain.ClientCodex }, wantError: true}, + {name: "selected delivery", change: func(r *domain.ActivationRequest) { + if err := json.Unmarshal([]byte(`{"mode":"unknown-test-delivery"}`), &r.Plan.SelectedDelivery); err != nil { + panic(err) + } + }, wantError: true}, + {name: "profile recheck failure", profileFails: true, wantError: true}, + } { + t.Run(tc.name, func(t *testing.T) { + base := t.TempDir() + active := filepath.Join(base, "project", "managed", "package") + if err := os.MkdirAll(active, 0700); err != nil { + t.Fatal(err) + } + user := filepath.Join(base, "user") + req := domain.ActivationRequest{ + Client: domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: user}, + Plan: domain.DeliveryPlan{ClientID: domain.ClientCline, Scope: domain.ScopeProject, ActivePath: active, TargetContext: domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: filepath.Join(base, "project"), UserConfigRoot: user}}, + Delivery: domain.StagedDelivery{ClientID: domain.ClientCline, ActivePath: active, OwnedBase: filepath.Dir(active)}, + DeclaredName: "handoff", PackageUnchanged: true, VerifyOnly: true, + } + if tc.change != nil { + tc.change(&req) + } + adapter := &handoffCaptureAdapter{id: req.Client.ClientID} + registry, err := clients.NewRegistry(adapter) + if err != nil { + t.Fatal(err) + } + binding := domain.ClientBinding{ClientBindingID: "current", ClientID: string(req.Client.ClientID), TargetLocator: active, Materialization: domain.MaterializationMaterialized, Activation: domain.ActivationPrepared} + state := domain.StateFileV2{Installations: []domain.Installation{{InstallationID: "group", Clients: map[string]domain.ClientBinding{"current": binding}}}} + kernel := nativeconfig.New() + profiles, callbacks := 0, 0 + profileErr := errors.New("profile changed") + seam := seamActivator{ + inner: providers.Activator{Registry: registry, NativeConfig: &kernel}, store: groupReportingStore{state: state}, + // Group facts may refer to another target. The current request + // supplies the client's qualified scope and original user root. + facts: BindingFacts{InstallationID: "group", BindingID: "first", ClientID: "codex"}, + profileCheck: func(context.Context) error { + profiles++ + if tc.profileFails { + return profileErr + } + return nil + }, + onCommitted: func(context.Context, BindingFacts) error { callbacks++; return nil }, + } + _, err = seam.Activate(t.Context(), req) + if (err != nil) != tc.wantError || callbacks != 0 { + t.Fatalf("err=%v callbacks=%d", err, callbacks) + } + if tc.profileFails && !errors.Is(err, profileErr) { + t.Fatal("profile failure lost", err) + } + if tc.wantError { + if len(adapter.requests) != 0 { + t.Fatal("rejected request reached adapter") + } + return + } + if profiles != 1 || len(adapter.requests) != 1 || adapter.requests[0].VerifyOnly != tc.wantVerify { + t.Fatalf("profiles=%d requests=%+v", profiles, adapter.requests) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_host_evidence_test.go b/install/integrationctl/agentplugins/installer/project_host_evidence_test.go new file mode 100644 index 000000000..3887cc029 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_host_evidence_test.go @@ -0,0 +1,112 @@ +package installer + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/gemini" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// This exercises the shared SDK boundary with an already scoped adapter. The +// Devin lane separately tests interpretation of the selected host evidence. +func TestProjectPreparePreservesSelectedHostVersion(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + project, pkg := filepath.Join(base, "project"), filepath.Join(base, "package") + for _, root := range []string{project, pkg} { + if err := os.MkdirAll(root, 0700); err != nil { + t.Fatal(err) + } + } + if err := os.WriteFile(filepath.Join(pkg, "plugin.json"), []byte(`{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"host-evidence","version":"1.0.0"}`), 0600); err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(gemini.New()) + if err != nil { + t.Fatal(err) + } + engine, err := newTestEngine(t, Config{Registry: registry, ProjectRoot: project, StateRoot: filepath.Join(project, ".uap")}) + if err != nil { + t.Fatal(err) + } + req := Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: OpInstall, PackageRoot: pkg, + ClientID: "gemini", ClientConfigRoot: filepath.Join(base, "profile"), ClientExecutable: filepath.Join(base, "gemini"), ClientVersion: "host-only-version"} + prepared, err := engine.Prepare(context.Background(), req) + if err != nil { + t.Fatal(err) + } + defer func() { _ = prepared.Close() }() + req.ClientVersion = "caller-changed" + if prepared.client.Version != "host-only-version" || prepared.req.ClientVersion != "host-only-version" || prepared.client.ConfigRoot != req.ClientConfigRoot || prepared.Plan().TargetContext.UserConfigRoot != req.ClientConfigRoot { + t.Fatalf("host evidence or original profile changed: %+v", prepared.client) + } + body, err := json.Marshal(prepared.req) + if err != nil || strings.Contains(string(body), "host-only-version") || strings.Contains(string(body), "ClientVersion") { + t.Fatalf("operational version leaked into diagnostic JSON: %s %v", body, err) + } + var decoded Request + if err := json.Unmarshal([]byte(`{"ClientVersion":"source-injected"}`), &decoded); err != nil || decoded.ClientVersion != "" { + t.Fatalf("JSON injected operational host version: %+v %v", decoded, err) + } + for _, root := range []string{"relative", filepath.Join(base, "profile") + string(filepath.Separator) + "."} { + req.ClientConfigRoot = root + if _, err := engine.Prepare(context.Background(), req); !errors.Is(err, ErrInvalidRequest) { + t.Fatalf("invalid nonempty project profile %q accepted: %v", root, err) + } + } +} + +func TestProjectHostEvidenceDoesNotRelaxUserProfileAdmission(t *testing.T) { + engine, err := newTestEngine(t, Config{StateRoot: filepath.Join(t.TempDir(), "state")}) + if err != nil { + t.Fatal(err) + } + _, err = engine.detectedClient(Request{Operation: OpRemove, ClientID: "codex", ClientVersion: "host-evidence"}) + if !errors.Is(err, ErrInvalidRequest) { + t.Fatalf("user scope accepted absent original profile: %v", err) + } +} + +func TestProjectHostEvidenceAllowsAbsentGenericProfileOnlyInProject(t *testing.T) { + registry, err := clients.NewRegistry(windsurf.New()) + if err != nil { + t.Fatal(err) + } + project, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + engine, err := newTestEngine(t, Config{Registry: registry, ProjectRoot: project, StateRoot: filepath.Join(project, ".uap")}) + if err != nil { + t.Fatal(err) + } + req := Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: OpInstall, ClientID: "windsurf", ClientVersion: "host-version"} + // Context validation is always before reconstruction in public Prepare. + // The adapter's new scoped lane has its own full public lifecycle coverage. + if err := engine.CheckProjectNamespace(); err != nil { + t.Fatal(err) + } + client, err := engine.detectedClient(req) + if err != nil || client.ConfigRoot != "" || client.Version != req.ClientVersion { + t.Fatalf("absent original profile: %+v %v", client, err) + } + strict, err := clients.NewRegistry(gemini.New()) + if err != nil { + t.Fatal(err) + } + engine.cfg.Registry = strict + req.ClientID = "gemini" + if _, err := engine.detectedClient(req); !errors.Is(err, ErrInvalidRequest) { + t.Fatalf("stricter profile resolver bypassed: %v", err) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_identity.go b/install/integrationctl/agentplugins/installer/project_identity.go new file mode 100644 index 000000000..3bd34d63a --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_identity.go @@ -0,0 +1,22 @@ +package installer + +import ( + "context" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +// Optional live discovery cannot disable project filesystem ownership checks. +// Both entry points deliberately retain the provider's prepared semantics. +type preparedProjectIdentityObserver struct { + identity providers.NativeIdentityObserver +} + +func (o preparedProjectIdentityObserver) ObserveNativeIdentity(ctx context.Context, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + return o.identity.ObservePreparedIdentity(ctx, client, plan, managed) +} + +func (o preparedProjectIdentityObserver) ObservePreparedIdentity(ctx context.Context, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + return o.identity.ObservePreparedIdentity(ctx, client, plan, managed) +} diff --git a/install/integrationctl/agentplugins/installer/project_identity_test.go b/install/integrationctl/agentplugins/installer/project_identity_test.go new file mode 100644 index 000000000..70f4375f0 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_identity_test.go @@ -0,0 +1,440 @@ +package installer + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cline" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/vscode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type projectFilesNoRunner struct{ calls int } + +func (r *projectFilesNoRunner) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("forbidden client/MCP command: %v", command.Argv) +} + +type projectFilesFixture struct { + sequence int + plan domain.DeliveryPlan + base, project, user, pkg, skill, config string + cfg Config + req Request + engine *Engine + runner *projectFilesNoRunner +} + +func newProjectFilesFixture(t *testing.T, id string, mode string) *projectFilesFixture { + t.Helper() + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + f := &projectFilesFixture{base: base, project: filepath.Join(base, "A"), user: filepath.Join(base, "user", "."+id), pkg: filepath.Join(base, "package"), runner: &projectFilesNoRunner{}} + deathWrite(t, filepath.Join(f.project, "foreign.txt"), "project control") + deathWrite(t, filepath.Join(base, "B", "keep.txt"), "other project") + deathWrite(t, filepath.Join(f.user, "keep.txt"), "original user") + deathWrite(t, filepath.Join(base, "user-uap", "state-v2.json"), "original user state control") + deathWrite(t, filepath.Join(f.pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"files-observer","version":"1.0.0"}`) + deathWrite(t, filepath.Join(f.pkg, "skills", "docs", "SKILL.md"), deathSkill("docs", "original")) + deathWrite(t, filepath.Join(f.pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"http://127.0.0.1:9/inert"}}}`) + registry, err := clients.NewRegistry(cline.New(), codex.New(), vscode.NewProject()) + if err != nil { + t.Fatal(err) + } + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + f.cfg = Config{ProjectRoot: f.project, StateRoot: filepath.Join(f.project, ".uap"), Registry: registry, TrustedLocalPackages: true, HelperExecutable: helper} + switch mode { + case "default": + case "disabled-with-runner": + f.cfg.Runner = f.runner + case "enabled-without-runner": + f.cfg.EnableNativeObserver = true + default: + t.Fatal("unknown fixture mode", mode) + } + f.req = Request{Scope: domain.ScopeProject, ProjectRoot: f.project, Operation: OpInstall, PackageRoot: f.pkg, ClientID: id, ClientConfigRoot: f.user, InstallationID: "00000000-0000-4000-8000-000000000074", RequiredComponents: []string{"skills", "mcp"}} + switch id { + case "cline": + f.skill = filepath.Join(f.project, ".cline", "skills", "docs", "SKILL.md") + f.config = filepath.Join(f.project, ".uap", "clients", "cline", "mcp.json") + case "vscode": + f.skill = filepath.Join(f.project, ".github", "skills", "docs", "SKILL.md") + f.config = filepath.Join(f.project, ".vscode", "mcp.json") + default: + f.skill = filepath.Join(f.project, ".agents", "skills", "docs", "SKILL.md") + f.config = filepath.Join(f.project, ".codex", "config.toml") + } + f.reopen(t) + return f +} +func (f *projectFilesFixture) reopen(t *testing.T) { + t.Helper() + var err error + f.engine, err = New(f.cfg) + if err != nil { + t.Fatal(err) + } +} +func (f *projectFilesFixture) execute(t *testing.T, op Operation) (Result, error) { + t.Helper() + req := f.req + f.sequence++ + req.Operation, req.OperationID = op, fmt.Sprintf("project-files-%s-%s-%d", op, req.ClientID, f.sequence) + handle, err := f.engine.Prepare(t.Context(), req) + if err != nil { + return Result{}, err + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + if len(handle.deliveries) > 0 { + f.plan = handle.deliveries[0].plan + } else if op != OpRemove { + t.Fatal("missing exact prepared delivery") + } + return f.engine.Apply(t.Context(), handle, Decision{Confirmed: true}) +} +func (f *projectFilesFixture) apply(t *testing.T, op Operation) Result { + t.Helper() + out, err := f.execute(t, op) + if err != nil || out.Outcome != OutcomeCompleted && out.Outcome != OutcomeUnchanged { + t.Fatalf("%s: %+v %v", op, out, err) + } + return out +} +func (f *projectFilesFixture) state(t *testing.T) string { + t.Helper() + body, err := os.ReadFile(filepath.Join(f.cfg.StateRoot, "state-v2.json")) + if os.IsNotExist(err) { + return "" + } + if err != nil { + t.Fatal(err) + } + return string(body) +} +func (f *projectFilesFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := f.engine.store.Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("state %+v %v", state, err) + } + for _, b := range state.Installations[0].Clients { + if b.ClientID == f.req.ClientID { + return b + } + } + t.Fatal("missing project binding") + return domain.ClientBinding{} +} +func (f *projectFilesFixture) untouched(t *testing.T) map[string]map[string]deathTreeEntry { + t.Helper() + out := map[string]map[string]deathTreeEntry{} + for _, root := range []string{filepath.Join(f.base, "user"), filepath.Join(f.base, "B"), filepath.Join(f.base, "user-uap")} { + out[root] = deathTree(t, root) + } + return out +} +func (f *projectFilesFixture) assertUntouched(t *testing.T, want map[string]map[string]deathTreeEntry) { + t.Helper() + if !reflect.DeepEqual(want, f.untouched(t)) || f.runner.calls != 0 { + t.Fatal("project operation changed original user/B or ran client/MCP") + } +} + +// The original default-SDK gap committed a managed package before discovering +// this collision. All unavailable/disabled discovery compositions must refuse +// in public Prepare/Apply before durable installation publication. +func TestProjectFileAdmissionWithoutLiveObserver(t *testing.T) { + for _, mode := range []string{"default", "disabled-with-runner", "enabled-without-runner"} { + for _, collision := range []string{"skill", "mcp", "malformed-mcp"} { + t.Run(mode+"/"+collision, func(t *testing.T) { + f := newProjectFilesFixture(t, "cline", mode) + foreign := f.skill + body := "foreign skill bytes" + if collision != "skill" { + foreign = f.config + body = `{"mcpServers":{"docs":{"transport":{"type":"streamableHttp","url":"https://foreign.test/mcp"}}}}` + if collision == "malformed-mcp" { + body = `{"mcpServers":` + } + } + deathWrite(t, foreign, body) + before, other := f.state(t), f.untouched(t) + out, err := f.execute(t, OpInstall) + if err == nil || out.Mutated { + t.Fatalf("foreign admission: %+v %v", out, err) + } + if f.state(t) != before || deathRead(t, foreign) != body { + t.Fatal("refusal changed state or foreign component") + } + for _, name := range []string{"managed", "data"} { + if _, err := os.Lstat(filepath.Join(f.cfg.StateRoot, name)); !os.IsNotExist(err) { + t.Fatalf("refusal published %s: %v", name, err) + } + } + f.assertUntouched(t, other) + }) + } + } +} + +func TestProjectFileLifecycleWithoutLiveObserver(t *testing.T) { + for _, id := range []string{"cline", "codex", "vscode"} { + t.Run(id, func(t *testing.T) { + f := newProjectFilesFixture(t, id, "disabled-with-runner") + other := f.untouched(t) + first := f.apply(t, OpInstall) + if !first.Mutated { + t.Fatal("first install did not publish") + } + binding := f.binding(t) + if binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled { + t.Fatalf("invented native activation %+v", binding) + } + before := f.state(t) + again := f.apply(t, OpInstall) + if again.Mutated || f.state(t) != before { + t.Fatal("unchanged replay changed durable state") + } + deathWrite(t, filepath.Join(f.pkg, "skills", "docs", "SKILL.md"), deathSkill("docs", "updated")) + f.apply(t, OpUpdate) + if deathRead(t, f.skill) != deathSkill("docs", "updated") { + t.Fatal("update did not project new skill") + } + f.reopen(t) + for _, path := range []string{f.skill, f.config} { + original := deathRead(t, path) + deathWrite(t, path, "foreign replacement") + before = f.state(t) + out, err := f.execute(t, OpRepair) + if err == nil || out.Mutated || before != f.state(t) || deathRead(t, path) != "foreign replacement" { + t.Fatalf("tampered component was accepted: %+v %v", out, err) + } + deathWrite(t, path, original) + } + if err := os.RemoveAll(filepath.Dir(f.skill)); err != nil { + t.Fatal(err) + } + f.apply(t, OpRepair) + if deathRead(t, f.skill) != deathSkill("docs", "updated") { + t.Fatal("missing skill not repaired") + } + binding = f.binding(t) + managedSkill := filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md") + deathWrite(t, managedSkill, "known managed drift") + f.apply(t, OpRepair) + if deathRead(t, managedSkill) != deathSkill("docs", "updated") { + t.Fatal("known managed drift not restored") + } + if err := os.RemoveAll(binding.TargetLocator); err != nil { + t.Fatal(err) + } + f.apply(t, OpRepair) + if deathRead(t, managedSkill) != deathSkill("docs", "updated") { + t.Fatal("known absent managed package not restored") + } + f.apply(t, OpRemove) + if _, err := os.Lstat(filepath.Dir(f.skill)); !os.IsNotExist(err) { + t.Fatal("owned skill survived remove", err) + } + f.reopen(t) + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required { + t.Fatalf("reopen %+v %v", view, err) + } + f.assertUntouched(t, other) + }) + } +} + +func TestProjectFileObserverGroupRepairsOnlyKnownMissingManagedPackage(t *testing.T) { + for _, foreign := range []bool{false, true} { + t.Run(fmt.Sprint(foreign), func(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "default") + f.apply(t, OpInstall) + codexUser := filepath.Join(f.base, "user", ".codex") + deathWrite(t, filepath.Join(codexUser, "keep.txt"), "original Codex user") + f.req.ClientID, f.req.ClientConfigRoot = "codex", codexUser + f.apply(t, OpInstall) + f.req.ClientID, f.req.ClientConfigRoot = "cline", f.user + binding := f.binding(t) + native, config := deathRead(t, f.skill), deathRead(t, f.config) + if err := os.RemoveAll(binding.TargetLocator); err != nil { + t.Fatal(err) + } + if foreign { + deathWrite(t, f.skill, "foreign replacement") + } + before, other := f.state(t), f.untouched(t) + f.req.Targets = []ClientTarget{{ClientID: "cline", ClientConfigRoot: f.user}, {ClientID: "codex", ClientConfigRoot: codexUser}} + out, err := f.execute(t, OpRepair) + if foreign { + if err == nil || out.Mutated || before != f.state(t) || deathRead(t, f.skill) != "foreign replacement" { + t.Fatalf("group adopted foreign native file: %+v %v", out, err) + } + if _, statErr := os.Stat(binding.TargetLocator); !os.IsNotExist(statErr) { + t.Fatal("foreign refusal restored managed package", statErr) + } + } else { + // The file operation completed; the actual client lifecycles + // and remaining native-loading instructions stay Prepared. + if err != nil || out.Outcome != OutcomeCompleted || !out.Mutated || out.NoChange || len(out.ManualActions) == 0 { + t.Fatalf("prepared group file completion: %+v %v", out, err) + } + for _, target := range out.Targets { + if target.Activation != "prepared" || target.Verification != "installation_verified" { + t.Fatalf("promoted project client: %+v", target) + } + } + if deathRead(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md")) != native { + t.Fatal("known managed package was not restored") + } + if deathRead(t, f.skill) != native || deathRead(t, f.config) != config { + t.Fatal("group repair changed intact native files") + } + if got := f.binding(t); got.Activation != domain.ActivationPrepared || got.Verification != domain.VerificationInstalled { + t.Fatalf("group repair promoted native lifecycle %+v", got) + } + } + f.assertUntouched(t, other) + }) + } +} + +func TestProjectFileObserverRetainsActualPendingAttempt(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + f.apply(t, OpInstall) + deathWrite(t, filepath.Join(f.pkg, "skills", "docs", "SKILL.md"), deathSkill("docs", "updated before failed handoff")) + sentinel := errors.New("real host handoff stopped before native activation") + f.cfg.OnCommittedBinding = func(_ context.Context, facts BindingFacts) error { + if deathRead(t, filepath.Join(facts.TargetPath, "skills", "docs", "SKILL.md")) == deathSkill("docs", "updated before failed handoff") { + return sentinel + } + return nil + } + f.reopen(t) + out, err := f.execute(t, OpUpdate) + if !errors.Is(err, sentinel) || !out.Mutated { + t.Fatalf("did not create actual unresolved effect: %+v %v", out, err) + } + binding := f.binding(t) + if binding.NativeActivationAttempt == "" { + t.Fatal("actual effect omitted persisted attempt") + } + // Native files still match the last acknowledged receipts. The newly + // committed package also has its exact new digest. Neither clears uncertainty. + helper, _ := f.engine.helper() + svc := f.engine.lifecycle(helper, BindingFacts{}, map[domain.ClientID]domain.DetectedClient{}) + client := domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: f.user} + plan := f.plan + plan.PreviousNativeObjects = binding.NativeObjects + kernel := nativeconfig.New() + finding, findErr := cline.New().InspectNativeRegistry(t.Context(), clients.Env{NativeConfig: kernel}, client, plan, &binding) + if findErr != nil || finding != clients.RegistryExpected { + t.Fatalf("fixture did not have exact expected native files: %v %v", finding, findErr) + } + observed, obsErr := svc.NativeObserver.ObserveNativeIdentity(t.Context(), client, plan, &binding) + if obsErr != nil || observed.State != domain.NativeIdentityManaged || !observed.ReceiptReconciled || observed.NativeDiscoveryAttempted || observed.NativeDiscoveryReconciled { + t.Fatalf("prepared file check fabricated native evidence: %+v %v", observed, obsErr) + } + before, other := f.state(t), f.untouched(t) + native, config := deathRead(t, f.skill), deathRead(t, f.config) + f.cfg.OnCommittedBinding = nil + f.reopen(t) + view, inspectErr := f.engine.Inspect(t.Context()) + if inspectErr == nil || !view.Recovery.Required || !strings.Contains(view.Recovery.Reason, "retain uncertainty") { + t.Fatalf("lost actual uncertainty %+v %v", view, inspectErr) + } + recovered, recoverErr := f.engine.Recover(t.Context(), view) + if !errors.Is(recoverErr, ErrRecoveryRequired) || recovered.Outcome != OutcomeRecovery { + t.Fatalf("default observer guessed recovery %+v %v", recovered, recoverErr) + } + if f.state(t) != before || deathRead(t, f.skill) != native || deathRead(t, f.config) != config { + t.Fatal("refused recovery changed retained ownership") + } + f.assertUntouched(t, other) +} + +// An existing adapter seam makes the foreign edit after successful initial +// ownership admission, while the real staged projection is being produced. +type projectFilesEditDuringStage struct { + *cline.Adapter + after func() +} + +func (a projectFilesEditDuringStage) Project(ctx context.Context, in clients.ProjectionInput) ([]domain.NativeObjectOwnership, error) { + objects, err := a.Adapter.Project(ctx, in) + if err == nil { + a.after() + } + return objects, err +} +func TestProjectFileRepairRefusesForeignStateWithManagedDamage(t *testing.T) { + for _, damage := range []string{"absent", "drift"} { + for _, component := range []string{"skill", "mcp"} { + t.Run(damage+"/"+component, func(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "default") + f.apply(t, OpInstall) + binding := f.binding(t) + if damage == "absent" { + if err := os.RemoveAll(binding.TargetLocator); err != nil { + t.Fatal(err) + } + } else { + deathWrite(t, filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md"), "managed drift") + } + path := f.skill + if component == "mcp" { + path = f.config + } + deathWrite(t, path, "foreign native bytes") + before, other := f.state(t), f.untouched(t) + out, err := f.execute(t, OpRepair) + if err == nil || out.Mutated || f.state(t) != before || deathRead(t, path) != "foreign native bytes" { + t.Fatalf("managed damage masked native collision %+v %v", out, err) + } + f.assertUntouched(t, other) + }) + } + } +} +func TestProjectFileRepairRechecksOwnershipAfterStaging(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + f.apply(t, OpInstall) + binding := f.binding(t) + managedSkill := filepath.Join(binding.TargetLocator, "skills", "docs", "SKILL.md") + deathWrite(t, managedSkill, "known managed drift") + staged := 0 + adapter := projectFilesEditDuringStage{Adapter: cline.New(), after: func() { staged++; deathWrite(t, f.skill, "foreign edit during staging") }} + registry, err := clients.NewRegistry(adapter, codex.New()) + if err != nil { + t.Fatal(err) + } + f.cfg.Registry = registry + f.reopen(t) + before, other := f.state(t), f.untouched(t) + out, err := f.execute(t, OpRepair) + if err == nil || out.Mutated || staged != 1 || f.state(t) != before || deathRead(t, managedSkill) != "known managed drift" || deathRead(t, f.skill) != "foreign edit during staging" { + t.Fatalf("staging-time edit crossed publication: %+v %v stage=%d", out, err, staged) + } + f.assertUntouched(t, other) +} diff --git a/install/integrationctl/agentplugins/installer/project_kimi_fixture_test.go b/install/integrationctl/agentplugins/installer/project_kimi_fixture_test.go new file mode 100644 index 000000000..5416be65c --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_kimi_fixture_test.go @@ -0,0 +1,96 @@ +package installer_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kimi" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +type kimiSDKFixture struct { + project, user, pkg, data string + engine *installer.Engine + request installer.Request +} + +func kimiSDKWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func kimiSDKRead(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} + +func newKimiSDKFixture(t *testing.T, project, user, pkg, helper string) *kimiSDKFixture { + t.Helper() + if err := os.MkdirAll(project, 0700); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(project, ".git"), 0700); err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(kimi.New()) + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{ProjectRoot: project, StateRoot: filepath.Join(project, ".uap"), Registry: registry, HelperExecutable: helper, TrustedLocalPackages: true}) + if err != nil { + t.Fatal(err) + } + return &kimiSDKFixture{project: project, user: user, pkg: pkg, engine: engine, request: installer.Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: installer.OpInstall, PackageRoot: pkg, ClientID: "kimi", ClientConfigRoot: user, ClientExecutable: filepath.Join(filepath.Dir(project), "kimi"), ClientVersion: "2.3.1", + InstallationID: "00000000-0000-4000-8000-000000002331", OperationID: "kimi-project-sdk", RequiredComponents: []string{"skills", "mcp"}}} +} + +func (f *kimiSDKFixture) apply(t *testing.T, op installer.Operation, label string) installer.Result { + t.Helper() + f.request.Operation, f.request.OperationID = op, label + handle, err := f.engine.Prepare(t.Context(), f.request) + if err != nil { + t.Fatalf("prepare Kimi SDK %s: %v", label, err) + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + result, err := f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) + if err != nil || result.Outcome != installer.OutcomeCompleted && result.Outcome != installer.OutcomeUnchanged { + t.Fatalf("Kimi SDK %s=%+v,%v", label, result, err) + } + if result.Binding.DataRoot != "" { + if f.data != "" && f.data != result.Binding.DataRoot { + t.Fatal("SDK changed selected persistent data authority") + } + f.data = result.Binding.DataRoot + } + return result +} + +func (f *kimiSDKFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := (statev2.Store{Path: filepath.Join(f.project, ".uap", "state-v2.json"), ProjectRoot: f.project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("Kimi SDK state=%+v,%v", state, err) + } + for _, binding := range state.Installations[0].Clients { + return binding + } + t.Fatal("Kimi SDK binding absent") + return domain.ClientBinding{} +} diff --git a/install/integrationctl/agentplugins/installer/project_kimi_lifecycle_test.go b/install/integrationctl/agentplugins/installer/project_kimi_lifecycle_test.go new file mode 100644 index 000000000..c9ff6d3b1 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_kimi_lifecycle_test.go @@ -0,0 +1,142 @@ +package installer_test + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +// Public SDK composition with real filesystem/state and inert stdio helper +// bytes. No runner, native client, launcher or MCP process is executed. +func TestProjectKimiPublicSDKStdioDataRepairRestartAndRemove(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + user, pkg, helper := filepath.Join(base, "user", ".kimi-code"), filepath.Join(base, "package"), filepath.Join(base, "inert-helper") + kimiSDKWrite(t, helper, "inert helper bytes; never executed") + if err := os.Chmod(helper, 0700); err != nil { + t.Fatal(err) + } + const original = "---\nname: docs\ndescription: Inert Kimi SDK fixture\n---\nOriginal pinned bytes.\n" + kimiSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-kimi","version":"1.0.0"}`) + kimiSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), original) + kimiSDKWrite(t, filepath.Join(pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_DATA}"}}}`) + kimiSDKWrite(t, filepath.Join(pkg, "server.js"), "// Inert fixture; never executed.\n") + const foreign = `{"mcpServers":{"foreign":{"url":"https://foreign.test","enabled":false}},"policy":"keep"}` + kimiSDKWrite(t, filepath.Join(user, "mcp.json"), foreign) + a, b := newKimiSDKFixture(t, filepath.Join(base, "A"), user, pkg, helper), newKimiSDKFixture(t, filepath.Join(base, "B"), user, pkg, helper) + for _, f := range []*kimiSDKFixture{a, b} { + kimiSDKWrite(t, filepath.Join(f.project, ".kimi-code", "mcp.json"), foreign) + f.apply(t, installer.OpInstall, "install") + binding := f.binding(t) + if binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled || f.data == "" { + t.Fatalf("Kimi SDK overclaimed/omitted data: %+v", binding) + } + kimiSDKWrite(t, filepath.Join(f.data, "keep.txt"), f.project) + kimiSDKCheckStdio(t, f) + } + if a.data == b.data { + t.Fatal("project stdio data receipts alias") + } + beforeB := kimiSDKRead(t, filepath.Join(b.project, ".uap", "state-v2.json")) + bBinding := b.binding(t) + kimiSDKRepairUpdateRemove(t, a, pkg, helper, original) + if beforeB != kimiSDKRead(t, filepath.Join(b.project, ".uap", "state-v2.json")) || !reflect.DeepEqual(bBinding, b.binding(t)) { + t.Fatal("Kimi SDK changed project B ownership") + } + kimiSDKCheckStdio(t, b) + if kimiSDKRead(t, filepath.Join(user, "mcp.json")) != foreign { + t.Fatal("Kimi SDK changed global native configuration") + } + for _, f := range []*kimiSDKFixture{a, b} { + if kimiSDKRead(t, filepath.Join(f.data, "keep.txt")) != f.project { + t.Fatal("SDK removed scoped persistent data") + } + } +} + +func kimiSDKCheckStdio(t *testing.T, f *kimiSDKFixture) { + t.Helper() + binding := f.binding(t) + servers, err := kimiSDKServers(filepath.Join(f.project, ".kimi-code", "mcp.json")) + if err != nil { + t.Fatal(err) + } + entry := servers["docs"].(map[string]any) + env := entry["env"].(map[string]any) + args := entry["args"].([]any) + if env["PLUGIN_ROOT"] != binding.TargetLocator || env["PLUGIN_DATA"] != f.data || len(args) < 7 || args[1] != binding.TargetLocator || args[2] != f.data || args[3] != f.data || args[4] != "data" || entry["cwd"] != nil || entry["transport"] != "stdio" { + t.Fatalf("SDK stdio binding = %+v", entry) + } + if kimiSDKRead(t, entry["command"].(string)) != "inert helper bytes; never executed" { + t.Fatal("SDK launcher did not come from injected trusted helper") + } +} + +func kimiSDKRepairUpdateRemove(t *testing.T, f *kimiSDKFixture, pkg, helper, original string) { + t.Helper() + binding := f.binding(t) + nativeSkill := filepath.Join(f.project, ".kimi-code", "skills", "docs", "SKILL.md") + if got := f.apply(t, installer.OpUpdate, "unchanged"); got.Mutated { + t.Fatalf("unchanged=%+v", got) + } + pinned, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"plugin.json", "mcp.json", "server.js"} { + kimiSDKWrite(t, filepath.Join(pinned, name), kimiSDKRead(t, filepath.Join(pkg, name))) + } + kimiSDKWrite(t, filepath.Join(pinned, "skills", "docs", "SKILL.md"), original) + kimiSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Advanced Kimi SDK fixture\n---\nUpdated bytes.\n") + if err := os.RemoveAll(filepath.Dir(nativeSkill)); err != nil { + t.Fatal(err) + } + f.request.PackageRoot, f.request.SourceRoot = pinned, pkg + f.apply(t, installer.OpRepair, "repair-installed-pin") + if kimiSDKRead(t, nativeSkill) != original { + t.Fatal("SDK repair followed moving source") + } + fresh := newKimiSDKFixture(t, f.project, f.user, pkg, helper) + fresh.data = f.data + *f = *fresh + f.apply(t, installer.OpUpdate, "update-after-restart") + if kimiSDKRead(t, nativeSkill) == original { + t.Fatal("SDK update failed to advance selected project") + } + kimiSDKCheckStdio(t, f) + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required { + t.Fatalf("Kimi SDK status=%+v,%v", view, err) + } + f.request.ClientVersion, f.request.ClientExecutable = "", "" + f.apply(t, installer.OpRemove, "remove") + if _, err := os.Stat(binding.TargetLocator); !os.IsNotExist(err) { + t.Fatalf("SDK managed artifact survived: %v", err) + } + if _, err := os.Stat(nativeSkill); !os.IsNotExist(err) { + t.Fatalf("SDK native skill survived: %v", err) + } + servers, err := kimiSDKServers(filepath.Join(f.project, ".kimi-code", "mcp.json")) + if err != nil || servers["docs"] != nil || servers["foreign"] == nil { + t.Fatalf("SDK remove ownership=%+v,%v", servers, err) + } +} + +func kimiSDKServers(path string) (map[string]any, error) { + body, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var document struct { + Servers map[string]any `json:"mcpServers"` + } + err = json.Unmarshal(body, &document) + return document.Servers, err +} diff --git a/install/integrationctl/agentplugins/installer/project_kiro_fixture_test.go b/install/integrationctl/agentplugins/installer/project_kiro_fixture_test.go new file mode 100644 index 000000000..9335b7259 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_kiro_fixture_test.go @@ -0,0 +1,93 @@ +package installer_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +type kiroSDKFixture struct { + project, user, pkg, data string + engine *installer.Engine + request installer.Request +} + +func kiroSDKWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func kiroSDKRead(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} + +func newKiroSDKFixture(t *testing.T, project, user, pkg, helper string) *kiroSDKFixture { + t.Helper() + if err := os.MkdirAll(project, 0700); err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(kiro.New()) + if err != nil { + t.Fatal(err) + } + engine, err := installer.New(installer.Config{ProjectRoot: project, StateRoot: filepath.Join(project, ".uap"), Registry: registry, HelperExecutable: helper, TrustedLocalPackages: true}) + if err != nil { + t.Fatal(err) + } + return &kiroSDKFixture{project: project, user: user, pkg: pkg, engine: engine, request: installer.Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: installer.OpInstall, PackageRoot: pkg, ClientID: "kiro", ClientConfigRoot: user, + InstallationID: "00000000-0000-4000-8000-000000002331", OperationID: "kiro-project-sdk", RequiredComponents: []string{"skills", "mcp"}}} +} + +func (f *kiroSDKFixture) apply(t *testing.T, op installer.Operation, label string) installer.Result { + t.Helper() + f.request.Operation, f.request.OperationID = op, label + handle, err := f.engine.Prepare(t.Context(), f.request) + if err != nil { + t.Fatalf("prepare Kiro SDK %s: %v", label, err) + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + result, err := f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) + if err != nil || result.Outcome != installer.OutcomeCompleted && result.Outcome != installer.OutcomeUnchanged { + t.Fatalf("Kiro SDK %s=%+v,%v", label, result, err) + } + if result.Binding.DataRoot != "" { + if f.data != "" && f.data != result.Binding.DataRoot { + t.Fatal("SDK changed selected persistent data authority") + } + f.data = result.Binding.DataRoot + } + return result +} + +func (f *kiroSDKFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := (statev2.Store{Path: filepath.Join(f.project, ".uap", "state-v2.json"), ProjectRoot: f.project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("Kiro SDK state=%+v,%v", state, err) + } + for _, binding := range state.Installations[0].Clients { + return binding + } + t.Fatal("Kiro SDK binding absent") + return domain.ClientBinding{} +} diff --git a/install/integrationctl/agentplugins/installer/project_kiro_lifecycle_test.go b/install/integrationctl/agentplugins/installer/project_kiro_lifecycle_test.go new file mode 100644 index 000000000..b71949b33 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_kiro_lifecycle_test.go @@ -0,0 +1,129 @@ +package installer_test + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" +) + +// Public SDK composition with real filesystem/state and inert stdio helper +// bytes. No runner, native client, launcher or MCP process is executed. +func TestProjectKiroPublicSDKStdioDataRepairRestartAndRemove(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + user, pkg, helper := filepath.Join(base, "user", ".kiro"), filepath.Join(base, "package"), filepath.Join(base, "inert-helper") + kiroSDKWrite(t, helper, "inert helper bytes; never executed") + if err := os.Chmod(helper, 0700); err != nil { + t.Fatal(err) + } + const original = "---\nname: docs\ndescription: Inert Kiro SDK fixture\n---\nOriginal pinned bytes.\n" + kiroSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-kiro","version":"1.0.0"}`) + kiroSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), original) + kiroSDKWrite(t, filepath.Join(pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_DATA}"}}}`) + kiroSDKWrite(t, filepath.Join(pkg, "server.js"), "// Inert fixture; never executed.\n") + const foreign = `{"mcpServers":{"foreign":{"url":"https://foreign.test","disabled":true}},"policy":"keep"}` + kiroSDKWrite(t, filepath.Join(user, "settings", "mcp.json"), foreign) + a, b := newKiroSDKFixture(t, filepath.Join(base, "A"), user, pkg, helper), newKiroSDKFixture(t, filepath.Join(base, "B"), user, pkg, helper) + for _, f := range []*kiroSDKFixture{a, b} { + kiroSDKWrite(t, filepath.Join(f.project, ".kiro", "settings", "mcp.json"), foreign) + f.apply(t, installer.OpInstall, "install") + binding := f.binding(t) + if binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled || f.data == "" { + t.Fatalf("Kiro SDK overclaimed/omitted data: %+v", binding) + } + kiroSDKWrite(t, filepath.Join(f.data, "keep.txt"), f.project) + kiroSDKCheckStdio(t, f) + } + if a.data == b.data { + t.Fatal("project stdio data receipts alias") + } + beforeB := kiroSDKRead(t, filepath.Join(b.project, ".uap", "state-v2.json")) + bBinding := b.binding(t) + kiroSDKRepairUpdateRemove(t, a, pkg, helper, original) + if beforeB != kiroSDKRead(t, filepath.Join(b.project, ".uap", "state-v2.json")) || !reflect.DeepEqual(bBinding, b.binding(t)) { + t.Fatal("Kiro SDK changed project B ownership") + } + kiroSDKCheckStdio(t, b) + if kiroSDKRead(t, filepath.Join(user, "settings", "mcp.json")) != foreign { + t.Fatal("Kiro SDK changed global native configuration") + } + for _, f := range []*kiroSDKFixture{a, b} { + if kiroSDKRead(t, filepath.Join(f.data, "keep.txt")) != f.project { + t.Fatal("SDK removed scoped persistent data") + } + } +} + +func kiroSDKCheckStdio(t *testing.T, f *kiroSDKFixture) { + t.Helper() + binding := f.binding(t) + servers, _, _, _, err := kiro.ReadMCPConfig(filepath.Join(f.project, ".kiro", "settings", "mcp.json")) + if err != nil { + t.Fatal(err) + } + entry := servers["docs"].(map[string]any) + env := entry["env"].(map[string]any) + args := entry["args"].([]any) + if env["PLUGIN_ROOT"] != binding.TargetLocator || env["PLUGIN_DATA"] != f.data || len(args) < 7 || args[1] != binding.TargetLocator || args[2] != f.data || args[3] != f.data || args[4] != "data" || entry["cwd"] != nil || entry["type"] != nil { + t.Fatalf("SDK stdio binding = %+v", entry) + } + if kiroSDKRead(t, entry["command"].(string)) != "inert helper bytes; never executed" { + t.Fatal("SDK launcher did not come from injected trusted helper") + } +} + +func kiroSDKRepairUpdateRemove(t *testing.T, f *kiroSDKFixture, pkg, helper, original string) { + t.Helper() + binding := f.binding(t) + nativeSkill := filepath.Join(f.project, ".kiro", "skills", "docs", "SKILL.md") + if got := f.apply(t, installer.OpUpdate, "unchanged"); got.Mutated { + t.Fatalf("unchanged=%+v", got) + } + pinned, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"plugin.json", "mcp.json", "server.js"} { + kiroSDKWrite(t, filepath.Join(pinned, name), kiroSDKRead(t, filepath.Join(pkg, name))) + } + kiroSDKWrite(t, filepath.Join(pinned, "skills", "docs", "SKILL.md"), original) + kiroSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Advanced Kiro SDK fixture\n---\nUpdated bytes.\n") + if err := os.RemoveAll(filepath.Dir(nativeSkill)); err != nil { + t.Fatal(err) + } + f.request.PackageRoot, f.request.SourceRoot = pinned, pkg + f.apply(t, installer.OpRepair, "repair-installed-pin") + if kiroSDKRead(t, nativeSkill) != original { + t.Fatal("SDK repair followed moving source") + } + fresh := newKiroSDKFixture(t, f.project, f.user, pkg, helper) + fresh.data = f.data + *f = *fresh + f.apply(t, installer.OpUpdate, "update-after-restart") + if kiroSDKRead(t, nativeSkill) == original { + t.Fatal("SDK update failed to advance selected project") + } + kiroSDKCheckStdio(t, f) + view, err := f.engine.Inspect(t.Context()) + if err != nil || view.Recovery.Required { + t.Fatalf("Kiro SDK status=%+v,%v", view, err) + } + f.apply(t, installer.OpRemove, "remove") + if _, err := os.Stat(binding.TargetLocator); !os.IsNotExist(err) { + t.Fatalf("SDK managed artifact survived: %v", err) + } + if _, err := os.Stat(nativeSkill); !os.IsNotExist(err) { + t.Fatalf("SDK native skill survived: %v", err) + } + servers, _, _, _, err := kiro.ReadMCPConfig(filepath.Join(f.project, ".kiro", "settings", "mcp.json")) + if err != nil || servers["docs"] != nil || servers["foreign"] == nil { + t.Fatalf("SDK remove ownership=%+v,%v", servers, err) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_namespace.go b/install/integrationctl/agentplugins/installer/project_namespace.go new file mode 100644 index 000000000..5139cda47 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_namespace.go @@ -0,0 +1,99 @@ +package installer + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" +) + +type projectNamespaceGuard struct { + mu sync.Mutex + project, namespace os.FileInfo +} + +// CheckProjectNamespace validates the selected physical namespace without +// creating it. The CLI reuses this guard before environment locks and writes. +// Existing roots are pinned in memory and revalidated before apply/recovery. +func (e *Engine) CheckProjectNamespace() error { + if e == nil { + return fmt.Errorf("%w: installer engine is required", ErrInvalidConfig) + } + if e.cfg.ProjectRoot == "" { + return nil + } + if e.projectGuard == nil { + return fmt.Errorf("%w: project namespace guard is not configured", ErrInvalidConfig) + } + e.projectGuard.mu.Lock() + defer e.projectGuard.mu.Unlock() + return e.checkProjectNamespaceLocked() +} + +func (e *Engine) checkProjectNamespaceLocked() error { + root := e.cfg.ProjectRoot + if e.cfg.StateRoot != filepath.Join(root, ".uap") { + return fmt.Errorf("project mutable namespace must be the selected project's .uap directory") + } + project, err := projectDirectoryIdentity(root) + if err != nil { + return fmt.Errorf("project root must remain an existing real directory: %w", err) + } + if previous := e.projectGuard.project; previous != nil && !os.SameFile(previous, project) { + return fmt.Errorf("%w: selected project directory identity changed", ErrPlanChanged) + } + if err := pathpolicy.RequireContainedChild(root, e.cfg.StateRoot); err != nil { + return fmt.Errorf("unsafe project namespace: %w", err) + } + for _, child := range []string{e.cfg.StateFile, e.cfg.LockFile, e.cfg.OperationsDir, e.cfg.PluginDataBase, e.cfg.ManagedRoot} { + if err := pathpolicy.RequireContainedChild(e.cfg.StateRoot, child); err != nil { + return fmt.Errorf("unsafe project mutable path: %w", err) + } + } + if !e.cfg.temporaryRoot { + if err := pathpolicy.RequireContainedChild(e.cfg.StateRoot, e.cfg.TempRoot); err != nil { + return fmt.Errorf("unsafe project temporary path: %w", err) + } + } + namespace, err := projectDirectoryIdentity(e.cfg.StateRoot) + if err != nil && !os.IsNotExist(err) { + return err + } + if namespace != nil && !namespace.IsDir() { + return fmt.Errorf("project mutable namespace is not a directory") + } + if previous := e.projectGuard.namespace; previous != nil && + (namespace == nil || !os.SameFile(previous, namespace)) { + return fmt.Errorf("%w: project mutable namespace identity changed", ErrPlanChanged) + } + e.projectGuard.project, e.projectGuard.namespace = project, namespace + return nil +} + +// Windows Lstat defers file-ID lookup until SameFile and reopens its old path. +// Capture the identity from an open handle before retaining it across renames. +func projectDirectoryIdentity(path string) (os.FileInfo, error) { + before, err := os.Lstat(path) + if err != nil { + return nil, err + } + if !before.IsDir() || before.Mode()&os.ModeSymlink != 0 { + return nil, fmt.Errorf("project directory is not an exact real directory") + } + file, err := os.Open(path) + if err != nil { + return nil, err + } + identity, statErr := file.Stat() + closeErr := file.Close() + if err := errors.Join(statErr, closeErr); err != nil { + return nil, err + } + if !identity.IsDir() || !os.SameFile(before, identity) { + return nil, fmt.Errorf("%w: project directory changed while capturing identity", ErrPlanChanged) + } + return identity, nil +} diff --git a/install/integrationctl/agentplugins/installer/project_namespace_test.go b/install/integrationctl/agentplugins/installer/project_namespace_test.go new file mode 100644 index 000000000..28ac62016 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_namespace_test.go @@ -0,0 +1,129 @@ +package installer + +import ( + "errors" + "os" + "path/filepath" + "testing" +) + +func TestProjectNamespaceRejectsAnotherProjectsSymlink(t *testing.T) { + root := t.TempDir() + a, b := filepath.Join(root, "a"), filepath.Join(root, "b") + for _, dir := range []string{a, filepath.Join(b, ".uap")} { + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + } + marker := filepath.Join(b, ".uap", "foreign-marker") + if err := os.WriteFile(marker, []byte("preserve-project-b"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(b, ".uap"), filepath.Join(a, ".uap")); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if _, err := New(testConfig(t, Config{ProjectRoot: a, StateRoot: filepath.Join(a, ".uap")})); !errors.Is(err, ErrInvalidConfig) { + t.Fatalf("another project's namespace was accepted: %v", err) + } + if body, err := os.ReadFile(marker); err != nil || string(body) != "preserve-project-b" { + t.Fatalf("foreign project changed: %q, %v", body, err) + } + entries, err := os.ReadDir(filepath.Join(b, ".uap")) + if err != nil || len(entries) != 1 { + t.Fatalf("namespace rejection created effects: %v, %v", entries, err) + } +} + +func TestProjectNamespacePinsExistingDirectoryIdentity(t *testing.T) { + root := t.TempDir() + state := filepath.Join(root, ".uap") + if err := os.Mkdir(state, 0o700); err != nil { + t.Fatal(err) + } + engine, err := New(testConfig(t, Config{ProjectRoot: root, StateRoot: state})) + if err != nil { + t.Fatal(err) + } + if err := os.Rename(state, filepath.Join(root, ".uap-original")); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(state, 0o700); err != nil { + t.Fatal(err) + } + if err := engine.CheckProjectNamespace(); !errors.Is(err, ErrPlanChanged) { + t.Fatalf("replaced namespace identity was accepted: %v", err) + } + if _, err := engine.WithTemporaryRoot(t.TempDir()); !errors.Is(err, ErrPlanChanged) { + t.Fatalf("temporary preview rebound a replaced namespace: %v", err) + } +} + +func TestProjectNamespacePinsSelectedProjectIdentity(t *testing.T) { + base := t.TempDir() + root := filepath.Join(base, "project") + if err := os.Mkdir(root, 0700); err != nil { + t.Fatal(err) + } + engine, err := New(testConfig(t, Config{ProjectRoot: root, StateRoot: filepath.Join(root, ".uap")})) + if err != nil { + t.Fatal(err) + } + if err := os.Rename(root, filepath.Join(base, "original")); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(root, 0700); err != nil { + t.Fatal(err) + } + if err := engine.CheckProjectNamespace(); !errors.Is(err, ErrPlanChanged) { + t.Fatalf("replaced selected project identity was accepted: %v", err) + } + if _, err := os.Stat(filepath.Join(root, ".uap")); !os.IsNotExist(err) { + t.Fatalf("refused project replacement created mutable namespace: %v", err) + } +} + +func TestProjectNamespacePreviewViewsSharePhysicalAuthority(t *testing.T) { + for _, kind := range []string{"projected engine", "temporary root"} { + t.Run(kind, func(t *testing.T) { + root := t.TempDir() + namespace := filepath.Join(root, ".uap") + engine, err := New(testConfig(t, Config{ProjectRoot: root, StateRoot: namespace})) + if err != nil { + t.Fatal(err) + } + projected := *engine // PreviewInstallAfterUpdate retains this authority. + view := &projected + if kind == "temporary root" { + view, err = engine.WithTemporaryRoot(t.TempDir()) + if err != nil { + t.Fatal(err) + } + } + if err := os.Mkdir(namespace, 0700); err != nil { + t.Fatal(err) + } + if err := view.CheckProjectNamespace(); err != nil { + t.Fatal(err) + } + if err := os.Rename(namespace, filepath.Join(root, ".uap-original")); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(namespace, 0700); err != nil { + t.Fatal(err) + } + for _, observer := range []*Engine{engine, view} { + if err := observer.CheckProjectNamespace(); !errors.Is(err, ErrPlanChanged) { + t.Fatalf("preview view reset selected namespace authority: %v", err) + } + } + }) + } +} + +func TestProjectNamespaceRejectsUninitializedGuard(t *testing.T) { + for _, engine := range []*Engine{nil, {cfg: Config{ProjectRoot: t.TempDir()}}} { + if err := engine.CheckProjectNamespace(); !errors.Is(err, ErrInvalidConfig) { + t.Fatalf("invalid project engine accepted: %v", err) + } + } +} diff --git a/install/integrationctl/agentplugins/installer/project_process_death_fixture_test.go b/install/integrationctl/agentplugins/installer/project_process_death_fixture_test.go new file mode 100644 index 000000000..50b55de68 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_process_death_fixture_test.go @@ -0,0 +1,344 @@ +package installer + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + processadapter "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +const ( + deathManagedBoundary = "managed_package_committed_before_native_activation" + deathNativeBoundary = "native_skill_swapped_before_mcp_lock_and_commit" + deathOldVersion = "inert version one" + deathNewVersion = "inert version two" + deathComponent = "sample-notify" +) + +type deathFixtureRunner struct{ calls int } + +func (r *deathFixtureRunner) Run(ctx context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return (processadapter.OS{}).Run(ctx, command) +} + +// Only the child uses this wrapper. All scope, ownership, projection and native +// mutation behavior delegates to the real adapter. The existing primitive seam +// pauses after the real native skill swap and before the MCP writer lock/write. +type deathBoundaryCodex struct { + *codex.Adapter + beforeMCPLock func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) +} + +func (a deathBoundaryCodex) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + env.NativeConfig = nativeconfig.NewWithLockAcquirer(a.beforeMCPLock) + return a.Adapter.Activate(ctx, env, request) +} + +func deathEngine(t *testing.T, base, project string, adapter clients.Adapter, committed func(context.Context, BindingFacts) error) (*Engine, *deathFixtureRunner) { + t.Helper() + if adapter == nil { + adapter = codex.New() + } + registry, err := clients.NewRegistry(adapter) + if err != nil { + t.Fatal(err) + } + state := filepath.Join(base, "user-uap") + if project != "" { + state = filepath.Join(project, ".uap") + } + runner := &deathFixtureRunner{} + engine, err := New(Config{ProjectRoot: project, StateRoot: state, Registry: registry, + Runner: runner, EnableNativeObserver: true, TrustedLocalPackages: true, + HelperExecutable: filepath.Join(base, "codex-fixture"), OnCommittedBinding: committed}) + if err != nil { + t.Fatal(err) + } + return engine, runner +} + +func deathWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func deathRead(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} + +func deathSkill(name, version string) string { + return "---\nname: " + name + "\ndescription: Inert process-death fixture\n---\n" + version + "\n" +} + +func deathPackage(t *testing.T, base, name, version string) string { + t.Helper() + root := filepath.Join(base, "package-"+name) + deathWrite(t, filepath.Join(root, "plugin.json"), fmt.Sprintf(`{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":%q,"version":"1.0.0"}`, name)) + deathWrite(t, filepath.Join(root, "skills", name, "SKILL.md"), deathSkill(name, version)) + deathWrite(t, filepath.Join(root, "mcp.json"), fmt.Sprintf(`{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{%q:{"type":"stdio","command":"./bin/probe","args":[%q],"env":{}}}}`, name, version)) + deathWrite(t, filepath.Join(root, "bin", "probe"), deathRead(t, filepath.Join(base, "codex-fixture"))) + if err := os.Chmod(filepath.Join(root, "bin", "probe"), 0700); err != nil { + t.Fatal(err) + } + return root +} + +func deathRequest(t *testing.T, engine *Engine, base, project string, op Operation) Request { + t.Helper() + scope, owner := domain.ScopeUser, "user-default" + if project != "" { + scope, owner = domain.ScopeProject, "project-default" + } + // Direct local SDK packages have the existing public source-key fallback + // github::; this fixture fetches no GitHub source or vendor content. + req := Request{Scope: scope, ProjectRoot: project, Operation: op, + PackageRoot: filepath.Join(base, "package-"+deathComponent), ClientID: "codex", + ClientConfigRoot: filepath.Join(base, "user", ".codex"), ClientExecutable: filepath.Join(base, "codex-fixture"), + RequiredComponents: []string{"skills", "mcp"}, OperationID: "process-death-" + string(op), + EnvironmentAuthority: &domain.EnvironmentAuthority{EnvironmentID: owner, Alias: deathComponent, ClientID: "codex", ExpectedAction: string(op), SourceKey: "github::"}} + if op != OpInstall { + view, err := engine.Inspect(t.Context()) + if err != nil { + t.Fatal(err) + } + if len(view.Installations) != 1 || len(view.Installations[0].Bindings) != 1 { + t.Fatalf("unexpected process-death baseline: %+v", view) + } + binding := view.Installations[0].Bindings[0] + if binding.Owner == nil { + t.Fatal("fixture installation lacks a real environment receipt") + } + req.InstallationID = view.Installations[0].InstallationID + req.EnvironmentAuthority.ReceiptID = binding.Owner.ReceiptID + req.EnvironmentAuthority.InstallationID = req.InstallationID + req.EnvironmentAuthority.ClientBindingID = binding.BindingID + } + return req +} + +func deathApply(t *testing.T, engine *Engine, req Request) Result { + t.Helper() + prepared, err := engine.Prepare(t.Context(), req) + if err != nil { + t.Fatalf("prepare %s: %v", req.Operation, err) + } + defer func() { + if err := prepared.Close(); err != nil { + t.Error(err) + } + }() + result, err := engine.Apply(t.Context(), prepared, Decision{Confirmed: true}) + if err != nil || result.Outcome != OutcomeCompleted { + t.Fatalf("apply %s: %+v, %v", req.Operation, result, err) + } + return result +} + +func deathBinding(t *testing.T, base, project string) domain.ClientBinding { + t.Helper() + root := filepath.Join(base, "user-uap") + if project != "" { + root = filepath.Join(project, ".uap") + } + state, err := (statev2.Store{Path: filepath.Join(root, "state-v2.json"), ProjectRoot: project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("read genuine fixture state: %+v, %v", state, err) + } + for _, binding := range state.Installations[0].Clients { + return binding + } + t.Fatal("fixture binding absent") + return domain.ClientBinding{} +} + +func deathNativeObjects(binding domain.ClientBinding) []domain.NativeObjectOwnership { + var out []domain.NativeObjectOwnership + for _, object := range binding.NativeObjects { + if object.Kind != "managed_package_directory" { + out = append(out, object) + } + } + return out +} + +type deathTreeEntry struct { + Mode os.FileMode + Hash string +} + +func deathTree(t *testing.T, root string) map[string]deathTreeEntry { + t.Helper() + out := map[string]deathTreeEntry{} + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + fact := deathTreeEntry{Mode: info.Mode()} + if info.Mode()&os.ModeSymlink != 0 { + fact.Hash, err = os.Readlink(path) + } else if !entry.IsDir() { + var file *os.File + file, err = os.Open(path) + if err == nil { + hash := sha256.New() + _, readErr := io.Copy(hash, file) + err = file.Close() + if readErr != nil { + return readErr + } + fact.Hash = fmt.Sprintf("%x", hash.Sum(nil)) + } + } + out[relative] = fact + return err + }) + if err != nil { + t.Fatal(err) + } + return out +} + +func deathAssertTree(t *testing.T, root string, before map[string]deathTreeEntry) { + t.Helper() + if !reflect.DeepEqual(before, deathTree(t, root)) { + t.Fatalf("process death/reopen changed retained tree %s", root) + } +} + +type deathBarrier struct { + Boundary, TargetPath, AttemptID, BindingID, Backup string +} + +func deathProveBoundary(t *testing.T, base, boundary string, before domain.ClientBinding, oldConfig string) deathBarrier { + t.Helper() + project := filepath.Join(base, "A") + clientID, profile, skillRoot, config := "codex", project, filepath.Join(project, ".agents", "skills"), filepath.Join(project, ".codex", "config.toml") + if before.ClientID == "vscode" { + clientID, profile, skillRoot, config = "vscode", filepath.Join(project, ".vscode"), filepath.Join(project, ".github", "skills"), filepath.Join(project, ".vscode", "mcp.json") + } + binding := deathBinding(t, base, project) + if binding.Scope != string(domain.ScopeProject) || binding.ClientID != clientID || binding.NativeProfileRoot != profile || binding.ClientBindingID != before.ClientBindingID || binding.TargetLocator != before.TargetLocator || binding.DataReceiptID == "" || binding.DataReceiptID != before.DataReceiptID { + t.Fatalf("boundary changed selected scope, binding or data authority: %+v", binding) + } + if binding.NativeActivationAttempt == "" || binding.PendingNativeIntent != nil || recordedBindingDigest(binding, "") == "" || recordedBindingDigest(binding, "") == recordedBindingDigest(before, "") { + t.Fatalf("boundary lacks durable changed package/attempt identity: %+v", binding) + } + if !reflect.DeepEqual(deathNativeObjects(binding), deathNativeObjects(before)) { + t.Fatal("boundary acknowledged new native ownership before the native commit") + } + if deathRead(t, filepath.Join(binding.TargetLocator, "skills", deathComponent, "SKILL.md")) != deathSkill(deathComponent, deathNewVersion) { + t.Fatal("managed package was not replaced before the barrier") + } + if deathRead(t, config) != oldConfig { + t.Fatal("MCP config changed before its admitted commit boundary") + } + native := filepath.Join(skillRoot, deathComponent, "SKILL.md") + want := deathOldVersion + barrier := deathBarrier{Boundary: boundary, TargetPath: binding.TargetLocator, AttemptID: binding.NativeActivationAttempt, BindingID: binding.ClientBindingID} + backups, err := filepath.Glob(filepath.Join(skillRoot, ".agentplugins-native-*")) + if err != nil { + t.Fatal(err) + } + if boundary == deathNativeBoundary { + want = deathNewVersion + if len(backups) != 1 { + t.Fatalf("real native swap did not retain exactly one recovery root: %v", backups) + } + barrier.Backup = backups[0] + if deathRead(t, filepath.Join(barrier.Backup, "old-"+deathComponent, "SKILL.md")) != deathSkill(deathComponent, deathOldVersion) { + t.Fatal("real native swap lost the old skill backup") + } + } else if boundary != deathManagedBoundary || len(backups) != 0 { + t.Fatalf("unexpected managed-only boundary/backups: %q %v", boundary, backups) + } + if deathRead(t, native) != deathSkill(deathComponent, want) { + t.Fatalf("native skill differs at %s", boundary) + } + return barrier +} + +func deathEmitAndBlock(t *testing.T, barrier deathBarrier) { + t.Helper() + if err := json.NewEncoder(os.Stdout).Encode(barrier); err != nil { + t.Fatal(err) + } + var release [1]byte + _, err := os.Stdin.Read(release[:]) + t.Fatalf("parent did not kill the child at its barrier: %v", err) +} + +// Abrupt process death intentionally leaves the sealed acquisition snapshot in +// place. Keep its original modes for every ownership/receipt/state assertion; +// only testing's final cleanup may regain owner-write on its exact directories. +// No file bytes, native files, binding facts or production permissions change. +func deathAllowSnapshotCleanup(t *testing.T, base string) { + t.Helper() + tmp := filepath.Join(base, "A", ".uap", "tmp") + info, err := os.Lstat(tmp) + if os.IsNotExist(err) { + return + } + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + t.Errorf("process-death snapshot cleanup root is not ordinary: %v", err) + return + } + entries, err := os.ReadDir(tmp) + if err != nil { + t.Error(err) + return + } + for _, entry := range entries { + if !strings.HasPrefix(entry.Name(), "agentplugins-package-") || !entry.IsDir() { + continue + } + root := filepath.Join(tmp, entry.Name()) + if err := filepath.WalkDir(root, func(path string, item os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if !item.IsDir() { // WalkDir never follows a leaf or intermediate link. + return nil + } + info, err := item.Info() + if err != nil { + return err + } + return os.Chmod(path, info.Mode().Perm()|0200) + }); err != nil { + t.Errorf("restore only disposable snapshot directory write bits: %v", err) + } + } +} diff --git a/install/integrationctl/agentplugins/installer/project_process_death_test.go b/install/integrationctl/agentplugins/installer/project_process_death_test.go new file mode 100644 index 000000000..d8a2b0179 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_process_death_test.go @@ -0,0 +1,283 @@ +package installer + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" +) + +// The parent kills a real SDK child without unwinding its callbacks. Client +// commands use only the existing inert Codex fixture, never a vendor binary or +// MCP server. Each reopened engine uses the ordinary Codex adapter. +func TestProjectProcessDeathReopenRetainsUnacknowledgedNativeAttempt(t *testing.T) { + skipWindowsLauncherExecuteBit(t) + probe := buildCodexProbe(t) + for _, boundary := range []string{deathManagedBoundary, deathNativeBoundary} { + t.Run(boundary, func(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + // Register after TempDir so this runs only after all test assertions, + // immediately before testing removes its disposable root. A killed + // SDK child cannot unseal its package snapshots through deferred Close. + t.Cleanup(func() { deathAllowSnapshotCleanup(t, base) }) + deathWrite(t, filepath.Join(base, "codex-fixture"), deathRead(t, probe)) + if err := os.Chmod(filepath.Join(base, "codex-fixture"), 0700); err != nil { + t.Fatal(err) + } + deathPackage(t, base, deathComponent, deathOldVersion) + freshPackage := deathPackage(t, base, "fresh-death", deathOldVersion) + a, b := filepath.Join(base, "A"), filepath.Join(base, "B") + user := filepath.Join(base, "user", ".codex") + deathWrite(t, filepath.Join(user, "config.toml"), "# original disposable user profile\n") + for _, project := range []string{a, b} { + deathWrite(t, filepath.Join(project, ".codex", "config.toml"), "foreign_setting = true\n") + deathWrite(t, filepath.Join(project, ".agents", "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + var aEngine *Engine + for _, project := range []string{"", a, b} { + engine, _ := deathEngine(t, base, project, nil, nil) + result := deathApply(t, engine, deathRequest(t, engine, base, project, OpInstall)) + if result.Binding.DataRoot == "" { + t.Fatal("public install omitted owned data authority") + } + deathWrite(t, filepath.Join(result.Binding.DataRoot, "keep.txt"), "retained owner data\n") + if project == a { + aEngine = engine + } + } + deathWrite(t, filepath.Join(user, "config.toml"), deathRead(t, filepath.Join(user, "config.toml"))+"\n[foreign_fixture]\nkeep = true\n") + deathWrite(t, filepath.Join(user, "foreign.txt"), "foreign user bytes\n") + before := deathBinding(t, base, a) + update := deathRequest(t, aEngine, base, a, OpUpdate) + oldConfig := deathRead(t, filepath.Join(a, ".codex", "config.toml")) + userNative, userState, bTree := deathTree(t, user), deathTree(t, filepath.Join(base, "user-uap")), deathTree(t, b) + deathPackage(t, base, deathComponent, deathNewVersion) + barrier := deathKillAtBarrier(t, base, boundary, func(observed deathBarrier) { + expected := deathProveBoundary(t, base, boundary, before, oldConfig) + if !reflect.DeepEqual(observed, expected) { + t.Fatalf("child barrier differs from independently read persisted facts: %+v, want %+v", observed, expected) + } + }) + if barrier.Boundary != boundary { + t.Fatal("wrong child boundary") + } + deathProveBoundary(t, base, boundary, before, oldConfig) + retained := deathTree(t, a) + hostCalls := 0 + fresh, runner := deathEngine(t, base, a, nil, func(context.Context, BindingFacts) error { hostCalls++; return nil }) + view, inspectErr := fresh.Inspect(t.Context()) + const uncertainty = "native attempt has no persisted intent; retain uncertainty" + if inspectErr == nil || !view.Recovery.Required || view.Recovery.Reason != uncertainty || len(view.Installations) != 1 || len(view.Installations[0].Bindings) != 1 { + t.Fatalf("fresh read-only inspect lost pending facts: %+v, %v", view, inspectErr) + } + deathAssertTree(t, a, retained) + for _, operation := range []Operation{OpUpdate, OpRepair, OpRemove, OpInstall} { + req := update + authority := *update.EnvironmentAuthority + req.EnvironmentAuthority = &authority + req.Operation, req.OperationID = operation, "fenced-after-death-"+string(operation) + authority.ExpectedAction = string(operation) + if operation == OpInstall { + req.PackageRoot, req.InstallationID = freshPackage, "" + authority.Alias, authority.ReceiptID, authority.InstallationID, authority.ClientBindingID = "fresh-death", "", "", "" + } + handle, err := fresh.Prepare(t.Context(), req) + if err == nil { + result, applyErr := fresh.Apply(t.Context(), handle, Decision{Confirmed: true}) + err = applyErr + if result.Mutated || result.Outcome != OutcomeRecovery { + t.Fatalf("ordinary %s was not fenced: %+v, %v", operation, result, err) + } + } + if handle != nil { + if closeErr := handle.Close(); closeErr != nil { + t.Fatal(closeErr) + } + } + blocked := errors.Is(err, ErrRecoveryRequired) || err != nil && strings.Contains(err.Error(), "native activation attempt") && strings.Contains(err.Error(), "unresolved") + if operation == OpRemove && errors.Is(err, ErrInvalidRequest) && strings.Contains(err.Error(), "removal binding scope or native attempt differs") { + // The exact selected scope/binding was proven above; the real + // removal preflight refuses its retained native attempt earlier. + blocked = true + } + if !blocked { + t.Fatalf("ordinary %s refused for an unrelated reason: %v", operation, err) + } + deathAssertTree(t, a, retained) + } + if result, err := fresh.RecoverEnvironment(t.Context(), view, "project-default"); !errors.Is(err, ErrRecoveryRequired) || result.Outcome != OutcomeRecovery { + t.Fatalf("zero-selected delivery guessed native recovery: %+v, %v", result, err) + } + other, _ := deathEngine(t, base, b, nil, nil) + if _, err := other.RecoverEnvironment(t.Context(), view, "project-default"); !errors.Is(err, ErrPlanChanged) { + t.Fatalf("project B accepted project A recovery scope: %v", err) + } + again, err := fresh.Inspect(t.Context()) + if err == nil || !reflect.DeepEqual(view, again) || runner.calls != 0 || hostCalls != 0 { + t.Fatalf("inspect/retry/recovery changed pending facts or called host/native process: %+v, %v; runner=%d host=%d", again, err, runner.calls, hostCalls) + } + deathAssertTree(t, a, retained) + deathAssertTree(t, user, userNative) + deathAssertTree(t, filepath.Join(base, "user-uap"), userState) + deathAssertTree(t, b, bTree) + deathProveBoundary(t, base, boundary, before, oldConfig) + }) + } +} + +func TestProjectProcessDeathChild(t *testing.T) { + var args []string + for i, arg := range os.Args { + if arg == "--" { + args = os.Args[i+1:] + break + } + } + if len(args) == 0 { + t.Skip("disposable parent-controlled SDK child only") + } + if len(args) != 2 || args[1] != deathManagedBoundary && args[1] != deathNativeBoundary { + t.Fatal("invalid process-death fixture arguments") + } + base, boundary := args[0], args[1] + project := filepath.Join(base, "A") + before := deathBinding(t, base, project) + oldConfig := deathRead(t, filepath.Join(project, ".codex", "config.toml")) + var adapter clients.Adapter + var committed func(context.Context, BindingFacts) error + if boundary == deathManagedBoundary { + committed = func(_ context.Context, facts BindingFacts) error { + if deathRead(t, filepath.Join(facts.TargetPath, "skills", deathComponent, "SKILL.md")) != deathSkill(deathComponent, deathNewVersion) { + return nil // The old Prepared host handoff precedes this replacement. + } + deathEmitAndBlock(t, deathProveBoundary(t, base, boundary, before, oldConfig)) + return fmt.Errorf("unreachable parent-controlled boundary") + } + } else { + adapter = deathBoundaryCodex{Adapter: codex.New(), beforeMCPLock: func(paths nativeconfig.Paths, codec nativeconfig.Codec) (func() error, error) { + if codec != nativeconfig.CodecCodex || paths.JSON != filepath.Join(project, ".codex", "config.toml") || paths.JSONC != "" { + t.Fatalf("primitive boundary selected another codec/path: %+v, %s", paths, codec) + } + deathEmitAndBlock(t, deathProveBoundary(t, base, boundary, before, oldConfig)) + return nil, fmt.Errorf("unreachable parent-controlled boundary") + }} + } + engine, _ := deathEngine(t, base, project, adapter, committed) + deathApply(t, engine, deathRequest(t, engine, base, project, OpUpdate)) + t.Fatal("child SDK update completed without reaching the selected kill boundary") +} + +func deathKillAtBarrier(t *testing.T, base, boundary string, verify func(deathBarrier)) deathBarrier { + t.Helper() + return deathKillAtBarrierChild(t, "TestProjectProcessDeathChild", base, boundary, verify) +} + +func deathKillAtBarrierChild(t *testing.T, childTest, base, boundary string, verify func(deathBarrier)) deathBarrier { + t.Helper() + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + command := exec.CommandContext(ctx, executable, "-test.run=^"+childTest+"$", "-test.timeout=25s", "--", base, boundary) + stdin, err := command.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := command.StdoutPipe() + if err != nil { + t.Fatal(err) + } + var stderr deathChildOutput + command.Stderr = &stderr + if err := command.Start(); err != nil { + t.Fatal(err) + } + waited := false + defer func() { + if !waited { + _ = command.Process.Kill() + _ = stdin.Close() + _ = command.Wait() + } else { + _ = stdin.Close() + } + }() + type response struct { + barrier deathBarrier + err error + } + ready := make(chan response, 1) + go func() { + line, err := bufio.NewReader(io.LimitReader(stdout, 4096)).ReadBytes('\n') + var barrier deathBarrier + if err == nil { + err = json.Unmarshal(line, &barrier) + } + ready <- response{barrier, err} + }() + select { + case observed := <-ready: + if observed.err != nil { + t.Fatalf("child did not reach %s: %v; stderr=%s", boundary, observed.err, stderr.String()) + } + verify(observed.barrier) + if err := command.Process.Kill(); err != nil { + t.Fatal(err) + } + err := command.Wait() + waited = true + var exit *exec.ExitError + if !errors.As(err, &exit) || command.ProcessState.Success() { + t.Fatalf("child was not killed ungracefully: %v; stderr=%s", err, stderr.String()) + } + return observed.barrier + case <-ctx.Done(): + t.Fatalf("child kill boundary deadline: %v; stderr=%s", ctx.Err(), stderr.String()) + } + return deathBarrier{} +} + +// A failed child must not race diagnostic reads or grow parent memory unbounded. +type deathChildOutput struct { + mu sync.Mutex + body bytes.Buffer +} + +func (out *deathChildOutput) Write(body []byte) (int, error) { + out.mu.Lock() + defer out.mu.Unlock() + size := len(body) + remaining := 64*1024 - out.body.Len() + if remaining > 0 { + if len(body) > remaining { + body = body[:remaining] + } + _, _ = out.body.Write(body) + } + return size, nil +} +func (out *deathChildOutput) String() string { + out.mu.Lock() + defer out.mu.Unlock() + return out.body.String() +} diff --git a/install/integrationctl/agentplugins/installer/project_qoder_test.go b/install/integrationctl/agentplugins/installer/project_qoder_test.go new file mode 100644 index 000000000..022876592 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_qoder_test.go @@ -0,0 +1,254 @@ +package installer_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/filetree" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/qoder" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type qoderSDKNoNative struct{ calls int } + +func (r *qoderSDKNoNative) Run(_ context.Context, c legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("forbidden Qoder native command %v", c.Argv) +} + +type qoderSDKFixture struct { + project, user, pkg, pin string + sequence int + engine *installer.Engine + request installer.Request +} + +func newQoderSDKFixture(t *testing.T, project, user, pkg string, enabled bool, runner *qoderSDKNoNative) *qoderSDKFixture { + t.Helper() + registry, err := clients.NewRegistry(qoder.New()) + if err != nil { + t.Fatal(err) + } + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + config := installer.Config{ProjectRoot: project, StateRoot: filepath.Join(project, ".uap"), Registry: registry, EnableNativeObserver: enabled, TrustedLocalPackages: true, HelperExecutable: helper} + if enabled { + config.Runner = runner + } + engine, err := installer.New(config) + if err != nil { + t.Fatal(err) + } + return &qoderSDKFixture{project: project, user: user, pkg: pkg, engine: engine, request: installer.Request{Scope: domain.ScopeProject, ProjectRoot: project, Operation: installer.OpInstall, PackageRoot: pkg, ClientID: "qoder", ClientConfigRoot: user, InstallationID: "00000000-0000-4000-8000-000000000078", OperationID: "qoder-project", RequiredComponents: []string{"skills"}}} +} +func (f *qoderSDKFixture) execute(t *testing.T, req installer.Request) (installer.Result, error) { + t.Helper() + f.sequence++ + req.OperationID = fmt.Sprintf("qoder-%s-%d", req.Operation, f.sequence) + handle, err := f.engine.Prepare(t.Context(), req) + if err != nil { + return installer.Result{}, err + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + return f.engine.Apply(t.Context(), handle, installer.Decision{Confirmed: true}) +} +func (f *qoderSDKFixture) apply(t *testing.T, op installer.Operation) installer.Result { + t.Helper() + req := f.request + req.Operation, req.OperationID = op, "qoder-"+string(op) + if op == installer.OpRepair && f.pin != "" { + req.PackageRoot = f.pin + } + out, err := f.execute(t, req) + if err != nil || out.Outcome != installer.OutcomeCompleted && out.Outcome != installer.OutcomeUnchanged { + t.Fatalf("%s: %+v %v", op, out, err) + } + return out +} +func (f *qoderSDKFixture) state(t *testing.T) string { + t.Helper() + body, err := os.ReadFile(filepath.Join(f.project, ".uap", "state-v2.json")) + if os.IsNotExist(err) { + return "" + } + if err != nil { + t.Fatal(err) + } + return string(body) +} +func (f *qoderSDKFixture) binding(t *testing.T) domain.ClientBinding { + t.Helper() + state, err := (statev2.Store{Path: filepath.Join(f.project, ".uap", "state-v2.json"), ProjectRoot: f.project}).Load() + if err != nil || len(state.Installations) != 1 { + t.Fatalf("state %+v %v", state, err) + } + if len(state.Installations[0].DataReceipts) != 0 { + t.Fatal("unselected MCP created persistent data ownership") + } + for _, b := range state.Installations[0].Clients { + return b + } + t.Fatal("missing binding") + return domain.ClientBinding{} +} +func qoderSDKTree(t *testing.T, root string) map[string]string { + t.Helper() + out := map[string]string{} + err := filepath.WalkDir(root, func(path string, e os.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + if e.IsDir() { + out[rel] = "directory" + return nil + } + body, err := os.ReadFile(path) + if err != nil { + return err + } + out[rel] = string(body) + return nil + }) + if err != nil { + t.Fatal(err) + } + return out +} +func TestProjectQoderPublicPreparedLifecycleNeverCallsNative(t *testing.T) { + for _, enabled := range []bool{false, true} { + t.Run(fmt.Sprintf("observer-%t", enabled), func(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + a, b, user, pkg := filepath.Join(base, "A"), filepath.Join(base, "B"), filepath.Join(base, "home", ".qoder"), filepath.Join(base, "package") + for _, root := range []string{a, b, user} { + claudeSDKWrite(t, filepath.Join(root, "keep"), "original foreign") + } + claudeSDKWrite(t, filepath.Join(user, "plugins", "installed_plugins_v2.json"), `{"foreign":"original global native state"}`) + claudeSDKWrite(t, filepath.Join(pkg, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"sdk-qoder","version":"1.0.0"}`) + skill := "---\nname: docs\ndescription: Qoder Prepared fixture\n---\nPinned original.\n" + claudeSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), skill) + claudeSDKWrite(t, filepath.Join(pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"never","args":["${PLUGIN_DATA}"]}}}`) + runner := &qoderSDKNoNative{} + other := newQoderSDKFixture(t, b, user, pkg, enabled, runner) + other.apply(t, installer.OpInstall) + bBefore, userBefore := qoderSDKTree(t, b), qoderSDKTree(t, user) + f := newQoderSDKFixture(t, a, user, pkg, enabled, runner) + reject := func(op installer.Operation) { + t.Helper() + req := f.request + req.Operation, req.ActivationComplete = op, true + before := f.state(t) + if out, err := f.execute(t, req); err == nil { + t.Fatalf("attestation accepted %+v", out) + } + if f.state(t) != before || runner.calls != 0 { + t.Fatal("refusal changed state or invoked native") + } + } + reject(installer.OpInstall) + installed := f.apply(t, installer.OpInstall) + if !installed.Mutated { + t.Fatal("first installation lost mutation") + } + binding := f.binding(t) + if binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled || binding.NativeProfileRoot != filepath.Join(a, ".uap", "managed", "clients", "qoder") || len(binding.NativeObjects) != 1 || binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil { + t.Fatalf("bundle claimed native ownership %+v", binding) + } + view := filepath.Join(binding.TargetLocator, ".agentplugins-qoder-native") + if claudeSDKBytes(t, filepath.Join(view, "skills", "docs", "SKILL.md")) != skill { + t.Fatal("actual native view missing") + } + if !strings.Contains(strings.Join(installed.ManualActions, " "), view) { + t.Fatal("native import boundary not retained", installed.ManualActions) + } + data := filepath.Join(a, ".uap", "caller-data", "keep") + claudeSDKWrite(t, data, "persistent caller data") + for _, op := range []installer.Operation{installer.OpInstall, installer.OpUpdate, installer.OpRepair} { + reject(op) + before := f.state(t) + out := f.apply(t, op) + if out.Mutated || !out.NoChange || before != f.state(t) { + t.Fatalf("intact Prepared %s not stable %+v", op, out) + } + } + sequence := f.sequence + f = newQoderSDKFixture(t, a, user, pkg, enabled, runner) + f.sequence = sequence + beforeCalls := runner.calls + status, err := f.engine.Inspect(t.Context()) + if err != nil || status.Recovery.Required || runner.calls != beforeCalls { + t.Fatalf("status not host-free %+v %v", status, err) + } + // The public SDK requires an explicit package root. Its caller supplies + // an immutable captured pin; the saved CLI separately tests cache resolution. + pin := filepath.Join(base, "immutable-pin") + if err := filetree.CopyDir(pkg, pin); err != nil { + t.Fatal(err) + } + f.pin = pin + advanced := strings.Replace(skill, "Pinned original.", "Advanced source.", 1) + claudeSDKWrite(t, filepath.Join(pkg, "skills", "docs", "SKILL.md"), advanced) + req := f.request + req.Operation = installer.OpRepair + stateBefore := f.state(t) + if out, err := f.execute(t, req); err == nil || !strings.Contains(err.Error(), "install cannot change an active revision") || out.Mutated || stateBefore != f.state(t) { + t.Fatalf("advanced explicit SDK repair root bypassed revision guard: %+v %v", out, err) + } + claudeSDKWrite(t, filepath.Join(view, "skills", "docs", "SKILL.md"), "outside drift") + if out := f.apply(t, installer.OpRepair); !out.Mutated { + t.Fatal("drift repair hid mutation") + } + if got := f.binding(t); got.Activation != domain.ActivationPrepared || got.Verification != domain.VerificationInstalled { + t.Fatalf("repaired file proof lost: %+v", got) + } + if claudeSDKBytes(t, filepath.Join(view, "skills", "docs", "SKILL.md")) != skill { + t.Fatal("repair followed later source") + } + if err := os.RemoveAll(binding.TargetLocator); err != nil { + t.Fatal(err) + } + f.apply(t, installer.OpRepair) + if claudeSDKBytes(t, filepath.Join(view, "skills", "docs", "SKILL.md")) != skill { + t.Fatal("missing repair followed later source") + } + if out := f.apply(t, installer.OpUpdate); !out.Mutated { + t.Fatal("changed source update hid mutation") + } + if claudeSDKBytes(t, filepath.Join(view, "skills", "docs", "SKILL.md")) != advanced { + t.Fatal("update did not deliver current source") + } + current := f.binding(t) + if current.ClientBindingID != binding.ClientBindingID || current.TargetLocator != binding.TargetLocator { + t.Fatal("update changed exact authority") + } + f.apply(t, installer.OpRemove) + if _, err := os.Lstat(binding.TargetLocator); !os.IsNotExist(err) { + t.Fatal("bundle survived removal", err) + } + if claudeSDKBytes(t, data) != "persistent caller data" || runner.calls != 0 || !reflect.DeepEqual(userBefore, qoderSDKTree(t, user)) || !reflect.DeepEqual(bBefore, qoderSDKTree(t, b)) { + t.Fatal("project operation changed another authority or native runtime") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_remove_preflight.go b/install/integrationctl/agentplugins/installer/project_remove_preflight.go new file mode 100644 index 000000000..03e63d440 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_remove_preflight.go @@ -0,0 +1,56 @@ +package installer + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +// Observe the recorded native file set at each existing removal fence. Empty +// Components deliberately asks the adapter to verify prior ownership only; +// there is no new desired projection, native discovery or command execution. +func (e *Engine) confirmProjectRemovalFiles(ctx context.Context, client domain.DetectedClient, name string, binding domain.ClientBinding, target domain.DeliveryTarget, digest string) error { + if e.scope() != domain.ScopeProject || !binding.SelectedDelivery.IsZero() || + binding.SelectedDelivery.EffectiveTraitsForScope(client.ClientID, e.scope()).LifecycleKind != domain.LifecycleNativeConfig { + return nil + } + plan := domain.DeliveryPlan{ + ClientID: client.ClientID, Scope: domain.ScopeProject, DeclaredName: name, + TargetContext: target.TargetContext, TargetAnchor: target.TargetAnchor, + TargetRoot: target.TargetRoot, ActivePath: target.ActivePath, NativeRegistryRoot: target.NativeRegistryRoot, + PhysicalArtifactID: binding.PhysicalArtifact, SelectedDelivery: binding.SelectedDelivery, + PreviousNativeObjects: append([]domain.NativeObjectOwnership(nil), binding.NativeObjects...), + LocalEntryObservation: binding.LocalEntryObservation.Clone(), + }.WithProfileAuthority(target.ProfileAuthority(), target.ProfileNamespace()) + client.ProfileAuthority = domain.CloneProfileAuthority(client.ProfileAuthority) + managed := projectRemovalObservationBinding(binding) + kernel := nativeconfig.New() + observer := providers.NativeIdentityObserver{Stager: providers.Stager{}, NativeConfig: &kernel, Registry: e.cfg.Registry} + observed, err := observer.ObservePreparedIdentity(ctx, client, plan, &managed) + if err != nil { + return fmt.Errorf("%w: project native file removal preflight: %w", ErrInvalidRequest, err) + } + if observed.State != domain.NativeIdentityManaged || !observed.ReceiptReconciled || observed.Digest != digest || + observed.NativeDiscoveryState != domain.NativeIdentityAbsent || observed.NativeDiscoveryAttempted || observed.NativeDiscoveryReconciled { + return fmt.Errorf("%w: project native file ownership could not be verified before removal", ErrInvalidRequest) + } + return nil +} + +// Give the inspector the saved identity and complete native ownership, without +// sharing mutable state/history metadata with the frozen comparison basis. +func projectRemovalObservationBinding(binding domain.ClientBinding) domain.ClientBinding { + return domain.ClientBinding{ + ClientBindingID: binding.ClientBindingID, ClientID: binding.ClientID, Scope: binding.Scope, + TargetLocator: binding.TargetLocator, PhysicalArtifact: binding.PhysicalArtifact, + NativeProfileRoot: binding.NativeProfileRoot, ProfileNamespace: binding.ProfileNamespace, + ProfileAuthority: domain.CloneProfileAuthority(binding.ProfileAuthority), + LocalEntryObservation: binding.LocalEntryObservation.Clone(), SelectedDelivery: binding.SelectedDelivery, + Materialization: binding.Materialization, Activation: binding.Activation, + Authentication: binding.Authentication, Policy: binding.Policy, Verification: binding.Verification, + NativeObjects: append([]domain.NativeObjectOwnership(nil), binding.NativeObjects...), + } +} diff --git a/install/integrationctl/agentplugins/installer/project_remove_preflight_test.go b/install/integrationctl/agentplugins/installer/project_remove_preflight_test.go new file mode 100644 index 000000000..4a0792ba8 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_remove_preflight_test.go @@ -0,0 +1,255 @@ +package installer + +import ( + "context" + "errors" + "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/domain" +) + +func TestProjectRemovalForeignFilesRefuseBeforeNativeAttempt(t *testing.T) { + for _, variant := range []string{"single-cline", "single-codex", "group-first", "group-second"} { + for _, kind := range []string{"skill", "mcp"} { + for _, afterPrepare := range []bool{false, true} { + name := variant + "/" + kind + if afterPrepare { + name += "/after-prepare" + } + t.Run(name, func(t *testing.T) { + f, path := projectRemovalForeignFixture(t, variant, kind) + var handle *PreparedOperation + if afterPrepare { + req := f.req + req.Operation, req.OperationID = OpRemove, "foreign-after-prepare" + var err error + handle, err = f.engine.Prepare(t.Context(), req) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := handle.Close(); err != nil { + t.Error(err) + } + }() + } + foreign := "foreign skill bytes" + if kind == "mcp" { + original := deathRead(t, path) + foreign = strings.ReplaceAll(original, "http://127.0.0.1:9/inert", "https://foreign.example/mcp") + if foreign == original { + t.Fatal("actual projected marker missing") + } + } + deathWrite(t, path, foreign) + before, other := deathTree(t, f.project), f.untouched(t) + var result Result + var err error + if afterPrepare { + result, err = f.engine.Apply(t.Context(), handle, Decision{Confirmed: true}) + } else { + result, err = f.execute(t, OpRemove) + } + if err == nil || result.Mutated || !reflect.DeepEqual(before, deathTree(t, f.project)) { + t.Fatalf("foreign %s refusal changed project/state/journal: mutation=%t error=%v", kind, result.Mutated, err) + } + f.assertUntouched(t, other) + }) + } + } + } +} + +func projectRemovalForeignFixture(t *testing.T, variant, kind string) (*projectFilesFixture, string) { + t.Helper() + var f *projectFilesFixture + if strings.HasPrefix(variant, "group-") { + f = projectRemovalGroup(t) + } else { + f = newProjectFilesFixture(t, strings.TrimPrefix(variant, "single-"), "disabled-with-runner") + f.apply(t, OpInstall) + } + path := f.skill + if kind == "mcp" { + path = f.config + } + if variant == "group-second" { + path = filepath.Join(f.project, ".agents", "skills", "docs", "SKILL.md") + if kind == "mcp" { + path = filepath.Join(f.project, ".codex", "config.toml") + } + } + return f, path +} + +func TestProjectRemovalAllowsAbsentOwnedNativeFiles(t *testing.T) { + for _, id := range []string{"cline", "codex"} { + t.Run(id, func(t *testing.T) { + f := newProjectFilesFixture(t, id, "disabled-with-runner") + f.apply(t, OpInstall) + other := f.untouched(t) + for _, path := range []string{filepath.Dir(f.skill), f.config} { + if err := os.RemoveAll(path); err != nil { + t.Fatal(err) + } + } + result := f.apply(t, OpRemove) + if !result.Mutated { + t.Fatal("owned package removal lost mutation") + } + f.assertUntouched(t, other) + }) + } +} + +type removalFileInspector struct { + finding clients.RegistryFinding + err error + calls int + check func(clients.Env, domain.DetectedClient, domain.DeliveryPlan, *domain.ClientBinding) +} + +func (*removalFileInspector) ID() domain.ClientID { return domain.ClientCline } +func (*removalFileInspector) UsesNativeRegistryExecutable() bool { return true } +func (*removalFileInspector) InspectPreparedRegistry(domain.DeliveryPlan, string, bool) (clients.RegistryFinding, error) { + return clients.RegistryClear, nil +} +func (a *removalFileInspector) InspectNativeRegistry(_ context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, binding *domain.ClientBinding) (clients.RegistryFinding, error) { + a.calls++ + if a.check != nil { + a.check(env, client, plan, binding) + } + return a.finding, a.err +} + +func TestProjectRemovalObservationUsesFrozenRecordedSet(t *testing.T) { + for _, finding := range []clients.RegistryFinding{clients.RegistryExpected, clients.RegistryClear, clients.RegistryCollision, clients.RegistryIndeterminate, clients.RegistryFinding(255)} { + t.Run(fmt.Sprintf("finding-%d", finding), func(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + f.apply(t, OpInstall) + binding := f.binding(t) + objects := append([]domain.NativeObjectOwnership(nil), binding.NativeObjects...) + client := domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: f.user} + target, err := f.engine.planner().ResolveTarget(t.Context(), client, domain.ScopeProject, binding.PhysicalArtifact) + if err != nil { + t.Fatal(err) + } + inspector := &removalFileInspector{finding: finding} + inspector.check = func(env clients.Env, got domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) { + if env.Runner != nil || got.ClientID != client.ClientID || got.ConfigRoot != f.user || plan.DeclaredName != "files-observer" || plan.TargetContext != target.TargetContext || plan.TargetAnchor != target.TargetAnchor || plan.TargetRoot != target.TargetRoot || plan.ActivePath != target.ActivePath || plan.NativeRegistryRoot != target.NativeRegistryRoot || len(plan.Components) != 0 || !reflect.DeepEqual(plan.PreviousNativeObjects, objects) || !reflect.DeepEqual(managed.NativeObjects, objects) { + t.Fatal("removal observation lost exact frozen authority or acquired a native runner") + } + // Each external packet owns independent slices. A callback cannot + // change the plan/binding used by subsequent confirmation fences. + plan.PreviousNativeObjects[0].Path = "callback-only" + if !reflect.DeepEqual(managed.NativeObjects, objects) { + t.Fatal("plan shares managed ownership") + } + managed.NativeObjects[len(managed.NativeObjects)-1].LogicalName = "callback-only" + } + registry, err := clients.NewRegistry(inspector) + if err != nil { + t.Fatal(err) + } + f.engine.cfg.Registry = registry + before, other := deathTree(t, f.project), f.untouched(t) + err = f.engine.confirmProjectRemovalFiles(t.Context(), client, "files-observer", binding, target, managedPackageDigest(binding)) + wantError := finding != clients.RegistryExpected && finding != clients.RegistryClear + if (err != nil) != wantError || inspector.calls != 1 || !reflect.DeepEqual(binding.NativeObjects, objects) || !reflect.DeepEqual(before, deathTree(t, f.project)) { + t.Fatalf("finding=%v error=%v calls=%d", finding, err, inspector.calls) + } + f.assertUntouched(t, other) + }) + } +} + +func TestProjectRemovalObservationRejectsMissingOrFailedInspector(t *testing.T) { + for _, missing := range []bool{false, true} { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + f.apply(t, OpInstall) + binding := f.binding(t) + client := domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: f.user} + target, err := f.engine.planner().ResolveTarget(t.Context(), client, domain.ScopeProject, binding.PhysicalArtifact) + if err != nil { + t.Fatal(err) + } + failure := errors.New("inert inspector failure") + var adapter clients.Adapter = &removalFileInspector{finding: clients.RegistryClear, err: failure} + if missing { + adapter = &handoffCaptureAdapter{id: domain.ClientCline} + } + registry, err := clients.NewRegistry(adapter) + if err != nil { + t.Fatal(err) + } + f.engine.cfg.Registry = registry + before := deathTree(t, f.project) + err = f.engine.confirmProjectRemovalFiles(t.Context(), client, "files-observer", binding, target, managedPackageDigest(binding)) + if err == nil || !missing && !errors.Is(err, failure) || !reflect.DeepEqual(before, deathTree(t, f.project)) { + t.Fatalf("missing=%t error=%v", missing, err) + } + } +} + +func TestProjectRemovalObservationPreservesOtherLifecycles(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + f.apply(t, OpInstall) + inspector := &removalFileInspector{finding: clients.RegistryCollision} + registry, err := clients.NewRegistry(inspector) + if err != nil { + t.Fatal(err) + } + f.engine.cfg.Registry = registry + before := deathTree(t, f.project) + for _, id := range []domain.ClientID{domain.ClientClaude, domain.ClientGrok} { + if err := f.engine.confirmProjectRemovalFiles(t.Context(), domain.DetectedClient{ClientID: id}, "", domain.ClientBinding{}, domain.DeliveryTarget{}, ""); err != nil { + t.Fatal(err) + } + } + f.engine.cfg.ProjectRoot = "" + if err := f.engine.confirmProjectRemovalFiles(t.Context(), domain.DetectedClient{ClientID: domain.ClientCline}, "", domain.ClientBinding{}, domain.DeliveryTarget{}, ""); err != nil { + t.Fatal(err) + } + if inspector.calls != 0 || !reflect.DeepEqual(before, deathTree(t, f.project)) { + t.Fatal("file observer entered a historical lifecycle") + } +} + +func TestProjectRemovalObservationRejectsContradictoryPacket(t *testing.T) { + for _, bad := range []string{"name", "target-scope", "user-profile", "digest", "owned-receipt"} { + t.Run(bad, func(t *testing.T) { + f := newProjectFilesFixture(t, "cline", "disabled-with-runner") + f.apply(t, OpInstall) + binding := f.binding(t) + client := domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: f.user} + target, err := f.engine.planner().ResolveTarget(t.Context(), client, domain.ScopeProject, binding.PhysicalArtifact) + if err != nil { + t.Fatal(err) + } + name, digest := "files-observer", managedPackageDigest(binding) + switch bad { + case "name": + name = "" + case "target-scope": + target.TargetContext.Scope = domain.ScopeUser + case "user-profile": + target.TargetContext.UserConfigRoot = filepath.Join(f.base, "B") + case "digest": + digest = "sha256:foreign" + case "owned-receipt": + binding.NativeObjects = nil + } + before, other := deathTree(t, f.project), f.untouched(t) + if err := f.engine.confirmProjectRemovalFiles(t.Context(), client, name, binding, target, digest); err == nil || !reflect.DeepEqual(before, deathTree(t, f.project)) { + t.Fatalf("bad %s admitted or mutated: %v", bad, err) + } + f.assertUntouched(t, other) + }) + } +} diff --git a/install/integrationctl/agentplugins/installer/project_scope.go b/install/integrationctl/agentplugins/installer/project_scope.go new file mode 100644 index 000000000..f4e5ea72b --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_scope.go @@ -0,0 +1,65 @@ +package installer + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func (e *Engine) scope() domain.InstallScope { + if e.cfg.ProjectRoot != "" { + return domain.ScopeProject + } + return domain.ScopeUser +} + +func (e *Engine) nativeRegistryRoot(client domain.DetectedClient) (string, error) { + if e.scope() != domain.ScopeProject { + return client.ConfigRoot, nil + } + target, err := e.planner().ResolveTarget(context.Background(), client, e.scope(), "project-context") + if err != nil { + return "", err + } + return target.NativeRegistryRoot, nil +} + +func preparedTargetContext(prepared *PreparedOperation, clientID string) domain.TargetContext { + client := prepared.detected[domain.ClientID(clientID)] + if client.ClientID == "" && string(prepared.client.ClientID) == clientID { + client = prepared.client + } + return domain.TargetContext{Scope: prepared.req.Scope, ProjectRoot: prepared.req.ProjectRoot, UserConfigRoot: client.ConfigRoot} +} + +func (e *Engine) validatePrepareContext(req Request) error { + if req.ProjectRoot != e.cfg.ProjectRoot || (req.Scope == domain.ScopeProject) != (e.cfg.ProjectRoot != "") { + return fmt.Errorf("%w: request scope/project root differs from configured engine", ErrInvalidRequest) + } + if req.Scope != domain.ScopeUser && req.Scope != domain.ScopeProject { + return fmt.Errorf("%w: unsupported request scope %q", ErrInvalidRequest, req.Scope) + } + if req.Scope == domain.ScopeProject { + if err := e.validateProjectClients(req); err != nil { + return err + } + } + return e.CheckProjectNamespace() +} + +func (e *Engine) validateProjectClients(req Request) error { + ids := []string{req.ClientID} + if len(req.Targets) != 0 { + ids = make([]string, 0, len(req.Targets)) + for _, target := range req.Targets { + ids = append(ids, target.ClientID) + } + } + for _, id := range ids { + if !ClientScopeSupported(e.cfg.Registry, id, req.Scope) { + return fmt.Errorf("%w: project scope is not qualified for %s", ErrInvalidRequest, id) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/installer/project_vscode_death_test.go b/install/integrationctl/agentplugins/installer/project_vscode_death_test.go new file mode 100644 index 000000000..3c1465830 --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_vscode_death_test.go @@ -0,0 +1,156 @@ +package installer + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/vscode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Reuse the existing SDK process-death harness and exact native lock seam. The +// child invokes the real workspace adapter; it runs no VS Code or MCP process. +func TestProjectVSCodeProcessDeathKeepsNativeAttemptFenced(t *testing.T) { + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { deathAllowSnapshotCleanup(t, base) }) + helper, err := os.Executable() + if err != nil { + t.Fatal(err) + } + deathWrite(t, filepath.Join(base, "codex-fixture"), deathRead(t, helper)) + if err := os.Chmod(filepath.Join(base, "codex-fixture"), 0700); err != nil { + t.Fatal(err) + } + deathPackage(t, base, deathComponent, deathOldVersion) + freshPackage := deathPackage(t, base, "fresh-death", deathOldVersion) + user, a, b := filepath.Join(base, "user"), filepath.Join(base, "A"), filepath.Join(base, "B") + deathWrite(t, filepath.Join(user, "Code", "User", "mcp.json"), `{"servers":{"USER":{"type":"http","url":"https://foreign.test/mcp"}}}`) + deathWrite(t, filepath.Join(base, "user-uap", "keep"), "original user state") + var aEngine *Engine + for _, project := range []string{a, b} { + deathWrite(t, filepath.Join(project, ".vscode", "mcp.json"), "{// foreign comment\n\"inputs\":[],\"servers\":{\"foreign\":{\"type\":\"http\",\"url\":\"https://foreign.test/mcp\"},},}\n") + deathWrite(t, filepath.Join(project, ".github", "skills", "foreign", "SKILL.md"), "foreign skill") + engine, _ := deathEngine(t, base, project, vscode.NewProject(), nil) + result := deathApply(t, engine, workspaceDeathRequest(t, engine, base, project, OpInstall)) + deathWrite(t, filepath.Join(result.Binding.DataRoot, "keep"), "retained data") + if project == a { + aEngine = engine + } + } + before, oldConfig := deathBinding(t, base, a), deathRead(t, filepath.Join(a, ".vscode", "mcp.json")) + update := workspaceDeathRequest(t, aEngine, base, a, OpUpdate) + userBefore, userState, bBefore := deathTree(t, user), deathTree(t, filepath.Join(base, "user-uap")), deathTree(t, b) + deathPackage(t, base, deathComponent, deathNewVersion) + deathKillAtBarrierChild(t, "TestProjectVSCodeProcessDeathChild", base, deathNativeBoundary, func(observed deathBarrier) { + if want := deathProveBoundary(t, base, deathNativeBoundary, before, oldConfig); !reflect.DeepEqual(observed, want) { + t.Fatalf("workspace child barrier differs from persisted facts: %+v, want %+v", observed, want) + } + }) + retained := deathTree(t, a) + hostCalls := 0 + fresh, runner := deathEngine(t, base, a, vscode.NewProject(), func(context.Context, BindingFacts) error { hostCalls++; return nil }) + view, err := fresh.Inspect(t.Context()) + if err == nil || !view.Recovery.Required || view.Recovery.Reason != "native attempt has no persisted intent; retain uncertainty" || len(view.Installations) != 1 { + t.Fatalf("workspace reopen lost pending native attempt: %+v %v", view, err) + } + for _, operation := range []Operation{OpUpdate, OpRepair, OpRemove, OpInstall} { + req := update + authority := *update.EnvironmentAuthority + req.EnvironmentAuthority = &authority + req.Operation, req.OperationID, authority.ExpectedAction = operation, "workspace-after-death-"+string(operation), string(operation) + if operation == OpInstall { + req.PackageRoot, req.InstallationID = freshPackage, "" + authority.Alias, authority.ReceiptID, authority.InstallationID, authority.ClientBindingID = "fresh-death", "", "", "" + } + handle, err := fresh.Prepare(t.Context(), req) + if err == nil { + result, applyErr := fresh.Apply(t.Context(), handle, Decision{Confirmed: true}) + err = applyErr + if result.Mutated || result.Outcome != OutcomeRecovery { + t.Fatalf("workspace %s crossed unresolved attempt: %+v %v", operation, result, err) + } + } + if handle != nil { + if closeErr := handle.Close(); closeErr != nil { + t.Fatal(closeErr) + } + } + blocked := errors.Is(err, ErrRecoveryRequired) || err != nil && strings.Contains(err.Error(), "native activation attempt") && strings.Contains(err.Error(), "unresolved") + if operation == OpRemove && errors.Is(err, ErrInvalidRequest) && strings.Contains(err.Error(), "removal binding scope or native attempt differs") { + blocked = true + } + if !blocked { + t.Fatalf("workspace %s refused for an unrelated reason: %v", operation, err) + } + deathAssertTree(t, a, retained) + } + if result, err := fresh.RecoverEnvironment(t.Context(), view, "project-default"); !errors.Is(err, ErrRecoveryRequired) || result.Outcome != OutcomeRecovery { + t.Fatalf("workspace recovery guessed native ownership: %+v %v", result, err) + } + again, err := fresh.Inspect(t.Context()) + if err == nil || !reflect.DeepEqual(view, again) || runner.calls != 0 || hostCalls != 0 { + t.Fatalf("workspace retry changed pending facts or invoked process/host: %+v %v; native=%d host=%d", again, err, runner.calls, hostCalls) + } + deathAssertTree(t, a, retained) + deathAssertTree(t, user, userBefore) + deathAssertTree(t, filepath.Join(base, "user-uap"), userState) + deathAssertTree(t, b, bBefore) + deathProveBoundary(t, base, deathNativeBoundary, before, oldConfig) +} + +func workspaceDeathRequest(t *testing.T, engine *Engine, base, project string, op Operation) Request { + t.Helper() + req := deathRequest(t, engine, base, project, op) + req.ClientID, req.ClientConfigRoot, req.ClientExecutable = "vscode", filepath.Join(base, "user", "Code"), "" + req.EnvironmentAuthority.ClientID = "vscode" + return req +} + +type workspaceDeathAdapter struct { + *vscode.ProjectAdapter + beforeMCPLock func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) +} + +func (a workspaceDeathAdapter) Activate(ctx context.Context, env clients.Env, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + env.NativeConfig = nativeconfig.NewWithLockAcquirer(a.beforeMCPLock) + return a.ProjectAdapter.Activate(ctx, env, request) +} + +func TestProjectVSCodeProcessDeathChild(t *testing.T) { + var args []string + for i, arg := range os.Args { + if arg == "--" { + args = os.Args[i+1:] + break + } + } + if len(args) == 0 { + t.Skip("disposable parent-controlled SDK child only") + } + if len(args) != 2 || args[1] != deathNativeBoundary { + t.Fatal("invalid workspace process-death fixture arguments") + } + base, project := args[0], filepath.Join(args[0], "A") + before, oldConfig := deathBinding(t, base, project), deathRead(t, filepath.Join(project, ".vscode", "mcp.json")) + adapter := workspaceDeathAdapter{ProjectAdapter: vscode.NewProject(), beforeMCPLock: func(paths nativeconfig.Paths, codec nativeconfig.Codec) (func() error, error) { + if codec != nativeconfig.CodecVSCode || paths.JSON != filepath.Join(project, ".vscode", "mcp.json") || paths.JSONC != "" { + t.Fatalf("workspace primitive selected another codec/path: %+v %s", paths, codec) + } + deathEmitAndBlock(t, deathProveBoundary(t, base, deathNativeBoundary, before, oldConfig)) + return nil, fmt.Errorf("unreachable parent-controlled workspace boundary") + }} + engine, _ := deathEngine(t, base, project, adapter, nil) + deathApply(t, engine, workspaceDeathRequest(t, engine, base, project, OpUpdate)) + t.Fatal("workspace SDK update completed without the selected kill boundary") +} diff --git a/install/integrationctl/agentplugins/installer/project_vscode_test.go b/install/integrationctl/agentplugins/installer/project_vscode_test.go new file mode 100644 index 000000000..cf0f690ee --- /dev/null +++ b/install/integrationctl/agentplugins/installer/project_vscode_test.go @@ -0,0 +1,218 @@ +package installer + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/all" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/vscode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectVSCodeDefaultSDKRefusesForeignNativeBeforePublication(t *testing.T) { + for _, collision := range []string{"skill", "mcp", "malformed"} { + t.Run(collision, func(t *testing.T) { + f := newProjectFilesFixture(t, "vscode", "default") + path, body := f.skill, "foreign skill" + if collision != "skill" { + path, body = f.config, `{"servers":{"docs":{"type":"http","url":"https://foreign.test/mcp"}}}` + if collision == "malformed" { + body = `{"servers":` + } + } + deathWrite(t, path, body) + other, before := f.untouched(t), f.state(t) + out, err := f.execute(t, OpInstall) + if err == nil || out.Mutated || f.state(t) != before || deathRead(t, path) != body { + t.Fatalf("foreign workspace publication: %+v %v", out, err) + } + if _, err := os.Lstat(filepath.Join(f.cfg.StateRoot, "managed")); !os.IsNotExist(err) { + t.Fatal("native refusal published a managed workspace package", err) + } + f.assertUntouched(t, other) + }) + } +} + +func TestProjectVSCodeDefaultRegistryRefusesBeforeSourceAcquisition(t *testing.T) { + f := newProjectFilesFixture(t, "vscode", "default") + f.cfg.Registry = all.Default() + f.reopen(t) + f.req.PackageRoot = filepath.Join(f.base, "source-that-must-not-be-read") + other := f.untouched(t) + if out, err := f.execute(t, OpInstall); !errors.Is(err, ErrInvalidRequest) || !strings.Contains(err.Error(), "project scope is not qualified for vscode") || out.Mutated || f.state(t) != "" { + t.Fatalf("default registry reached project acquisition: %+v %v", out, err) + } + for _, path := range []string{f.config, f.skill, filepath.Join(f.cfg.StateRoot, "managed"), filepath.Join(f.cfg.StateRoot, "data")} { + if _, err := os.Lstat(path); !os.IsNotExist(err) { + t.Fatalf("default registry published project output %s: %v", path, err) + } + } + f.assertUntouched(t, other) +} + +func TestProjectVSCodeManagedStdioRequiresExactPackageDataAndLauncher(t *testing.T) { + f := newProjectFilesFixture(t, "vscode", "disabled-with-runner") + deathWrite(t, filepath.Join(f.pkg, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"env":{"TASK_MODE":"inert"},"cwd":"${PLUGIN_DATA}"}}}`) + deathWrite(t, filepath.Join(f.pkg, "server.js"), "// Inert fixture; never executed.\n") + prepared, err := f.engine.Prepare(t.Context(), f.req) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := prepared.Close(); err != nil { + t.Error(err) + } + }() + if len(prepared.deliveries) != 1 { + t.Fatal("missing selected workspace delivery") + } + envelope, plan := prepared.envelope, prepared.deliveries[0].plan + result, err := f.engine.Apply(t.Context(), prepared, Decision{Confirmed: true}) + if err != nil || !result.Mutated || result.Client.Activation != string(domain.ActivationPrepared) { + t.Fatalf("stdio preparation: %+v %v", result, err) + } + if err := prepared.Close(); err != nil { + t.Fatal(err) + } + binding := f.binding(t) + root, data := binding.TargetLocator, result.Binding.DataRoot + if data == "" || result.Binding.DataReceiptID == "" { + t.Fatal("stdio preparation omitted selected data authority") + } + deathWrite(t, filepath.Join(data, "keep.txt"), "retained plugin data") + deathWrite(t, filepath.Join(f.base, "unowned-data", "keep"), "foreign data") + adapter := vscode.NewProject() + objects, err := adapter.ProjectActiveNative(t.Context(), root, envelope, plan, data) + if err != nil || len(objects) != 2 { + t.Fatalf("exact active stdio projection: %+v %v", objects, err) + } + config, state, other := deathRead(t, f.config), f.state(t), f.untouched(t) + var native struct { + Servers map[string]struct { + Command string + Args []string + Env map[string]string + CWD string + } + } + if err := json.Unmarshal([]byte(config), &native); err != nil { + t.Fatal(err) + } + server := native.Servers["docs"] + if server.Env["PLUGIN_ROOT"] != root || server.Env["PLUGIN_DATA"] != data || server.Env["TASK_MODE"] != "inert" || len(server.Args) < 7 || server.Args[1] != root || server.Args[2] != data || server.Args[3] != data || server.Args[4] != "data" || server.CWD != "" || deathRead(t, server.Command) != deathRead(t, f.cfg.HelperExecutable) { + t.Fatalf("stdio launcher lost exact immutable package/data/helper binding: %+v", server) + } + managed := filepath.Join(root, "mcp.json") + original := deathRead(t, managed) + for _, mismatch := range []string{"data", "package", "launcher"} { + t.Run(mismatch, func(t *testing.T) { + selectedRoot, selectedData := root, data + switch mismatch { + case "data": + selectedData = filepath.Join(f.base, "unowned-data") + case "package": + selectedRoot = filepath.Join(f.base, "unowned-package") + case "launcher": + var document map[string]any + if err := json.Unmarshal([]byte(original), &document); err != nil { + t.Fatal(err) + } + entry := document["mcpServers"].(map[string]any)["docs"].(map[string]any) + entry["command"] = filepath.Join(f.base, "foreign-launcher") + body, err := json.Marshal(document) + if err != nil { + t.Fatal(err) + } + deathWrite(t, managed, string(body)) + t.Cleanup(func() { deathWrite(t, managed, original) }) + } + if _, err := adapter.ProjectActiveNative(t.Context(), selectedRoot, envelope, plan, selectedData); err == nil || mismatch != "package" && !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("mismatched %s gained projection authority: %v", mismatch, err) + } + if deathRead(t, f.config) != config || f.state(t) != state { + t.Fatal("projection refusal changed native files or state") + } + }) + } + if restored, err := adapter.ProjectActiveNative(t.Context(), root, envelope, plan, data); err != nil || !reflect.DeepEqual(restored, objects) { + t.Fatalf("restored exact projection differs: %+v %v", restored, err) + } + f.apply(t, OpRemove) + if deathRead(t, filepath.Join(data, "keep.txt")) != "retained plugin data" { + t.Fatal("workspace removal changed plugin data") + } + f.assertUntouched(t, other) +} + +func TestProjectVSCodeRefusesMissingOrDuplicateManagedOwnership(t *testing.T) { + for _, malformed := range []string{"missing", "duplicate"} { + t.Run(malformed, func(t *testing.T) { + f := newProjectFilesFixture(t, "vscode", "disabled-with-runner") + f.apply(t, OpInstall) + state, err := f.engine.store.Load() + if err != nil { + t.Fatal(err) + } + for key, binding := range state.Installations[0].Clients { + var objects []domain.NativeObjectOwnership + for _, object := range binding.NativeObjects { + if object.Kind == "managed_package_directory" { + if malformed == "duplicate" { + objects = append(objects, object, object) + } + } else { + objects = append(objects, object) + } + } + binding.NativeObjects = objects + state.Installations[0].Clients[key] = binding + } + body, err := json.Marshal(state) + if err != nil { + t.Fatal(err) + } + deathWrite(t, filepath.Join(f.cfg.StateRoot, "state-v2.json"), string(body)) + before := deathTree(t, f.project) + f.reopen(t) + if out, err := f.execute(t, OpRemove); err == nil || out.Mutated { + t.Fatalf("%s managed ownership admitted removal: %+v %v", malformed, out, err) + } + deathAssertTree(t, f.project, before) + if f.runner.calls != 0 { + t.Fatal("malformed ownership invoked a native process") + } + }) + } +} + +func TestProjectVSCodeRejectsAttestationAndCopilotSharedSkillAdoption(t *testing.T) { + f := newProjectFilesFixture(t, "vscode", "disabled-with-runner") + f.cfg.Registry = all.Project() + f.reopen(t) + other := f.untouched(t) + f.req.ActivationComplete = true + if out, err := f.execute(t, OpInstall); err == nil || out.Mutated || f.state(t) != "" { + t.Fatalf("workspace attestation admitted before installation: %+v %v", out, err) + } + f.req.ActivationComplete = false + f.apply(t, OpInstall) + before, skill, config := f.state(t), deathRead(t, f.skill), deathRead(t, f.config) + f.req.ActivationComplete = true + if out, err := f.execute(t, OpRepair); err == nil || out.Mutated || f.state(t) != before || deathRead(t, f.skill) != skill || deathRead(t, f.config) != config { + t.Fatalf("workspace attestation changed installed files: %+v %v", out, err) + } + f.req.ActivationComplete = false + f.req.ClientID = "copilot" + f.req.ClientConfigRoot = filepath.Join(f.base, "copilot-user") + deathWrite(t, filepath.Join(f.req.ClientConfigRoot, "keep"), "original Copilot user") + if out, err := f.execute(t, OpInstall); err == nil || out.Mutated || f.state(t) != before || deathRead(t, f.skill) != skill { + t.Fatalf("Copilot adopted the same-byte VS Code skill: %+v %v", out, err) + } + f.assertUntouched(t, other) +} diff --git a/install/integrationctl/agentplugins/installer/seams.go b/install/integrationctl/agentplugins/installer/seams.go index f22a9cae6..8f21811f5 100644 --- a/install/integrationctl/agentplugins/installer/seams.go +++ b/install/integrationctl/agentplugins/installer/seams.go @@ -44,6 +44,7 @@ func (s seamStager) StageWithPluginData(ctx context.Context, envelope domain.Pac } } facts := s.facts + facts.TargetContext = plan.TargetContext facts.ProfileAuthority = plan.ProfileAuthority() facts.TargetPath = plan.ActivePath facts.DataRoot = data @@ -144,7 +145,7 @@ func (a seamActivator) Activate(ctx context.Context, request domain.ActivationRe // Selected deliveries retain their separate confirmation contract. resume := request.PackageUnchanged && request.Plan.SelectedDelivery.IsZero() && a.facts.BindingID != "" if resume { - if a.hostHandoffPending(request) { + if a.hostHandoffPending(request) && !keepProjectVerifyOnly(request) { request.VerifyOnly = false } } else if a.onCommitted != nil && !request.VerifyOnly { @@ -164,6 +165,19 @@ func (a seamActivator) Activate(ctx context.Context, request domain.ActivationRe return a.inner.Activate(ctx, request) } +// keepProjectVerifyOnly preserves the core's existing read-only decision for +// verified project component files. Prepared session activation alone must not +// turn that verification back into a write. It never invents VerifyOnly. +func keepProjectVerifyOnly(request domain.ActivationRequest) bool { + if !request.VerifyOnly || !request.PackageUnchanged || !request.Plan.SelectedDelivery.IsZero() || + request.Plan.Scope != domain.ScopeProject || request.Plan.ClientID != request.Client.ClientID { + return false + } + target, err := domain.NewTargetContext(domain.ScopeProject, request.Plan.TargetContext.ProjectRoot, request.Client.ConfigRoot) + return err == nil && target == request.Plan.TargetContext && + request.Plan.SelectedDelivery.EffectiveTraitsForScope(request.Client.ClientID, request.Plan.Scope).LifecycleKind == domain.LifecycleNativeConfig +} + func hostHandoffPending(binding domain.ClientBinding) bool { switch binding.Materialization { case domain.MaterializationMaterialized, domain.MaterializationDegraded: @@ -220,6 +234,7 @@ func (a seamActivator) Deactivate(ctx context.Context, request domain.Deactivati func (a seamActivator) committedFacts(request domain.ActivationRequest) (BindingFacts, error) { facts := a.facts + facts.TargetContext = request.Plan.TargetContext if a.store == nil { return uncommittedFacts(facts, request.Plan.SelectedDelivery) } diff --git a/install/integrationctl/agentplugins/installer/selected_lifecycle.go b/install/integrationctl/agentplugins/installer/selected_lifecycle.go index 755a6fbc6..31ae5b786 100644 --- a/install/integrationctl/agentplugins/installer/selected_lifecycle.go +++ b/install/integrationctl/agentplugins/installer/selected_lifecycle.go @@ -63,14 +63,17 @@ func (e *Engine) validateRemovalBinding(client domain.DetectedClient, binding do if err := binding.ValidateLocalEntryObservation(); err != nil { return err } - if binding.ClientID != string(client.ClientID) || binding.Scope != string(domain.ScopeUser) || binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil { + if binding.ClientID != string(client.ClientID) || binding.Scope != string(e.scope()) || binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil { return fmt.Errorf("%w: removal binding scope or native attempt differs", ErrInvalidRequest) } if err := binding.SelectedDelivery.Validate(); err != nil { return err } - if binding.SelectedDelivery.EffectiveTraits(client.ClientID).BindsNativeProfileRoot && (client.ConfigRoot == "" || client.ConfigRoot != binding.NativeProfileRoot) { - return fmt.Errorf("%w: removal profile differs from persisted native root", ErrInvalidRequest) + if binding.SelectedDelivery.EffectiveTraitsForScope(client.ClientID, e.scope()).BindsNativeProfileRoot { + root, err := e.nativeRegistryRoot(client) + if err != nil || root == "" || root != binding.NativeProfileRoot { + return fmt.Errorf("%w: removal profile differs from persisted native root", ErrInvalidRequest) + } } if binding.SelectedDelivery.IsZero() { return nil @@ -104,7 +107,7 @@ func (e *Engine) confirmRemoveBindings(ctx context.Context, prepared *PreparedOp return fmt.Errorf("%w: persisted removal authority changed", ErrPlanChanged) } if found { - if err := e.confirmRemovalPreflight(ctx, client, binding, receipt, state); err != nil { + if err := e.confirmRemovalPreflight(ctx, client, live.DeclaredName, binding, receipt, state); err != nil { return fmt.Errorf("%w: %w", ErrPlanChanged, err) } } @@ -243,16 +246,16 @@ func (e *Engine) confirmRemoveDispatch(ctx context.Context, prepared *PreparedOp // Keep the already recorded Service attempt in the comparison. An observer // cannot replace any binding, settings identity or receipt authority, even // when it returns an error or cancellation. Check before baseline capture. - return scope, scope.confirm(e.confirmRemovalPreflight(ctx, request.Client, binding, receipt, state)) + return scope, scope.confirm(e.confirmRemovalPreflight(ctx, request.Client, current.DeclaredName, binding, receipt, state)) } -func (e *Engine) confirmRemovalPreflight(ctx context.Context, client domain.DetectedClient, binding domain.ClientBinding, receipt domain.DataReceipt, approved domain.StateFileV2) error { +func (e *Engine) confirmRemovalPreflight(ctx context.Context, client domain.DetectedClient, declaredName string, binding domain.ClientBinding, receipt domain.DataReceipt, approved domain.StateFileV2) error { manager := dirswap.Manager{Namespace: e.cfg.StateRoot, JournalDir: e.cfg.OperationsDir} journals, err := manager.ListOpen() if err != nil { return errors.Join(ErrPlanChanged, err) } - cause := e.removalPreflight(ctx, client, binding, receipt) + cause := e.removalPreflight(ctx, client, declaredName, binding, receipt) after, err := manager.ListOpen() if err != nil || !reflect.DeepEqual(after, journals) { cause = errors.Join(cause, ErrPlanChanged, err) @@ -308,7 +311,7 @@ func (e *Engine) confirmSelectedNativeEntry(ctx context.Context, client domain.D if !ok || adapter.UsesNativeRegistryExecutable() { return fmt.Errorf("%w: selected client has no read-only profile inspection", ErrUnsupported) } - plan := domain.DeliveryPlan{ClientID: client.ClientID, Scope: domain.ScopeUser, ActivePath: binding.TargetLocator, + plan := domain.DeliveryPlan{ClientID: client.ClientID, Scope: e.scope(), ActivePath: binding.TargetLocator, LocalEntryObservation: binding.LocalEntryObservation.Clone(), PreviousNativeObjects: append([]domain.NativeObjectOwnership(nil), binding.NativeObjects...), PhysicalArtifactID: binding.PhysicalArtifact, SelectedDelivery: binding.SelectedDelivery, NativeRegistryRoot: binding.SelectedDelivery.ProfileRoot()} finding, err := adapter.InspectNativeRegistry(ctx, clients.Env{NativeConfig: nativeconfig.New()}, client, plan, &binding) diff --git a/install/integrationctl/agentplugins/installer/target_facts.go b/install/integrationctl/agentplugins/installer/target_facts.go index e381c4d90..f85341434 100644 --- a/install/integrationctl/agentplugins/installer/target_facts.go +++ b/install/integrationctl/agentplugins/installer/target_facts.go @@ -18,7 +18,7 @@ func (e *Engine) knownTargetIndex(operation Operation, facts []TargetFacts) (map if !validRoot(fact.ConfigRoot) { return nil, fmt.Errorf("%w: ConfigRoot must be an explicit absolute clean path for %s", ErrTargetFactsUnavailable, fact.ClientID) } - if (fact.Executable != "" && !validRoot(fact.Executable)) || (operation != OpRemove && requiresExecutable(e.cfg.Registry, domain.ClientID(fact.ClientID)) && fact.Executable == "") { + if (fact.Executable != "" && !validRoot(fact.Executable)) || (operation != OpRemove && requiresExecutable(e.cfg.Registry, domain.ClientID(fact.ClientID), e.scope()) && fact.Executable == "") { return nil, fmt.Errorf("%w: Executable must be an explicit absolute clean path for %s", ErrTargetFactsUnavailable, fact.ClientID) } if _, exists := indexed[fact.ClientID]; exists { diff --git a/install/integrationctl/agentplugins/installer/types.go b/install/integrationctl/agentplugins/installer/types.go index f8361ccce..51f5847d4 100644 --- a/install/integrationctl/agentplugins/installer/types.go +++ b/install/integrationctl/agentplugins/installer/types.go @@ -33,6 +33,8 @@ const ( // Request is copied by Prepare. Subsequent caller edits do not change the handle. type Request struct { + Scope domain.InstallScope + ProjectRoot string `json:"-"` // Completion flags attest explicit host-observed lifecycle completion. ActivationComplete, AuthComplete bool physical map[domain.ClientID]domain.DetectedClient @@ -54,10 +56,13 @@ type Request struct { ClientID string ClientConfigRoot string ClientExecutable string - InstallationID string - OperationID string - Selector string - RequiredComponents []string + // ClientVersion is host-observed evidence for the selected executable. + // It is operational input, never portable package or diagnostic metadata. + ClientVersion string `json:"-"` + InstallationID string + OperationID string + Selector string + RequiredComponents []string // Assessment is a host decision for these exact local bytes. It is copied // and digest-checked by Prepare. Confirmation of the filesystem plan does // not create or alter this security decision. @@ -94,10 +99,11 @@ type ClientTarget struct { // TargetFacts is the request-scoped host view of one existing client binding. // BindingID ties host-owned profile and executable paths to UAP-owned state. type TargetFacts struct { - ClientID string - BindingID string - ConfigRoot string - Executable string + TargetContext domain.TargetContext `json:"-"` + ClientID string + BindingID string + ConfigRoot string + Executable string } // Decision is host UI confirmation, outside mutation locks. @@ -107,6 +113,7 @@ type Decision struct { // BindingFacts is the typed committed-binding view for host seams. type BindingFacts struct { + TargetContext domain.TargetContext `json:"-"` ProfileAuthority *domain.ProfileAuthority `json:"-"` // SelectedDelivery is immutable operational authority, excluded from diagnostic JSON. SelectedDelivery domain.SelectedDelivery `json:"-"` @@ -118,6 +125,8 @@ type BindingFacts struct { // Plan is an immutable copy for presentation. Operational paths are included // because the embedding host already chose explicit roots. type Plan struct { + Scope domain.InstallScope + TargetContext domain.TargetContext `json:"-"` ProfileAuthority *domain.ProfileAuthority `json:"-"` OpenCodeProfile *opencodehost.Profile `json:",omitempty"` OpenCodeSelections []opencodehost.Selection `json:",omitempty"` diff --git a/install/integrationctl/agentplugins/planner/intent.go b/install/integrationctl/agentplugins/planner/intent.go index 93e4da7e2..ec625a8a6 100644 --- a/install/integrationctl/agentplugins/planner/intent.go +++ b/install/integrationctl/agentplugins/planner/intent.go @@ -24,8 +24,14 @@ func ApplyInstallIntent(registry *clients.Registry, plan *domain.DeliveryPlan, i if err := intent.Validate(plan.ClientID); err != nil { return err } - if intent == domain.InstallIntentPrepare && plan.Scope != domain.ScopeUser { - return fmt.Errorf("preparation supports user scope only") + if intent == domain.InstallIntentPrepare { + if err := validatePreparationScope(registry, *plan); err != nil { + return err + } + } + if intent == domain.InstallIntentPrepare && plan.Scope != domain.ScopeUser && + !plan.SelectedDelivery.EffectiveTraitsForScope(plan.ClientID, plan.Scope).Allows(intent) { + return fmt.Errorf("preparation is not qualified for scope %s", plan.Scope) } plan.InstallIntent = intent if intent != domain.InstallIntentPrepare || plan.Status == domain.PlanUnsupported { @@ -37,3 +43,19 @@ func ApplyInstallIntent(registry *clients.Registry, plan *domain.DeliveryPlan, i } return refiner.RefinePreparation(plan) } + +// Reapplying an intent to a previously built plan must not bypass explicit +// project context admission. Genuine historical user plans retain their default. +func validatePreparationScope(registry *clients.Registry, plan domain.DeliveryPlan) error { + if plan.Scope != domain.ScopeProject && plan.TargetContext.Scope != domain.ScopeProject && plan.TargetContext.ProjectRoot == "" { + return nil + } + if !clients.SupportsScope(registry, plan.ClientID, domain.ScopeProject) { + return fmt.Errorf("preparation is not qualified for scope %s", domain.ScopeProject) + } + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return fmt.Errorf("project preparation requires matching explicit project scope") + } + _, err := domain.NewTargetContext(plan.TargetContext.Scope, plan.TargetContext.ProjectRoot, plan.TargetContext.UserConfigRoot) + return err +} diff --git a/install/integrationctl/agentplugins/planner/intent_test.go b/install/integrationctl/agentplugins/planner/intent_test.go index b928162f9..e4b94104e 100644 --- a/install/integrationctl/agentplugins/planner/intent_test.go +++ b/install/integrationctl/agentplugins/planner/intent_test.go @@ -1,9 +1,12 @@ package planner import ( + "os" + "path/filepath" "testing" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/all" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" ) @@ -17,7 +20,7 @@ func TestPreparationCannotOverrideUnsupportedPackage(t *testing.T) { } } -func TestPreparationRequiresSupportedNativeUserTarget(t *testing.T) { +func TestPreparationRequiresQualifiedScopeAndNativeTarget(t *testing.T) { for _, tc := range []struct { client domain.ClientID scope domain.InstallScope @@ -34,3 +37,100 @@ func TestPreparationRequiresSupportedNativeUserTarget(t *testing.T) { } } } + +func qualifiedKiroPreparation(t *testing.T) domain.DeliveryPlan { + t.Helper() + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + project, user := filepath.Join(base, "project"), filepath.Join(base, "user", ".kiro") + for _, path := range []string{project, user} { + if err := os.MkdirAll(path, 0700); err != nil { + t.Fatal(err) + } + } + if err := os.WriteFile(filepath.Join(user, "keep.txt"), []byte("original user bytes"), 0600); err != nil { + t.Fatal(err) + } + target, err := domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + managed := filepath.Join(project, ".uap", "managed") + native := filepath.Join(project, ".kiro") + return domain.DeliveryPlan{ClientID: domain.ClientKiro, Scope: domain.ScopeProject, TargetContext: target, Status: domain.PlanReady, NativeRegistryRoot: native, TargetAnchor: managed, TargetRoot: filepath.Join(managed, "clients", "kiro"), PhysicalArtifactID: "demo-0123456789ab", ActivePath: filepath.Join(managed, "clients", "kiro", "demo-0123456789ab"), Components: []domain.ComponentDecision{{Kind: domain.ComponentMCPServer, Name: "docs", Support: domain.SupportPrepared}}} +} +func TestPreparationQualifiedKiroProjectIsReadonlyPrepared(t *testing.T) { + plan := qualifiedKiroPreparation(t) + if err := ApplyInstallIntent(all.Default(), &plan, domain.InstallIntentPrepare); err != nil { + t.Fatal(err) + } + if plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared || plan.Verification != domain.VerificationPackageValid || plan.InstallIntent != domain.InstallIntentPrepare { + t.Fatalf("qualified preparation %+v", plan) + } + if len(plan.UserActions) != 1 || plan.UserActions[0] != kiro.KiroProjectAction { + t.Fatal("project preparation lost scoped next action") + } + entries, err := os.ReadDir(plan.TargetContext.ProjectRoot) + if err != nil || len(entries) != 0 { + t.Fatalf("preparation mutated project: %v %v", entries, err) + } + body, err := os.ReadFile(filepath.Join(plan.TargetContext.UserConfigRoot, "keep.txt")) + if err != nil || string(body) != "original user bytes" { + t.Fatal("preparation changed original user bytes") + } +} +func TestPreparationMalformedProjectRefusesBeforeReadiness(t *testing.T) { + for _, name := range []string{"missing root", "relative native", "wrong native", "wrong artifact", "wrong active", "user labels", "wrong target scope"} { + t.Run(name, func(t *testing.T) { + plan := qualifiedKiroPreparation(t) + switch name { + case "missing root": + plan.TargetContext.ProjectRoot = "" + case "relative native": + plan.NativeRegistryRoot = "native" + case "wrong native": + plan.NativeRegistryRoot = plan.TargetContext.UserConfigRoot + case "wrong artifact": + plan.PhysicalArtifactID = "foreign" + case "wrong active": + plan.ActivePath = filepath.Join(plan.TargetRoot, "foreign") + case "user labels": + plan.Scope = domain.ScopeUser + plan.TargetContext.Scope = domain.ScopeUser + case "wrong target scope": + plan.TargetContext.Scope = domain.ScopeUser + } + for _, direct := range []bool{false, true} { + candidate := plan + var err error + if direct { + err = kiro.New().RefinePreparation(&candidate) + } else { + err = ApplyInstallIntent(all.Default(), &candidate, domain.InstallIntentPrepare) + } + if err == nil || candidate.Activation == domain.ActivationPrepared || candidate.Verification == domain.VerificationPackageValid { + t.Fatalf("malformed project promoted direct%t: %+v %v", direct, candidate, err) + } + } + }) + } +} + +func TestPreparationProjectCannotPromoteUnsupportedCompatibility(t *testing.T) { + plan := qualifiedKiroPreparation(t) + plan.Status = domain.PlanUnsupported + for _, direct := range []bool{false, true} { + candidate := plan + var err error + if direct { + err = kiro.New().RefinePreparation(&candidate) + } else { + err = ApplyInstallIntent(all.Default(), &candidate, domain.InstallIntentPrepare) + } + if err != nil || candidate.Status != domain.PlanUnsupported || candidate.Activation == domain.ActivationPrepared || candidate.Verification == domain.VerificationPackageValid { + t.Fatalf("unsupported project compatibility promoted direct%t %+v %v", direct, candidate, err) + } + } +} diff --git a/install/integrationctl/agentplugins/planner/planner.go b/install/integrationctl/agentplugins/planner/planner.go index 03ca0407a..31992a1b9 100644 --- a/install/integrationctl/agentplugins/planner/planner.go +++ b/install/integrationctl/agentplugins/planner/planner.go @@ -14,6 +14,7 @@ import ( ) type Planner struct { + ProjectRoot string `json:"-"` ManagedRoot string Paths ports.PathPolicy // Registry supplies the client adapters that decide where a package lands @@ -58,6 +59,16 @@ func (planner Planner) Plan(ctx context.Context, request domain.PlanRequest) (do if planner.Registry == nil { return domain.DeliveryPlan{}, clients.ErrRegistryRequired } + if request.Scope == domain.ScopeProject && !clients.SupportsScope(planner.Registry, request.Client.ClientID, request.Scope) { + plan := domain.DeliveryPlan{ClientID: request.Client.ClientID, Scope: request.Scope, PhysicalArtifactID: request.PhysicalArtifactID} + rejectPlan(&plan, "scope_not_supported") + return plan, nil + } + targetContext, err := planner.requestContext(request) + if err != nil { + return domain.DeliveryPlan{}, err + } + request.Scope = targetContext.Scope plan, err := planner.plan(ctx, request) if err != nil { return plan, err @@ -79,7 +90,8 @@ func (planner Planner) plan(ctx context.Context, request domain.PlanRequest) (do return domain.DeliveryPlan{}, err } input := clients.PlanInput{ - Envelope: request.Envelope, Client: request.Client, + TargetContext: plan.TargetContext, + Envelope: request.Envelope, Client: request.Client, Detected: request.Detected, Intent: request.InstallIntent, LocalEntryObservation: request.LocalEntryObservation.Clone(), PreviousNativeObjects: append([]domain.NativeObjectOwnership(nil), request.PreviousNativeObjects...), @@ -88,6 +100,13 @@ func (planner Planner) plan(ctx context.Context, request domain.PlanRequest) (do if !admissible(definition, request, &plan) { return plan, nil } + if !clients.SupportsScope(planner.Registry, request.Client.ClientID, request.Scope) { + rejectPlan(&plan, "scope_not_supported") + return plan, nil + } + if err := planner.validateProjectRegistry(plan); err != nil { + return plan, err + } if precondition, ok := clients.As[clients.PlanPrecondition](planner.Registry, plan.ClientID); ok { if err := precondition.CheckPlanPrecondition(input, &plan); err != nil { return plan, err @@ -134,7 +153,12 @@ func (planner Planner) startPlan(ctx context.Context, request domain.PlanRequest return domain.ClientDefinition{}, domain.DeliveryPlan{}, fmt.Errorf("unsupported client %q", request.Client.ClientID) } capabilities := definition.Capabilities + targetContext, err := planner.requestContext(request) + if err != nil { + return definition, domain.DeliveryPlan{}, err + } return definition, domain.DeliveryPlan{ + TargetContext: targetContext, LocalEntryObservation: request.LocalEntryObservation.Clone(), PreviousNativeObjects: append([]domain.NativeObjectOwnership(nil), request.PreviousNativeObjects...), ClientID: request.Client.ClientID, @@ -353,7 +377,14 @@ func (planner Planner) ResolveTarget( if !supportsScope(capabilities.Scopes, scope) { return domain.DeliveryTarget{}, fmt.Errorf("scope %q is unsupported for %s", scope, client.ClientID) } - targetAnchor, targetRoot, err := planner.targetRoot(client, capabilities.PackageMode) + if !clients.SupportsScope(planner.Registry, client.ClientID, scope) { + return domain.DeliveryTarget{}, fmt.Errorf("scope %q has no qualified adapter layout for %s", scope, client.ClientID) + } + targetContext, err := domain.NewTargetContext(scope, planner.ProjectRoot, client.ConfigRoot) + if err != nil { + return domain.DeliveryTarget{}, err + } + targetAnchor, targetRoot, err := planner.targetRootForScope(client, capabilities.PackageMode, scope) if err != nil { return domain.DeliveryTarget{}, err } @@ -361,7 +392,15 @@ func (planner Planner) ResolveTarget( if err := planner.Paths.RequireContainedChild(targetRoot, activePath); err != nil { return domain.DeliveryTarget{}, fmt.Errorf("unsafe client target path: %w", err) } - return domain.DeliveryTarget{TargetAnchor: targetAnchor, TargetRoot: targetRoot, ActivePath: activePath}, nil + nativeRoot := client.ConfigRoot + if scope == domain.ScopeProject { + layout, _ := clients.As[clients.NativeRegistryLayout](planner.Registry, client.ClientID) + nativeRoot, _ = layout.NativeRegistry(clients.PlanInput{Client: client, TargetContext: targetContext}) + if err := planner.validateProjectRegistry(domain.DeliveryPlan{Scope: scope, ClientID: client.ClientID, TargetContext: targetContext, NativeRegistryRoot: nativeRoot}); err != nil { + return domain.DeliveryTarget{}, err + } + } + return domain.DeliveryTarget{TargetContext: targetContext, NativeRegistryRoot: nativeRoot, TargetAnchor: targetAnchor, TargetRoot: targetRoot, ActivePath: activePath}, nil } func Capabilities(clientID domain.ClientID) (domain.ClientCapabilities, bool) { @@ -374,10 +413,28 @@ func Capabilities(clientID domain.ClientID) (domain.ClientCapabilities, bool) { // containment check afterwards applies to both answers alike: an adapter // chooses a location, it does not get to escape one. func (planner Planner) targetRoot(client domain.DetectedClient, mode domain.PackageMode) (string, string, error) { + return planner.targetRootForScope(client, mode, domain.ScopeUser) +} + +func (planner Planner) targetRootForScope(client domain.DetectedClient, mode domain.PackageMode, scope domain.InstallScope) (string, string, error) { if planner.Paths == nil { return "", "", errPathPolicyRequired } - anchor, root, err := planner.clientTargetRoot(client, mode) + var anchor, root string + var err error + if scope == domain.ScopeProject { + targetContext, contextErr := domain.NewTargetContext(scope, planner.ProjectRoot, client.ConfigRoot) + if contextErr != nil { + return "", "", contextErr + } + layout, ok := clients.As[clients.ProjectTargetLayout](planner.Registry, client.ClientID) + if !ok { + return "", "", fmt.Errorf("project target layout is required for %s", client.ClientID) + } + anchor, root, err = layout.ProjectTargetRoot(client, mode, planner.ManagedRoot, targetContext) + } else { + anchor, root, err = planner.clientTargetRoot(client, mode) + } if err != nil { return "", "", err } @@ -390,6 +447,11 @@ func (planner Planner) targetRoot(client domain.DetectedClient, mode domain.Pack return "", "", err } absoluteAnchor, absolute = filepath.Clean(absoluteAnchor), filepath.Clean(absolute) + if scope == domain.ScopeProject && absoluteAnchor != planner.ProjectRoot { + if err := planner.Paths.RequireContainedChild(planner.ProjectRoot, absoluteAnchor); err != nil { + return "", "", fmt.Errorf("unsafe project target anchor: %w", err) + } + } if err := planner.Paths.RequireContainedChild(absoluteAnchor, absolute); err != nil { return "", "", fmt.Errorf("unsafe client target root: %w", err) } diff --git a/install/integrationctl/agentplugins/planner/planner_test.go b/install/integrationctl/agentplugins/planner/planner_test.go index ab596d496..5e48ba714 100644 --- a/install/integrationctl/agentplugins/planner/planner_test.go +++ b/install/integrationctl/agentplugins/planner/planner_test.go @@ -257,14 +257,26 @@ func TestPlannerFailsClosedForUndetectedClientAndUnsupportedScope(t *testing.T) if plan.Status != domain.PlanUnsupported || !contains(plan.Warnings, "client_not_detected") { t.Fatalf("undetected plan = %+v", plan) } - client = detectedClient(domain.ClientCursor, filepath.Join(t.TempDir(), ".cursor")) - plan, err = planner.Plan(context.Background(), testEnvelope(), client, domain.ScopeProject, "demo-0123456789ab") + // ChatGPT is actually unsupported for filesystem project installation. + projectRoot, rootErr := filepath.EvalSymlinks(t.TempDir()) + if rootErr != nil { + t.Fatal(rootErr) + } + unsupportedPlanner := testPlanner(Planner{ProjectRoot: projectRoot, ManagedRoot: filepath.Join(projectRoot, ".uap", "managed")}) + client = detectedClient(domain.ClientChatGPT, filepath.Join(t.TempDir(), ".chatgpt")) + plan, err = unsupportedPlanner.Plan(context.Background(), testEnvelope(), client, domain.ScopeProject, "demo-0123456789ab") if err != nil { t.Fatal(err) } if plan.Status != domain.PlanUnsupported || !contains(plan.Warnings, "scope_not_supported") { t.Fatalf("project plan = %+v", plan) } + // Cursor is qualified, but cannot borrow a missing selected project root. + client = detectedClient(domain.ClientCursor, filepath.Join(t.TempDir(), ".cursor")) + invalid, err := planner.Plan(context.Background(), testEnvelope(), client, domain.ScopeProject, "demo-0123456789ab") + if err == nil || invalid.ActivePath != "" { + t.Fatalf("qualified project with missing root resolved an artifact: %+v %v", invalid, err) + } } func TestChatGPTRemoteTargetSupportsSkillsWithoutDesktopDetection(t *testing.T) { diff --git a/install/integrationctl/agentplugins/planner/profile_authority.go b/install/integrationctl/agentplugins/planner/profile_authority.go index 25f505453..bbd076a16 100644 --- a/install/integrationctl/agentplugins/planner/profile_authority.go +++ b/install/integrationctl/agentplugins/planner/profile_authority.go @@ -13,7 +13,7 @@ var _ ports.PhysicalProfileAuthority = Planner{} func (p Planner) CaptureProfileAuthority(ctx context.Context, client domain.DetectedClient) (domain.ProfileAuthority, error) { if a, ok := clients.As[clients.PhysicalProfileAuthority](p.Registry, client.ClientID); ok { - token, err := a.CaptureProfileAuthority(ctx, client) + token, err := p.captureProfileAuthority(ctx, client, a) if err != nil { return domain.ProfileAuthority{}, err } @@ -24,6 +24,30 @@ func (p Planner) CaptureProfileAuthority(ctx context.Context, client domain.Dete } return domain.ProfileAuthority{}, nil } + +func (p Planner) captureProfileAuthority(ctx context.Context, client domain.DetectedClient, user clients.PhysicalProfileAuthority) (domain.ProfileAuthority, error) { + if p.ProjectRoot == "" { + return user.CaptureProfileAuthority(ctx, client) + } + project, ok := clients.As[clients.ProjectPhysicalAuthority](p.Registry, client.ClientID) + if !ok { + return domain.ProfileAuthority{}, fmt.Errorf("project physical authority capture is required") + } + target, err := domain.NewTargetContext(domain.ScopeProject, p.ProjectRoot, client.ConfigRoot) + if err != nil { + return domain.ProfileAuthority{}, err + } + layout, ok := clients.As[clients.NativeRegistryLayout](p.Registry, client.ClientID) + if !ok { + return domain.ProfileAuthority{}, fmt.Errorf("project native registry layout is required") + } + root, _ := layout.NativeRegistry(clients.PlanInput{Client: client, TargetContext: target}) + token, err := project.CaptureProjectAuthority(ctx, client, target, root) + if err == nil && token.Facts().CanonicalRoot != root { + return token, fmt.Errorf("project authority differs from native registry") + } + return token, err +} func (p Planner) RevalidateProfileAuthority(ctx context.Context, id domain.ClientID, token domain.ProfileAuthority) error { if a, ok := clients.As[clients.PhysicalProfileAuthority](p.Registry, id); ok { if token.IsZero() { diff --git a/install/integrationctl/agentplugins/planner/project.go b/install/integrationctl/agentplugins/planner/project.go new file mode 100644 index 000000000..779ab9f7e --- /dev/null +++ b/install/integrationctl/agentplugins/planner/project.go @@ -0,0 +1,39 @@ +package planner + +import ( + "fmt" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func (planner Planner) requestContext(request domain.PlanRequest) (domain.TargetContext, error) { + target, err := domain.NewTargetContext(request.Scope, request.ProjectRoot, request.Client.ConfigRoot) + if err != nil { + return domain.TargetContext{}, err + } + if target.ProjectRoot != planner.ProjectRoot { + return domain.TargetContext{}, fmt.Errorf("request project root differs from configured planner") + } + return target, nil +} + +func (planner Planner) validateProjectRegistry(plan domain.DeliveryPlan) error { + if plan.Scope != domain.ScopeProject { + return nil + } + if _, ok := clients.As[clients.NativeRegistryLayout](planner.Registry, plan.ClientID); !ok { + return fmt.Errorf("project native registry layout is required for %s", plan.ClientID) + } + root := plan.NativeRegistryRoot + if root == "" || !filepath.IsAbs(root) || filepath.Clean(root) != root { + return fmt.Errorf("project native registry root must be explicit absolute and clean") + } + if root != plan.TargetContext.ProjectRoot { + if err := planner.Paths.RequireContainedChild(plan.TargetContext.ProjectRoot, root); err != nil { + return fmt.Errorf("unsafe project native registry: %w", err) + } + } + return nil +} diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/compat_invalid_components.json b/install/integrationctl/agentplugins/planner/testdata/golden/compat_invalid_components.json index de0253c2a..cf0e90540 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/compat_invalid_components.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/compat_invalid_components.json @@ -55,7 +55,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -95,7 +96,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -135,7 +137,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -176,7 +179,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -217,7 +221,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -258,7 +263,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -299,7 +305,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -340,7 +347,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -381,7 +389,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -422,7 +431,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -462,7 +472,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -503,7 +514,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -544,7 +556,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/compat_remote_mcp_and_app.json b/install/integrationctl/agentplugins/planner/testdata/golden/compat_remote_mcp_and_app.json index 8fbb5d9e9..2a74de414 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/compat_remote_mcp_and_app.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/compat_remote_mcp_and_app.json @@ -79,7 +79,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -141,7 +142,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -203,7 +205,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -266,7 +269,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -328,7 +332,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -390,7 +395,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -453,7 +459,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -516,7 +523,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -579,7 +587,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -642,7 +651,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -704,7 +714,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -767,7 +778,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -829,7 +841,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/compat_skills_only.json b/install/integrationctl/agentplugins/planner/testdata/golden/compat_skills_only.json index 09692c991..2937a612d 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/compat_skills_only.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/compat_skills_only.json @@ -51,7 +51,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -87,7 +88,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -123,7 +125,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -160,7 +163,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -197,7 +201,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -234,7 +239,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -271,7 +277,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -308,7 +315,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -345,7 +353,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -382,7 +391,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -418,7 +428,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -455,7 +466,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -492,7 +504,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/compat_stdio_mcp.json b/install/integrationctl/agentplugins/planner/testdata/golden/compat_stdio_mcp.json index 6111451ec..20e9c754b 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/compat_stdio_mcp.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/compat_stdio_mcp.json @@ -62,7 +62,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -105,7 +106,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -148,7 +150,8 @@ }, "PackageMode": "compatibility_projection", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "projected" }, @@ -192,7 +195,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -236,7 +240,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -280,7 +285,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -324,7 +330,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -368,7 +375,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -412,7 +420,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -456,7 +465,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -499,7 +509,8 @@ }, "PackageMode": "native", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "native" }, @@ -543,7 +554,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, @@ -587,7 +599,8 @@ }, "PackageMode": "prepared_package", "Scopes": [ - "user" + "user", + "project" ], "SkillSupport": "prepared" }, diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/plan_detected_divergence.json b/install/integrationctl/agentplugins/planner/testdata/golden/plan_detected_divergence.json index f7c49de4e..af61802dc 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/plan_detected_divergence.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/plan_detected_divergence.json @@ -33,6 +33,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/vscode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/vscode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -77,6 +82,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/vscode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/vscode", "UserActions": [ "verify the plugin's authentication requirements before using it", diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/plan_invalid_components.json b/install/integrationctl/agentplugins/planner/testdata/golden/plan_invalid_components.json index a6ee8edd1..87321aadf 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/plan_invalid_components.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/plan_invalid_components.json @@ -35,6 +35,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/chatgpt" + }, "TargetRoot": "\u003croot\u003e/managed/clients/chatgpt", "UserActions": null, "Verification": "package_validated", @@ -81,6 +86,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/clients/claude", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/claude" + }, "TargetRoot": "\u003croot\u003e/clients/claude/skills", "UserActions": null, "Verification": "package_validated", @@ -115,7 +125,7 @@ "LocalActions": null, "LocalEntryObservation": null, "LocalPreparationAuthorized": false, - "NativeRegistryExecutable": "\u003croot\u003e/bin/cline", + "NativeRegistryExecutable": "", "NativeRegistryRoot": "\u003croot\u003e/clients/cline", "OpenCodeHost": null, "PackageMode": "native", @@ -127,6 +137,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cline" + }, "TargetRoot": "\u003croot\u003e/managed/clients/cline", "UserActions": null, "Verification": "package_validated", @@ -173,6 +188,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/codex" + }, "TargetRoot": "\u003croot\u003e/managed/clients/codex", "UserActions": null, "Verification": "package_validated", @@ -219,6 +239,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/copilot" + }, "TargetRoot": "\u003croot\u003e/managed/clients/copilot", "UserActions": null, "Verification": "package_validated", @@ -265,6 +290,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/clients/cursor", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cursor" + }, "TargetRoot": "\u003croot\u003e/clients/cursor/plugins/local", "UserActions": null, "Verification": "package_validated", @@ -311,6 +341,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/gemini" + }, "TargetRoot": "\u003croot\u003e/managed/clients/gemini", "UserActions": null, "Verification": "package_validated", @@ -357,6 +392,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/clients/grok", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/grok" + }, "TargetRoot": "\u003croot\u003e/clients/grok/plugins", "UserActions": null, "Verification": "package_validated", @@ -403,6 +443,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/clients/kimi", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kimi" + }, "TargetRoot": "\u003croot\u003e/clients/kimi/plugins/managed", "UserActions": null, "Verification": "package_validated", @@ -449,6 +494,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kiro" + }, "TargetRoot": "\u003croot\u003e/managed/clients/kiro", "UserActions": null, "Verification": "package_validated", @@ -495,6 +545,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/opencode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/opencode", "UserActions": null, "Verification": "package_validated", @@ -541,6 +596,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/qoder" + }, "TargetRoot": "\u003croot\u003e/managed/clients/qoder", "UserActions": null, "Verification": "package_validated", @@ -587,6 +647,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/vscode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/vscode", "UserActions": null, "Verification": "package_validated", @@ -633,6 +698,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/windsurf" + }, "TargetRoot": "\u003croot\u003e/managed/clients/windsurf", "UserActions": null, "Verification": "package_validated", diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/plan_remote_mcp_and_app.json b/install/integrationctl/agentplugins/planner/testdata/golden/plan_remote_mcp_and_app.json index 3668c17da..c84c86bcf 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/plan_remote_mcp_and_app.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/plan_remote_mcp_and_app.json @@ -59,6 +59,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/chatgpt" + }, "TargetRoot": "\u003croot\u003e/managed/clients/chatgpt", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -130,6 +135,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/claude", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/claude" + }, "TargetRoot": "\u003croot\u003e/clients/claude/skills", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -189,7 +199,7 @@ "LocalActions": null, "LocalEntryObservation": null, "LocalPreparationAuthorized": false, - "NativeRegistryExecutable": "\u003croot\u003e/bin/cline", + "NativeRegistryExecutable": "", "NativeRegistryRoot": "\u003croot\u003e/clients/cline", "OpenCodeHost": null, "PackageMode": "native", @@ -201,6 +211,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cline" + }, "TargetRoot": "\u003croot\u003e/managed/clients/cline", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -272,6 +287,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/codex" + }, "TargetRoot": "\u003croot\u003e/managed/clients/codex", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -343,6 +363,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/copilot" + }, "TargetRoot": "\u003croot\u003e/managed/clients/copilot", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -414,6 +439,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/clients/cursor", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cursor" + }, "TargetRoot": "\u003croot\u003e/clients/cursor/plugins/local", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -486,6 +516,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/gemini" + }, "TargetRoot": "\u003croot\u003e/managed/clients/gemini", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -557,6 +592,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/grok", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/grok" + }, "TargetRoot": "\u003croot\u003e/clients/grok/plugins", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -628,6 +668,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/kimi", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kimi" + }, "TargetRoot": "\u003croot\u003e/clients/kimi/plugins/managed", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -699,6 +744,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kiro" + }, "TargetRoot": "\u003croot\u003e/managed/clients/kiro", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -770,6 +820,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/opencode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/opencode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -841,6 +896,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/qoder" + }, "TargetRoot": "\u003croot\u003e/managed/clients/qoder", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -912,6 +972,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/vscode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/vscode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -983,6 +1048,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/windsurf" + }, "TargetRoot": "\u003croot\u003e/managed/clients/windsurf", "UserActions": [ "verify the plugin's authentication requirements before using it", diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/plan_skills_only.json b/install/integrationctl/agentplugins/planner/testdata/golden/plan_skills_only.json index 1b200a0cf..ad269062d 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/plan_skills_only.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/plan_skills_only.json @@ -35,6 +35,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/chatgpt" + }, "TargetRoot": "\u003croot\u003e/managed/clients/chatgpt", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -82,6 +87,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/claude", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/claude" + }, "TargetRoot": "\u003croot\u003e/clients/claude/skills", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -117,7 +127,7 @@ "LocalActions": null, "LocalEntryObservation": null, "LocalPreparationAuthorized": false, - "NativeRegistryExecutable": "\u003croot\u003e/bin/cline", + "NativeRegistryExecutable": "", "NativeRegistryRoot": "\u003croot\u003e/clients/cline", "OpenCodeHost": null, "PackageMode": "native", @@ -129,6 +139,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cline" + }, "TargetRoot": "\u003croot\u003e/managed/clients/cline", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -176,6 +191,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/codex" + }, "TargetRoot": "\u003croot\u003e/managed/clients/codex", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -223,6 +243,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/copilot" + }, "TargetRoot": "\u003croot\u003e/managed/clients/copilot", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -270,6 +295,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/clients/cursor", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cursor" + }, "TargetRoot": "\u003croot\u003e/clients/cursor/plugins/local", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -318,6 +348,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/gemini" + }, "TargetRoot": "\u003croot\u003e/managed/clients/gemini", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -365,6 +400,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/grok", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/grok" + }, "TargetRoot": "\u003croot\u003e/clients/grok/plugins", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -412,6 +452,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/kimi", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kimi" + }, "TargetRoot": "\u003croot\u003e/clients/kimi/plugins/managed", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -459,6 +504,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kiro" + }, "TargetRoot": "\u003croot\u003e/managed/clients/kiro", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -506,6 +556,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/opencode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/opencode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -553,6 +608,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/qoder" + }, "TargetRoot": "\u003croot\u003e/managed/clients/qoder", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -600,6 +660,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/vscode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/vscode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -647,6 +712,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/windsurf" + }, "TargetRoot": "\u003croot\u003e/managed/clients/windsurf", "UserActions": [ "verify the plugin's authentication requirements before using it", diff --git a/install/integrationctl/agentplugins/planner/testdata/golden/plan_stdio_mcp.json b/install/integrationctl/agentplugins/planner/testdata/golden/plan_stdio_mcp.json index 67531aaa8..70ee82267 100644 --- a/install/integrationctl/agentplugins/planner/testdata/golden/plan_stdio_mcp.json +++ b/install/integrationctl/agentplugins/planner/testdata/golden/plan_stdio_mcp.json @@ -43,6 +43,11 @@ "SelectedDelivery": {}, "Status": "unsupported", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/chatgpt" + }, "TargetRoot": "\u003croot\u003e/managed/clients/chatgpt", "UserActions": [ "this package is not ready for ChatGPT. Connect its remote MCP server in ChatGPT Plugins developer mode; full plugin installation also needs the publisher's registered connection mapping (.app.json). You do not need to create this file. Setup: https://developers.openai.com/plugins/build/plugins for: local" @@ -98,6 +103,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/claude", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/claude" + }, "TargetRoot": "\u003croot\u003e/clients/claude/skills", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -141,7 +151,7 @@ "LocalActions": null, "LocalEntryObservation": null, "LocalPreparationAuthorized": false, - "NativeRegistryExecutable": "\u003croot\u003e/bin/cline", + "NativeRegistryExecutable": "", "NativeRegistryRoot": "\u003croot\u003e/clients/cline", "OpenCodeHost": null, "PackageMode": "native", @@ -153,6 +163,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cline" + }, "TargetRoot": "\u003croot\u003e/managed/clients/cline", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -208,6 +223,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/codex" + }, "TargetRoot": "\u003croot\u003e/managed/clients/codex", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -263,6 +283,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/copilot" + }, "TargetRoot": "\u003croot\u003e/managed/clients/copilot", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -318,6 +343,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/clients/cursor", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/cursor" + }, "TargetRoot": "\u003croot\u003e/clients/cursor/plugins/local", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -374,6 +404,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/gemini" + }, "TargetRoot": "\u003croot\u003e/managed/clients/gemini", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -429,6 +464,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/grok", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/grok" + }, "TargetRoot": "\u003croot\u003e/clients/grok/plugins", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -484,6 +524,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/clients/kimi", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kimi" + }, "TargetRoot": "\u003croot\u003e/clients/kimi/plugins/managed", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -539,6 +584,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/kiro" + }, "TargetRoot": "\u003croot\u003e/managed/clients/kiro", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -594,6 +644,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/opencode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/opencode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -649,6 +704,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/qoder" + }, "TargetRoot": "\u003croot\u003e/managed/clients/qoder", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -704,6 +764,11 @@ "SelectedDelivery": {}, "Status": "manual_activation_required", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/vscode" + }, "TargetRoot": "\u003croot\u003e/managed/clients/vscode", "UserActions": [ "verify the plugin's authentication requirements before using it", @@ -759,6 +824,11 @@ "SelectedDelivery": {}, "Status": "ready", "TargetAnchor": "\u003croot\u003e/managed", + "TargetContext": { + "ProjectRoot": "", + "Scope": "user", + "UserConfigRoot": "\u003croot\u003e/clients/windsurf" + }, "TargetRoot": "\u003croot\u003e/managed/clients/windsurf", "UserActions": [ "verify the plugin's authentication requirements before using it", diff --git a/install/integrationctl/agentplugins/providers/activator.go b/install/integrationctl/agentplugins/providers/activator.go index 67206ebde..d696b8af7 100644 --- a/install/integrationctl/agentplugins/providers/activator.go +++ b/install/integrationctl/agentplugins/providers/activator.go @@ -30,8 +30,12 @@ func (activator Activator) requireRegistry() error { return nil } -func requireNativeConfigKernel(id domain.ClientID, kernel nativeconfig.Kernel) error { - if domain.ClientTraitsFor(id).LifecycleKind != domain.LifecycleNativeConfig { +func requireNativeConfigKernel(id domain.ClientID, kernel nativeconfig.Kernel, scopes ...domain.InstallScope) error { + scope := domain.ScopeUser + if len(scopes) > 0 && scopes[0] != "" { + scope = scopes[0] + } + if (domain.SelectedDelivery{}).EffectiveTraitsForScope(id, scope).LifecycleKind != domain.LifecycleNativeConfig { return nil } return kernel.RequireFileIO() @@ -90,7 +94,7 @@ func (activator Activator) Deactivate(ctx context.Context, request domain.Deacti if err := activator.requireRegistry(); err != nil { return domain.DeactivationOutcome{}, err } - if err := requireNativeConfigKernel(request.Client.ClientID, activator.nativeConfigKernel()); err != nil { + if err := requireNativeConfigKernel(request.Client.ClientID, activator.nativeConfigKernel(), request.TargetContext.Scope); err != nil { return domain.DeactivationOutcome{}, err } if lifecycle, ok := clients.As[clients.Lifecycle](activator.Registry, request.Client.ClientID); ok { @@ -106,7 +110,7 @@ func (activator Activator) Activate(ctx context.Context, request domain.Activati if err := activator.requireRegistry(); err != nil { return domain.ActivationOutcome{}, err } - if err := requireNativeConfigKernel(request.Client.ClientID, activator.nativeConfigKernel()); err != nil { + if err := requireNativeConfigKernel(request.Client.ClientID, activator.nativeConfigKernel(), request.Plan.Scope); err != nil { return domain.ActivationOutcome{}, err } if err := shared.ActivationIdentityMismatch(request); err != nil { diff --git a/install/integrationctl/agentplugins/providers/native_identity.go b/install/integrationctl/agentplugins/providers/native_identity.go index 68b3368e7..8c4c9bfa0 100644 --- a/install/integrationctl/agentplugins/providers/native_identity.go +++ b/install/integrationctl/agentplugins/providers/native_identity.go @@ -72,7 +72,7 @@ func (observer NativeIdentityObserver) ObserveNativeIdentity(ctx context.Context return observer.observeIdentity(ctx, client, plan, managed, true) } -// ObservePreparedIdentity inspects only filesystem-backed package identities. +// ObservePreparedIdentity inspects filesystem-backed package and project component identities. // It deliberately excludes native CLI discovery so dry-run cannot launch the // detected client executable. func (observer NativeIdentityObserver) ObservePreparedIdentity(ctx context.Context, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (domain.NativeIdentityObservation, error) { @@ -90,10 +90,15 @@ func (observer NativeIdentityObserver) observeIdentity(ctx context.Context, clie if err := observer.requireRegistry(); err != nil { return domain.NativeIdentityObservation{State: domain.NativeIdentityIndeterminate}, err } - if err := requireNativeConfigKernel(client.ClientID, observer.nativeConfigKernel()); err != nil { + if err := requireNativeConfigKernel(client.ClientID, observer.nativeConfigKernel(), plan.Scope); err != nil { return domain.NativeIdentityObservation{State: domain.NativeIdentityIndeterminate}, err } prepared, preparedErr := observer.inspectPreparedRegistry(client.ClientID, plan, name, managed != nil) + if !includeNativeRegistry { + local, localErr := observer.inspectPreparedProjectFiles(ctx, client, plan, managed) + prepared = combinePreparedFindings(prepared, local) + preparedErr = errors.Join(preparedErr, localErr) + } if preparedErr != nil { prepared = registryIndeterminate } diff --git a/install/integrationctl/agentplugins/providers/native_identity_project.go b/install/integrationctl/agentplugins/providers/native_identity_project.go new file mode 100644 index 000000000..e54d7e7ef --- /dev/null +++ b/install/integrationctl/agentplugins/providers/native_identity_project.go @@ -0,0 +1,67 @@ +package providers + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Project component files are an ownership boundary even when live discovery +// is disabled. Their findings stay on the prepared side: a file match cannot +// authorize native recovery or claim that a client loaded the component. +func (observer NativeIdentityObserver) inspectPreparedProjectFiles(ctx context.Context, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (registryFinding, error) { + target := plan.TargetContext + if plan.Scope != domain.ScopeProject && target.Scope != domain.ScopeProject && target.ProjectRoot == "" { + return registryClear, nil + } + if client.ClientID != plan.ClientID || plan.Scope != domain.ScopeProject || target.Scope != domain.ScopeProject || + target.UserConfigRoot != client.ConfigRoot { + return registryIndeterminate, fmt.Errorf("project file observation requires matching client, scope and original user profile") + } + if _, err := domain.NewTargetContext(target.Scope, target.ProjectRoot, target.UserConfigRoot); err != nil { + return registryIndeterminate, err + } + canonical, err := filepath.EvalSymlinks(target.ProjectRoot) + if err != nil { + return registryIndeterminate, err + } + info, err := os.Stat(canonical) + if err != nil { + return registryIndeterminate, err + } + if canonical != target.ProjectRoot || !info.IsDir() { + return registryIndeterminate, fmt.Errorf("project file observation requires the exact canonical project directory") + } + switch plan.SelectedDelivery.EffectiveTraitsForScope(client.ClientID, plan.Scope).LifecycleKind { + case domain.LifecycleNativeConfig: + case domain.LifecycleCLIRegistry, domain.LifecyclePrepared: + return registryClear, nil + default: + return registryIndeterminate, fmt.Errorf("project file observation requires a qualified scoped lifecycle") + } + // The adapter receives the same injected kernel and complete ownership + // receipts, but no runner regardless of the caller's live-discovery policy. + observer.Runner = nil + return observer.inspectNativeRegistry(ctx, client, plan, managed) +} + +func combinePreparedFindings(first, second registryFinding) registryFinding { + valid := func(finding registryFinding) registryFinding { + switch finding { + case registryCollision, registryIndeterminate, registryExpected, registryClear: + return finding + default: + return registryIndeterminate + } + } + first, second = valid(first), valid(second) + for _, finding := range []registryFinding{registryCollision, registryIndeterminate, registryExpected, registryClear} { + if first == finding || second == finding { + return finding + } + } + return registryIndeterminate +} diff --git a/install/integrationctl/agentplugins/providers/native_identity_project_test.go b/install/integrationctl/agentplugins/providers/native_identity_project_test.go new file mode 100644 index 000000000..ea1a08375 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/native_identity_project_test.go @@ -0,0 +1,207 @@ +package providers + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type preparedProjectAdapterID struct{ id domain.ClientID } + +func (a preparedProjectAdapterID) ID() domain.ClientID { return a.id } + +type preparedProjectInspector struct { + preparedProjectAdapterID + finding clients.RegistryFinding + err error + calls int + env clients.Env + client domain.DetectedClient + plan domain.DeliveryPlan + managed *domain.ClientBinding +} + +func (a *preparedProjectInspector) InspectPreparedRegistry(domain.DeliveryPlan, string, bool) (clients.RegistryFinding, error) { + return clients.RegistryClear, nil +} +func (a *preparedProjectInspector) UsesNativeRegistryExecutable() bool { return true } +func (a *preparedProjectInspector) InspectNativeRegistry(_ context.Context, env clients.Env, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (clients.RegistryFinding, error) { + a.calls++ + a.env, a.client, a.plan, a.managed = env, client, plan, managed + return a.finding, a.err +} + +// A distinct injected FileIO lets the test verify the exact supplied kernel +// crosses the composition boundary; this identity-only spy does not mutate it. +type preparedProjectFileIO struct{} + +func (*preparedProjectFileIO) ReadNoFollow(string) ([]byte, os.FileMode, bool, error) { + return nil, 0, false, errors.New("unexpected file read") +} +func (*preparedProjectFileIO) WriteAtomic(string, []byte, os.FileMode) error { + return errors.New("unexpected file write") +} +func (*preparedProjectFileIO) RemoveNoFollow(string) error { + return errors.New("unexpected file remove") +} +func preparedProjectPlan(t *testing.T) (domain.DetectedClient, domain.DeliveryPlan) { + t.Helper() + base, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + root := filepath.Join(base, "A") + if err := os.Mkdir(root, 0700); err != nil { + t.Fatal(err) + } + user := filepath.Join(base, "user") + client := domain.DetectedClient{ClientID: domain.ClientCodex, ConfigRoot: user} + plan := identityPlan(filepath.Join(root, "managed")) + plan.ClientID = client.ClientID + plan.Scope = domain.ScopeProject + plan.TargetContext = domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: root, UserConfigRoot: user} + return client, plan +} +func TestPreparedProjectFileFindingRetainsDiscoveryBoundary(t *testing.T) { + for _, tc := range []struct { + name string + finding clients.RegistryFinding + owned bool + want domain.NativeIdentityState + failure bool + }{ + {"owned", clients.RegistryExpected, true, domain.NativeIdentityManaged, false}, + {"unowned-expected", clients.RegistryExpected, false, domain.NativeIdentityUnmanaged, false}, + {"collision", clients.RegistryCollision, true, domain.NativeIdentityUnmanaged, false}, + {"indeterminate", clients.RegistryIndeterminate, true, domain.NativeIdentityIndeterminate, false}, + {"unknown", clients.RegistryFinding(255), true, domain.NativeIdentityIndeterminate, false}, + {"error", clients.RegistryClear, true, domain.NativeIdentityIndeterminate, true}, + } { + t.Run(tc.name, func(t *testing.T) { + client, plan := preparedProjectPlan(t) + if err := os.MkdirAll(plan.ActivePath, 0700); err != nil { + t.Fatal(err) + } + a := &preparedProjectInspector{preparedProjectAdapterID: preparedProjectAdapterID{client.ClientID}, finding: tc.finding} + sentinel := errors.New("file observation failed") + if tc.failure { + a.err = sentinel + } + registry, err := clients.NewRegistry(a) + if err != nil { + t.Fatal(err) + } + kernel := nativeconfig.NewWithFileIO(&preparedProjectFileIO{}) + runner := &identityRunner{} + observer := NativeIdentityObserver{Stager: acceptingPackageVerifier{}, Runner: runner, NativeConfig: &kernel, Registry: registry} + var managed *domain.ClientBinding + if tc.owned { + managed = &domain.ClientBinding{ClientID: "codex", Scope: "project", NativeObjects: []domain.NativeObjectOwnership{{Kind: "managed_package_directory", ManagedDigest: "sha256:owned"}}} + } + got, err := observer.ObservePreparedIdentity(t.Context(), client, plan, managed) + if got.State != tc.want || (err != nil) != tc.failure || tc.failure && !errors.Is(err, sentinel) { + t.Fatalf("finding %+v %v", got, err) + } + if got.NativeDiscoveryAttempted || got.NativeDiscoveryReconciled || got.NativeDiscoveryState != domain.NativeIdentityAbsent { + t.Fatalf("local files became native discovery %+v", got) + } + if a.calls != 1 || a.env.Runner != nil || len(runner.commands) != 0 || !reflect.DeepEqual(a.env.NativeConfig, kernel) || a.managed != managed || !reflect.DeepEqual(a.plan, plan) || !reflect.DeepEqual(a.client, client) { + t.Fatal("file inspector lost supplied authority/kernel or received native runner") + } + }) + } +} +func TestPreparedProjectFileObservationRefusesMalformedAuthority(t *testing.T) { + for _, bad := range []string{"client", "scope", "target-scope", "missing-root", "relative-root", "other-user", "missing-user", "unqualified-policy", "project-alias"} { + t.Run(bad, func(t *testing.T) { + client, plan := preparedProjectPlan(t) + a := &preparedProjectInspector{preparedProjectAdapterID: preparedProjectAdapterID{client.ClientID}, finding: clients.RegistryClear} + registry, err := clients.NewRegistry(a) + if err != nil { + t.Fatal(err) + } + kernel := nativeconfig.New() + switch bad { + case "client": + plan.ClientID = domain.ClientCline + case "scope": + plan.Scope = domain.ScopeUser + case "target-scope": + plan.TargetContext.Scope = domain.ScopeUser + case "missing-root": + plan.TargetContext.ProjectRoot = "" + case "relative-root": + plan.TargetContext.ProjectRoot = "relative" + case "other-user": + plan.TargetContext.UserConfigRoot = filepath.Dir(client.ConfigRoot) + case "missing-user": + plan.TargetContext.UserConfigRoot = "" + case "unqualified-policy": + client.ClientID = domain.ClientChatGPT + plan.ClientID = client.ClientID + a.id = client.ClientID + registry, err = clients.NewRegistry(a) + if err != nil { + t.Fatal(err) + } + case "project-alias": + link := plan.TargetContext.ProjectRoot + "-alias" + if err := os.Symlink(plan.TargetContext.ProjectRoot, link); err != nil { + t.Skip("symlink unavailable") + } + plan.TargetContext.ProjectRoot = link + } + got, err := (NativeIdentityObserver{Registry: registry, NativeConfig: &kernel}).ObservePreparedIdentity(t.Context(), client, plan, nil) + if err == nil || got.State != domain.NativeIdentityIndeterminate || got.NativeDiscoveryReconciled || a.calls != 0 { + t.Fatalf("malformed authority entered file registry: %+v %v calls=%d", got, err, a.calls) + } + }) + } +} +func TestPreparedProjectFileObservationMissingCapabilityFailsClosed(t *testing.T) { + for _, missing := range []string{"inspector", "kernel"} { + t.Run(missing, func(t *testing.T) { + client, plan := preparedProjectPlan(t) + var adapter clients.Adapter = preparedProjectAdapterID{client.ClientID} + kernel := nativeconfig.New() + observer := NativeIdentityObserver{NativeConfig: &kernel} + if missing == "kernel" { + adapter = &preparedProjectInspector{preparedProjectAdapterID: preparedProjectAdapterID{client.ClientID}} + observer.NativeConfig = nil + } + var err error + observer.Registry, err = clients.NewRegistry(adapter) + if err != nil { + t.Fatal(err) + } + got, err := observer.ObservePreparedIdentity(t.Context(), client, plan, nil) + if got.State != domain.NativeIdentityIndeterminate || got.NativeDiscoveryAttempted || got.NativeDiscoveryReconciled { + t.Fatalf("missing %s was accepted %+v %v", missing, got, err) + } + }) + } +} + +func TestPreparedProjectCLIStillDoesNotDiscoverNativeRegistry(t *testing.T) { + client, plan := preparedProjectPlan(t) + client.ClientID = domain.ClientClaude + plan.ClientID = client.ClientID + plan.NativeRegistryExecutable = "/forbidden/claude" + adapter := &preparedProjectInspector{preparedProjectAdapterID: preparedProjectAdapterID{client.ClientID}, finding: clients.RegistryExpected} + registry, err := clients.NewRegistry(adapter) + if err != nil { + t.Fatal(err) + } + runner := &identityRunner{} + got, err := (NativeIdentityObserver{Registry: registry, Runner: runner}).ObservePreparedIdentity(t.Context(), client, plan, nil) + if err != nil || got.State != domain.NativeIdentityAbsent || got.NativeDiscoveryAttempted || got.NativeDiscoveryReconciled || adapter.calls != 0 || len(runner.commands) != 0 { + t.Fatalf("CLI prepared path discovered native registry: %+v %v", got, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/opencode_namespace.go b/install/integrationctl/agentplugins/providers/opencode_namespace.go index 2b92a9a7c..bf42c251c 100644 --- a/install/integrationctl/agentplugins/providers/opencode_namespace.go +++ b/install/integrationctl/agentplugins/providers/opencode_namespace.go @@ -24,7 +24,10 @@ func (guard OpenCodeNamespacePreflight) CheckMCPNamespace(ctx context.Context, c if !domain.ClientTraitsFor(client.ClientID).ReportsMCPToolNamespaceCollision || len(domain.SelectedMCPNames(plan)) == 0 { return nil } - root := strings.TrimSpace(client.ConfigRoot) + root, err := openCodeNamespaceRoot(client, plan) + if err != nil { + return err + } if !filepath.IsAbs(root) { return fmt.Errorf("OpenCode user config root is unavailable") } @@ -53,6 +56,23 @@ func (guard OpenCodeNamespacePreflight) CheckMCPNamespace(ctx context.Context, c return err } +func openCodeNamespaceRoot(client domain.DetectedClient, plan domain.DeliveryPlan) (string, error) { + if plan.Scope != domain.ScopeProject && plan.TargetContext.Scope != domain.ScopeProject { + return strings.TrimSpace(client.ConfigRoot), nil + } + if plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject { + return "", fmt.Errorf("OpenCode namespace scope differs from its target context") + } + target, err := domain.NewTargetContext(plan.TargetContext.Scope, plan.TargetContext.ProjectRoot, plan.TargetContext.UserConfigRoot) + if err != nil { + return "", err + } + if plan.NativeRegistryRoot != target.ProjectRoot || target.UserConfigRoot != "" && target.UserConfigRoot != client.ConfigRoot { + return "", fmt.Errorf("OpenCode namespace differs from the selected project and user context") + } + return target.ProjectRoot, nil +} + func (guard OpenCodeNamespacePreflight) inspectOwnedOpenCodeMCP(paths nativeconfig.Paths, managed *domain.ClientBinding) ([]string, nativeconfig.Codec, []nativeconfig.Receipt, error) { previous := []string{} var source nativeconfig.Codec diff --git a/install/integrationctl/agentplugins/providers/opencode_prepare_test.go b/install/integrationctl/agentplugins/providers/opencode_prepare_test.go index 3d3f8170a..1f89a2264 100644 --- a/install/integrationctl/agentplugins/providers/opencode_prepare_test.go +++ b/install/integrationctl/agentplugins/providers/opencode_prepare_test.go @@ -64,15 +64,16 @@ func TestOpenCodeClientPreparationFreezesExplicitNativeTarget(t *testing.T) { if err := host.ValidateNative(false, []string{"stdio"}); err == nil { t.Fatal("prepared HTTP selection authorized a new stdio effect") } - file, err := os.OpenFile(v2, os.O_WRONLY|os.O_APPEND, 0) + // Replace the executed fixture inode rather than truncating/appending to + // a file the kernel may still have mapped after the completed probe. + body, err := os.ReadFile(v2) if err != nil { t.Fatal(err) } - if _, err := file.Write([]byte("changed-after-preflight")); err != nil { - _ = file.Close() + if err := os.WriteFile(v2+".next", append(body, []byte("changed-after-preflight")...), 0700); err != nil { t.Fatal(err) } - if err := file.Close(); err != nil { + if err := os.Rename(v2+".next", v2); err != nil { t.Fatal(err) } if _, err := preparer.PrepareClient(t.Context(), envelope, client, nil, v2, false); !errors.Is(err, clientdetect.ErrProbeTargetChanged) { diff --git a/install/integrationctl/agentplugins/providers/project_claude_fixture_test.go b/install/integrationctl/agentplugins/providers/project_claude_fixture_test.go new file mode 100644 index 000000000..4dad9698a --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_claude_fixture_test.go @@ -0,0 +1,149 @@ +package providers_test + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/claude" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/commandext" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type claudeProjectFixture struct { + request domain.ActivationRequest + user, other, source string + registry *clients.Registry + stager providers.Stager + runner *claudeProjectRunner +} + +func newClaudeProjectFixture(t *testing.T) *claudeProjectFixture { + t.Helper() + base := projectGeminiTemp(t) + project, user, other := filepath.Join(base, "A"), filepath.Join(base, "user", ".claude"), filepath.Join(base, "B") + for _, root := range []string{user, filepath.Join(project, ".claude"), filepath.Join(other, ".claude")} { + projectGeminiWrite(t, filepath.Join(root, "settings.json"), `{"enabledPlugins":{"foreign@skills-dir":false}}`) + projectGeminiWrite(t, filepath.Join(root, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + registry, err := clients.NewRegistry(claude.New()) + if err != nil { + t.Fatal(err) + } + f := &claudeProjectFixture{user: user, other: other, source: filepath.Join(base, "source"), registry: registry} + f.request.Client = domain.DetectedClient{ClientID: domain.ClientClaude, ConfigRoot: user, ExecutablePath: "/inert/claude-not-executed", Status: domain.DetectionDetected} + f.request.Plan.TargetContext, err = domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + f.stager = providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + f.runner = &claudeProjectRunner{t: t, fixture: f, scope: "project", enabled: true} + projectGeminiWrite(t, filepath.Join(f.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert Claude fixture\n---\nVersion one.\n") + projectGeminiWrite(t, filepath.Join(f.source, filepath.FromSlash(commandext.RelativeDirectory), "echo.md"), "---\ndescription: Echo inert marker\ndisable-model-invocation: false\n---\nUAP-CLAUDE-PROJECT-MARKER\n") + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v1"}}}`) + f.stage(t, "initial") + return f +} + +func (f *claudeProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + envelope, err := (loader.Loader{Registry: schemas}).Load(t.Context(), domain.LoadInput{SnapshotRoot: f.source}) + if err != nil { + t.Fatal(err) + } + project := f.request.Plan.TargetContext.ProjectRoot + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: f.registry} + plan, err := planning.Plan(t.Context(), domain.PlanRequest{Envelope: envelope, Client: f.request.Client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.NativeRegistryRoot != filepath.Join(project, ".claude") { + t.Fatalf("Claude project plan=%+v, %v", plan, err) + } + delivery, err := f.stager.Stage(t.Context(), envelope, plan, operation, domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if filepath.Dir(delivery.StagingPath) == plan.TargetRoot { + t.Fatal("staging entered watched skill root") + } + if err := os.MkdirAll(plan.TargetRoot, 0700); err != nil { + t.Fatal(err) + } + if err := os.RemoveAll(plan.ActivePath); err != nil { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + f.request.Plan, f.request.Delivery, f.request.DeclaredName, f.request.BackendExecutable = plan, delivery, "demo", f.request.Client.ExecutablePath +} + +func (f *claudeProjectFixture) activator() providers.Activator { + return providers.Activator{Registry: f.registry, Runner: f.runner} +} + +func (f *claudeProjectFixture) removal() domain.DeactivationRequest { + return domain.DeactivationRequest{Client: f.request.Client, TargetContext: f.request.Plan.TargetContext, NativeRegistryRoot: f.request.Plan.NativeRegistryRoot, + Confirmed: true, BackendExecutable: f.request.BackendExecutable, DeclaredName: "demo", ManagedArtifactPath: f.request.Plan.ActivePath, CurrentActivation: domain.ActivationActive} +} + +// A fixture runner validates production command contracts without executing a +// native client or package. Its listing is fixture evidence only. +type claudeProjectRunner struct { + t *testing.T + fixture *claudeProjectFixture + scope string + enabled bool + wrongPath, malformed bool + suppressed, otherSource bool + calls int +} + +func (r *claudeProjectRunner) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.t.Helper() + f := r.fixture + if !reflect.DeepEqual(command.Argv, []string{f.request.Client.ExecutablePath, "plugin", "list", "--json"}) || command.Dir != f.request.Plan.TargetContext.ProjectRoot { + r.t.Fatalf("wrong scoped native command: %+v", command) + } + want := "CLAUDE_CONFIG_DIR=" + f.user + found := false + for _, value := range command.Env { + if value == want { + found = true + } + } + if !found { + r.t.Fatalf("original user authority lost: %q", command.Env) + } + r.calls++ + if r.malformed { + return legacyports.CommandResult{Stdout: []byte(`{"unexpected":true}`)}, nil + } + path := f.request.Plan.ActivePath + if r.wrongPath { + path = filepath.Join(f.other, ".claude", "skills", filepath.Base(path)) + } + rows := []map[string]any{{"id": "demo@skills-dir", "scope": r.scope, "enabled": r.enabled, "installPath": path}} + if r.suppressed { + rows = []map[string]any{{"id": "(suppressed)@skills-dir", "version": "unknown", "scope": "project", "enabled": false, "installPath": "", "notes": []string{"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies."}}} + } + if r.otherSource { + rows = append(rows, map[string]any{"id": "demo@skills-dir", "scope": "user", "enabled": true, "installPath": filepath.Join(f.user, "skills", "demo-user")}) + } + body, err := json.Marshal(rows) + return legacyports.CommandResult{Stdout: body}, err +} diff --git a/install/integrationctl/agentplugins/providers/project_claude_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_claude_lifecycle_test.go new file mode 100644 index 000000000..579efb3ff --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_claude_lifecycle_test.go @@ -0,0 +1,186 @@ +package providers_test + +import ( + "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/claude" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +// Production loader/planner/rendering/staging and provider lifecycle, with an +// inert exact-list runner. This is not a native Claude execution observation. +func TestProjectClaudeComposedProjectionListingAndScopeIsolation(t *testing.T) { + f := newClaudeProjectFixture(t) + user, other := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + actor := f.activator() + for _, verify := range []bool{false, true, false} { + f.request.VerifyOnly = verify + outcome, err := actor.Activate(t.Context(), f.request) + if err != nil || outcome.Activation != domain.ActivationActive || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("exact project listing=%+v,%v", outcome, err) + } + } + active := f.request.Plan.ActivePath + body, err := os.ReadFile(filepath.Join(active, claude.ClaudeRuntimeDirectory, "skills", "demo-echo", "SKILL.md")) + if err != nil || !strings.Contains(string(body), "UAP-CLAUDE-PROJECT-MARKER") { + t.Fatalf("rendered command=%q,%v", body, err) + } + mcp, err := os.ReadFile(filepath.Join(active, ".mcp.json")) + if err != nil || !strings.Contains(string(mcp), "https://managed.test/v1") { + t.Fatalf("project plugin MCP=%q,%v", mcp, err) + } + finding, err := claude.New().InspectNativeRegistry(t.Context(), clients.Env{Runner: f.runner}, f.request.Client, f.request.Plan, &domain.ClientBinding{}) + if err != nil || finding != clients.RegistryExpected { + t.Fatalf("project registry=%v,%v", finding, err) + } + for range 2 { + outcome, err := actor.Deactivate(t.Context(), f.removal()) + if err != nil || !outcome.ExternalRemovalComplete { + t.Fatalf("exact project removal admission=%+v,%v", outcome, err) + } + } + if !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("project listing/projection changed other scopes") + } +} + +func TestProjectClaudeWrongDisabledOrUnknownNativeListingRefuses(t *testing.T) { + for _, condition := range []string{"user-scope", "wrong-project", "malformed"} { + t.Run(condition, func(t *testing.T) { + f := newClaudeProjectFixture(t) + switch condition { + case "user-scope": + f.runner.scope = "user" + case "wrong-project": + f.runner.wrongPath = true + case "disabled": + f.runner.enabled = false + case "malformed": + f.runner.malformed = true + } + before := projectGeminiInventory(t, f.request.Plan.TargetContext.ProjectRoot) + if _, err := f.activator().Activate(t.Context(), f.request); err == nil { + t.Fatal("nonexact listing established activation") + } + if _, err := f.activator().Deactivate(t.Context(), f.removal()); err == nil { + t.Fatal("nonexact listing admitted active removal") + } + f.request.ActivationComplete = true + for _, prepared := range []bool{false, true} { + if prepared { + f.request.Plan.Components[0].Support = domain.SupportPrepared + } + if _, err := f.activator().Activate(t.Context(), f.request); err == nil || !strings.Contains(err.Error(), "activation attestation") { + t.Fatalf("attestation bypassed project listing: %v", err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.request.Plan.TargetContext.ProjectRoot)) { + t.Fatal("read-only listing refusal changed native files") + } + }) + } +} + +func TestProjectClaudeChangedContextRefusesBeforeNativeListing(t *testing.T) { + for _, change := range []string{"scope", "original-user", "registry", "copied-project"} { + t.Run(change, func(t *testing.T) { + f := newClaudeProjectFixture(t) + switch change { + case "scope": + f.request.Plan.TargetContext.Scope = domain.ScopeUser + case "original-user": + f.request.Plan.TargetContext.UserConfigRoot = f.other + case "registry": + f.request.Plan.NativeRegistryRoot = f.user + case "copied-project": + f.request.Plan.TargetContext.ProjectRoot = f.other + f.request.Plan.NativeRegistryRoot = filepath.Join(f.other, ".claude") + } + if _, err := f.activator().Activate(t.Context(), f.request); err == nil { + t.Fatal("changed project context admitted") + } + if f.runner.calls != 0 { + t.Fatal("changed authority reached native runner") + } + }) + } +} + +func TestProjectClaudeBlockedDiscoveryIsPreparedAndInactiveRemovalIsOwned(t *testing.T) { + for _, condition := range []string{"disabled", "suppressed", "suppressed-user"} { + t.Run(condition, func(t *testing.T) { + f := newClaudeProjectFixture(t) + if condition == "disabled" { + f.runner.enabled = false + } else { + f.runner.suppressed = true + } + f.runner.otherSource = condition == "suppressed-user" + beforeUser, beforeB := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + for _, verify := range []bool{false, true} { + f.request.VerifyOnly = verify + out, err := f.activator().Activate(t.Context(), f.request) + if err != nil || out.Activation != domain.ActivationPrepared || out.Verification != domain.VerificationInstalled || out.ActivationAttested || len(out.UserActions) == 0 { + t.Fatalf("blocked observation=%+v,%v", out, err) + } + } + request := f.removal() + if _, err := f.activator().Deactivate(t.Context(), request); err == nil { + t.Fatal("blocked native evidence silently removed formerly active binding") + } + request.CurrentActivation = domain.ActivationPrepared + out, err := f.activator().Deactivate(t.Context(), request) + if err != nil || !out.ExternalRemovalComplete || !out.ArtifactRemovalAllowed { + t.Fatalf("inactive removal admission=%+v,%v", out, err) + } + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, f.other)) { + t.Fatal("blocked lifecycle changed other scope") + } + }) + } +} + +func TestProjectClaudeSuppressionDoesNotSupplyOwnershipOrDiscovery(t *testing.T) { + f := newClaudeProjectFixture(t) + f.runner.suppressed = true + observer := providers.NativeIdentityObserver{Registry: f.registry, Runner: f.runner, Stager: f.stager} + binding := domain.ClientBinding{NativeObjects: f.request.Delivery.NativeObjects} + observe := func(managed *domain.ClientBinding) domain.NativeIdentityObservation { + t.Helper() + out, err := observer.ObserveNativeIdentity(t.Context(), f.request.Client, f.request.Plan, managed) + if err != nil { + t.Fatal(err) + } + if out.NativeDiscoveryReconciled { + t.Fatal("suppression reconciled native discovery") + } + return out + } + if out := observe(&binding); out.State != domain.NativeIdentityManaged || !out.ReceiptReconciled { + t.Fatalf("owned suppressed=%+v", out) + } + if out := observe(nil); out.State != domain.NativeIdentityUnmanaged { + t.Fatalf("unowned exact destination was adopted: %+v", out) + } + if err := os.RemoveAll(f.request.Plan.ActivePath); err != nil { + t.Fatal(err) + } + if out := observe(nil); out.State != domain.NativeIdentityAbsent { + t.Fatalf("clear initial destination=%+v", out) + } + f.runner.suppressed, f.runner.enabled = false, false + if out := observe(nil); out.State != domain.NativeIdentityUnmanaged { + t.Fatalf("unowned disabled native candidate was adopted: %+v", out) + } + f.runner.suppressed = true + projectGeminiWrite(t, filepath.Join(f.request.Plan.TargetRoot, "foreign-same-name", ".claude-plugin", "plugin.json"), `{"name":"demo"}`) + if out := observe(nil); out.State != domain.NativeIdentityUnmanaged { + t.Fatalf("suppression hid foreign project artifact: %+v", out) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_cline_fixture_test.go b/install/integrationctl/agentplugins/providers/project_cline_fixture_test.go new file mode 100644 index 000000000..2e7dfee3e --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_cline_fixture_test.go @@ -0,0 +1,132 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cline" + "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/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +const clineForeignConfig = `{"mcpServers":{"foreign":{"transport":{"type":"streamableHttp","url":"https://foreign.test/mcp"},"disabled":true}},"preference":"keep"}` +const clineSkill = "---\nname: docs\ndescription: Inert Cline project fixture\n---\nKIMI-PROJECT-ONE\n" + +type clineProjectFixture struct { + base, project, user, other, source, data string + config string + request domain.ActivationRequest + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager +} + +func newClineProjectFixture(t *testing.T, stdio bool) *clineProjectFixture { + t.Helper() + f := &clineProjectFixture{base: projectGeminiTemp(t)} + f.project, f.user, f.other, f.source = filepath.Join(f.base, "A"), filepath.Join(f.base, "user"), filepath.Join(f.base, "B"), filepath.Join(f.base, "source") + for _, root := range []string{f.project, f.user, f.other} { + native := filepath.Join(root, ".cline") + if root == f.user { + native = f.user + } + config := filepath.Join(root, ".uap", "clients", "cline", "mcp.json") + if root == f.user { + config = filepath.Join(f.user, "data", "settings", "cline_mcp_settings.json") + } + projectGeminiWrite(t, config, clineForeignConfig) + projectGeminiWrite(t, filepath.Join(native, "skills", "foreign", "SKILL.md"), "foreign skill\n") + if root != f.user { + if err := os.Mkdir(filepath.Join(root, ".git"), 0700); err != nil { + t.Fatal(err) + } + } + } + f.config = filepath.Join(f.project, ".uap", "clients", "cline", "mcp.json") + + var err error + f.registry, err = clients.NewRegistry(cline.New()) + if err != nil { + t.Fatal(err) + } + f.request.Client = domain.DetectedClient{ClientID: domain.ClientCline, ConfigRoot: f.user, ExecutablePath: filepath.Join(f.base, "cline"), Version: "3.0.73", Status: domain.DetectionDetected} + f.stager = providers.Stager{Registry: f.registry, Paths: pathpolicy.Policy{}} + f.data = filepath.Join(f.project, ".uap", "data", "cline-exact-receipt") + projectGeminiWrite(t, filepath.Join(f.data, "keep.txt"), "persistent data\n") + projectGeminiWrite(t, filepath.Join(f.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), clineSkill) + mcp := `{"type":"streamable-http","url":"https://managed.test/one"}` + if stdio { + mcp = `{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_ROOT}","env":{"MARKER":"${PLUGIN_DATA}/marker"}}` + projectGeminiWrite(t, filepath.Join(f.source, "server.js"), "// Inert; never executed.\n") + launcher := filepath.Join(f.base, "TEST-inert-launcher") + if err := os.WriteFile(launcher, []byte("TEST trusted launcher bytes; never executed"), 0700); err != nil { + t.Fatal(err) + } + f.stager.LauncherSource, err = managedstdio.NewSource(launcher, "fixture") + if err != nil { + t.Fatal(err) + } + } + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":`+mcp+`}}`) + f.stage(t, "initial") + return f +} + +func (f *clineProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + f.envelope, err = (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: f.source}) + if err != nil { + t.Fatal(err) + } + planning := planner.Planner{ProjectRoot: f.project, ManagedRoot: filepath.Join(f.project, ".uap", "managed"), Registry: f.registry, Paths: pathpolicy.Policy{}} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: f.envelope, Client: f.request.Client, Scope: domain.ScopeProject, ProjectRoot: f.project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.NativeRegistryRoot != f.project || plan.Activation != domain.ActivationPrepared { + t.Fatalf("Cline repository plan = %+v, %v", plan, err) + } + delivery, err := f.stager.StageWithPluginData(context.Background(), f.envelope, plan, operation, domain.CompatibilityHints{}, f.data) + if err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(plan.ActivePath); err == nil { + if err := os.Rename(plan.ActivePath, plan.ActivePath+"-"+operation+"-old"); err != nil { + t.Fatal(err) + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + f.request.Plan, f.request.Delivery, f.request.DeclaredName = plan, delivery, "demo" +} + +func (f *clineProjectFixture) activator(kernel nativeconfig.Kernel) providers.Activator { + return providers.Activator{Registry: f.registry, NativeConfig: &kernel} +} + +func (f *clineProjectFixture) remove(objects []domain.NativeObjectOwnership) domain.DeactivationRequest { + return domain.DeactivationRequest{Client: f.request.Client, TargetContext: f.request.Plan.TargetContext, + NativeRegistryRoot: f.project, PhysicalArtifactID: f.request.Plan.PhysicalArtifactID, ManagedArtifactPath: f.request.Plan.ActivePath, Confirmed: true, NativeObjects: objects} +} + +func clinePrepared(t *testing.T, outcome domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("Cline configured state = %+v, %v", outcome, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_cline_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_cline_lifecycle_test.go new file mode 100644 index 000000000..53e43fc46 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_cline_lifecycle_test.go @@ -0,0 +1,181 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cline" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Real loader/planner/stager/kernel; the client has no runner at all. +func TestProjectClineComposedFileLifecycleIgnoresAmbientOverrides(t *testing.T) { + f := newClineProjectFixture(t, false) + outside := filepath.Join(f.base, "ambient.json") + projectGeminiWrite(t, outside, clineForeignConfig) + t.Setenv("CLINE_MCP_SETTINGS_PATH", outside) + t.Setenv("CLINE_DATA_DIR", filepath.Join(f.base, "ambient-data")) + user, other := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + actor := f.activator(nativeconfig.New()) + out, err := actor.Activate(t.Context(), f.request) + clinePrepared(t, out, err) + projectGeminiAssert(t, filepath.Join(f.project, ".cline", "skills", "docs", "SKILL.md"), clineSkill) + body, err := os.ReadFile(f.config) + if err != nil || !strings.Contains(string(body), "managed.test/one") || !strings.Contains(string(body), "foreign.test") || !strings.Contains(string(body), `"disabled":true`) { + t.Fatalf("prepared Cline file: %s %v", body, err) + } + action := strings.Join(out.UserActions, " ") + for _, want := range []string{"CLINE_MCP_SETTINGS_PATH", "CLINE_NO_AUTO_UPDATE", "--auto-approve", "whole MCP source", "not merged", "IDE behavior"} { + if !strings.Contains(action, want) { + t.Fatalf("missing truthful action %q: %s", want, action) + } + } + f.request.PreviousNativeObjects, f.request.VerifyOnly = out.NativeObjects, true + replay, err := actor.Activate(t.Context(), f.request) + clinePrepared(t, replay, err) + if replay.NativeEffect != domain.NativeEffectUnchanged { + t.Fatal("verify-only changed native files") + } + for range 2 { + removed, err := actor.Deactivate(t.Context(), f.remove(out.NativeObjects)) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("remove: %+v %v", removed, err) + } + } + if _, err := os.Stat(filepath.Join(f.project, ".cline", "skills", "docs")); !os.IsNotExist(err) { + t.Fatal("owned skill survives", err) + } + body, err = os.ReadFile(f.config) + if err != nil || strings.Contains(string(body), "managed.test") || !strings.Contains(string(body), "foreign.test") { + t.Fatalf("foreign Cline member lost: %s %v", body, err) + } + projectGeminiAssert(t, outside, clineForeignConfig) + if !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("project lifecycle changed user or B") + } +} + +func TestProjectClineRejectsAttestationMalformedContextAndIncompleteDelivery(t *testing.T) { + for _, bad := range []string{"attestation", "both-user", "other-client", "other-profile", "other-artifact", "other-active", "empty-delivery", "empty-selection", "user-object"} { + t.Run(bad, func(t *testing.T) { + f := newClineProjectFixture(t, false) + before := projectGeminiInventory(t, f.base) + switch bad { + case "attestation": + f.request.ActivationComplete = true + case "both-user": + f.request.Plan.Scope, f.request.Plan.TargetContext.Scope = domain.ScopeUser, domain.ScopeUser + case "other-client": + f.request.Client.ClientID = domain.ClientCodex + case "other-profile": + f.request.Plan.TargetContext.UserConfigRoot = f.other + case "other-artifact": + f.request.Plan.PhysicalArtifactID = "foreign" + case "other-active": + f.request.Delivery.ActivePath = f.other + case "empty-delivery": + f.request.Delivery = domain.StagedDelivery{} + case "empty-selection": + f.request.Delivery.NativeObjects = nil + case "user-object": + f.request.Delivery.NativeObjects[0].Kind = cline.ClineSkillObjectKind + } + if _, err := f.activator(nativeconfig.New()).Activate(t.Context(), f.request); err == nil { + t.Fatal("malformed/attested project accepted") + } + if _, err := cline.New().Activate(t.Context(), clients.Env{NativeConfig: nativeconfig.New()}, f.request); err == nil { + t.Fatal("direct adapter bypass") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("refused request changed files") + } + }) + } + f := newClineProjectFixture(t, false) + early := f.request + early.Delivery = domain.StagedDelivery{} + if err := f.activator(nativeconfig.New()).PreflightActivation(early); err != nil { + t.Fatal("legitimate unstaged preflight refused", err) + } +} + +func TestProjectClinePreservesForeignCollisionAndChangedOwnedFiles(t *testing.T) { + for _, tamper := range []bool{false, true} { + t.Run(map[bool]string{false: "foreign", true: "owned-tamper"}[tamper], func(t *testing.T) { + f := newClineProjectFixture(t, false) + actor := f.activator(nativeconfig.New()) + if tamper { + out, err := actor.Activate(t.Context(), f.request) + clinePrepared(t, out, err) + f.request.PreviousNativeObjects = out.NativeObjects + } + path := filepath.Join(f.project, ".cline", "skills", "docs", "SKILL.md") + projectGeminiWrite(t, path, "outside edit") + before := projectGeminiInventory(t, f.base) + if _, err := actor.Activate(context.Background(), f.request); err == nil { + t.Fatal("foreign/tampered skill accepted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("collision changed files") + } + }) + } +} + +func TestProjectClineStdioDataAndCurrentProjection(t *testing.T) { + f := newClineProjectFixture(t, true) + out, err := f.activator(nativeconfig.New()).Activate(t.Context(), f.request) + clinePrepared(t, out, err) + body, err := os.ReadFile(f.config) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{f.request.Plan.ActivePath, f.data} { + if !strings.Contains(string(body), strings.ReplaceAll(want, `\`, `\\`)) { + t.Fatalf("stdio contract missing %q: %s", want, body) + } + } + for _, token := range []string{"${PLUGIN_ROOT}", "${PLUGIN_DATA}", "${package.root}"} { + if strings.Contains(string(body), token) { + t.Fatal("unresolved runtime token", token) + } + } +} + +func TestProjectClineSupportsIndependentSkillAndMCPSelections(t *testing.T) { + for _, selected := range []domain.ComponentKind{domain.ComponentSkill, domain.ComponentMCPServer} { + t.Run(string(selected), func(t *testing.T) { + f := newClineProjectFixture(t, false) + for i := range f.request.Plan.Components { + if f.request.Plan.Components[i].Kind != selected { + f.request.Plan.Components[i].Support = domain.SupportUnsupported + } + } + objects := []domain.NativeObjectOwnership{} + for _, object := range f.request.Delivery.NativeObjects { + skill := strings.Contains(object.Kind, "skill_directory") + if skill == (selected == domain.ComponentSkill) { + objects = append(objects, object) + } + } + f.request.Delivery.NativeObjects = objects + before := projectGeminiInventory(t, f.user) + out, err := f.activator(nativeconfig.New()).Activate(t.Context(), f.request) + clinePrepared(t, out, err) + if selected == domain.ComponentSkill { + projectGeminiAssert(t, f.config, clineForeignConfig) + } else if _, err := os.Stat(filepath.Join(f.project, ".cline", "skills", "docs")); !os.IsNotExist(err) { + t.Fatal("MCP-only delivery wrote a skill", err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.user)) { + t.Fatal("single selection changed user profile") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_codex_fixture_test.go b/install/integrationctl/agentplugins/providers/project_codex_fixture_test.go new file mode 100644 index 000000000..b72b1a6b9 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_codex_fixture_test.go @@ -0,0 +1,114 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/codextoml" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/commandext" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +type projectCodexFixture struct { + user, a, b, source string + registry *clients.Registry + kernel nativeconfig.Kernel + activator providers.Activator +} + +func projectCodexWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func newProjectCodexFixture(t *testing.T) projectCodexFixture { + t.Helper() + base, err := os.MkdirTemp("", "uap-project-codex-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.RemoveAll(base) }) + base, err = filepath.EvalSymlinks(base) + if err != nil { + t.Fatal(err) + } + f := projectCodexFixture{user: filepath.Join(base, "user", ".codex"), a: filepath.Join(base, "A"), b: filepath.Join(base, "B"), source: filepath.Join(base, "source")} + for _, config := range []string{filepath.Join(f.user, "config.toml"), filepath.Join(f.a, ".codex", "config.toml"), filepath.Join(f.b, ".codex", "config.toml")} { + projectCodexWrite(t, config, "# foreign native policy\nmodel = 'foreign-model'\n[mcp_servers.foreign]\nurl='https://foreign.test/mcp' # keep server\n") + } + projectCodexWrite(t, filepath.Join(f.user, "plugins", "cache", "foreign", "keep.txt"), "user plugin cache\n") + for _, root := range []string{f.a, f.b} { + projectCodexWrite(t, filepath.Join(root, ".agents", "skills", "foreign", "SKILL.md"), "foreign repository skill\n") + } + projectCodexWrite(t, filepath.Join(f.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectCodexWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert project fixture\n---\nUAP-CODEX-PROJECT\n") + projectCodexWrite(t, filepath.Join(f.source, filepath.FromSlash(commandext.RelativeDirectory), "echo.md"), "---\ndescription: Inert command\ndisable-model-invocation: false\n---\nUAP-CODEX-COMMAND\n") + projectCodexWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/mcp","headers":{"X-Fixture":"inert"}},"local":{"type":"stdio","command":"go","args":["version"],"env":{"DATA":"${PLUGIN_DATA}"}}}}`) + f.registry, err = clients.NewRegistry(codex.New()) + if err != nil { + t.Fatal(err) + } + f.kernel = nativeconfig.New().WithCodexDocumentParser(codextoml.Parse) + f.activator = providers.Activator{Registry: f.registry, NativeConfig: &f.kernel} + return f +} + +func (f projectCodexFixture) stage(t *testing.T, hints domain.CompatibilityHints) domain.ActivationRequest { + t.Helper() + envelope := f.load(t) + client := domain.DetectedClient{ClientID: domain.ClientCodex, ConfigRoot: f.user, Version: "0.153.4", Status: domain.DetectionDetected} + plan, err := (planner.Planner{ProjectRoot: f.a, ManagedRoot: filepath.Join(f.a, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: f.registry}).Plan(context.Background(), domain.PlanRequest{ + Envelope: envelope, Client: client, Scope: domain.ScopeProject, ProjectRoot: f.a, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared || plan.NativeRegistryRoot != f.a { + t.Fatalf("project Codex plan = %+v, %v", plan, err) + } + delivery, err := (providers.Stager{Registry: f.registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(f.a, ".uap", "data")}).StageWithPluginData(t.Context(), envelope, plan, "project-codex-fixture", hints, filepath.Join(f.a, ".uap", "data", "owned-slot")) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(plan.ActivePath), 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + return domain.ActivationRequest{Client: client, Plan: plan, Delivery: delivery, DeclaredName: "demo"} +} + +func (f projectCodexFixture) load(t *testing.T) domain.PackageEnvelope { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + envelope, err := (loader.Loader{Registry: schemas}).Load(t.Context(), domain.LoadInput{SnapshotRoot: f.source}) + if err != nil { + t.Fatal(err) + } + return envelope +} + +func projectCodexRead(t *testing.T, path string) string { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(body) +} diff --git a/install/integrationctl/agentplugins/providers/project_codex_guards_test.go b/install/integrationctl/agentplugins/providers/project_codex_guards_test.go new file mode 100644 index 000000000..481c4a0b4 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_codex_guards_test.go @@ -0,0 +1,90 @@ +package providers_test + +import ( + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/codex" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectCodexRejectsChangedContextBeforeNativeEffects(t *testing.T) { + for _, change := range []string{"user-registry", "original-user", "plan-scope", "context-scope"} { + t.Run(change, func(t *testing.T) { + f := newProjectCodexFixture(t) + r := f.stage(t, domain.CompatibilityHints{}) + switch change { + case "user-registry": + r.Plan.NativeRegistryRoot = f.user + case "original-user": + r.Plan.TargetContext.UserConfigRoot = f.b + case "plan-scope": + r.Plan.Scope = domain.ScopeUser + case "context-scope": + r.Plan.TargetContext.Scope = domain.ScopeUser + } + before, user := projectGeminiInventory(t, f.a), projectGeminiInventory(t, f.user) + if _, err := f.activator.Activate(t.Context(), r); err == nil { + t.Fatal("changed project context reached native activation") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.a)) || !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) { + t.Fatal("refused context changed project or user files") + } + }) + } +} + +func TestProjectCodexMalformedCollisionAndWriterFailurePreserveNativeFiles(t *testing.T) { + for _, name := range []string{"malformed", "foreign-disabled", "writer-failure"} { + t.Run(name, func(t *testing.T) { + f := newProjectCodexFixture(t) + r := f.stage(t, domain.CompatibilityHints{}) + actor := f.activator + switch name { + case "malformed": + projectCodexWrite(t, filepath.Join(f.a, ".codex", "config.toml"), "[mcp_servers\n") + case "foreign-disabled": + projectCodexWrite(t, filepath.Join(f.a, ".codex", "config.toml"), "[mcp_servers.docs]\nurl='https://foreign.test/keep'\nenabled=false\n") + case "writer-failure": + kernel := nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("TEST writer unavailable") + }) + actor.NativeConfig = &kernel + } + before, user, b := projectGeminiInventory(t, f.a), projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.b) + if _, err := actor.Activate(t.Context(), r); err == nil { + t.Fatal("invalid/failed project native mutation succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.a)) || !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(b, projectGeminiInventory(t, f.b)) { + t.Fatal("failed project mutation left native effects or consumed foreign files") + } + }) + } +} + +func TestProjectCodexRegistryUsesProjectComponentsAndExactBinding(t *testing.T) { + f := newProjectCodexFixture(t) + r := f.stage(t, domain.CompatibilityHints{}) + outcome, err := f.activator.Activate(t.Context(), r) + if err != nil { + t.Fatal(err) + } + r.Plan.PreviousNativeObjects = outcome.NativeObjects + binding := domain.ClientBinding{Scope: string(domain.ScopeProject), NativeProfileRoot: f.a, PhysicalArtifact: r.Plan.PhysicalArtifactID, NativeObjects: outcome.NativeObjects} + adapter := codex.New() + finding, err := adapter.InspectNativeRegistry(t.Context(), clients.Env{NativeConfig: f.kernel}, r.Client, r.Plan, &binding) + if err != nil || finding != clients.RegistryExpected { + t.Fatalf("project component registry = %v, %v", finding, err) + } + if finding, err := adapter.InspectPreparedRegistry(r.Plan, "demo", true); err != nil || finding != clients.RegistryExpected { + t.Fatalf("read-only prepared project registry = %v, %v", finding, err) + } + binding.PhysicalArtifact = "different-artifact" + if _, err := adapter.InspectNativeRegistry(t.Context(), clients.Env{NativeConfig: f.kernel}, r.Client, r.Plan, &binding); err == nil { + t.Fatal("another artifact supplied project ownership") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_codex_scope_test.go b/install/integrationctl/agentplugins/providers/project_codex_scope_test.go new file mode 100644 index 000000000..ff8be7679 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_codex_scope_test.go @@ -0,0 +1,89 @@ +package providers_test + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Production standard loader, planner, renderer, stager, provider and native +// ownership kernel. No Codex session, model or configured MCP is executed. +func TestProjectCodexComponentsUseNativePathsAndPreserveOtherScopes(t *testing.T) { + f := newProjectCodexFixture(t) + request := f.stage(t, domain.CompatibilityHints{OpenAIMCPAuth: map[string]domain.OpenAIMCPAuthHint{"docs": {BearerTokenEnvVar: "TEST_TOKEN_REFERENCE", OAuthResource: "https://managed.test/resource"}}}) + userBefore, bBefore := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.b) + for pass := range 3 { + outcome, err := f.activator.Activate(t.Context(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested || len(outcome.NativeObjects) != 5 { + t.Fatalf("pass %d project activation = %+v, %v", pass, outcome, err) + } + request.PreviousNativeObjects = outcome.NativeObjects + request.VerifyOnly = pass == 0 + if pass == 1 { + if err := os.RemoveAll(filepath.Join(f.a, ".agents", "skills", "docs")); err != nil { + t.Fatal(err) + } + } + } + if body := projectCodexRead(t, filepath.Join(f.a, ".agents", "skills", "docs", "SKILL.md")); !strings.Contains(body, "UAP-CODEX-PROJECT") { + t.Fatal("same-pin missing skill was not repaired") + } + if body := projectCodexRead(t, filepath.Join(f.a, ".agents", "skills", "demo-echo", "SKILL.md")); !strings.Contains(body, "UAP-CODEX-COMMAND") { + t.Fatal("rendered command skill lost its preserved instructions") + } + settings := projectCodexRead(t, filepath.Join(f.a, ".codex", "config.toml")) + for _, value := range []string{"foreign native policy", "foreign.test", "http_headers", "TEST_TOKEN_REFERENCE", "oauth_resource"} { + if !strings.Contains(settings, value) { + t.Fatalf("native TOML lost %q: %s", value, settings) + } + } + remove := domain.DeactivationRequest{Client: request.Client, TargetContext: request.Plan.TargetContext, NativeRegistryRoot: f.a, Confirmed: true, NativeObjects: request.PreviousNativeObjects} + for range 2 { + outcome, err := f.activator.Deactivate(t.Context(), remove) + if err != nil || !outcome.ExternalRemovalComplete { + t.Fatalf("project Codex exact remove = %+v, %v", outcome, err) + } + } + if _, err := os.Stat(filepath.Join(f.a, ".agents", "skills", "docs")); !os.IsNotExist(err) { + t.Fatal("managed skill survived removal") + } + if !reflect.DeepEqual(userBefore, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(bBefore, projectGeminiInventory(t, f.b)) { + t.Fatal("Codex project lifecycle changed user profile/cache or project B") + } + if strings.Contains(projectCodexRead(t, filepath.Join(f.a, ".codex", "config.toml")), "managed.test") { + t.Fatal("managed MCP registration survived removal") + } +} + +func TestProjectCodexRestrictionsAndAttestationRemainPrepared(t *testing.T) { + f := newProjectCodexFixture(t) + request := f.stage(t, domain.CompatibilityHints{}) + projectCodexWrite(t, filepath.Join(f.user, "config.toml"), "[mcp_servers.docs]\nenabled=false\n[[skills.config]]\nname='docs'\nenabled=false\n") + outcome, err := f.activator.Activate(t.Context(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared { + t.Fatalf("restricted project configuration = %+v, %v", outcome, err) + } + actions := strings.Join(outcome.UserActions, "\n") + for _, value := range []string{`disable skill "docs"`, `disable MCP server "docs"`, "project trust", "remain unobserved"} { + if !strings.Contains(actions, value) { + t.Fatalf("project restriction/proof boundary missing %q: %s", value, actions) + } + } + before := projectGeminiInventory(t, f.a) + for _, automatic := range []bool{true, false} { + request.ActivationComplete = true + if !automatic { + request.Plan.Components[0].Support = domain.SupportPrepared + } + if _, err := f.activator.Activate(t.Context(), request); err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("project attestation bypassed preflight: %v", err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.a)) { + t.Fatal("attestation changed project native objects") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_codex_skill_policy_test.go b/install/integrationctl/agentplugins/providers/project_codex_skill_policy_test.go new file mode 100644 index 000000000..0e5371695 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_codex_skill_policy_test.go @@ -0,0 +1,85 @@ +package providers_test + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// These production provider observations report only visible user-file rules. +// Project selectors are not native skill policy; session overrides remain unknown. +func TestProjectCodexSkillWarningsRespectPinnedUserSelectorContract(t *testing.T) { + for _, name := range []string{"disabled-document", "disabled-trimmed-name", "later-name-enable", "later-path-enable", "both-selectors-ignored", "directory-not-document", "project-selector-ignored", "missing-required-enabled", "relative-path-invalidates-section"} { + t.Run(name, func(t *testing.T) { + f := newProjectCodexFixture(t) + request := f.stage(t, domain.CompatibilityHints{}) + document := filepath.Join(f.a, ".agents", "skills", "docs", "SKILL.md") + pathRule := fmt.Sprintf("[[skills.config]]\npath=%q\nenabled=false\n", document) + user, project, want := pathRule, "", true + switch name { + case "disabled-trimmed-name": + user = "[[skills.config]]\nname=' docs '\nenabled=false\n" + case "later-name-enable": + user += "[[skills.config]]\nname='docs'\nenabled=true\n" + want = false + case "later-path-enable": + user = "[[skills.config]]\nname='docs'\nenabled=false\n" + strings.Replace(pathRule, "enabled=false", "enabled=true", 1) + want = false + case "both-selectors-ignored": + user = strings.Replace(pathRule, "enabled=false", "name='docs'\nenabled=false", 1) + want = false + case "directory-not-document": + user = fmt.Sprintf("[[skills.config]]\npath=%q\nenabled=false\n", filepath.Dir(document)) + want = false + case "project-selector-ignored": + user, project, want = "", pathRule, false + case "missing-required-enabled": + user += "[[skills.config]]\nname='unrelated'\n" + want = false + case "relative-path-invalidates-section": + user += "[[skills.config]]\npath='relative/SKILL.md'\nname='unrelated'\nenabled=true\n" + want = false + } + projectCodexWrite(t, filepath.Join(f.user, "config.toml"), user) + if project != "" { + projectCodexWrite(t, filepath.Join(f.a, ".codex", "config.toml"), project) + } + before := projectGeminiInventory(t, f.user) + outcome, err := f.activator.Activate(t.Context(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled { + t.Fatalf("configured project = %+v, %v", outcome, err) + } + if got := strings.Contains(strings.Join(outcome.UserActions, "\n"), `disable skill "docs"`); got != want { + t.Fatalf("disabled warning = %t, want %t: %q", got, want, outcome.UserActions) + } + if got := projectGeminiInventory(t, f.user); !reflect.DeepEqual(got, before) { + t.Fatal("selector observation changed user settings") + } + }) + } +} + +func TestProjectCodexSkillWarningRecognizesCanonicalDocumentAlias(t *testing.T) { + f := newProjectCodexFixture(t) + request := f.stage(t, domain.CompatibilityHints{}) + outcome, err := f.activator.Activate(t.Context(), request) + if err != nil { + t.Fatal(err) + } + document := filepath.Join(f.a, ".agents", "skills", "docs", "SKILL.md") + alias := filepath.Join(f.a, "inert-skill-document-alias") + if err := os.Symlink(document, alias); err != nil { + t.Skipf("document alias unavailable: %v", err) + } + projectCodexWrite(t, filepath.Join(f.user, "config.toml"), fmt.Sprintf("[[skills.config]]\npath=%q\nenabled=false\n", alias)) + request.PreviousNativeObjects, request.VerifyOnly = outcome.NativeObjects, true + outcome, err = f.activator.Activate(t.Context(), request) + if err != nil || !strings.Contains(strings.Join(outcome.UserActions, "\n"), `disable skill "docs"`) || outcome.Activation != domain.ActivationPrepared { + t.Fatalf("canonical document selector = %+v, %v", outcome, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_codex_update_test.go b/install/integrationctl/agentplugins/providers/project_codex_update_test.go new file mode 100644 index 000000000..6f3ecd981 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_codex_update_test.go @@ -0,0 +1,59 @@ +package providers_test + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +func TestProjectCodexUpdateKeepsNativeOwnershipAndDataBindings(t *testing.T) { + f := newProjectCodexFixture(t) + r := f.stage(t, domain.CompatibilityHints{}) + old, err := f.activator.Activate(t.Context(), r) + if err != nil { + t.Fatal(err) + } + user, b := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.b) + projectCodexWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Updated inert project fixture\n---\nUAP-CODEX-UPDATED\n") + mcp := projectCodexRead(t, filepath.Join(f.source, "mcp.json")) + projectCodexWrite(t, filepath.Join(f.source, "mcp.json"), strings.ReplaceAll(mcp, "https://managed.test/mcp", "https://managed.test/v2")) + if err := os.Rename(r.Delivery.ActivePath, r.Delivery.ActivePath+"-previous"); err != nil { + t.Fatal(err) + } + next := f.stage(t, domain.CompatibilityHints{}) + next.PreviousNativeObjects, next.Replacing = old.NativeObjects, true + updated, err := f.activator.Activate(t.Context(), next) + if err != nil || updated.Activation != domain.ActivationPrepared || updated.Verification != domain.VerificationInstalled { + t.Fatalf("project Codex update = %+v, %v", updated, err) + } + if !strings.Contains(projectCodexRead(t, filepath.Join(f.a, ".agents", "skills", "docs", "SKILL.md")), "UAP-CODEX-UPDATED") || !strings.Contains(projectCodexRead(t, filepath.Join(f.a, ".codex", "config.toml")), "managed.test/v2") { + t.Fatal("native components did not receive the selected updated bytes") + } + if !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(b, projectGeminiInventory(t, f.b)) { + t.Fatal("project update changed another scope") + } +} + +func TestProjectCodexActiveProjectionChecksExactDataLocatorWithoutWrites(t *testing.T) { + f := newProjectCodexFixture(t) + r := f.stage(t, domain.CompatibilityHints{}) + stager := providers.Stager{Registry: f.registry, Paths: pathpolicy.Policy{}} + before := projectGeminiInventory(t, r.Delivery.ActivePath) + data := filepath.Join(f.a, ".uap", "data", "owned-slot") + objects, err := stager.ProjectActiveNative(t.Context(), f.load(t), r.Plan, r.Delivery.ArtifactDigest, data) + if err != nil || len(objects) != 4 { + t.Fatalf("read-only active native projection = %+v, %v", objects, err) + } + if _, err := stager.ProjectActiveNative(t.Context(), f.load(t), r.Plan, r.Delivery.ArtifactDigest, filepath.Join(f.a, "wrong-data")); err == nil { + t.Fatal("active projection accepted another persistent data locator") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, r.Delivery.ActivePath)) { + t.Fatal("read-only active projection changed digest-covered package") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_copilot_boundaries_test.go b/install/integrationctl/agentplugins/providers/project_copilot_boundaries_test.go new file mode 100644 index 000000000..011f7814b --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_copilot_boundaries_test.go @@ -0,0 +1,216 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/copilot" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +func TestProjectCopilotRefusesUnsafeConfigWithoutEffects(t *testing.T) { + for name, mutate := range map[string]func(*testing.T, *copilotProjectFixture) nativeconfig.Kernel{ + "bare native map": func(t *testing.T, f *copilotProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, f.config, `{"foreign":{"type":"http","url":"https://foreign.test/mcp"}}`) + return nativeconfig.New() + }, + "malformed JSON": func(t *testing.T, f *copilotProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, f.config, `{"mcpServers":{},}`) + return nativeconfig.New() + }, + "disabled foreign same-name": func(t *testing.T, f *copilotProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, f.config, `{"mcpServers":{"docs":{"type":"http","url":"https://foreign.test/mcp","enabled":false}}}`) + return nativeconfig.New() + }, + "foreign skill": func(t *testing.T, f *copilotProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, filepath.Join(f.project, ".github", "skills", "docs", "SKILL.md"), "foreign claimed name\n") + return nativeconfig.New() + }, + "effective config changed after stage": func(t *testing.T, f *copilotProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, filepath.Join(f.project, ".mcp.json"), copilotForeignConfig) + return nativeconfig.New() + }, + "native writer unavailable": func(*testing.T, *copilotProjectFixture) nativeconfig.Kernel { + return nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("TEST native writer unavailable") + }) + }, + } { + t.Run(name, func(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + kernel := mutate(t, f) + before := projectGeminiInventory(t, f.base) + if _, err := f.activator(kernel).Activate(context.Background(), f.request); err == nil { + t.Fatal("unsafe Copilot project activation succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("refused Copilot activation left filesystem effects") + } + }) + } +} + +func TestProjectCopilotPreparedCollisionAndExactBindingAuthority(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + adapter := copilot.New() + kernel := nativeconfig.New() + observer := providers.NativeIdentityObserver{Registry: f.registry, NativeConfig: &kernel} + plan := f.request.Plan + plan.DeclaredName = "demo" + before := projectGeminiInventory(t, f.base) + finding, err := observer.ObservePreparedIdentity(context.Background(), f.request.Client, plan, nil) + if err != nil || finding.NativeDiscoveryAttempted { + t.Fatalf("read-only prepared inspection = %+v, %v", finding, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("prepared inspection created state") + } + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + plan.PreviousNativeObjects = out.NativeObjects + binding := domain.ClientBinding{Scope: "project", PhysicalArtifact: plan.PhysicalArtifactID, TargetLocator: plan.ActivePath, + NativeProfileRoot: f.project, NativeObjects: out.NativeObjects} + if err := adapter.ValidateBindingProfile(f.project, binding); err != nil { + t.Fatal(err) + } + finding, err = observer.ObservePreparedIdentity(context.Background(), f.request.Client, plan, nil) + if err != nil || finding.State != domain.NativeIdentityUnmanaged { + t.Fatalf("unowned native components were adopted: %+v, %v", finding, err) + } + for _, change := range []func(*domain.ClientBinding){ + func(b *domain.ClientBinding) { b.Scope = "user" }, + func(b *domain.ClientBinding) { b.PhysicalArtifact = "other" }, + func(b *domain.ClientBinding) { b.TargetLocator = filepath.Join(f.other, "artifact") }, + func(b *domain.ClientBinding) { b.NativeProfileRoot = f.other }, + } { + wrong := binding + change(&wrong) + if _, err := adapter.InspectNativeRegistry(context.Background(), clientsCopilotEnv(), f.request.Client, plan, &wrong); err == nil { + t.Fatalf("mismatched binding accepted: %+v", wrong) + } + } +} + +func TestProjectCopilotScopeAndAliasCannotReachUserOrOtherProject(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + for _, change := range []func(*domain.ActivationRequest){ + func(r *domain.ActivationRequest) { r.Plan.Scope = domain.ScopeUser }, + func(r *domain.ActivationRequest) { r.Plan.NativeRegistryRoot = f.user }, + func(r *domain.ActivationRequest) { r.Plan.TargetContext.UserConfigRoot = f.other }, + } { + request := f.request + change(&request) + before := projectGeminiInventory(t, f.base) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), request); err == nil { + t.Fatal("mismatched Copilot project context succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("scope refusal changed bytes") + } + } + if err := os.Rename(filepath.Join(f.project, ".github"), filepath.Join(f.project, "github-preserved")); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(f.other, ".github"), filepath.Join(f.project, ".github")); err != nil { + t.Skipf("test symlink unavailable: %v", err) + } + other := projectGeminiInventory(t, f.other) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil { + t.Fatal("Copilot project followed an alias into B") + } + if !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("alias refusal changed B") + } +} + +func TestProjectCopilotStaleOwnedNativeMemberCannotBeRemovedOrOverwritten(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + a := f.activator(nativeconfig.New()) + out, err := a.Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + projectGeminiWrite(t, f.config, `{"mcpServers":{"docs":{"type":"http","url":"https://external.test/changed","enabled":false},"foreign":{"type":"http","url":"https://foreign.test/mcp"}}}`) + before := projectGeminiInventory(t, f.base) + f.request.PreviousNativeObjects = out.NativeObjects + if _, err := a.Activate(context.Background(), f.request); err == nil { + t.Fatal("Copilot overwrote a stale owned MCP member") + } + if _, err := a.Deactivate(context.Background(), f.remove(out.NativeObjects)); err == nil { + t.Fatal("Copilot removed a stale owned MCP member") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("stale receipt refusal changed native or package bytes") + } +} + +func TestProjectCopilotRejectsReboundPhysicalArtifactBeforeEffects(t *testing.T) { + for _, mismatch := range []string{"in-project-package", "physical-artifact"} { + t.Run(mismatch, func(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + request := f.request + if mismatch == "in-project-package" { + request.Plan.ActivePath = filepath.Join(f.project, "other-package") + request.Delivery.ActivePath = request.Plan.ActivePath + projectGeminiWrite(t, filepath.Join(request.Plan.ActivePath, "keep.txt"), "other project child\n") + } else { + request.Plan.PhysicalArtifactID = "another-artifact" + } + before := projectGeminiInventory(t, f.base) + adapter := copilot.New() + operations := map[string]func() error{ + "composed activation": func() error { + _, err := f.activator(nativeconfig.New()).Activate(context.Background(), request) + return err + }, + "direct activation": func() error { + _, err := adapter.Activate(context.Background(), clientsCopilotEnv(), request) + return err + }, + "direct projection": func() error { + _, err := adapter.Project(context.Background(), clients.ProjectionInput{StagingPath: request.Delivery.ActivePath, Plan: request.Plan, Envelope: f.envelope}) + return err + }, + "direct active reuse": func() error { + _, err := adapter.ProjectActiveNative(context.Background(), request.Plan.ActivePath, f.envelope, request.Plan, "") + return err + }, + } + for name, operation := range operations { + if err := operation(); err == nil || !strings.Contains(err.Error(), "managed package differs") { + t.Fatalf("%s accepted rebound %s: %v", name, mismatch, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatalf("%s changed native/package/user/B bytes", name) + } + } + }) + } +} + +func TestProjectCopilotDirectActivationRequiresExactDeliveredArtifact(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + other := filepath.Join(f.project, "other-delivered-package") + projectGeminiWrite(t, filepath.Join(other, "keep.txt"), "foreign in-project package\n") + before := projectGeminiInventory(t, f.base) + adapter := copilot.New() + for _, path := range []string{other, ""} { + request := f.request + request.Delivery.ActivePath = path + if err := adapter.PreflightActivation(clientsCopilotEnv(), request); (path == "") != (err == nil) { + t.Fatalf("pre-staging empty delivery/project mismatch admission %q = %v", path, err) + } + if _, err := adapter.Activate(context.Background(), clientsCopilotEnv(), request); err == nil || !strings.Contains(err.Error(), "delivered package differs") { + t.Fatalf("direct activation accepted delivery-only mismatch %q: %v", path, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("delivery-only mismatch changed project/native/user/B bytes") + } + } +} diff --git a/install/integrationctl/agentplugins/providers/project_copilot_fixture_test.go b/install/integrationctl/agentplugins/providers/project_copilot_fixture_test.go new file mode 100644 index 000000000..d2aa4242a --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_copilot_fixture_test.go @@ -0,0 +1,122 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/copilot" + "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/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +const copilotForeignConfig = `{"mcpServers":{"foreign":{"type":"http","url":"https://foreign.test/mcp","enabled":false}},"preference":"keep"}` +const copilotSkill = "---\nname: docs\ndescription: Inert Copilot project fixture\n---\nCOPILOT-PROJECT-ONE\n" + +type copilotProjectFixture struct { + base, project, user, other, source, data string + config string + request domain.ActivationRequest + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager +} + +func newCopilotProjectFixture(t *testing.T, stdio, rootConfig bool) *copilotProjectFixture { + t.Helper() + f := &copilotProjectFixture{base: projectGeminiTemp(t)} + f.project, f.user, f.other, f.source = filepath.Join(f.base, "A"), filepath.Join(f.base, "user"), filepath.Join(f.base, "B"), filepath.Join(f.base, "source") + for _, root := range []string{f.project, f.user, f.other} { + projectGeminiWrite(t, filepath.Join(root, ".github", "mcp.json"), copilotForeignConfig) + projectGeminiWrite(t, filepath.Join(root, ".github", "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + f.config = filepath.Join(f.project, ".github", "mcp.json") + if rootConfig { + f.config = filepath.Join(f.project, ".mcp.json") + projectGeminiWrite(t, f.config, copilotForeignConfig) + } + var err error + f.registry, err = clients.NewRegistry(copilot.New()) + if err != nil { + t.Fatal(err) + } + f.request.Client = domain.DetectedClient{ClientID: domain.ClientCopilot, ConfigRoot: f.user, ExecutablePath: "TEST-must-never-run-copilot", Status: domain.DetectionDetected} + f.stager = providers.Stager{Registry: f.registry, Paths: pathpolicy.Policy{}} + f.data = filepath.Join(f.project, ".uap", "data", "copilot-exact-receipt") + projectGeminiWrite(t, filepath.Join(f.data, "keep.txt"), "persistent data\n") + projectGeminiWrite(t, filepath.Join(f.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), copilotSkill) + mcp := `{"type":"streamable-http","url":"https://managed.test/one"}` + if stdio { + mcp = `{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_ROOT}","env":{"MARKER":"${PLUGIN_DATA}/marker"}}` + projectGeminiWrite(t, filepath.Join(f.source, "server.js"), "// Inert; never executed.\n") + launcher := filepath.Join(f.base, "TEST-inert-launcher") + if err := os.WriteFile(launcher, []byte("TEST trusted launcher bytes; never executed"), 0700); err != nil { + t.Fatal(err) + } + f.stager.LauncherSource, err = managedstdio.NewSource(launcher, "fixture") + if err != nil { + t.Fatal(err) + } + } + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":`+mcp+`}}`) + f.stage(t, "initial") + return f +} + +func (f *copilotProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + f.envelope, err = (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: f.source}) + if err != nil { + t.Fatal(err) + } + planning := planner.Planner{ProjectRoot: f.project, ManagedRoot: filepath.Join(f.project, ".uap", "managed"), Registry: f.registry, Paths: pathpolicy.Policy{}} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: f.envelope, Client: f.request.Client, Scope: domain.ScopeProject, ProjectRoot: f.project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.NativeRegistryRoot != f.project || plan.Activation != domain.ActivationPrepared { + t.Fatalf("Copilot repository plan = %+v, %v", plan, err) + } + delivery, err := f.stager.StageWithPluginData(context.Background(), f.envelope, plan, operation, domain.CompatibilityHints{}, f.data) + if err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(plan.ActivePath); err == nil { + if err := os.Rename(plan.ActivePath, plan.ActivePath+"-"+operation+"-old"); err != nil { + t.Fatal(err) + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + f.request.Plan, f.request.Delivery, f.request.DeclaredName = plan, delivery, "demo" +} + +func (f *copilotProjectFixture) activator(kernel nativeconfig.Kernel) providers.Activator { + return providers.Activator{Registry: f.registry, NativeConfig: &kernel} +} + +func (f *copilotProjectFixture) remove(objects []domain.NativeObjectOwnership) domain.DeactivationRequest { + return domain.DeactivationRequest{Client: f.request.Client, TargetContext: f.request.Plan.TargetContext, + NativeRegistryRoot: f.project, Confirmed: true, NativeObjects: objects} +} + +func copilotPrepared(t *testing.T, outcome domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("Copilot configured state = %+v, %v", outcome, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_copilot_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_copilot_lifecycle_test.go new file mode 100644 index 000000000..18acbfa46 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_copilot_lifecycle_test.go @@ -0,0 +1,102 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Real loader/planner/stager/native kernel; no Copilot or server process runs. +func TestProjectCopilotComposedLifecycleSelectsEffectiveConfig(t *testing.T) { + for _, rootConfig := range []bool{false, true} { + t.Run(map[bool]string{false: "github-fallback", true: "root-precedence"}[rootConfig], func(t *testing.T) { + f := newCopilotProjectFixture(t, false, rootConfig) + user, other := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + activator := f.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), f.request) + copilotPrepared(t, outcome, err) + projectGeminiAssert(t, filepath.Join(f.project, ".github", "skills", "docs", "SKILL.md"), copilotSkill) + config, err := os.ReadFile(f.config) + if err != nil || !strings.Contains(string(config), "managed.test/one") || !strings.Contains(string(config), `"enabled":false`) || !strings.Contains(string(config), `"type":"http"`) { + t.Fatalf("Copilot config lost transport or foreign settings: %s, %v", config, err) + } + if rootConfig { + projectGeminiAssert(t, filepath.Join(f.project, ".github", "mcp.json"), copilotForeignConfig) + } + f.request.PreviousNativeObjects = outcome.NativeObjects + for _, verify := range []bool{false, true} { + f.request.VerifyOnly = verify + outcome, err = activator.Activate(context.Background(), f.request) + copilotPrepared(t, outcome, err) + } + for range 2 { + removed, err := activator.Deactivate(context.Background(), f.remove(outcome.NativeObjects)) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("exact repeated Copilot remove = %+v, %v", removed, err) + } + } + if _, err := os.Stat(filepath.Join(f.project, ".github", "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("Copilot managed skill survived: %v", err) + } + remaining, err := os.ReadFile(f.config) + if err != nil || strings.Contains(string(remaining), "managed.test") || !strings.Contains(string(remaining), "foreign.test") { + t.Fatalf("Copilot removal changed foreign MCP data: %s, %v", remaining, err) + } + projectGeminiAssert(t, filepath.Join(f.data, "keep.txt"), "persistent data\n") + if !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("Copilot project lifecycle mutated user or B") + } + }) + } +} + +func TestProjectCopilotUpdatesRepairsAndDropsMCP(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + a := f.activator(nativeconfig.New()) + out, err := a.Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + f.request.PreviousNativeObjects = out.NativeObjects + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), strings.Replace(copilotSkill, "ONE", "TWO", 1)) + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"sse","url":"https://managed.test/two"}}}`) + f.stage(t, "update") + out, err = a.Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + skill := filepath.Join(f.project, ".github", "skills", "docs") + if err := os.RemoveAll(skill); err != nil { + t.Fatal(err) + } + f.request.PreviousNativeObjects = out.NativeObjects + out, err = a.Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + projectGeminiAssert(t, filepath.Join(skill, "SKILL.md"), strings.Replace(copilotSkill, "ONE", "TWO", 1)) + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{}}`) + f.request.PreviousNativeObjects = out.NativeObjects + f.stage(t, "drop-mcp") + out, err = a.Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + body, err := os.ReadFile(f.config) + if err != nil || strings.Contains(string(body), "managed.test") || !strings.Contains(string(body), "foreign.test") { + t.Fatalf("dropped MCP/foreign config = %s, %v", body, err) + } +} + +func TestProjectCopilotRejectsAttestationBeforeAutomaticFallback(t *testing.T) { + f := newCopilotProjectFixture(t, false, false) + before := projectGeminiInventory(t, f.base) + f.request.ActivationComplete = true + for _, support := range []domain.SupportLevel{domain.SupportNative, domain.SupportPrepared} { + f.request.Plan.Components[0].Support = support + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("Copilot project attestation bypass: %v", err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("Copilot attestation created effects") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_copilot_stdio_test.go b/install/integrationctl/agentplugins/providers/project_copilot_stdio_test.go new file mode 100644 index 000000000..20b06ddec --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_copilot_stdio_test.go @@ -0,0 +1,54 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio" +) + +func clientsCopilotEnv() clients.Env { return clients.Env{NativeConfig: nativeconfig.New()} } + +func TestProjectCopilotStdioBindsImmutablePackageAndSelectedData(t *testing.T) { + if !managedstdio.Supported() { + t.Skip("managed stdio platform unsupported") + } + f := newCopilotProjectFixture(t, true, true) + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + copilotPrepared(t, out, err) + body, err := os.ReadFile(f.config) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{`"type":"stdio"`, `"tools":["*"]`, `"PLUGIN_DATA"`, "stdio-v1"} { + if !strings.Contains(string(body), want) { + t.Fatalf("Copilot launcher contract omitted %s: %s", want, body) + } + } + active := f.request.Plan.ActivePath + projectGeminiWrite(t, filepath.Join(f.other, "data", "marker"), "other data\n") + before := projectGeminiInventory(t, f.base) + objects, err := f.stager.ProjectActiveNative(context.Background(), f.envelope, f.request.Plan, f.request.Delivery.ArtifactDigest, f.data) + if err != nil || len(objects) != 2 { + t.Fatalf("correct receipt projection = %+v, %v", objects, err) + } + for _, selected := range []string{"", "relative", filepath.Join(f.other, "data")} { + if _, err := f.stager.ProjectActiveNative(context.Background(), f.envelope, f.request.Plan, f.request.Delivery.ArtifactDigest, selected); !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("wrong selected stdio data %q = %v", selected, err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("active receipt inspection rewrote immutable package, native config, user or B") + } + if _, err := os.Stat(filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName)); err != nil { + t.Fatal(err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_cursor_boundaries_test.go b/install/integrationctl/agentplugins/providers/project_cursor_boundaries_test.go new file mode 100644 index 000000000..9452a1945 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_cursor_boundaries_test.go @@ -0,0 +1,108 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cursor" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectCursorAttestationCannotUpgradeConfiguration(t *testing.T) { + fixture := newCursorProjectFixture(t, false) + before := projectGeminiInventory(t, fixture.request.Plan.NativeRegistryRoot) + fixture.request.ActivationComplete = true + for _, prepared := range []bool{false, true} { + if prepared { + fixture.request.Plan.Components[0].Support = domain.SupportPrepared + } + _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request) + if err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("attestation bypassed preflight: %v", err) + } + } + if _, err := cursor.New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, fixture.request); err == nil { + t.Fatal("direct adapter bypassed preflight") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, fixture.request.Plan.NativeRegistryRoot)) { + t.Fatal("attestation changed project files") + } +} + +func TestProjectCursorForeignDisabledMalformedAndWriterFailurePreserveNativeFiles(t *testing.T) { + for _, failure := range []string{"disabled-collision", "malformed", "writer-lock", "skill-collision"} { + t.Run(failure, func(t *testing.T) { + fixture := newCursorProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + kernel := nativeconfig.New() + switch failure { + case "disabled-collision": + projectGeminiWrite(t, filepath.Join(root, "mcp.json"), `{"mcpServers":{"docs":{"url":"https://foreign.test/keep","disabled":true}}}`) + case "malformed": + projectGeminiWrite(t, filepath.Join(root, "mcp.json"), `{"mcpServers":{},}`) + case "skill-collision": + projectGeminiWrite(t, filepath.Join(root, "skills", "docs", "SKILL.md"), "foreign same-name skill\n") + case "writer-lock": + kernel = nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("inert writer refusal") + }) + } + before, user, other := projectGeminiInventory(t, root), projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + if _, err := fixture.activator(kernel).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("unsafe/failed native mutation succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) || !reflect.DeepEqual(user, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, fixture.other)) { + t.Fatal("failed install changed native files") + } + }) + } +} + +func TestProjectCursorScopeRootAndBindingAuthorityRefuse(t *testing.T) { + for _, mismatch := range []string{"user-registry", "user-context", "other-user", "copied-project"} { + t.Run(mismatch, func(t *testing.T) { + fixture := newCursorProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + before := projectGeminiInventory(t, root) + switch mismatch { + case "user-registry": + fixture.request.Plan.NativeRegistryRoot = fixture.user + case "user-context": + fixture.request.Plan.TargetContext.Scope = domain.ScopeUser + case "other-user": + fixture.request.Plan.TargetContext.UserConfigRoot = fixture.other + case "copied-project": + fixture.request.Plan.TargetContext.ProjectRoot = fixture.other + fixture.request.Plan.NativeRegistryRoot = filepath.Join(fixture.other, ".cursor") + } + if _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("mismatched project authority admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("authority refusal changed original project") + } + }) + } +} + +func TestProjectCursorContainedNativeRootAliasRefuses(t *testing.T) { + fixture := newCursorProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + original := root + "-physical" + if err := os.Rename(root, original); err != nil { + t.Fatal(err) + } + if err := os.Symlink(original, root); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("contained native root alias admitted") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_cursor_fixture_test.go b/install/integrationctl/agentplugins/providers/project_cursor_fixture_test.go new file mode 100644 index 000000000..c8183e5f9 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_cursor_fixture_test.go @@ -0,0 +1,121 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cursor" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/commandext" + "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/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +type cursorProjectFixture struct { + request domain.ActivationRequest + user, other, source string + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager +} + +func newCursorProjectFixture(t *testing.T, stdio bool) *cursorProjectFixture { + t.Helper() + base := projectGeminiTemp(t) + project, user, other := filepath.Join(base, "A"), filepath.Join(base, "user", ".cursor"), filepath.Join(base, "B") + for _, root := range []string{user, filepath.Join(project, ".cursor"), filepath.Join(other, ".cursor")} { + projectGeminiWrite(t, filepath.Join(root, "mcp.json"), `{"mcpServers":{"foreign":{"url":"https://foreign.test/mcp","disabled":true}},"policy":"preserve"}`) + projectGeminiWrite(t, filepath.Join(root, "hooks.json"), "foreign hooks\n") + projectGeminiWrite(t, filepath.Join(root, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + registry, err := clients.NewRegistry(cursor.New()) + if err != nil { + t.Fatal(err) + } + fixture := &cursorProjectFixture{source: filepath.Join(base, "source"), user: user, other: other, registry: registry} + fixture.stager = providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + fixture.request.Client = domain.DetectedClient{ClientID: domain.ClientCursor, ConfigRoot: user, ExecutablePath: "fixture-editor-never-executed", Status: domain.DetectionDetected} + fixture.request.Plan.TargetContext, err = domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + projectGeminiWrite(t, filepath.Join(fixture.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(fixture.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert project fixture\n---\nVersion one.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, filepath.FromSlash(commandext.RelativeDirectory), "echo.md"), "---\ndescription: Echo inert marker\ndisable-model-invocation: false\n---\nUAP-CURSOR-PROJECT-MARKER\n") + mcp := `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v1"}}}` + if stdio { + mcp = `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_ROOT}"}}}` + projectGeminiWrite(t, filepath.Join(fixture.source, "server.js"), "// Inert fixture; never executed.\n") + fixture.stager.LauncherSource = cursorFixtureLauncher(t, base) + } + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), mcp) + fixture.stage(t, "initial") + return fixture +} + +func cursorFixtureLauncher(t *testing.T, base string) *managedstdio.Source { + t.Helper() + if !managedstdio.Supported() { + t.Skip("managed stdio platform unsupported") + } + path := filepath.Join(base, "inert-launcher") + if err := os.WriteFile(path, []byte("trusted fixture bytes; never executed"), 0700); err != nil { + t.Fatal(err) + } + source, err := managedstdio.NewSource(path, "fixture") + if err != nil { + t.Fatal(err) + } + return source +} + +func (fixture *cursorProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + fixture.envelope, err = (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: fixture.source}) + if err != nil { + t.Fatal(err) + } + project := fixture.request.Plan.TargetContext.ProjectRoot + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: fixture.registry} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: fixture.envelope, Client: fixture.request.Client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared { + t.Fatalf("Cursor project plan = %+v, %v", plan, err) + } + delivery, err := fixture.stager.Stage(context.Background(), fixture.envelope, plan, operation, domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(plan.ActivePath); err == nil { + if err := os.Rename(plan.ActivePath, plan.ActivePath+"-"+operation+"-backup"); err != nil { + t.Fatal(err) + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + fixture.request.Plan, fixture.request.Delivery, fixture.request.DeclaredName = plan, delivery, "demo" +} + +func (fixture *cursorProjectFixture) activator(kernel nativeconfig.Kernel) providers.Activator { + return providers.Activator{Registry: fixture.registry, NativeConfig: &kernel} +} + +func (fixture *cursorProjectFixture) removal(objects []domain.NativeObjectOwnership) domain.DeactivationRequest { + return domain.DeactivationRequest{Client: fixture.request.Client, TargetContext: fixture.request.Plan.TargetContext, + NativeRegistryRoot: fixture.request.Plan.NativeRegistryRoot, Confirmed: true, NativeObjects: objects} +} diff --git a/install/integrationctl/agentplugins/providers/project_cursor_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_cursor_lifecycle_test.go new file mode 100644 index 000000000..002f41919 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_cursor_lifecycle_test.go @@ -0,0 +1,98 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/cursor" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func cursorAssertPrepared(t *testing.T, outcome domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("Cursor configured state = %+v, %v", outcome, err) + } +} + +// Inert production loader/planner/renderer/stager/provider evidence; this does +// not execute Cursor, the injected launcher, a model or an MCP server. +func TestProjectCursorComposedLifecycleAndScopeIsolation(t *testing.T) { + fixture := newCursorProjectFixture(t, false) + userBefore, bBefore := projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + activator := fixture.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + root := fixture.request.Plan.NativeRegistryRoot + body, err := os.ReadFile(filepath.Join(root, "skills", "demo-echo", "SKILL.md")) + if err != nil || !strings.Contains(string(body), "UAP-CURSOR-PROJECT-MARKER") { + t.Fatalf("rendered command skill = %q, %v", body, err) + } + activeBefore := projectGeminiInventory(t, fixture.request.Plan.ActivePath) + objects, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, "") + if err != nil || len(objects) != 3 || !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, fixture.request.Plan.ActivePath)) { + t.Fatalf("read-only projection = %+v, %v", objects, err) + } + binding := domain.ClientBinding{Scope: "project", PhysicalArtifact: fixture.request.Plan.PhysicalArtifactID, + TargetLocator: fixture.request.Plan.ActivePath, NativeProfileRoot: root, NativeObjects: outcome.NativeObjects} + if err := cursor.New().ValidateBindingProfile(root, binding); err != nil { + t.Fatal(err) + } + fixture.request.PreviousNativeObjects = outcome.NativeObjects + for _, verify := range []bool{false, true} { + fixture.request.VerifyOnly = verify + outcome, err = activator.Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + } + data := filepath.Join(fixture.request.Plan.TargetContext.ProjectRoot, ".uap", "data", "keep.txt") + projectGeminiWrite(t, data, "persistent data\n") + for range 2 { + removed, err := activator.Deactivate(context.Background(), fixture.removal(outcome.NativeObjects)) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("repeat exact remove = %+v, %v", removed, err) + } + } + projectGeminiAssert(t, data, "persistent data\n") + if _, err := os.Stat(filepath.Join(root, "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("managed skill survived remove: %v", err) + } + if !reflect.DeepEqual(userBefore, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(bBefore, projectGeminiInventory(t, fixture.other)) { + t.Fatal("Cursor project lifecycle changed user or project B") + } + projectGeminiAssert(t, filepath.Join(root, "hooks.json"), "foreign hooks\n") +} + +func TestProjectCursorUpdateRepairAndRemovedComponent(t *testing.T) { + fixture := newCursorProjectFixture(t, false) + activator := fixture.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + fixture.request.PreviousNativeObjects = outcome.NativeObjects + projectGeminiWrite(t, filepath.Join(fixture.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Updated inert fixture\n---\nVersion two.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v2"}}}`) + fixture.stage(t, "update") + outcome, err = activator.Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + skill := filepath.Join(fixture.request.Plan.NativeRegistryRoot, "skills", "docs") + if err := os.RemoveAll(skill); err != nil { + t.Fatal(err) + } + fixture.request.PreviousNativeObjects = outcome.NativeObjects + outcome, err = activator.Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + projectGeminiAssert(t, filepath.Join(skill, "SKILL.md"), "---\nname: docs\ndescription: Updated inert fixture\n---\nVersion two.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{}}`) + fixture.request.PreviousNativeObjects = outcome.NativeObjects + fixture.stage(t, "drop-mcp") + outcome, err = activator.Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + config, err := os.ReadFile(filepath.Join(fixture.request.Plan.NativeRegistryRoot, "mcp.json")) + if err != nil || strings.Contains(string(config), "managed.test") || !strings.Contains(string(config), `"disabled":true`) { + t.Fatalf("removed component/foreign data = %s, %v", config, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_cursor_stdio_test.go b/install/integrationctl/agentplugins/providers/project_cursor_stdio_test.go new file mode 100644 index 000000000..e6580616d --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_cursor_stdio_test.go @@ -0,0 +1,65 @@ +package providers_test + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio" +) + +func TestProjectCursorStdioUsesBoundManagedLauncherWithoutExecution(t *testing.T) { + fixture := newCursorProjectFixture(t, true) + beforeUser, beforeB := projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + outcome, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request) + cursorAssertPrepared(t, outcome, err) + body, err := os.ReadFile(filepath.Join(fixture.request.Plan.NativeRegistryRoot, "mcp.json")) + if err != nil { + t.Fatal(err) + } + var document struct { + Servers map[string]struct { + Command string `json:"command"` + Args []string `json:"args"` + Env map[string]string `json:"env"` + CWD string `json:"cwd"` + } `json:"mcpServers"` + } + if err := json.Unmarshal(body, &document); err != nil { + t.Fatal(err) + } + server := document.Servers["docs"] + active := fixture.request.Plan.ActivePath + data := filepath.Join(fixture.request.Plan.TargetContext.ProjectRoot, ".uap", "data", fixture.request.Plan.PhysicalArtifactID) + wantArgs := []string{managedstdio.Mode, active, data, active, "plugin", "--", "node", active + "/server.js", data} + if server.Command != filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) || !reflect.DeepEqual(server.Args, wantArgs) || server.CWD != "" || server.Env["PLUGIN_ROOT"] != active || server.Env["PLUGIN_DATA"] != data { + t.Fatalf("native launcher binding = %+v; want args %q", server, wantArgs) + } + projectGeminiAssert(t, server.Command, "trusted fixture bytes; never executed") + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, fixture.other)) { + t.Fatal("stdio configuration changed other scopes") + } + activeBefore := projectGeminiInventory(t, active) + objects, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, data) + if err != nil || len(objects) != 3 || !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, active)) { + t.Fatalf("stdio read-only projection = %+v, %v", objects, err) + } + for _, selectedData := range []string{"", filepath.Join(filepath.Dir(data), "other-receipt")} { + if _, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, selectedData); !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("different selected data locator was accepted: %v", err) + } + if !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, active)) { + t.Fatal("refused data locator changed immutable package bytes") + } + } + fixture.stager.LauncherSource = nil + if _, err := fixture.stager.Stage(context.Background(), fixture.envelope, fixture.request.Plan, "missing-launcher", domain.CompatibilityHints{}); err == nil { + t.Fatal("project stdio staged without trusted launcher") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_boundaries_test.go b/install/integrationctl/agentplugins/providers/project_devin_boundaries_test.go new file mode 100644 index 000000000..2d441e25b --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_boundaries_test.go @@ -0,0 +1,110 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectDevinAttestationCannotUpgradeConfiguration(t *testing.T) { + fixture := newDevinProjectFixture(t, false) + before := projectGeminiInventory(t, fixture.request.Plan.NativeRegistryRoot) + fixture.request.ActivationComplete = true + for _, prepared := range []bool{false, true} { + if prepared { + fixture.request.Plan.Components[0].Support = domain.SupportPrepared + } + _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request) + if err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("attestation bypassed preflight: %v", err) + } + } + if _, err := windsurf.New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, fixture.request); err == nil { + t.Fatal("direct adapter bypassed preflight") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, fixture.request.Plan.NativeRegistryRoot)) { + t.Fatal("attestation changed project files") + } +} + +func TestProjectDevinForeignDisabledMalformedAndWriterFailurePreserveNativeFiles(t *testing.T) { + for _, failure := range []string{"disabled-collision", "malformed", "skill-collision", "writer-lock"} { + t.Run(failure, func(t *testing.T) { + fixture := newDevinProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + kernel := nativeconfig.New() + switch failure { + case "disabled-collision": + projectGeminiWrite(t, filepath.Join(root, "mcp_config.json"), `{"mcpServers":{"docs":{"url":"https://foreign.test/keep","disabled":true}}}`) + case "malformed": + projectGeminiWrite(t, filepath.Join(root, "mcp_config.json"), `{"mcpServers":[]}`) + case "writer-lock": + kernel = nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("inert Devin writer refusal") + }) + case "skill-collision": + projectGeminiWrite(t, filepath.Join(root, "skills", "docs", "SKILL.md"), "foreign same-name skill\n") + } + before, user, other := projectGeminiInventory(t, root), projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + if _, err := fixture.activator(kernel).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("unsafe/failed native mutation succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) || !reflect.DeepEqual(user, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, fixture.other)) { + t.Fatal("failed install changed native files") + } + }) + } +} + +func TestProjectDevinScopeRootAndBindingAuthorityRefuse(t *testing.T) { + for _, mismatch := range []string{"user-registry", "user-context", "other-user", "omitted-user", "copied-project"} { + t.Run(mismatch, func(t *testing.T) { + fixture := newDevinProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + before := projectGeminiInventory(t, root) + switch mismatch { + case "user-registry": + fixture.request.Plan.NativeRegistryRoot = fixture.user + case "user-context": + fixture.request.Plan.TargetContext.Scope = domain.ScopeUser + case "omitted-user": + fixture.request.Plan.TargetContext.UserConfigRoot = "" + case "other-user": + fixture.request.Plan.TargetContext.UserConfigRoot = fixture.other + case "copied-project": + fixture.request.Plan.TargetContext.ProjectRoot = fixture.other + fixture.request.Plan.NativeRegistryRoot = filepath.Join(fixture.other, ".devin") + } + if _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("mismatched project authority admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("authority refusal changed original project") + } + }) + } +} + +func TestProjectDevinContainedNativeRootAliasRefuses(t *testing.T) { + fixture := newDevinProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + original := root + "-physical" + if err := os.Rename(root, original); err != nil { + t.Fatal(err) + } + if err := os.Symlink(original, root); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("contained native root alias admitted") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_failure_test.go b/install/integrationctl/agentplugins/providers/project_devin_failure_test.go new file mode 100644 index 000000000..b373b450d --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_failure_test.go @@ -0,0 +1,72 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/atomicfile" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type devinFailAfterVisibleWrite struct{ fail bool } + +func (*devinFailAfterVisibleWrite) ReadNoFollow(path string) ([]byte, os.FileMode, bool, error) { + snapshot, err := nativeconfig.New().ReadExactFile(path) + return snapshot.Body, snapshot.Mode, snapshot.Exists, err +} +func (files *devinFailAfterVisibleWrite) WriteAtomic(path string, body []byte, mode os.FileMode) error { + if err := atomicfile.Write(path, body, mode); err != nil { + return err + } + if files.fail { + files.fail = false + return errors.New("inert failure after visible Devin MCP replacement") + } + return nil +} +func (*devinFailAfterVisibleWrite) RemoveNoFollow(path string) error { return os.Remove(path) } + +func TestProjectDevinVisibleMCPWriteFailureRestoresNativeSkillAndBytes(t *testing.T) { + f := newDevinProjectFixture(t, false) + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + devinAssertPrepared(t, out, err) + root := f.request.Plan.NativeRegistryRoot + before, user, other := projectGeminiInventory(t, root), projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + f.request.PreviousNativeObjects = out.NativeObjects + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert new skill\n---\nNew bytes.\n") + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/new"}}}`) + f.stage(t, "fault-update") + kernel := nativeconfig.NewWithFileIO(&devinFailAfterVisibleWrite{fail: true}) + failed, err := f.activator(kernel).Activate(context.Background(), f.request) + if err == nil || failed.NativeEffect == domain.NativeEffectCommitted { + t.Fatalf("visible failure misreported: %+v,%v", failed, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) || !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("visible write failure failed exact rollback or affected another scope") + } +} + +func TestProjectDevinCommittedCleanupKeepsNewNativeReceipts(t *testing.T) { + f := newDevinProjectFixture(t, false) + kernel := nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return func() error { return errors.New("inert committed unlock failure") }, nil + }) + out, err := f.activator(kernel).Activate(context.Background(), f.request) + devinAssertPrepared(t, out, err) + if out.NativeEffect != domain.NativeEffectCommitted || !reflect.DeepEqual(out.NativeObjects, f.request.Delivery.NativeObjects) || !strings.Contains(strings.Join(out.UserActions, " "), "lock cleanup degraded") { + t.Fatalf("committed cleanup lost authority: %+v", out) + } + f.request.PreviousNativeObjects = out.NativeObjects + f.request.VerifyOnly = true + verified, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + devinAssertPrepared(t, verified, err) + if !reflect.DeepEqual(out.NativeObjects, verified.NativeObjects) { + t.Fatal("committed cleanup receipts were not reusable") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_fixture_test.go b/install/integrationctl/agentplugins/providers/project_devin_fixture_test.go new file mode 100644 index 000000000..560e2ce6e --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_fixture_test.go @@ -0,0 +1,119 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" + "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/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +type devinProjectFixture struct { + request domain.ActivationRequest + user, other, source string + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager +} + +func newDevinProjectFixture(t *testing.T, stdio bool) *devinProjectFixture { + t.Helper() + base := projectGeminiTemp(t) + project, user, other := filepath.Join(base, "A"), filepath.Join(base, "user", ".devin"), filepath.Join(base, "B") + for _, root := range []string{user, filepath.Join(project, ".devin"), filepath.Join(other, ".devin")} { + projectGeminiWrite(t, filepath.Join(root, "mcp_config.json"), `{"mcpServers":{"foreign":{"url":"https://foreign.test/mcp","disabled":true}},"policy":"preserve"}`) + projectGeminiWrite(t, filepath.Join(root, "hooks.json"), "foreign hooks\n") + projectGeminiWrite(t, filepath.Join(root, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + registry, err := clients.NewRegistry(windsurf.New()) + if err != nil { + t.Fatal(err) + } + fixture := &devinProjectFixture{source: filepath.Join(base, "source"), user: user, other: other, registry: registry} + fixture.stager = providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + fixture.request.Client = domain.DetectedClient{ClientID: domain.ClientWindsurf, ConfigRoot: user, ExecutablePath: filepath.Join(base, "devin"), Version: "3000.11.205", Status: domain.DetectionDetected} + fixture.request.Plan.TargetContext, err = domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + projectGeminiWrite(t, filepath.Join(fixture.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(fixture.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert project fixture\n---\nVersion one.\n") + mcp := `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v1"}}}` + if stdio { + mcp = `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_ROOT}"}}}` + projectGeminiWrite(t, filepath.Join(fixture.source, "server.js"), "// Inert fixture; never executed.\n") + fixture.stager.LauncherSource = devinFixtureLauncher(t, base) + } + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), mcp) + fixture.stage(t, "initial") + return fixture +} + +func devinFixtureLauncher(t *testing.T, base string) *managedstdio.Source { + t.Helper() + if !managedstdio.Supported() { + t.Skip("managed stdio platform unsupported") + } + path := filepath.Join(base, "inert-launcher") + if err := os.WriteFile(path, []byte("trusted fixture bytes; never executed"), 0700); err != nil { + t.Fatal(err) + } + source, err := managedstdio.NewSource(path, "fixture") + if err != nil { + t.Fatal(err) + } + return source +} + +func (fixture *devinProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + fixture.envelope, err = (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: fixture.source}) + if err != nil { + t.Fatal(err) + } + project := fixture.request.Plan.TargetContext.ProjectRoot + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: fixture.registry} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: fixture.envelope, Client: fixture.request.Client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared { + t.Fatalf("Devin project plan = %+v, %v", plan, err) + } + delivery, err := fixture.stager.Stage(context.Background(), fixture.envelope, plan, operation, domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(plan.ActivePath); err == nil { + if err := os.Rename(plan.ActivePath, plan.ActivePath+"-"+operation+"-backup"); err != nil { + t.Fatal(err) + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + fixture.request.Plan, fixture.request.Delivery, fixture.request.DeclaredName = plan, delivery, "demo" +} + +func (fixture *devinProjectFixture) activator(kernel nativeconfig.Kernel) providers.Activator { + return providers.Activator{Registry: fixture.registry, NativeConfig: &kernel} +} + +func (fixture *devinProjectFixture) removal(objects []domain.NativeObjectOwnership) domain.DeactivationRequest { + return domain.DeactivationRequest{Client: fixture.request.Client, TargetContext: fixture.request.Plan.TargetContext, + NativeRegistryRoot: fixture.request.Plan.NativeRegistryRoot, Confirmed: true, NativeObjects: objects} +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_devin_lifecycle_test.go new file mode 100644 index 000000000..3df734062 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_lifecycle_test.go @@ -0,0 +1,94 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func devinAssertPrepared(t *testing.T, outcome domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("Devin configured state = %+v, %v", outcome, err) + } +} + +// Inert production loader/planner/renderer/stager/provider evidence; this does +// not execute Devin, the injected launcher, a model or an MCP server. +func TestProjectDevinComposedLifecycleAndScopeIsolation(t *testing.T) { + fixture := newDevinProjectFixture(t, false) + userBefore, bBefore := projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + activator := fixture.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + root := fixture.request.Plan.NativeRegistryRoot + activeBefore := projectGeminiInventory(t, fixture.request.Plan.ActivePath) + objects, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, "") + if err != nil || len(objects) != 2 || !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, fixture.request.Plan.ActivePath)) { + t.Fatalf("read-only projection = %+v, %v", objects, err) + } + binding := domain.ClientBinding{ClientID: "windsurf", Scope: "project", PhysicalArtifact: fixture.request.Plan.PhysicalArtifactID, + TargetLocator: fixture.request.Plan.ActivePath, NativeProfileRoot: root, NativeObjects: outcome.NativeObjects} + if err := windsurf.New().ValidateBindingProfile(root, binding); err != nil { + t.Fatal(err) + } + fixture.request.PreviousNativeObjects = outcome.NativeObjects + for _, verify := range []bool{false, true} { + fixture.request.VerifyOnly = verify + outcome, err = activator.Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + } + data := filepath.Join(fixture.request.Plan.TargetContext.ProjectRoot, ".uap", "data", "keep.txt") + projectGeminiWrite(t, data, "persistent data\n") + for range 2 { + removed, err := activator.Deactivate(context.Background(), fixture.removal(outcome.NativeObjects)) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("repeat exact remove = %+v, %v", removed, err) + } + } + projectGeminiAssert(t, data, "persistent data\n") + if _, err := os.Stat(filepath.Join(root, "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("managed skill survived remove: %v", err) + } + if !reflect.DeepEqual(userBefore, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(bBefore, projectGeminiInventory(t, fixture.other)) { + t.Fatal("Devin project lifecycle changed user or project B") + } + projectGeminiAssert(t, filepath.Join(root, "hooks.json"), "foreign hooks\n") +} + +func TestProjectDevinUpdateRepairAndRemovedComponent(t *testing.T) { + fixture := newDevinProjectFixture(t, false) + activator := fixture.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + fixture.request.PreviousNativeObjects = outcome.NativeObjects + projectGeminiWrite(t, filepath.Join(fixture.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Updated inert fixture\n---\nVersion two.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v2"}}}`) + fixture.stage(t, "update") + outcome, err = activator.Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + skill := filepath.Join(fixture.request.Plan.NativeRegistryRoot, "skills", "docs") + if err := os.RemoveAll(skill); err != nil { + t.Fatal(err) + } + fixture.request.PreviousNativeObjects = outcome.NativeObjects + outcome, err = activator.Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + projectGeminiAssert(t, filepath.Join(skill, "SKILL.md"), "---\nname: docs\ndescription: Updated inert fixture\n---\nVersion two.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{}}`) + fixture.request.PreviousNativeObjects = outcome.NativeObjects + fixture.stage(t, "drop-mcp") + outcome, err = activator.Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + config, err := os.ReadFile(filepath.Join(fixture.request.Plan.NativeRegistryRoot, "mcp_config.json")) + if err != nil || strings.Contains(string(config), "managed.test") || !strings.Contains(string(config), `"disabled":true`) { + t.Fatalf("removed component/foreign data = %s, %v", config, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_policy_test.go b/install/integrationctl/agentplugins/providers/project_devin_policy_test.go new file mode 100644 index 000000000..2ba4d5c3a --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_policy_test.go @@ -0,0 +1,119 @@ +package providers_test + +import ( + "context" + "fmt" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" +) + +func TestProjectDevinMigrationAndLocalShadowingRefuseWithoutEffects(t *testing.T) { + for _, name := range []string{"config.json", "config.local.json", "mcp_config.local.json"} { + t.Run(name, func(t *testing.T) { + f := newDevinProjectFixture(t, false) + root := f.request.Plan.NativeRegistryRoot + projectGeminiWrite(t, filepath.Join(root, name), `{"mcpServers":{"docs":{"url":"https://foreign.test","disabled":true}},"policy":"keep"}`) + before, user, other := projectGeminiInventory(t, root), projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + input := clients.PlanInput{Client: f.request.Client, TargetContext: f.request.Plan.TargetContext, Envelope: f.envelope} + plan := f.request.Plan + if err := windsurf.New().CheckPlanPrecondition(input, &plan); err == nil { + t.Fatal("migration/shadowing passed planning") + } + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil { + t.Fatal("changed local policy passed activation") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) || !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("policy refusal changed scope bytes") + } + }) + } +} + +func TestProjectDevinExactArtifactAndDeliveryCannotBeRebound(t *testing.T) { + for _, change := range []string{"existing-package", "artifact", "empty-delivery", "other-delivery"} { + t.Run(change, func(t *testing.T) { + f := newDevinProjectFixture(t, false) + root := f.request.Plan.NativeRegistryRoot + switch change { + case "existing-package": + f.request.Plan.ActivePath = f.source + f.request.Delivery.ActivePath = f.source + case "artifact": + f.request.Plan.PhysicalArtifactID = "other-artifact" + case "empty-delivery": + f.request.Delivery.ActivePath = "" + case "other-delivery": + f.request.Delivery.ActivePath = f.source + } + before := projectGeminiInventory(t, root) + if _, err := windsurf.New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, f.request); err == nil { + t.Fatal("rebound delivery admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("artifact refusal changed native files") + } + }) + } +} + +func TestProjectDevinLocalOverrideForeignEntriesArePreserved(t *testing.T) { + f := newDevinProjectFixture(t, false) + root := f.request.Plan.NativeRegistryRoot + override := filepath.Join(root, "mcp_config.local.json") + const foreign = `{"mcpServers":{"other":{"url":"https://foreign.test","transport":"http","disabled":true}},"policy":"keep"}` + projectGeminiWrite(t, override, foreign) + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + devinAssertPrepared(t, out, err) + projectGeminiAssert(t, override, foreign) + // Removal relies on recorded ownership even if later local policy shadows it. + projectGeminiWrite(t, override, `{"mcpServers":{"docs":{"disabled":true}}}`) + removal := f.removal(out.NativeObjects) + removal.Client.Version = "" + removal.Client.ExecutablePath = "" + outRemove, err := f.activator(nativeconfig.New()).Deactivate(context.Background(), removal) + if err != nil || !outRemove.ExternalRemovalComplete { + t.Fatalf("recorded removal=%+v,%v", outRemove, err) + } + projectGeminiAssert(t, override, `{"mcpServers":{"docs":{"disabled":true}}}`) +} + +func TestProjectDevinCommentedNativePolicyPreservesBytesAndRefusesUnsafeEntries(t *testing.T) { + for _, name := range []string{"config.json", "config.local.json", "mcp_config.local.json"} { + for _, unsafe := range []bool{false, true} { + t.Run(name+fmt.Sprint(unsafe), func(t *testing.T) { + f := newDevinProjectFixture(t, false) + root := f.request.Plan.NativeRegistryRoot + body := `{ // foreign native policy +"policy":"keep" /* foreign comment */}` + if name == "mcp_config.local.json" { + body = `{ // foreign local override +"mcpServers":{"other":{"url":"https://foreign.test","disabled":true}},"policy":"keep"}` + } + if unsafe { + body = `{ // native legacy or local-shadow refusal +"mcpServers":{"docs":{"url":"https://foreign.test","disabled":true}}}` + } + path := filepath.Join(root, name) + projectGeminiWrite(t, path, body) + before := projectGeminiInventory(t, root) + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + if unsafe { + if err == nil { + t.Fatal("commented unsafe policy admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("commented refusal changed bytes") + } + } else { + devinAssertPrepared(t, out, err) + } + projectGeminiAssert(t, path, body) + }) + } + } +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_receipt_test.go b/install/integrationctl/agentplugins/providers/project_devin_receipt_test.go new file mode 100644 index 000000000..627af46d3 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_receipt_test.go @@ -0,0 +1,85 @@ +package providers_test + +import ( + "context" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/windsurf" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectDevinExactReceiptAuthorityRefusesBeforeEffects(t *testing.T) { + for _, mismatch := range []string{"unknown-kind", "id", "duplicate", "other-project", "source-traversal", "nonclean-path"} { + t.Run(mismatch, func(t *testing.T) { + f := newDevinProjectFixture(t, false) + root := f.request.Plan.NativeRegistryRoot + objects := append([]domain.NativeObjectOwnership(nil), f.request.Delivery.NativeObjects...) + skill := 0 + for i, object := range objects { + if object.Kind == windsurf.DevinProjectSkillKind { + skill = i + } + } + switch mismatch { + case "unknown-kind": + objects[skill].Kind = "foreign_kind" + case "id": + objects[skill].ObjectID = "devin-skill:foreign" + case "duplicate": + objects = append(objects, objects[skill]) + case "other-project": + objects[skill].Path = filepath.Join(f.other, ".devin", "skills", "docs") + case "source-traversal": + objects[skill].SourceRelative = "skills/../docs" + case "nonclean-path": + objects[skill].Path = filepath.Dir(objects[skill].Path) + string(filepath.Separator) + ".." + string(filepath.Separator) + "skills" + string(filepath.Separator) + "docs" + } + f.request.Delivery.NativeObjects = objects + before := projectGeminiInventory(t, root) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil { + t.Fatal("forged project native ownership was admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("receipt refusal changed native files") + } + }) + } +} + +func TestProjectDevinRegistryRejectsCopiedBinding(t *testing.T) { + f := newDevinProjectFixture(t, false) + // Only the existing automatic intent is qualified for this project lane. + outcome, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + devinAssertPrepared(t, outcome, err) + binding := domain.ClientBinding{ClientID: "windsurf", Scope: "project", PhysicalArtifact: f.request.Plan.PhysicalArtifactID, TargetLocator: f.request.Plan.ActivePath, NativeProfileRoot: f.request.Plan.NativeRegistryRoot, NativeObjects: outcome.NativeObjects} + adapter := windsurf.New() + finding, err := adapter.InspectNativeRegistry(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, f.request.Client, f.request.Plan, &binding) + if err != nil || finding != clients.RegistryExpected { + t.Fatalf("exact project registry = %v,%v", finding, err) + } + for _, change := range []string{"client", "scope", "artifact", "package", "root"} { + copied := binding + switch change { + case "client": + copied.ClientID = "codex" + case "scope": + copied.Scope = "user" + case "artifact": + copied.PhysicalArtifact = "different" + case "package": + copied.TargetLocator = filepath.Join(f.other, ".uap", "managed", "clients", "windsurf", binding.PhysicalArtifact) + case "root": + copied.NativeProfileRoot = filepath.Join(f.other, ".devin") + } + if err := adapter.ValidateBindingProfile(binding.NativeProfileRoot, copied); change != "scope" && err == nil { + t.Fatalf("direct copied %s binding admitted", change) + } + if _, err := adapter.InspectNativeRegistry(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, f.request.Client, f.request.Plan, &copied); err == nil { + t.Fatalf("copied %s binding admitted", change) + } + } +} diff --git a/install/integrationctl/agentplugins/providers/project_devin_stdio_test.go b/install/integrationctl/agentplugins/providers/project_devin_stdio_test.go new file mode 100644 index 000000000..1a4fa5f47 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_devin_stdio_test.go @@ -0,0 +1,72 @@ +package providers_test + +import ( + "context" + "encoding/json" + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio" +) + +func TestProjectDevinStdioUsesBoundManagedLauncherWithoutExecution(t *testing.T) { + fixture := newDevinProjectFixture(t, true) + beforeUser, beforeB := projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + outcome, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request) + devinAssertPrepared(t, outcome, err) + servers, err := readDevinFixtureConfig(filepath.Join(fixture.request.Plan.NativeRegistryRoot, "mcp_config.json")) + if err != nil { + t.Fatal(err) + } + server := servers["docs"].(map[string]any) + args := make([]string, 0, len(server["args"].([]any))) + for _, arg := range server["args"].([]any) { + args = append(args, arg.(string)) + } + env := server["env"].(map[string]any) + active := fixture.request.Plan.ActivePath + data := filepath.Join(fixture.request.Plan.TargetContext.ProjectRoot, ".uap", "data", fixture.request.Plan.PhysicalArtifactID) + wantArgs := []string{managedstdio.Mode, active, data, active, "plugin", "--", "node", active + "/server.js", data} + if server["command"] != filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) || !reflect.DeepEqual(args, wantArgs) || server["cwd"] != nil || env["PLUGIN_ROOT"] != active || env["PLUGIN_DATA"] != data { + t.Fatalf("native launcher binding = %+v; want args %q", server, wantArgs) + } + projectGeminiAssert(t, server["command"].(string), "trusted fixture bytes; never executed") + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, fixture.other)) { + t.Fatal("stdio configuration changed other scopes") + } + activeBefore := projectGeminiInventory(t, active) + objects, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, data) + if err != nil || len(objects) != 2 || !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, active)) { + t.Fatalf("stdio read-only projection = %+v, %v", objects, err) + } + for _, selectedData := range []string{"", filepath.Join(filepath.Dir(data), "other-receipt")} { + if _, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, selectedData); !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("different selected data locator was accepted: %v", err) + } + if !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, active)) { + t.Fatal("refused data locator changed immutable package bytes") + } + } + fixture.stager.LauncherSource = nil + if _, err := fixture.stager.Stage(context.Background(), fixture.envelope, fixture.request.Plan, "missing-launcher", domain.CompatibilityHints{}); err == nil { + t.Fatal("project stdio staged without trusted launcher") + } +} + +func readDevinFixtureConfig(path string) (map[string]any, error) { + file, err := nativeconfig.New().ReadExactFile(path) + if err != nil { + return nil, err + } + var document struct { + Servers map[string]any `json:"mcpServers"` + } + if err := json.Unmarshal(file.Body, &document); err != nil { + return nil, err + } + return document.Servers, nil +} diff --git a/install/integrationctl/agentplugins/providers/project_gemini_helpers_test.go b/install/integrationctl/agentplugins/providers/project_gemini_helpers_test.go new file mode 100644 index 000000000..e22ded930 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_gemini_helpers_test.go @@ -0,0 +1,65 @@ +package providers_test + +import ( + "crypto/sha256" + "fmt" + "io/fs" + "os" + "path/filepath" + "testing" +) + +func projectGeminiTemp(t *testing.T) string { + t.Helper() + root, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + return root +} + +func projectGeminiWrite(t *testing.T, path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } +} + +func projectGeminiAssert(t *testing.T, path, want string) { + t.Helper() + body, err := os.ReadFile(path) + if err != nil || string(body) != want { + t.Fatalf("fixture bytes at %s = %q, %v", path, body, err) + } +} + +func projectGeminiInventory(t *testing.T, root string) map[string]string { + t.Helper() + files := map[string]string{} + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + if entry.IsDir() { + files[path] = fmt.Sprintf("dir:%s", info.Mode()) + return nil + } + body, err := os.ReadFile(path) + if err != nil { + return err + } + files[path] = fmt.Sprintf("%s:%x", info.Mode(), sha256.Sum256(body)) + return nil + }) + if err != nil { + t.Fatal(err) + } + return files +} diff --git a/install/integrationctl/agentplugins/providers/project_gemini_scope_test.go b/install/integrationctl/agentplugins/providers/project_gemini_scope_test.go new file mode 100644 index 000000000..5804799d9 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_gemini_scope_test.go @@ -0,0 +1,105 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/gemini" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +func projectGeminiRequest(t *testing.T) (domain.ActivationRequest, string, string) { + t.Helper() + base := projectGeminiTemp(t) + user, projectA, projectB := filepath.Join(base, "user", ".gemini"), filepath.Join(base, "A"), filepath.Join(base, "B") + for _, root := range []string{user, filepath.Join(projectA, ".gemini"), filepath.Join(projectB, ".gemini")} { + projectGeminiWrite(t, filepath.Join(root, "settings.json"), "{\n // preserve foreign settings\n \"mcpServers\": {\"foreign\": {\"url\":\"https://foreign.test/mcp\"}}, \"theme\":\"foreign\"\n}\n") + projectGeminiWrite(t, filepath.Join(root, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + client := domain.DetectedClient{ClientID: domain.ClientGemini, ConfigRoot: user, Status: domain.DetectionDetected} + target, err := domain.NewTargetContext(domain.ScopeProject, projectA, user) + if err != nil { + t.Fatal(err) + } + active := filepath.Join(projectA, ".uap", "managed", "clients", "gemini", "archive-demo") + projectGeminiWrite(t, filepath.Join(active, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Project fixture\n---\nInert fixture.\n") + projectGeminiWrite(t, filepath.Join(active, "mcp.json"), `{"mcpServers":{"docs":{"type":"sse","url":"https://managed.test/mcp"}}}`) + plan := domain.DeliveryPlan{ClientID: domain.ClientGemini, Scope: domain.ScopeProject, TargetContext: target, InstallIntent: domain.InstallIntentAutomatic, + NativeRegistryRoot: filepath.Join(projectA, ".gemini"), ActivePath: active, Components: []domain.ComponentDecision{ + {Kind: domain.ComponentSkill, Name: "docs", Support: domain.SupportNative}, {Kind: domain.ComponentMCPServer, Name: "docs", Support: domain.SupportNative}, + }} + envelope := domain.PackageEnvelope{Skills: map[string]domain.Skill{"docs": {Name: "docs", RelativePath: "skills/docs/SKILL.md"}}, + MCP: domain.MCPComponent{Servers: map[string]domain.MCPServer{"docs": {Name: "docs", Type: "sse", Decoded: map[string]any{"url": "https://managed.test/mcp"}}}}} + objects, err := gemini.BuildGeminiNativeObjects(active, envelope, plan, filepath.Join(projectA, ".uap", "data", "demo")) + if err != nil { + t.Fatal(err) + } + return domain.ActivationRequest{Client: client, Plan: plan, DeclaredName: "archive-demo", Delivery: domain.StagedDelivery{ + ClientID: domain.ClientGemini, ActivePath: active, OwnedBase: filepath.Dir(active), NativeObjects: objects}}, projectB, user +} + +func TestProjectGeminiNativeLifecycleKeepsUserAndOtherProject(t *testing.T) { + request, projectB, user := projectGeminiRequest(t) + userBefore, bBefore := projectGeminiInventory(t, user), projectGeminiInventory(t, projectB) + registry, err := clients.NewRegistry(gemini.New()) + if err != nil { + t.Fatal(err) + } + kernel := nativeconfig.New() + activator := providers.Activator{Registry: registry, NativeConfig: &kernel} + outcome, err := activator.Activate(context.Background(), request) + if err != nil || outcome.Verification != domain.VerificationInstalled || len(outcome.NativeObjects) != 2 { + t.Fatalf("project config activation = %+v, %v", outcome, err) + } + root := request.Plan.NativeRegistryRoot + projectGeminiAssert(t, filepath.Join(root, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Project fixture\n---\nInert fixture.\n") + settings, err := os.ReadFile(filepath.Join(root, "settings.json")) + if err != nil || !strings.Contains(string(settings), "preserve foreign settings") || !strings.Contains(string(settings), "managed.test") || !strings.Contains(string(settings), "foreign.test") { + t.Fatalf("project MCP merge lost entries/comments: %s, %v", settings, err) + } + request.VerifyOnly, request.PreviousNativeObjects = true, outcome.NativeObjects + if _, err := activator.Activate(context.Background(), request); err != nil { + t.Fatal(err) + } + remove := domain.DeactivationRequest{Client: request.Client, TargetContext: request.Plan.TargetContext, NativeRegistryRoot: root, Confirmed: true, NativeObjects: outcome.NativeObjects} + for range 2 { + removed, err := activator.Deactivate(context.Background(), remove) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("exact project removal = %+v, %v", removed, err) + } + } + if _, err := os.Stat(filepath.Join(root, "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("managed project skill survived removal: %v", err) + } + if !reflect.DeepEqual(userBefore, projectGeminiInventory(t, user)) || !reflect.DeepEqual(bBefore, projectGeminiInventory(t, projectB)) { + t.Fatal("project lifecycle mutated user profile or project B") + } +} + +func TestProjectGeminiTargetContextRejectsUserRegistryAndAliases(t *testing.T) { + request, _, user := projectGeminiRequest(t) + adapter := gemini.New() + managed := filepath.Join(request.Plan.TargetContext.ProjectRoot, ".uap", "managed") + if _, root, err := adapter.ProjectTargetRoot(request.Client, domain.PackageNative, managed, request.Plan.TargetContext); err != nil || root != filepath.Join(managed, "clients", "gemini") { + t.Fatalf("project artifact layout = %q, %v", root, err) + } + if _, _, err := adapter.ProjectTargetRoot(request.Client, domain.PackageNative, filepath.Dir(user), request.Plan.TargetContext); err == nil { + t.Fatal("project artifacts escaped to user profile") + } + request.Plan.NativeRegistryRoot = user + if _, err := adapter.Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, request); err == nil { + t.Fatal("project plan accepted user registry") + } + request.Plan.NativeRegistryRoot = filepath.Join(request.Plan.TargetContext.ProjectRoot, ".gemini") + request.Plan.TargetContext.UserConfigRoot = filepath.Join(user, "other") + if _, err := adapter.Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, request); err == nil { + t.Fatal("project plan replaced original user profile authority") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_gemini_stager_test.go b/install/integrationctl/agentplugins/providers/project_gemini_stager_test.go new file mode 100644 index 000000000..6287608fd --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_gemini_stager_test.go @@ -0,0 +1,87 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/gemini" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/commandext" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +// This uses the production loader, planner, renderer, stager and activator. +// It proves an inert contract journey, not Gemini session/model discovery. +func TestProjectGeminiPlannerStagerRendersCommandsAndOwnsExactNativeComponents(t *testing.T) { + request, projectB, user := projectGeminiRequest(t) + beforeUser, beforeB := projectGeminiInventory(t, user), projectGeminiInventory(t, projectB) + source := filepath.Join(projectGeminiTemp(t), "source") + projectGeminiWrite(t, filepath.Join(source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Project docs\n---\nInert docs.\n") + projectGeminiWrite(t, filepath.Join(source, filepath.FromSlash(commandext.RelativeDirectory), "echo.md"), "---\ndescription: Echo the marker\ndisable-model-invocation: false\n---\nUAP-PROJECT-INERT\n") + projectGeminiWrite(t, filepath.Join(source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"sse","url":"https://managed.test/mcp"}}}`) + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + envelope, err := (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: source}) + if err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(gemini.New()) + if err != nil { + t.Fatal(err) + } + project := request.Plan.TargetContext.ProjectRoot + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: registry} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: envelope, Client: request.Client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared || plan.NativeRegistryRoot != filepath.Join(project, ".gemini") { + t.Fatalf("scoped Gemini plan = %+v, %v", plan, err) + } + stager := providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + delivery, err := stager.Stage(context.Background(), envelope, plan, "project-gemini-command", domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if len(gemini.GeminiObjects(delivery.NativeObjects)) != 3 { + t.Fatalf("staged skill/command/MCP objects = %+v", delivery.NativeObjects) + } + if err := os.MkdirAll(filepath.Dir(plan.ActivePath), 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + request.Plan, request.Delivery, request.DeclaredName = plan, delivery, "demo" + activator := projectGeminiActivator(t, nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled { + t.Fatalf("staged project activation = %+v, %v", outcome, err) + } + body, err := os.ReadFile(filepath.Join(project, ".gemini", "skills", "demo-echo", "SKILL.md")) + if err != nil || !strings.Contains(string(body), "UAP-PROJECT-INERT") { + t.Fatalf("rendered command skill = %q, %v", body, err) + } + activeObjects, err := stager.ProjectActiveNative(context.Background(), envelope, plan, delivery.ArtifactDigest, "") + if err != nil || !reflect.DeepEqual(activeObjects, gemini.GeminiObjects(delivery.NativeObjects)) { + t.Fatalf("read-only native objects changed = %+v, %v", activeObjects, err) + } + remove := domain.DeactivationRequest{Client: request.Client, TargetContext: plan.TargetContext, NativeRegistryRoot: plan.NativeRegistryRoot, Confirmed: true, NativeObjects: outcome.NativeObjects} + if out, err := activator.Deactivate(context.Background(), remove); err != nil || !out.ExternalRemovalComplete { + t.Fatalf("project cleanup = %+v, %v", out, err) + } + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, projectB)) { + t.Fatal("stager journey changed another scope") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_gemini_status_test.go b/install/integrationctl/agentplugins/providers/project_gemini_status_test.go new file mode 100644 index 000000000..dfd299b55 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_gemini_status_test.go @@ -0,0 +1,119 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/gemini" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +func projectGeminiActivator(t *testing.T, kernel nativeconfig.Kernel) providers.Activator { + t.Helper() + registry, err := clients.NewRegistry(gemini.New()) + if err != nil { + t.Fatal(err) + } + return providers.Activator{Registry: registry, NativeConfig: &kernel} +} + +func TestProjectGeminiConfiguredStateDoesNotClaimNativeDiscovery(t *testing.T) { + request, _, _ := projectGeminiRequest(t) + // An ambient bypass flag is never proof of durable workspace trust. + t.Setenv("GEMINI_CLI_TRUST_WORKSPACE", "true") + activator := projectGeminiActivator(t, nativeconfig.New()) + for pass := range 3 { + outcome, err := activator.Activate(context.Background(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("pass %d configured status = %+v, %v", pass, outcome, err) + } + if !strings.Contains(strings.Join(outcome.UserActions, "\n"), "workspace trust, system policy and MCP initialization remain unobserved") { + t.Fatalf("native proof boundary missing: %+v", outcome) + } + request.PreviousNativeObjects = outcome.NativeObjects + // A second automatic reconciliation and a fresh verifier both stay + // exact and prepared; neither config presence nor a restart promotes it. + request.VerifyOnly = pass > 0 + } + before := projectGeminiInventory(t, request.Plan.NativeRegistryRoot) + for _, automatic := range []bool{true, false} { + request.ActivationComplete = true + if !automatic { + request.Plan.Components[0].Support = domain.SupportPrepared + } + if _, err := activator.Activate(context.Background(), request); err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("generic attestation bypassed project preflight: %v", err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, request.Plan.NativeRegistryRoot)) { + t.Fatal("attestation mutated project configuration") + } +} + +func TestProjectGeminiPreservesRestrictionsAndSkillAlias(t *testing.T) { + request, projectB, user := projectGeminiRequest(t) + userSettings := `{"skills":{"disabled":["docs"]},"mcp":{"allowed":[]},"mcpServers":{"docs":{"url":"https://user.test/mcp"}}}` + workspaceSettings := "{ // preserve workspace restriction\n\"skills\":{\"enabled\":false},\"mcp\":{\"excluded\":[\"docs\"]},\"mcpServers\":{}}\n" + projectGeminiWrite(t, filepath.Join(user, "settings.json"), userSettings) + projectGeminiWrite(t, filepath.Join(request.Plan.NativeRegistryRoot, "settings.json"), workspaceSettings) + alias := filepath.Join(request.Plan.TargetContext.ProjectRoot, ".agents", "skills", "docs", "SKILL.md") + const aliasBody = "---\nname: docs\ndescription: Foreign alias\n---\nKeep alias bytes.\n" + projectGeminiWrite(t, alias, aliasBody) + beforeUser, beforeB := projectGeminiInventory(t, user), projectGeminiInventory(t, projectB) + outcome, err := projectGeminiActivator(t, nativeconfig.New()).Activate(context.Background(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared { + t.Fatalf("restricted configuration = %+v, %v", outcome, err) + } + actions := strings.Join(outcome.UserActions, "\n") + for _, reason := range []string{`disable skill "docs"`, `restrict MCP server "docs"`, `.agents/skills/docs/SKILL.md may shadow`} { + if !strings.Contains(actions, reason) { + t.Fatalf("missing %q in %s", reason, actions) + } + } + projectGeminiAssert(t, alias, aliasBody) + settings, err := os.ReadFile(filepath.Join(request.Plan.NativeRegistryRoot, "settings.json")) + if err != nil || !strings.Contains(string(settings), `"enabled":false`) || !strings.Contains(string(settings), `"excluded":["docs"]`) || !strings.Contains(string(settings), "// preserve workspace restriction") { + t.Fatalf("workspace policy changed: %s, %v", settings, err) + } + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, projectB)) { + t.Fatal("restriction inspection mutated another scope") + } +} + +func TestProjectGeminiMalformedSettingsAndWriterFailureLeaveNativeStateUnchanged(t *testing.T) { + for name, prepare := range map[string]func(*testing.T, domain.ActivationRequest) nativeconfig.Kernel{ + "malformed project": func(t *testing.T, r domain.ActivationRequest) nativeconfig.Kernel { + projectGeminiWrite(t, filepath.Join(r.Plan.NativeRegistryRoot, "settings.json"), `{"skills":{"enabled":false},}`) + return nativeconfig.New() + }, + "foreign MCP collision": func(t *testing.T, r domain.ActivationRequest) nativeconfig.Kernel { + projectGeminiWrite(t, filepath.Join(r.Plan.NativeRegistryRoot, "settings.json"), `{"mcpServers":{"docs":{"url":"https://foreign.test/keep"}}}`) + return nativeconfig.New() + }, + "MCP writer lock failure": func(*testing.T, domain.ActivationRequest) nativeconfig.Kernel { + return nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("TEST writer lock unavailable") + }) + }, + } { + t.Run(name, func(t *testing.T) { + request, b, user := projectGeminiRequest(t) + kernel := prepare(t, request) + beforeProject, beforeUser, beforeB := projectGeminiInventory(t, request.Plan.NativeRegistryRoot), projectGeminiInventory(t, user), projectGeminiInventory(t, b) + if _, err := projectGeminiActivator(t, kernel).Activate(context.Background(), request); err == nil { + t.Fatal("unsafe/failed project mutation succeeded") + } + if !reflect.DeepEqual(beforeProject, projectGeminiInventory(t, request.Plan.NativeRegistryRoot)) || !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, b)) { + t.Fatal("failed project install left native mutations") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_grok_test.go b/install/integrationctl/agentplugins/providers/project_grok_test.go new file mode 100644 index 000000000..e9f281667 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_grok_test.go @@ -0,0 +1,107 @@ +package providers_test + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/grok" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type grokProjectProviderRunner struct { + t *testing.T + request domain.ActivationRequest + calls int +} + +func (r *grokProjectProviderRunner) Run(_ context.Context, command legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + plan := r.request.Plan + if !reflect.DeepEqual(command.Argv, []string{r.request.Client.ExecutablePath, "inspect", "--json"}) || command.Dir != plan.TargetContext.ProjectRoot { + r.t.Fatal("non-readonly or wrong project command", command) + } + body, _ := json.Marshal(map[string]any{"grokVersion": "1.0.67", "cwd": command.Dir, "projectRoot": command.Dir + string(filepath.Separator), "projectTrusted": false, "skills": []any{}, "plugins": []any{map[string]any{"name": plan.DeclaredName, "scope": "project", "path": plan.ActivePath, "enabled": true}}}) + return legacyports.CommandResult{Stdout: body}, nil +} +func TestProjectGrokComposedPlannerStagerActivationAttestation(t *testing.T) { + base := projectGeminiTemp(t) + project, user, source := filepath.Join(base, "A"), filepath.Join(base, "home", ".grok"), filepath.Join(base, "source") + if err := os.MkdirAll(project, 0700); err != nil { + t.Fatal(err) + } + projectGeminiWrite(t, filepath.Join(user, "keep"), "original") + projectGeminiWrite(t, filepath.Join(source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert Grok project fixture\n---\nText only.\n") + projectGeminiWrite(t, filepath.Join(source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://inert.test/mcp"}}}`) + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + envelope, err := (loader.Loader{Registry: schemas}).Load(t.Context(), domain.LoadInput{SnapshotRoot: source}) + if err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(grok.New()) + if err != nil { + t.Fatal(err) + } + client := domain.DetectedClient{ClientID: domain.ClientGrok, ConfigRoot: user, ExecutablePath: "/inert/grok-not-executed", Status: domain.DetectionDetected} + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: registry} + plan, err := planning.Plan(t.Context(), domain.PlanRequest{Envelope: envelope, Client: client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.TargetRoot != filepath.Join(project, ".grok", "plugins") { + t.Fatalf("Grok plan %+v %v", plan, err) + } + for _, component := range plan.Components { + if component.Kind == domain.ComponentMCPServer && component.Support != domain.SupportUnsupported { + t.Fatal("MCP project qualification invented") + } + } + stager := providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + delivery, err := stager.Stage(t.Context(), envelope, plan, "inert", domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if filepath.Dir(delivery.StagingPath) != plan.TargetAnchor { + t.Fatal("staging entered watched plugin root") + } + if _, err := os.Stat(filepath.Join(delivery.StagingPath, "mcp.json")); !os.IsNotExist(err) { + t.Fatal("unsupported bundled MCP projected", err) + } + if err := os.MkdirAll(plan.TargetRoot, 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + request := domain.ActivationRequest{Client: client, Plan: plan, Delivery: delivery, DeclaredName: "demo", BackendExecutable: client.ExecutablePath} + runner := &grokProjectProviderRunner{t: t, request: request} + actor := providers.Activator{Registry: registry, Runner: runner} + outcome, err := actor.Activate(t.Context(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationPackageValid || outcome.AuthoritativeObservation { + t.Fatalf("composed source promoted: %+v %v", outcome, err) + } + request.ActivationComplete = true + before := runner.calls + projectBefore, userBefore := projectGeminiInventory(t, project), projectGeminiInventory(t, user) + if err := actor.PreflightActivation(request); err == nil { + t.Fatal("provider preflight accepted attestation") + } + if _, err := actor.Activate(t.Context(), request); err == nil { + t.Fatal("provider accepted attestation") + } + if before != runner.calls || !reflect.DeepEqual(projectBefore, projectGeminiInventory(t, project)) || !reflect.DeepEqual(userBefore, projectGeminiInventory(t, user)) { + t.Fatal("refused attestation made effects") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kimi_admission_test.go b/install/integrationctl/agentplugins/providers/project_kimi_admission_test.go new file mode 100644 index 000000000..e96fd1ed4 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kimi_admission_test.go @@ -0,0 +1,95 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" +) + +func TestProjectKimiRequiresCurrentObservedHarnessAndNearestGit(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + p := planner.Planner{Registry: f.registry, Paths: pathpolicy.Policy{}, ProjectRoot: f.project, ManagedRoot: filepath.Join(f.project, ".uap", "managed")} + qualified, err := p.Plan(context.Background(), domain.PlanRequest{Envelope: f.envelope, Client: f.request.Client, Scope: domain.ScopeProject, ProjectRoot: f.project, PhysicalArtifactID: f.request.Plan.PhysicalArtifactID}) + if err != nil || qualified.Status != domain.PlanReady { + t.Fatalf("current observed Kimi harness did not qualify: %+v, %v", qualified, err) + } + for _, change := range []func(*domain.DetectedClient){ + func(c *domain.DetectedClient) { c.Version = "" }, func(c *domain.DetectedClient) { c.Version = "2.3.0" }, func(c *domain.DetectedClient) { c.Version = "2.3.1-alpha" }, func(c *domain.DetectedClient) { c.ExecutablePath = filepath.Join(f.base, "kimi-cli") }, + } { + client := f.request.Client + change(&client) + before := projectGeminiInventory(t, f.base) + if _, err := p.Plan(context.Background(), domain.PlanRequest{Envelope: f.envelope, Client: client, Scope: domain.ScopeProject, ProjectRoot: f.project, PhysicalArtifactID: f.request.Plan.PhysicalArtifactID}); err == nil || !strings.Contains(err.Error(), "require the explicitly selected current Kimi Code 2.3.1 executable/version") { + t.Fatalf("unobserved current Kimi harness refusal = %v", err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("admission refusal created effects") + } + } + if err := os.Remove(filepath.Join(f.project, ".git")); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Join(f.base, ".git"), 0700); err != nil { + t.Fatal(err) + } + before := projectGeminiInventory(t, f.base) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil || !strings.Contains(err.Error(), "differs from selected project") { + t.Fatalf("nearest Git ancestor mismatch = %v", err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("nearest Git refusal changed files") + } +} + +func TestProjectKimiExactRemovalDoesNotNeedCurrentVersionOrGit(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + a := f.activator(nativeconfig.New()) + out, err := a.Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + r := f.remove(out.NativeObjects) + r.Client.Version, r.Client.ExecutablePath = "", "" + if err := os.Remove(filepath.Join(f.project, ".git")); err != nil { + t.Fatal(err) + } + removed, err := a.Deactivate(context.Background(), r) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("exact receipt removal depended on native harness/discovery: %+v %v", removed, err) + } +} + +func TestProjectKimiSSEIsScopedAndLowerPrioritySourcesStayForeign(t *testing.T) { + f := newKimiProjectFixture(t, false, true) + userConfig := filepath.Join(f.user, "mcp.json") + const lower = `{"mcpServers":{"docs":{"transport":"http","url":"https://lower.test/mcp","enabled":false}}}` + projectGeminiWrite(t, userConfig, lower) + projectGeminiWrite(t, filepath.Join(f.project, ".mcp.json"), lower) + if err := os.RemoveAll(filepath.Join(f.source, "skills")); err != nil { + t.Fatal(err) + } + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"sse","url":"https://managed.test/events"}}}`) + f.stage(t, "sse-only") + if !strings.Contains(strings.Join(f.request.Plan.Warnings, " "), "overrides a same-name lower-priority") { + t.Fatal("project-over-user/compatibility policy was not exposed") + } + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + body, err := os.ReadFile(f.config) + if err != nil || !strings.Contains(string(body), `"transport":"sse"`) { + t.Fatalf("project-only SSE transport = %s,%v", body, err) + } + projectGeminiAssert(t, userConfig, lower) + projectGeminiAssert(t, filepath.Join(f.project, ".mcp.json"), lower) + userPlanner := planner.Planner{Registry: f.registry, Paths: pathpolicy.Policy{}, ManagedRoot: filepath.Join(f.base, "user-managed")} + plan, err := userPlanner.Plan(context.Background(), domain.PlanRequest{Envelope: f.envelope, Client: f.request.Client, Scope: domain.ScopeUser, PhysicalArtifactID: "sse-user-0123456789ab"}) + if err != nil || len(plan.Components) != 1 || plan.Components[0].Support != domain.SupportUnsupported { + t.Fatalf("project SSE widened preserved user policy: %+v,%v", plan, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kimi_boundaries_test.go b/install/integrationctl/agentplugins/providers/project_kimi_boundaries_test.go new file mode 100644 index 000000000..7b5b96267 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kimi_boundaries_test.go @@ -0,0 +1,218 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kimi" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +func TestProjectKimiRefusesUnsafeConfigWithoutEffects(t *testing.T) { + for name, mutate := range map[string]func(*testing.T, *kimiProjectFixture) nativeconfig.Kernel{ + "malformed JSON": func(t *testing.T, f *kimiProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, f.config, `{"mcpServers":{},}`) + return nativeconfig.New() + }, + "disabled foreign same-name": func(t *testing.T, f *kimiProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, f.config, `{"mcpServers":{"docs":{"transport":"http","url":"https://foreign.test/mcp","enabled":false}}}`) + return nativeconfig.New() + }, + "foreign skill": func(t *testing.T, f *kimiProjectFixture) nativeconfig.Kernel { + projectGeminiWrite(t, filepath.Join(f.project, ".kimi-code", "skills", "docs", "SKILL.md"), "foreign claimed name\n") + return nativeconfig.New() + }, + "native writer unavailable": func(*testing.T, *kimiProjectFixture) nativeconfig.Kernel { + return nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("TEST native writer unavailable") + }) + }, + } { + t.Run(name, func(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + kernel := mutate(t, f) + before := projectGeminiInventory(t, f.base) + if _, err := f.activator(kernel).Activate(context.Background(), f.request); err == nil { + t.Fatal("unsafe Kimi project activation succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("refused Kimi activation left filesystem effects") + } + }) + } +} + +func TestProjectKimiPreparedCollisionAndExactBindingAuthority(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + adapter := kimi.New() + kernel := nativeconfig.New() + observer := providers.NativeIdentityObserver{Registry: f.registry, NativeConfig: &kernel} + plan := f.request.Plan + plan.DeclaredName = "demo" + before := projectGeminiInventory(t, f.base) + finding, err := observer.ObservePreparedIdentity(context.Background(), f.request.Client, plan, nil) + if err != nil || finding.NativeDiscoveryAttempted { + t.Fatalf("read-only prepared inspection = %+v, %v", finding, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("prepared inspection created state") + } + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + plan.PreviousNativeObjects = out.NativeObjects + binding := domain.ClientBinding{ClientID: "kimi", Scope: "project", PhysicalArtifact: plan.PhysicalArtifactID, TargetLocator: plan.ActivePath, + NativeProfileRoot: filepath.Join(f.project, ".kimi-code"), NativeObjects: out.NativeObjects} + if err := adapter.ValidateBindingProfile(filepath.Join(f.project, ".kimi-code"), binding); err != nil { + t.Fatal(err) + } + copied := binding + copied.ClientID = "codex" + before = projectGeminiInventory(t, f.base) + if err := adapter.ValidateBindingProfile(binding.NativeProfileRoot, copied); err == nil || !strings.Contains(err.Error(), "recorded project client differs") { + t.Fatalf("direct project profile validator accepted another client: %v", err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("direct copied-client refusal changed bytes") + } + finding, err = observer.ObservePreparedIdentity(context.Background(), f.request.Client, plan, nil) + if err != nil || finding.State != domain.NativeIdentityUnmanaged { + t.Fatalf("unowned native components were adopted: %+v, %v", finding, err) + } + for _, change := range []func(*domain.ClientBinding){ + func(b *domain.ClientBinding) { b.Scope = "user" }, + func(b *domain.ClientBinding) { b.ClientID = "codex" }, + func(b *domain.ClientBinding) { b.PhysicalArtifact = "other" }, + func(b *domain.ClientBinding) { b.TargetLocator = filepath.Join(f.other, "artifact") }, + func(b *domain.ClientBinding) { b.NativeProfileRoot = f.other }, + } { + wrong := binding + change(&wrong) + if _, err := adapter.InspectNativeRegistry(context.Background(), clientsKimiEnv(), f.request.Client, plan, &wrong); err == nil { + t.Fatalf("mismatched binding accepted: %+v", wrong) + } + } +} + +func TestProjectKimiScopeAndAliasCannotReachUserOrOtherProject(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + for _, change := range []func(*domain.ActivationRequest){ + func(r *domain.ActivationRequest) { r.Plan.Scope = domain.ScopeUser }, + func(r *domain.ActivationRequest) { r.Plan.NativeRegistryRoot = f.user }, + func(r *domain.ActivationRequest) { r.Plan.TargetContext.UserConfigRoot = f.other }, + } { + request := f.request + change(&request) + before := projectGeminiInventory(t, f.base) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), request); err == nil { + t.Fatal("mismatched Kimi project context succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("scope refusal changed bytes") + } + } + if err := os.Rename(filepath.Join(f.project, ".kimi-code"), filepath.Join(f.project, "github-preserved")); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(f.other, ".kimi-code"), filepath.Join(f.project, ".kimi-code")); err != nil { + t.Skipf("test symlink unavailable: %v", err) + } + other := projectGeminiInventory(t, f.other) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil { + t.Fatal("Kimi project followed an alias into B") + } + if !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("alias refusal changed B") + } +} + +func TestProjectKimiStaleOwnedNativeMemberCannotBeRemovedOrOverwritten(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + a := f.activator(nativeconfig.New()) + out, err := a.Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + projectGeminiWrite(t, f.config, `{"mcpServers":{"docs":{"transport":"http","url":"https://external.test/changed","enabled":false},"foreign":{"transport":"http","url":"https://foreign.test/mcp"}}}`) + before := projectGeminiInventory(t, f.base) + f.request.PreviousNativeObjects = out.NativeObjects + if _, err := a.Activate(context.Background(), f.request); err == nil { + t.Fatal("Kimi overwrote a stale owned MCP member") + } + if _, err := a.Deactivate(context.Background(), f.remove(out.NativeObjects)); err == nil { + t.Fatal("Kimi removed a stale owned MCP member") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("stale receipt refusal changed native or package bytes") + } +} + +func TestProjectKimiRejectsReboundPhysicalArtifactBeforeEffects(t *testing.T) { + for _, mismatch := range []string{"in-project-package", "physical-artifact"} { + t.Run(mismatch, func(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + request := f.request + if mismatch == "in-project-package" { + request.Plan.ActivePath = filepath.Join(f.project, "other-package") + request.Delivery.ActivePath = request.Plan.ActivePath + projectGeminiWrite(t, filepath.Join(request.Plan.ActivePath, "keep.txt"), "other project child\n") + } else { + request.Plan.PhysicalArtifactID = "another-artifact" + } + before := projectGeminiInventory(t, f.base) + adapter := kimi.New() + operations := map[string]func() error{ + "composed activation": func() error { + _, err := f.activator(nativeconfig.New()).Activate(context.Background(), request) + return err + }, + "direct activation": func() error { + _, err := adapter.Activate(context.Background(), clientsKimiEnv(), request) + return err + }, + "direct projection": func() error { + _, err := adapter.Project(context.Background(), clients.ProjectionInput{StagingPath: request.Delivery.ActivePath, Plan: request.Plan, Envelope: f.envelope}) + return err + }, + "direct active reuse": func() error { + _, err := adapter.ProjectActiveNative(context.Background(), request.Plan.ActivePath, f.envelope, request.Plan, "") + return err + }, + } + for name, operation := range operations { + if err := operation(); err == nil || !strings.Contains(err.Error(), "managed package differs") { + t.Fatalf("%s accepted rebound %s: %v", name, mismatch, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatalf("%s changed native/package/user/B bytes", name) + } + } + }) + } +} + +func TestProjectKimiDirectActivationRequiresExactDeliveredArtifact(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + other := filepath.Join(f.project, "other-delivered-package") + projectGeminiWrite(t, filepath.Join(other, "keep.txt"), "foreign in-project package\n") + before := projectGeminiInventory(t, f.base) + adapter := kimi.New() + for _, path := range []string{other, ""} { + request := f.request + request.Delivery.ActivePath = path + if err := adapter.PreflightActivation(clientsKimiEnv(), request); (path == "") != (err == nil) { + t.Fatalf("pre-staging empty delivery/project mismatch admission %q = %v", path, err) + } + if _, err := adapter.Activate(context.Background(), clientsKimiEnv(), request); err == nil || !strings.Contains(err.Error(), "delivery differs") { + t.Fatalf("direct activation accepted delivery-only mismatch %q: %v", path, err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("delivery-only mismatch changed project/native/user/B bytes") + } + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kimi_fixture_test.go b/install/integrationctl/agentplugins/providers/project_kimi_fixture_test.go new file mode 100644 index 000000000..468a8bc10 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kimi_fixture_test.go @@ -0,0 +1,130 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kimi" + "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/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +const kimiForeignConfig = `{"mcpServers":{"foreign":{"transport":"http","url":"https://foreign.test/mcp","enabled":false}},"preference":"keep"}` +const kimiSkill = "---\nname: docs\ndescription: Inert Kimi project fixture\n---\nKIMI-PROJECT-ONE\n" + +type kimiProjectFixture struct { + base, project, user, other, source, data string + config string + request domain.ActivationRequest + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager +} + +func newKimiProjectFixture(t *testing.T, stdio, rootConfig bool) *kimiProjectFixture { + t.Helper() + f := &kimiProjectFixture{base: projectGeminiTemp(t)} + f.project, f.user, f.other, f.source = filepath.Join(f.base, "A"), filepath.Join(f.base, "user"), filepath.Join(f.base, "B"), filepath.Join(f.base, "source") + for _, root := range []string{f.project, f.user, f.other} { + native := filepath.Join(root, ".kimi-code") + if root == f.user { + native = f.user + } + projectGeminiWrite(t, filepath.Join(native, "mcp.json"), kimiForeignConfig) + projectGeminiWrite(t, filepath.Join(native, "skills", "foreign", "SKILL.md"), "foreign skill\n") + if root != f.user { + if err := os.Mkdir(filepath.Join(root, ".git"), 0700); err != nil { + t.Fatal(err) + } + } + } + f.config = filepath.Join(f.project, ".kimi-code", "mcp.json") + if rootConfig { + projectGeminiWrite(t, filepath.Join(f.project, ".mcp.json"), kimiForeignConfig) + } + var err error + f.registry, err = clients.NewRegistry(kimi.New()) + if err != nil { + t.Fatal(err) + } + f.request.Client = domain.DetectedClient{ClientID: domain.ClientKimi, ConfigRoot: f.user, ExecutablePath: filepath.Join(f.base, "kimi"), Version: "2.3.1", Status: domain.DetectionDetected} + f.stager = providers.Stager{Registry: f.registry, Paths: pathpolicy.Policy{}} + f.data = filepath.Join(f.project, ".uap", "data", "kimi-exact-receipt") + projectGeminiWrite(t, filepath.Join(f.data, "keep.txt"), "persistent data\n") + projectGeminiWrite(t, filepath.Join(f.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), kimiSkill) + mcp := `{"type":"streamable-http","url":"https://managed.test/one"}` + if stdio { + mcp = `{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_ROOT}","env":{"MARKER":"${PLUGIN_DATA}/marker"}}` + projectGeminiWrite(t, filepath.Join(f.source, "server.js"), "// Inert; never executed.\n") + launcher := filepath.Join(f.base, "TEST-inert-launcher") + if err := os.WriteFile(launcher, []byte("TEST trusted launcher bytes; never executed"), 0700); err != nil { + t.Fatal(err) + } + f.stager.LauncherSource, err = managedstdio.NewSource(launcher, "fixture") + if err != nil { + t.Fatal(err) + } + } + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":`+mcp+`}}`) + f.stage(t, "initial") + return f +} + +func (f *kimiProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + f.envelope, err = (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: f.source}) + if err != nil { + t.Fatal(err) + } + planning := planner.Planner{ProjectRoot: f.project, ManagedRoot: filepath.Join(f.project, ".uap", "managed"), Registry: f.registry, Paths: pathpolicy.Policy{}} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: f.envelope, Client: f.request.Client, Scope: domain.ScopeProject, ProjectRoot: f.project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.NativeRegistryRoot != filepath.Join(f.project, ".kimi-code") || plan.Activation != domain.ActivationPrepared { + t.Fatalf("Kimi repository plan = %+v, %v", plan, err) + } + delivery, err := f.stager.StageWithPluginData(context.Background(), f.envelope, plan, operation, domain.CompatibilityHints{}, f.data) + if err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(plan.ActivePath); err == nil { + if err := os.Rename(plan.ActivePath, plan.ActivePath+"-"+operation+"-old"); err != nil { + t.Fatal(err) + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + f.request.Plan, f.request.Delivery, f.request.DeclaredName = plan, delivery, "demo" +} + +func (f *kimiProjectFixture) activator(kernel nativeconfig.Kernel) providers.Activator { + return providers.Activator{Registry: f.registry, NativeConfig: &kernel} +} + +func (f *kimiProjectFixture) remove(objects []domain.NativeObjectOwnership) domain.DeactivationRequest { + return domain.DeactivationRequest{Client: f.request.Client, TargetContext: f.request.Plan.TargetContext, + NativeRegistryRoot: filepath.Join(f.project, ".kimi-code"), PhysicalArtifactID: f.request.Plan.PhysicalArtifactID, ManagedArtifactPath: f.request.Plan.ActivePath, Confirmed: true, NativeObjects: objects} +} + +func kimiPrepared(t *testing.T, outcome domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("Kimi configured state = %+v, %v", outcome, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kimi_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_kimi_lifecycle_test.go new file mode 100644 index 000000000..1a38f9d6e --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kimi_lifecycle_test.go @@ -0,0 +1,102 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Real loader/planner/stager/native kernel; no Kimi or server process runs. +func TestProjectKimiComposedLifecycleSelectsEffectiveConfig(t *testing.T) { + for _, rootConfig := range []bool{false, true} { + t.Run(map[bool]string{false: "github-fallback", true: "root-precedence"}[rootConfig], func(t *testing.T) { + f := newKimiProjectFixture(t, false, rootConfig) + user, other := projectGeminiInventory(t, f.user), projectGeminiInventory(t, f.other) + activator := f.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), f.request) + kimiPrepared(t, outcome, err) + projectGeminiAssert(t, filepath.Join(f.project, ".kimi-code", "skills", "docs", "SKILL.md"), kimiSkill) + config, err := os.ReadFile(f.config) + if err != nil || !strings.Contains(string(config), "managed.test/one") || !strings.Contains(string(config), `"enabled":false`) || !strings.Contains(string(config), `"transport":"http"`) { + t.Fatalf("Kimi config lost transport or foreign settings: %s, %v", config, err) + } + if rootConfig { + projectGeminiAssert(t, filepath.Join(f.project, ".mcp.json"), kimiForeignConfig) + } + f.request.PreviousNativeObjects = outcome.NativeObjects + for _, verify := range []bool{false, true} { + f.request.VerifyOnly = verify + outcome, err = activator.Activate(context.Background(), f.request) + kimiPrepared(t, outcome, err) + } + for range 2 { + removed, err := activator.Deactivate(context.Background(), f.remove(outcome.NativeObjects)) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("exact repeated Kimi remove = %+v, %v", removed, err) + } + } + if _, err := os.Stat(filepath.Join(f.project, ".kimi-code", "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("Kimi managed skill survived: %v", err) + } + remaining, err := os.ReadFile(f.config) + if err != nil || strings.Contains(string(remaining), "managed.test") || !strings.Contains(string(remaining), "foreign.test") { + t.Fatalf("Kimi removal changed foreign MCP data: %s, %v", remaining, err) + } + projectGeminiAssert(t, filepath.Join(f.data, "keep.txt"), "persistent data\n") + if !reflect.DeepEqual(user, projectGeminiInventory(t, f.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, f.other)) { + t.Fatal("Kimi project lifecycle mutated user or B") + } + }) + } +} + +func TestProjectKimiUpdatesRepairsAndDropsMCP(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + a := f.activator(nativeconfig.New()) + out, err := a.Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + f.request.PreviousNativeObjects = out.NativeObjects + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), strings.Replace(kimiSkill, "ONE", "TWO", 1)) + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"sse","url":"https://managed.test/two"}}}`) + f.stage(t, "update") + out, err = a.Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + skill := filepath.Join(f.project, ".kimi-code", "skills", "docs") + if err := os.RemoveAll(skill); err != nil { + t.Fatal(err) + } + f.request.PreviousNativeObjects = out.NativeObjects + out, err = a.Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + projectGeminiAssert(t, filepath.Join(skill, "SKILL.md"), strings.Replace(kimiSkill, "ONE", "TWO", 1)) + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{}}`) + f.request.PreviousNativeObjects = out.NativeObjects + f.stage(t, "drop-mcp") + out, err = a.Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + body, err := os.ReadFile(f.config) + if err != nil || strings.Contains(string(body), "managed.test") || !strings.Contains(string(body), "foreign.test") { + t.Fatalf("dropped MCP/foreign config = %s, %v", body, err) + } +} + +func TestProjectKimiRejectsAttestationBeforeAutomaticFallback(t *testing.T) { + f := newKimiProjectFixture(t, false, false) + before := projectGeminiInventory(t, f.base) + f.request.ActivationComplete = true + for _, support := range []domain.SupportLevel{domain.SupportNative, domain.SupportPrepared} { + f.request.Plan.Components[0].Support = support + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("Kimi project attestation bypass: %v", err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("Kimi attestation created effects") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kimi_stdio_test.go b/install/integrationctl/agentplugins/providers/project_kimi_stdio_test.go new file mode 100644 index 000000000..d8060d674 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kimi_stdio_test.go @@ -0,0 +1,54 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio" +) + +func clientsKimiEnv() clients.Env { return clients.Env{NativeConfig: nativeconfig.New()} } + +func TestProjectKimiStdioBindsImmutablePackageAndSelectedData(t *testing.T) { + if !managedstdio.Supported() { + t.Skip("managed stdio platform unsupported") + } + f := newKimiProjectFixture(t, true, true) + out, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + kimiPrepared(t, out, err) + body, err := os.ReadFile(f.config) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{`"transport":"stdio"`, `"PLUGIN_DATA"`, "stdio-v1"} { + if !strings.Contains(string(body), want) { + t.Fatalf("Kimi launcher contract omitted %s: %s", want, body) + } + } + active := f.request.Plan.ActivePath + projectGeminiWrite(t, filepath.Join(f.other, "data", "marker"), "other data\n") + before := projectGeminiInventory(t, f.base) + objects, err := f.stager.ProjectActiveNative(context.Background(), f.envelope, f.request.Plan, f.request.Delivery.ArtifactDigest, f.data) + if err != nil || len(objects) != 2 { + t.Fatalf("correct receipt projection = %+v, %v", objects, err) + } + for _, selected := range []string{"", "relative", filepath.Join(f.other, "data")} { + if _, err := f.stager.ProjectActiveNative(context.Background(), f.envelope, f.request.Plan, f.request.Delivery.ArtifactDigest, selected); !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("wrong selected stdio data %q = %v", selected, err) + } + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("active receipt inspection rewrote immutable package, native config, user or B") + } + if _, err := os.Stat(filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName)); err != nil { + t.Fatal(err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kiro_boundaries_test.go b/install/integrationctl/agentplugins/providers/project_kiro_boundaries_test.go new file mode 100644 index 000000000..0de1d6df4 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kiro_boundaries_test.go @@ -0,0 +1,110 @@ +package providers_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectKiroAttestationCannotUpgradeConfiguration(t *testing.T) { + fixture := newKiroProjectFixture(t, false) + before := projectGeminiInventory(t, fixture.request.Plan.NativeRegistryRoot) + fixture.request.ActivationComplete = true + for _, prepared := range []bool{false, true} { + if prepared { + fixture.request.Plan.Components[0].Support = domain.SupportPrepared + } + _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request) + if err == nil || !strings.Contains(err.Error(), "cannot be established by activation attestation") { + t.Fatalf("attestation bypassed preflight: %v", err) + } + } + if _, err := kiro.New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, fixture.request); err == nil { + t.Fatal("direct adapter bypassed preflight") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, fixture.request.Plan.NativeRegistryRoot)) { + t.Fatal("attestation changed project files") + } +} + +func TestProjectKiroForeignDisabledMalformedAndWriterFailurePreserveNativeFiles(t *testing.T) { + for _, failure := range []string{"disabled-collision", "malformed", "skill-collision", "writer-lock"} { + t.Run(failure, func(t *testing.T) { + fixture := newKiroProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + kernel := nativeconfig.New() + switch failure { + case "disabled-collision": + projectGeminiWrite(t, filepath.Join(root, "settings", "mcp.json"), `{"mcpServers":{"docs":{"url":"https://foreign.test/keep","disabled":true}}}`) + case "malformed": + projectGeminiWrite(t, filepath.Join(root, "settings", "mcp.json"), `{"mcpServers":[]}`) + case "writer-lock": + kernel = nativeconfig.NewWithLockAcquirer(func(nativeconfig.Paths, nativeconfig.Codec) (func() error, error) { + return nil, errors.New("inert Kiro writer refusal") + }) + case "skill-collision": + projectGeminiWrite(t, filepath.Join(root, "skills", "docs", "SKILL.md"), "foreign same-name skill\n") + } + before, user, other := projectGeminiInventory(t, root), projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + if _, err := fixture.activator(kernel).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("unsafe/failed native mutation succeeded") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) || !reflect.DeepEqual(user, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(other, projectGeminiInventory(t, fixture.other)) { + t.Fatal("failed install changed native files") + } + }) + } +} + +func TestProjectKiroScopeRootAndBindingAuthorityRefuse(t *testing.T) { + for _, mismatch := range []string{"user-registry", "user-context", "other-user", "omitted-user", "copied-project"} { + t.Run(mismatch, func(t *testing.T) { + fixture := newKiroProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + before := projectGeminiInventory(t, root) + switch mismatch { + case "user-registry": + fixture.request.Plan.NativeRegistryRoot = fixture.user + case "user-context": + fixture.request.Plan.TargetContext.Scope = domain.ScopeUser + case "omitted-user": + fixture.request.Plan.TargetContext.UserConfigRoot = "" + case "other-user": + fixture.request.Plan.TargetContext.UserConfigRoot = fixture.other + case "copied-project": + fixture.request.Plan.TargetContext.ProjectRoot = fixture.other + fixture.request.Plan.NativeRegistryRoot = filepath.Join(fixture.other, ".kiro") + } + if _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("mismatched project authority admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("authority refusal changed original project") + } + }) + } +} + +func TestProjectKiroContainedNativeRootAliasRefuses(t *testing.T) { + fixture := newKiroProjectFixture(t, false) + root := fixture.request.Plan.NativeRegistryRoot + original := root + "-physical" + if err := os.Rename(root, original); err != nil { + t.Fatal(err) + } + if err := os.Symlink(original, root); err != nil { + t.Skipf("symlink unavailable: %v", err) + } + if _, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request); err == nil { + t.Fatal("contained native root alias admitted") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kiro_fixture_test.go b/install/integrationctl/agentplugins/providers/project_kiro_fixture_test.go new file mode 100644 index 000000000..5d979d49c --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kiro_fixture_test.go @@ -0,0 +1,120 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "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/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +type kiroProjectFixture struct { + request domain.ActivationRequest + user, other, source string + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager +} + +func newKiroProjectFixture(t *testing.T, stdio bool) *kiroProjectFixture { + t.Helper() + base := projectGeminiTemp(t) + project, user, other := filepath.Join(base, "A"), filepath.Join(base, "user", ".kiro"), filepath.Join(base, "B") + for _, root := range []string{user, filepath.Join(project, ".kiro"), filepath.Join(other, ".kiro")} { + projectGeminiWrite(t, filepath.Join(root, "settings", "mcp.json"), `{ // preserve comment +"mcpServers":{"foreign":{"url":"https://foreign.test/mcp","disabled":true}},"policy":"preserve"}`) + projectGeminiWrite(t, filepath.Join(root, "hooks.json"), "foreign hooks\n") + projectGeminiWrite(t, filepath.Join(root, "skills", "foreign", "SKILL.md"), "foreign skill\n") + } + registry, err := clients.NewRegistry(kiro.New()) + if err != nil { + t.Fatal(err) + } + fixture := &kiroProjectFixture{source: filepath.Join(base, "source"), user: user, other: other, registry: registry} + fixture.stager = providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + fixture.request.Client = domain.DetectedClient{ClientID: domain.ClientKiro, ConfigRoot: user, ExecutablePath: "fixture-editor-never-executed", Status: domain.DetectionDetected} + fixture.request.Plan.TargetContext, err = domain.NewTargetContext(domain.ScopeProject, project, user) + if err != nil { + t.Fatal(err) + } + projectGeminiWrite(t, filepath.Join(fixture.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(fixture.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert project fixture\n---\nVersion one.\n") + mcp := `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v1"}}}` + if stdio { + mcp = `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"stdio","command":"node","args":["${PLUGIN_ROOT}/server.js","${PLUGIN_DATA}"],"cwd":"${PLUGIN_ROOT}"}}}` + projectGeminiWrite(t, filepath.Join(fixture.source, "server.js"), "// Inert fixture; never executed.\n") + fixture.stager.LauncherSource = kiroFixtureLauncher(t, base) + } + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), mcp) + fixture.stage(t, "initial") + return fixture +} + +func kiroFixtureLauncher(t *testing.T, base string) *managedstdio.Source { + t.Helper() + if !managedstdio.Supported() { + t.Skip("managed stdio platform unsupported") + } + path := filepath.Join(base, "inert-launcher") + if err := os.WriteFile(path, []byte("trusted fixture bytes; never executed"), 0700); err != nil { + t.Fatal(err) + } + source, err := managedstdio.NewSource(path, "fixture") + if err != nil { + t.Fatal(err) + } + return source +} + +func (fixture *kiroProjectFixture) stage(t *testing.T, operation string) { + t.Helper() + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + fixture.envelope, err = (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: fixture.source}) + if err != nil { + t.Fatal(err) + } + project := fixture.request.Plan.TargetContext.ProjectRoot + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: fixture.registry} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: fixture.envelope, Client: fixture.request.Client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared { + t.Fatalf("Kiro project plan = %+v, %v", plan, err) + } + delivery, err := fixture.stager.Stage(context.Background(), fixture.envelope, plan, operation, domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if _, err := os.Lstat(plan.ActivePath); err == nil { + if err := os.Rename(plan.ActivePath, plan.ActivePath+"-"+operation+"-backup"); err != nil { + t.Fatal(err) + } + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + fixture.request.Plan, fixture.request.Delivery, fixture.request.DeclaredName = plan, delivery, "demo" +} + +func (fixture *kiroProjectFixture) activator(kernel nativeconfig.Kernel) providers.Activator { + return providers.Activator{Registry: fixture.registry, NativeConfig: &kernel} +} + +func (fixture *kiroProjectFixture) removal(objects []domain.NativeObjectOwnership) domain.DeactivationRequest { + return domain.DeactivationRequest{Client: fixture.request.Client, TargetContext: fixture.request.Plan.TargetContext, + NativeRegistryRoot: fixture.request.Plan.NativeRegistryRoot, Confirmed: true, NativeObjects: objects} +} diff --git a/install/integrationctl/agentplugins/providers/project_kiro_lifecycle_test.go b/install/integrationctl/agentplugins/providers/project_kiro_lifecycle_test.go new file mode 100644 index 000000000..ca44a0773 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kiro_lifecycle_test.go @@ -0,0 +1,94 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func kiroAssertPrepared(t *testing.T, outcome domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled || outcome.ActivationAttested { + t.Fatalf("Kiro configured state = %+v, %v", outcome, err) + } +} + +// Inert production loader/planner/renderer/stager/provider evidence; this does +// not execute Kiro, the injected launcher, a model or an MCP server. +func TestProjectKiroComposedLifecycleAndScopeIsolation(t *testing.T) { + fixture := newKiroProjectFixture(t, false) + userBefore, bBefore := projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + activator := fixture.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + root := fixture.request.Plan.NativeRegistryRoot + activeBefore := projectGeminiInventory(t, fixture.request.Plan.ActivePath) + objects, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, "") + if err != nil || len(objects) != 2 || !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, fixture.request.Plan.ActivePath)) { + t.Fatalf("read-only projection = %+v, %v", objects, err) + } + binding := domain.ClientBinding{Scope: "project", PhysicalArtifact: fixture.request.Plan.PhysicalArtifactID, + TargetLocator: fixture.request.Plan.ActivePath, NativeProfileRoot: root, NativeObjects: outcome.NativeObjects} + if err := kiro.New().ValidateBindingProfile(root, binding); err != nil { + t.Fatal(err) + } + fixture.request.PreviousNativeObjects = outcome.NativeObjects + for _, verify := range []bool{false, true} { + fixture.request.VerifyOnly = verify + outcome, err = activator.Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + } + data := filepath.Join(fixture.request.Plan.TargetContext.ProjectRoot, ".uap", "data", "keep.txt") + projectGeminiWrite(t, data, "persistent data\n") + for range 2 { + removed, err := activator.Deactivate(context.Background(), fixture.removal(outcome.NativeObjects)) + if err != nil || !removed.ExternalRemovalComplete { + t.Fatalf("repeat exact remove = %+v, %v", removed, err) + } + } + projectGeminiAssert(t, data, "persistent data\n") + if _, err := os.Stat(filepath.Join(root, "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("managed skill survived remove: %v", err) + } + if !reflect.DeepEqual(userBefore, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(bBefore, projectGeminiInventory(t, fixture.other)) { + t.Fatal("Kiro project lifecycle changed user or project B") + } + projectGeminiAssert(t, filepath.Join(root, "hooks.json"), "foreign hooks\n") +} + +func TestProjectKiroUpdateRepairAndRemovedComponent(t *testing.T) { + fixture := newKiroProjectFixture(t, false) + activator := fixture.activator(nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + fixture.request.PreviousNativeObjects = outcome.NativeObjects + projectGeminiWrite(t, filepath.Join(fixture.source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Updated inert fixture\n---\nVersion two.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/v2"}}}`) + fixture.stage(t, "update") + outcome, err = activator.Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + skill := filepath.Join(fixture.request.Plan.NativeRegistryRoot, "skills", "docs") + if err := os.RemoveAll(skill); err != nil { + t.Fatal(err) + } + fixture.request.PreviousNativeObjects = outcome.NativeObjects + outcome, err = activator.Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + projectGeminiAssert(t, filepath.Join(skill, "SKILL.md"), "---\nname: docs\ndescription: Updated inert fixture\n---\nVersion two.\n") + projectGeminiWrite(t, filepath.Join(fixture.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{}}`) + fixture.request.PreviousNativeObjects = outcome.NativeObjects + fixture.stage(t, "drop-mcp") + outcome, err = activator.Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + config, err := os.ReadFile(filepath.Join(fixture.request.Plan.NativeRegistryRoot, "settings", "mcp.json")) + if err != nil || strings.Contains(string(config), "managed.test") || !strings.Contains(string(config), `"disabled":true`) { + t.Fatalf("removed component/foreign data = %s, %v", config, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kiro_receipt_test.go b/install/integrationctl/agentplugins/providers/project_kiro_receipt_test.go new file mode 100644 index 000000000..43b9f26d7 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kiro_receipt_test.go @@ -0,0 +1,80 @@ +package providers_test + +import ( + "context" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectKiroExactReceiptAuthorityRefusesBeforeEffects(t *testing.T) { + for _, mismatch := range []string{"unknown-kind", "id", "duplicate", "other-project", "source-traversal", "nonclean-path"} { + t.Run(mismatch, func(t *testing.T) { + f := newKiroProjectFixture(t, false) + root := f.request.Plan.NativeRegistryRoot + objects := append([]domain.NativeObjectOwnership(nil), f.request.Delivery.NativeObjects...) + skill := 0 + for i, object := range objects { + if object.Kind == kiro.SkillObjectKind { + skill = i + } + } + switch mismatch { + case "unknown-kind": + objects[skill].Kind = "foreign_kind" + case "id": + objects[skill].ObjectID = "kiro-skill:foreign" + case "duplicate": + objects = append(objects, objects[skill]) + case "other-project": + objects[skill].Path = filepath.Join(f.other, ".kiro", "skills", "docs") + case "source-traversal": + objects[skill].SourceRelative = "skills/../docs" + case "nonclean-path": + objects[skill].Path = filepath.Dir(objects[skill].Path) + string(filepath.Separator) + ".." + string(filepath.Separator) + "skills" + string(filepath.Separator) + "docs" + } + f.request.Delivery.NativeObjects = objects + before := projectGeminiInventory(t, root) + if _, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request); err == nil { + t.Fatal("forged project native ownership was admitted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, root)) { + t.Fatal("receipt refusal changed native files") + } + }) + } +} + +func TestProjectKiroPrepareIntentAndRegistryRejectCopiedBinding(t *testing.T) { + f := newKiroProjectFixture(t, false) + f.request.Plan.InstallIntent = domain.InstallIntentPrepare + outcome, err := f.activator(nativeconfig.New()).Activate(context.Background(), f.request) + kiroAssertPrepared(t, outcome, err) + binding := domain.ClientBinding{Scope: "project", PhysicalArtifact: f.request.Plan.PhysicalArtifactID, TargetLocator: f.request.Plan.ActivePath, NativeProfileRoot: f.request.Plan.NativeRegistryRoot, NativeObjects: outcome.NativeObjects} + adapter := kiro.New() + finding, err := adapter.InspectNativeRegistry(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, f.request.Client, f.request.Plan, &binding) + if err != nil || finding != clients.RegistryExpected { + t.Fatalf("exact project registry = %v,%v", finding, err) + } + for _, change := range []string{"scope", "artifact", "package", "root"} { + copied := binding + switch change { + case "scope": + copied.Scope = "user" + case "artifact": + copied.PhysicalArtifact = "different" + case "package": + copied.TargetLocator = filepath.Join(f.other, ".uap", "managed", "clients", "kiro", binding.PhysicalArtifact) + case "root": + copied.NativeProfileRoot = filepath.Join(f.other, ".kiro") + } + if _, err := adapter.InspectNativeRegistry(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, f.request.Client, f.request.Plan, &copied); err == nil { + t.Fatalf("copied %s binding admitted", change) + } + } +} diff --git a/install/integrationctl/agentplugins/providers/project_kiro_stdio_test.go b/install/integrationctl/agentplugins/providers/project_kiro_stdio_test.go new file mode 100644 index 000000000..b25ce321c --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_kiro_stdio_test.go @@ -0,0 +1,58 @@ +package providers_test + +import ( + "context" + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/kiro" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio" +) + +func TestProjectKiroStdioUsesBoundManagedLauncherWithoutExecution(t *testing.T) { + fixture := newKiroProjectFixture(t, true) + beforeUser, beforeB := projectGeminiInventory(t, fixture.user), projectGeminiInventory(t, fixture.other) + outcome, err := fixture.activator(nativeconfig.New()).Activate(context.Background(), fixture.request) + kiroAssertPrepared(t, outcome, err) + servers, _, _, _, err := kiro.ReadMCPConfig(filepath.Join(fixture.request.Plan.NativeRegistryRoot, "settings", "mcp.json")) + if err != nil { + t.Fatal(err) + } + server := servers["docs"].(map[string]any) + args := make([]string, 0, len(server["args"].([]any))) + for _, arg := range server["args"].([]any) { + args = append(args, arg.(string)) + } + env := server["env"].(map[string]any) + active := fixture.request.Plan.ActivePath + data := filepath.Join(fixture.request.Plan.TargetContext.ProjectRoot, ".uap", "data", fixture.request.Plan.PhysicalArtifactID) + wantArgs := []string{managedstdio.Mode, active, data, active, "plugin", "--", "node", active + "/server.js", data} + if server["command"] != filepath.Join(active, filepath.FromSlash(managedstdio.RelativeDirectory), managedstdio.ExecutableName) || !reflect.DeepEqual(args, wantArgs) || server["cwd"] != nil || env["PLUGIN_ROOT"] != active || env["PLUGIN_DATA"] != data { + t.Fatalf("native launcher binding = %+v; want args %q", server, wantArgs) + } + projectGeminiAssert(t, server["command"].(string), "trusted fixture bytes; never executed") + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, fixture.user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, fixture.other)) { + t.Fatal("stdio configuration changed other scopes") + } + activeBefore := projectGeminiInventory(t, active) + objects, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, data) + if err != nil || len(objects) != 2 || !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, active)) { + t.Fatalf("stdio read-only projection = %+v, %v", objects, err) + } + for _, selectedData := range []string{"", filepath.Join(filepath.Dir(data), "other-receipt")} { + if _, err := fixture.stager.ProjectActiveNative(context.Background(), fixture.envelope, fixture.request.Plan, fixture.request.Delivery.ArtifactDigest, selectedData); !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("different selected data locator was accepted: %v", err) + } + if !reflect.DeepEqual(activeBefore, projectGeminiInventory(t, active)) { + t.Fatal("refused data locator changed immutable package bytes") + } + } + fixture.stager.LauncherSource = nil + if _, err := fixture.stager.Stage(context.Background(), fixture.envelope, fixture.request.Plan, "missing-launcher", domain.CompatibilityHints{}); err == nil { + t.Fatal("project stdio staged without trusted launcher") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_opencode_alias_test.go b/install/integrationctl/agentplugins/providers/project_opencode_alias_test.go new file mode 100644 index 000000000..c8dd44d67 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_opencode_alias_test.go @@ -0,0 +1,43 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/opencode" +) + +func TestProjectOpenCodeRejectsContainedNativeAliases(t *testing.T) { + for _, kind := range []string{"skills", "config"} { + t.Run(kind, func(t *testing.T) { + request, _, _ := projectOpenCodeRequest(t) + project := request.Plan.TargetContext.ProjectRoot + alias, target := filepath.Join(project, ".opencode"), filepath.Join(project, "other-native") + if err := os.Mkdir(target, 0700); err != nil { + t.Fatal(err) + } + if kind == "config" { + alias, target = filepath.Join(project, "opencode.jsonc"), filepath.Join(target, "opencode.jsonc") + projectGeminiWrite(t, target, `{"mcp":{}}`) + if err := os.Remove(alias); err != nil { + t.Fatal(err) + } + } + before := projectGeminiInventory(t, filepath.Join(project, "other-native")) + if err := os.Symlink(target, alias); err != nil { + t.Skipf("native symlink fixture unavailable: %v", err) + } + if _, err := opencode.New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, request); err == nil { + t.Fatal("project accepted a contained native alias") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, filepath.Join(project, "other-native"))) { + t.Fatal("refused native alias changed its contained target") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_opencode_data_test.go b/install/integrationctl/agentplugins/providers/project_opencode_data_test.go new file mode 100644 index 000000000..d27a3f674 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_opencode_data_test.go @@ -0,0 +1,51 @@ +package providers_test + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/opencode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectOpenCodeReadOnlyProjectionRequiresSelectedDataAuthority(t *testing.T) { + request, other, user := projectOpenCodeRequest(t) + plan := request.Plan + plan.Components = []domain.ComponentDecision{{Kind: domain.ComponentMCPServer, Name: "docs", Support: domain.SupportNative}} + envelope := domain.PackageEnvelope{MCP: domain.MCPComponent{Servers: map[string]domain.MCPServer{ + "docs": {Name: "docs", Type: "stdio", Decoded: map[string]any{"command": "go", "args": []any{"version"}, "env": map[string]any{"DATA": "${PLUGIN_DATA}"}}}, + }}} + data := filepath.Join(plan.TargetContext.ProjectRoot, ".uap", "plugin-data", "selected") + // Rebuild this test-owned package as stdio before taking its immutable + // baseline; Project refuses authored use of its private projection path. + if err := os.Remove(filepath.Join(plan.ActivePath, opencode.OpenCodeProjectionFile)); err != nil { + t.Fatal(err) + } + adapter := opencode.New() + want, err := adapter.Project(t.Context(), clients.ProjectionInput{StagingPath: plan.ActivePath, Envelope: envelope, Plan: plan, PluginDataPath: data}) + if err != nil { + t.Fatal(err) + } + before, beforeUser, beforeOther := projectGeminiInventory(t, plan.TargetContext.ProjectRoot), projectGeminiInventory(t, user), projectGeminiInventory(t, other) + got, err := adapter.ProjectActiveNative(t.Context(), plan.ActivePath, envelope, plan, data) + if err != nil || !reflect.DeepEqual(got, want) { + t.Fatalf("exact selected data projection = %+v, %v", got, err) + } + for _, changed := range []string{"", "relative-data", filepath.Join(other, ".uap", "plugin-data", "selected"), data + "-other"} { + if got, err := adapter.ProjectActiveNative(t.Context(), plan.ActivePath, envelope, plan, changed); !errors.Is(err, domain.ErrProjectionRefreshRequired) || len(got) != 0 { + t.Fatalf("changed data authority %q accepted: %+v, %v", changed, got, err) + } + } + changed := plan + changed.ActivePath += "-other" + if _, err := adapter.ProjectActiveNative(t.Context(), plan.ActivePath, envelope, changed, data); !errors.Is(err, domain.ErrProjectionRefreshRequired) { + t.Fatalf("changed active package authority accepted: %v", err) + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, plan.TargetContext.ProjectRoot)) || !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, user)) || !reflect.DeepEqual(beforeOther, projectGeminiInventory(t, other)) { + t.Fatal("read-only projection mutated package/native/user/project-B bytes") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_opencode_keys_test.go b/install/integrationctl/agentplugins/providers/project_opencode_keys_test.go new file mode 100644 index 000000000..e3a18974e --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_opencode_keys_test.go @@ -0,0 +1,29 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestProjectOpenCodePreservesExactMCPLogicalKeys(t *testing.T) { + request, _, _ := projectOpenCodeRequestWithNames(t, []string{"api/server", "other api server"}) + activator := projectOpenCodeActivator(t, nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), request) + if err != nil || outcome.Verification != domain.VerificationInstalled { + t.Fatalf("exact project logical keys: %+v, %v", outcome, err) + } + body, err := os.ReadFile(filepath.Join(request.Plan.NativeRegistryRoot, "opencode.jsonc")) + if err != nil || !strings.Contains(string(body), `"api/server"`) || !strings.Contains(string(body), `"other api server"`) { + t.Fatalf("project normalized logical keys: %s, %v", body, err) + } + remove := domain.DeactivationRequest{Client: request.Client, TargetContext: request.Plan.TargetContext, NativeRegistryRoot: request.Plan.NativeRegistryRoot, Confirmed: true, NativeObjects: outcome.NativeObjects} + if result, err := activator.Deactivate(context.Background(), remove); err != nil || !result.ExternalRemovalComplete { + t.Fatalf("exact project-key removal: %+v, %v", result, err) + } +} diff --git a/install/integrationctl/agentplugins/providers/project_opencode_stager_test.go b/install/integrationctl/agentplugins/providers/project_opencode_stager_test.go new file mode 100644 index 000000000..5ba984e9e --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_opencode_stager_test.go @@ -0,0 +1,96 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/opencode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/commandext" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +// This uses the production loader, planner, renderer, stager and activator. +// It proves an inert contract journey, not OpenCode session/model discovery. +func TestProjectOpenCodePlannerStagerRendersCommandsAndOwnsExactNativeComponents(t *testing.T) { + request, projectB, user := projectOpenCodeRequest(t) + beforeUser, beforeB := projectGeminiInventory(t, user), projectGeminiInventory(t, projectB) + source := filepath.Join(projectGeminiTemp(t), "source") + projectGeminiWrite(t, filepath.Join(source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(source, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Project docs\n---\nInert docs.\n") + projectGeminiWrite(t, filepath.Join(source, filepath.FromSlash(commandext.RelativeDirectory), "echo.md"), "---\ndescription: Echo the marker\ndisable-model-invocation: false\n---\nUAP-PROJECT-INERT\n") + projectGeminiWrite(t, filepath.Join(source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"https://managed.test/mcp"}}}`) + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + envelope, err := (loader.Loader{Registry: schemas}).Load(context.Background(), domain.LoadInput{SnapshotRoot: source}) + if err != nil { + t.Fatal(err) + } + registry, err := clients.NewRegistry(opencode.New()) + if err != nil { + t.Fatal(err) + } + project := request.Plan.TargetContext.ProjectRoot + planning := planner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: registry} + plan, err := planning.Plan(context.Background(), domain.PlanRequest{Envelope: envelope, Client: request.Client, Scope: domain.ScopeProject, ProjectRoot: project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || plan.Status != domain.PlanReady || plan.Activation != domain.ActivationPrepared || plan.NativeRegistryRoot != project { + t.Fatalf("scoped OpenCode plan = %+v, %v", plan, err) + } + stager := providers.Stager{Registry: registry, Paths: pathpolicy.Policy{}, PluginDataRoot: filepath.Join(project, ".uap", "data")} + delivery, err := stager.Stage(context.Background(), envelope, plan, "project-opencode-command", domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if len(opencode.OpenCodeObjects(delivery.NativeObjects)) != 3 { + t.Fatalf("staged skill/command/MCP objects = %+v", delivery.NativeObjects) + } + if err := os.MkdirAll(filepath.Dir(plan.ActivePath), 0700); err != nil { + t.Fatal(err) + } + if err := os.Rename(delivery.StagingPath, plan.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + request.Plan, request.Delivery, request.DeclaredName = plan, delivery, "demo" + activator := projectOpenCodeActivator(t, nativeconfig.New()) + outcome, err := activator.Activate(context.Background(), request) + if err != nil || outcome.Activation != domain.ActivationPrepared || outcome.Verification != domain.VerificationInstalled { + t.Fatalf("staged project activation = %+v, %v", outcome, err) + } + body, err := os.ReadFile(filepath.Join(project, ".opencode", "skills", "demo-echo", "SKILL.md")) + if err != nil || !strings.Contains(string(body), "UAP-PROJECT-INERT") { + t.Fatalf("rendered command skill = %q, %v", body, err) + } + activeObjects, err := stager.ProjectActiveNative(context.Background(), envelope, plan, delivery.ArtifactDigest, "") + if err != nil || !reflect.DeepEqual(activeObjects, opencode.OpenCodeObjects(delivery.NativeObjects)) { + t.Fatalf("read-only native objects changed = %+v, %v", activeObjects, err) + } + remove := domain.DeactivationRequest{Client: request.Client, TargetContext: plan.TargetContext, NativeRegistryRoot: plan.NativeRegistryRoot, Confirmed: true, NativeObjects: outcome.NativeObjects} + if out, err := activator.Deactivate(context.Background(), remove); err != nil || !out.ExternalRemovalComplete { + t.Fatalf("project cleanup = %+v, %v", out, err) + } + if !reflect.DeepEqual(beforeUser, projectGeminiInventory(t, user)) || !reflect.DeepEqual(beforeB, projectGeminiInventory(t, projectB)) { + t.Fatal("stager journey changed another scope") + } +} + +func projectOpenCodeActivator(t *testing.T, kernel nativeconfig.Kernel) providers.Activator { + t.Helper() + registry, err := clients.NewRegistry(opencode.New()) + if err != nil { + t.Fatal(err) + } + return providers.Activator{Registry: registry, NativeConfig: &kernel} +} diff --git a/install/integrationctl/agentplugins/providers/project_opencode_test.go b/install/integrationctl/agentplugins/providers/project_opencode_test.go new file mode 100644 index 000000000..367c6e388 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_opencode_test.go @@ -0,0 +1,204 @@ +package providers_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/contracttest" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/opencode" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" +) + +func projectOpenCodeRequest(t *testing.T) (domain.ActivationRequest, string, string) { + return projectOpenCodeRequestWithNames(t, []string{"docs"}) +} + +func projectOpenCodeRequestWithNames(t *testing.T, names []string) (domain.ActivationRequest, string, string) { + t.Helper() + base := projectGeminiTemp(t) + user, projectA, projectB := filepath.Join(base, "user", "opencode"), filepath.Join(base, "A"), filepath.Join(base, "B") + foreign := "{ // preserve foreign settings\n\"mcp\":{\"foreign\":{\"type\":\"remote\",\"url\":\"https://foreign.test/mcp\",\"enabled\":false}},\"theme\":\"foreign\"}\n" + for _, root := range []string{user, projectA, projectB} { + projectGeminiWrite(t, filepath.Join(root, "opencode.jsonc"), foreign) + } + client := domain.DetectedClient{ClientID: domain.ClientOpenCode, ConfigRoot: user, Status: domain.DetectionDetected, OpenCodeHost: contracttest.OpenCodeV1Host{}} + target, err := domain.NewTargetContext(domain.ScopeProject, projectA, user) + if err != nil { + t.Fatal(err) + } + active := filepath.Join(projectA, ".uap", "managed", "clients", "opencode", "archive-demo") + projectGeminiWrite(t, filepath.Join(active, "skills", "docs", "SKILL.md"), "---\nname: docs\ndescription: Inert project fixture\n---\nProject skill.\n") + plan := domain.DeliveryPlan{ClientID: domain.ClientOpenCode, Scope: domain.ScopeProject, TargetContext: target, InstallIntent: domain.InstallIntentAutomatic, + NativeRegistryRoot: projectA, ActivePath: active, OpenCodeHost: client.OpenCodeHost, Components: []domain.ComponentDecision{ + {Kind: domain.ComponentSkill, Name: "docs", Support: domain.SupportNative}, + }} + envelope := domain.PackageEnvelope{Skills: map[string]domain.Skill{"docs": {Name: "docs", RelativePath: "skills/docs/SKILL.md"}}, + MCP: domain.MCPComponent{Servers: map[string]domain.MCPServer{}}} + for _, name := range names { + plan.Components = append(plan.Components, domain.ComponentDecision{Kind: domain.ComponentMCPServer, Name: name, Support: domain.SupportNative}) + envelope.MCP.Servers[name] = domain.MCPServer{Name: name, Type: "streamable-http", Decoded: map[string]any{"url": "https://managed.test/mcp"}} + } + objects, err := opencode.New().Project(context.Background(), clients.ProjectionInput{StagingPath: active, Envelope: envelope, Plan: plan, PluginDataPath: filepath.Join(projectA, ".uap", "plugin-data", "demo")}) + if err != nil { + t.Fatal(err) + } + return domain.ActivationRequest{Client: client, Plan: plan, DeclaredName: "archive-demo", Delivery: domain.StagedDelivery{ + ClientID: domain.ClientOpenCode, ActivePath: active, OwnedBase: filepath.Dir(active), NativeObjects: objects}}, projectB, user +} + +func TestProjectOpenCodeSplitNativeLifecycleKeepsUserAndOtherProject(t *testing.T) { + request, projectB, user := projectOpenCodeRequest(t) + userBefore, bBefore := projectGeminiInventory(t, user), projectGeminiInventory(t, projectB) + registry, err := clients.NewRegistry(opencode.New()) + if err != nil { + t.Fatal(err) + } + kernel := nativeconfig.New() + activator := providers.Activator{Registry: registry, NativeConfig: &kernel} + outcome, err := activator.Activate(context.Background(), request) + if err != nil || outcome.Verification != domain.VerificationInstalled || outcome.Activation != domain.ActivationPrepared || len(outcome.NativeObjects) != 2 { + t.Fatalf("project native activation: %+v, %v", outcome, err) + } + root := request.Plan.NativeRegistryRoot + if _, err := os.Stat(filepath.Join(root, ".opencode", "skills", "docs", "SKILL.md")); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(root, "skills")); !os.IsNotExist(err) { + t.Fatalf("project used user skill layout: %v", err) + } + before, err := os.ReadFile(filepath.Join(root, "opencode.jsonc")) + if err != nil || !strings.Contains(string(before), "preserve foreign settings") || !strings.Contains(string(before), "managed.test") || !strings.Contains(string(before), "foreign.test") { + t.Fatalf("native config lost exact foreign entries/comments: %s, %v", before, err) + } + request.PreviousNativeObjects = outcome.NativeObjects + for _, verifyOnly := range []bool{false, true} { + request.VerifyOnly = verifyOnly + result, err := activator.Activate(context.Background(), request) + if err != nil || result.Activation != domain.ActivationPrepared || result.Verification != domain.VerificationInstalled { + t.Fatalf("repeat project native activation: %+v, %v", result, err) + } + } + // Repair the exact same receipts after both selected native leaves disappear. + if err := os.RemoveAll(filepath.Join(root, ".opencode", "skills", "docs")); err != nil { + t.Fatal(err) + } + for _, object := range outcome.NativeObjects { + if object.ObjectID != "opencode-mcp:docs" { + continue + } + owned := nativeconfig.Receipt{Version: "1", Path: object.Path, Codec: nativeconfig.CodecOpenCode, Name: "docs", Digest: object.ManagedDigest} + if _, err := kernel.Apply(nativeconfig.Request{Paths: nativeconfig.Paths{JSON: filepath.Join(root, "opencode.json"), JSONC: filepath.Join(root, "opencode.jsonc")}, Codec: nativeconfig.CodecOpenCode, Action: nativeconfig.ActionRemove, Name: "docs", Owned: &owned}); err != nil { + t.Fatal(err) + } + } + request.VerifyOnly = false + if result, err := activator.Activate(context.Background(), request); err != nil || result.Verification != domain.VerificationInstalled || result.Activation != domain.ActivationPrepared { + t.Fatalf("same-pin native repair: %+v, %v", result, err) + } + remove := domain.DeactivationRequest{Client: request.Client, TargetContext: request.Plan.TargetContext, NativeRegistryRoot: root, Confirmed: true, NativeObjects: outcome.NativeObjects} + for range 2 { + result, err := activator.Deactivate(context.Background(), remove) + if err != nil || !result.ExternalRemovalComplete { + t.Fatalf("scoped removal: %+v, %v", result, err) + } + } + if _, err := os.Stat(filepath.Join(root, ".opencode", "skills", "docs")); !os.IsNotExist(err) { + t.Fatalf("managed skill survived remove: %v", err) + } + after, err := os.ReadFile(filepath.Join(root, "opencode.jsonc")) + if err != nil || strings.Contains(string(after), "managed.test") || !strings.Contains(string(after), "foreign.test") { + t.Fatalf("remove lost foreign native entry: %s, %v", after, err) + } + if !reflect.DeepEqual(userBefore, projectGeminiInventory(t, user)) || !reflect.DeepEqual(bBefore, projectGeminiInventory(t, projectB)) { + t.Fatal("project lifecycle mutated user or B") + } +} + +func TestProjectOpenCodeRejectsTamperedOwnershipBeforeNativeWrites(t *testing.T) { + for _, fault := range []string{"user-skill", "wrong-id", "padded-name", "relative-config"} { + t.Run(fault, func(t *testing.T) { + request, _, user := projectOpenCodeRequest(t) + before := projectGeminiInventory(t, filepath.Dir(request.Plan.TargetContext.ProjectRoot)) + for i := range request.Delivery.NativeObjects { + object := &request.Delivery.NativeObjects[i] + switch fault { + case "user-skill": + if object.ObjectID == "opencode-skill:docs" { + object.Path = filepath.Join(user, "skills", "docs") + } + case "wrong-id": + object.ObjectID = "foreign:" + object.LogicalName + case "padded-name": + object.LogicalName = " docs " + case "relative-config": + if object.ObjectID == "opencode-mcp:docs" { + object.Path = "opencode.jsonc" + } + } + } + if _, err := opencode.New().Activate(context.Background(), clients.Env{NativeConfig: nativeconfig.New()}, request); err == nil { + t.Fatal("tampered ownership reached native mutation") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, filepath.Dir(request.Plan.TargetContext.ProjectRoot))) { + t.Fatal("refused ownership changed files") + } + }) + } +} + +func TestProjectOpenCodeRefusesScopeMismatchAndAttestationBeforeEffects(t *testing.T) { + for _, fault := range []string{"user-registry", "user-context", "missing-scope", "attestation"} { + t.Run(fault, func(t *testing.T) { + request, _, _ := projectOpenCodeRequest(t) + root := request.Plan.TargetContext.ProjectRoot + before := projectGeminiInventory(t, filepath.Dir(root)) + switch fault { + case "user-registry": + request.Plan.NativeRegistryRoot = request.Client.ConfigRoot + case "user-context": + request.Plan.TargetContext.UserConfigRoot = filepath.Join(request.Client.ConfigRoot, "other") + case "missing-scope": + request.Plan.Scope = domain.ScopeUser + case "attestation": + request.ActivationComplete = true + } + registry, err := clients.NewRegistry(opencode.New()) + if err != nil { + t.Fatal(err) + } + kernel := nativeconfig.New() + activator := providers.Activator{Registry: registry, NativeConfig: &kernel} + if _, err := activator.Activate(context.Background(), request); err == nil { + t.Fatal("provider accepted invalid scoped authority") + } + if _, err := opencode.New().Activate(context.Background(), clients.Env{NativeConfig: kernel}, request); err == nil { + t.Fatal("direct adapter reuse accepted invalid scoped authority") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, filepath.Dir(root))) { + t.Fatal("refused project operation changed files") + } + }) + } +} + +func TestProjectOpenCodeNamespacePreflightUsesSelectedProject(t *testing.T) { + request, _, user := projectOpenCodeRequest(t) + guard := providers.OpenCodeNamespacePreflight{Kernel: nativeconfig.New()} + // A same-name user entry is a distinct configured scope and is preserved. + projectGeminiWrite(t, filepath.Join(user, "opencode.jsonc"), `{"mcp":{"docs":{"type":"remote","url":"https://user.test/mcp","enabled":false}}}`) + if err := guard.CheckMCPNamespace(context.Background(), request.Client, request.Plan, nil); err != nil { + t.Fatalf("project preflight used user config: %v", err) + } + // A disabled same-name entry in the selected project reserves that exact key. + projectGeminiWrite(t, filepath.Join(request.Plan.NativeRegistryRoot, "opencode.jsonc"), `{"mcp":{"docs":{"type":"remote","url":"https://project.test/mcp","enabled":false}}}`) + if err := guard.CheckMCPNamespace(context.Background(), request.Client, request.Plan, nil); err == nil { + t.Fatal("project preflight ignored its disabled native collision") + } +} diff --git a/install/integrationctl/agentplugins/providers/project_qoder_test.go b/install/integrationctl/agentplugins/providers/project_qoder_test.go new file mode 100644 index 000000000..c633b02a9 --- /dev/null +++ b/install/integrationctl/agentplugins/providers/project_qoder_test.go @@ -0,0 +1,268 @@ +package providers_test + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/pathpolicy" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/specregistry" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/qoder" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/commandext" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/providers" + "github.com/777genius/plugin-kit-ai/install/integrationctl/packagesnapshot" + legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +const qoderView = ".agentplugins-qoder-native" +const qoderProjectSkill = "---\nname: docs\ndescription: Inert Qoder project fixture\n---\nPinned original.\n" + +type qoderForbiddenRunner struct{ calls int } + +func (r *qoderForbiddenRunner) Run(_ context.Context, c legacyports.Command) (legacyports.CommandResult, error) { + r.calls++ + return legacyports.CommandResult{}, fmt.Errorf("forbidden Qoder native command: %v", c.Argv) +} + +type qoderProjectFixture struct { + base, project, user, other, source string + envelope domain.PackageEnvelope + registry *clients.Registry + stager providers.Stager + request domain.ActivationRequest +} + +func newQoderProjectFixture(t *testing.T, stage bool) *qoderProjectFixture { + t.Helper() + f := &qoderProjectFixture{base: projectGeminiTemp(t)} + f.project, f.user, f.other, f.source = filepath.Join(f.base, "A"), filepath.Join(f.base, "home", ".qoder"), filepath.Join(f.base, "B"), filepath.Join(f.base, "source") + for _, root := range []string{f.project, f.user, f.other} { + projectGeminiWrite(t, filepath.Join(root, "keep"), "foreign preserved") + } + projectGeminiWrite(t, filepath.Join(f.source, "plugin.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"demo","version":"1.0.0"}`) + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "SKILL.md"), qoderProjectSkill) + projectGeminiWrite(t, filepath.Join(f.source, "skills", "docs", "assets", "reference.txt"), "selected asset") + projectGeminiWrite(t, filepath.Join(f.source, "mcp.json"), `{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"docs":{"type":"streamable-http","url":"http://127.0.0.1:9/never"}}}`) + for _, aux := range []string{".mcp.json", "hooks/hooks.json", "agents/raw.md", ".qoder-plugin/plugin.json"} { + projectGeminiWrite(t, filepath.Join(f.source, filepath.FromSlash(aux)), "raw source; never import root") + } + projectGeminiWrite(t, filepath.Join(f.source, filepath.FromSlash(commandext.RelativeDirectory), "echo.md"), "---\ndescription: Echo text\ndisable-model-invocation: false\n---\nQODER-COMMAND\n") + projectGeminiWrite(t, filepath.Join(f.source, filepath.FromSlash(commandext.RelativeDirectory), "wizard.md"), "---\ndescription: Explicit only\ndisable-model-invocation: true\n---\nWITHHELD\n") + var err error + f.registry, err = clients.NewRegistry(qoder.New()) + if err != nil { + t.Fatal(err) + } + schemas, err := specregistry.New() + if err != nil { + t.Fatal(err) + } + f.envelope, err = (loader.Loader{Registry: schemas}).Load(t.Context(), domain.LoadInput{SnapshotRoot: f.source}) + if err != nil { + t.Fatal(err) + } + f.request.Client = domain.DetectedClient{ClientID: domain.ClientQoder, ConfigRoot: f.user, ExecutablePath: "/inert/qoder-must-never-execute", Status: domain.DetectionDetected} + planning := planner.Planner{ProjectRoot: f.project, ManagedRoot: filepath.Join(f.project, ".uap", "managed"), Paths: pathpolicy.Policy{}, Registry: f.registry} + f.request.Plan, err = planning.Plan(t.Context(), domain.PlanRequest{Envelope: f.envelope, Client: f.request.Client, Scope: domain.ScopeProject, ProjectRoot: f.project, PhysicalArtifactID: "demo-0123456789ab"}) + if err != nil || f.request.Plan.Status != domain.PlanReady { + t.Fatalf("Qoder project plan: %+v %v", f.request.Plan, err) + } + f.request.DeclaredName = "demo" + f.stager = providers.Stager{Registry: f.registry, Paths: pathpolicy.Policy{}} + if stage { + f.commit(t) + } + return f +} +func (f *qoderProjectFixture) commit(t *testing.T) { + t.Helper() + delivery, err := f.stager.Stage(t.Context(), f.envelope, f.request.Plan, "qoder-stage", domain.CompatibilityHints{}) + if err != nil { + t.Fatal(err) + } + if filepath.Dir(delivery.StagingPath) != f.request.Plan.TargetRoot { + t.Fatal("wrong staging base") + } + if err := os.Rename(delivery.StagingPath, delivery.ActivePath); err != nil { + t.Fatal(err) + } + delivery.StagingPath = "" + f.request.Delivery = delivery +} +func qoderPrepared(t *testing.T, out domain.ActivationOutcome, err error) { + t.Helper() + if err != nil || out.Activation != domain.ActivationPrepared || out.Verification != domain.VerificationInstalled || out.ActivationAttested || out.NativeEffect != domain.NativeEffectUnchanged || len(out.NativeObjects) != 0 { + t.Fatalf("Qoder Prepared file evidence: %+v %v", out, err) + } +} +func TestProjectQoderClosedViewAndNeutralDiscovery(t *testing.T) { + f := newQoderProjectFixture(t, true) + plan := f.request.Plan + if plan.TargetAnchor != filepath.Join(f.project, ".uap", "managed") || plan.TargetRoot != filepath.Join(plan.TargetAnchor, "clients", "qoder") || plan.NativeRegistryRoot != plan.TargetRoot || plan.NativeRegistryExecutable != "" { + t.Fatalf("wrong exact layout %+v", plan) + } + for _, c := range plan.Components { + if c.Kind == domain.ComponentMCPServer && c.Support != domain.SupportUnsupported { + t.Fatal("MCP promoted") + } + } + view := filepath.Join(plan.ActivePath, qoderView) + entries, err := os.ReadDir(view) + if err != nil || len(entries) != 2 { + t.Fatalf("view root: %v %v", entries, err) + } + projectGeminiAssert(t, filepath.Join(view, "skills", "docs", "SKILL.md"), qoderProjectSkill) + projectGeminiAssert(t, filepath.Join(view, "skills", "docs", "assets", "reference.txt"), "selected asset") + if body, err := os.ReadFile(filepath.Join(view, "skills", "demo-echo", "SKILL.md")); err != nil || !strings.Contains(string(body), "QODER-COMMAND") { + t.Fatalf("command skill %s %v", body, err) + } + for _, path := range []string{".mcp.json", "mcp.json", "hooks", "agents", "skills/demo-wizard"} { + if _, err := os.Lstat(filepath.Join(view, filepath.FromSlash(path))); !os.IsNotExist(err) { + t.Fatalf("unqualified native path %s: %v", path, err) + } + } + projectGeminiAssert(t, filepath.Join(plan.ActivePath, ".mcp.json"), "raw source; never import root") + projectGeminiAssert(t, filepath.Join(f.source, "hooks", "hooks.json"), "raw source; never import root") + runner := &qoderForbiddenRunner{} + actor := providers.Activator{Registry: f.registry, Runner: runner} + out, err := actor.Activate(t.Context(), f.request) + qoderPrepared(t, out, err) + actions := strings.Join(out.UserActions, " ") + for _, want := range []string{view, "--scope", "shared native registry/cache", "same-name/same-version", "independent", "does not unregister"} { + if !strings.Contains(actions, want) { + t.Fatalf("missing native limit %q: %s", want, actions) + } + } + var command struct { + WorkingDirectory string `json:"working_directory"` + Argv []string `json:"argv"` + } + encoded := strings.TrimPrefix(out.UserActions[1], "Optional separate native import (UAP does not execute this command): ") + if err := json.Unmarshal([]byte(encoded), &command); err != nil || command.WorkingDirectory != f.project || !reflect.DeepEqual(command.Argv, []string{"qoder", "plugins", "install", view, "--scope", "project", "--json"}) { + t.Fatalf("manual argv changed: %+v %v", command, err) + } + binding := &domain.ClientBinding{ClientID: "qoder", Scope: "project", PhysicalArtifact: plan.PhysicalArtifactID, TargetLocator: plan.ActivePath, NativeProfileRoot: plan.NativeRegistryRoot, NativeObjects: f.request.Delivery.NativeObjects} + for _, enabled := range []bool{false, true} { + var nativeRunner *qoderForbiddenRunner + if enabled { + nativeRunner = runner + } + observer := providers.NativeIdentityObserver{Registry: f.registry, Stager: f.stager} + if nativeRunner != nil { + observer.Runner = nativeRunner + } + observed, err := observer.ObserveNativeIdentity(t.Context(), f.request.Client, plan, binding) + if err != nil || observed.State != domain.NativeIdentityManaged || !observed.ReceiptReconciled || observed.NativeDiscoveryAttempted || observed.NativeDiscoveryReconciled || observed.NativeDiscoveryState != domain.NativeIdentityAbsent { + t.Fatalf("enabled%t package became native discovery: %+v %v", enabled, observed, err) + } + } + if qoder.New().AutomaticallyActivates(clients.Env{Runner: runner}, f.request) || !qoder.New().VerifierAvailable(f.request.Client, plan, "") || runner.calls != 0 { + t.Fatal("project used native availability helper") + } +} +func TestProjectQoderRejectsMalformedAuthorityBeforeNativeEffects(t *testing.T) { + for _, bad := range []string{"attestation", "both-user", "client", "profile", "root", "native-executable", "active", "delivery-client", "empty-delivery", "missing-receipt", "foreign-receipt", "physical", "no-skills"} { + t.Run(bad, func(t *testing.T) { + f := newQoderProjectFixture(t, true) + before := projectGeminiInventory(t, f.base) + r := f.request + switch bad { + case "attestation": + r.ActivationComplete = true + case "both-user": + r.Plan.Scope, r.Plan.TargetContext.Scope = domain.ScopeUser, domain.ScopeUser + case "client": + r.Client.ClientID = domain.ClientCodex + case "profile": + r.Plan.TargetContext.UserConfigRoot = f.other + case "root": + r.Plan.NativeRegistryRoot = f.user + case "native-executable": + r.Plan.NativeRegistryExecutable = r.Client.ExecutablePath + case "active": + r.Delivery.ActivePath = f.other + case "delivery-client": + r.Delivery.ClientID = domain.ClientClaude + case "empty-delivery": + r.Delivery = domain.StagedDelivery{} + case "missing-receipt": + r.Delivery.NativeObjects = nil + case "foreign-receipt": + r.Delivery.NativeObjects = append([]domain.NativeObjectOwnership(nil), r.Delivery.NativeObjects...) + r.Delivery.NativeObjects[0].Path = f.user + case "physical": + r.Plan.PhysicalArtifactID = "other" + case "no-skills": + r.Plan.Components = nil + } + runner := &qoderForbiddenRunner{} + if _, err := qoder.New().Activate(t.Context(), clients.Env{Runner: runner}, r); err == nil { + t.Fatal("malformed direct adapter request admitted") + } + if runner.calls != 0 || !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("refused request caused an effect") + } + }) + } + f := newQoderProjectFixture(t, false) + if err := qoder.New().PreflightActivation(clients.Env{}, f.request); err != nil { + t.Fatal("valid early unstaged request refused", err) + } + f.request.ActivationComplete = true + if err := qoder.New().PreflightActivation(clients.Env{}, f.request); err == nil { + t.Fatal("early attestation admitted") + } +} +func TestProjectQoderProjectionCollisionAndTamperRefuse(t *testing.T) { + f := newQoderProjectFixture(t, false) + projectGeminiWrite(t, filepath.Join(f.source, qoderView, "keep"), "foreign reserved surface") + before := projectGeminiInventory(t, f.user) + if _, err := f.stager.Stage(t.Context(), f.envelope, f.request.Plan, "reserved", domain.CompatibilityHints{}); err == nil { + t.Fatal("reserved source view adopted") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.user)) { + t.Fatal("reserved refusal touched user") + } + for _, tamper := range []string{"skill", "extra-root", "mcp-manifest", "receipt-digest"} { + t.Run(tamper, func(t *testing.T) { + f := newQoderProjectFixture(t, true) + switch tamper { + case "skill": + projectGeminiWrite(t, filepath.Join(f.request.Plan.ActivePath, qoderView, "skills", "docs", "SKILL.md"), "foreign") + case "extra-root": + projectGeminiWrite(t, filepath.Join(f.request.Plan.ActivePath, qoderView, ".mcp.json"), "{}") + case "mcp-manifest": + projectGeminiWrite(t, filepath.Join(f.request.Plan.ActivePath, qoderView, ".qoder-plugin", "plugin.json"), `{"name":"demo","version":"1.0.0","skills":"./skills/","mcpServers":"../.mcp.json"}`) + case "receipt-digest": + f.request.Delivery.ArtifactDigest = "sha256:foreign" + } + // Independently exercise the closed-surface verifier even if a direct + // caller supplies a digest for the modified package. SDK ownership + // admission separately refuses any rewrite of its recorded digest. + if tamper == "extra-root" || tamper == "mcp-manifest" { + digest, err := (packagesnapshot.Builder{}).Digest(t.Context(), f.request.Plan.ActivePath) + if err != nil { + t.Fatal(err) + } + f.request.Delivery.ArtifactDigest = digest + f.request.Delivery.NativeObjects[0].ManagedDigest = digest + } + before := projectGeminiInventory(t, f.base) + if _, err := qoder.New().Activate(t.Context(), clients.Env{}, f.request); err == nil { + t.Fatal("tampered bundle verified") + } + if !reflect.DeepEqual(before, projectGeminiInventory(t, f.base)) { + t.Fatal("tampered bundle changed") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/providers/stager.go b/install/integrationctl/agentplugins/providers/stager.go index d6e32755d..dc4368041 100644 --- a/install/integrationctl/agentplugins/providers/stager.go +++ b/install/integrationctl/agentplugins/providers/stager.go @@ -299,7 +299,7 @@ func (stager Stager) project( ) ([]domain.NativeObjectOwnership, error) { projector, ok := clients.As[clients.Projector](stager.Registry, plan.ClientID) if !ok { - if domain.RequiresNativeProjector(plan.ClientID) { + if domain.RequiresNativeProjectorForScope(plan.ClientID, plan.Scope) { return nil, fmt.Errorf("client %q requires a native projector", plan.ClientID) } return nil, nil diff --git a/install/integrationctl/agentplugins/providers/stager_sanitize.go b/install/integrationctl/agentplugins/providers/stager_sanitize.go index d64ff2d54..c7383e85a 100644 --- a/install/integrationctl/agentplugins/providers/stager_sanitize.go +++ b/install/integrationctl/agentplugins/providers/stager_sanitize.go @@ -32,7 +32,7 @@ func removeUnsupportedPortableHooks(paths ports.PathPolicy, root string, plan do // A client that convention-discovers package-root hooks/ and commands/ // receives them as target-native surfaces; the portable-hooks strip below // is only for clients with no consumer for that directory. - if domain.ClientTraitsFor(plan.ClientID).DiscoversNativeHooks { + if plan.SelectedDelivery.EffectiveTraitsForScope(plan.ClientID, plan.Scope).DiscoversNativeHooks { return nil } entries, err := os.ReadDir(root) diff --git a/install/integrationctl/agentplugins/usecase/apply.go b/install/integrationctl/agentplugins/usecase/apply.go index 4f75b6ac4..cfa943fe4 100644 --- a/install/integrationctl/agentplugins/usecase/apply.go +++ b/install/integrationctl/agentplugins/usecase/apply.go @@ -196,7 +196,7 @@ func (session *applySession) planAndPreflight() error { } session.plan = plan session.result.Plan = plan - return rejectNativeNameCollision(session.state, session.installationID, session.input.Envelope.Manifest.Name, session.input.Client.ClientID) + return rejectNativeNameCollision(session.state, session.installationID, session.input.Envelope.Manifest.Name, session.input.Client.ClientID, session.input.Scope) } func (session *applySession) resolveBinding() error { @@ -206,7 +206,7 @@ func (session *applySession) resolveBinding() error { session.registrationMigration = session.existing && session.input.Envelope.LocalChatGPTMapping != nil && session.state.Installations[session.installationIndex].LocalChatGPTMapping != nil && session.state.Installations[session.installationIndex].LocalChatGPTMapping.IsLegacyContext7Registration() && *session.state.Installations[session.installationIndex].LocalChatGPTMapping != *session.input.Envelope.LocalChatGPTMapping if session.isMaterialized { binding := session.state.Installations[session.installationIndex].Clients[session.clientBindingID] - if err := validateNativeBinding(binding, session.input.Client); err != nil { + if err := session.service.validateNativeBinding(session.ctx, binding, session.input.Client); err != nil { return err } session.managedBinding = &binding @@ -226,11 +226,11 @@ func (session *applySession) resolveBinding() error { } func (session *applySession) adoptSharedBinding() { - if !session.existing || !sharesPhysicalBackend(session.input.Client.ClientID, session.plan.SelectedDelivery) { + if !session.existing || !sharesPhysicalBackend(session.input.Client.ClientID, session.plan.SelectedDelivery, session.input.Scope) { return } for key, binding := range session.state.Installations[session.installationIndex].Clients { - if binding.Scope != string(session.input.Scope) || binding.Materialization == domain.MaterializationAbsent || binding.PhysicalArtifact != session.plan.PhysicalArtifactID || !sameNativeBackend(domain.ClientID(binding.ClientID), session.input.Client.ClientID) { + if binding.Scope != string(session.input.Scope) || binding.Materialization == domain.MaterializationAbsent || binding.PhysicalArtifact != session.plan.PhysicalArtifactID || !sameNativeBackend(domain.ClientID(binding.ClientID), session.input.Client.ClientID, session.input.Scope) { continue } session.clientBindingID = key @@ -327,7 +327,7 @@ func (session *applySession) finishExistingLifecycle(current domain.ClientBindin return true, session.result, err } } - if nativeLifecycleClient(session.input.Client.ClientID, session.plan.SelectedDelivery) { + if nativeLifecycleClient(session.input.Client.ClientID, session.plan.SelectedDelivery, session.input.Scope) { _, complete, _, err := session.service.activeNativeDelivery(session.ctx, session.input, session.plan, session.state.Installations[session.installationIndex], current) if err != nil { return true, session.result, err @@ -350,6 +350,9 @@ func (session *applySession) persistReadOnlyObservation(current domain.ClientBin } verified, verifyErr := session.service.verifyClientReadOnly(session.ctx, session.input, session.result, current) if verifyErr != nil { + if retainPreparedProjectVerificationFailure(&session.result, verified, verifyErr) { + return true, session.result, verifyErr + } if verified.Activation != "" && !session.input.DryRun && (session.input.Confirmed || session.input.PersistAuthoritativeObservations && verified.AuthoritativeObservation) { session.result.Activation = verified changed, updateErr := session.service.persistAuthoritativeObservation(session.ctx, session.input, session.installationID, session.clientBindingID, current, verified) diff --git a/install/integrationctl/agentplugins/usecase/client_traits.go b/install/integrationctl/agentplugins/usecase/client_traits.go index eae95ad43..c342de1de 100644 --- a/install/integrationctl/agentplugins/usecase/client_traits.go +++ b/install/integrationctl/agentplugins/usecase/client_traits.go @@ -2,13 +2,20 @@ package usecase import "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" -func nativeLifecycleClient(clientID domain.ClientID, selected domain.SelectedDelivery) bool { - traits := selected.EffectiveTraits(clientID) +func nativeLifecycleClient(clientID domain.ClientID, selected domain.SelectedDelivery, scopes ...domain.InstallScope) bool { + traits := selected.EffectiveTraitsForScope(clientID, selectedScope(scopes)) return traits.TracksNativeEffects || traits.LifecycleKind == domain.LifecycleNativeConfig } -func sharesPhysicalBackend(id domain.ClientID, selected domain.SelectedDelivery) bool { - return selected.SharesBackend(id) +func sharesPhysicalBackend(id domain.ClientID, selected domain.SelectedDelivery, scopes ...domain.InstallScope) bool { + return selected.SharesBackendForScope(id, selectedScope(scopes)) +} + +func selectedScope(scopes []domain.InstallScope) domain.InstallScope { + if len(scopes) == 0 || scopes[0] == "" { + return domain.ScopeUser + } + return scopes[0] } func clientDisplayName(id domain.ClientID) string { diff --git a/install/integrationctl/agentplugins/usecase/component_readiness.go b/install/integrationctl/agentplugins/usecase/component_readiness.go index 1cf7d84c1..8e1871d7d 100644 --- a/install/integrationctl/agentplugins/usecase/component_readiness.go +++ b/install/integrationctl/agentplugins/usecase/component_readiness.go @@ -64,7 +64,7 @@ type stdioHelperProbe struct { func (service Service) preflightOneStdio(envelope domain.PackageEnvelope, plan *domain.DeliveryPlan, server domain.MCPServer, probe *stdioHelperProbe) (*domain.ComponentReadinessError, error) { var failure *domain.ComponentReadinessError - if plan.SelectedDelivery.EffectiveTraits(plan.ClientID).UsesManagedStdioLauncher { + if plan.SelectedDelivery.EffectiveTraitsForScope(plan.ClientID, plan.Scope).UsesManagedStdioLauncher { var err error failure, err = service.checkedManagedStdioHelper(envelope.SnapshotRoot, probe) if err != nil { diff --git a/install/integrationctl/agentplugins/usecase/delivery.go b/install/integrationctl/agentplugins/usecase/delivery.go index 256ab6598..3062cf9fc 100644 --- a/install/integrationctl/agentplugins/usecase/delivery.go +++ b/install/integrationctl/agentplugins/usecase/delivery.go @@ -36,13 +36,13 @@ func openAIOAuthApplies(clientID domain.ClientID, envelope domain.PackageEnvelop // plan so activation and user-facing results still describe what the caller // selected. func bindStagedDeliveryToPhysicalOwner(delivery domain.StagedDelivery, plan domain.DeliveryPlan, managed *domain.ClientBinding) (domain.StagedDelivery, error) { - if !sharesPhysicalBackend(plan.ClientID, plan.SelectedDelivery) { + if !sharesPhysicalBackend(plan.ClientID, plan.SelectedDelivery, plan.Scope) { return delivery, nil } owner := plan.ClientID if managed != nil { owner = domain.ClientID(managed.ClientID) - if !sameNativeBackend(owner, plan.ClientID) || managed.PhysicalArtifact != plan.PhysicalArtifactID { + if !sameNativeBackend(owner, plan.ClientID, plan.Scope) || managed.PhysicalArtifact != plan.PhysicalArtifactID { return delivery, fmt.Errorf("shared physical binding identity does not match the staged delivery") } } @@ -69,7 +69,7 @@ func bindStagedDeliveryToPhysicalOwner(delivery domain.StagedDelivery, plan doma func initialLifecycle(plan domain.DeliveryPlan) (domain.ActivationState, domain.VerificationState) { // The plan describes the intended external result. A package transaction // cannot establish CLI registration before the activator verifies it. - if plan.SelectedDelivery.EffectiveTraits(plan.ClientID).LifecycleKind == domain.LifecycleCLIRegistry && plan.Activation == domain.ActivationActive { + if plan.SelectedDelivery.EffectiveTraitsForScope(plan.ClientID, plan.Scope).LifecycleKind == domain.LifecycleCLIRegistry && plan.Activation == domain.ActivationActive { return domain.ActivationPrepared, domain.VerificationPackageValid } return plan.Activation, plan.Verification @@ -126,6 +126,6 @@ func (service Service) preflightActivation(input AddInput, plan domain.DeliveryP } return preflighter.PreflightActivation(domain.ActivationRequest{ Client: input.Client, Plan: cloneLocalObservationPlan(plan), BackendExecutable: input.BackendExecutable, - VerifyOnly: input.DryRun, + VerifyOnly: input.DryRun, ActivationComplete: input.ActivationComplete, }) } diff --git a/install/integrationctl/agentplugins/usecase/group.go b/install/integrationctl/agentplugins/usecase/group.go index be238a180..b0d1e8d36 100644 --- a/install/integrationctl/agentplugins/usecase/group.go +++ b/install/integrationctl/agentplugins/usecase/group.go @@ -119,7 +119,7 @@ func (service Service) repairGroup(ctx context.Context, input GroupInput) (Group installation := state.Installations[index] matched := false for _, client := range installation.Clients { - if !sameNativeBackend(domain.ClientID(client.ClientID), target.Client.ClientID) || client.Scope != string(target.Scope) { + if !sameNativeBackend(domain.ClientID(client.ClientID), target.Client.ClientID, target.Scope) || client.Scope != string(target.Scope) { continue } if !repairPackageRevisionMatches(client.PackageRevision, target.Envelope, installation.OriginMode == domain.OriginModeDirectory) { @@ -221,6 +221,13 @@ func (service Service) applyGroup(ctx context.Context, input GroupInput, replace if err := session.revalidateHosts(); err != nil { return session.result, err } + for _, target := range session.planned { + if err := session.reportGroupProgress(target, GroupProgressConfiguring); err != nil { + return session.result, err + } + } + // Callers can change any target from a progress callback. Reobserve every + // identity after the final callback, before building or committing state. if err := session.reobserveGroupIdentity(); err != nil { return session.result, err } @@ -232,11 +239,6 @@ func (service Service) applyGroup(ctx context.Context, input GroupInput, replace target.input.Envelope.TreeDigest, target.plan.ProfileAuthority(), "committed") } } - for _, target := range session.planned { - if err := session.reportGroupProgress(target, GroupProgressConfiguring); err != nil { - return session.result, err - } - } if err := session.applyGroupKernel(); err != nil { return session.result, err } @@ -457,7 +459,7 @@ func validateGroupNativeIdentityObservation(observation domain.NativeIdentityObs // that failure mode, so they keep going through the ordinary, immediate // CLI-inclusive check. func (service Service) observeGroupRecoveryEligibility(ctx context.Context, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) bool { - if !plan.SelectedDelivery.EffectiveTraits(client.ClientID).SupportsPreparedRecovery || managed == nil || managedDigest(*managed) == "" || service.NativeObserver == nil { + if !plan.SelectedDelivery.EffectiveTraitsForScope(client.ClientID, plan.Scope).SupportsPreparedRecovery || managed == nil || managedDigest(*managed) == "" || service.NativeObserver == nil { return false } observation, err := service.preparedIdentityObservation(ctx, client, plan, managed) diff --git a/install/integrationctl/agentplugins/usecase/group_activate.go b/install/integrationctl/agentplugins/usecase/group_activate.go index f7e645db8..dab1fd73e 100644 --- a/install/integrationctl/agentplugins/usecase/group_activate.go +++ b/install/integrationctl/agentplugins/usecase/group_activate.go @@ -37,7 +37,7 @@ func (session *groupSession) activateGroupTargets() (GroupResult, error) { func (session *groupSession) activateOneGroupTarget(plannedIndex int, target plannedGroupTarget) error { previousObservation := target.plan.LocalEntryObservation.Clone() - outcome, nativeComplete, activationErr := session.activateGroupDelivery(target) + outcome, delivery, nativeComplete, activationErr := session.activateGroupDelivery(target) previousNativeObjects := []domain.NativeObjectOwnership(nil) if target.managed != nil { previousNativeObjects = target.managed.NativeObjects @@ -46,10 +46,13 @@ func (session *groupSession) activateOneGroupTarget(plannedIndex int, target pla if lifecycleChanged { session.result.Mutated = true } - if !nativeComplete && activationErr == nil { + preparedUnchanged := target.noChange && target.managed != nil && activationErr == nil && + session.service.verifiedPreparedObservationUnchanged(target.input, target.plan, *target.managed, delivery, outcome, nativeComplete) + if !nativeComplete && !preparedUnchanged && activationErr == nil { // An incomplete projection that re-activated successfully changed the - // target. A failure before or during activation claims no mutation; the - // recorded lifecycle change, if any, is the evidence. + // target unless fresh evidence proves the exact existing Prepared delivery. + // This does not make registration complete. A failure before or during + // activation claims no mutation; any recorded lifecycle change is the evidence. target.noChange = false } session.assignActivationOutcome(target, outcome, activationErr, persistErr, lifecycleChanged) @@ -83,7 +86,7 @@ func (session *groupSession) activateOneGroupTarget(plannedIndex int, target pla return nil } -func (session *groupSession) activateGroupDelivery(target plannedGroupTarget) (domain.ActivationOutcome, bool, error) { +func (session *groupSession) activateGroupDelivery(target plannedGroupTarget) (domain.ActivationOutcome, domain.StagedDelivery, bool, error) { delivery := target.delivery nativeComplete := true if target.noChange && target.managed != nil { @@ -96,7 +99,7 @@ func (session *groupSession) activateGroupDelivery(target plannedGroupTarget) (d // rather than a state refusal. outcome := lifecycleOutcome(*target.managed) outcome.NativeEffect = domain.NativeEffectUnchanged - return outcome, false, err + return outcome, delivery, false, err } } previous := []domain.NativeObjectOwnership(nil) @@ -115,7 +118,7 @@ func (session *groupSession) activateGroupDelivery(target plannedGroupTarget) (d } activationErr = session.normalizeActivationError(target, &outcome, activationErr) session.preserveNoChangeActivation(target, &outcome, activationErr) - return outcome, nativeComplete, activationErr + return outcome, delivery, nativeComplete, activationErr } func (session *groupSession) normalizeActivationError(target plannedGroupTarget, outcome *domain.ActivationOutcome, activationErr error) error { @@ -159,12 +162,14 @@ func (session *groupSession) preserveNoChangeActivation(target plannedGroupTarge } func (session *groupSession) assignActivationOutcome(target plannedGroupTarget, outcome domain.ActivationOutcome, activationErr, persistErr error, lifecycleChanged bool) { + mutated := !target.noChange || lifecycleChanged || outcome.NativeEffect == domain.NativeEffectCommitted for _, resultIndex := range target.resultIndexes { session.result.Targets[resultIndex].Activation = outcome - session.result.Targets[resultIndex].Mutated = !target.noChange || lifecycleChanged - if lifecycleChanged { + session.result.Targets[resultIndex].Mutated = mutated + if mutated { session.result.Targets[resultIndex].NoChange = false } + session.result.Mutated = session.result.Mutated || session.result.Targets[resultIndex].Mutated switch { case persistErr != nil: session.result.Targets[resultIndex].GroupPhase = GroupTargetExternalFailed diff --git a/install/integrationctl/agentplugins/usecase/group_commit.go b/install/integrationctl/agentplugins/usecase/group_commit.go index 4ef0433f2..5f85c6e8b 100644 --- a/install/integrationctl/agentplugins/usecase/group_commit.go +++ b/install/integrationctl/agentplugins/usecase/group_commit.go @@ -32,9 +32,9 @@ func (session *groupSession) upsertGroupTargetState(target plannedGroupTarget) { client.AffectedSurfaces = append(client.AffectedSurfaces, target.managed.AffectedSurfaces...) client.AffectedSurfaces = append(client.AffectedSurfaces, target.managed.ClientID) } - if sharesPhysicalBackend(target.input.Client.ClientID, target.plan.SelectedDelivery) { + if sharesPhysicalBackend(target.input.Client.ClientID, target.plan.SelectedDelivery, target.input.Scope) { client.AffectedSurfaces = append(client.AffectedSurfaces, string(target.input.Client.ClientID)) - for _, sibling := range domain.BackendSiblings(target.input.Client.ClientID) { + for _, sibling := range domain.BackendSiblingsForScope(target.input.Client.ClientID, target.input.Scope) { client.AffectedSurfaces = append(client.AffectedSurfaces, string(sibling)) } } @@ -99,7 +99,7 @@ func (session *groupSession) restoreGroupOrigin() { } func (session *groupSession) applyGroupKernel() error { - // Progress callbacks run after precommit observation and may change a host. + // Keep the prepared host authority pinned through the final kernel boundary. for _, target := range session.planned { if session.service.ClientPreparation != nil { if err := session.service.ClientPreparation.RevalidateClient(session.ctx, target.input.Client, target.plan); err != nil { diff --git a/install/integrationctl/agentplugins/usecase/group_plan.go b/install/integrationctl/agentplugins/usecase/group_plan.go index bb95560f5..a29afce9c 100644 --- a/install/integrationctl/agentplugins/usecase/group_plan.go +++ b/install/integrationctl/agentplugins/usecase/group_plan.go @@ -33,7 +33,7 @@ func (session *groupSession) planOneGroupTarget(targetIndex int, target AddInput return err } if managed != nil { - if err := validateNativeBinding(*managed, target.Client); err != nil { + if err := session.service.validateNativeBinding(session.ctx, *managed, target.Client); err != nil { return err } } @@ -58,7 +58,7 @@ func (session *groupSession) validateGroupTarget(targetIndex int, target *AddInp if !session.input.Repair && (target.Envelope.TreeDigest != session.first.Envelope.TreeDigest || target.Envelope.ManifestDigest != session.first.Envelope.ManifestDigest || domain.ComputeSourceBindingID(target.Envelope.Source) != session.sourceID) { return fmt.Errorf("all targets in one operation group must use one immutable package") } - if target.Scope != domain.ScopeUser { + if target.Scope != session.service.scope() { return fmt.Errorf("%s scope is not proven; group mutation supports user scope only", target.Scope) } if target.ReleaseRevoked && normalizedOriginMode(target.OriginMode) == domain.OriginModeDirectory { @@ -102,12 +102,12 @@ func (session *groupSession) planAndPreflightGroupTarget(targetIndex int, target func (session *groupSession) collideGroupTarget(targetIndex int, target AddInput, plan domain.DeliveryPlan) (bool, error) { key := plan.ActivePath - if sharesPhysicalBackend(target.Client.ClientID, plan.SelectedDelivery) { + if sharesPhysicalBackend(target.Client.ClientID, plan.SelectedDelivery, target.Scope) { definition, _ := domain.ClientDefinitionFor(target.Client.ClientID) - key = "shared-backend:" + definition.BackendFamily + ":" + plan.PhysicalArtifactID + key = "shared-backend:" + domain.BackendFamilyForScope(definition.ID, target.Scope) + ":" + plan.PhysicalArtifactID } else if session.existing { for _, binding := range session.state.Installations[session.installationIndex].Clients { - if binding.PhysicalArtifact == plan.PhysicalArtifactID && sameNativeBackend(domain.ClientID(binding.ClientID), target.Client.ClientID) && binding.Materialization != domain.MaterializationAbsent { + if binding.PhysicalArtifact == plan.PhysicalArtifactID && sameNativeBackend(domain.ClientID(binding.ClientID), target.Client.ClientID, target.Scope) && binding.Materialization != domain.MaterializationAbsent { key = binding.TargetLocator break } @@ -119,10 +119,10 @@ func (session *groupSession) collideGroupTarget(targetIndex int, target AddInput return false, nil } prior := &session.planned[priorIndex] - if !sameNativeBackend(prior.input.Client.ClientID, target.Client.ClientID) { + if !sameNativeBackend(prior.input.Client.ClientID, target.Client.ClientID, target.Scope) { return false, fmt.Errorf("targets collide on physical backend %s", key) } - if (prior.plan.SelectedDelivery.EffectiveTraits(prior.input.Client.ClientID).BindsNativeProfileRoot || plan.SelectedDelivery.EffectiveTraits(target.Client.ClientID).BindsNativeProfileRoot) && prior.input.Client.ConfigRoot != target.Client.ConfigRoot { + if (prior.plan.SelectedDelivery.EffectiveTraitsForScope(prior.input.Client.ClientID, prior.input.Scope).BindsNativeProfileRoot || plan.SelectedDelivery.EffectiveTraitsForScope(target.Client.ClientID, target.Scope).BindsNativeProfileRoot) && prior.input.Client.ConfigRoot != target.Client.ConfigRoot { return false, fmt.Errorf("targets select different native profile roots for physical backend %s", key) } // Coalescing discards one plan, so all frozen authority, including revision @@ -181,7 +181,7 @@ func (session *groupSession) resolveGroupManagedBinding(target AddInput, plan *d if !session.existing { return clientID, nil, nil } - if !sharesPhysicalBackend(target.Client.ClientID, plan.SelectedDelivery) { + if !sharesPhysicalBackend(target.Client.ClientID, plan.SelectedDelivery, target.Scope) { if binding, ok := session.state.Installations[session.installationIndex].Clients[clientID]; ok { owned := binding return clientID, &owned, nil @@ -282,7 +282,7 @@ func (session *groupSession) preflightOneCompatibleBinding(check AddInput, compa return err } for _, binding := range session.state.Installations[session.installationIndex].Clients { - if sameNativeBackend(domain.ClientID(binding.ClientID), check.Client.ClientID) && binding.Scope == string(check.Scope) { + if sameNativeBackend(domain.ClientID(binding.ClientID), check.Client.ClientID, check.Scope) && binding.Scope == string(check.Scope) { compatibleBindings[binding.ClientBindingID] = true } } diff --git a/install/integrationctl/agentplugins/usecase/group_prepared_observation_test.go b/install/integrationctl/agentplugins/usecase/group_prepared_observation_test.go new file mode 100644 index 000000000..46cad062a --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/group_prepared_observation_test.go @@ -0,0 +1,144 @@ +package usecase + +import ( + "context" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + clientplanner "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/planner" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/plannertest" +) + +type preparedGroupObservationActivator struct { + resumeObservationActivator + effectPath string +} + +func (a *preparedGroupObservationActivator) Activate(ctx context.Context, request domain.ActivationRequest) (domain.ActivationOutcome, error) { + outcome, err := a.resumeObservationActivator.Activate(ctx, request) + if err == nil && outcome.NativeEffect == domain.NativeEffectCommitted { + err = os.WriteFile(a.effectPath, []byte("real inert native fixture effect"), 0600) + } + return outcome, err +} + +func (*preparedGroupObservationActivator) AutomaticallyActivates(domain.ActivationRequest) bool { + return true +} + +func TestGroupedPreparedObservationReportsActualMutation(t *testing.T) { + for _, tc := range []struct { + name string + effect domain.NativeEffectState + activation domain.ActivationState + wantMutation bool + wantStateChange bool + policy domain.PolicyState + authComplete, activationAttested bool + }{ + {name: "fresh unchanged prepared"}, + {name: "same recorded facts with native effect", effect: domain.NativeEffectCommitted, wantMutation: true}, + {name: "complete projection with committed effect", activation: domain.ActivationActive, effect: domain.NativeEffectCommitted, wantMutation: true}, + {name: "new policy decision", policy: domain.PolicyBlocked, wantMutation: true, wantStateChange: true}, + {name: "explicit authentication completion", authComplete: true, wantMutation: true, wantStateChange: true}, + {name: "attested outcome is not read only", activationAttested: true, wantMutation: true}, + } { + t.Run(tc.name, func(t *testing.T) { + service, store, _ := serviceFixture(t) + project := t.TempDir() + service.ProjectRoot = project + store.Path, store.ProjectRoot = filepath.Join(project, ".uap", "state-v2.json"), project + service.StateStore, service.Kernel.StateStore = store, store + // This core fixture supplies the production path policy; the CLI + // regression separately exercises the SDK namespace guard. + service.CheckNamespace = func() error { return service.Paths.RequireContainedChild(project, store.Path) } + planner := plannertest.NewPlanner(clientplanner.Planner{ProjectRoot: project, ManagedRoot: filepath.Join(project, ".uap", "managed")}) + service.Planner, service.Targets = planner, planner + client := domain.DetectedClient{ClientID: domain.ClientClaude, Status: domain.DetectionDetected, ConfigRoot: filepath.Join(t.TempDir(), ".claude")} + client.ExecutablePath = filepath.Join(project, "inert-claude") + if err := os.WriteFile(client.ExecutablePath, []byte("never execute this fixture"), 0700); err != nil { + t.Fatal(err) + } + input := kiroPowerInput(t, client, "1.0.0", "sha256:prepared-tree", "sha256:prepared-manifest") + input.Scope = domain.ScopeProject + input.Confirmed = true + activator := &preparedGroupObservationActivator{resumeObservationActivator: resumeObservationActivator{ + observedActivator: observedActivator{outcome: domain.ActivationOutcome{Activation: domain.ActivationPrepared, Verification: domain.VerificationInstalled, Authentication: domain.AuthenticationNotChecked, Policy: domain.PolicyAllowed}}, + available: true, + }} + activator.effectPath = filepath.Join(project, "native-fixture-effect") + if tc.activation != "" { + activator.outcome.Activation = tc.activation + } + service.Activator = activator + added, err := service.AddGroup(context.Background(), GroupInput{Targets: []AddInput{input}, Confirmed: true, OperationGroupID: "prepared-group-add"}) + if err != nil { + t.Fatal(err) + } + before, err := store.Load() + if err != nil { + t.Fatal(err) + } + stateBytes, err := os.ReadFile(store.Path) + if err != nil { + t.Fatal(err) + } + binding := onlyBinding(before.Installations[0]) + treeBefore, err := os.ReadFile(filepath.Join(binding.TargetLocator, ".claude-plugin", "plugin.json")) + if err != nil { + t.Fatal(err) + } + count := len(activator.requests) + activator.outcome.NativeEffect = tc.effect + if tc.policy != "" { + activator.outcome.Policy = tc.policy + } + input.AuthComplete = tc.authComplete + activator.outcome.ActivationAttested = tc.activationAttested + updated, err := service.UpdateGroup(context.Background(), GroupInput{Targets: []AddInput{input}, CompatibilityChecks: []AddInput{input}, Confirmed: true, OperationGroupID: "prepared-group-update"}) + if err != nil { + t.Fatal(err) + } + if len(updated.Targets) != 1 || updated.Mutated != tc.wantMutation || updated.Targets[0].Mutated != tc.wantMutation || updated.Targets[0].NoChange == tc.wantMutation { + t.Fatalf("group/target mutation classification=%+v", updated) + } + if updated.Phase != GroupPhaseCompleted || len(updated.Receipts) != 0 { + t.Fatalf("unchanged managed group=%+v", updated) + } + if len(activator.requests) != count+1 { + t.Fatalf("fresh observation missing: requests=%d/%d", len(activator.requests), count) + } + request := activator.requests[len(activator.requests)-1] + if request.VerifyOnly != (tc.activation == domain.ActivationActive) || !request.PackageUnchanged { + t.Fatalf("incomplete registration facts changed=%+v", request) + } + if got := updated.Targets[0].Activation; got.Activation != binding.Activation || got.Verification != binding.Verification { + t.Fatalf("prepared observation promoted=%+v", got) + } + after, err := store.Load() + if err != nil || reflect.DeepEqual(before, after) == tc.wantStateChange { + t.Fatalf("same recorded facts rewritten: %+v %v", after, err) + } + afterBytes, err := os.ReadFile(store.Path) + if err != nil || (string(afterBytes) == string(stateBytes)) == tc.wantStateChange { + t.Fatalf("state bytes changed: %v", err) + } + treeAfter, err := os.ReadFile(filepath.Join(binding.TargetLocator, ".claude-plugin", "plugin.json")) + if err != nil || string(treeBefore) != string(treeAfter) { + t.Fatalf("managed bytes changed: %v", err) + } + if tc.effect == domain.NativeEffectCommitted { + body, err := os.ReadFile(activator.effectPath) + if err != nil || string(body) != "real inert native fixture effect" { + t.Fatalf("declared native effect was not performed: %v", err) + } + } + if added.InstallationID != updated.InstallationID { + t.Fatal("installation identity changed") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/usecase/group_progress_identity_test.go b/install/integrationctl/agentplugins/usecase/group_progress_identity_test.go new file mode 100644 index 000000000..fd32ecd5c --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/group_progress_identity_test.go @@ -0,0 +1,84 @@ +package usecase + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type progressIdentityObserver struct { + service Service + order *[]string +} + +func (observer progressIdentityObserver) ObserveNativeIdentity(ctx context.Context, client domain.DetectedClient, plan domain.DeliveryPlan, managed *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + *observer.order = append(*observer.order, "observe:"+string(client.ClientID)) + return observer.service.filesystemIdentityObservation(ctx, plan, managed) +} + +func TestGroupedConfiguringCallbacksPrecedeFinalIdentityObservation(t *testing.T) { + for _, inject := range []bool{false, true} { + name := "unchanged_callback" + if inject { + name = "last_callback_changes_first_target" + } + t.Run(name, func(t *testing.T) { + service, store, cursor := serviceFixture(t) + kiro := domain.DetectedClient{ClientID: domain.ClientKiro, Status: domain.DetectionDetected, ConfigRoot: filepath.Join(t.TempDir(), ".kiro")} + first := addInput(t, cursor, "https://example.com/progress-identity") + second := addInput(t, kiro, "https://example.com/progress-identity") + activator := &failNthGroupActivator{} + service.Activator = activator + order := []string{} + service.NativeObserver = progressIdentityObserver{service: service, order: &order} + paths := map[domain.ClientID]string{} + group := GroupInput{Targets: []AddInput{first, second}, OperationGroupID: "progress-identity", Confirmed: true} + group.Progress = func(event GroupProgressEvent) { + if event.Phase != GroupProgressConfiguring { + return + } + order = append(order, "callback:"+string(event.ClientID)) + paths[event.ClientID] = event.Result.Plan.ActivePath + if inject && event.ClientID == domain.ClientKiro { + if err := os.MkdirAll(paths[domain.ClientCursor], 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(paths[domain.ClientCursor], "FOREIGN"), []byte("preserve foreign identity"), 0600); err != nil { + t.Fatal(err) + } + } + } + result, err := service.AddGroup(t.Context(), group) + state, loadErr := store.Load() + if loadErr != nil { + t.Fatal(loadErr) + } + wantOrder := []string{"observe:cursor", "observe:kiro", "callback:cursor", "callback:kiro", "observe:cursor"} + if !inject { + wantOrder = append(wantOrder, "observe:kiro") + if err != nil || !result.Mutated || len(result.Receipts) != 2 || activator.calls != 2 || len(state.Installations) != 1 { + t.Fatalf("ordinary callbacks changed group success: result=%+v err=%v activations=%d state=%+v", result, err, activator.calls, state) + } + } else { + if err == nil || !strings.Contains(err.Error(), "native identity changed before group commit") || result.Mutated || len(result.Receipts) != 0 || activator.calls != 0 || len(state.Installations) != 0 { + t.Fatalf("callback ownership change escaped precommit refusal: result=%+v err=%v activations=%d state=%+v", result, err, activator.calls, state) + } + body, readErr := os.ReadFile(filepath.Join(paths[domain.ClientCursor], "FOREIGN")) + if readErr != nil || string(body) != "preserve foreign identity" { + t.Fatalf("foreign object changed: %q %v", body, readErr) + } + if _, statErr := os.Lstat(paths[domain.ClientKiro]); !os.IsNotExist(statErr) { + t.Fatalf("sibling target was published despite precommit refusal: %v", statErr) + } + } + if !reflect.DeepEqual(order, wantOrder) { + t.Fatalf("identity observation/callback order=%v want=%v", order, wantOrder) + } + }) + } +} diff --git a/install/integrationctl/agentplugins/usecase/identity.go b/install/integrationctl/agentplugins/usecase/identity.go index 1568018bd..e7057a016 100644 --- a/install/integrationctl/agentplugins/usecase/identity.go +++ b/install/integrationctl/agentplugins/usecase/identity.go @@ -25,7 +25,7 @@ func (service Service) verifyManagedTarget( return fmt.Errorf("managed package digest is missing; refusing %s and retaining state for reviewed recovery", operation) } targetClient := client - if sameNativeBackend(domain.ClientID(binding.ClientID), client.ClientID) { + if sameNativeBackend(domain.ClientID(binding.ClientID), client.ClientID, scope) { // Validate the persisted path against the physical binding's canonical // owner, even when the caller addressed the shared backend through its // other logical surface. diff --git a/install/integrationctl/agentplugins/usecase/installation.go b/install/integrationctl/agentplugins/usecase/installation.go index 5657f3f3b..08c844033 100644 --- a/install/integrationctl/agentplugins/usecase/installation.go +++ b/install/integrationctl/agentplugins/usecase/installation.go @@ -90,8 +90,11 @@ func stagedClientBinding(previous domain.ClientBinding, input AddInput, plan dom if token := plan.ProfileAuthority(); token != nil { profileRoot = token.Facts().CanonicalRoot } - if profileRoot == "" && plan.SelectedDelivery.EffectiveTraits(input.Client.ClientID).BindsNativeProfileRoot { + if profileRoot == "" && plan.SelectedDelivery.EffectiveTraitsForScope(input.Client.ClientID, input.Scope).BindsNativeProfileRoot { profileRoot = input.Client.ConfigRoot + if input.Scope == domain.ScopeProject { + profileRoot = plan.NativeRegistryRoot + } if facts, ok := plan.SelectedDelivery.LocalFacts(); ok { profileRoot = facts.ProfileRoot } @@ -112,18 +115,18 @@ func stagedClientBinding(previous domain.ClientBinding, input AddInput, plan dom NativeObjects: append([]domain.NativeObjectOwnership(nil), previous.NativeObjects...), NativeProfileRoot: profileRoot, NativeActivationAttempt: previous.NativeActivationAttempt, - AffectedSurfaces: preparedAffectedSurfaces(previous, input.Client.ClientID, plan.SelectedDelivery), + AffectedSurfaces: preparedAffectedSurfaces(previous, input.Client.ClientID, plan.SelectedDelivery, input.Scope), } } -func preparedAffectedSurfaces(previous domain.ClientBinding, requested domain.ClientID, selected domain.SelectedDelivery) []string { +func preparedAffectedSurfaces(previous domain.ClientBinding, requested domain.ClientID, selected domain.SelectedDelivery, scopes ...domain.InstallScope) []string { values := append([]string(nil), previous.AffectedSurfaces...) if previous.ClientID != "" { values = append(values, previous.ClientID) } values = append(values, string(requested)) - if selected.SharesBackend(requested) { - for _, sibling := range domain.BackendSiblings(requested) { + if selected.SharesBackendForScope(requested, selectedScope(scopes)) { + for _, sibling := range domain.BackendSiblingsForScope(requested, selectedScope(scopes)) { values = append(values, string(sibling)) } } @@ -180,14 +183,14 @@ func materializedClient(installation domain.Installation, clientBindingID string return ok && client.Materialization != domain.MaterializationAbsent } -func rejectNativeNameCollision(state domain.StateFileV2, installationID, declaredName string, clientID domain.ClientID) error { +func rejectNativeNameCollision(state domain.StateFileV2, installationID, declaredName string, clientID domain.ClientID, scopes ...domain.InstallScope) error { for _, installation := range state.Installations { if installation.InstallationID == installationID || installation.DeclaredName != declaredName { continue } for _, client := range installation.Clients { boundClientID := domain.ClientID(client.ClientID) - if sameNativeBackend(boundClientID, clientID) && client.Materialization != domain.MaterializationAbsent { + if sameNativeBackend(boundClientID, clientID, selectedScope(scopes)) && client.Materialization != domain.MaterializationAbsent { return fmt.Errorf("native client name collision for %q on %s; remove or rebind the existing source first", declaredName, clientID) } } @@ -195,8 +198,8 @@ func rejectNativeNameCollision(state domain.StateFileV2, installationID, declare return nil } -func sameNativeBackend(first, second domain.ClientID) bool { - return domain.SameClientBackend(first, second) +func sameNativeBackend(first, second domain.ClientID, scopes ...domain.InstallScope) bool { + return domain.SameClientBackendForScope(first, second, selectedScope(scopes)) } func (service Service) authorityPort() ports.PhysicalProfileAuthority { @@ -207,6 +210,13 @@ func (service Service) authorityPort() ports.PhysicalProfileAuthority { return port } func (service Service) checkProfiles(ctx context.Context) error { + if service.CheckNamespace != nil { + if err := service.CheckNamespace(); err != nil { + return err + } + } else if service.ProjectRoot != "" { + return fmt.Errorf("project namespace guard is required") + } if err := ctx.Err(); err != nil { return err } @@ -298,6 +308,9 @@ func (service Service) sharedProfiles(ctx context.Context, state domain.StateFil return nil, fmt.Errorf("shared data binding key differs") } c := domain.DetectedClient{ClientID: domain.ClientID(b.ClientID), ConfigRoot: b.NativeProfileRoot, ProfileAuthority: domain.CloneProfileAuthority(b.ProfileAuthority), ProfileNamespace: b.ProfileNamespace} + if service.ProjectRoot != "" { + c.ConfigRoot = service.Detected[c.ClientID].ConfigRoot + } if err := service.checkProfile(ctx, c); err != nil { return nil, err } @@ -306,14 +319,14 @@ func (service Service) sharedProfiles(ctx context.Context, state domain.StateFil } return shared, nil } -func recordedProfile(state domain.StateFileV2, installationID string, c domain.DetectedClient) (*domain.ClientBinding, error) { +func recordedProfile(state domain.StateFileV2, installationID string, c domain.DetectedClient, scopes ...domain.InstallScope) (*domain.ClientBinding, error) { var recorded *domain.ClientBinding for _, installation := range state.Installations { if installation.InstallationID != installationID { continue } for key, b := range installation.Clients { - if b.ClientID != string(c.ClientID) || b.Scope != string(domain.ScopeUser) { + if b.ClientID != string(c.ClientID) || b.Scope != string(selectedScope(scopes)) { continue } if key != b.ClientBindingID || recorded != nil { @@ -326,7 +339,7 @@ func recordedProfile(state domain.StateFileV2, installationID string, c domain.D return recorded, nil } func (service Service) freezeProfile(ctx context.Context, state domain.StateFileV2, installationID string, c domain.DetectedClient, capture bool) (domain.DetectedClient, error) { - recorded, err := recordedProfile(state, installationID, c) + recorded, err := recordedProfile(state, installationID, c, service.scope()) if err != nil { return c, err } @@ -348,11 +361,21 @@ func (service Service) freezeProfile(ctx context.Context, state domain.StateFile } if c.ProfileAuthority != nil { root := c.ProfileAuthority.Facts().CanonicalRoot - spelling, err := filepath.EvalSymlinks(c.ConfigRoot) + expected := c.ConfigRoot + if service.ProjectRoot != "" { + target, err := service.Targets.ResolveTarget(ctx, c, domain.ScopeProject, "project-context") + if err != nil { + return c, err + } + expected = target.NativeRegistryRoot + } + spelling, err := filepath.EvalSymlinks(expected) if err != nil || spelling != root { return c, fmt.Errorf("selected profile differs from recorded canonical root") } - c.ConfigRoot = root + if service.ProjectRoot == "" { + c.ConfigRoot = root + } } if err := service.checkProfile(ctx, c); err != nil { return c, err diff --git a/install/integrationctl/agentplugins/usecase/intent.go b/install/integrationctl/agentplugins/usecase/intent.go index 999183094..33d5a7edc 100644 --- a/install/integrationctl/agentplugins/usecase/intent.go +++ b/install/integrationctl/agentplugins/usecase/intent.go @@ -43,6 +43,7 @@ func (service Service) planInstall(ctx context.Context, input *AddInput, physica input.Client.OpenCodeHost = host } plan, err := service.Planner.Plan(ctx, domain.PlanRequest{ + ProjectRoot: service.ProjectRoot, LocalEntryObservation: observation.Clone(), PreviousNativeObjects: append([]domain.NativeObjectOwnership(nil), objects...), Envelope: input.Envelope, @@ -98,7 +99,7 @@ func resolvePersistedIntent(input *AddInput, installation *domain.Installation) } func validatePersonalMapping(input *AddInput, installation *domain.Installation) error { - traits := domain.ClientTraitsFor(input.Client.ClientID) + traits := (domain.SelectedDelivery{}).EffectiveTraitsForScope(input.Client.ClientID, input.Scope) if traits.RequiresPersonalMappingForPrepare && input.InstallIntent == domain.InstallIntentPrepare && input.Envelope.LocalChatGPTMapping == nil { return fmt.Errorf("%s preparation requires a personal Context7 registration receipt", clientDisplayName(input.Client.ClientID)) } diff --git a/install/integrationctl/agentplugins/usecase/lifecycle.go b/install/integrationctl/agentplugins/usecase/lifecycle.go index fe9ff188f..aa679c255 100644 --- a/install/integrationctl/agentplugins/usecase/lifecycle.go +++ b/install/integrationctl/agentplugins/usecase/lifecycle.go @@ -77,7 +77,7 @@ func (service Service) resume( } // A settled manual registration is a stable no-change outcome: this request // cannot finish it, and a later one the adapter can act on will. - result.NoChange = complete && !changed && (service.verifiedRegistrationUnchanged(input, result.Plan, client, outcome) || settled) + result.NoChange = !changed && (complete && (service.verifiedRegistrationUnchanged(input, result.Plan, client, outcome) || settled) || service.verifiedPreparedObservationUnchanged(input, result.Plan, client, delivery, outcome, complete)) return result, nil } @@ -141,6 +141,48 @@ func (service Service) verifiedRegistrationUnchanged(input AddInput, plan domain !input.AuthComplete && !outcome.AuthenticationAttested && len(outcome.UserActions) == 0 } +// verifiedPreparedObservationUnchanged classifies a repeated project-only, +// read-only observation, not lifecycle convergence. The verified existing +// delivery is reused; every subsequent request still reobserves the client. +func (service Service) verifiedPreparedObservationUnchanged(input AddInput, plan domain.DeliveryPlan, client domain.ClientBinding, delivery domain.StagedDelivery, outcome domain.ActivationOutcome, nativeComplete bool) bool { + if plan.SelectedDelivery.EffectiveTraitsForScope(input.Client.ClientID, input.Scope).LifecycleKind == domain.LifecycleNativeConfig { + return nativeComplete && service.verifiedPreparedNativeConfigUnchanged(input, plan, client, delivery, outcome) + } + return input.Scope == domain.ScopeProject && !nativeLifecycleClient(input.Client.ClientID, plan.SelectedDelivery, input.Scope) && + service.clientVerifierAvailable(input, plan) && client.Materialization == domain.MaterializationMaterialized && + client.Activation == domain.ActivationPrepared && client.Verification == domain.VerificationInstalled && + outcome.Activation == client.Activation && outcome.Verification == client.Verification && + outcome.Authentication == client.Authentication && outcome.Policy == client.Policy && + !input.ActivationComplete && !input.AuthComplete && !outcome.ActivationAttested && !outcome.AuthenticationAttested && + (outcome.NativeEffect == "" || outcome.NativeEffect == domain.NativeEffectUnchanged) && len(outcome.NativeObjects) == 0 && + preparedDeliveryUnchanged(client, delivery) +} + +// A complete native projection can retain Prepared while exact file verification +// succeeds. This classifies only that operation; it does not converge lifecycle. +func (service Service) verifiedPreparedNativeConfigUnchanged(input AddInput, plan domain.DeliveryPlan, client domain.ClientBinding, delivery domain.StagedDelivery, outcome domain.ActivationOutcome) bool { + return input.Scope == domain.ScopeProject && plan.Scope == domain.ScopeProject && client.Scope == string(domain.ScopeProject) && + plan.ClientID == input.Client.ClientID && client.ClientID == string(input.Client.ClientID) && delivery.ClientID == input.Client.ClientID && + plan.ActivePath != "" && plan.ActivePath == client.TargetLocator && + plan.SelectedDelivery.IsZero() && client.SelectedDelivery.IsZero() && service.clientVerifierAvailable(input, plan) && + len(client.NativeObjects) > 0 && reflect.DeepEqual(outcome.NativeObjects, client.NativeObjects) && + preparedDeliveryUnchanged(client, delivery) && preparedNativeConfigLifecycleUnchanged(input, client, outcome) +} + +func preparedNativeConfigLifecycleUnchanged(input AddInput, client domain.ClientBinding, outcome domain.ActivationOutcome) bool { + return outcome.Policy == domain.PolicyAllowed && + client.Materialization == domain.MaterializationMaterialized && client.Activation == domain.ActivationPrepared && client.Verification == domain.VerificationInstalled && + outcome.Activation == client.Activation && outcome.Verification == client.Verification && outcome.Authentication == client.Authentication && outcome.Policy == client.Policy && + client.NativeActivationAttempt == "" && client.PendingNativeIntent == nil && !input.ActivationComplete && !input.AuthComplete && + !outcome.ActivationAttested && !outcome.AuthenticationAttested && !outcome.AuthoritativeObservation && outcome.LocalEntryObservation == nil && + outcome.NativeEffect == domain.NativeEffectUnchanged +} + +func preparedDeliveryUnchanged(client domain.ClientBinding, delivery domain.StagedDelivery) bool { + return delivery.ActivePath == client.TargetLocator && delivery.ArtifactDigest != "" && + delivery.ArtifactDigest == managedDigest(client) && reflect.DeepEqual(delivery.NativeObjects, client.NativeObjects) +} + // clientVerifierAvailable asks the activator whether an exact client-side // verifier can observe this plan. An activator without the capability reports // no verifier, so prior evidence is re-derived rather than retained. @@ -333,7 +375,11 @@ func lifecycleOutcome(client domain.ClientBinding) domain.ActivationOutcome { } func (service Service) verifyClientReadOnly(ctx context.Context, input AddInput, result AddResult, client domain.ClientBinding) (domain.ActivationOutcome, error) { - if result.Plan.SelectedDelivery.IsZero() && !domain.ShouldReadOnlyVerify(input.Client.ClientID, input.BackendExecutable, input.InstallIntent) { + if preparedProjectFileCandidate(input, result.Plan) { + delivery := domain.StagedDelivery{ClientID: input.Client.ClientID, OwnedBase: result.Plan.TargetRoot, ActivePath: client.TargetLocator, ArtifactDigest: managedDigest(client), NativeObjects: client.NativeObjects} + return service.observePreparedProjectFiles(ctx, input, result.Plan, result.InstallationID, client, delivery) + } + if result.Plan.SelectedDelivery.IsZero() && !domain.ShouldReadOnlyVerifyForScope(input.Client.ClientID, input.Scope, input.BackendExecutable, input.InstallIntent) { return domain.ActivationOutcome{}, nil } delivery := domain.StagedDelivery{ClientID: input.Client.ClientID, OwnedBase: result.Plan.TargetRoot, ActivePath: client.TargetLocator, ArtifactDigest: managedDigest(client), NativeObjects: client.NativeObjects} diff --git a/install/integrationctl/agentplugins/usecase/local_observation.go b/install/integrationctl/agentplugins/usecase/local_observation.go index ae8fef218..ecc189fc6 100644 --- a/install/integrationctl/agentplugins/usecase/local_observation.go +++ b/install/integrationctl/agentplugins/usecase/local_observation.go @@ -141,7 +141,7 @@ func frozenPlanningBinding(input AddInput, installation *domain.Installation) (* if found != nil { return nil, fmt.Errorf("conflicting exact or shared client/scope bindings before planning") } - if (!binding.SelectedDelivery.IsZero() || sharesPhysicalBackend(input.Client.ClientID, binding.SelectedDelivery)) && (binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil) { + if (!binding.SelectedDelivery.IsZero() || sharesPhysicalBackend(input.Client.ClientID, binding.SelectedDelivery, input.Scope)) && (binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil) { return nil, fmt.Errorf("pending Local attempt forbids replacement planning") } if err := binding.ValidateLocalEntryObservation(); err != nil { @@ -160,12 +160,12 @@ func matchesPlanningBinding(input AddInput, installation *domain.Installation, b if binding.Scope != string(input.Scope) { return false } - if !sharesPhysicalBackend(input.Client.ClientID, binding.SelectedDelivery) { + if !sharesPhysicalBackend(input.Client.ClientID, binding.SelectedDelivery, input.Scope) { return binding.ClientID == string(input.Client.ClientID) } return binding.Materialization != domain.MaterializationAbsent && binding.PhysicalArtifact == domain.ComputePhysicalArtifactID(input.Envelope.Manifest.Name, installation.InstallationID) && - sameNativeBackend(domain.ClientID(binding.ClientID), input.Client.ClientID) + sameNativeBackend(domain.ClientID(binding.ClientID), input.Client.ClientID, input.Scope) } func validatePlannedObservation(plan domain.DeliveryPlan, observation *domain.LocalEntryObservation, objects []domain.NativeObjectOwnership) error { diff --git a/install/integrationctl/agentplugins/usecase/managed_selection.go b/install/integrationctl/agentplugins/usecase/managed_selection.go index dbc526267..ed7fc9014 100644 --- a/install/integrationctl/agentplugins/usecase/managed_selection.go +++ b/install/integrationctl/agentplugins/usecase/managed_selection.go @@ -57,7 +57,7 @@ func priorManagedBinding(installation *domain.Installation, plan *domain.Deliver return nil } for _, binding := range installation.Clients { - if binding.Materialization != domain.MaterializationAbsent && binding.Scope == string(plan.Scope) && binding.PhysicalArtifact == plan.PhysicalArtifactID && sameNativeBackend(domain.ClientID(binding.ClientID), plan.ClientID) { + if binding.Materialization != domain.MaterializationAbsent && binding.Scope == string(plan.Scope) && binding.PhysicalArtifact == plan.PhysicalArtifactID && sameNativeBackend(domain.ClientID(binding.ClientID), plan.ClientID, plan.Scope) { cloned := binding return &cloned } diff --git a/install/integrationctl/agentplugins/usecase/native_attempt.go b/install/integrationctl/agentplugins/usecase/native_attempt.go index e9a9ff3b7..5081edd80 100644 --- a/install/integrationctl/agentplugins/usecase/native_attempt.go +++ b/install/integrationctl/agentplugins/usecase/native_attempt.go @@ -105,7 +105,7 @@ func (service Service) activateWithNativeAttempt(ctx context.Context, installati return service.refusedPreparedActivation(installationID, bindingID, err) } } - if nativeLifecycleClient(request.Client.ClientID, request.Plan.SelectedDelivery) && !request.VerifyOnly { + if nativeLifecycleClient(request.Client.ClientID, request.Plan.SelectedDelivery, request.Plan.Scope) && !request.VerifyOnly { if err := service.beginNativeAttemptWithObservation(installationID, bindingID, domain.NativeIntentRegister, request.Plan.SelectedDelivery, request.Plan.LocalEntryObservation.Clone()); err != nil { return domain.ActivationOutcome{}, err } @@ -117,6 +117,9 @@ func (service Service) activateWithNativeAttempt(ctx context.Context, installati return domain.ActivationOutcome{}, fmt.Errorf("native attempt disappeared") } request.NativeAttempt = domain.NativeAttemptIdentity{OperationID: client.NativeActivationAttempt, InstallationID: installationID, BindingID: bindingID, NativeRoot: request.Client.ConfigRoot} + if request.Plan.Scope == domain.ScopeProject { + request.NativeAttempt.NativeRoot = request.Plan.NativeRegistryRoot + } } if request.Plan.SelectedDelivery.IsZero() { return service.activatePreparedClient(ctx, installationID, bindingID, request) diff --git a/install/integrationctl/agentplugins/usecase/native_binding.go b/install/integrationctl/agentplugins/usecase/native_binding.go index 6664184a7..d5e5c84d6 100644 --- a/install/integrationctl/agentplugins/usecase/native_binding.go +++ b/install/integrationctl/agentplugins/usecase/native_binding.go @@ -11,6 +11,10 @@ import ( ) func validateNativeBinding(client domain.ClientBinding, detected domain.DetectedClient) error { + return validateNativeBindingAt(client, detected, detected.ConfigRoot) +} + +func validateNativeBindingAt(client domain.ClientBinding, detected domain.DetectedClient, nativeRoot string) error { if err := client.ValidateLocalEntryObservation(); err != nil { return err } @@ -20,13 +24,13 @@ func validateNativeBinding(client domain.ClientBinding, detected domain.Detected if err := client.SelectedDelivery.Validate(); err != nil { return err } - if !client.SelectedDelivery.EffectiveTraits(detected.ClientID).BindsNativeProfileRoot { + if !client.SelectedDelivery.EffectiveTraitsForScope(detected.ClientID, domain.InstallScope(client.Scope)).BindsNativeProfileRoot { return nil } if client.NativeProfileRoot == "" { return fmt.Errorf("legacy native binding has no proven native profile root; reviewed rebind is required") } - if detected.ConfigRoot == "" || !filepath.IsAbs(detected.ConfigRoot) || filepath.Clean(detected.ConfigRoot) != client.NativeProfileRoot { + if nativeRoot == "" || !filepath.IsAbs(nativeRoot) || filepath.Clean(nativeRoot) != client.NativeProfileRoot { return fmt.Errorf("selected native profile differs from the binding's native profile root") } return nil diff --git a/install/integrationctl/agentplugins/usecase/native_projection.go b/install/integrationctl/agentplugins/usecase/native_projection.go index 1bac22102..287548a53 100644 --- a/install/integrationctl/agentplugins/usecase/native_projection.go +++ b/install/integrationctl/agentplugins/usecase/native_projection.go @@ -20,7 +20,7 @@ func (service Service) activeNativeDelivery(ctx context.Context, input AddInput, if delivery.ArtifactDigest == "" { return delivery, false, false, fmt.Errorf("managed package receipt is missing") } - if !nativeLifecycleClient(input.Client.ClientID, plan.SelectedDelivery) { + if !nativeLifecycleClient(input.Client.ClientID, plan.SelectedDelivery, input.Scope) { delivery.NativeObjects = append([]domain.NativeObjectOwnership(nil), client.NativeObjects...) complete = service.nonNativeRegistrationComplete(input, plan, client, delivery) settled = complete && service.manualRegistrationSettled(input, plan, client, delivery) @@ -126,7 +126,7 @@ func activationClassifierRequest(input AddInput, plan domain.DeliveryPlan, clien // rules stops a preview or a group with ErrProjectionRefreshRequired before // any effect. Other projection failures stay with the step that uses it. func (service Service) requireCurrentCommandProjection(ctx context.Context, input AddInput, plan domain.DeliveryPlan, installation domain.Installation, client domain.ClientBinding) error { - if !rendersCommands(input.Envelope, input.Client.ClientID) || !nativeLifecycleClient(input.Client.ClientID, plan.SelectedDelivery) { + if !rendersCommands(input.Envelope, input.Client.ClientID) || !nativeLifecycleClient(input.Client.ClientID, plan.SelectedDelivery, input.Scope) { return nil } if _, _, _, err := service.activeNativeDelivery(ctx, input, plan, installation, client); errors.Is(err, ErrProjectionRefreshRequired) { @@ -141,7 +141,7 @@ func (service Service) requireCurrentCommandProjection(ctx context.Context, inpu func (service Service) nonNativeRegistrationComplete(input AddInput, plan domain.DeliveryPlan, client domain.ClientBinding, delivery domain.StagedDelivery) bool { classifier, ok := service.Activator.(ports.AutomaticActivationClassifier) cliBacked := ok && classifier.AutomaticallyActivates(activationClassifierRequest(input, plan, client, delivery)) - if plan.SelectedDelivery.EffectiveTraits(input.Client.ClientID).LifecycleKind == domain.LifecycleCLIRegistry && cliBacked { + if plan.SelectedDelivery.EffectiveTraitsForScope(input.Client.ClientID, input.Scope).LifecycleKind == domain.LifecycleCLIRegistry && cliBacked { return input.ActivationComplete || client.Activation == domain.ActivationActive && (client.Verification == domain.VerificationInstalled || client.Verification == domain.VerificationRuntime) } return true diff --git a/install/integrationctl/agentplugins/usecase/opencode_namespace_group_test.go b/install/integrationctl/agentplugins/usecase/opencode_namespace_group_test.go index 88446fa86..62911902d 100644 --- a/install/integrationctl/agentplugins/usecase/opencode_namespace_group_test.go +++ b/install/integrationctl/agentplugins/usecase/opencode_namespace_group_test.go @@ -44,6 +44,48 @@ func TestOpenCodeNamespaceGroupEarlyConflictChangesNoTarget(t *testing.T) { } } +func TestOpenCodeNamespaceGroupCallbackConflictChangesNoTarget(t *testing.T) { + service, store, cursor := serviceFixture(t) + service.NativeObserver = providerstest.NewObserver(providers.NativeIdentityObserver{Stager: service.Stager}) + activator := &failNthGroupActivator{} + service.Activator = activator + openCode := domain.DetectedClient{ClientID: domain.ClientOpenCode, OpenCodeHost: contracttest.OpenCodeV1Host{}, Status: domain.DetectionDetected, ConfigRoot: filepath.Join(t.TempDir(), "opencode")} + if err := os.MkdirAll(openCode.ConfigRoot, 0700); err != nil { + t.Fatal(err) + } + config := filepath.Join(openCode.ConfigRoot, "opencode.json") + foreign := []byte(`{"mcp":{"docs_extra":{"type":"remote","url":"https://foreign.test"}}}`) + input := openCodePluginInput(t, openCode, "1.0.0", "sha256:namespace-callback", "sha256:namespace-callback-manifest", "sh") + cursorInput := openCodePluginInput(t, cursor, "1.0.0", "sha256:namespace-callback", "sha256:namespace-callback-manifest", "sh") + group := GroupInput{Targets: []AddInput{input, cursorInput}, OperationGroupID: "namespace-callback", Confirmed: true} + group.Progress = func(event GroupProgressEvent) { + if event.Phase == GroupProgressConfiguring && event.ClientID == domain.ClientCursor { + if err := os.WriteFile(config, foreign, 0600); err != nil { + t.Fatal(err) + } + } + } + result, err := service.AddGroup(t.Context(), group) + if err == nil || !strings.Contains(err.Error(), "MCP namespace changed before group commit") || !strings.Contains(err.Error(), "docs_extra") { + t.Fatalf("callback namespace conflict escaped precommit check: %v", err) + } + if result.Mutated || len(result.Receipts) != 0 || activator.calls != 0 { + t.Fatalf("callback refusal changed managed targets: result=%+v activations=%d", result, activator.calls) + } + state, err := store.Load() + if err != nil || len(state.Installations) != 0 { + t.Fatalf("callback refusal changed state: %+v, %v", state, err) + } + if body, err := os.ReadFile(config); err != nil || string(body) != string(foreign) { + t.Fatalf("callback foreign config changed: %s, %v", body, err) + } + for _, target := range result.Targets { + if _, err := os.Lstat(target.Plan.ActivePath); !os.IsNotExist(err) { + t.Fatalf("callback refusal published %s: %v", target.Plan.ClientID, err) + } + } +} + type injectOpenCodeConfigActivator struct { inner ports.ClientActivator path string diff --git a/install/integrationctl/agentplugins/usecase/prepared_native_config_unchanged_test.go b/install/integrationctl/agentplugins/usecase/prepared_native_config_unchanged_test.go new file mode 100644 index 000000000..14782e993 --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/prepared_native_config_unchanged_test.go @@ -0,0 +1,78 @@ +package usecase + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestPreparedNativeConfigNoChangeRequiresCompleteUnchangedOwnership(t *testing.T) { + for _, name := range []string{"exact", "incomplete projection", "missing ownership", "delivery mismatch", "outcome mismatch", "committed effect", "unknown effect", "pending attempt", "pending intent", "activation completion", "auth completion", "activation attestation", "auth attestation", "user scope", "wrong client", "wrong active path", "unverified", "blocked policy", "changed auth", "no verifier"} { + t.Run(name, func(t *testing.T) { + root := t.TempDir() + active := filepath.Join(root, ".uap", "managed", "fixture") + digest := "sha256:" + strings.Repeat("1", 64) + objects := []domain.NativeObjectOwnership{ + {Kind: "managed_package_directory", ObjectID: "package:codex:fixture", Path: active, ManagedDigest: digest}, + {Kind: "codex_project_skill_directory", ObjectID: "skill:docs", Path: filepath.Join(root, ".agents", "skills", "docs"), ManagedDigest: digest}, + } + input := AddInput{Scope: domain.ScopeProject, Client: domain.DetectedClient{ClientID: domain.ClientCodex}} + plan := domain.DeliveryPlan{ClientID: domain.ClientCodex, Scope: domain.ScopeProject, ActivePath: active} + binding := domain.ClientBinding{ClientID: "codex", Scope: "project", TargetLocator: active, Materialization: domain.MaterializationMaterialized, Activation: domain.ActivationPrepared, Verification: domain.VerificationInstalled, Authentication: domain.AuthenticationNotChecked, Policy: domain.PolicyAllowed, NativeObjects: append([]domain.NativeObjectOwnership(nil), objects...)} + delivery := domain.StagedDelivery{ClientID: domain.ClientCodex, ActivePath: active, ArtifactDigest: digest, NativeObjects: append([]domain.NativeObjectOwnership(nil), objects...)} + outcome := lifecycleOutcome(binding) + outcome.NativeEffect, outcome.NativeObjects = domain.NativeEffectUnchanged, append([]domain.NativeObjectOwnership(nil), objects...) + actor := &resumeObservationActivator{available: true} + service := Service{Activator: actor} + complete := true + switch name { + case "incomplete projection": + complete = false + case "missing ownership": + binding.NativeObjects, delivery.NativeObjects, outcome.NativeObjects = nil, nil, nil + case "delivery mismatch": + delivery.NativeObjects[1].Path += "-foreign" + case "outcome mismatch": + outcome.NativeObjects[1].Path += "-foreign" + case "committed effect": + outcome.NativeEffect = domain.NativeEffectCommitted + case "unknown effect": + outcome.NativeEffect = "" + case "pending attempt": + binding.NativeActivationAttempt = "unresolved" + case "pending intent": + binding.PendingNativeIntent = &domain.PendingNativeIntent{} + case "activation completion": + input.ActivationComplete = true + case "auth completion": + input.AuthComplete = true + case "activation attestation": + outcome.ActivationAttested = true + case "auth attestation": + outcome.AuthenticationAttested = true + case "user scope": + input.Scope, plan.Scope, binding.Scope = domain.ScopeUser, domain.ScopeUser, "user" + case "wrong client": + binding.ClientID = "opencode" + case "wrong active path": + plan.ActivePath += "-foreign" + case "unverified": + outcome.Verification = domain.VerificationPackageValid + case "blocked policy": + binding.Policy, outcome.Policy = domain.PolicyBlocked, domain.PolicyBlocked + case "changed auth": + outcome.Authentication = domain.AuthenticationComplete + case "no verifier": + actor.available = false + } + if got := service.verifiedPreparedObservationUnchanged(input, plan, binding, delivery, outcome, complete); got != (name == "exact") { + t.Fatalf("contradictory or missing proof classified as unchanged=%t", got) + } + if binding.Activation != domain.ActivationPrepared || binding.LifecycleConverged() { + t.Fatal("operation classification promoted Prepared lifecycle") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/usecase/prepared_project_verifier.go b/install/integrationctl/agentplugins/usecase/prepared_project_verifier.go new file mode 100644 index 000000000..ce5a2de87 --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/prepared_project_verifier.go @@ -0,0 +1,157 @@ +package usecase + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// This error separates a file-only verifier refusal from native absence that +// an acting repair could reconcile. Its actual outcome remains in AddResult. +type preparedProjectVerificationError struct{ cause error } + +func (e *preparedProjectVerificationError) Error() string { + return "prepared project file verification: " + e.cause.Error() +} +func (e *preparedProjectVerificationError) Unwrap() error { return e.cause } + +func preparedProjectFileCandidate(input AddInput, plan domain.DeliveryPlan) bool { + project := input.Scope == domain.ScopeProject || plan.Scope == domain.ScopeProject || plan.TargetContext.Scope == domain.ScopeProject || plan.TargetContext.ProjectRoot != "" + traits := (domain.SelectedDelivery{}).EffectiveTraitsForScope(input.Client.ClientID, domain.ScopeProject) + return project && traits.LifecycleKind == domain.LifecyclePrepared && !traits.TracksNativeEffects +} + +// Applicability is separate from authority and capability. A malformed project +// candidate must not fall through to a legacy executable-based verifier gate. +func (service Service) preparedProjectFileVerifier(input AddInput, plan domain.DeliveryPlan, id string, binding domain.ClientBinding, delivery domain.StagedDelivery) (bool, error) { + if input.Scope != domain.ScopeProject || plan.Scope != domain.ScopeProject || plan.TargetContext.Scope != domain.ScopeProject || plan.ClientID != input.Client.ClientID || !plan.SelectedDelivery.IsZero() { + return false, fmt.Errorf("file verifier scope, client or delivery differs from project") + } + target, err := domain.NewTargetContext(plan.TargetContext.Scope, plan.TargetContext.ProjectRoot, plan.TargetContext.UserConfigRoot) + if err != nil || target.ProjectRoot == "" || target.ProjectRoot != service.ProjectRoot || target.UserConfigRoot == "" || target.UserConfigRoot != input.Client.ConfigRoot { + return false, fmt.Errorf("file verifier target differs from original project context") + } + if err := matchPreparedProjectBinding(id, plan, binding, delivery); err != nil { + return false, err + } + copyInput := input + copyInput.Client = clonePreparedVerifierClient(input.Client) + return service.clientVerifierAvailable(copyInput, clonePreparedVerifierPlan(plan)), nil +} + +// Classifier and activation each receive a distinct packet. Neither owns the +// caller's frozen slices or authority, even if it mutates its request in place. +func clonePreparedVerifierClient(client domain.DetectedClient) domain.DetectedClient { + client.ProfileAuthority = domain.CloneProfileAuthority(client.ProfileAuthority) + client.Surfaces = append([]domain.ClientSurface(nil), client.Surfaces...) + return client +} +func clonePreparedVerifierPlan(plan domain.DeliveryPlan) domain.DeliveryPlan { + plan = cloneLocalObservationPlan(plan) + plan = plan.WithProfileAuthority(plan.ProfileAuthority(), plan.ProfileNamespace()) + plan.Components = append([]domain.ComponentDecision(nil), plan.Components...) + plan.UserActions = append([]string(nil), plan.UserActions...) + plan.LocalActions = append([]string(nil), plan.LocalActions...) + plan.Warnings = append([]string(nil), plan.Warnings...) + plan.Diagnostics = append([]domain.Diagnostic(nil), plan.Diagnostics...) + return plan +} +func matchPreparedProjectBinding(id string, plan domain.DeliveryPlan, binding domain.ClientBinding, delivery domain.StagedDelivery) error { + if !samePreparedProjectBinding(id, plan, binding) || !samePreparedProjectProfile(plan, binding) { + return fmt.Errorf("file verifier binding differs from exact prepared project") + } + if binding.NativeActivationAttempt != "" || binding.PendingNativeIntent != nil { + return fmt.Errorf("file verifier cannot acknowledge an unresolved native attempt") + } + if !samePreparedProjectDelivery(plan, binding, delivery) { + return fmt.Errorf("file verifier requires the exact current managed receipt") + } + object := binding.NativeObjects[0] + if object.Kind != "managed_package_directory" || object.ObjectID != "package:"+string(plan.ClientID)+":"+plan.PhysicalArtifactID || object.Path != plan.ActivePath || object.LogicalName != plan.DeclaredName { + return fmt.Errorf("file verifier receipt is not its managed package") + } + return nil +} +func samePreparedProjectBinding(id string, plan domain.DeliveryPlan, binding domain.ClientBinding) bool { + return id != "" && binding.ClientBindingID == domain.ComputeClientBindingID(id, string(plan.ClientID), "project", plan.ActivePath) && + binding.ClientID == string(plan.ClientID) && binding.Scope == "project" && binding.Activation == domain.ActivationPrepared && + binding.TargetLocator == plan.ActivePath && binding.PhysicalArtifact == plan.PhysicalArtifactID && binding.SelectedDelivery.IsZero() +} +func samePreparedProjectProfile(plan domain.DeliveryPlan, binding domain.ClientBinding) bool { + root := plan.NativeRegistryRoot + if authority := plan.ProfileAuthority(); authority != nil { + root = authority.Facts().CanonicalRoot + } + return root != "" && binding.NativeProfileRoot == root && binding.ProfileNamespace == plan.ProfileNamespace() && domain.SameProfileAuthority(binding.ProfileAuthority, plan.ProfileAuthority()) +} +func samePreparedProjectDelivery(plan domain.DeliveryPlan, binding domain.ClientBinding, delivery domain.StagedDelivery) bool { + return delivery.ClientID == plan.ClientID && delivery.ActivePath == plan.ActivePath && delivery.OwnedBase == plan.TargetRoot && + delivery.ArtifactDigest != "" && delivery.ArtifactDigest == managedDigest(binding) && reflect.DeepEqual(delivery.NativeObjects, binding.NativeObjects) && len(binding.NativeObjects) == 1 +} +func validPreparedProjectFileOutcome(binding domain.ClientBinding, out domain.ActivationOutcome) bool { + return out.Activation == domain.ActivationPrepared && out.Verification == domain.VerificationInstalled && out.Policy == domain.PolicyAllowed && out.Authentication == binding.Authentication && + !out.ActivationAttested && !out.AuthenticationAttested && !out.AuthoritativeObservation && out.LocalEntryObservation == nil && len(out.NativeObjects) == 0 && len(out.UserActions) > 0 && + (out.NativeEffect == "" || out.NativeEffect == domain.NativeEffectUnchanged) +} +func (service Service) observePreparedProjectFiles(ctx context.Context, input AddInput, plan domain.DeliveryPlan, id string, binding domain.ClientBinding, delivery domain.StagedDelivery) (domain.ActivationOutcome, error) { + available, err := service.preparedProjectFileVerifier(input, plan, id, binding, delivery) + if err != nil { + return domain.ActivationOutcome{}, &preparedProjectVerificationError{cause: err} + } + if !available { + return domain.ActivationOutcome{}, nil + } + delivery.NativeObjects = append([]domain.NativeObjectOwnership(nil), delivery.NativeObjects...) + out, err := service.activateReadOnlyWithObservation(ctx, binding, domain.ActivationRequest{Client: clonePreparedVerifierClient(input.Client), Plan: clonePreparedVerifierPlan(plan), Delivery: delivery, DeclaredName: plan.DeclaredName, Replacing: true, BackendExecutable: input.BackendExecutable, PreviousNativeObjects: append([]domain.NativeObjectOwnership(nil), binding.NativeObjects...), VerifyOnly: true}) + out = preserveManagedAuthentication(out, binding.Authentication) + out.UserActions = append([]string(nil), out.UserActions...) + out.LocalActions = append([]string(nil), out.LocalActions...) + out.LocalEntryObservation = out.LocalEntryObservation.Clone() + out.NativeObjects = append([]domain.NativeObjectOwnership(nil), out.NativeObjects...) + if err == nil && !validPreparedProjectFileOutcome(binding, out) { + err = fmt.Errorf("verifier returned contradictory file-only evidence (native effect %q)", out.NativeEffect) + } + if err != nil { + return out, &preparedProjectVerificationError{cause: err} + } + return out, nil +} +func retainPreparedProjectVerificationFailure(result *AddResult, out domain.ActivationOutcome, err error) bool { + var refusal *preparedProjectVerificationError + if !errors.As(err, &refusal) { + return false + } + result.Activation = out + result.NoChange = false + result.Mutated = result.Mutated || out.NativeEffect == domain.NativeEffectCommitted + return true +} + +func (session *repairSession) verifyRepairedProjectFiles(delivery domain.StagedDelivery) (AddResult, error) { + // commitRepairDirectory has already placed the current receipt in this exact + // session binding; never reuse the predecessor's native ownership objects. + if session.index < 0 || session.index >= len(session.state.Installations) { + return session.result, fmt.Errorf("fresh repaired project installation is missing") + } + binding, ok := session.state.Installations[session.index].Clients[session.clientKey] + if !ok { + return session.result, fmt.Errorf("fresh repaired project binding is missing") + } + out, err := session.service.observePreparedProjectFiles(session.ctx, session.input, session.plan, session.installation.InstallationID, binding, delivery) + session.result.Activation = out + if err != nil { + retainPreparedProjectVerificationFailure(&session.result, out, err) + return session.result, err + } + if out.Activation == "" { + session.result.Activation = lifecycleOutcome(binding) + return session.result, nil + } + if _, err := session.service.updateLifecycle(session.installation.InstallationID, session.clientKey, out); err != nil { + return session.result, fmt.Errorf("persist repaired project file verification: %w", err) + } + return session.result, nil +} diff --git a/install/integrationctl/agentplugins/usecase/prepared_project_verifier_test.go b/install/integrationctl/agentplugins/usecase/prepared_project_verifier_test.go new file mode 100644 index 000000000..66088724b --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/prepared_project_verifier_test.go @@ -0,0 +1,388 @@ +package usecase + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +type preparedFileStore struct { + state domain.StateFileV2 + saves int + fail error +} + +func (s *preparedFileStore) Load() (domain.StateFileV2, error) { + out := s.state + out.Installations = append([]domain.Installation(nil), out.Installations...) + for i := range out.Installations { + out.Installations[i].Clients = map[string]domain.ClientBinding{} + for key, b := range s.state.Installations[i].Clients { + b.NativeObjects = append([]domain.NativeObjectOwnership(nil), b.NativeObjects...) + out.Installations[i].Clients[key] = b + } + } + return out, nil +} +func (s *preparedFileStore) Save(state domain.StateFileV2) error { + if s.fail != nil { + return s.fail + } + s.saves++ + s.state = state + return nil +} + +type preparedFileActivator struct { + observedActivator + available bool + requests []domain.ActivationRequest + marker string + capabilityCalls int + mutateCapability func(domain.DeliveryPlan) + mutateActivation func(*domain.ActivationRequest) +} + +func (a *preparedFileActivator) VerifierAvailable(_ domain.DetectedClient, plan domain.DeliveryPlan, _ string) bool { + a.capabilityCalls++ + if a.mutateCapability != nil { + a.mutateCapability(plan) + } + return a.available +} +func (a *preparedFileActivator) Activate(_ context.Context, r domain.ActivationRequest) (domain.ActivationOutcome, error) { + a.requests = append(a.requests, r) + if a.mutateActivation != nil { + a.mutateActivation(&r) + } + if a.marker != "" { + if err := os.WriteFile(a.marker, []byte("actual inert effect"), 0600); err != nil { + return domain.ActivationOutcome{}, err + } + } + return a.outcome, a.err +} + +type preparedFileFixture struct { + service Service + input AddInput + plan domain.DeliveryPlan + binding domain.ClientBinding + delivery domain.StagedDelivery + store *preparedFileStore + actor *preparedFileActivator + id string +} + +func newPreparedFileFixture(t *testing.T) *preparedFileFixture { + t.Helper() + base := t.TempDir() + project := filepath.Join(base, "A") + root := filepath.Join(project, ".uap", "managed", "clients", "qoder") + active := filepath.Join(root, "demo-physical") + f := &preparedFileFixture{id: "prepared-file-install"} + f.input = AddInput{Client: domain.DetectedClient{ClientID: domain.ClientQoder, ConfigRoot: filepath.Join(base, "user")}, Scope: domain.ScopeProject, InstallationID: f.id, Confirmed: true} + f.plan = domain.DeliveryPlan{ClientID: domain.ClientQoder, Scope: domain.ScopeProject, DeclaredName: "demo", PhysicalArtifactID: "demo-physical", TargetRoot: root, ActivePath: active, NativeRegistryRoot: root, TargetContext: domain.TargetContext{Scope: domain.ScopeProject, ProjectRoot: project, UserConfigRoot: f.input.Client.ConfigRoot}} + object := domain.NativeObjectOwnership{Kind: "managed_package_directory", ObjectID: "package:qoder:demo-physical", LogicalName: "demo", Path: active, ManagedDigest: "sha256:recorded"} + f.binding = domain.ClientBinding{ClientBindingID: domain.ComputeClientBindingID(f.id, "qoder", "project", active), ClientID: "qoder", Scope: "project", PhysicalArtifact: "demo-physical", TargetLocator: active, NativeProfileRoot: root, Materialization: domain.MaterializationMaterialized, Activation: domain.ActivationPrepared, Verification: domain.VerificationInstalled, Authentication: domain.AuthenticationNotChecked, Policy: domain.PolicyAllowed, NativeObjects: []domain.NativeObjectOwnership{object}} + f.delivery = domain.StagedDelivery{ClientID: domain.ClientQoder, OwnedBase: root, ActivePath: active, ArtifactDigest: object.ManagedDigest, NativeObjects: append([]domain.NativeObjectOwnership(nil), f.binding.NativeObjects...)} + f.actor = &preparedFileActivator{available: true, observedActivator: observedActivator{outcome: domain.ActivationOutcome{Activation: domain.ActivationPrepared, Verification: domain.VerificationInstalled, Authentication: domain.AuthenticationNotChecked, Policy: domain.PolicyAllowed, NativeEffect: domain.NativeEffectUnchanged, UserActions: []string{"review the independent native import"}}}} + f.store = &preparedFileStore{state: domain.StateFileV2{SchemaVersion: domain.StateSchemaVersion, Installations: []domain.Installation{{InstallationID: f.id, Clients: map[string]domain.ClientBinding{f.binding.ClientBindingID: f.binding}}}}} + f.service = Service{ProjectRoot: project, Activator: f.actor, StateStore: f.store, Now: func() time.Time { return time.Unix(10, 0) }} + return f +} +func (f *preparedFileFixture) session(post bool) *repairSession { + state, _ := f.store.Load() + binding := state.Installations[0].Clients[f.binding.ClientBindingID] + if post { + binding.Verification = domain.VerificationPackageValid + state.Installations[0].Clients[binding.ClientBindingID] = binding + f.store.state = state + } + return &repairSession{service: f.service, ctx: context.Background(), input: f.input, plan: f.plan, result: AddResult{InstallationID: f.id, Plan: f.plan, Mutated: post}, state: state, index: 0, installation: state.Installations[0], clientKey: binding.ClientBindingID, client: binding} +} +func TestPreparedProjectFileVerifierUsesExactFreshReadOnlyDelivery(t *testing.T) { + for _, post := range []bool{false, true} { + t.Run(fmt.Sprint(post), func(t *testing.T) { + f := newPreparedFileFixture(t) + session := f.session(post) + var out AddResult + var err error + if post { + out, err = session.verifyRepairedProjectFiles(f.delivery) + } else { + observed, verifyErr := f.service.verifyClientReadOnly(t.Context(), f.input, session.result, f.binding) + if verifyErr != nil { + t.Fatal(verifyErr) + } + out, err = session.correctIntactLifecycle(observed) + } + if err != nil || out.Mutated != post || out.NoChange == post || out.Activation.Activation != domain.ActivationPrepared || out.Activation.Verification != domain.VerificationInstalled { + t.Fatalf("file result %+v %v", out, err) + } + if len(f.actor.requests) != 1 { + t.Fatal("no fresh verifier") + } + r := f.actor.requests[0] + if !r.VerifyOnly || !reflect.DeepEqual(r.Client, f.input.Client) || !reflect.DeepEqual(r.Plan, f.plan) || !reflect.DeepEqual(r.Delivery, f.delivery) || !reflect.DeepEqual(r.PreviousNativeObjects, f.binding.NativeObjects) { + t.Fatalf("verifier lost exact authority %+v", r) + } + state, _ := f.store.Load() + saved := state.Installations[0].Clients[f.binding.ClientBindingID] + if saved.Verification != domain.VerificationInstalled || saved.NativeActivationAttempt != "" || saved.PendingNativeIntent != nil || !reflect.DeepEqual(saved.NativeObjects, f.binding.NativeObjects) { + t.Fatalf("file proof became native ownership %+v", saved) + } + }) + } +} +func TestPreparedProjectFileVerifierRejectsContradictoryOutcomesWithoutAdoption(t *testing.T) { + for _, post := range []bool{false, true} { + for _, kind := range []string{"committed", "committed-error", "uncertain", "unknown-effect", "objects", "local-observation", "activation-attested", "auth-attested", "authoritative", "no-actions", "active", "unverified", "blocked", "auth-change", "error"} { + t.Run(fmt.Sprintf("post%t/%s", post, kind), func(t *testing.T) { + f := newPreparedFileFixture(t) + session := f.session(post) + sentinel := errors.New("inert verifier failure") + switch kind { + case "committed", "committed-error": + f.actor.outcome.NativeEffect = domain.NativeEffectCommitted + f.actor.marker = filepath.Join(t.TempDir(), "inert-effect") + if kind == "committed-error" { + f.actor.err = sentinel + } + case "uncertain": + f.actor.outcome.NativeEffect = domain.NativeEffectUncertain + case "unknown-effect": + f.actor.outcome.NativeEffect = "future" + case "objects": + f.actor.outcome.NativeObjects = []domain.NativeObjectOwnership{{Kind: "foreign", Path: "outside"}} + case "local-observation": + f.actor.outcome.LocalEntryObservation = &domain.LocalEntryObservation{} + case "activation-attested": + f.actor.outcome.ActivationAttested = true + case "auth-attested": + f.actor.outcome.AuthenticationAttested = true + case "authoritative": + f.actor.outcome.AuthoritativeObservation = true + case "no-actions": + f.actor.outcome.UserActions = nil + case "active": + f.actor.outcome.Activation = domain.ActivationActive + case "unverified": + f.actor.outcome.Verification = domain.VerificationPackageValid + case "blocked": + f.actor.outcome.Policy = domain.PolicyBlocked + case "auth-change": + f.actor.outcome.Authentication = domain.AuthenticationComplete + case "error": + f.actor.err = sentinel + } + before, _ := json.Marshal(f.store.state) + var out AddResult + var err error + if post { + out, err = session.verifyRepairedProjectFiles(f.delivery) + } else { + out, err = session.repairNative() + } + if err == nil || out.NoChange || out.Mutated != (post || kind == "committed" || kind == "committed-error") || len(f.actor.requests) != 1 || !f.actor.requests[0].VerifyOnly { + t.Fatalf("contradiction hidden %+v %v calls%d", out, err, len(f.actor.requests)) + } + if f.actor.err != nil && !errors.Is(err, sentinel) { + t.Fatalf("cause lost %v", err) + } + if out.Activation.NativeEffect != f.actor.outcome.NativeEffect { + t.Fatal("reported effect rewritten") + } + after, _ := json.Marshal(f.store.state) + if string(before) != string(after) || f.store.saves != 0 { + t.Fatal("contradictory outcome changed saved facts/receipts") + } + if f.actor.marker != "" { + if body, err := os.ReadFile(f.actor.marker); err != nil || string(body) != "actual inert effect" { + t.Fatal("committed claim lacks actual fixture effect", err) + } + } + }) + } + } +} +func TestPreparedProjectFileVerifierRefusesMalformedAuthorityBeforeCapability(t *testing.T) { + for _, kind := range []string{"input-scope", "plan-scope", "context-scope", "context-root", "configured-root", "user-root", "client", "selection", "binding-client", "binding-scope", "binding-id", "binding-path", "binding-physical", "binding-profile", "binding-activation", "attempt", "intent", "receipt-digest", "receipt-path", "extra-object", "delivery-root"} { + t.Run(kind, func(t *testing.T) { + f := newPreparedFileFixture(t) + switch kind { + case "input-scope": + f.input.Scope = domain.ScopeUser + case "plan-scope": + f.plan.Scope = domain.ScopeUser + case "context-scope": + f.plan.TargetContext.Scope = domain.ScopeUser + case "context-root": + f.plan.TargetContext.ProjectRoot = "" + case "configured-root": + f.service.ProjectRoot += "-other" + case "user-root": + f.plan.TargetContext.UserConfigRoot += "-other" + case "client": + f.plan.ClientID = domain.ClientGrok + case "selection": + if err := json.Unmarshal([]byte(`{"mode":"unknown-project-test"}`), &f.plan.SelectedDelivery); err != nil { + t.Fatal(err) + } + case "binding-client": + f.binding.ClientID = "grok" + case "binding-scope": + f.binding.Scope = "user" + case "binding-id": + f.binding.ClientBindingID = "other" + case "binding-path": + f.binding.TargetLocator += "-other" + case "binding-physical": + f.binding.PhysicalArtifact = "other" + case "binding-profile": + f.binding.NativeProfileRoot += "-other" + case "binding-activation": + f.binding.Activation = domain.ActivationActive + case "attempt": + f.binding.NativeActivationAttempt = "unacknowledged" + case "intent": + f.binding.PendingNativeIntent = &domain.PendingNativeIntent{} + case "receipt-digest": + f.delivery.ArtifactDigest = "sha256:foreign" + case "receipt-path": + f.delivery.NativeObjects[0].Path = "foreign" + case "extra-object": + f.delivery.NativeObjects = append(f.delivery.NativeObjects, domain.NativeObjectOwnership{Kind: "foreign"}) + case "delivery-root": + f.delivery.OwnedBase += "-other" + } + if !preparedProjectFileCandidate(f.input, f.plan) { + t.Fatal("malformed explicit project escaped candidate gate") + } + out, err := f.service.observePreparedProjectFiles(t.Context(), f.input, f.plan, f.id, f.binding, f.delivery) + var refusal *preparedProjectVerificationError + if err == nil || !errors.As(err, &refusal) || f.actor.capabilityCalls != 0 || len(f.actor.requests) != 0 || out.Activation != "" { + t.Fatalf("malformed authority reached fallback/verifier %+v %v", out, err) + } + }) + } +} +func TestPreparedProjectFileVerifierUnavailableNeverCallsInheritedGate(t *testing.T) { + f := newPreparedFileFixture(t) + f.actor.available = false + f.input.BackendExecutable = "/inert/qoder-static-auth-hint-must-not-enable" + out, err := f.service.verifyClientReadOnly(t.Context(), f.input, AddResult{InstallationID: f.id, Plan: f.plan}, f.binding) + if err != nil || out.Activation != "" || len(f.actor.requests) != 0 || f.actor.capabilityCalls != 1 { + t.Fatalf("unavailable verifier executed %+v %v", out, err) + } + f.service.Activator = &observedActivator{} + out, err = f.service.verifyClientReadOnly(t.Context(), f.input, AddResult{InstallationID: f.id, Plan: f.plan}, f.binding) + if err != nil || out.Activation != "" { + t.Fatalf("missing capability enabled verifier %+v %v", out, err) + } + for _, id := range []domain.ClientID{domain.ClientClaude, domain.ClientCodex} { + f.input.Client.ClientID = id + f.plan.ClientID = id + if preparedProjectFileCandidate(f.input, f.plan) { + t.Fatalf("other lifecycle borrowed prepared contract %s", id) + } + } + f = newPreparedFileFixture(t) + f.input.Scope = domain.ScopeUser + f.plan.Scope = domain.ScopeUser + f.plan.TargetContext = domain.TargetContext{Scope: domain.ScopeUser, UserConfigRoot: f.input.Client.ConfigRoot} + if preparedProjectFileCandidate(f.input, f.plan) { + t.Fatal("user route changed") + } +} +func TestPreparedProjectFilePostCommitPersistenceFailureRetainsMutation(t *testing.T) { + f := newPreparedFileFixture(t) + session := f.session(true) + sentinel := errors.New("inert persistence failure") + f.store.fail = sentinel + before, _ := json.Marshal(f.store.state) + out, err := session.verifyRepairedProjectFiles(f.delivery) + if !errors.Is(err, sentinel) || !out.Mutated || out.NoChange || len(f.actor.requests) != 1 { + t.Fatalf("postcommit error hid mutation %+v %v", out, err) + } + after, _ := json.Marshal(f.store.state) + if string(before) != string(after) { + t.Fatal("failed persistence fabricated verification state") + } +} + +func TestPreparedProjectFileFailureCannotBecomeAuthoritativePreviewPersistence(t *testing.T) { + f := newPreparedFileFixture(t) + f.actor.outcome.NativeEffect = domain.NativeEffectCommitted + f.actor.outcome.AuthoritativeObservation = true + f.actor.marker = filepath.Join(t.TempDir(), "preview-effect") + session := &applySession{service: f.service, ctx: t.Context(), input: f.input, result: AddResult{InstallationID: f.id, Plan: f.plan}, installationID: f.id, clientBindingID: f.binding.ClientBindingID} + before, _ := json.Marshal(f.store.state) + _, out, err := session.persistReadOnlyObservation(f.binding) + if err == nil || !out.Mutated || out.NoChange || out.Activation.NativeEffect != domain.NativeEffectCommitted || len(f.actor.requests) != 1 { + t.Fatalf("preview hid actual effect: %+v %v", out, err) + } + after, _ := json.Marshal(f.store.state) + if string(before) != string(after) || f.store.saves != 0 { + t.Fatal("invalid file-only outcome became authoritative saved evidence") + } +} + +func TestPreparedProjectFileVerifierCannotMutateFrozenOwnershipPacket(t *testing.T) { + for _, post := range []bool{false, true} { + for _, boundary := range []string{"capability", "activation"} { + t.Run(fmt.Sprintf("post%t/%s", post, boundary), func(t *testing.T) { + f := newPreparedFileFixture(t) + session := f.session(post) + if !post { + session.client.Verification = domain.VerificationPackageValid + } + session.plan.PreviousNativeObjects = session.client.NativeObjects + session.result.Plan = session.plan + beforeObjects := append([]domain.NativeObjectOwnership(nil), session.client.NativeObjects...) + mutate := func(objects []domain.NativeObjectOwnership) { + objects[0].ManagedDigest = "sha256:unowned-mutated-input" + objects[0].Path = "foreign-input-path" + } + if boundary == "capability" { + f.actor.mutateCapability = func(plan domain.DeliveryPlan) { mutate(plan.PreviousNativeObjects) } + } else { + f.actor.mutateActivation = func(request *domain.ActivationRequest) { + mutate(request.Delivery.NativeObjects) + mutate(request.Plan.PreviousNativeObjects) + mutate(request.PreviousNativeObjects) + } + } + var out AddResult + var err error + if post { + out, err = session.verifyRepairedProjectFiles(f.delivery) + } else { + var observed domain.ActivationOutcome + observed, err = f.service.verifyClientReadOnly(t.Context(), f.input, session.result, session.client) + if err == nil { + out, err = session.correctIntactLifecycle(observed) + } + } + if err != nil || out.Activation.Verification != domain.VerificationInstalled { + t.Fatalf("valid observer outcome failed: %+v %v", out, err) + } + if boundary == "capability" && (len(f.actor.requests) != 1 || !reflect.DeepEqual(f.actor.requests[0].Plan.PreviousNativeObjects, beforeObjects) || !reflect.DeepEqual(f.actor.requests[0].Delivery.NativeObjects, beforeObjects)) { + t.Fatal("classifier mutation contaminated activation packet") + } + state, _ := f.store.Load() + if !reflect.DeepEqual(session.client.NativeObjects, beforeObjects) || !reflect.DeepEqual(session.plan.PreviousNativeObjects, beforeObjects) || !reflect.DeepEqual(state.Installations[0].Clients[f.binding.ClientBindingID].NativeObjects, beforeObjects) { + t.Fatal("external verifier mutated or persisted unowned input receipt") + } + }) + } + } +} diff --git a/install/integrationctl/agentplugins/usecase/project_context.go b/install/integrationctl/agentplugins/usecase/project_context.go new file mode 100644 index 000000000..e6680fb57 --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/project_context.go @@ -0,0 +1,48 @@ +package usecase + +import ( + "context" + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func (service Service) scope() domain.InstallScope { + if service.ProjectRoot != "" { + return domain.ScopeProject + } + return domain.ScopeUser +} + +func (service Service) targetContext(client domain.DetectedClient, scope domain.InstallScope) domain.TargetContext { + return domain.TargetContext{Scope: scope, ProjectRoot: service.ProjectRoot, UserConfigRoot: client.ConfigRoot} +} + +func (service Service) validateNativeBinding(ctx context.Context, binding domain.ClientBinding, client domain.DetectedClient) error { + scope := domain.InstallScope(binding.Scope) + if scope != domain.ScopeProject { + return validateNativeBinding(binding, client) + } + if service.ProjectRoot == "" || service.Targets == nil { + return fmt.Errorf("project binding requires its explicit project target resolver") + } + target, err := service.Targets.ResolveTarget(ctx, client, scope, binding.PhysicalArtifact) + if err != nil { + return err + } + if target.TargetContext != service.targetContext(client, scope) || target.ActivePath != binding.TargetLocator || + target.NativeRegistryRoot == "" || target.NativeRegistryRoot != binding.NativeProfileRoot { + return fmt.Errorf("project binding differs from its selected layout or native registry") + } + return validateNativeBindingAt(binding, client, target.NativeRegistryRoot) +} + +func nativeRegistryForBinding(binding domain.ClientBinding, client domain.DetectedClient) string { + if binding.Scope == string(domain.ScopeProject) { + return binding.NativeProfileRoot + } + if binding.NativeProfileRoot != "" { + return binding.NativeProfileRoot + } + return client.ConfigRoot +} diff --git a/install/integrationctl/agentplugins/usecase/remove_group_native.go b/install/integrationctl/agentplugins/usecase/remove_group_native.go index 433a25e8b..0d3e006af 100644 --- a/install/integrationctl/agentplugins/usecase/remove_group_native.go +++ b/install/integrationctl/agentplugins/usecase/remove_group_native.go @@ -10,13 +10,15 @@ func (session *removeGroupSession) removeGroupNative() error { externalCompleted := 0 for plannedIndex := range session.planned { item := &session.planned[plannedIndex] - nativeAttempt := nativeLifecycleClient(item.input.Client.ClientID, item.client.SelectedDelivery) + nativeAttempt := nativeLifecycleClient(item.input.Client.ClientID, item.client.SelectedDelivery, item.input.Scope) if nativeAttempt { if err := session.service.beginNativeAttemptWithObservation(session.installation.InstallationID, item.clientKey, domain.NativeIntentRemove, item.client.SelectedDelivery, item.client.LocalEntryObservation.Clone()); err != nil { return err } } outcome, err := session.service.deactivateWithFrozenObservation(session.ctx, session.installation.InstallationID, item.clientKey, domain.DeactivationRequest{ + TargetContext: session.service.targetContext(item.input.Client, item.input.Scope), + NativeRegistryRoot: nativeRegistryForBinding(item.client, item.input.Client), LocalEntryObservation: item.client.LocalEntryObservation.Clone(), RemoveOwnedEntry: item.client.SelectedDelivery.OwnsProfileEntry(item.client.NativeObjects), SelectedDelivery: item.client.SelectedDelivery, diff --git a/install/integrationctl/agentplugins/usecase/remove_group_session.go b/install/integrationctl/agentplugins/usecase/remove_group_session.go index 479591bc1..5371a8017 100644 --- a/install/integrationctl/agentplugins/usecase/remove_group_session.go +++ b/install/integrationctl/agentplugins/usecase/remove_group_session.go @@ -112,14 +112,14 @@ func (session *removeGroupSession) resolveOneRemoval(targetIndex int, targetInpu if err := validateDeliverySelection(client.SelectedDelivery, targetInput.SelectedDelivery); err != nil { return err } - if err := validateNativeBinding(client, targetInput.Client); err != nil { + if err := session.service.validateNativeBinding(session.ctx, client, targetInput.Client); err != nil { return err } session.result.Targets[targetIndex] = RemoveResult{ InstallationID: session.installation.InstallationID, Plugin: session.installation.DeclaredName, ClientID: targetInput.Client.ClientID, - AffectedSurfaces: preparedAffectedSurfaces(client, targetInput.Client.ClientID, client.SelectedDelivery), + AffectedSurfaces: preparedAffectedSurfaces(client, targetInput.Client.ClientID, client.SelectedDelivery, targetInput.Scope), } if prior, ok := session.byBinding[clientKey]; ok { item := &session.planned[prior] @@ -132,6 +132,8 @@ func (session *removeGroupSession) resolveOneRemoval(targetIndex int, targetInpu return err } outcome, err := session.service.deactivateWithFrozenObservation(session.ctx, session.installation.InstallationID, clientKey, domain.DeactivationRequest{ + TargetContext: session.service.targetContext(targetInput.Client, targetInput.Scope), + NativeRegistryRoot: nativeRegistryForBinding(client, targetInput.Client), LocalEntryObservation: client.LocalEntryObservation.Clone(), RemoveOwnedEntry: client.SelectedDelivery.OwnsProfileEntry(client.NativeObjects), SelectedDelivery: client.SelectedDelivery, @@ -158,7 +160,7 @@ func (session *removeGroupSession) lookupRemovalBinding(targetInput RemoveInput) return clientKey, client, nil } for key, candidate := range session.installation.Clients { - if sameNativeBackend(domain.ClientID(candidate.ClientID), targetInput.Client.ClientID) && candidate.Scope == string(targetInput.Scope) && candidate.Materialization != domain.MaterializationAbsent { + if sameNativeBackend(domain.ClientID(candidate.ClientID), targetInput.Client.ClientID, targetInput.Scope) && candidate.Scope == string(targetInput.Scope) && candidate.Materialization != domain.MaterializationAbsent { return key, candidate, nil } } @@ -174,7 +176,7 @@ func (session *removeGroupSession) verifyRemovalOwnership(targetInput RemoveInpu if err != nil { return "", err } - if sameNativeBackend(domain.ClientID(client.ClientID), targetInput.Client.ClientID) { + if sameNativeBackend(domain.ClientID(client.ClientID), targetInput.Client.ClientID, targetInput.Scope) { target.ActivePath = client.TargetLocator target.TargetRoot = filepath.Dir(client.TargetLocator) } diff --git a/install/integrationctl/agentplugins/usecase/remove_session.go b/install/integrationctl/agentplugins/usecase/remove_session.go index 63b21da4a..ed2061535 100644 --- a/install/integrationctl/agentplugins/usecase/remove_session.go +++ b/install/integrationctl/agentplugins/usecase/remove_session.go @@ -46,7 +46,7 @@ func (session *removeSession) loadRemoveTarget() error { if err := validateDeliverySelection(client.SelectedDelivery, session.input.SelectedDelivery); err != nil { return err } - if err := validateNativeBinding(client, session.input.Client); err != nil { + if err := session.service.validateNativeBinding(session.ctx, client, session.input.Client); err != nil { return err } if err := validatePurgeCoversActiveBindings(session.input.PurgeData, installation, clientKey); err != nil { @@ -61,7 +61,7 @@ func (session *removeSession) loadRemoveTarget() error { InstallationID: installation.InstallationID, Plugin: installation.DeclaredName, ClientID: session.input.Client.ClientID, - AffectedSurfaces: preparedAffectedSurfaces(client, session.input.Client.ClientID, client.SelectedDelivery), + AffectedSurfaces: preparedAffectedSurfaces(client, session.input.Client.ClientID, client.SelectedDelivery, session.input.Scope), } if len(session.result.AffectedSurfaces) == 0 { session.result.AffectedSurfaces = []string{client.ClientID} @@ -70,13 +70,15 @@ func (session *removeSession) loadRemoveTarget() error { } func (session *removeSession) deactivateRemoveTarget() error { - nativeAttempt := nativeLifecycleClient(session.input.Client.ClientID, session.client.SelectedDelivery) && session.input.Confirmed && !session.input.DryRun + nativeAttempt := nativeLifecycleClient(session.input.Client.ClientID, session.client.SelectedDelivery, session.input.Scope) && session.input.Confirmed && !session.input.DryRun if nativeAttempt { if err := session.service.beginNativeAttemptWithObservation(session.installation.InstallationID, session.clientKey, domain.NativeIntentRemove, session.client.SelectedDelivery, session.client.LocalEntryObservation.Clone()); err != nil { return err } } deactivation, err := session.service.deactivateWithFrozenObservation(session.ctx, session.installation.InstallationID, session.clientKey, domain.DeactivationRequest{ + TargetContext: session.service.targetContext(session.input.Client, session.input.Scope), + NativeRegistryRoot: nativeRegistryForBinding(session.client, session.input.Client), LocalEntryObservation: session.client.LocalEntryObservation.Clone(), RemoveOwnedEntry: session.client.SelectedDelivery.OwnsProfileEntry(session.client.NativeObjects), SelectedDelivery: session.client.SelectedDelivery, @@ -113,7 +115,7 @@ func (session *removeSession) reloadAfterExternalRemoval() error { if err != nil { return err } - if session.result.Deactivation.ExternalRemovalComplete && !nativeLifecycleClient(session.input.Client.ClientID, session.client.SelectedDelivery) && session.client.Activation == domain.ActivationActive { + if session.result.Deactivation.ExternalRemovalComplete && !nativeLifecycleClient(session.input.Client.ClientID, session.client.SelectedDelivery, session.input.Scope) && session.client.Activation == domain.ActivationActive { if err := session.service.markDeactivated(state, session.installationIndex, session.clientKey); err != nil { return err } diff --git a/install/integrationctl/agentplugins/usecase/repair_native.go b/install/integrationctl/agentplugins/usecase/repair_native.go index ed57795a2..d4f0d51b8 100644 --- a/install/integrationctl/agentplugins/usecase/repair_native.go +++ b/install/integrationctl/agentplugins/usecase/repair_native.go @@ -23,10 +23,13 @@ func (session *repairSession) repairNative() (AddResult, error) { } verified, clientVerifyErr := session.service.verifyClientReadOnly(session.ctx, session.input, session.result, session.client) if clientVerifyErr != nil { + if retainPreparedProjectVerificationFailure(&session.result, verified, clientVerifyErr) { + return session.result, clientVerifyErr + } if session.recordedAbsenceRepairable(verified, clientVerifyErr) { return session.reapplyIntactNative(delivery) } - if !nativeLifecycleClient(session.input.Client.ClientID, session.plan.SelectedDelivery) || session.client.LocalEntryObservation != nil || verified.LocalEntryObservation != nil || session.ctx.Err() != nil { + if !nativeLifecycleClient(session.input.Client.ClientID, session.plan.SelectedDelivery, session.input.Scope) || session.client.LocalEntryObservation != nil || verified.LocalEntryObservation != nil || session.ctx.Err() != nil { return session.result, clientVerifyErr } return session.reapplyIntactNative(delivery) @@ -47,6 +50,9 @@ func (session *repairSession) reapplyIntactNative(delivery domain.StagedDelivery session.result.RequiresConfirmation = true return session.result, nil } + if err := session.verifyProjectRepairOwnership(); err != nil { + return session.result, err + } outcome, activationErr := session.service.activateWithNativeAttempt(session.ctx, session.installation.InstallationID, session.clientKey, domain.ActivationRequest{ Client: session.input.Client, Plan: session.result.Plan, Delivery: delivery, DeclaredName: session.input.Envelope.Manifest.Name, Replacing: true, @@ -93,6 +99,10 @@ func (session *repairSession) correctIntactLifecycle(verified domain.ActivationO corrected.Verification = verified.Verification } session.result.Activation = lifecycleOutcome(corrected) + // Fresh verification guidance is transient; the binding intentionally stores + // lifecycle values only. Preserve the current adapter's actions in this result. + session.result.Activation.UserActions = append([]string(nil), verified.UserActions...) + session.result.Activation.LocalActions = append([]string(nil), verified.LocalActions...) if corrected.Materialization == session.client.Materialization && sameLifecycleOutcome(lifecycleOutcome(corrected), lifecycleOutcome(session.client)) { session.result.NoChange = true return session.result, nil diff --git a/install/integrationctl/agentplugins/usecase/repair_package.go b/install/integrationctl/agentplugins/usecase/repair_package.go index 8178927f5..69c248e3d 100644 --- a/install/integrationctl/agentplugins/usecase/repair_package.go +++ b/install/integrationctl/agentplugins/usecase/repair_package.go @@ -135,6 +135,9 @@ func (session *repairSession) commitRepairDirectory(delivery domain.StagedDelive return session.result, fmt.Errorf("prepared host changed before repair commit: %w", err) } } + if err := session.verifyProjectRepairOwnership(); err != nil { + return session.result, err + } // The user or client can change the native object while staging runs. Repair // may replace the exact absent/digest-mismatched object reviewed above, but // never a different object that appeared after preflight. @@ -180,10 +183,13 @@ func (session *repairSession) commitRepairDirectory(delivery domain.StagedDelive } func (session *repairSession) reactivateRepaired(delivery domain.StagedDelivery, verifiedState domain.ActivationOutcome) (AddResult, error) { - if nativeLifecycleClient(session.input.Client.ClientID, session.plan.SelectedDelivery) { + if preparedProjectFileCandidate(session.input, session.plan) { + return session.verifyRepairedProjectFiles(delivery) + } + if nativeLifecycleClient(session.input.Client.ClientID, session.plan.SelectedDelivery, session.input.Scope) { return session.reapplyRepairedNative(delivery) } - if session.plan.SelectedDelivery.EffectiveTraits(session.input.Client.ClientID).UsesManagedStdioLauncher { + if session.plan.SelectedDelivery.EffectiveTraitsForScope(session.input.Client.ClientID, session.input.Scope).UsesManagedStdioLauncher { return session.verifyRepairedLauncher(delivery) } _ = verifiedState diff --git a/install/integrationctl/agentplugins/usecase/repair_project.go b/install/integrationctl/agentplugins/usecase/repair_project.go new file mode 100644 index 000000000..63a2e8682 --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/repair_project.go @@ -0,0 +1,53 @@ +package usecase + +import ( + "fmt" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/ports" +) + +// Managed-package repair may restore known absent or drifted bytes. It must +// first prove that the separate project component files remain exclusively +// owned. A package-only fallback cannot supply this prepared evidence. +func (session *repairSession) verifyProjectRepairOwnership() error { + if session.input.Scope != domain.ScopeProject || session.plan.SelectedDelivery.EffectiveTraitsForScope(session.input.Client.ClientID, session.input.Scope).LifecycleKind != domain.LifecycleNativeConfig { + return nil + } + if _, ok := session.service.NativeObserver.(PreparedIdentityObserver); !ok { + return fmt.Errorf("project component repair requires a prepared ownership observer") + } + observation, err := session.service.preparedIdentityObservation(session.ctx, session.input.Client, session.plan, &session.client) + if err != nil { + return fmt.Errorf("observe project files before repair: %w", err) + } + // Prepared providers explicitly report no native observation. A wrapper's + // legacy package-only fallback has no such finding and cannot qualify. + if observation.NativeDiscoveryAttempted || observation.NativeDiscoveryReconciled || observation.NativeDiscoveryState != domain.NativeIdentityAbsent { + return fmt.Errorf("project repair requires filesystem-only ownership evidence") + } + if session.verifyErr == nil { + if observation.State != domain.NativeIdentityManaged || !observation.ReceiptReconciled { + return fmt.Errorf("project repair ownership receipt is not reconciled") + } + return validateNativeIdentityObservation(observation, &session.client) + } + verification, known := repairMismatchKind(session.verifyErr) + if !known { + return fmt.Errorf("managed package integrity could not be determined before project repair: %w", session.verifyErr) + } + if observation.ReceiptReconciled { + return fmt.Errorf("project repair observation contradicts managed package integrity") + } + switch verification.Kind { + case ports.VerificationAbsent: + if observation.State == domain.NativeIdentityAbsent && observation.Digest == "" { + return nil + } + case ports.VerificationDigestMismatch: + if verification.ActualDigest != "" && observation.State == domain.NativeIdentityIndeterminate && observation.Digest == verification.ActualDigest { + return nil + } + } + return fmt.Errorf("project component ownership or managed package changed before repair") +} diff --git a/install/integrationctl/agentplugins/usecase/repair_project_test.go b/install/integrationctl/agentplugins/usecase/repair_project_test.go new file mode 100644 index 000000000..1e5cf5973 --- /dev/null +++ b/install/integrationctl/agentplugins/usecase/repair_project_test.go @@ -0,0 +1,97 @@ +package usecase + +import ( + "context" + "errors" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/ports" +) + +type projectRepairObserver struct { + observation domain.NativeIdentityObservation + err error + prepared, native int +} + +func (o *projectRepairObserver) ObserveNativeIdentity(context.Context, domain.DetectedClient, domain.DeliveryPlan, *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + o.native++ + return domain.NativeIdentityObservation{}, errors.New("native discovery forbidden") +} +func (o *projectRepairObserver) ObservePreparedIdentity(context.Context, domain.DetectedClient, domain.DeliveryPlan, *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + o.prepared++ + return o.observation, o.err +} + +type projectRepairNativeOnly struct{} + +func (projectRepairNativeOnly) ObserveNativeIdentity(context.Context, domain.DetectedClient, domain.DeliveryPlan, *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + return domain.NativeIdentityObservation{}, errors.New("native discovery forbidden") +} + +func TestProjectRepairAdmissionMatchesIndependentManagedEvidence(t *testing.T) { + const owned = "sha256:owned" + intact := domain.NativeIdentityObservation{State: domain.NativeIdentityManaged, Digest: owned, ReceiptReconciled: true, NativeDiscoveryState: domain.NativeIdentityAbsent} + absent := domain.NativeIdentityObservation{State: domain.NativeIdentityAbsent, NativeDiscoveryState: domain.NativeIdentityAbsent} + drift := domain.NativeIdentityObservation{State: domain.NativeIdentityIndeterminate, Digest: "sha256:drift", NativeDiscoveryState: domain.NativeIdentityAbsent} + for _, tc := range []struct { + name string + verify error + observation domain.NativeIdentityObservation + observeErr error + change func(*domain.NativeIdentityObservation) + want bool + }{ + {name: "intact", observation: intact, want: true}, + {name: "known-absent", verify: &ports.VerificationError{Kind: ports.VerificationAbsent}, observation: absent, want: true}, + {name: "known-drift", verify: &ports.VerificationError{Kind: ports.VerificationDigestMismatch, ActualDigest: "sha256:drift"}, observation: drift, want: true}, + {name: "unreconciled-receipt", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.ReceiptReconciled = false }}, + {name: "missing-receipt-digest", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.Digest = "" }}, + {name: "different-receipt-digest", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.Digest = "sha256:other" }}, + {name: "discovery-attempt", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.NativeDiscoveryAttempted = true }}, + {name: "discovery-reconciliation", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.NativeDiscoveryReconciled = true }}, + {name: "package-only-fallback", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.NativeDiscoveryState = "" }}, + {name: "unknown-state", observation: intact, change: func(o *domain.NativeIdentityObservation) { o.State = "unknown" }}, + {name: "foreign-local", observation: domain.NativeIdentityObservation{State: domain.NativeIdentityUnmanaged, NativeDiscoveryState: domain.NativeIdentityAbsent}}, + {name: "observation-error", observation: intact, observeErr: errors.New("read refused")}, + {name: "unknown-integrity-error", verify: errors.New("permission denied"), observation: drift}, + {name: "changed-between-measurements", verify: &ports.VerificationError{Kind: ports.VerificationDigestMismatch, ActualDigest: "sha256:first"}, observation: drift}, + {name: "mismatch-without-actual", verify: &ports.VerificationError{Kind: ports.VerificationDigestMismatch}, observation: drift}, + {name: "mismatch-without-observed", verify: &ports.VerificationError{Kind: ports.VerificationDigestMismatch, ActualDigest: "sha256:drift"}, observation: drift, change: func(o *domain.NativeIdentityObservation) { o.Digest = "" }}, + {name: "mismatch-contradictory-receipt", verify: &ports.VerificationError{Kind: ports.VerificationDigestMismatch, ActualDigest: "sha256:drift"}, observation: drift, change: func(o *domain.NativeIdentityObservation) { o.ReceiptReconciled = true }}, + {name: "absent-with-digest", verify: &ports.VerificationError{Kind: ports.VerificationAbsent}, observation: absent, change: func(o *domain.NativeIdentityObservation) { o.Digest = owned }}, + {name: "absent-with-foreign-local", verify: &ports.VerificationError{Kind: ports.VerificationAbsent}, observation: domain.NativeIdentityObservation{State: domain.NativeIdentityUnmanaged, NativeDiscoveryState: domain.NativeIdentityAbsent}}, + {name: "drift-with-unknown-local", verify: &ports.VerificationError{Kind: ports.VerificationDigestMismatch, ActualDigest: "sha256:drift"}, observation: domain.NativeIdentityObservation{State: domain.NativeIdentityIndeterminate, NativeDiscoveryState: domain.NativeIdentityAbsent}}, + } { + t.Run(tc.name, func(t *testing.T) { + observer := &projectRepairObserver{observation: tc.observation, err: tc.observeErr} + if tc.change != nil { + tc.change(&observer.observation) + } + session := repairSession{ctx: t.Context(), service: Service{NativeObserver: observer}, input: AddInput{Scope: domain.ScopeProject, Client: domain.DetectedClient{ClientID: domain.ClientCline}}, client: domain.ClientBinding{NativeObjects: []domain.NativeObjectOwnership{{Kind: "managed_package_directory", ManagedDigest: owned}}}, verifyErr: tc.verify} + err := session.verifyProjectRepairOwnership() + if (err == nil) != tc.want || observer.prepared != 1 || observer.native != 0 { + t.Fatalf("admission=%v want=%t prepared=%d native=%d", err, tc.want, observer.prepared, observer.native) + } + }) + } +} +func TestProjectRepairAdmissionRequiresPreparedCapability(t *testing.T) { + for _, kind := range []string{"nil", "native-only", "profile-wrapped-fallback"} { + t.Run(kind, func(t *testing.T) { + session := repairSession{ctx: t.Context(), input: AddInput{Scope: domain.ScopeProject, Client: domain.DetectedClient{ClientID: domain.ClientCline}}, verifyErr: &ports.VerificationError{Kind: ports.VerificationAbsent}} + switch kind { + case "native-only": + session.service.NativeObserver = projectRepairNativeOnly{} + case "profile-wrapped-fallback": + session.service.NativeObserver = profileObserver{NativeIdentityObserver: projectRepairNativeOnly{}, check: func() error { return nil }, fallback: func(context.Context, domain.DeliveryPlan, *domain.ClientBinding) (domain.NativeIdentityObservation, error) { + return domain.NativeIdentityObservation{State: domain.NativeIdentityAbsent}, nil + }} + } + if err := session.verifyProjectRepairOwnership(); err == nil { + t.Fatal("package-only compatibility fallback admitted project component repair") + } + }) + } +} diff --git a/install/integrationctl/agentplugins/usecase/repair_session.go b/install/integrationctl/agentplugins/usecase/repair_session.go index 6fa2ee9ed..3970dc1e4 100644 --- a/install/integrationctl/agentplugins/usecase/repair_session.go +++ b/install/integrationctl/agentplugins/usecase/repair_session.go @@ -97,10 +97,10 @@ func (session *repairSession) loadRepairTarget() error { func (session *repairSession) bindRepairClient(physicalID string) error { session.clientKey = domain.ComputeClientBindingID(session.installation.InstallationID, string(session.input.Client.ClientID), string(session.input.Scope), session.plan.ActivePath) client, ok := session.installation.Clients[session.clientKey] - if !ok && sharesPhysicalBackend(session.input.Client.ClientID, session.plan.SelectedDelivery) { + if !ok && sharesPhysicalBackend(session.input.Client.ClientID, session.plan.SelectedDelivery, session.input.Scope) { for key, binding := range session.installation.Clients { if binding.Scope != string(session.input.Scope) || binding.Materialization == domain.MaterializationAbsent || - binding.PhysicalArtifact != session.plan.PhysicalArtifactID || !sameNativeBackend(domain.ClientID(binding.ClientID), session.input.Client.ClientID) { + binding.PhysicalArtifact != session.plan.PhysicalArtifactID || !sameNativeBackend(domain.ClientID(binding.ClientID), session.input.Client.ClientID, session.input.Scope) { continue } session.clientKey, client, ok = key, binding, true @@ -113,11 +113,11 @@ func (session *repairSession) bindRepairClient(physicalID string) error { if !ok || (client.Materialization != domain.MaterializationMaterialized && client.Materialization != domain.MaterializationDegraded) { return fmt.Errorf("plugin is not materialized for %s", session.input.Client.ClientID) } - if client.ClientBindingID != session.clientKey || !sameNativeBackend(domain.ClientID(client.ClientID), session.input.Client.ClientID) || + if client.ClientBindingID != session.clientKey || !sameNativeBackend(domain.ClientID(client.ClientID), session.input.Client.ClientID, session.input.Scope) || client.Scope != string(session.input.Scope) || client.PhysicalArtifact != physicalID { return fmt.Errorf("managed repair target identity does not match the selected binding") } - if err := validateNativeBinding(client, session.input.Client); err != nil { + if err := session.service.validateNativeBinding(session.ctx, client, session.input.Client); err != nil { return err } session.client = client @@ -158,7 +158,7 @@ func (session *repairSession) verifyRepairPreconditions() error { return fmt.Errorf("managed package digest is missing; refusing repair") } session.verifyErr = session.service.Stager.Verify(session.ctx, target.ActivePath, session.expectedDigest) - return nil + return session.verifyProjectRepairOwnership() } func (session *repairSession) persistRepair(client domain.ClientBinding) error { diff --git a/install/integrationctl/agentplugins/usecase/selected_delivery.go b/install/integrationctl/agentplugins/usecase/selected_delivery.go index 04df6f4dd..4b53520b0 100644 --- a/install/integrationctl/agentplugins/usecase/selected_delivery.go +++ b/install/integrationctl/agentplugins/usecase/selected_delivery.go @@ -16,7 +16,7 @@ func validateSelectedPlan(plan *domain.DeliveryPlan, envelope domain.PackageEnve return nil } for _, binding := range installation.Clients { - if binding.Scope != string(plan.Scope) || !sameNativeBackend(domain.ClientID(binding.ClientID), plan.ClientID) { + if binding.Scope != string(plan.Scope) || !sameNativeBackend(domain.ClientID(binding.ClientID), plan.ClientID, plan.Scope) { continue } if err := binding.SelectedDelivery.Validate(); err != nil { diff --git a/install/integrationctl/agentplugins/usecase/service.go b/install/integrationctl/agentplugins/usecase/service.go index 30caaaf8c..fedbb12a5 100644 --- a/install/integrationctl/agentplugins/usecase/service.go +++ b/install/integrationctl/agentplugins/usecase/service.go @@ -17,6 +17,8 @@ import ( ) type Service struct { + CheckNamespace func() error `json:"-"` + ProjectRoot string `json:"-"` environmentAuthority *domain.EnvironmentAuthority ClientPreparation ClientPreparation // OpenCodeHosts retains the explicit legacy preparation seam for callers diff --git a/install/integrationctl/research/vscodeworkspace/authority_linux_test.go b/install/integrationctl/research/vscodeworkspace/authority_linux_test.go new file mode 100644 index 000000000..58322431f --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/authority_linux_test.go @@ -0,0 +1,259 @@ +//go:build linux + +package main + +import ( + "net" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + "testing" +) + +func TestFixtureAncestorsHaveExplicitSealedOrWritableRoles(t *testing.T) { + base := t.TempDir() + if err := os.Chmod(base, 0755); err != nil { + t.Fatal(err) + } + c := &controller{request: sealedRequest{Base: base, Input: inputRequest{UID: os.Getuid(), GID: os.Getgid()}}} + if err := c.fixtures(); err != nil { + t.Fatal(err) + } + // The builder UID is root in the real guarded controller. Running this pure + // fixture test as an ordinary CI user still checks actual owner roles/modes. + roles := map[string]os.FileMode{base: 0755, filepath.Join(base, "probes"): 0755, filepath.Join(base, "home"): 0755} + for _, label := range probeLabels { + root := filepath.Join(base, "probes", label) + roles[root] = 0755 + for _, leaf := range []string{"profile", "extensions", "results"} { + roles[filepath.Join(root, leaf)] = 0700 + } + roles[filepath.Join(root, "profile", "User")] = 0755 + for _, cache := range []string{"globalStorage", "workspaceStorage", "History", "snippets"} { + roles[filepath.Join(root, "profile", "User", cache)] = 0700 + } + } + for path, want := range roles { + info, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + uid, err := statUID(info) + if err != nil || uid != uint32(os.Getuid()) || !info.IsDir() || info.Mode().Perm() != want { + t.Fatalf("native path authority %s: uid=%d mode=%v error=%v", path, uid, info.Mode(), err) + } + if want == 0755 && info.Mode().Perm()&0005 != 0005 { + t.Fatal("sealed ancestor is not traversable", path) + } + } +} +func TestImmutableDirectoryNeedsOwnerModesAndNarrowStickyException(t *testing.T) { + path := t.TempDir() + owner := uint32(os.Getuid()) + for _, tc := range []struct { + mode os.FileMode + sticky, allowed bool + }{{0755, false, true}, {0777, false, false}, {0777 | os.ModeSticky, false, false}, {0777 | os.ModeSticky, true, true}} { + if err := os.Chmod(path, tc.mode); err != nil { + t.Fatal(err) + } + info, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + if immutableDirectory(info, owner, tc.sticky) != tc.allowed || immutableDirectory(info, owner+1, tc.sticky) { + t.Fatalf("owner/mode authority mismatch mode=%v", info.Mode()) + } + } +} +func TestReadyGateWaitsForCompleteSingleLinkAndPreservesForeignLeaves(t *testing.T) { + root := t.TempDir() + final := filepath.Join(root, "observation.json") + pending := filepath.Join(root, ".observation.pending") + ready := filepath.Join(root, "observation.ready") + if err := requireFreshResultPaths(final); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(pending, []byte(`{"partial":`), 0600); err != nil { + t.Fatal(err) + } + if _, ok, err := admitReadyResult(final); err != nil || ok { + t.Fatalf("pending became observation %t %v", ok, err) + } + if err := os.WriteFile(pending, []byte(`{"complete":true}`), 0600); err != nil { + t.Fatal(err) + } + if err := os.Link(pending, final); err != nil { + t.Fatal(err) + } + if _, ok, err := admitReadyResult(final); err != nil || ok { + t.Fatalf("two-link intermediate admitted %t %v", ok, err) + } + if err := os.Remove(pending); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(ready, nil, 0600); err != nil { + t.Fatal(err) + } + body, ok, err := admitReadyResult(final) + if err != nil || !ok || string(body) != `{"complete":true}` { + t.Fatalf("final not admitted %q %t %v", body, ok, err) + } + if err := requireFreshResultPaths(final); err == nil { + t.Fatal("preexisting protocol adopted") + } + if err := os.Remove(ready); err != nil { + t.Fatal(err) + } + if err := os.Symlink(final, ready); err != nil { + t.Fatal(err) + } + if _, _, err := admitReadyResult(final); err == nil { + t.Fatal("aliased ready adopted") + } + if string(mustReadInert(t, final)) != `{"complete":true}` { + t.Fatal("foreign final was modified") + } +} +func mustReadInert(t *testing.T, path string) []byte { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return body +} +func TestObserverPinsMatchEveryExactCopiedSource(t *testing.T) { + root := filepath.Join("..", "..", "..", "..", "scripts", "vscode-workspace-observer") + if len(observerPins) != 4 { + t.Fatal("observer closure differs") + } + for _, name := range []string{"package.json", "extension.cjs", "shape.cjs", "runner.cjs"} { + digest, err := hashFile(filepath.Join(root, name), 1<<20) + if err != nil || observerPins[name] != digest { + t.Fatalf("observer source pin mismatch %s %v", name, err) + } + } +} +func TestDisplayMetadataRejectsWrongPIDOwnerTypeAndReplacedAsset(t *testing.T) { + root, err := os.MkdirTemp("/tmp", "uap-vsc-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll(root); err != nil { + t.Error(err) + } + }) + lock := filepath.Join(root, "lock") + body := []byte(strconv.Itoa(os.Getpid()) + "\n") + if err := os.WriteFile(lock, body, 0600); err != nil { + t.Fatal(err) + } + listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", filepath.Join(root, "private-inert-socket")) + if err != nil { + t.Fatal(err) + } + defer func() { _ = listener.Close() }() + lockInfo, err := os.Lstat(lock) + if err != nil { + t.Fatal(err) + } + socketInfo, err := os.Lstat(filepath.Join(root, "private-inert-socket")) + if err != nil { + t.Fatal(err) + } + proof := isolationProof{PID: os.Getpid(), UID: os.Getuid()} + if err := validateDisplayReady(body, lockInfo, socketInfo, proof); err != nil { + t.Fatal(err) + } + bad := proof + bad.PID++ + if validateDisplayReady(body, lockInfo, socketInfo, bad) == nil { + t.Fatal("foreign server PID admitted") + } + bad = proof + bad.UID++ + if validateDisplayReady(body, lockInfo, socketInfo, bad) == nil { + t.Fatal("foreign server owner admitted") + } + if validateDisplayReady(body, lockInfo, lockInfo, proof) == nil { + t.Fatal("ordinary file became display socket") + } + held, err := holdDisplayAsset(lock, lockInfo) + if err != nil { + t.Fatal(err) + } + defer func() { _ = held.file.Close() }() + if err := os.Remove(lock); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(lock, body, 0600); err != nil { + t.Fatal(err) + } + replacement, err := os.Lstat(lock) + if err != nil { + t.Fatal(err) + } + if sameFile(lockInfo, replacement) { + t.Fatal("replacement retained display-unlink authority") + } + if held.verify(lock) == nil || os.SameFile(lockInfo, replacement) { + t.Fatal("replacement reused a retained kernel identity") + } + if string(mustReadInert(t, lock)) != string(body) { + t.Fatal("replacement bytes changed") + } +} +func TestFixedTrustedCommandsKeepIsolatedLiteralSyntaxAndClosedOperands(t *testing.T) { + base := "/tmp/uap-vscode-sealed-inert" + args := []string{"-I", "-c", cleanupCode, base, "1", "2"} + cmd, err := fixedSystemCommand(t.Context(), "/usr/bin/python3", args) + if err != nil || !reflect.DeepEqual(cmd.Args, append([]string{"/usr/bin/python3"}, args...)) { + t.Fatalf("fixed cleanup argv %v %v", cmd, err) + } + args[2] = "unreviewed script" + if _, err := fixedSystemCommand(t.Context(), "/usr/bin/python3", args); err == nil { + t.Fatal("unreviewed cleanup code admitted") + } + for _, path := range []string{"/usr/bin/bash", "/bin/python3"} { + if _, err := fixedSystemCommand(t.Context(), path, nil); err == nil { + t.Fatal("unreviewed program admitted", path) + } + } + if _, err := fixedSystemCommand(t.Context(), "/usr/bin/xauth", []string{"-f", base + "/auth/authority", "source", "-"}); err != nil { + t.Fatal(err) + } + if _, err := fixedSystemCommand(t.Context(), "/usr/bin/xauth", []string{"-f", "/tmp/foreign", "source", "-"}); err == nil { + t.Fatal("foreign xauth operand admitted") + } +} +func TestWatchedFixtureParentAliasRefusesDespiteUnchangedOrdinaryLeaf(t *testing.T) { + profile := filepath.Join(t.TempDir(), "profile") + user := filepath.Join(profile, "User") + if err := os.MkdirAll(user, 0755); err != nil { + t.Fatal(err) + } + watched := filepath.Join(user, "settings.json") + body := []byte(`{"chat.mcp.autostart":"never"}`) + if err := os.WriteFile(watched, body, 0444); err != nil { + t.Fatal(err) + } + if _, err := exactDirectory(filepath.Dir(watched), os.Getuid()); err != nil { + t.Fatal("ordinary fixture ancestry refused", err) + } + if err := os.Rename(user, user+"-original"); err != nil { + t.Fatal(err) + } + if err := os.Symlink("User-original", user); err != nil { + t.Fatal(err) + } + if string(mustReadInert(t, watched)) != string(body) { + t.Fatal("alias fixture changed leaf bytes") + } + if _, err := snapshot([]string{watched}); err == nil || !strings.Contains(err.Error(), "directory ancestor is not ordinary") { + t.Fatalf("watched file parent alias admitted or refused for the wrong reason: %v", err) + } +} diff --git a/install/integrationctl/research/vscodeworkspace/bootstrap_linux.go b/install/integrationctl/research/vscodeworkspace/bootstrap_linux.go new file mode 100644 index 000000000..69769e21d --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/bootstrap_linux.go @@ -0,0 +1,494 @@ +//go:build linux + +package main + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "golang.org/x/sys/unix" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/adapters/process" +) + +var shaPattern = regexp.MustCompile("^[0-9a-f]{64}$") +var commitPattern = regexp.MustCompile("^[0-9a-f]{40}$") + +func hostedRoot() error { + for key, want := range map[string]string{"GITHUB_ACTIONS": "true", "RUNNER_ENVIRONMENT": "github-hosted", "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED": "1"} { + if os.Getenv(key) != want { + return fmt.Errorf("genuine disposable hosted admission required") + } + } + if os.Getuid() != 0 || os.Geteuid() != 0 || !commitPattern.MatchString(buildCommit) || os.Getenv("EXPECTED_COMMIT") != buildCommit { + return fmt.Errorf("fixed exact-source privileged controller required") + } + return nil +} +func callerIDs() (int, int, error) { + uid, e := strconv.Atoi(os.Getenv("SUDO_UID")) + gid, ge := strconv.Atoi(os.Getenv("SUDO_GID")) + if e != nil || ge != nil || uid <= 0 || gid <= 0 || uid > 1<<30 || gid > 1<<30 { + return 0, 0, fmt.Errorf("trusted original sudo caller required") + } + return uid, gid, nil +} +func decodeInput(root string) (inputRequest, error) { + var req inputRequest + uid, gid, err := callerIDs() + if err != nil { + return req, err + } + info, err := exactDirectory(root, uid) + if err != nil { + return req, err + } + if filepath.Dir(root) != "/tmp" || !strings.HasPrefix(filepath.Base(root), "vscode-workspace-input-") || info.Mode().Perm() != 0700 { + return req, fmt.Errorf("task-only input authority refused") + } + body, err := readOrdinary(filepath.Join(root, "request.json"), 16<<10) + if err != nil { + return req, err + } + if _, err = uniqueJSON(body); err != nil { + return req, err + } + dec := json.NewDecoder(strings.NewReader(string(body))) + dec.DisallowUnknownFields() + if err = dec.Decode(&req); err != nil { + return req, err + } + if req.UID != uid || req.EUID != uid || req.GID != gid || req.EGID != gid || req.SourceCommit != buildCommit || !shaPattern.MatchString(req.HelperSHA) { + return req, fmt.Errorf("request identity or source differs from trusted caller") + } + deadline, err := deadlineAt(req.StartNS, 420*time.Second) + if err != nil || time.Until(deadline) <= 0 { + return req, fmt.Errorf("work budget exhausted before initialization") + } + if _, err = exactDirectory(req.ObserverRoot, uid); err != nil { + return req, err + } + if _, err = exactDirectory(req.OutputRoot, uid); err != nil { + return req, err + } + return req, nil +} +func trustedSystem(path string) error { + canonical, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + if !filepath.IsAbs(canonical) { + return fmt.Errorf("absolute system executable required") + } + info, err := os.Lstat(canonical) + if err != nil { + return err + } + uid, err := statUID(info) + if err != nil || uid != 0 || !regular(info, 128<<20) || info.Mode().Perm()&022 != 0 || info.Mode().Perm()&0111 == 0 { + return fmt.Errorf("ordinary root-owned system executable required") + } + return immutableAncestors(filepath.Dir(canonical)) +} +func bootstrap(root string) (resultErr error) { + req, err := decodeInput(root) + if err != nil { + return err + } + deadline, err := deadlineAt(req.StartNS, 420*time.Second) + if err != nil { + return err + } + ctx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + base, err := os.MkdirTemp("/tmp", "uap-vscode-sealed-") + if err != nil { + return err + } + failed := true + defer func() { + if failed { + resultErr = errors.Join(resultErr, cleanSealed(base)) + } + }() + if err = prepareSealed(ctx, root, base, req); err != nil { + return err + } + env := []string{"PATH=/usr/bin:/bin", "LANG=C.UTF-8", "GITHUB_ACTIONS=true", "RUNNER_ENVIRONMENT=github-hosted", "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED=1", "EXPECTED_COMMIT=" + buildCommit, "SUDO_UID=" + strconv.Itoa(req.UID), "SUDO_GID=" + strconv.Itoa(req.GID)} + failed = false + err = unix.Exec(filepath.Join(base, "control", "controller"), []string{filepath.Join(base, "control", "controller"), "--sealed-root", base}, env) + failed = true + return err +} +func prepareSealed(ctx context.Context, root, base string, req inputRequest) error { + if err := os.Mkdir(filepath.Join(base, "control"), 0700); err != nil { + return err + } + if err := os.Mkdir(filepath.Join(base, "publisher"), 0755); err != nil { + return err + } + hashes, err := copyPublisher(ctx, filepath.Join(root, "publisher", publisherPrefix), filepath.Join(base, "publisher", publisherPrefix), req.UID) + if err != nil { + return err + } + if err := copyObserver(ctx, req.ObserverRoot, base, req.UID); err != nil { + return err + } + source, err := os.Executable() + if err != nil { + return err + } + digest, err := copyOrdinary(ctx, source, filepath.Join(base, "control", "controller"), req.UID, true) + if err != nil { + return err + } + if digest != req.HelperSHA { + return fmt.Errorf("controller binary digest differs") + } + body, err := encode(sealedRequest{Input: req, Base: base, ExecutableHashes: hashes}, 16<<10) + if err != nil { + return err + } + if err = os.WriteFile(filepath.Join(base, "control", "request.json"), body, 0600); err != nil { + return err + } + return os.Chmod(base, 0755) +} +func copyObserver(ctx context.Context, source, base string, uid int) error { + if len(observerPins) != 4 { + return fmt.Errorf("exact reviewed observer pins missing") + } + observer := filepath.Join(base, "observer") + if err := os.Mkdir(observer, 0755); err != nil { + return err + } + for name, want := range observerPins { + if filepath.Base(name) != name || !shaPattern.MatchString(want) { + return fmt.Errorf("observer pin is not closed") + } + digest, err := copyOrdinary(ctx, filepath.Join(source, name), filepath.Join(observer, name), uid, false) + if err != nil { + return err + } + if digest != want { + return fmt.Errorf("observer source digest differs") + } + } + return nil +} + +const cleanupCode = ` +import os,stat,sys +path,device,inode=sys.argv[1:] +parent,name=os.path.split(path) +pfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW) +root=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd) +identity=os.fstat(root) +if (identity.st_dev,identity.st_ino)!=(int(device),int(inode)):raise SystemExit("cleanup root identity changed") +def clear(fd): + for entry in os.listdir(fd): + item=os.stat(entry,dir_fd=fd,follow_symlinks=False) + if stat.S_ISDIR(item.st_mode): + child=os.open(entry,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd) + try: + if not os.path.samestat(item,os.fstat(child)):raise RuntimeError("cleanup directory changed") + clear(child) + finally:os.close(child) + os.rmdir(entry,dir_fd=fd) + else:os.unlink(entry,dir_fd=fd) +clear(root) +if not os.path.samestat(identity,os.stat(name,dir_fd=pfd,follow_symlinks=False)):raise SystemExit("cleanup root replaced") +os.close(root) +os.rmdir(name,dir_fd=pfd) +os.close(pfd) +` + +type liveBuffer struct { + mu sync.Mutex + body []byte + limit int + overflow bool + cancel context.CancelFunc +} + +func (b *liveBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + n := len(p) + allow := b.limit - len(b.body) + if n > allow { + b.body = append(b.body, p[:allow]...) + b.overflow = true + b.cancel() + return allow, fmt.Errorf("bounded child output exceeded") + } + b.body = append(b.body, p...) + return n, nil +} +func (b *liveBuffer) bytes() []byte { + b.mu.Lock() + defer b.mu.Unlock() + return append([]byte(nil), b.body...) +} +func boundedSystem(parent context.Context, path string, args []string, input []byte, limit int, timeout time.Duration) ([]byte, []byte, error) { + if err := trustedSystem(path); err != nil { + return nil, nil, err + } + ctx, cancel := context.WithTimeout(parent, timeout) + defer cancel() + out, stderr := &liveBuffer{limit: limit, cancel: cancel}, &liveBuffer{limit: limit, cancel: cancel} + cmd, err := fixedSystemCommand(ctx, path, args) + if err != nil { + return nil, nil, err + } + cmd.Dir = "/" + cmd.Env = []string{"PATH=/usr/bin:/bin", "LANG=C.UTF-8"} + cmd.Stdin = strings.NewReader(string(input)) + cmd.Stdout = out + cmd.Stderr = stderr + cmd.WaitDelay = 100 * time.Millisecond + err = cmd.Run() + if ctx.Err() != nil { + err = errors.Join(err, ctx.Err()) + } + return out.bytes(), stderr.bytes(), err +} +func fixedSystemCommand(ctx context.Context, path string, args []string) (*exec.Cmd, error) { + switch path { + case "/usr/bin/python3": + if len(args) != 6 || args[0] != "-I" || args[1] != "-c" || args[2] != cleanupCode || !sealedRootOperand(args[3]) || !decimalOperand(args[4]) || !decimalOperand(args[5]) { + return nil, fmt.Errorf("cleanup argv is outside the fixed table") + } + cmd := exec.CommandContext(ctx, "/usr/bin/python3", "-I", "-c", cleanupCode) + cmd.Args = append(cmd.Args, args[3], args[4], args[5]) + return cmd, nil + case "/usr/bin/xauth": + if len(args) != 4 || args[0] != "-f" || args[2] != "source" || args[3] != "-" || filepath.Base(args[1]) != "authority" || filepath.Base(filepath.Dir(args[1])) != "auth" || !sealedRootOperand(filepath.Dir(filepath.Dir(args[1]))) { + return nil, fmt.Errorf("xauth argv is outside the fixed table") + } + cmd := exec.CommandContext(ctx, "/usr/bin/xauth", "-f") + cmd.Args = append(cmd.Args, args[1], "source", "-") + return cmd, nil + default: + return nil, fmt.Errorf("system command is outside the fixed table") + } +} +func sealedRootOperand(path string) bool { + return filepath.IsAbs(path) && filepath.Clean(path) == path && filepath.Dir(path) == "/tmp" && strings.HasPrefix(filepath.Base(path), "uap-vscode-sealed-") +} +func decimalOperand(value string) bool { + number, err := strconv.ParseUint(value, 10, 64) + return err == nil && strconv.FormatUint(number, 10) == value +} +func cleanSealedCapture(base string) ([]byte, []byte, error) { + info, err := exactDirectory(base, 0) + if err != nil { + return nil, nil, err + } + if !sealedRootOperand(base) { + return nil, nil, fmt.Errorf("cleanup authority refused") + } + st, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return nil, nil, fmt.Errorf("cleanup Linux identity unavailable") + } + return boundedSystem(context.Background(), "/usr/bin/python3", []string{"-I", "-c", cleanupCode, base, strconv.FormatUint(st.Dev, 10), strconv.FormatUint(st.Ino, 10)}, nil, controlLimit, 4500*time.Millisecond) +} +func cleanSealed(base string) error { + out, stderr, err := cleanSealedCapture(base) + if err != nil { + return fmt.Errorf("held-root cleanup failed: %w; stdout=%q stderr=%q", err, out, stderr) + } + return nil +} +func randomCookie() (string, error) { + var body [16]byte + if _, err := rand.Read(body[:]); err != nil { + return "", err + } + return hex.EncodeToString(body[:]), nil +} +func runSealed(base string) (err error) { + if !sealedRootOperand(base) { + return fmt.Errorf("sealed task root refused") + } + if _, err = exactDirectory(base, 0); err != nil { + return err + } + if err = immutableAncestors(base); err != nil { + return err + } + cleaned := false + defer func() { + if !cleaned { + err = errors.Join(err, cleanSealed(base)) + } + }() + req, err := loadSealedRequest(base) + if err != nil { + return err + } + work, cancel, final, err := sealedWork(req) + if err != nil { + return err + } + defer cancel() + c, err := newSealedController(work, req, cancel, final) + if err != nil { + return err + } + defer func() { _ = c.output.Close() }() + runErr := c.execute() + out, stderr, baseErr := cleanSealedCapture(base) + cleanupErr := errors.Join(baseErr, c.displayCleanupErr) + cleaned = true + return c.finish(runErr, cleanupErr, out, stderr) +} +func loadSealedRequest(base string) (sealedRequest, error) { + body, err := readOrdinary(filepath.Join(base, "control", "request.json"), 16<<10) + if err != nil { + return sealedRequest{}, err + } + var req sealedRequest + decoder := json.NewDecoder(strings.NewReader(string(body))) + decoder.DisallowUnknownFields() + if err = decoder.Decode(&req); err != nil { + return sealedRequest{}, err + } + uid, gid, err := callerIDs() + if err != nil || req.Base != base || req.Input.UID != uid || req.Input.GID != gid || req.Input.SourceCommit != buildCommit { + return sealedRequest{}, fmt.Errorf("sealed request identity differs") + } + return req, nil +} +func sealedWork(req sealedRequest) (context.Context, context.CancelFunc, time.Time, error) { + final, err := deadlineAt(req.Input.StartNS, 440*time.Second) + if err != nil { + return nil, nil, time.Time{}, err + } + deadline, err := deadlineAt(req.Input.StartNS, 420*time.Second) + if err != nil { + return nil, nil, time.Time{}, err + } + work, cancel := context.WithDeadline(context.Background(), deadline) + go func() { var one [1]byte; _, _ = os.Stdin.Read(one[:]); cancel() }() + signals, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + go func() { + select { + case <-signals.Done(): + cancel() + case <-work.Done(): + } + }() + return work, func() { cancel(); stop() }, final, nil +} +func newSealedController(work context.Context, req sealedRequest, cancel context.CancelFunc, final time.Time) (*controller, error) { + fd, err := unix.Open(req.Input.OutputRoot, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) + if err != nil { + return nil, err + } + output := os.NewFile(uintptr(fd), req.Input.OutputRoot) + c := &controller{request: req, deadline: final, work: work, cancelWork: cancel, runner: process.OS{}, files: map[string][]byte{}, output: output, hashes: req.ExecutableHashes} + var ns unix.Stat_t + if err := unix.Stat("/proc/self/ns/net", &ns); err != nil { + _ = output.Close() + return nil, err + } + c.parentNS = ns.Ino + return c, nil +} +func (c *controller) finish(runErr, cleanupErr error, out, stderr []byte) error { + if e := c.keep("root-cleanup.stdout.log", out); e != nil { + cleanupErr = errors.Join(cleanupErr, e) + } + if e := c.keep("root-cleanup.stderr.log", stderr); e != nil { + cleanupErr = errors.Join(cleanupErr, e) + } + outHash, errHash := sha256.Sum256(out), sha256.Sum256(stderr) + summary := map[string]any{"evidence_kind": "vscode-pinned-untrusted-workspace-research", "source_commit": buildCommit, "version": publisherVersion, "publisher_commit": publisherCommit, "collection_complete": runErr == nil && cleanupErr == nil, "native_project_qualified": false, "native_mcp_initialization_qualified": false, "native_runtime_qualified": false, "children": c.children, "observations": c.observations, "executable_hashes": c.hashes, "error": clipped(runErr), "cleanup": map[string]any{"complete": cleanupErr == nil, "error": clipped(cleanupErr), "stdout_file": "root-cleanup.stdout.log", "stderr_file": "root-cleanup.stderr.log", "stdout_bytes": len(out), "stderr_bytes": len(stderr), "stdout_sha256": hex.EncodeToString(outHash[:]), "stderr_sha256": hex.EncodeToString(errHash[:])}, "diagnostics": "verifier-prefixed merged streams; planned teardown may truncate trailing diagnostics; API result is separate"} + body, e := encode(summary, 1<<20) + if e != nil { + return errors.Join(runErr, cleanupErr, e) + } + if e = c.keep("vscode-workspace-controller.json", body); e != nil { + return errors.Join(runErr, cleanupErr, e) + } + if e = c.publish(); e != nil { + return errors.Join(runErr, cleanupErr, e) + } + public, e := encode(map[string]any{"collection_complete": runErr == nil && cleanupErr == nil, "cleanup_complete": cleanupErr == nil, "error": clipped(errors.Join(runErr, cleanupErr))}, controlLimit) + if e != nil { + return errors.Join(runErr, cleanupErr, e) + } + _, e = os.Stdout.Write(public) + return errors.Join(runErr, cleanupErr, e) +} +func (c *controller) publish() error { + before, err := c.output.Stat() + if err != nil { + return err + } + current, err := os.Lstat(c.request.Input.OutputRoot) + if err != nil || !os.SameFile(before, current) || !current.IsDir() { + return fmt.Errorf("evidence output root changed") + } + for _, name := range sortedKeys(c.files) { + deadline, e := deadlineAt(c.request.Input.StartNS, 450*time.Second) + if e != nil || time.Until(deadline) <= 0 { + return fmt.Errorf("controller publication deadline") + } + fd, err := unix.Openat(int(c.output.Fd()), name, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0600) + if err != nil { + return err + } + f := os.NewFile(uintptr(fd), name) + _, err = f.Write(c.files[name]) + if err == nil { + err = f.Chown(c.request.Input.UID, c.request.Input.GID) + } + closeErr := f.Close() + if err != nil || closeErr != nil { + return errors.Join(err, closeErr) + } + } + return nil +} +func main() { + if err := hostedRoot(); err != nil { + fmt.Fprintln(os.Stderr, clipped(err)) + os.Exit(2) + } + var err error + if len(os.Args) != 3 { + err = fmt.Errorf("fixed controller entry required") + } else { + switch os.Args[1] { + case "--input-root": + err = bootstrap(os.Args[2]) + case "--sealed-root": + err = runSealed(os.Args[2]) + default: + err = fmt.Errorf("fixed controller entry required") + } + } + if err != nil { + fmt.Fprintln(os.Stderr, clipped(err)) + os.Exit(1) + } +} diff --git a/install/integrationctl/research/vscodeworkspace/capture_linux.go b/install/integrationctl/research/vscodeworkspace/capture_linux.go new file mode 100644 index 000000000..669c19df6 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/capture_linux.go @@ -0,0 +1,248 @@ +//go:build linux + +package main + +import ( + "bufio" + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strconv" + "sync" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +const proofPrefix = "UAP_VSCODE_ISOLATION_V1 " +const isolationVerifier = ` +import json,os,socket,sys +parent,uid,gid,env,argv=sys.argv[1:] +p={"pid":os.getpid(),"network_namespace":os.stat("/proc/self/ns/net").st_ino,"parent_network_namespace":int(parent),"uid":os.getuid(),"gid":os.getgid(),"euid":os.geteuid(),"egid":os.getegid(),"groups":os.getgroups(),"interfaces":[name for _,name in socket.if_nameindex()]} +if p["network_namespace"]==int(parent) or p["uid"]!=int(uid) or p["gid"]!=int(gid) or p["uid"]==0 or p["gid"]==0 or p["euid"]!=int(uid) or p["egid"]!=int(gid) or p["groups"] or p["interfaces"]!=["lo"]:raise SystemExit("isolated process admission failed") +with open("/proc/self/status") as f:status=dict(line.rstrip().split(":",1) for line in f if ":" in line) +if any(int(status[k].strip(),16) for k in ("CapInh","CapPrm","CapEff","CapBnd","CapAmb")) or status["NoNewPrivs"].strip()!="1":raise SystemExit("native privilege drop failed") +p["capabilities_zero"]=True;p["no_new_privileges"]=True +os.write(1,("UAP_VSCODE_ISOLATION_V1 "+json.dumps(p,separators=(",",":"))+"\n").encode()) +os.dup2(1,2) +os.execve(json.loads(argv)[0],json.loads(argv),json.loads(env)) +` + +type mergedCapture struct { + mu sync.Mutex + body []byte + overflow bool + err error + proof isolationProof + ready chan struct{} + done chan struct{} + cancel context.CancelFunc + planned bool +} + +func (m *mergedCapture) append(p []byte) bool { + m.mu.Lock() + defer m.mu.Unlock() + remaining := streamLimit - len(m.body) + if len(p) > remaining { + m.body = append(m.body, p[:remaining]...) + m.overflow = true + m.err = fmt.Errorf("merged diagnostic limit exceeded") + m.cancel() + return false + } + m.body = append(m.body, p...) + return true +} +func (m *mergedCapture) fail(err error) { + m.mu.Lock() + m.err = errors.Join(m.err, err) + m.mu.Unlock() + m.cancel() +} +func (m *mergedCapture) drain(reader io.Reader, uid, gid int, parent uint64) { + defer close(m.done) + r := bufio.NewReaderSize(reader, 4097) + line, err := r.ReadSlice('\n') + m.append(line) + if err != nil || len(line) > 4096 || !bytes.HasPrefix(line, []byte(proofPrefix)) { + m.fail(errors.Join(fmt.Errorf("same-process proof frame missing/oversized"), err)) + close(m.ready) + return + } + body := bytes.TrimSuffix(line[len(proofPrefix):], []byte{'\n'}) + var proof isolationProof + if _, err = uniqueJSON(body); err == nil { + err = json.Unmarshal(body, &proof) + } + m.mu.Lock() + m.proof = proof + m.mu.Unlock() + if proofErr := validateIsolationProof(proof, uid, gid, parent, err); proofErr != nil { + m.fail(proofErr) + close(m.ready) + return + } + close(m.ready) + buffer := make([]byte, 64<<10) + for { + n, e := r.Read(buffer) + if n > 0 && !m.append(buffer[:n]) { + return + } + if e != nil { + m.mu.Lock() + planned := m.planned + m.mu.Unlock() + if !errors.Is(e, io.EOF) && (!planned || !errors.Is(e, os.ErrClosed)) { + m.fail(fmt.Errorf("merged diagnostic read failed: %w", e)) + } + return + } + } +} +func validateIsolationProof(proof isolationProof, uid, gid int, parent uint64, err error) error { + if err != nil || proof.PID <= 0 || uid <= 0 || gid <= 0 || parent == 0 || proof.Namespace == 0 || proof.UID != uid || proof.GID != gid || proof.EUID != uid || proof.EGID != gid || proof.Namespace == parent || proof.ParentNamespace != parent || len(proof.Groups) != 0 || !reflect.DeepEqual(proof.Interfaces, []string{"lo"}) || !proof.CapabilitiesZero || !proof.NoNewPrivileges { + return errors.Join(fmt.Errorf("same-process identity/namespace/privilege proof differs"), err) + } + return nil +} +func (c *controller) isolationCommand(argv []string, cwd string, env map[string]string) (ports.Command, error) { + for _, path := range []string{"/usr/bin/unshare", "/usr/bin/setpriv", "/usr/bin/env", "/usr/bin/python3"} { + if err := trustedSystem(path); err != nil { + return ports.Command{}, err + } + } + environment, err := json.Marshal(env) + if err != nil { + return ports.Command{}, err + } + args, err := json.Marshal(argv) + if err != nil { + return ports.Command{}, err + } + command := []string{"/usr/bin/unshare", "--net", "--", "/usr/bin/setpriv", "--reuid=" + strconv.Itoa(c.request.Input.UID), "--regid=" + strconv.Itoa(c.request.Input.GID), "--clear-groups", "--bounding-set=-all", "--inh-caps=-all", "--ambient-caps=-all", "--no-new-privs", "/usr/bin/env", "-i", "/usr/bin/python3", "-I", "-c", isolationVerifier, strconv.FormatUint(c.parentNS, 10), strconv.Itoa(c.request.Input.UID), strconv.Itoa(c.request.Input.GID), string(environment), string(args)} + return ports.Command{Argv: command, Env: []string{"PATH=/usr/bin:/bin", "LANG=C.UTF-8"}, Dir: cwd}, nil +} +func (c *controller) strict(ctx context.Context, label string, argv []string, cwd string, env map[string]string, exchange func(context.Context, isolationProof) error) (childRecord, error) { + if err := ctx.Err(); err != nil { + return childRecord{Label: label, Error: clipped(err)}, err + } + record := childRecord{Label: label, Argv: append([]string(nil), argv...), Cwd: cwd, Diagnostics: "verifier-prefixed merged stdout/stderr; planned teardown may truncate final diagnostic tail"} + command, err := c.isolationCommand(argv, cwd, env) + if err != nil { + return record, err + } + childCtx, cancel := context.WithCancel(ctx) + defer cancel() + capture := &mergedCapture{ready: make(chan struct{}), done: make(chan struct{}), cancel: cancel} + if label == "Xvfb" { + capture.cancel = c.cancelWork + } + started := false + err = c.runner.RunDuplexWithPlannedShutdown(childCtx, command, func(_ io.Writer, r io.Reader) error { + started = true + go capture.drain(r, c.request.Input.UID, c.request.Input.GID, c.parentNS) + select { + case <-capture.ready: + case <-childCtx.Done(): + return childCtx.Err() + } + capture.mu.Lock() + proofErr := capture.err + proof := capture.proof + capture.mu.Unlock() + if proofErr != nil { + return proofErr + } + exchangeErr := exchange(childCtx, proof) + if exchangeErr == nil { + capture.mu.Lock() + capture.planned = true + capture.mu.Unlock() + } + return exchangeErr + }) + if started { + select { + case <-capture.done: + case <-time.After(time.Second): + err = errors.Join(err, fmt.Errorf("merged drain did not join after strict pipe close")) + } + } + return c.finishCapture(label, record, capture, err) +} +func (c *controller) finishCapture(label string, record childRecord, capture *mergedCapture, err error) (childRecord, error) { + capture.mu.Lock() + body := append([]byte(nil), capture.body...) + record.Proof = capture.proof + record.MergedOverflow = capture.overflow + err = errors.Join(err, capture.err) + capture.mu.Unlock() + if c.cookie != "" { + body = bytes.ReplaceAll(body, []byte(c.cookie), []byte("[task-cookie-redacted]")) + record.Diagnostics += "; task-cookie bytes redacted" + } + sum := sha256.Sum256(body) + record.MergedFile = label + ".merged.log" + record.MergedBytes = len(body) + record.MergedSHA = hex.EncodeToString(sum[:]) + record.PlannedStop = err == nil + record.TreeReaped = err == nil + record.Error = clipped(err) + if e := c.keep(record.MergedFile, body); e != nil { + err = errors.Join(err, e) + record.Error = clipped(err) + } + c.children = append(c.children, record) + return record, err +} +func (c *controller) environment(folder, result, profile string) map[string]string { + base := c.request.Base + home := filepath.Join(base, "home") + env := map[string]string{"HOME": home, "USERPROFILE": home, "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", "TMPDIR": filepath.Join(base, "tmp"), "DISPLAY": display, "XAUTHORITY": filepath.Join(base, "auth", "authority"), "UAP_VSCODE_WORKSPACE_FOLDER": folder, "UAP_VSCODE_WORKSPACE_RESULT": result} + for key, dir := range map[string]string{"XDG_CONFIG_HOME": "config", "XDG_CACHE_HOME": "cache", "XDG_STATE_HOME": "state", "XDG_DATA_HOME": "data", "NPM_CONFIG_CACHE": "npm-cache"} { + env[key] = filepath.Join(home, dir) + } + for key, name := range map[string]string{"NPM_CONFIG_USERCONFIG": "npm-user-config", "NPM_CONFIG_GLOBALCONFIG": "npm-global-config", "GIT_CONFIG_GLOBAL": "git-config"} { + env[key] = filepath.Join(home, name) + } + env["GIT_CONFIG_NOSYSTEM"] = "1" + env["GIT_TERMINAL_PROMPT"] = "0" + _ = profile + return env +} +func guiArgv(base, folder, profile string) []string { + return []string{filepath.Join(base, "publisher", publisherPrefix, "code"), "--new-window", "--disable-crash-reporter", "--user-data-dir", profile, "--extensions-dir", filepath.Join(filepath.Dir(profile), "extensions"), "--extensionDevelopmentPath", filepath.Join(base, "observer"), "--extensionTestsPath", filepath.Join(base, "observer", "runner.cjs"), "--enable-proposed-api", "uap-research.workspace-observer", folder} +} +func (c *controller) system(label, path string, args []string, input []byte, redact bool) error { + stdout, stderr, err := boundedSystem(c.work, path, args, input, controlLimit, 5*time.Second) + if redact { + stdout = bytes.ReplaceAll(stdout, []byte(c.cookie), []byte("[task-cookie-redacted]")) + stderr = bytes.ReplaceAll(stderr, []byte(c.cookie), []byte("[task-cookie-redacted]")) + } + if e := c.keep(label+".stdout.log", stdout); e != nil { + err = errors.Join(err, e) + } + if e := c.keep(label+".stderr.log", stderr); e != nil { + err = errors.Join(err, e) + } + outHash, errHash := sha256.Sum256(stdout), sha256.Sum256(stderr) + record := map[string]any{"argv": append([]string{path}, args...), "error": clipped(err), "redacted": redact, "stdout_bytes": len(stdout), "stderr_bytes": len(stderr), "stdout_sha256": hex.EncodeToString(outHash[:]), "stderr_sha256": hex.EncodeToString(errHash[:])} + body, e := encode(record, controlLimit) + if e == nil { + e = c.keep(label+".json", body) + } + return errors.Join(err, e) +} +func fixedResultPath(base, label string) string { + return filepath.Join(base, "probes", label, "results", "observation.json") +} diff --git a/install/integrationctl/research/vscodeworkspace/controller_linux_test.go b/install/integrationctl/research/vscodeworkspace/controller_linux_test.go new file mode 100644 index 000000000..18d98ba91 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/controller_linux_test.go @@ -0,0 +1,307 @@ +//go:build linux + +package main + +import ( + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +func testProof() string { + body, _ := json.Marshal(isolationProof{PID: 123, UID: 1001, GID: 1001, EUID: 1001, EGID: 1001, Namespace: 2, ParentNamespace: 1, Groups: []int{}, Interfaces: []string{"lo"}, CapabilitiesZero: true, NoNewPrivileges: true}) + return proofPrefix + string(body) + "\n" +} + +type inertDuplexRunner struct { + body string + calls int + command ports.Command +} + +func (r *inertDuplexRunner) RunDuplexWithPlannedShutdown(_ context.Context, command ports.Command, exchange func(io.Writer, io.Reader) error) error { + r.calls++ + r.command = command + return exchange(io.Discard, strings.NewReader(r.body)) +} +func testController(t *testing.T, runner strictRunner) *controller { + t.Helper() + work, cancel := context.WithCancel(t.Context()) + t.Cleanup(cancel) + return &controller{request: sealedRequest{Base: "/tmp/uap-vscode-sealed-inert", Input: inputRequest{UID: 1001, GID: 1001}}, parentNS: 1, work: work, cancelWork: cancel, runner: runner, files: map[string][]byte{}} +} +func TestInertStrictExchangeKeepsVerifierFirstLiteralIsolationAndRedaction(t *testing.T) { + runner := &inertDuplexRunner{body: testProof() + "cookie-value\n"} + c := testController(t, runner) + c.cookie = "cookie-value" + record, err := c.strict(t.Context(), "A", []string{"/inert/native-never-executed"}, "/", map[string]string{}, func(_ context.Context, proof isolationProof) error { + if proof.UID != 1001 || proof.Namespace != 2 { + t.Fatal(proof) + } + return nil + }) + if err != nil || runner.calls != 1 || !record.TreeReaped || !record.PlannedStop { + t.Fatalf("fake exchange failed %+v %v", record, err) + } + joined := strings.Join(runner.command.Argv, "\n") + for _, part := range []string{"/usr/bin/unshare", "--net", "--no-new-privs", "--clear-groups", "-I"} { + if !strings.Contains(joined, part) { + t.Fatal("literal isolation part missing", part) + } + } + body := string(c.files["A.merged.log"]) + if strings.Contains(body, "cookie-value") || !strings.Contains(body, "[task-cookie-redacted]") { + t.Fatal("cookie retained", body) + } +} +func TestInertInvalidProofAndCancellationCannotBecomePlannedSuccess(t *testing.T) { + for _, body := range []string{"native-before-proof\n", strings.Repeat("x", 4097), strings.Replace(testProof(), `"network_namespace":2`, `"network_namespace":1`, 1), strings.Replace(testProof(), `"interfaces":["lo"]`, `"interfaces":["eth0"]`, 1)} { + runner := &inertDuplexRunner{body: body} + c := testController(t, runner) + called := false + record, err := c.strict(t.Context(), "A", []string{"/inert"}, "/", map[string]string{}, func(context.Context, isolationProof) error { called = true; return nil }) + if err == nil || called || record.PlannedStop || record.TreeReaped { + t.Fatalf("proof admitted %+v %v", record, err) + } + } + runner := &inertDuplexRunner{} + c := testController(t, runner) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := c.strict(ctx, "A", nil, "/", nil, func(context.Context, isolationProof) error { return nil }); err == nil || runner.calls != 0 { + t.Fatal("canceled exchange dispatched") + } +} +func TestDuplicateAmbiguousTrailingAndOversizedJSONRefuse(t *testing.T) { + for _, body := range []string{`{"a":1,"a":2}`, `{"a":1,"A":2}`, `{} {}`, strings.Repeat("[", 34) + "0" + strings.Repeat("]", 34), "[" + strings.Repeat("0,", 256) + "0]"} { + if _, err := uniqueJSON([]byte(body)); err == nil { + t.Fatal("ambiguous/bounded JSON admitted", body[:min(len(body), 80)]) + } + } + if _, err := uniqueJSON([]byte{0xff}); err == nil { + t.Fatal("invalid UTF-8 admitted") + } + if _, err := uniqueJSON([]byte(`{"a":[1,true,null]}`)); err != nil { + t.Fatal(err) + } +} +func TestOrdinaryFileRejectsAliasesHardlinksAndSparseOverlimit(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "file") + if err := os.WriteFile(path, []byte("raw"), 0600); err != nil { + t.Fatal(err) + } + if body, err := readOrdinary(path, 3); err != nil || string(body) != "raw" { + t.Fatal(err) + } + alias := filepath.Join(root, "alias") + if err := os.Symlink(path, alias); err != nil { + t.Fatal(err) + } + if _, err := readOrdinary(alias, 3); err == nil { + t.Fatal("alias admitted") + } + if err := os.Remove(alias); err != nil { + t.Fatal(err) + } + if err := os.Link(path, alias); err != nil { + t.Fatal(err) + } + if _, err := readOrdinary(path, 3); err == nil { + t.Fatal("hardlink admitted") + } + if err := os.Remove(alias); err != nil { + t.Fatal(err) + } + if err := os.Truncate(path, 1<<30); err != nil { + t.Fatal(err) + } + if _, err := readOrdinary(path, 3); err == nil { + t.Fatal("sparse overlimit admitted") + } +} +func markerSkill(path, name, description string) map[string]any { + return map[string]any{"uri": map[string]any{"value": "file://" + path, "scheme": "file", "authority": "", "path": path}, "name": name, "description": description, "source": "local", "disableModelInvocation": true} +} +func validObservation(base, folder string) map[string]any { + return map[string]any{"schema_version": 1, "collected": true, "version": publisherVersion, "app_root": filepath.Join(base, "publisher", publisherPrefix, "resources", "app"), "app_host": "desktop", "observer": map[string]any{"id": "uap-research.workspace-observer", "mode": 3, "proposals": []string{"mcpServerDefinitions", "chatPromptFiles"}}, "folder": map[string]any{"value": "file://" + folder, "scheme": "file", "path": folder, "authority": ""}, "trusted": false, "policy": map[string]any{"skills_enabled": true, "mcp_access": "all", "mcp_autostart": "never"}, "mcp": []any{}, "skills": []any{}, "native_project_qualified": false, "native_mcp_initialization_qualified": false, "native_runtime_qualified": false} +} +func TestObservedContextPolicyShapesAndQualificationAreClosed(t *testing.T) { + base := "/tmp/uap-vscode-sealed-inert" + folder := filepath.Join(base, "projects", "A") + good := validObservation(base, folder) + body, _ := json.Marshal(good) + m, err := validateObservation(body, base, folder) + if err != nil || m == nil { + t.Fatal(err) + } + for _, kind := range []string{"trusted", "qualification", "policy", "mode", "unknown", "skills", "source", "collected", "stage-error", "stage-error-empty"} { + value := validObservation(base, folder) + switch kind { + case "trusted": + value["trusted"] = true + case "qualification": + value["native_project_qualified"] = true + case "policy": + value["policy"].(map[string]any)["mcp_autostart"] = "newAndOutdated" + case "mode": + value["observer"].(map[string]any)["mode"] = 1 + case "unknown": + value["unreviewed"] = true + case "skills": + value["skills"] = nil + case "source": + skill := markerSkill("/tmp/A", "x", "x") + skill["source"] = "future" + value["skills"] = []any{skill} + case "stage-error": + value["stage_error"] = "contradictory error" + case "stage-error-empty": + value["stage_error"] = "" + case "collected": + value["collected"] = false + value["stage_error"] = "API refused" + } + body, _ = json.Marshal(value) + if _, err := validateObservation(body, base, folder); err == nil { + t.Fatal("invalid native metadata admitted", kind) + } + } +} +func TestAllPhysicalMarkerURIAppearancesBlockContradictoryDuplicates(t *testing.T) { + base := "/tmp/uap-vscode-sealed-inert" + folder := filepath.Join(base, "projects", "A") + owned := filepath.Join(folder, ".github", "skills", "uap-a", "SKILL.md") + home := filepath.Join(base, "home", ".copilot", "skills", "uap-user", "SKILL.md") + for _, kind := range []string{"valid", "owned-name", "owned-description", "user-name", "identical", "sibling"} { + rows := []any{markerSkill(owned, "uap-a", "A"), markerSkill(home, "uap-user", "USER")} + switch kind { + case "owned-name": + rows = append(rows, markerSkill(owned, "wrong", "A")) + case "owned-description": + rows = append(rows, markerSkill(owned, "uap-a", "wrong")) + case "user-name": + rows = append(rows, markerSkill(home, "wrong", "USER")) + case "identical": + rows = append(rows, markerSkill(owned, "uap-a", "A")) + case "sibling": + rows = append(rows, markerSkill(filepath.Join(base, "projects", "B", "foreign", "SKILL.md"), "foreign", "B")) + } + result := metadataAttribution(map[string]any{"skills": rows, "mcp": []any{}}, folder, false) + if (result["skills"] == "passed") != (kind == "valid") { + t.Fatalf("kind%s attribution%v", kind, result) + } + if result["native_project_qualified"] != false { + t.Fatal("metadata became runtime proof") + } + } +} +func TestMonotonicBudgetAndLocalHostedAdmissionFailClosed(t *testing.T) { + for _, start := range []int64{0, -1, 1 << 62} { + if _, err := deadlineAt(start, time.Second); err == nil { + t.Fatal("invalid task clock admitted", start) + } + } + t.Setenv("GITHUB_ACTIONS", "") + if hostedRoot() == nil { + t.Fatal("local host guard admitted") + } +} + +func TestZeroByteArtifactsStillHaveExclusiveNames(t *testing.T) { + c := testController(t, &inertDuplexRunner{}) + if err := c.keep("empty.log", nil); err != nil { + t.Fatal(err) + } + if err := c.keep("empty.log", []byte{}); err == nil { + t.Fatal("zero-byte artifact name reused") + } + if len(c.files) != 1 || c.bytes != 0 { + t.Fatal("duplicate empty artifact changed envelope") + } +} +func TestSnapshotCapacityAdmits256ThenRefuses257WithoutInsertion(t *testing.T) { + entries := map[string]string{} + for i := range 256 { + if err := snapshotCapacity(entries); err != nil { + t.Fatalf("entry%d refused: %v", i+1, err) + } + entries[strings.Repeat("x", i+1)] = "finite fixture entry" + } + if err := snapshotCapacity(entries); err == nil { + t.Fatal("257th entry admitted") + } + if len(entries) != 256 { + t.Fatal("capacity check changed recorded entries") + } +} + +func TestMCPEnvironmentFiniteNumbersAndNullSurviveWireAdmission(t *testing.T) { + base := "/tmp/uap-vscode-sealed-inert" + folder := filepath.Join(base, "projects", "A") + value := validObservation(base, folder) + value["mcp"] = []any{map[string]any{"label": "inert", "kind": "stdio", "command": "/never-run", "args": []any{}, "env": map[string]any{"TEXT": "value", "PORT": json.Number("42"), "FRACTION": json.Number("0.5"), "UNSET": nil}}} + body, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + observed, err := validateObservation(body, base, folder) + if err != nil { + t.Fatal(err) + } + row := observed["mcp"].([]any)[0].(map[string]any) + env := row["env"].(map[string]any) + unset, present := env["UNSET"] + if env["PORT"] != json.Number("42") || env["FRACTION"] != json.Number("0.5") || !present || unset != nil || env["TEXT"] != "value" { + t.Fatal("environment values changed during metadata admission", env) + } +} + +func TestMCPMalformedContainersNumbersAndMixedTransportsRefuse(t *testing.T) { + stdio := func() map[string]any { + return map[string]any{"label": "inert", "kind": "stdio", "command": "/never-run", "args": []any{}, "env": map[string]any{}} + } + http := func() map[string]any { + return map[string]any{"label": "inert", "kind": "http", "uri": map[string]any{"value": "http://127.0.0.1:9/inert", "scheme": "http", "path": "/inert", "authority": "127.0.0.1:9"}, "headers": map[string]any{}} + } + for _, number := range []json.Number{"NaN", "Infinity", "-Infinity", "1e1000"} { + row := stdio() + row["env"] = map[string]any{"KEY": number} + if metadataMCP(row) == nil { + t.Fatal("nonfinite environment number admitted", number) + } + } + for _, key := range []string{"args", "env"} { + row := stdio() + row[key] = nil + if metadataMCP(row) == nil { + t.Fatal("null metadata container admitted", key) + } + } + for _, headers := range []any{nil, map[string]any{"KEY": json.Number("42")}, map[string]any{"KEY": nil}} { + row := http() + row["headers"] = headers + if metadataMCP(row) == nil { + t.Fatal("invalid HTTP header container/value admitted", headers) + } + } + for _, key := range []string{"command", "args", "env", "cwd"} { + row := http() + row[key] = nil + if metadataMCP(row) == nil { + t.Fatal("mixed HTTP/stdio metadata admitted", key) + } + } + row := stdio() + row["headers"] = map[string]any{} + if metadataMCP(row) == nil { + t.Fatal("HTTP headers admitted on stdio metadata") + } +} diff --git a/install/integrationctl/research/vscodeworkspace/display_authority_linux.go b/install/integrationctl/research/vscodeworkspace/display_authority_linux.go new file mode 100644 index 000000000..c197a92c2 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/display_authority_linux.go @@ -0,0 +1,44 @@ +//go:build linux + +package main + +import ( + "fmt" + "os" + "strconv" + "strings" + "syscall" +) + +func validateDisplayReady(lock []byte, lockInfo, socketInfo os.FileInfo, proof isolationProof) error { + pid, err := strconv.Atoi(strings.TrimSpace(string(lock))) + if err != nil || pid <= 0 || pid != proof.PID { + return fmt.Errorf("new display lock PID differs") + } + lockUID, err := statUID(lockInfo) + if err != nil || int64(lockUID) != int64(proof.UID) || !regular(lockInfo, 64) { + return fmt.Errorf("display lock owner/type differs") + } + socketUID, err := statUID(socketInfo) + st, ok := socketInfo.Sys().(*syscall.Stat_t) + if err != nil || int64(socketUID) != int64(proof.UID) || !ok || st.Nlink != 1 || socketInfo.Mode()&os.ModeSocket == 0 { + return fmt.Errorf("new display socket identity differs") + } + return nil +} + +func (c *controller) retainReadyDisplay(lock []byte, lockInfo, socketInfo os.FileInfo, proof isolationProof) error { + if e := validateDisplayReady(lock, lockInfo, socketInfo, proof); e != nil { + return e + } + after, ae := os.Lstat("/tmp/.X11-unix/X93") + if ae != nil || !sameFile(socketInfo, after) { + return fmt.Errorf("display socket changed during readiness") + } + assets, holdErr := holdDisplayAssets("/tmp/.X93-lock", "/tmp/.X11-unix/X93", lockInfo, socketInfo) + if holdErr != nil { + return holdErr + } + c.displayAssets = assets + return nil +} diff --git a/install/integrationctl/research/vscodeworkspace/display_cleanup_linux.go b/install/integrationctl/research/vscodeworkspace/display_cleanup_linux.go new file mode 100644 index 000000000..c5f202b26 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/display_cleanup_linux.go @@ -0,0 +1,109 @@ +//go:build linux + +package main + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + + "golang.org/x/sys/unix" +) + +// SIGKILL does not run Xvfb's own lock/socket cleanup. Remove only the two +// exact stable assets captured from its verified startup, after strict success +// proved its tree reaped. A failed/unproved tree never grants unlink authority. +func (c *controller) cleanupDisplay() error { + for _, path := range []string{"/tmp/.X11-unix/X93", "/tmp/.X93-lock"} { + if err := c.cleanupDisplayAsset(path); err != nil { + return err + } + } + return c.cleanupCreatedDisplayParent() +} +func (c *controller) cleanupDisplayAsset(path string) error { + asset, recorded := c.displayAssets[path] + current, err := os.Lstat(path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + if !recorded || asset.verify(path) != nil || !sameFile(asset.info, current) { + return fmt.Errorf("display asset identity changed; not removed") + } + parent := filepath.Dir(path) + info, err := exactDirectory(parent, 0) + if err != nil { + return err + } + if info.Mode().Perm()&0002 != 0 && info.Mode()&os.ModeSticky == 0 { + return fmt.Errorf("global display parent lacks sticky ownership") + } + fd, err := unix.Open(parent, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) + if err != nil { + return err + } + var held unix.Stat_t + err = unix.Fstatat(fd, filepath.Base(path), &held, unix.AT_SYMLINK_NOFOLLOW) + if err == nil && !sameDisplayStat(held, current) { + err = fmt.Errorf("display unlink identity changed") + } + if err == nil { + err = unix.Unlinkat(fd, filepath.Base(path), 0) + } + closeErr := unix.Close(fd) + if err != nil { + return err + } + if closeErr != nil { + return closeErr + } + return nil +} +func (c *controller) unprovedDisplayCleanup() error { + for _, path := range []string{"/tmp/.X93-lock", "/tmp/.X11-unix/X93"} { + if _, err := os.Lstat(path); !os.IsNotExist(err) { + return fmt.Errorf("display tree/asset cleanup unproved; no global unlink authorized") + } + } + return c.cleanupCreatedDisplayParent() +} + +// The shared X11 parent is removed only if this task created that exact inode +// and it is empty. A preexisting directory or another display is never removed. +func (c *controller) cleanupCreatedDisplayParent() error { + if c.createdDisplayParent == nil { + return nil + } + path := "/tmp/.X11-unix" + current, err := os.Lstat(path) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + if !current.IsDir() || !os.SameFile(c.createdDisplayParent, current) { + return fmt.Errorf("created display parent changed; retained") + } + owner, err := statUID(current) + if err != nil || owner != 0 || current.Mode() != c.createdDisplayParent.Mode() { + return fmt.Errorf("created display parent authority changed") + } + entries, err := os.ReadDir(path) + if err != nil { + return err + } + if len(entries) != 0 { + return fmt.Errorf("created display parent has foreign members; retained") + } + return os.Remove(path) +} + +func sameDisplayStat(held unix.Stat_t, current os.FileInfo) bool { + stat, ok := current.Sys().(*syscall.Stat_t) + return ok && held.Dev == stat.Dev && held.Ino == stat.Ino && held.Mode == stat.Mode && held.Nlink == stat.Nlink && held.Uid == stat.Uid && held.Size == stat.Size && held.Ctim.Sec == stat.Ctim.Sec && held.Ctim.Nsec == stat.Ctim.Nsec +} diff --git a/install/integrationctl/research/vscodeworkspace/display_descriptors_linux.go b/install/integrationctl/research/vscodeworkspace/display_descriptors_linux.go new file mode 100644 index 000000000..e7d8b6d4d --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/display_descriptors_linux.go @@ -0,0 +1,75 @@ +//go:build linux + +package main + +import ( + "errors" + "fmt" + "os" + + "golang.org/x/sys/unix" +) + +// Keeping the original inode open prevents immediate unlink/recreate from +// recycling that identity within the filesystem's metadata time quantum. +type heldDisplayAsset struct { + info os.FileInfo + file *os.File +} + +func (asset heldDisplayAsset) verify(path string) error { + if asset.file == nil || asset.info == nil { + return fmt.Errorf("display asset descriptor is missing") + } + held, err := asset.file.Stat() + if err != nil || !sameFile(asset.info, held) { + return fmt.Errorf("held display asset identity changed or closed") + } + named, err := os.Lstat(path) + if err != nil || !sameFile(held, named) { + return fmt.Errorf("named display asset differs from held identity") + } + return nil +} + +func holdDisplayAsset(path string, info os.FileInfo) (heldDisplayAsset, error) { + fd, err := unix.Open(path, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) + if err != nil { + return heldDisplayAsset{}, err + } + asset := heldDisplayAsset{info: info, file: os.NewFile(uintptr(fd), path)} + if err := asset.verify(path); err != nil { + return heldDisplayAsset{}, errors.Join(err, asset.file.Close()) + } + return asset, nil +} + +func holdDisplayAssets(lockPath, socketPath string, lockInfo, socketInfo os.FileInfo) (map[string]heldDisplayAsset, error) { + if lockPath == socketPath || lockInfo == nil || socketInfo == nil || os.SameFile(lockInfo, socketInfo) { + return nil, fmt.Errorf("display asset identities must be distinct") + } + lock, err := holdDisplayAsset(lockPath, lockInfo) + if err != nil { + return nil, err + } + socket, err := holdDisplayAsset(socketPath, socketInfo) + if err != nil { + return nil, errors.Join(err, lock.file.Close()) + } + if err := errors.Join(lock.verify(lockPath), socket.verify(socketPath)); err != nil { + return nil, errors.Join(err, lock.file.Close(), socket.file.Close()) + } + return map[string]heldDisplayAsset{lockPath: lock, socketPath: socket}, nil +} + +func (c *controller) closeDisplayAssets() (resultErr error) { + for _, asset := range c.displayAssets { + if asset.file == nil { + resultErr = errors.Join(resultErr, fmt.Errorf("display asset descriptor missing at close")) + continue + } + resultErr = errors.Join(resultErr, asset.file.Close()) + } + c.displayAssets = nil + return resultErr +} diff --git a/install/integrationctl/research/vscodeworkspace/display_descriptors_linux_test.go b/install/integrationctl/research/vscodeworkspace/display_descriptors_linux_test.go new file mode 100644 index 000000000..bb7d43f98 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/display_descriptors_linux_test.go @@ -0,0 +1,134 @@ +//go:build linux + +package main + +import ( + "net" + "os" + "path/filepath" + "testing" + + "golang.org/x/sys/unix" +) + +func TestDisplayDescriptorsHoldLockAndActualSocketUntilClose(t *testing.T) { + root, err := os.MkdirTemp("/tmp", "uap-vsc-fd-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.RemoveAll(root) }) + lock, socket := filepath.Join(root, "lock"), filepath.Join(root, "socket") + if err := os.WriteFile(lock, []byte("inert lock"), 0600); err != nil { + t.Fatal(err) + } + listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", socket) + if err != nil { + t.Fatal(err) + } + defer func() { _ = listener.Close() }() + lockInfo, socketInfo := inertAssetInfo(t, lock), inertAssetInfo(t, socket) + assets, err := holdDisplayAssets(lock, socket, lockInfo, socketInfo) + if err != nil || len(assets) != 2 { + t.Fatalf("held display pair: %v %v", assets, err) + } + c := &controller{displayAssets: assets} + defer func() { _ = c.closeDisplayAssets() }() + for path, asset := range assets { + if err := asset.verify(path); err != nil { + t.Fatal(err) + } + flags, err := unix.FcntlInt(asset.file.Fd(), unix.F_GETFL, 0) + if err != nil || flags&unix.O_PATH == 0 { + t.Fatal("display handle is not path-only", err) + } + fdFlags, err := unix.FcntlInt(asset.file.Fd(), unix.F_GETFD, 0) + if err != nil || fdFlags&unix.FD_CLOEXEC == 0 { + t.Fatal("display handle can cross exec", err) + } + } + if err := c.closeDisplayAssets(); err != nil || len(c.displayAssets) != 0 { + t.Fatalf("close both display handles: %v", err) + } + for path, asset := range assets { + if asset.verify(path) == nil { + t.Fatal("closed descriptor retained display authority") + } + } +} + +func TestDisplayDescriptorPairClosesFirstOnSecondOpenFailure(t *testing.T) { + root := t.TempDir() + lock, other := filepath.Join(root, "lock"), filepath.Join(root, "other") + for _, path := range []string{lock, other} { + if err := os.WriteFile(path, []byte("inert"), 0600); err != nil { + t.Fatal(err) + } + } + lockInfo, otherInfo := inertAssetInfo(t, lock), inertAssetInfo(t, other) + if err := os.Remove(other); err != nil { + t.Fatal(err) + } + before := inertAssetFDCount(t, lock) + assets, err := holdDisplayAssets(lock, other, lockInfo, otherInfo) + if err == nil || assets != nil || inertAssetFDCount(t, lock) != before { + t.Fatal("partial display admission retained a descriptor or authority", err) + } +} + +func TestDisplayDescriptorRejectsSymlinkAndMissingOrClosedAuthority(t *testing.T) { + root := t.TempDir() + path, link := filepath.Join(root, "asset"), filepath.Join(root, "link") + if err := os.WriteFile(path, []byte("unchanged"), 0600); err != nil { + t.Fatal(err) + } + info := inertAssetInfo(t, path) + if err := os.Symlink(path, link); err != nil { + t.Fatal(err) + } + if _, err := holdDisplayAsset(link, info); err == nil { + t.Fatal("display descriptor followed a symlink") + } + c := &controller{displayAssets: map[string]heldDisplayAsset{path: {info: info}}} + if c.cleanupDisplayAsset(path) == nil { + t.Fatal("missing descriptor granted unlink authority") + } + asset, err := holdDisplayAsset(path, info) + if err != nil { + t.Fatal(err) + } + if err := asset.file.Close(); err != nil { + t.Fatal(err) + } + c.displayAssets[path] = asset + if c.cleanupDisplayAsset(path) == nil || c.closeDisplayAssets() == nil { + t.Fatal("closed descriptor granted unlink or concealed close failure") + } + if string(mustReadInert(t, path)) != "unchanged" { + t.Fatal("refused descriptor cleanup changed named bytes") + } +} + +func inertAssetInfo(t *testing.T, path string) os.FileInfo { + t.Helper() + info, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + return info +} + +func inertAssetFDCount(t *testing.T, path string) int { + t.Helper() + entries, err := os.ReadDir("/proc/self/fd") + if err != nil { + t.Fatal(err) + } + count := 0 + for _, entry := range entries { + target, err := os.Readlink(filepath.Join("/proc/self/fd", entry.Name())) + if err == nil && target == path { + count++ + } + } + return count +} diff --git a/install/integrationctl/research/vscodeworkspace/files_linux.go b/install/integrationctl/research/vscodeworkspace/files_linux.go new file mode 100644 index 000000000..20ee7541e --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/files_linux.go @@ -0,0 +1,452 @@ +//go:build linux + +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + "syscall" + "unicode/utf8" + + "golang.org/x/sys/unix" +) + +func sameFile(a, b os.FileInfo) bool { + if a == nil || b == nil || !os.SameFile(a, b) || a.Mode() != b.Mode() || a.Size() != b.Size() || !a.ModTime().Equal(b.ModTime()) { + return false + } + aa, ok := a.Sys().(*syscall.Stat_t) + if !ok { + return false + } + bb, ok := b.Sys().(*syscall.Stat_t) + return ok && aa.Nlink == bb.Nlink && aa.Ctim == bb.Ctim && aa.Uid == bb.Uid && aa.Gid == bb.Gid +} +func statUID(info os.FileInfo) (uint32, error) { + st, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return 0, fmt.Errorf("linux stat authority missing") + } + return st.Uid, nil +} +func regular(info os.FileInfo, limit int64) bool { + st, ok := info.Sys().(*syscall.Stat_t) + return ok && info.Mode().IsRegular() && st.Nlink == 1 && info.Size() >= 0 && info.Size() <= limit +} +func anchoredInfo(path string) (os.FileInfo, error) { + absolute, err := filepath.Abs(path) + if err != nil { + return nil, err + } + root, err := os.OpenRoot("/") + if err != nil { + return nil, err + } + defer func() { _ = root.Close() }() + relative := strings.TrimPrefix(absolute, "/") + if relative == "" { + relative = "." + } + return root.Lstat(relative) +} +func exactDirectory(path string, owner int) (os.FileInfo, error) { + if !filepath.IsAbs(path) || filepath.Clean(path) != path { + return nil, fmt.Errorf("exact absolute directory required") + } + current := "/" + for _, part := range strings.Split(strings.TrimPrefix(path, "/"), "/") { + current = filepath.Join(current, part) + info, err := anchoredInfo(current) + if err != nil { + return nil, err + } + if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return nil, fmt.Errorf("directory ancestor is not ordinary") + } + } + info, err := anchoredInfo(path) + if err != nil { + return nil, err + } + uid, err := statUID(info) + if err != nil || int64(uid) != int64(owner) { + return nil, fmt.Errorf("directory owner differs") + } + return info, nil +} +func openOrdinary(path string, limit int64) (*os.File, os.FileInfo, error) { + before, err := anchoredInfo(path) + if err != nil { + return nil, nil, err + } + if !regular(before, limit) { + return nil, nil, fmt.Errorf("ordinary single-link bounded file required") + } + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + f := os.NewFile(uintptr(fd), path) + opened, err := f.Stat() + if err != nil || !sameFile(before, opened) { + _ = f.Close() + return nil, nil, fmt.Errorf("opened file identity differs") + } + return f, before, nil +} +func readOrdinary(path string, limit int64) ([]byte, error) { + f, before, err := openOrdinary(path, limit) + if err != nil { + return nil, err + } + defer func() { _ = f.Close() }() + body, err := io.ReadAll(io.LimitReader(f, limit+1)) + if err != nil { + return nil, err + } + after, e := f.Stat() + current, pe := anchoredInfo(path) + if e != nil || pe != nil || int64(len(body)) > limit || int64(len(body)) != before.Size() || !sameFile(before, after) || !sameFile(before, current) { + return nil, fmt.Errorf("bounded file changed while reading") + } + return body, nil +} +func hashFile(path string, limit int64) (string, error) { + f, before, err := openOrdinary(path, limit) + if err != nil { + return "", err + } + defer func() { _ = f.Close() }() + h := sha256.New() + n, err := io.Copy(h, io.LimitReader(f, limit+1)) + if err != nil { + return "", err + } + after, e := f.Stat() + current, pe := anchoredInfo(path) + if e != nil || pe != nil || n != before.Size() || n > limit || !sameFile(before, after) || !sameFile(before, current) { + return "", fmt.Errorf("hashed file changed") + } + return hex.EncodeToString(h.Sum(nil)), nil +} +func copyOrdinary(ctx context.Context, source, target string, uid int, executable bool) (string, error) { + f, before, err := openOrdinary(source, 512<<20) + if err != nil { + return "", err + } + defer func() { _ = f.Close() }() + owner, err := statUID(before) + if err != nil || int64(owner) != int64(uid) { + return "", fmt.Errorf("input file owner differs") + } + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", err + } + total, digest, err := copyBounded(ctx, f, out, before.Size()) + closeErr := out.Close() + if err != nil { + return "", err + } + if closeErr != nil { + return "", closeErr + } + after, e := f.Stat() + current, pe := anchoredInfo(source) + if e != nil || pe != nil || total != before.Size() || !sameFile(before, after) || !sameFile(before, current) { + return "", fmt.Errorf("copied input changed") + } + mode := os.FileMode(0444) + if executable { + var head [4]byte + if _, err = f.ReadAt(head[:], 0); err != nil || string(head[:]) != "\x7fELF" { + return "", fmt.Errorf("allowlisted executable is not ELF") + } + mode = 0555 + } + if err = os.Chmod(target, mode); err != nil { + return "", err + } + return digest, nil +} +func copyBounded(ctx context.Context, f, out *os.File, limit int64) (int64, string, error) { + h := sha256.New() + var err error + buf := make([]byte, 64<<10) + var total int64 + for { + if err = ctx.Err(); err != nil { + break + } + var n int + n, err = f.Read(buf) + if n > 0 { + total += int64(n) + if total > limit { + err = fmt.Errorf("input file grew") + break + } + if _, err = out.Write(buf[:n]); err != nil { + break + } + h.Write(buf[:n]) + } + if errors.Is(err, io.EOF) { + err = nil + break + } + if err != nil { + break + } + } + return total, hex.EncodeToString(h.Sum(nil)), err +} +func copyPublisher(ctx context.Context, source, target string, uid int) (map[string]string, error) { + if _, err := exactDirectory(source, uid); err != nil { + return nil, err + } + hashes := map[string]string{} + entries := 0 + var bytes int64 + root, err := os.OpenRoot(source) + if err != nil { + return nil, err + } + defer func() { _ = root.Close() }() + err = fs.WalkDir(root.FS(), ".", func(rel string, _ os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if err := ctx.Err(); err != nil { + return err + } + entries++ + if entries > 100000 { + return fmt.Errorf("publisher entry bound") + } + if strings.Count(rel, string(os.PathSeparator)) > 32 { + return fmt.Errorf("publisher path depth") + } + path := filepath.Join(source, rel) + info, err := anchoredInfo(path) + if err != nil { + return err + } + owner, err := statUID(info) + if err != nil || int64(owner) != int64(uid) { + return fmt.Errorf("publisher input owner differs") + } + if info.IsDir() { + return os.Mkdir(filepath.Join(target, rel), 0755) + } + if !regular(info, 512<<20) { + return fmt.Errorf("publisher input not ordinary") + } + bytes += info.Size() + if bytes > 2<<30 { + return fmt.Errorf("publisher byte bound") + } + return copyPublisherEntry(ctx, path, filepath.Join(target, rel), rel, uid, hashes) + }) + if err != nil { + return nil, err + } + if len(hashes) != len(requiredPublisher) { + return nil, fmt.Errorf("allowlisted publisher resource missing") + } + return hashes, nil +} +func copyPublisherEntry(ctx context.Context, source, target, relative string, uid int, hashes map[string]string) error { + digest, err := copyOrdinary(ctx, source, target, uid, relative == "code" || relative == rgExecutable) + if err != nil { + return err + } + for _, required := range requiredPublisher { + if relative == required { + hashes[relative] = digest + } + } + return nil +} +func snapshot(paths []string) (map[string]string, error) { + result := map[string]string{} + for _, root := range paths { + if err := snapshotSource(root, result); err != nil { + return nil, err + } + } + return result, nil +} +func snapshotSource(root string, result map[string]string) error { + if _, err := exactDirectory(filepath.Dir(root), 0); err != nil { + return err + } + err := filepath.WalkDir(root, func(path string, _ os.DirEntry, e error) error { + if e != nil { + return e + } + if err := snapshotCapacity(result); err != nil { + return err + } + info, err := anchoredInfo(path) + if err != nil { + return err + } + uid, err := statUID(info) + if err != nil || uid != 0 || info.Mode().Perm()&022 != 0 { + return fmt.Errorf("sealed source writable or foreign") + } + if info.IsDir() { + result[path] = fmt.Sprintf("directory:%o", info.Mode()) + return nil + } + if !regular(info, 1<<20) { + return fmt.Errorf("sealed source is not ordinary") + } + digest, err := hashFile(path, 1<<20) + if err != nil { + return err + } + result[path] = fmt.Sprintf("%o:%d:%s", info.Mode(), info.Size(), digest) + return nil + }) + if err != nil { + return err + } + _, err = exactDirectory(filepath.Dir(root), 0) + return err +} +func sortedKeys(m map[string][]byte) []string { + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} +func uniqueJSON(body []byte) (any, error) { + if !utf8.Valid(body) { + return nil, fmt.Errorf("invalid UTF-8 JSON") + } + dec := json.NewDecoder(strings.NewReader(string(body))) + dec.UseNumber() + value, err := readJSONValue(dec, 0) + if err != nil { + return nil, err + } + if _, err = dec.Token(); !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("trailing JSON data") + } + return value, nil +} +func readJSONValue(dec *json.Decoder, depth int) (any, error) { + if depth > 32 { + return nil, fmt.Errorf("JSON nesting bound") + } + token, err := dec.Token() + if err != nil { + return nil, err + } + delimiter, ok := token.(json.Delim) + if !ok { + return token, nil + } + switch delimiter { + case '{': + return readJSONObject(dec, depth) + case '[': + return readJSONArray(dec, depth) + default: + return nil, fmt.Errorf("unexpected JSON delimiter") + } +} +func readJSONObject(dec *json.Decoder, depth int) (any, error) { + result := map[string]any{} + seen := map[string]bool{} + for dec.More() { + key, err := dec.Token() + if err != nil { + return nil, err + } + name, ok := key.(string) + fold := strings.ToLower(name) + if !ok || seen[fold] { + return nil, fmt.Errorf("duplicate/ambiguous JSON key") + } + seen[fold] = true + value, err := readJSONValue(dec, depth+1) + if err != nil { + return nil, err + } + result[name] = value + } + if end, err := dec.Token(); err != nil || end != json.Delim('}') { + return nil, fmt.Errorf("invalid object end") + } + return result, nil +} +func readJSONArray(dec *json.Decoder, depth int) (any, error) { + result := []any{} + for dec.More() { + if len(result) >= 256 { + return nil, fmt.Errorf("JSON array bound") + } + value, err := readJSONValue(dec, depth+1) + if err != nil { + return nil, err + } + result = append(result, value) + } + if end, err := dec.Token(); err != nil || end != json.Delim(']') { + return nil, fmt.Errorf("invalid array end") + } + return result, nil +} +func immutableDirectory(info os.FileInfo, owner uint32, stickyException bool) bool { + uid, err := statUID(info) + if err != nil || uid != owner || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return false + } + if info.Mode().Perm()&0022 == 0 { + return true + } + return stickyException && info.Mode()&os.ModeSticky != 0 +} +func immutableAncestors(path string) error { + if !filepath.IsAbs(path) || filepath.Clean(path) != path { + return fmt.Errorf("immutable ancestor path is not exact") + } + current := "/" + info, err := anchoredInfo(current) + if err != nil || !immutableDirectory(info, 0, false) { + return fmt.Errorf("root ancestor authority differs") + } + for _, part := range strings.Split(strings.TrimPrefix(path, "/"), "/") { + if part == "" { + continue + } + current = filepath.Join(current, part) + info, err = anchoredInfo(current) + if err != nil || !immutableDirectory(info, 0, current == "/tmp") { + return fmt.Errorf("immutable executable ancestor refused: %s", current) + } + } + return nil +} + +func snapshotCapacity(result map[string]string) error { + if len(result) >= 256 { + return fmt.Errorf("sealed source entry bound") + } + return nil +} diff --git a/install/integrationctl/research/vscodeworkspace/metadata_linux.go b/install/integrationctl/research/vscodeworkspace/metadata_linux.go new file mode 100644 index 000000000..6177336ea --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/metadata_linux.go @@ -0,0 +1,265 @@ +//go:build linux + +package main + +import ( + "encoding/json" + "fmt" + "math" + "path/filepath" + "strings" + "unicode/utf8" +) + +func objectKeys(value any, required, optional []string) (map[string]any, error) { + m, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("metadata object required") + } + allowed := map[string]bool{} + for _, key := range required { + if _, ok := m[key]; !ok { + return nil, fmt.Errorf("required metadata field missing") + } + allowed[key] = true + } + for _, key := range optional { + allowed[key] = true + } + for key := range m { + if !allowed[key] { + return nil, fmt.Errorf("unknown metadata field %q", key) + } + } + return m, nil +} +func metadataText(value any) error { + s, ok := value.(string) + if !ok || len(s) > 8192 || !utf8.ValidString(s) || strings.ContainsRune(s, 0) { + return fmt.Errorf("metadata string invalid or oversized") + } + return nil +} +func metadataStrings(value any) error { + rows, ok := value.([]any) + if !ok || len(rows) > 128 { + return fmt.Errorf("metadata string array differs") + } + for _, row := range rows { + if err := metadataText(row); err != nil { + return err + } + } + return nil +} +func metadataURI(value any) error { + m, err := objectKeys(value, []string{"value", "scheme", "path", "authority"}, nil) + if err != nil { + return err + } + for _, v := range m { + if err := metadataText(v); err != nil { + return err + } + } + return nil +} +func metadataDictionary(value any, nullable bool) error { + m, ok := value.(map[string]any) + if !ok || len(m) > 128 { + return fmt.Errorf("metadata dictionary differs") + } + for key, v := range m { + if err := metadataText(key); err != nil { + return err + } + if nullable { + if v == nil { + continue + } + if number, ok := v.(json.Number); ok { + parsed, err := number.Float64() + if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) { + return fmt.Errorf("metadata environment number is not finite") + } + continue + } + } + if err := metadataText(v); err != nil { + return err + } + } + return nil +} +func metadataMCP(value any) error { + m, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("MCP object required") + } + kind, ok := m["kind"].(string) + if !ok { + return fmt.Errorf("MCP kind missing") + } + var err error + switch kind { + case "http": + m, err = objectKeys(m, []string{"label", "kind", "uri", "headers"}, []string{"version"}) + if err == nil { + err = metadataURI(m["uri"]) + } + if err == nil { + err = metadataDictionary(m["headers"], false) + } + case "stdio": + m, err = objectKeys(m, []string{"label", "kind", "command", "args", "env"}, []string{"version", "cwd"}) + if err == nil { + err = metadataText(m["command"]) + } + if err == nil { + err = metadataStrings(m["args"]) + } + if err == nil { + err = metadataDictionary(m["env"], true) + } + if cwd, present := m["cwd"]; err == nil && present { + err = metadataURI(cwd) + } + default: + return fmt.Errorf("MCP kind unknown") + } + if err != nil { + return err + } + if err = metadataText(m["label"]); err != nil { + return err + } + if version, present := m["version"]; present { + return metadataText(version) + } + return nil +} +func metadataSkill(value any) error { + m, err := objectKeys(value, []string{"uri", "name", "source", "disableModelInvocation"}, []string{"description", "sessionTypes", "userInvocable", "extensionId", "pluginUri"}) + if err != nil { + return err + } + if err = metadataURI(m["uri"]); err != nil { + return err + } + if err = metadataText(m["name"]); err != nil { + return err + } + switch m["source"] { + case "local", "user", "extension", "plugin", "builtin": + default: + return fmt.Errorf("unknown skill source") + } + if _, ok := m["disableModelInvocation"].(bool); !ok { + return fmt.Errorf("unknown skill invocation flag") + } + for _, key := range []string{"description", "extensionId"} { + if v, present := m[key]; present { + if err = metadataText(v); err != nil { + return err + } + } + } + if v, present := m["sessionTypes"]; present { + if err = metadataStrings(v); err != nil { + return err + } + } + if v, present := m["userInvocable"]; present { + if _, ok := v.(bool); !ok { + return fmt.Errorf("unknown user invocation flag") + } + } + if v, present := m["pluginUri"]; present { + return metadataURI(v) + } + return nil +} +func metadataRows(value any, validate func(any) error) error { + rows, ok := value.([]any) + if !ok || len(rows) > 128 { + return fmt.Errorf("unknown/oversized native metadata array") + } + for _, row := range rows { + if err := validate(row); err != nil { + return err + } + } + return nil +} +func metadataAttribution(m map[string]any, folder string, removed bool) map[string]any { + result := map[string]any{"mcp": "blocked", "skills": "blocked", "mcp_source": "unique sealed marker URL inference; API provides no config-file origin", "same_name": "all native rows retained; no simultaneous activation claim", "connection_attempts": "not instrumented", "native_project_qualified": false} + if mcpAttribution(m, folder, removed) { + result["mcp"] = "passed" + } + if skillAttribution(m, folder, removed) { + result["skills"] = "passed" + } + return result +} +func mcpAttribution(m map[string]any, folder string, removed bool) bool { + rows, _ := m["mcp"].([]any) + urls := map[string]int{} + for _, row := range rows { + r, _ := row.(map[string]any) + u, _ := r["uri"].(map[string]any) + v, _ := u["value"].(string) + urls[v]++ + } + label := filepath.Base(folder) + other := "B" + if label == "B" { + other = "A" + } + selected, foreign, user := "http://127.0.0.1:9/UAP-"+label, "http://127.0.0.1:9/UAP-"+other, "http://127.0.0.1:9/UAP-USER" + return urls[user] == 1 && urls[foreign] == 0 && ((!removed && urls[selected] == 1) || (removed && urls[selected] == 0)) +} +func skillAttribution(m map[string]any, folder string, removed bool) bool { + label := filepath.Base(folder) + other := "B" + if label == "B" { + other = "A" + } + rows, _ := m["skills"].([]any) + owned := filepath.Join(folder, ".github", "skills", "uap-"+strings.ToLower(label), "SKILL.md") + home := filepath.Join(filepath.Dir(filepath.Dir(folder)), "home", ".copilot", "skills", "uap-user", "SKILL.md") + counts, allPaths, sibling := skillMarkerCounts(rows, folder, other, owned, home, label) + return counts[home] == 1 && allPaths[home] == 1 && !sibling && ((!removed && counts[owned] == 1 && allPaths[owned] == 1) || (removed && !hasSkillPath(rows, owned))) +} +func hasSkillPath(rows []any, path string) bool { + for _, row := range rows { + r, _ := row.(map[string]any) + u, _ := r["uri"].(map[string]any) + if u["path"] == path { + return true + } + } + return false +} +func skillMarkerCounts(rows []any, folder, other, owned, home, label string) (map[string]int, map[string]int, bool) { + counts, allPaths := map[string]int{}, map[string]int{} + sibling := false + for _, row := range rows { + r, _ := row.(map[string]any) + u, _ := r["uri"].(map[string]any) + p, _ := u["path"].(string) + allPaths[p]++ + if strings.HasPrefix(p, filepath.Join(filepath.Dir(folder), other)+string(filepath.Separator)) { + sibling = true + } + if u["scheme"] != "file" || u["authority"] != "" || u["value"] != "file://"+p || r["disableModelInvocation"] != true { + continue + } + if p == owned && r["name"] == "uap-"+strings.ToLower(label) && r["description"] == label { + counts[p]++ + } + if p == home && r["name"] == "uap-user" && r["description"] == "USER" { + counts[p]++ + } + } + return counts, allPaths, sibling +} diff --git a/install/integrationctl/research/vscodeworkspace/model_linux.go b/install/integrationctl/research/vscodeworkspace/model_linux.go new file mode 100644 index 000000000..162b81e4c --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/model_linux.go @@ -0,0 +1,146 @@ +//go:build linux + +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "regexp" + "strings" + "time" + + "golang.org/x/sys/unix" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +const publisherVersion = "1.143.1" +const publisherCommit = "e646868227c3e3a9e3f24277c8c7220be42c0be7" +const publisherPrefix = "VSCode-linux-x64" +const rgExecutable = "resources/app/node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/linux-x64/rg" +const streamLimit = 1 << 20 +const controlLimit = 64 << 10 +const resultLimit = 512 << 10 +const artifactLimit = 32 << 20 +const display = ":93" + +var buildCommit string +var probeLabels = []string{"A", "B", "A-restart", "A-after-removal"} +var requiredPublisher = []string{"code", "resources/app/out/cli.js", "resources/app/product.json", rgExecutable} +var observerPins = map[string]string{ + "package.json": "441f882bfafd76e8e71e9ab59803f51a1e4b2890fb32f993f8298440fa992cb2", + "extension.cjs": "4c66f495dfe950ae2b32dda642f799ff3a53f319bc56d5149ed2fb850e232574", + "shape.cjs": "78a0046fe261156849f03ceb1cb5cc7036e3666400996fbe281b9b3301513802", + "runner.cjs": "4858024108996491b5b1701c219e77e56a41013de1dc1feff2727ad5023e711c", +} +var artifactName = regexp.MustCompile("^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$") + +type inputRequest struct { + UID int `json:"uid"` + GID int `json:"gid"` + EUID int `json:"euid"` + EGID int `json:"egid"` + StartNS int64 `json:"start_monotonic_ns"` + SourceCommit string `json:"source_commit"` + ObserverRoot string `json:"observer_root"` + OutputRoot string `json:"output_root"` + HelperSHA string `json:"helper_sha256"` +} +type sealedRequest struct { + Input inputRequest `json:"input"` + Base string `json:"base"` + ExecutableHashes map[string]string `json:"executable_hashes"` +} +type isolationProof struct { + PID int `json:"pid"` + UID int `json:"uid"` + GID int `json:"gid"` + EUID int `json:"euid"` + EGID int `json:"egid"` + Namespace uint64 `json:"network_namespace"` + ParentNamespace uint64 `json:"parent_network_namespace"` + Groups []int `json:"groups"` + Interfaces []string `json:"interfaces"` + CapabilitiesZero bool `json:"capabilities_zero"` + NoNewPrivileges bool `json:"no_new_privileges"` +} +type childRecord struct { + Label string `json:"label"` + Argv []string `json:"argv"` + Cwd string `json:"cwd"` + Proof isolationProof `json:"isolation"` + Diagnostics string `json:"diagnostics"` + MergedFile string `json:"merged_file"` + MergedBytes int `json:"merged_bytes"` + MergedSHA string `json:"merged_sha256"` + MergedOverflow bool `json:"merged_overflow"` + PlannedStop bool `json:"planned_stop"` + TreeReaped bool `json:"tree_reaped"` + Error string `json:"error"` +} +type strictRunner interface { + RunDuplexWithPlannedShutdown(context.Context, ports.Command, func(io.Writer, io.Reader) error) error +} +type controller struct { + request sealedRequest + deadline time.Time + work context.Context + cancelWork context.CancelFunc + runner strictRunner + files map[string][]byte + bytes int + output *os.File + hashes map[string]string + parentNS uint64 + cookie string + children []childRecord + observations []map[string]any + displayAssets map[string]heldDisplayAsset + createdDisplayParent os.FileInfo + displayCleanupErr error +} + +func deadlineAt(start int64, limit time.Duration) (time.Time, error) { + var now unix.Timespec + if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil { + return time.Time{}, err + } + ns := now.Nano() + if start <= 0 || start > ns || ns-start > int64(480*time.Second) || limit <= 0 || limit > 480*time.Second { + return time.Time{}, fmt.Errorf("monotonic task budget authority refused") + } + return time.Now().Add(limit - time.Duration(ns-start)), nil +} +func encode(value any, limit int) ([]byte, error) { + body, err := json.MarshalIndent(value, "", " ") + if err != nil { + return nil, err + } + body = append(body, '\n') + if len(body) > limit { + return nil, fmt.Errorf("encoded evidence exceeds bound") + } + return body, nil +} +func clipped(err error) string { + if err == nil { + return "" + } + text := err.Error() + if len(text) > 2048 { + return text[:2048] + } + return text +} +func (c *controller) keep(name string, body []byte) error { + _, exists := c.files[name] + if !artifactName.MatchString(name) || (!strings.HasSuffix(name, ".json") && !strings.HasSuffix(name, ".log")) || exists || len(c.files) >= 64 || len(body) > 1<<20 || c.bytes+len(body) > artifactLimit-(64<<10) { + return fmt.Errorf("bounded flat evidence admission refused") + } + c.files[name] = append([]byte(nil), body...) + c.bytes += len(body) + return nil +} diff --git a/install/integrationctl/research/vscodeworkspace/sequence_linux.go b/install/integrationctl/research/vscodeworkspace/sequence_linux.go new file mode 100644 index 000000000..fca03c68a --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/sequence_linux.go @@ -0,0 +1,52 @@ +//go:build linux + +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" +) + +// This is the literal research table, not a command/execution endpoint. Private +// callbacks keep its nesting testable without starting any operating-system +// process; production passes only the existing strict runner and fixed probes. +func runDisplayTable(work context.Context, outer func(func(context.Context, isolationProof) error) (childRecord, error), ready func(context.Context, isolationProof) error, probe func(string) error) (childRecord, error) { + return outer(func(ctx context.Context, proof isolationProof) error { + if err := ready(ctx, proof); err != nil { + return err + } + for _, label := range probeLabels { + if err := work.Err(); err != nil { + return err + } + if err := probe(label); err != nil { + return err + } + } + return nil + }) +} +func requireFreshResultPaths(result string) error { + for _, path := range []string{result, filepath.Join(filepath.Dir(result), ".observation.pending"), filepath.Join(filepath.Dir(result), "observation.ready")} { + if _, err := os.Lstat(path); !os.IsNotExist(err) { + return fmt.Errorf("fresh result protocol leaf already exists") + } + } + return nil +} +func admitReadyResult(result string) ([]byte, bool, error) { + _, err := readOrdinary(filepath.Join(filepath.Dir(result), "observation.ready"), 0) + if os.IsNotExist(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + body, err := readOrdinary(result, resultLimit) + if err != nil { + return nil, false, fmt.Errorf("ready result is missing/invalid: %w", err) + } + return body, true, nil +} diff --git a/install/integrationctl/research/vscodeworkspace/sequence_linux_test.go b/install/integrationctl/research/vscodeworkspace/sequence_linux_test.go new file mode 100644 index 000000000..f5f28aef7 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/sequence_linux_test.go @@ -0,0 +1,200 @@ +//go:build linux + +package main + +import ( + "context" + "encoding/json" + "errors" + "io" + "os" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/ports" +) + +type sequenceEvents struct { + mu sync.Mutex + items []string +} + +func (e *sequenceEvents) add(s string) { e.mu.Lock(); e.items = append(e.items, s); e.mu.Unlock() } +func (e *sequenceEvents) copy() []string { + e.mu.Lock() + defer e.mu.Unlock() + return append([]string(nil), e.items...) +} + +type sequenceRunner struct { + events *sequenceEvents + outerReader io.ReadCloser +} + +func (r *sequenceRunner) RunDuplexWithPlannedShutdown(ctx context.Context, command ports.Command, exchange func(io.Writer, io.Reader) error) error { + var argv []string + if err := json.Unmarshal([]byte(command.Argv[len(command.Argv)-1]), &argv); err != nil { + return err + } + label := argv[0] + r.events.add("start:" + label) + var reader io.Reader = strings.NewReader(testProof() + "inert\n") + if label == "Xvfb" && r.outerReader != nil { + reader = r.outerReader + } + err := exchange(io.Discard, reader) + if label == "Xvfb" && r.outerReader != nil { + _ = r.outerReader.Close() + } + r.events.add("reap:" + label) + return err +} +func runInertSequence(t *testing.T, fail string, overflow bool) ([]string, []childRecord, error) { + t.Helper() + events := &sequenceEvents{} + work, cancelWork := context.WithCancel(t.Context()) + defer cancelWork() + c := &controller{request: sealedRequest{Input: inputRequest{UID: 1001, GID: 1001}}, parentNS: 1, work: work, files: map[string][]byte{}} + c.cancelWork = func() { events.add("work-cancel"); cancelWork() } + runner := &sequenceRunner{events: events} + c.runner = runner + innerStarted := make(chan struct{}) + writerDone := make(chan struct{}) + if overflow { + reader, writer := io.Pipe() + runner.outerReader = reader + go func() { + defer close(writerDone) + defer func() { _ = writer.Close() }() + _, _ = writer.Write([]byte(testProof())) + <-innerStarted + _, _ = writer.Write([]byte(strings.Repeat("x", streamLimit+1))) + }() + } + outerCtx, cancel := context.WithTimeout(t.Context(), 2*time.Second) + defer cancel() + _, err := runDisplayTable(work, func(exchange func(context.Context, isolationProof) error) (childRecord, error) { + return c.strict(outerCtx, "Xvfb", []string{"Xvfb"}, t.TempDir(), map[string]string{}, exchange) + }, func(context.Context, isolationProof) error { events.add("ready"); return nil }, func(label string) error { + _, err := c.strict(work, label, []string{label}, t.TempDir(), map[string]string{}, func(ctx context.Context, _ isolationProof) error { + if overflow { + close(innerStarted) + <-ctx.Done() + return ctx.Err() + } + if label == fail { + return errors.New("inert probe failure") + } + return nil + }) + return err + }) + if overflow { + select { + case <-writerDone: + case <-time.After(time.Second): + t.Fatal("fake producer did not join") + } + } + return events.copy(), c.children, err +} +func TestFixedFiveCallNestingJoinsEveryInnerBeforeDisplay(t *testing.T) { + events, records, err := runInertSequence(t, "", false) + if err != nil { + t.Fatal(err) + } + want := []string{"start:Xvfb", "ready", "start:A", "reap:A", "start:B", "reap:B", "start:A-restart", "reap:A-restart", "start:A-after-removal", "reap:A-after-removal", "reap:Xvfb"} + if !reflect.DeepEqual(events, want) || len(records) != 5 { + t.Fatalf("nested order %v records=%d", events, len(records)) + } + for _, record := range records { + if !record.TreeReaped || !record.PlannedStop { + t.Fatal("inert strict boundary lost", record) + } + } +} +func TestFailedGUIStopsTableBeforeAnotherProbeAndDisplayReturnsLast(t *testing.T) { + events, _, err := runInertSequence(t, "B", false) + if err == nil { + t.Fatal("failed exchange became success") + } + want := []string{"start:Xvfb", "ready", "start:A", "reap:A", "start:B", "reap:B", "reap:Xvfb"} + if !reflect.DeepEqual(events, want) { + t.Fatal(events) + } +} +func TestOuterOverflowCancelsWorkThenJoinsInnerBeforeDisplay(t *testing.T) { + events, records, err := runInertSequence(t, "", true) + if err == nil { + t.Fatal("overflow became success") + } + want := []string{"start:Xvfb", "ready", "start:A", "work-cancel", "reap:A", "reap:Xvfb"} + if !reflect.DeepEqual(events, want) { + t.Fatal(events) + } + if len(records) != 2 || records[1].PlannedStop || !records[1].MergedOverflow { + t.Fatal(records) + } +} + +type endingErrorReader struct{ cause error } + +func (r endingErrorReader) Read([]byte) (int, error) { return 0, r.cause } +func TestUnexpectedMergedReadErrorCannotBecomePlannedSuccess(t *testing.T) { + sentinel := errors.New("inert read failure") + _, cancel := context.WithCancel(t.Context()) + defer cancel() + m := &mergedCapture{ready: make(chan struct{}), done: make(chan struct{}), cancel: cancel} + m.drain(io.MultiReader(strings.NewReader(testProof()), endingErrorReader{sentinel}), 1001, 1001, 1) + if !errors.Is(m.err, sentinel) { + t.Fatal("read error discarded", m.err) + } +} + +type unjoinedReader struct { + release <-chan struct{} + joined chan<- struct{} +} + +func (r unjoinedReader) Read([]byte) (int, error) { + <-r.release + close(r.joined) + return 0, io.EOF +} + +type unjoinedRunner struct{ reader io.Reader } + +func (r unjoinedRunner) RunDuplexWithPlannedShutdown(_ context.Context, _ ports.Command, exchange func(io.Writer, io.Reader) error) error { + return exchange(io.Discard, r.reader) +} +func TestUnjoinedDrainCannotBecomePlannedSuccess(t *testing.T) { + release, joined := make(chan struct{}), make(chan struct{}) + work, cancel := context.WithCancel(t.Context()) + defer cancel() + reader := io.MultiReader(strings.NewReader(testProof()), unjoinedReader{release, joined}) + c := &controller{request: sealedRequest{Input: inputRequest{UID: 1001, GID: 1001}}, parentNS: 1, work: work, files: map[string][]byte{}, runner: unjoinedRunner{reader}} + record, err := c.strict(work, "A", []string{"inert"}, t.TempDir(), map[string]string{}, func(context.Context, isolationProof) error { return nil }) + close(release) + select { + case <-joined: + case <-time.After(time.Second): + t.Fatal("inert reader failed to join after release") + } + if err == nil || record.PlannedStop || record.TreeReaped || !strings.Contains(record.Error, "merged drain did not join") { + t.Fatalf("unjoined drain became success: %+v %v", record, err) + } +} +func TestOnlyExplicitPlannedCloseMayDiscardClosedPipe(t *testing.T) { + for _, planned := range []bool{false, true} { + _, cancel := context.WithCancel(t.Context()) + m := &mergedCapture{ready: make(chan struct{}), done: make(chan struct{}), cancel: cancel, planned: planned} + m.drain(io.MultiReader(strings.NewReader(testProof()), endingErrorReader{os.ErrClosed}), 1001, 1001, 1) + cancel() + if (m.err == nil) != planned { + t.Fatalf("closed pipe planned=%t error=%v", planned, m.err) + } + } +} diff --git a/install/integrationctl/research/vscodeworkspace/workbench_linux.go b/install/integrationctl/research/vscodeworkspace/workbench_linux.go new file mode 100644 index 000000000..df94165a6 --- /dev/null +++ b/install/integrationctl/research/vscodeworkspace/workbench_linux.go @@ -0,0 +1,486 @@ +//go:build linux + +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "time" +) + +func (c *controller) ownedDirectory(path string) error { + if err := os.MkdirAll(path, 0700); err != nil { + return err + } + return os.Chown(path, c.request.Input.UID, c.request.Input.GID) +} +func writeSealed(path string, body []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, body, 0444) +} +func skillBody(name, marker string) []byte { + return []byte("---\nname: " + name + "\ndescription: " + marker + "\ndisable-model-invocation: true\n---\nInert research marker. No command or tool request.\n") +} +func markerMCP(marker string) []byte { + return []byte(`{"servers":{"uap-` + strings.ToLower(marker) + `":{"type":"http","url":"http://127.0.0.1:9/UAP-` + marker + `"}}}` + "\n") +} +func (c *controller) fixtures() error { + if err := c.homeFixtures(); err != nil { + return err + } + if err := c.projectFixtures(); err != nil { + return err + } + if err := os.Mkdir(filepath.Join(c.request.Base, "probes"), 0755); err != nil { + return err + } + for _, label := range probeLabels { + if err := c.probeFixture(label); err != nil { + return err + } + } + return nil +} +func (c *controller) homeFixtures() error { + base := c.request.Base + if err := os.Mkdir(filepath.Join(base, "home"), 0755); err != nil { + return err + } + for _, name := range []string{"config", "cache", "state", "data", "npm-cache"} { + if err := c.ownedDirectory(filepath.Join(base, "home", name)); err != nil { + return err + } + } + for _, name := range []string{"npm-user-config", "npm-global-config", "git-config"} { + if err := writeSealed(filepath.Join(base, "home", name), nil); err != nil { + return err + } + } + if err := c.ownedDirectory(filepath.Join(base, "tmp")); err != nil { + return err + } + for _, item := range []struct{ name, marker string }{{"uap-user", "USER"}, {"uap-shared", "USER-SHARED"}} { + if err := writeSealed(filepath.Join(base, "home", ".copilot", "skills", item.name, "SKILL.md"), skillBody(item.name, item.marker)); err != nil { + return err + } + } + return nil +} +func (c *controller) projectFixtures() error { + for _, label := range []string{"A", "B"} { + folder := filepath.Join(c.request.Base, "projects", label) + if err := writeSealed(filepath.Join(folder, ".vscode", "mcp.json"), markerMCP(label)); err != nil { + return err + } + for _, item := range []struct{ name, marker string }{{"uap-" + strings.ToLower(label), label}, {"uap-shared", label + "-SHARED"}} { + if err := writeSealed(filepath.Join(folder, ".github", "skills", item.name, "SKILL.md"), skillBody(item.name, item.marker)); err != nil { + return err + } + } + } + return nil +} +func (c *controller) probeFixture(label string) error { + root := filepath.Join(c.request.Base, "probes", label) + if err := os.Mkdir(root, 0755); err != nil { + return err + } + for _, name := range []string{"profile", "extensions", "results"} { + if err := c.ownedDirectory(filepath.Join(root, name)); err != nil { + return err + } + } + settings := []byte(`{"update.mode":"none","telemetry.telemetryLevel":"off","extensions.autoUpdate":false,"extensions.autoCheckUpdates":false,"workbench.startupEditor":"none","window.restoreWindows":"none","security.workspace.trust.enabled":true,"chat.mcp.autostart":"never"}` + "\n") + if err := writeSealed(filepath.Join(root, "profile", "User", "settings.json"), settings); err != nil { + return err + } + if err := writeSealed(filepath.Join(root, "profile", "User", "mcp.json"), markerMCP("USER")); err != nil { + return err + } + // Only these declared cache leaves are writable; authored User files/parent remain sealed. + for _, cache := range []string{"globalStorage", "workspaceStorage", "History", "snippets"} { + if err := c.ownedDirectory(filepath.Join(root, "profile", "User", cache)); err != nil { + return err + } + } + return nil +} +func (c *controller) sourcePaths(label string) []string { + base := c.request.Base + return []string{filepath.Join(base, "observer"), filepath.Join(base, "projects"), filepath.Join(base, "home", ".copilot", "skills"), filepath.Join(base, "probes", label, "profile", "User", "settings.json"), filepath.Join(base, "probes", label, "profile", "User", "mcp.json")} +} +func (c *controller) checkPublisher() error { + if err := immutableAncestors(filepath.Join(c.request.Base, "observer")); err != nil { + return err + } + self := filepath.Join(c.request.Base, "control", "controller") + if err := immutableAncestors(filepath.Dir(self)); err != nil { + return err + } + if digest, err := hashFile(self, 64<<20); err != nil || digest != c.request.Input.HelperSHA { + return errors.Join(fmt.Errorf("sealed controller binary changed"), err) + } + for _, name := range requiredPublisher { + path := filepath.Join(c.request.Base, "publisher", publisherPrefix, name) + if err := immutableAncestors(filepath.Dir(path)); err != nil { + return err + } + info, err := os.Lstat(path) + if err != nil { + return err + } + uid, err := statUID(info) + mode := os.FileMode(0444) + if name == "code" || name == rgExecutable { + mode = 0555 + } + if err != nil || uid != 0 || info.Mode().Perm() != mode { + return fmt.Errorf("sealed publisher owner/mode differs") + } + digest, err := hashFile(path, 512<<20) + if err != nil || digest != c.hashes[name] { + return errors.Join(fmt.Errorf("sealed publisher digest changed"), err) + } + } + return nil +} +func (c *controller) setupDisplay() error { + if err := c.prepareDisplayParent(); err != nil { + return err + } + if err := c.systemIdentities(); err != nil { + return err + } + return c.setupAuthority() +} +func (c *controller) prepareDisplayParent() error { + parent := "/tmp/.X11-unix" + if _, err := os.Lstat(parent); os.IsNotExist(err) { + if err = os.Mkdir(parent, 0700); err != nil { + return err + } + if err = os.Chmod(parent, os.ModeSticky|0777); err != nil { + return err + } + c.createdDisplayParent, err = os.Lstat(parent) + if err != nil { + return err + } + } + parentInfo, err := exactDirectory(parent, 0) + if err != nil { + return err + } + if parentInfo.Mode().Perm()&0002 != 0 && parentInfo.Mode()&os.ModeSticky == 0 { + return fmt.Errorf("display parent is not protected by sticky ownership") + } + for _, name := range []string{"/tmp/.X93-lock", "/tmp/.X11-unix/X93"} { + if _, err := os.Lstat(name); !os.IsNotExist(err) { + return fmt.Errorf("display/lock collision; refusing to attach: %s", name) + } + } + return nil +} +func (c *controller) systemIdentities() error { + for _, path := range []string{"/usr/bin/Xvfb", "/usr/bin/xauth"} { + if err := trustedSystem(path); err != nil { + return err + } + } + identities := map[string]any{} + for _, path := range []string{"/usr/bin/Xvfb", "/usr/bin/xauth"} { + digest, err := hashFile(path, 128<<20) + if err != nil { + return err + } + info, err := os.Lstat(path) + if err != nil { + return err + } + identities[path] = map[string]any{"sha256": digest, "bytes": info.Size(), "version": "not inferred from binary identity"} + } + identityBody, err := encode(identities, controlLimit) + if err != nil { + return err + } + if err = c.keep("system-executables.json", identityBody); err != nil { + return err + } + return nil +} +func (c *controller) setupAuthority() error { + var err error + auth := filepath.Join(c.request.Base, "auth") + if err = os.Mkdir(auth, 0700); err != nil { + return err + } + c.cookie, err = randomCookie() + if err != nil { + return err + } + file := filepath.Join(auth, "authority") + if err = c.system("xauth", "/usr/bin/xauth", []string{"-f", file, "source", "-"}, []byte("add "+display+" . "+c.cookie+"\n"), true); err != nil { + return err + } + authority, _, err := openOrdinary(file, 16<<10) + if err != nil { + return err + } + if err = authority.Close(); err != nil { + return err + } + if err = os.Chown(auth, 0, c.request.Input.GID); err != nil { + return err + } + if err = os.Chmod(auth, 0710); err != nil { + return err + } + if err = os.Chown(file, 0, c.request.Input.GID); err != nil { + return err + } + return os.Chmod(file, 0440) +} +func (c *controller) displayReady(ctx context.Context, proof isolationProof) error { + deadline := time.NewTimer(10 * time.Second) + defer deadline.Stop() + tick := time.NewTicker(25 * time.Millisecond) + defer tick.Stop() + for { + lock, err := readOrdinary("/tmp/.X93-lock", 64) + if err == nil { + lockInfo, le := os.Lstat("/tmp/.X93-lock") + if le != nil { + return le + } + info, se := os.Lstat("/tmp/.X11-unix/X93") + if se == nil { + return c.retainReadyDisplay(lock, lockInfo, info, proof) + } + if !os.IsNotExist(se) { + return se + } + } else if !os.IsNotExist(err) { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline.C: + return fmt.Errorf("display readiness exceeded ten seconds") + case <-tick.C: + } + } +} +func (c *controller) removeA() error { + base := filepath.Join(c.request.Base, "projects", "A") + for _, path := range []string{filepath.Join(base, ".vscode", "mcp.json"), filepath.Join(base, ".github", "skills", "uap-a", "SKILL.md"), filepath.Join(base, ".github", "skills", "uap-shared", "SKILL.md")} { + info, err := os.Lstat(path) + if err != nil || !regular(info, 1<<20) { + return errors.Join(fmt.Errorf("declared removal fixture differs"), err) + } + if err = os.Remove(path); err != nil { + return err + } + } + return nil +} +func validateObservation(body []byte, base, folder string) (map[string]any, error) { + value, err := uniqueJSON(body) + if err != nil { + return nil, err + } + m, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("observer result is not an object") + } + if err := validateObservationEnvelope(m); err != nil { + return nil, err + } + if m["collected"] != true { + return m, fmt.Errorf("observer stage failed: %.2048v", m["stage_error"]) + } + if err := validateObservationContext(m, base, folder); err != nil { + return m, err + } + if err := validateObservationPolicy(m); err != nil { + return m, err + } + if err := validateObserverIdentity(m); err != nil { + return m, err + } + if err := metadataRows(m["mcp"], metadataMCP); err != nil { + return m, err + } + if err := metadataRows(m["skills"], metadataSkill); err != nil { + return m, err + } + return m, nil +} +func (c *controller) observe(label string) error { + deadline, err := deadlineAt(c.request.Input.StartNS, 420*time.Second) + if err != nil || time.Until(deadline) < 65*time.Second { + return fmt.Errorf("insufficient reserved budget for another GUI") + } + if label == "A-after-removal" { + if err = c.removeA(); err != nil { + return err + } + } + folderLabel := label + if label != "B" { + folderLabel = "A" + } + folder := filepath.Join(c.request.Base, "projects", folderLabel) + before, err := snapshot(c.sourcePaths(label)) + if err != nil { + return err + } + if err = c.checkPublisher(); err != nil { + return err + } + root := filepath.Join(c.request.Base, "probes", label) + result := fixedResultPath(c.request.Base, label) + if err = requireFreshResultPaths(result); err != nil { + return err + } + ctx, cancel := context.WithTimeout(c.work, 45*time.Second) + defer cancel() + var observed map[string]any + _, runErr := c.strict(ctx, label, guiArgv(c.request.Base, folder, filepath.Join(root, "profile")), folder, c.environment(folder, result, filepath.Join(root, "profile")), func(exchange context.Context, _ isolationProof) error { + var err error + observed, err = c.waitObservation(exchange, label, result, folder) + return err + }) + after, afterErr := snapshot(c.sourcePaths(label)) + if afterErr != nil || !reflect.DeepEqual(before, after) { + runErr = errors.Join(runErr, errors.Join(fmt.Errorf("authored source/fixture changed"), afterErr)) + } + if e := c.checkPublisher(); e != nil { + runErr = errors.Join(runErr, e) + } + if observed != nil { + c.observations = append(c.observations, map[string]any{"probe": label, "metadata_collected": runErr == nil, "attribution": metadataAttribution(observed, folder, label == "A-after-removal"), "error": clipped(runErr)}) + } + return runErr +} +func (c *controller) execute() (resultErr error) { + attemptedDisplay := false + reapedDisplay := false + defer func() { + if !attemptedDisplay { + c.displayCleanupErr = c.cleanupCreatedDisplayParent() + } else if reapedDisplay { + c.displayCleanupErr = c.cleanupDisplay() + } else { + c.displayCleanupErr = c.unprovedDisplayCleanup() + } + c.displayCleanupErr = errors.Join(c.displayCleanupErr, c.closeDisplayAssets()) + resultErr = errors.Join(resultErr, c.displayCleanupErr) + }() + if err := c.fixtures(); err != nil { + return err + } + if err := c.setupDisplay(); err != nil { + return err + } + if err := c.work.Err(); err != nil { + return err + } + env := c.environment(filepath.Join(c.request.Base, "projects", "A"), "", "") + argv := []string{"/usr/bin/Xvfb", display, "-screen", "0", "1024x768x24", "-nolisten", "tcp", "-auth", filepath.Join(c.request.Base, "auth", "authority")} + displayCtx, cancel := context.WithDeadline(context.Background(), c.deadline) + defer cancel() + attemptedDisplay = true + record, err := runDisplayTable(c.work, func(exchange func(context.Context, isolationProof) error) (childRecord, error) { + return c.strict(displayCtx, "Xvfb", argv, c.request.Base, env, exchange) + }, c.displayReady, c.observe) + reapedDisplay = record.TreeReaped + return err +} +func (c *controller) waitObservation(ctx context.Context, label, result, folder string) (map[string]any, error) { + tick := time.NewTicker(25 * time.Millisecond) + defer tick.Stop() + for { + body, ready, err := admitReadyResult(result) + if err != nil { + return nil, err + } + if ready { + if c.cookie != "" && strings.Contains(string(body), c.cookie) { + return nil, fmt.Errorf("task cookie appeared in result; raw body withheld") + } + if err = c.keep(label+".observation.json", body); err != nil { + return nil, err + } + return validateObservation(body, c.request.Base, folder) + } + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-tick.C: + } + } +} +func validateObservationEnvelope(m map[string]any) error { + if m["collected"] == true { + if _, present := m["stage_error"]; present { + return fmt.Errorf("successful observer envelope contains stage_error") + } + } + allowed := map[string]bool{} + for _, key := range []string{"schema_version", "collected", "stage_error", "version", "app_root", "app_host", "observer", "folder", "trusted", "policy", "mcp", "skills", "native_project_qualified", "native_mcp_initialization_qualified", "native_runtime_qualified"} { + allowed[key] = true + } + for key := range m { + if !allowed[key] { + return fmt.Errorf("unknown observer result field") + } + } + if m["schema_version"] != json.Number("1") || m["native_project_qualified"] != false || m["native_mcp_initialization_qualified"] != false || m["native_runtime_qualified"] != false { + return fmt.Errorf("observer schema/qualification differs") + } + return nil +} +func validateObservationContext(m map[string]any, base, folder string) error { + if m["version"] != publisherVersion || m["app_root"] != filepath.Join(base, "publisher", publisherPrefix, "resources", "app") || m["app_host"] != "desktop" || m["trusted"] != false { + return fmt.Errorf("publisher/context differs") + } + if err := metadataURI(m["folder"]); err != nil { + return err + } + selected, ok := m["folder"].(map[string]any) + if !ok || selected["scheme"] != "file" || selected["authority"] != "" || selected["path"] != folder || selected["value"] != "file://"+folder { + return fmt.Errorf("selected native folder differs") + } + return nil +} +func validateObservationPolicy(m map[string]any) error { + policy, policyErr := objectKeys(m["policy"], []string{"skills_enabled", "mcp_access", "mcp_autostart"}, nil) + ok := policyErr == nil + if !ok || policy["mcp_autostart"] != "never" { + return fmt.Errorf("actual restrictive policy missing") + } + if _, ok = policy["skills_enabled"].(bool); !ok { + return fmt.Errorf("unknown skills policy type") + } + if value := policy["mcp_access"]; value != "all" && value != "none" && value != "registry" { + return fmt.Errorf("unknown MCP policy type") + } + return nil +} +func validateObserverIdentity(m map[string]any) error { + observer, observerErr := objectKeys(m["observer"], []string{"id", "mode", "proposals"}, nil) + ok := observerErr == nil + if !ok || observer["id"] != "uap-research.workspace-observer" || observer["mode"] != json.Number("3") || !reflect.DeepEqual(observer["proposals"], []any{"mcpServerDefinitions", "chatPromptFiles"}) { + return fmt.Errorf("observer identity/proposals differ") + } + return nil +} diff --git a/landing/tests/browser/landing.spec.ts b/landing/tests/browser/landing.spec.ts index e9493bc33..7695502de 100644 --- a/landing/tests/browser/landing.spec.ts +++ b/landing/tests/browser/landing.spec.ts @@ -176,6 +176,12 @@ test('below-fold sections reveal quickly on scroll without animating the hero', }) => { await page.setViewportSize({ width: 1280, height: 480 }); await page.goto('./'); + await hydrated(page); + // The enhancement establishes its hidden state and observer in two frames. + await page.evaluate(async () => { + await new Promise(requestAnimationFrame); + await new Promise(requestAnimationFrame); + }); await expect(page.locator('.hero-shell')).not.toHaveClass(/scroll-reveal/); await expect(page.locator('#plugins')).toHaveClass(/scroll-reveal/); diff --git a/repotests/agentplugins_claude_project_diagnostic_contract_test.go b/repotests/agentplugins_claude_project_diagnostic_contract_test.go new file mode 100644 index 000000000..f967bf093 --- /dev/null +++ b/repotests/agentplugins_claude_project_diagnostic_contract_test.go @@ -0,0 +1,61 @@ +package pluginkitairepo_test + +import ( + "context" + "errors" + "reflect" + "strings" + "testing" + "time" +) + +func TestClaudeProjectDiagnosticLiveCapPreservesExactBoundary(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + stream := &claudeDiagnosticStream{limit: 4, cancel: cancel} + if n, err := stream.Write([]byte("abcd")); n != 4 || err != nil || ctx.Err() != nil { + t.Fatalf("exact boundary=%d,%v", n, err) + } + if n, err := stream.Write([]byte("e")); n != 0 || err == nil || !errors.Is(ctx.Err(), context.Canceled) { + t.Fatalf("overflow not canceled=%d,%v", n, err) + } + if n, err := stream.Write([]byte(strings.Repeat("x", 4096))); n != 0 || err == nil { + t.Fatal("later writes exceeded cap") + } + body, overflow := stream.snapshot() + if string(body) != "abcd" || !overflow { + t.Fatalf("captured=%q overflow=%t", body, overflow) + } + body[0] = 'z' + snapshot, _ := stream.snapshot() + if string(snapshot) != "abcd" { + t.Fatal("snapshot aliases capture") + } +} + +func TestClaudeProjectDiagnosticFiniteReadonlySelectionAndSeparateStreams(t *testing.T) { + calls := 0 + env := []string{"HOME=/fresh/home", "CLAUDE_CONFIG_DIR=/fresh/original"} + records, logs := claudeFailureListingRecords("/verified/claude", "/fresh/A", "/fresh/B", env, func(client, cwd string, childEnv, args []string) claudeDiagnosticResult { + calls++ + if client != "/verified/claude" || cwd != []string{"/fresh/A", "/fresh/B"}[calls-1] || !reflect.DeepEqual(childEnv, env) || !reflect.DeepEqual(args, []string{"plugin", "list", "--json"}) { + t.Fatal("diagnostic selection changed") + } + childEnv[0] = "inert changed copy" + return claudeDiagnosticResult{Stdout: []byte("unknown raw JSON"), Stderr: []byte("bounded failure"), Exit: 1, Error: "original child error", Deadline: true, StderrLimit: true, Elapsed: time.Millisecond} + }) + if calls != 2 || len(records) != 2 || len(logs) != 4 || env[0] != "HOME=/fresh/home" { + t.Fatal("diagnostic count/env/log boundary changed") + } + for _, r := range records { + if r["qualification"] != false || r["exit"] != 1 || r["deadline"] != true || r["stderr_limit"] != true || r["error"] != "original child error" || r["stdout_bytes"] != len("unknown raw JSON") || r["stderr_bytes"] != len("bounded failure") { + t.Fatalf("failure metadata fabricated success: %+v", r) + } + if len(r["stdout_sha256"].(string)) != 64 || string(logs[r["stdout_file"].(string)]) != "unknown raw JSON" || string(logs[r["stderr_file"].(string)]) != "bounded failure" { + t.Fatal("raw streams/hash metadata changed") + } + } + if len(boundedClaudeDiagnosticError(errors.New(strings.Repeat("x", 2048)))) != 512 { + t.Fatal("unbounded diagnostic error") + } +} diff --git a/repotests/agentplugins_claude_project_diagnostic_test.go b/repotests/agentplugins_claude_project_diagnostic_test.go new file mode 100644 index 000000000..7c1c23dd0 --- /dev/null +++ b/repotests/agentplugins_claude_project_diagnostic_test.go @@ -0,0 +1,101 @@ +package pluginkitairepo_test + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "os/exec" + "sync" + "time" +) + +const claudeDiagnosticLimit = 1 << 20 + +// Writes are capped while exec copies each pipe; overflow cancels the direct +// read-only child. WaitDelay bounds inherited pipes, not a process sandbox. +type claudeDiagnosticStream struct { + mu sync.Mutex + body []byte + limit int + overflow bool + cancel context.CancelFunc +} + +func (s *claudeDiagnosticStream) Write(body []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + n := len(body) + if remaining := s.limit - len(s.body); n > remaining { + n = remaining + s.overflow = true + } + s.body = append(s.body, body[:n]...) + if s.overflow { + s.cancel() + return n, errors.New("Claude diagnostic output limit exceeded") + } + return n, nil +} + +func (s *claudeDiagnosticStream) snapshot() ([]byte, bool) { + s.mu.Lock() + defer s.mu.Unlock() + return append([]byte(nil), s.body...), s.overflow +} + +type claudeDiagnosticResult struct { + Stdout, Stderr []byte + Exit int + Error string + Deadline, StdoutLimit, StderrLimit bool + Elapsed time.Duration +} + +type claudeDiagnosticCapture func(string, string, []string, []string) claudeDiagnosticResult + +func captureClaudeDiagnostic(client, cwd string, env, args []string) claudeDiagnosticResult { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + stdout := &claudeDiagnosticStream{limit: claudeDiagnosticLimit, cancel: cancel} + stderr := &claudeDiagnosticStream{limit: claudeDiagnosticLimit, cancel: cancel} + command := exec.CommandContext(ctx, client, args...) + command.Dir, command.Env = cwd, append([]string(nil), env...) + command.Stdin = nil + command.Stdout, command.Stderr = stdout, stderr + command.WaitDelay = 250 * time.Millisecond + started := time.Now() + err := command.Run() + out := claudeDiagnosticResult{Exit: -1, Elapsed: time.Since(started), Deadline: errors.Is(ctx.Err(), context.DeadlineExceeded)} + out.Stdout, out.StdoutLimit = stdout.snapshot() + out.Stderr, out.StderrLimit = stderr.snapshot() + if command.ProcessState != nil { + out.Exit = command.ProcessState.ExitCode() + } + if err != nil { + out.Error = boundedClaudeDiagnosticError(err) + } + return out +} + +func boundedClaudeDiagnosticError(err error) string { + value := err.Error() + if len(value) > 512 { + return value[:512] + } + return value +} + +func claudeFailureListingRecords(client, project, other string, env []string, capture claudeDiagnosticCapture) ([]map[string]any, map[string][]byte) { + records := make([]map[string]any, 0, 2) + logs := map[string][]byte{} + for _, selected := range []struct{ label, cwd string }{{"failure-project-A-list", project}, {"failure-project-B-list", other}} { + args := []string{"plugin", "list", "--json"} + result := capture(client, selected.cwd, append([]string(nil), env...), args) + stdout, stderr := selected.label+".stdout.log", selected.label+".stderr.log" + logs[stdout], logs[stderr] = result.Stdout, result.Stderr + digest := func(body []byte) string { sum := sha256.Sum256(body); return hex.EncodeToString(sum[:]) } + records = append(records, map[string]any{"argv": append([]string{client}, args...), "cwd": selected.cwd, "exit": result.Exit, "error": result.Error, "deadline": result.Deadline, "stdout_limit": result.StdoutLimit, "stderr_limit": result.StderrLimit, "elapsed_ms": result.Elapsed.Milliseconds(), "stdout_file": stdout, "stderr_file": stderr, "stdout_bytes": len(result.Stdout), "stderr_bytes": len(result.Stderr), "stdout_sha256": digest(result.Stdout), "stderr_sha256": digest(result.Stderr), "qualification": false}) + } + return records, logs +} diff --git a/repotests/agentplugins_claude_project_native_e2e_test.go b/repotests/agentplugins_claude_project_native_e2e_test.go new file mode 100644 index 000000000..bd19cbd02 --- /dev/null +++ b/repotests/agentplugins_claude_project_native_e2e_test.go @@ -0,0 +1,103 @@ +package pluginkitairepo_test + +import ( + "os" + "path/filepath" + "testing" +) + +// This current-source gate proves prepared file lifecycle with real pinned +// Claude suppression. It does not grant trust or certify loaded project skills. +func TestAgentpluginsClaudeNativeProjectPreparedLifecycle(t *testing.T) { + if os.Getenv("AGENTPLUGINS_CLAUDE_PROJECT_NATIVE_E2E") != "1" { + t.Skip("opt-in source project native client execution") + } + nativeRequireDisposable(t) + f := newClaudeProjectNativeFixture(t) + stages := claudeProjectPreparedStages() + evidence := claudeProjectNativeIdentity(t, f) + evidence["project_contract"] = "untrusted_prepared_v1" + evidence["native_surface"] = "fresh bounded native project lists prove suppression; owned file lifecycle is prepared; no trust/model/MCP initialization" + defer f.writeEvidence(t, evidence, stages) + copyTree(t, commandsFixtureSource(t), f.base.PackageRoot) + c := commandsNativeClient{target: "claude", pkg: f.base.PackageRoot, statePath: filepath.Join(f.project, ".uap", "state-v2.json"), run: f.run} + + nativeAttempt(stages, "install", "add-installer.json") + added := c.succeed(t, "add", "add", c.pkg) + requireClaudePreparedResult(t, added) + if err := commandsPlanSelection(claudePreparedEnvelope(added), "echo", "wizard"); err != nil { + t.Fatal(err) + } + managed := f.preparedPath(t, "binding-after-add") + f.requirePreparedSkills(t, managed, "echo") + f.requireSuppression(t, "after-add", true) + stages["install"] = nativeStage{Status: "passed", Reason: "production initial add installed owned files as Prepared+Installed; native project scan remains suppressed", Artifacts: []string{"add-installer.json", "binding-after-add.json"}} + stages["prepared_command_projection"] = nativeStage{Status: "passed", Reason: "owned projected files contain exact rendered ordinary command bytes; this is filesystem evidence"} + stages["prepared_explicit_command_withheld"] = nativeStage{Status: "passed", Reason: "plan and owned projected files withhold the explicit-only command; no native loaded-skill claim"} + + nativeAttempt(stages, "unchanged_add", "add-repeated-installer.json") + before := nativeSHA(t, c.statePath) + repeated := c.succeed(t, "add-repeated", "add", c.pkg) + requireClaudePreparedResult(t, repeated) + requireClaudePreparedMutation(t, "repeated blocked add", repeated, false) + if nativeSHA(t, c.statePath) != before { + t.Fatal("repeated prepared add rewrote state") + } + f.requireSuppression(t, "after-repeated-add", true) + stages["unchanged_add"] = nativeStage{Status: "passed", Reason: "repeated add reobserved real suppression without file/state mutation"} + + nativeAttempt(stages, "unchanged_update", "update-unchanged-installer.json") + unchanged := c.succeed(t, "update-unchanged", "update", commandsPluginName) + requireClaudePreparedResult(t, unchanged) + requireClaudePreparedMutation(t, "unchanged prepared update", unchanged, false) + if nativeSHA(t, c.statePath) != before { + t.Fatal("unchanged prepared update rewrote state") + } + f.requirePreparedSkills(t, managed, "echo") + stages["unchanged_update"] = nativeStage{Status: "passed", Reason: "unchanged prepared update retained exact state and projected bytes"} + + nativeAttempt(stages, "invocation_change_update", "update-invocation-change-installer.json") + commandsSwapInvocation(t, c.pkg) + changed := c.succeed(t, "update-invocation-change", "update", commandsPluginName) + requireClaudePreparedResult(t, changed) + requireClaudePreparedMutation(t, "changed prepared update", changed, true) + if err := commandsPlanSelection(claudePreparedEnvelope(changed), "wizard", "echo"); err != nil { + t.Fatal(err) + } + if next := f.preparedPath(t, "binding-after-update"); next != managed { + t.Fatal("update changed owned binding path") + } + f.requirePreparedSkills(t, managed, "wizard") + f.requireSuppression(t, "after-update", true) + stages["invocation_change_update"] = nativeStage{Status: "passed", Reason: "production update changed owned command projection while native discovery stayed blocked"} + + nativeAttempt(stages, "intact_repair", "repair-intact-installer.json") + requireClaudePreparedResult(t, c.succeed(t, "repair-intact", "repair", commandsPluginName)) + f.requirePreparedSkills(t, managed, "wizard") + stages["intact_repair"] = nativeStage{Status: "passed", Reason: "intact repair preserved prepared files and explicit native blocker"} + + nativeAttempt(stages, "absent_repair", "repair-absent-installer.json") + nativeRequireContained(t, f.project, managed) + if err := os.RemoveAll(managed); err != nil { + t.Fatal(err) + } + f.requireSuppression(t, "after-owned-directory-loss", false) + requireClaudePreparedResult(t, c.succeed(t, "repair-absent", "repair", commandsPluginName)) + if next := f.preparedPath(t, "binding-after-repair"); next != managed { + t.Fatal("repair changed recorded owned binding") + } + f.requirePreparedSkills(t, managed, "wizard") + f.requireSuppression(t, "after-repair", true) + stages["absent_repair"] = nativeStage{Status: "passed", Reason: "production repair rebuilt missing exact-owned directory; fresh native list again suppressed it"} + + nativeAttempt(stages, "remove", "remove-installer.json") + c.succeed(t, "remove", "remove", commandsPluginName) + if _, err := os.Stat(managed); !os.IsNotExist(err) { + t.Fatalf("owned directory survived remove: %v", err) + } + f.requireSuppression(t, "after-remove", false) + stages["remove"] = nativeStage{Status: "passed", Reason: "production inactive removal deleted only owned project files; fresh native list no longer suppresses a managed directory"} + f.requireForeign(t) + stages["project_scope_preservation"] = nativeStage{Status: "passed", Reason: "foreign user/cache/project A/project B hashes remained exact; fresh project-B native lists never exposed A"} + stages["native_suppression_boundary"] = nativeStage{Status: "passed", Reason: "bounded raw pinned native A/B lists retained the untrusted project suppression boundary"} +} diff --git a/repotests/agentplugins_claude_project_native_fixture_test.go b/repotests/agentplugins_claude_project_native_fixture_test.go new file mode 100644 index 000000000..f7d88ee57 --- /dev/null +++ b/repotests/agentplugins_claude_project_native_fixture_test.go @@ -0,0 +1,105 @@ +package pluginkitairepo_test + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +type claudeProjectNativeFixture struct { + base *claudeNativeFixture + client, installer, project, otherProject string + foreign map[string]string + diagnosticAttempted bool +} + +func newClaudeProjectNativeFixture(t *testing.T) *claudeProjectNativeFixture { + t.Helper() + client := claudeNativeBinary(t, "AGENTPLUGINS_CLAUDE_BIN") + installer := claudeNativeBinary(t, "AGENTPLUGINS_INSTALLER_BIN") + if want := os.Getenv("AGENTPLUGINS_CLAUDE_SHA256"); len(want) != 64 || claudeSHA256(t, client) != want { + t.Fatal("Claude binary pin mismatch") + } + f := &claudeProjectNativeFixture{base: newClaudeNativeFixture(t), client: client, installer: installer, foreign: map[string]string{}} + f.project, f.otherProject = filepath.Join(f.base.Root, "project-A"), filepath.Join(f.base.Root, "project-B") + for _, root := range []string{f.base.ConfigDir, filepath.Join(f.project, ".claude"), filepath.Join(f.otherProject, ".claude")} { + f.keep(t, filepath.Join(root, "settings.json"), `{"enabledPlugins":{"foreign-project-fixture@skills-dir":false}}`) + f.keep(t, filepath.Join(root, "skills", "foreign-project-fixture", "SKILL.md"), "---\nname: foreign-project-fixture\ndescription: Preserve foreign project fixture\n---\nInert foreign bytes.\n") + } + f.keep(t, filepath.Join(f.base.ConfigDir, "plugins", "cache", "foreign", "keep.txt"), "foreign user plugin cache\n") + return f +} + +func (f *claudeProjectNativeFixture) keep(t *testing.T, path, body string) { + t.Helper() + nativeWrite(t, path, []byte(body), 0600) + f.foreign[path] = nativeSHA(t, path) +} + +func (f *claudeProjectNativeFixture) requireForeign(t *testing.T) { + t.Helper() + for path, want := range f.foreign { + if nativeSHA(t, path) != want { + t.Fatalf("foreign user/project configuration changed: %s", path) + } + } +} + +func (f *claudeProjectNativeFixture) run(t *testing.T, label string, args ...string) (map[string]any, error) { + t.Helper() + if len(args) > 0 && args[0] == "add" { + args = append(args, "--no-save") + } + args = append(args, "--scope", "project", "--project-root", f.project) + result, err := commandsRunInstaller(t, f.base.Root, f.project, f.base.env(filepath.Dir(f.client)), f.installer, label, args...) + if err != nil && !f.diagnosticAttempted { + f.diagnosticAttempted = true + records, logs := claudeFailureListingRecords(f.client, f.project, f.otherProject, f.base.env(filepath.Dir(f.client)), captureClaudeDiagnostic) + for name, body := range logs { + nativeWrite(t, filepath.Join(f.base.Root, name), body, 0600) + } + nativeJSON(t, filepath.Join(f.base.Root, "failure-listing-diagnostics.json"), map[string]any{"installer_label": label, "original_installer_error": boundedClaudeDiagnosticError(err), "diagnostics": records, "original_failure_preserved": true, "project_qualified": false}) + } + f.requireForeign(t) + return result, err +} + +func (f *claudeProjectNativeFixture) writeEvidence(t *testing.T, evidence map[string]any, stages map[string]nativeStage) { + t.Helper() + hashes := map[string]string{} + entries, err := os.ReadDir(f.base.Root) + if err != nil { + t.Fatal(err) + } + for _, entry := range entries { + if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".json") || strings.HasSuffix(entry.Name(), ".jsonl") || strings.HasSuffix(entry.Name(), ".log")) { + hashes[entry.Name()] = nativeSHA(t, filepath.Join(f.base.Root, entry.Name())) + } + } + evidence["stages"], evidence["artifact_sha256"], evidence["finished_utc"] = stages, hashes, time.Now().UTC().Format(time.RFC3339Nano) + nativeJSON(t, filepath.Join(f.base.Root, "claude-project-plugin-evidence.json"), evidence) + if evidence["project_contract"] == "untrusted_prepared_v1" { + if err := validateClaudePreparedStages(stages); err != nil { + t.Error(err) + } + } + if err := nativeStageFailures(stages); err != nil { + t.Error(err) + } +} + +func claudeProjectNativeIdentity(t *testing.T, f *claudeProjectNativeFixture) map[string]any { + t.Helper() + identity, err := nativeSourceIdentity(os.Getenv("AGENTPLUGINS_INSTALLER_COMMIT"), os.Getenv("AGENTPLUGINS_INSTALLER_TREE"), os.Getenv("AGENTPLUGINS_INSTALLER_PATCH_SHA256")) + if err != nil { + t.Fatal(err) + } + version := claudeVersionString(t, f.base, f.client) + if want := os.Getenv("AGENTPLUGINS_CLAUDE_VERSION"); want == "" || strings.TrimSpace(version) != want { + t.Fatalf("wrong native Claude version=%q,want=%q", version, want) + } + return map[string]any{"started_utc": time.Now().UTC().Format(time.RFC3339Nano), "os": runtime.GOOS, "arch": runtime.GOARCH, "source_identity": identity, "client": "claude", "client_version_measured": version, "client_sha256": nativeSHA(t, f.client), "installer_sha256": nativeSHA(t, f.installer), "native_surface": "fresh exact project plugin listing and details; no session/model/trust/MCP initialization", "scanner": nativeProvisionScanner(t, &nativeFixture{Root: f.base.Root})} +} diff --git a/repotests/agentplugins_claude_project_prepared_contract_test.go b/repotests/agentplugins_claude_project_prepared_contract_test.go new file mode 100644 index 000000000..8beda5cdd --- /dev/null +++ b/repotests/agentplugins_claude_project_prepared_contract_test.go @@ -0,0 +1,104 @@ +package pluginkitairepo_test + +import ( + "encoding/json" + "testing" +) + +func TestClaudePreparedEvidenceCannotCertifyNativeDiscovery(t *testing.T) { + valid := func() map[string]nativeStage { + stages := claudeProjectPreparedStages() + for name, stage := range stages { + if stage.Reason == "not reached" { + stage.Status, stage.Reason = "passed", "fixture contract exercised" + stages[name] = stage + } + } + return stages + } + if err := validateClaudePreparedStages(valid()); err != nil { + t.Fatal(err) + } + for name := range valid() { + t.Run(name, func(t *testing.T) { + stages := valid() + stage := stages[name] + if stage.Status == "passed" { + stage.Status = "not_evaluated" + } else { + stage.Status = "passed" + } + stages[name] = stage + if err := validateClaudePreparedStages(stages); err == nil { + t.Fatal("changed native/owned-file qualification contract was accepted") + } + }) + } + missing := valid() + delete(missing, "native_skill_discovery") + if err := validateClaudePreparedStages(missing); err == nil { + t.Fatal("missing native discovery boundary accepted") + } +} + +func TestClaudePreparedNativeSuppressionRequiresExactObservedShape(t *testing.T) { + sentinel := func() map[string]any { + return map[string]any{"id": "(suppressed)@skills-dir", "version": "unknown", "scope": "project", "enabled": false, "installPath": "", "notes": []string{"1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies."}} + } + for _, change := range []string{"exact", "absent", "wrong-path", "enabled", "scope", "reason", "duplicate", "loaded"} { + t.Run(change, func(t *testing.T) { + row := sentinel() + rows := make([]map[string]any, 1, 2) + rows[0] = row + want := true + switch change { + case "absent": + rows = make([]map[string]any, 0, 2) + want = false + case "wrong-path": + row["installPath"] = "/untrusted" + case "enabled": + row["enabled"] = true + case "scope": + row["scope"] = "user" + case "reason": + row["notes"] = []string{"different reason"} + case "duplicate": + rows = append(rows, sentinel()) + case "loaded": + rows = append(rows, map[string]any{"id": claudeCommandsPluginID}) + } + body, err := json.Marshal(rows) + if err != nil { + t.Fatal(err) + } + err = validateClaudeProjectSuppression(body, want) + if (err == nil) != (change == "exact" || change == "absent") { + t.Fatalf("native qualification change=%s err=%v", change, err) + } + }) + } +} + +func TestClaudePreparedNativeSuppressionRejectsUnknownJSON(t *testing.T) { + for _, body := range []string{"null", `{}`, `[] {}`, `[{"id":"x","id":"y"}]`, `[false]`} { + if err := validateClaudeProjectSuppression([]byte(body), false); err == nil { + t.Fatalf("malformed native list accepted: %s", body) + } + } +} + +func TestClaudePreparedAssertionSupportsPublicFirstAddAndReplayShapes(t *testing.T) { + outcome := map[string]any{"activation": map[string]any{"activation": "prepared", "verification": "installation_verified", "user_actions": []any{"native suppression"}}} + for _, envelope := range []map[string]any{ + {"data": map[string]any{"targets": []any{map[string]any{"output": map[string]any{"result": outcome}}}}}, + {"data": map[string]any{"result": outcome}}, + } { + before, _ := json.Marshal(envelope) + requireClaudePreparedResult(t, envelope) + after, _ := json.Marshal(envelope) + if string(before) != string(after) { + t.Fatal("assertion view changed the recorded public envelope") + } + } +} diff --git a/repotests/agentplugins_claude_project_prepared_fixture_test.go b/repotests/agentplugins_claude_project_prepared_fixture_test.go new file mode 100644 index 000000000..5e3933e03 --- /dev/null +++ b/repotests/agentplugins_claude_project_prepared_fixture_test.go @@ -0,0 +1,212 @@ +package pluginkitairepo_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func claudeProjectPreparedStages() map[string]nativeStage { + stages := map[string]nativeStage{} + for _, name := range []string{"install", "unchanged_add", "unchanged_update", "invocation_change_update", "intact_repair", "absent_repair", "remove", "project_scope_preservation", "native_suppression_boundary", "prepared_command_projection", "prepared_explicit_command_withheld"} { + stages[name] = nativeStage{Status: "not_evaluated", Reason: "not reached"} + } + for _, name := range []string{"native_skill_discovery", "command_skill_discovery"} { + stages[name] = nativeStage{Status: "blocked", Reason: "project trust is not granted; filesystem projection does not prove native discovery"} + } + for _, name := range []string{"native_mcp_initialization", "real_model_command_use", "explicit_invocation_enforced"} { + stages[name] = nativeStage{Status: "not_evaluated", Reason: "no trust, MCP initialization, model invocation or explicit invocation enforcement is exercised"} + } + return stages +} + +func validateClaudePreparedStages(stages map[string]nativeStage) error { + expected := claudeProjectPreparedStages() + if len(stages) != len(expected) { + return fmt.Errorf("prepared evidence has an unexpected stage set") + } + for name, contract := range expected { + stage, ok := stages[name] + want := contract.Status + if want == "not_evaluated" && contract.Reason == "not reached" { + want = "passed" + } + if !ok || stage.Status != want || stage.Reason == "" { + return fmt.Errorf("prepared evidence stage %s is %q, want %q with reason", name, stage.Status, want) + } + } + return nil +} + +// The public first-add envelope is per-target; repeats may use the established +// single-result shape. Normalize only the assertion view, never recorded output. +func claudePreparedEnvelope(result map[string]any) map[string]any { + data, _ := result["data"].(map[string]any) + if _, ok := data["targets"]; ok { + return result + } + if outcome, ok := data["result"].(map[string]any); ok { + return map[string]any{"data": map[string]any{"targets": []any{map[string]any{"output": map[string]any{"result": outcome}}}}} + } + return result +} + +func requireClaudePreparedMutation(t *testing.T, label string, result map[string]any, want bool) { + t.Helper() + commandsRequireMutated(t, label, claudePreparedEnvelope(result), want) +} + +func requireClaudePreparedResult(t *testing.T, result map[string]any) { + t.Helper() + out, err := commandsTargetOutcome(claudePreparedEnvelope(result)) + if err != nil { + t.Fatal(err) + } + activation, _ := out["activation"].(map[string]any) + if activation["activation"] != "prepared" || activation["verification"] != "installation_verified" || activation["activation_attested"] == true { + t.Fatalf("native suppression manufactured active result: %+v", activation) + } + actions, _ := activation["user_actions"].([]any) + if len(actions) == 0 { + t.Fatal("prepared result omitted fresh native guidance") + } +} + +// The public persisted binding provides file authority; a suppression sentinel +// deliberately supplies no package id or path. It cannot be used as ownership. +func (f *claudeProjectNativeFixture) preparedPath(t *testing.T, label string) string { + t.Helper() + body, err := os.ReadFile(filepath.Join(f.project, ".uap", "state-v2.json")) + if err != nil { + t.Fatal(err) + } + var state domain.StateFileV2 + if err := json.Unmarshal(body, &state); err != nil { + t.Fatal(err) + } + var selected *domain.ClientBinding + for _, installation := range state.Installations { + if installation.DeclaredName != commandsPluginName { + continue + } + for _, binding := range installation.Clients { + if binding.ClientID == "claude" && binding.Scope == "project" { + if selected != nil { + t.Fatal("ambiguous project binding") + } + copied := binding + selected = &copied + } + } + } + if selected == nil || selected.Activation != domain.ActivationPrepared || selected.Verification != domain.VerificationInstalled || filepath.Dir(selected.TargetLocator) != filepath.Join(f.project, ".claude", "skills") { + t.Fatalf("unrecognized prepared binding=%+v", selected) + } + nativeRequireContained(t, f.project, selected.TargetLocator) + owned := 0 + for _, object := range selected.NativeObjects { + if object.Kind == "managed_package_directory" && object.Path == selected.TargetLocator && object.ManagedDigest != "" { + owned++ + } + } + if owned != 1 { + t.Fatal("project binding lacks unique owned directory receipt") + } + nativeJSON(t, filepath.Join(f.base.Root, label+".json"), map[string]any{"binding_id": selected.ClientBindingID, "scope": selected.Scope, "target_path": selected.TargetLocator, "activation": selected.Activation, "verification": selected.Verification, "evidence_kind": "owned_files_not_native_discovery"}) + return selected.TargetLocator +} + +func (f *claudeProjectNativeFixture) requirePreparedSkills(t *testing.T, managed, command string) { + t.Helper() + root := filepath.Join(managed, ".agentplugins-runtime", "skills") + entries, err := os.ReadDir(root) + if err != nil { + t.Fatal(err) + } + files := map[string]string{} + for _, entry := range entries { + if entry.IsDir() { + files[entry.Name()] = filepath.Join(root, entry.Name(), "SKILL.md") + } + } + if err := commandsCheckSkills(files, command); err != nil { + t.Fatalf("prepared filesystem projection: %v", err) + } +} + +func (f *claudeProjectNativeFixture) requireSuppression(t *testing.T, label string, want bool) { + t.Helper() + for _, selected := range []struct { + suffix, cwd string + want bool + }{{"A", f.project, want}, {"B", f.otherProject, false}} { + result := captureClaudeDiagnostic(f.client, selected.cwd, f.base.env(filepath.Dir(f.client)), []string{"plugin", "list", "--json"}) + prefix := label + "-native-" + selected.suffix + nativeWrite(t, filepath.Join(f.base.Root, prefix+".stdout.log"), result.Stdout, 0600) + nativeWrite(t, filepath.Join(f.base.Root, prefix+".stderr.log"), result.Stderr, 0600) + nativeJSON(t, filepath.Join(f.base.Root, prefix+".json"), map[string]any{"argv": []string{f.client, "plugin", "list", "--json"}, "cwd": selected.cwd, "exit": result.Exit, "error": result.Error, "deadline": result.Deadline, "stdout_limit": result.StdoutLimit, "stderr_limit": result.StderrLimit, "elapsed_ms": result.Elapsed.Milliseconds(), "expected_suppression": selected.want}) + if result.Exit != 0 || result.Error != "" || result.Deadline || result.StdoutLimit || result.StderrLimit { + t.Fatalf("bounded native listing failed: %+v", result) + } + if err := validateClaudeProjectSuppression(result.Stdout, selected.want); err != nil { + t.Fatal(err) + } + } + f.requireForeign(t) +} + +func validateClaudeProjectSuppression(body []byte, want bool) error { + decoder := json.NewDecoder(bytes.NewReader(body)) + value, err := shared.DecodeUniqueJSONValue(decoder) + if err != nil { + return err + } + if _, err := decoder.Token(); !errors.Is(err, io.EOF) { + return fmt.Errorf("trailing native list output") + } + rows, ok := value.([]any) + if !ok { + return fmt.Errorf("native list is not an array") + } + count := 0 + for _, value := range rows { + row, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("native list row is not an object") + } + if row["id"] == claudeCommandsPluginID { + return fmt.Errorf("untrusted project unexpectedly loaded exact plugin") + } + if row["id"] != "(suppressed)@skills-dir" { + id, idOK := row["id"].(string) + scope, scopeOK := row["scope"].(string) + _, enabledOK := row["enabled"].(bool) + path, pathOK := row["installPath"].(string) + if !idOK || id == "" || !scopeOK || scope == "" || !enabledOK || !pathOK || !filepath.IsAbs(path) || filepath.Clean(path) != path { + return fmt.Errorf("malformed unrelated native row") + } + continue + } + count++ + notes, ok := row["notes"].([]any) + if len(row) != 6 || row["version"] != "unknown" || row["scope"] != "project" || row["enabled"] != false || row["installPath"] != "" || !ok || len(notes) != 1 || notes[0] != "1 project-scope directory under ./.claude/skills/ that may load as a plugin was skipped because this workspace was not trusted when plugins were scanned. After accepting the trust dialog, run /reload-plugins (or relaunch) to load what qualifies." { + return fmt.Errorf("unrecognized native suppression=%+v", row) + } + } + expected := 0 + if want { + expected = 1 + } + if count != expected { + return fmt.Errorf("native suppression count=%d,want=%d", count, expected) + } + return nil +} diff --git a/repotests/agentplugins_claude_project_trusted_native_test.go b/repotests/agentplugins_claude_project_trusted_native_test.go new file mode 100644 index 000000000..6da536952 --- /dev/null +++ b/repotests/agentplugins_claude_project_trusted_native_test.go @@ -0,0 +1,162 @@ +package pluginkitairepo_test + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" +) + +// This positive discovery fixture authors documented trust in its own original +// disposable profile. It never changes production trust or the untrusted gate. +func TestAgentpluginsClaudeNativeTrustedProjectDiscovery(t *testing.T) { + if os.Getenv("AGENTPLUGINS_CLAUDE_PROJECT_NATIVE_E2E") != "1" { + t.Skip("opt-in source trusted project discovery") + } + nativeRequireDisposable(t) + f := newClaudeProjectNativeFixture(t) + trust, err := json.Marshal(map[string]any{"projects": map[string]any{ + filepath.ToSlash(f.project): map[string]any{"hasTrustDialogAccepted": true}, + filepath.ToSlash(f.otherProject): map[string]any{"hasTrustDialogAccepted": true}, + }}) + if err != nil { + t.Fatal(err) + } + nativeWrite(t, filepath.Join(f.base.ConfigDir, ".claude.json"), trust, 0600) + nativeWrite(t, filepath.Join(f.base.Root, "trust-profile-authored.json"), trust, 0600) + nativeJSON(t, filepath.Join(f.base.Root, "trust-fixture.json"), map[string]any{"authored_before_baseline": true, "config_path": filepath.Join(f.base.ConfigDir, ".claude.json"), "configuration": json.RawMessage(trust), "production_trust_mutation": false}) + copyTree(t, commandsFixtureSource(t), f.base.PackageRoot) + stages := map[string]nativeStage{} + for _, name := range []string{"profile_initialization", "install", "native_project_discovery", "remove", "scope_preservation"} { + stages[name] = nativeStage{Status: "not_evaluated", Reason: "not reached"} + } + stages["native_mcp_initialization"] = nativeStage{Status: "not_evaluated", Reason: "skills-only listing/details fixture"} + stages["model_use"] = nativeStage{Status: "not_evaluated", Reason: "no session/model/account or trust-dialog execution"} + evidence := claudeProjectNativeIdentity(t, f) + evidence["project_contract"], evidence["native_surface"] = "trusted_project_discovery_v1", "documented fixture-authored exact project trust; genuine plugin list/details and owned skill bytes; no MCP/model/authentication claim" + defer f.writeEvidence(t, evidence, stages) + defer claudeTrustedRecordProfile(t, f, "trust-profile-final.json") + nativeAttempt(stages, "profile_initialization", "trust-profile-initialized.json") + claudeTrustedInitializeProfile(t, f, trust) + stages["profile_initialization"] = nativeStage{Status: "passed", Reason: "fixed genuine A/B initialization preserved authored trust and foreign files; one native-produced profile baseline remained byte exact through fixed second A/B controls"} + c := commandsNativeClient{target: "claude", pkg: f.base.PackageRoot, run: f.run} + nativeAttempt(stages, "install", "trusted-add-installer.json") + added := c.succeed(t, "trusted-add", "add", c.pkg) + outcome, err := commandsTargetOutcome(claudePreparedEnvelope(added)) + if err != nil { + t.Fatal(err) + } + plan, _ := outcome["plan"].(map[string]any) + activation, _ := outcome["activation"].(map[string]any) + if plan["client_id"] != "claude" || plan["scope"] != "project" || activation["activation"] != "active" || activation["verification"] != "installation_verified" || activation["activation_attested"] == true { + t.Fatalf("trusted native discovery did not reconcile exact project identity: %v", outcome) + } + physical, _ := plan["physical_artifact_id"].(string) + if physical == "" { + t.Fatal("exact project artifact missing") + } + managed := filepath.Join(f.project, ".claude", "skills", physical) + nativeRequireContained(t, f.project, managed) + stages["install"] = nativeStage{Status: "passed", Reason: "public UAP project add reconciled genuine exact trusted native identity without attestation"} + nativeAttempt(stages, "native_project_discovery", "trusted-installed-native-A.stdout.log") + state := filepath.Join(f.project, ".uap", "state-v2.json") + before := nativeSHA(t, state) + claudeTrustedProjectObservation(t, f, "trusted-installed", managed, true) + if nativeSHA(t, state) != before { + t.Fatal("native observation changed installer state") + } + stages["native_project_discovery"] = nativeStage{Status: "passed", Reason: "fresh enabled project-scope listing has exact version/path and native details expose exact guide/ordinary command skills and bytes; no A plugin in trusted B"} + nativeAttempt(stages, "remove", "trusted-remove-installer.json") + c.succeed(t, "trusted-remove", "remove", commandsPluginName) + if _, err := os.Lstat(managed); !os.IsNotExist(err) { + t.Fatalf("owned project directory survived removal: %v", err) + } + claudeTrustedProjectObservation(t, f, "trusted-removed", managed, false) + stages["remove"] = nativeStage{Status: "passed", Reason: "owned project directory absent and fresh trusted A/B native listings expose no removed plugin"} + f.requireForeign(t) + stages["scope_preservation"] = nativeStage{Status: "passed", Reason: "original user profile/trust and disabled foreign user/A/B settings, skills and cache bytes remained exact"} +} + +func claudeTrustedProjectObservation(t *testing.T, f *claudeProjectNativeFixture, label, managed string, present bool) { + t.Helper() + capture := func(suffix, project string, args ...string) []byte { + t.Helper() + result := captureClaudeDiagnostic(f.client, project, f.base.env(filepath.Dir(f.client)), args) + prefix := label + "-native-" + suffix + nativeWrite(t, filepath.Join(f.base.Root, prefix+".stdout.log"), result.Stdout, 0600) + nativeWrite(t, filepath.Join(f.base.Root, prefix+".stderr.log"), result.Stderr, 0600) + nativeJSON(t, filepath.Join(f.base.Root, prefix+".json"), map[string]any{"argv": args, "cwd": project, "exit": result.Exit, "error": result.Error, "deadline": result.Deadline, "stdout_limit": result.StdoutLimit, "stderr_limit": result.StderrLimit}) + if result.Exit != 0 || result.Error != "" || result.Deadline || result.StdoutLimit || result.StderrLimit { + t.Fatal("bounded trusted native observation failed; raw evidence retained") + } + profile := claudeTrustedRecordProfile(t, f, prefix+"-profile.json") + if err := validateClaudeTrustedProfile(profile, f.project, f.otherProject); err != nil { + t.Fatal(err) + } + f.requireForeign(t) + return result.Stdout + } + for _, selected := range []struct { + suffix, project string + want bool + }{{"A", f.project, present}, {"B", f.otherProject, false}} { + body := capture(selected.suffix, selected.project, "plugin", "list", "--json") + if err := validateClaudeTrustedListing(body, f.project, managed, selected.want); err != nil { + t.Fatal(err) + } + } + if present { + details := capture("A-details", f.project, "plugin", "details", claudeCommandsPluginID) + skills, err := claudeDiscoveredSkills(string(details), managed) + if err != nil { + t.Fatal(err) + } + if err := commandsCheckSkills(skills, "echo"); err != nil { + t.Fatal(err) + } + } +} + +func validateClaudeTrustedListing(body []byte, project, managed string, present bool) error { + value, err := shared.DecodeUniqueJSONValue(json.NewDecoder(bytes.NewReader(body))) + rows, ok := value.([]any) + if err != nil || !json.Valid(body) || !ok { + return fmt.Errorf("native trusted plugin list is not a unique whole JSON array") + } + count := 0 + for _, item := range rows { + row, ok := item.(map[string]any) + if !ok { + return fmt.Errorf("native plugin list row is not an object") + } + if row["id"] == "(suppressed)@skills-dir" { + return fmt.Errorf("trusted control returned native project suppression") + } + if row["id"] != claudeCommandsPluginID { + continue + } + for _, key := range []string{"errors", "notes"} { + if value, exists := row[key]; exists { + items, ok := value.([]any) + if !ok || len(items) != 0 { + return fmt.Errorf("trusted native candidate has malformed/nonempty errors or notes") + } + } + } + count++ + if !present || row["scope"] != "project" || row["enabled"] != true || row["version"] != "1.0.0" || row["installPath"] != managed { + return fmt.Errorf("wrong trusted project native identity") + } + if value, exists := row["projectPath"]; exists && value != project { + return fmt.Errorf("native project attribution differs") + } + } + if count != 0 && !present || count != 1 && present { + return fmt.Errorf("native trusted project identity absent or duplicated") + } + return nil +} diff --git a/repotests/agentplugins_claude_project_trusted_profile_contract_test.go b/repotests/agentplugins_claude_project_trusted_profile_contract_test.go new file mode 100644 index 000000000..958fdabdd --- /dev/null +++ b/repotests/agentplugins_claude_project_trusted_profile_contract_test.go @@ -0,0 +1,67 @@ +package pluginkitairepo_test + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestClaudeTrustedProfileRequiresExactAuthoredTrust(t *testing.T) { + project := filepath.Join(t.TempDir(), "project-A") + key, _ := json.Marshal(filepath.ToSlash(project)) + valid := `{"projects":{` + string(key) + `:{"hasTrustDialogAccepted":true}},"machineID":"native-initialization"}` + if err := validateClaudeTrustedProfile([]byte(valid), project); err != nil { + t.Fatal(err) + } + for _, body := range []string{ + `null`, `[]`, `{}`, `{"projects":null}`, `{"projects":{}}`, valid + `{}`, + `{"projects":{` + string(key) + `:{"hasTrustDialogAccepted":false}}}`, + `{"projects":{` + string(key) + `:{"hasTrustDialogAccepted":"true"}}}`, + `{"projects":{` + string(key) + `:{"hasTrustDialogAccepted":true,"extra":1}}}`, + `{"projects":{` + string(key) + `:{"hasTrustDialogAccepted":true,"hasTrustDialogAccepted":false}}}`, + } { + if err := validateClaudeTrustedProfile([]byte(body), project); err == nil { + t.Fatalf("changed/malformed authored trust accepted: %s", body) + } + } + if err := validateClaudeTrustedProfile([]byte(valid), project, filepath.Join(filepath.Dir(project), "project-B")); err == nil { + t.Fatal("missing second project trust was accepted") + } +} + +func TestClaudeTrustedEmptyControlRejectsSuppressionAndManagedIdentity(t *testing.T) { + if err := validateClaudeTrustedListing([]byte(`[]`), "/A", "", false); err != nil { + t.Fatal(err) + } + for _, body := range []string{ + `null`, `{}`, `[] []`, `[false]`, `[{"id":"x","id":"y"}]`, + `[{"id":"(suppressed)@skills-dir"}]`, `[{"id":"` + claudeCommandsPluginID + `"}]`, + } { + if err := validateClaudeTrustedListing([]byte(body), "/A", "", false); err == nil { + t.Fatalf("invalid empty native control accepted: %s", body) + } + } +} + +func TestClaudeTrustedProfileReadIsBoundedAndOrdinary(t *testing.T) { + path := filepath.Join(t.TempDir(), ".claude.json") + body := bytes.Repeat([]byte(" "), claudeDiagnosticLimit) + if err := os.WriteFile(path, body, 0600); err != nil { + t.Fatal(err) + } + got, err := readClaudeTrustedProfile(path) + if err != nil || !bytes.Equal(got, body) { + t.Fatal("exact profile byte boundary changed") + } + if err := os.WriteFile(path, append(body, ' '), 0600); err != nil { + t.Fatal(err) + } + if _, err := readClaudeTrustedProfile(path); err == nil { + t.Fatal("profile byte limit was not enforced while reading") + } + if _, err := readClaudeTrustedProfile(filepath.Dir(path)); err == nil { + t.Fatal("directory was admitted as profile") + } +} diff --git a/repotests/agentplugins_claude_project_trusted_profile_test.go b/repotests/agentplugins_claude_project_trusted_profile_test.go new file mode 100644 index 000000000..5db9e0af4 --- /dev/null +++ b/repotests/agentplugins_claude_project_trusted_profile_test.go @@ -0,0 +1,110 @@ +package pluginkitairepo_test + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "sort" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/shared" +) + +// Native first-start metadata belongs to fixture setup. There is exactly one +// baseline, before public UAP; every later command keeps the whole-byte guard. +func claudeTrustedInitializeProfile(t *testing.T, f *claudeProjectNativeFixture, authored []byte) { + t.Helper() + claudeTrustedProjectObservation(t, f, "trusted-prime", "", false) + body := claudeTrustedRecordProfile(t, f, "trust-profile-initialized.json") + path := filepath.Join(f.base.ConfigDir, ".claude.json") + f.foreign[path] = fmt.Sprintf("%x", sha256.Sum256(body)) + var before, after map[string]any + if err := json.Unmarshal(authored, &before); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(body, &after); err != nil { + t.Fatal(err) + } + changed := []string{} + for key, value := range after { + if !reflect.DeepEqual(before[key], value) { + changed = append(changed, key) + } + } + sort.Strings(changed) + nativeJSON(t, filepath.Join(f.base.Root, "trust-profile-initialization.json"), map[string]any{ + "authored_sha256": fmt.Sprintf("%x", sha256.Sum256(authored)), "initialized_sha256": fmt.Sprintf("%x", sha256.Sum256(body)), + "native_changed_top_level_keys": changed, "baseline_captured_before_public_uap": true, + "baseline_rewritten": false, "baseline_capture_count": 1, + }) + claudeTrustedProjectObservation(t, f, "trusted-stable", "", false) +} + +func claudeTrustedRecordProfile(t *testing.T, f *claudeProjectNativeFixture, name string) []byte { + t.Helper() + body, err := readClaudeTrustedProfile(filepath.Join(f.base.ConfigDir, ".claude.json")) + if err != nil { + t.Fatal(err) + } + nativeWrite(t, filepath.Join(f.base.Root, name), body, 0600) + return body +} + +func readClaudeTrustedProfile(path string) (body []byte, readErr error) { + named, err := os.Lstat(path) + if err != nil { + return nil, err + } + if !named.Mode().IsRegular() { + return nil, fmt.Errorf("trusted profile is not an ordinary regular file") + } + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer func() { readErr = errors.Join(readErr, file.Close()) }() + opened, err := file.Stat() + if err != nil || !os.SameFile(named, opened) { + return nil, fmt.Errorf("trusted profile changed while opening") + } + body, err = io.ReadAll(io.LimitReader(file, claudeDiagnosticLimit+1)) + if err != nil { + return nil, err + } + if len(body) > claudeDiagnosticLimit { + return nil, fmt.Errorf("trusted profile exceeds fixed byte limit") + } + current, err := os.Lstat(path) + if err != nil || !current.Mode().IsRegular() || !os.SameFile(opened, current) { + return nil, fmt.Errorf("trusted profile changed while reading") + } + return body, nil +} + +func validateClaudeTrustedProfile(body []byte, projects ...string) error { + value, err := shared.DecodeUniqueJSONValue(json.NewDecoder(bytes.NewReader(body))) + if err != nil || !json.Valid(body) { + return fmt.Errorf("trusted profile is not unique whole JSON") + } + object, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("trusted profile is not an object") + } + entries, ok := object["projects"].(map[string]any) + if !ok { + return fmt.Errorf("trusted profile projects are missing or malformed") + } + for _, project := range projects { + entry, ok := entries[filepath.ToSlash(project)].(map[string]any) + if !ok || len(entry) != 1 || entry["hasTrustDialogAccepted"] != true { + return fmt.Errorf("authored project trust entry changed") + } + } + return nil +} diff --git a/repotests/agentplugins_codex_project_mcp_native_test.go b/repotests/agentplugins_codex_project_mcp_native_test.go new file mode 100644 index 000000000..abfae2aec --- /dev/null +++ b/repotests/agentplugins_codex_project_mcp_native_test.go @@ -0,0 +1,220 @@ +package pluginkitairepo_test + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// Trust is an explicit fixture input in the original disposable user profile. +// The separate untrusted project test remains the installer trust-boundary gate. +func TestAgentpluginsCodexNativeTrustedProjectMCP(t *testing.T) { + if os.Getenv("AGENTPLUGINS_CODEX_PROJECT_NATIVE_E2E") != "1" { + t.Skip("opt-in source trusted project MCP execution") + } + nativeRequireDisposable(t) + if _, err := os.Lstat("/etc/codex"); !os.IsNotExist(err) { + t.Fatal("system Codex config must be absent in disposable image") + } + client := nativeBinary(t, "AGENTPLUGINS_CODEX_BIN") + if want := os.Getenv("AGENTPLUGINS_CODEX_SHA256"); len(want) != 64 || nativeSHA(t, client) != want { + t.Fatal("Codex binary pin mismatch") + } + f := &codexProjectNativeFixture{base: newNativeFixture(t, true), client: client, installer: nativeBinary(t, "AGENTPLUGINS_INSTALLER_BIN"), unchanged: map[string]string{}} + f.base.ClientBinDir, f.otherProject = filepath.Dir(client), filepath.Join(f.base.Root, "project-B") + for _, project := range []string{f.base.Project, f.otherProject} { + if err := os.MkdirAll(filepath.Join(project, ".git"), 0700); err != nil { + t.Fatal(err) + } + } + provider := nativeScriptedProvider(t, f.base) + config := fmt.Sprintf("cli_auth_credentials_store = \"file\"\nmcp_oauth_credentials_store = \"file\"\nmodel_provider = \"fixture\"\nmodel = \"fixture-model\"\n[model_providers.fixture]\nname = \"Fixture\"\nbase_url = %q\nwire_api = \"responses\"\nrequires_openai_auth = false\n[analytics]\nenabled = false\n[feedback]\nenabled = false\n[projects.%q]\ntrust_level = \"trusted\"\n[projects.%q]\ntrust_level = \"trusted\"\n", provider, f.base.Project, f.otherProject) + f.foreign(t, filepath.Join(f.base.CodexHome, "config.toml"), config) + f.foreign(t, filepath.Join(f.otherProject, ".codex", "config.toml"), "# preserve control project\nmodel = 'fixture-model'\n") + f.foreign(t, filepath.Join(f.base.Project, "foreign-keep.txt"), "preserve project foreign bytes\n") + nativeJSON(t, filepath.Join(f.base.Root, "trust-fixture.json"), map[string]any{"authored_before_baseline": true, "user_config": config, "trusted_projects": []string{f.base.Project, f.otherProject}, "production_trust_mutation": false}) + f.base.writePackage(t, "A", "1.0.0") + stages := map[string]nativeStage{} + for _, name := range []string{"install", "native_project_mcp", "unchanged", "remove", "scope_preservation", "retained_data"} { + stages[name] = nativeStage{Status: "not_evaluated", Reason: "not reached"} + } + stages["model_use"] = nativeStage{Status: "not_evaluated", Reason: "no turn/start; direct MCP RPC only"} + stages["update_repair_runtime"] = nativeStage{Status: "not_evaluated", Reason: "changed update/missing repair are covered by the separate component suite; this test qualifies add/replay/removal MCP only"} + evidence := f.evidence(t, stages) + evidence["project_contract"], evidence["native_surface"] = "trusted_project_mcp_v1", "explicit fixture trust; native thread MCP initialization/direct stdio tools; no model turn/authentication/external service or network-isolation claim" + defer func() { + evidence["captured_model_provider_requests"] = len(f.base.ProviderRequests) + f.writeEvidence(t, evidence) + if err := nativeStageFailures(stages); err != nil { + t.Error(err) + } + }() + codexProjectMCPSession(t, f, f.otherProject, "control-before-add", nil, "", false) + nativeAttempt(stages, "install", "trusted-add-installer.json") + added := f.run(t, "trusted-add", "add", f.base.Package, "--no-save") + codexNativeProjectPrepared(t, added) + binding, data := codexProjectMCPBinding(t, f) + stages["install"] = nativeStage{Status: "passed", Reason: "public project add recorded exact Prepared package, MCP and data ownership"} + state := filepath.Join(f.base.Project, ".uap", "state-v2.json") + projectConfig := filepath.Join(f.base.Project, ".codex", "config.toml") + beforeState, beforeConfig := nativeSHA(t, state), nativeSHA(t, projectConfig) + requireStable := func() { + t.Helper() + if nativeSHA(t, state) != beforeState || nativeSHA(t, projectConfig) != beforeConfig { + t.Fatal("unchanged project operation/native session rewrote Prepared state or config") + } + } + nativeAttempt(stages, "native_project_mcp", "trusted-add-rpc.jsonl") + codexProjectMCPSession(t, f, f.base.Project, "trusted-add", &binding, data, true) + requireStable() + stages["native_project_mcp"] = nativeStage{Status: "passed", Reason: "exact project configuration servers initialized and direct nonce-correlated stdio tools returned bound package/data/cwd/args"} + nativeAttempt(stages, "unchanged", "trusted-repeat-add-installer.json") + for _, step := range []struct { + label string + args []string + }{{"trusted-repeat-add", []string{"add", f.base.Package, "--no-save"}}, {"trusted-unchanged-update", []string{"update", "native-proof"}}} { + result := codexProjectAssertionEnvelope(f.run(t, step.label, step.args...)) + codexNativeProjectPrepared(t, result) + commandsRequireMutated(t, step.label, result, false) + outcome, err := commandsTargetOutcome(result) + if err != nil || outcome["no_change"] != true { + t.Fatalf("unchanged project result: %v %v", outcome, err) + } + requireStable() + codexProjectMCPSession(t, f, f.base.Project, step.label, &binding, data, true) + requireStable() + } + stages["unchanged"] = nativeStage{Status: "passed", Reason: "repeat add/update preserved exact Prepared state/config and direct MCP tool/data behavior"} + codexProjectMCPSession(t, f, f.otherProject, "control-after-replay", nil, "", false) + nativeAttempt(stages, "remove", "trusted-remove-installer.json") + f.run(t, "trusted-remove", "remove", "native-proof") + codexProjectMCPSession(t, f, f.base.Project, "removed-A", nil, "", false) + codexProjectMCPSession(t, f, f.otherProject, "removed-B", nil, "", false) + stages["remove"] = nativeStage{Status: "passed", Reason: "fresh trusted A/B sessions have no fixture servers or new MCP process events after owned removal"} + marker, err := os.ReadFile(filepath.Join(data, "native-marker.txt")) + if err != nil || string(marker) != "persist-"+f.base.Nonce { + t.Fatal("removal changed retained tool data") + } + stages["retained_data"] = nativeStage{Status: "passed", Reason: "bounded tool marker remains at exact recorded project data locator"} + f.requireForeign(t) + stages["scope_preservation"] = nativeStage{Status: "passed", Reason: "original user trust/provider config, B preferences and A foreign bytes remained exact"} +} + +func codexProjectMCPBinding(t *testing.T, f *codexProjectNativeFixture) (domain.ClientBinding, string) { + t.Helper() + body, err := os.ReadFile(filepath.Join(f.base.Project, ".uap", "state-v2.json")) + if err != nil { + t.Fatal(err) + } + var state domain.StateFileV2 + if err := json.Unmarshal(body, &state); err != nil || len(state.Installations) != 1 { + t.Fatalf("exact project installation: %v", err) + } + installation := state.Installations[0] + if installation.DeclaredName != "native-proof" || len(installation.Clients) != 1 { + t.Fatal("ambiguous project package/binding") + } + for _, binding := range installation.Clients { + data := installation.DataReceipts[binding.DataReceiptID] + if binding.ClientID != "codex" || binding.Scope != "project" || binding.NativeProfileRoot != f.base.Project || binding.Activation != domain.ActivationPrepared || binding.Verification != domain.VerificationInstalled || binding.PhysicalArtifact == "" || data.DataReceiptID != binding.DataReceiptID || data.Scope != "project" || data.PhysicalBackend != binding.PhysicalArtifact || data.State != domain.DataReceiptOwned || data.OwnershipDigest == "" { + t.Fatal("project binding/data authority differs") + } + nativeRequireContained(t, filepath.Join(f.base.Project, ".uap", "managed"), binding.TargetLocator) + nativeRequireContained(t, filepath.Join(f.base.Project, ".uap", "plugin-data"), data.Locator) + owned := map[string]bool{} + for _, object := range binding.NativeObjects { + if object.Kind == "codex_project_mcp_server" { + if object.Path != filepath.Join(f.base.Project, ".codex", "config.toml") || object.ManagedDigest == "" || owned[object.LogicalName] { + t.Fatal("MCP ownership differs") + } + owned[object.LogicalName] = true + } + } + if len(owned) != 2 || !owned["default"] || !owned["explicit"] { + t.Fatal("exact project MCP receipt names missing") + } + return binding, data.Locator + } + t.Fatal("project binding absent") + return domain.ClientBinding{}, "" +} + +func codexProjectMCPSession(t *testing.T, f *codexProjectNativeFixture, project, label string, binding *domain.ClientBinding, data string, present bool) { + t.Helper() + before, err := os.ReadFile(f.base.Events) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + selected := *f.base + selected.Project = project + r := startNativeRPC(t, &selected, f.client, label) + defer r.close() + r.must(t, "initialize", map[string]any{"clientInfo": map[string]string{"name": "uap_project_mcp_fixture", "version": "1.0.0"}, "capabilities": map[string]any{"experimentalApi": true}}) + if err := json.NewEncoder(r.in).Encode(map[string]any{"method": "initialized"}); err != nil { + t.Fatal(err) + } + var thread struct { + Thread struct { + ID string `json:"id"` + } `json:"thread"` + } + if err := json.Unmarshal(r.must(t, "thread/start", map[string]any{"cwd": project, "ephemeral": true, "approvalPolicy": "never", "sandbox": "read-only", "model": "fixture-model", "modelProvider": "fixture"}), &thread); err != nil || thread.Thread.ID == "" { + t.Fatal("native project thread missing") + } + var inventory struct { + Data []struct { + Name, PluginID, RuntimeStatus string + Tools map[string]json.RawMessage + } `json:"data"` + NextCursor *string `json:"nextCursor"` + } + if err := json.Unmarshal(r.must(t, "mcpServerStatus/list", map[string]any{"threadId": thread.Thread.ID, "limit": 100}), &inventory); err != nil || inventory.NextCursor != nil { + t.Fatal("incomplete native MCP inventory") + } + if present { + seen := map[string]bool{} + for _, server := range inventory.Data { + if binding == nil || server.PluginID != "" || (server.Name != "default" && server.Name != "explicit") || server.RuntimeStatus != "connected" || seen[server.Name] || len(server.Tools) != 1 || server.Tools["inspect_runtime"] == nil { + t.Fatalf("unexpected project MCP server: %+v", server) + } + seen[server.Name] = true + operation := "read" + if label == "trusted-add" { + operation = "write" + } + facts := nativeDecodeFacts(t, r.must(t, "mcpServer/tool/call", map[string]any{"threadId": thread.Thread.ID, "server": server.Name, "tool": "inspect_runtime", "arguments": map[string]string{"nonce": f.base.Nonce, "operation": operation, "marker": "persist-" + f.base.Nonce}})) + nativeAssertEvent(t, f.base, facts) + cwd := binding.TargetLocator + if server.Name == "explicit" { + cwd = filepath.Join(cwd, "skills") + } + if facts.Nonce != f.base.Nonce || facts.Revision != "A" || facts.Root != binding.TargetLocator || facts.Data != data || facts.CWD != cwd || facts.Marker != "persist-"+f.base.Nonce || facts.Literal != "literal ${UNKNOWN} $HOME" || facts.Once != facts.Root+"/${UNKNOWN}" || len(facts.Argv) != 3 || facts.Argv[0] != "argument with spaces" || facts.Argv[1] != facts.Root+"/argument" || facts.Argv[2] != "${UNKNOWN}" { + t.Fatalf("native project runtime facts differ: %+v", facts) + } + } + if len(seen) != 2 { + t.Fatal("native project MCP servers missing") + } + } else if len(inventory.Data) != 0 { + t.Fatalf("project MCP leaked into absent/control session: %+v", inventory.Data) + } + r.close() + if !present { + after, err := os.ReadFile(f.base.Events) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Fatal("absent/control session started fixture MCP processes") + } + } + if len(f.base.ProviderRequests) != 0 { + t.Fatal("direct MCP test unexpectedly requested model inference") + } + f.requireForeign(t) +} diff --git a/repotests/agentplugins_codex_project_native_e2e_test.go b/repotests/agentplugins_codex_project_native_e2e_test.go new file mode 100644 index 000000000..1211f66d4 --- /dev/null +++ b/repotests/agentplugins_codex_project_native_e2e_test.go @@ -0,0 +1,181 @@ +package pluginkitairepo_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +// This source-only suite uses genuine Codex skills/list after production UAP +// project operations. It does not attest activation, establish native trust, +// initialize MCP, start a thread or issue a model request. +func TestAgentpluginsCodexNativeProjectComponentsLifecycle(t *testing.T) { + if os.Getenv("AGENTPLUGINS_CODEX_PROJECT_NATIVE_E2E") != "1" { + t.Skip("opt-in source project native client execution") + } + nativeRequireDisposable(t) + if _, err := os.Lstat("/etc/codex"); !os.IsNotExist(err) { + t.Fatal("system Codex config must be absent in disposable image") + } + f := newCodexProjectNativeFixture(t) + stages := map[string]nativeStage{} + for _, name := range []string{"project_install", "project_unchanged_add", "project_unchanged_update", "project_update", "project_missing_skill_repair", "project_remove", "project_foreign_preservation"} { + stages[name] = nativeStage{Status: "not_evaluated", Reason: "not reached"} + } + stages["native_mcp_initialization"] = nativeStage{Status: "not_evaluated", Reason: "skills-only fixture; no trust mutation or MCP initialization"} + stages["model_use"] = nativeStage{Status: "not_evaluated", Reason: "discovery-only RPC; no thread or model request"} + evidence := f.evidence(t, stages) + defer func() { + f.writeEvidence(t, evidence) + if err := nativeStageFailures(stages); err != nil { + t.Error(err) + } + }() + codexNativeProjectInstall(t, f, stages) + codexNativeProjectUnchanged(t, f, stages) + codexNativeProjectUpdate(t, f, stages) + codexNativeProjectRepair(t, f, stages) + codexNativeProjectRemove(t, f, stages) + f.requireForeign(t) + stages["project_foreign_preservation"] = nativeStage{Status: "passed", Reason: "user config/cache and both projects' foreign configuration and skills retain exact bytes"} +} + +func codexNativeProjectPrepared(t *testing.T, result map[string]any) { + t.Helper() + outcome, err := commandsTargetOutcome(result) + if err != nil { + t.Fatal(err) + } + activation, _ := outcome["activation"].(map[string]any) + if activation["activation"] != string(domain.ActivationPrepared) || activation["verification"] != string(domain.VerificationInstalled) || activation["activation_attested"] == true { + t.Fatalf("project file outcome manufactured activation proof: %v", activation) + } +} + +func codexNativeProjectSkills(t *testing.T, f *codexProjectNativeFixture, label, command string) { + t.Helper() + if err := commandsCheckSkills(f.skills(t, label+"-project-A", f.base.Project), command); err != nil { + t.Fatal(err) + } + if leaked := f.skills(t, label+"-project-B", f.otherProject); len(leaked) != 0 { + t.Fatalf("project A skills leaked into project B: %v", leaked) + } +} + +func codexNativeProjectInstall(t *testing.T, f *codexProjectNativeFixture, stages map[string]nativeStage) { + t.Helper() + nativeAttempt(stages, "project_install", "project-add-installer.json") + added := f.run(t, "project-add", "add", f.base.Package, "--no-save") + codexNativeProjectPrepared(t, added) + if err := commandsPlanSelection(added, "echo", "wizard"); err != nil { + t.Fatal(err) + } + codexNativeProjectSkills(t, f, "skills-after-project-add", "echo") + stages["project_install"] = nativeStage{Status: "passed", Reason: "UAP project add remains prepared; genuine Codex discovers exact project-A repository skill paths and rendered bytes, with no fixture skills in project B"} +} + +func codexNativeProjectUnchanged(t *testing.T, f *codexProjectNativeFixture, stages map[string]nativeStage) { + t.Helper() + for _, step := range []struct { + stage, label string + args []string + }{ + {"project_unchanged_add", "project-add-repeated", []string{"add", f.base.Package, "--no-save"}}, + {"project_unchanged_update", "project-update-unchanged", []string{"update", commandsPluginName}}, + } { + nativeAttempt(stages, step.stage, step.label+"-installer.json") + before := map[string]string{} + for _, path := range []string{ + filepath.Join(f.base.Project, ".uap", "state-v2.json"), + filepath.Join(f.base.Project, ".agents", "skills", commandsGuideSkill, "SKILL.md"), + filepath.Join(f.base.Project, ".agents", "skills", commandsSkill("echo"), "SKILL.md"), + } { + before[path] = nativeSHA(t, path) + } + result := codexProjectAssertionEnvelope(f.run(t, step.label, step.args...)) + codexNativeProjectPrepared(t, result) + commandsRequireMutated(t, step.label, result, false) + outcome, err := commandsTargetOutcome(result) + if err != nil { + t.Fatal(err) + } + plan, _ := outcome["plan"].(map[string]any) + if outcome["no_change"] != true || plan["client_id"] != "codex" || plan["scope"] != "project" { + t.Fatalf("unchanged operation lost its exact project Codex result: %v", outcome) + } + if err := commandsPlanSelection(result, "echo", "wizard"); err != nil { + t.Fatal(err) + } + requireUnchanged := func() { + t.Helper() + for path, digest := range before { + if nativeSHA(t, path) != digest { + t.Fatalf("%s changed stable project state or owned skill bytes: %s", step.label, path) + } + } + } + requireUnchanged() + codexNativeProjectSkills(t, f, "skills-after-"+step.label, "echo") + requireUnchanged() + stages[step.stage] = nativeStage{Status: "passed", Reason: "public unchanged operation reports mutated=false and no_change=true; exact project state/owned skill bytes persist before and after fresh native A/B discovery", Artifacts: []string{step.label + "-installer.json"}} + } +} + +// A replay may use the public single-result envelope. Wrap only the assertion +// view; commandsRunInstaller retains the original JSON bytes as evidence. +func codexProjectAssertionEnvelope(result map[string]any) map[string]any { + data, _ := result["data"].(map[string]any) + if _, ok := data["targets"]; ok { + return result + } + if outcome, ok := data["result"].(map[string]any); ok { + return map[string]any{"data": map[string]any{"targets": []any{map[string]any{"output": map[string]any{"result": outcome}}}}} + } + return result +} + +func codexNativeProjectUpdate(t *testing.T, f *codexProjectNativeFixture, stages map[string]nativeStage) { + t.Helper() + nativeAttempt(stages, "project_update", "project-update-installer.json") + commandsSwapInvocation(t, f.base.Package) + updated := f.run(t, "project-update", "update", commandsPluginName) + codexNativeProjectPrepared(t, updated) + commandsRequireMutated(t, "project update", updated, true) + if err := commandsPlanSelection(updated, "wizard", "echo"); err != nil { + t.Fatal(err) + } + codexNativeProjectSkills(t, f, "skills-after-project-update", "wizard") + stages["project_update"] = nativeStage{Status: "passed", Reason: "UAP update replaces the command skill after invocation rules swap; fresh Codex discovery sees only the newly ordinary command with exact bytes"} +} + +func codexNativeProjectRepair(t *testing.T, f *codexProjectNativeFixture, stages map[string]nativeStage) { + t.Helper() + nativeAttempt(stages, "project_missing_skill_repair", "project-repair-installer.json") + missing := filepath.Join(f.base.Project, ".agents", "skills", commandsSkill("wizard")) + nativeRequireContained(t, f.base.Project, missing) + if err := os.RemoveAll(missing); err != nil { + t.Fatal(err) + } + if found := f.skills(t, "skills-with-project-command-missing", f.base.Project); len(found) != 1 || found[commandsGuideSkill] == "" { + t.Fatalf("native discovery did not observe the missing owned command skill: %v", found) + } + repaired := f.run(t, "project-repair", "repair", commandsPluginName) + codexNativeProjectPrepared(t, repaired) + codexNativeProjectSkills(t, f, "skills-after-project-repair", "wizard") + stages["project_missing_skill_repair"] = nativeStage{Status: "passed", Reason: "fresh Codex observes the missing owned directory before repair and exact restored command skill after UAP repair"} +} + +func codexNativeProjectRemove(t *testing.T, f *codexProjectNativeFixture, stages map[string]nativeStage) { + t.Helper() + nativeAttempt(stages, "project_remove", "project-remove-installer.json") + f.run(t, "project-remove", "remove", commandsPluginName) + for _, project := range []string{f.base.Project, f.otherProject} { + label := "skills-after-project-remove-" + filepath.Base(project) + if remaining := f.skills(t, label, project); len(remaining) != 0 { + t.Fatalf("native Codex still discovers removed fixture skills: %v", remaining) + } + } + stages["project_remove"] = nativeStage{Status: "passed", Reason: "fresh native Codex processes list no removed fixture skills in either project after UAP removal"} +} diff --git a/repotests/agentplugins_codex_project_native_fixture_test.go b/repotests/agentplugins_codex_project_native_fixture_test.go new file mode 100644 index 000000000..293b1d93f --- /dev/null +++ b/repotests/agentplugins_codex_project_native_fixture_test.go @@ -0,0 +1,122 @@ +package pluginkitairepo_test + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +type codexProjectNativeFixture struct { + base *nativeFixture + client, installer string + otherProject string + unchanged map[string]string +} + +func newCodexProjectNativeFixture(t *testing.T) *codexProjectNativeFixture { + t.Helper() + client := nativeBinary(t, "AGENTPLUGINS_CODEX_BIN") + installer := nativeBinary(t, "AGENTPLUGINS_INSTALLER_BIN") + if expected := os.Getenv("AGENTPLUGINS_CODEX_SHA256"); len(expected) != 64 || nativeSHA(t, client) != expected { + t.Fatal("Codex binary pin mismatch") + } + f := &codexProjectNativeFixture{base: newNativeFixture(t, true), client: client, installer: installer, unchanged: map[string]string{}} + f.base.ClientBinDir, f.otherProject = filepath.Dir(client), filepath.Join(f.base.Root, "project-B") + for _, project := range []string{f.base.Project, f.otherProject} { + if err := os.MkdirAll(filepath.Join(project, ".git"), 0700); err != nil { + t.Fatal(err) + } + } + config := "cli_auth_credentials_store = \"file\"\nmcp_oauth_credentials_store = \"file\"\n[analytics]\nenabled = false\n[feedback]\nenabled = false\n" + f.foreign(t, filepath.Join(f.base.CodexHome, "config.toml"), config) + f.foreign(t, filepath.Join(f.base.Project, ".codex", "config.toml"), "# preserve project preferences\nmodel = 'unobserved-model'\n") + f.foreign(t, filepath.Join(f.otherProject, ".codex", "config.toml"), "# preserve project B\nmodel = 'project-B-model'\n") + f.foreign(t, filepath.Join(f.base.CodexHome, "plugins", "cache", "foreign", "keep.txt"), "foreign user plugin cache\n") + for _, root := range []string{f.base.Home, f.base.Project, f.otherProject} { + f.foreign(t, filepath.Join(root, ".agents", "skills", "foreign-codex-project", "SKILL.md"), "---\nname: foreign-codex-project\ndescription: Inert foreign fixture\n---\nPreserve foreign bytes.\n") + } + copyTree(t, commandsFixtureSource(t), f.base.Package) + return f +} + +func (f *codexProjectNativeFixture) foreign(t *testing.T, path, body string) { + t.Helper() + nativeWrite(t, path, []byte(body), 0600) + f.unchanged[path] = nativeSHA(t, path) +} + +func (f *codexProjectNativeFixture) requireForeign(t *testing.T) { + t.Helper() + for path, digest := range f.unchanged { + if nativeSHA(t, path) != digest { + t.Fatalf("foreign user/project-A/project-B bytes changed: %s", path) + } + } +} + +func (f *codexProjectNativeFixture) run(t *testing.T, label string, args ...string) map[string]any { + t.Helper() + args = append(args, "--target", "codex", "--scope", "project", "--project-root", f.base.Project) + result, err := commandsRunInstaller(t, f.base.Root, f.base.Project, f.base.env(filepath.Dir(f.client)), f.installer, label, args...) + if err != nil || result["result"] != "success" { + t.Fatalf("%s failed: %v; %v", label, err, result) + } + f.requireForeign(t) + return result +} + +func (f *codexProjectNativeFixture) skills(t *testing.T, label, project string) map[string]string { + t.Helper() + selected := *f.base + selected.Project = project + r := startNativeRPC(t, &selected, f.client, label) + defer r.close() + r.must(t, "initialize", map[string]any{"clientInfo": map[string]string{"name": "uap_project_fixture", "version": "1.0.0"}, "capabilities": map[string]any{"experimentalApi": true}}) + if err := json.NewEncoder(r.in).Encode(map[string]any{"method": "initialized"}); err != nil { + t.Fatal(err) + } + found, err := codexProjectDiscoveredSkills(r.must(t, "skills/list", map[string]any{"cwds": []string{project}, "forceReload": true}), project) + if err != nil { + t.Fatal(err) + } + f.requireForeign(t) + return found +} + +func (f *codexProjectNativeFixture) evidence(t *testing.T, stages map[string]nativeStage) map[string]any { + t.Helper() + identity, err := nativeSourceIdentity(os.Getenv("AGENTPLUGINS_INSTALLER_COMMIT"), os.Getenv("AGENTPLUGINS_INSTALLER_TREE"), os.Getenv("AGENTPLUGINS_INSTALLER_PATCH_SHA256")) + if err != nil { + t.Fatal(err) + } + version, err := nativeCommand(t, f.base, f.client, "client-version", "--version") + if want := os.Getenv("AGENTPLUGINS_CODEX_VERSION"); err != nil || want == "" || strings.TrimSpace(string(version)) != "codex-cli "+want { + t.Fatalf("wrong pinned client: %s %v", version, err) + } + return map[string]any{"started_utc": time.Now().UTC().Format(time.RFC3339Nano), "os": runtime.GOOS, "arch": runtime.GOARCH, + "client": "codex", "client_version_measured": strings.TrimSpace(string(version)), "client_sha256": nativeSHA(t, f.client), + "installer_sha256": nativeSHA(t, f.installer), "source_identity": identity, "acquisition": "local_directory", + "native_surface": "fresh app-server skills/list with exact selected-project repository paths; no thread/model/trust/MCP initialization", + "scanner": nativeProvisionScanner(t, f.base), "stages": stages} +} + +func (f *codexProjectNativeFixture) writeEvidence(t *testing.T, evidence map[string]any) { + t.Helper() + evidence["finished_utc"] = time.Now().UTC().Format(time.RFC3339Nano) + hashes := map[string]string{} + entries, err := os.ReadDir(f.base.Root) + if err != nil { + t.Fatal(err) + } + for _, entry := range entries { + if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".json") || strings.HasSuffix(entry.Name(), ".jsonl") || strings.HasSuffix(entry.Name(), ".log")) { + hashes[entry.Name()] = nativeSHA(t, filepath.Join(f.base.Root, entry.Name())) + } + } + evidence["artifact_sha256"] = hashes + nativeJSON(t, filepath.Join(f.base.Root, "codex-project-components-evidence.json"), evidence) +} diff --git a/repotests/agentplugins_codex_project_native_skills_test.go b/repotests/agentplugins_codex_project_native_skills_test.go new file mode 100644 index 000000000..0071510dd --- /dev/null +++ b/repotests/agentplugins_codex_project_native_skills_test.go @@ -0,0 +1,81 @@ +package pluginkitairepo_test + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + "testing" +) + +// codexProjectDiscoveredSkills accepts the exact repository paths returned by +// native skills/list. A plugin, disabled item, duplicate, wrong scope or other +// project's same-named skill cannot satisfy project discovery. +func codexProjectDiscoveredSkills(listing []byte, project string) (map[string]string, error) { + var decoded struct { + Data []struct { + CWD string `json:"cwd"` + Errors []any `json:"errors"` + Skills []struct { + Name, Path, Scope string + PluginID string `json:"pluginId"` + Enabled bool + } `json:"skills"` + } `json:"data"` + } + if err := json.Unmarshal(listing, &decoded); err != nil { + return nil, err + } + if len(decoded.Data) != 1 || !openCodeSamePath(decoded.Data[0].CWD, project) || len(decoded.Data[0].Errors) != 0 { + return nil, fmt.Errorf("Codex skills/list has a different cwd or discovery errors") + } + found := map[string]string{} + for _, skill := range decoded.Data[0].Skills { + if skill.Name != commandsGuideSkill && !strings.HasPrefix(skill.Name, commandsPluginName+"-") { + continue + } + expected := filepath.Join(project, ".agents", "skills", skill.Name, "SKILL.md") + if skill.PluginID != "" || skill.Scope != "repo" || !skill.Enabled || found[skill.Name] != "" || !openCodeSamePath(skill.Path, expected) { + return nil, fmt.Errorf("unexpected Codex project skill %q", skill.Name) + } + found[skill.Name] = skill.Path + } + return found, nil +} + +func TestCodexProjectDiscoveredSkills(t *testing.T) { + project := filepath.Join(t.TempDir(), "project") + entry := func(name string) map[string]any { + return map[string]any{"name": name, "path": filepath.Join(project, ".agents", "skills", name, "SKILL.md"), "scope": "repo", "enabled": true} + } + for _, name := range []string{"valid", "wrong-cwd", "errors", "disabled", "plugin", "scope", "path", "duplicate"} { + t.Run(name, func(t *testing.T) { + skill := entry(commandsGuideSkill) + group := map[string]any{"cwd": project, "skills": []any{skill, entry(commandsSkill("echo"))}} + switch name { + case "wrong-cwd": + group["cwd"] = project + "-other" + case "errors": + group["errors"] = []any{map[string]any{"message": "unreadable skill"}} + case "disabled": + skill["enabled"] = false + case "plugin": + skill["pluginId"] = "foreign@marketplace" + case "scope": + skill["scope"] = "user" + case "path": + skill["path"] = filepath.Join(project+"-other", ".agents", "skills", commandsGuideSkill, "SKILL.md") + case "duplicate": + group["skills"] = []any{skill, skill} + } + body, err := json.Marshal(map[string]any{"data": []any{group}}) + if err != nil { + t.Fatal(err) + } + found, err := codexProjectDiscoveredSkills(body, project) + if name == "valid" && (err != nil || len(found) != 2) || name != "valid" && err == nil { + t.Fatalf("found = %v, error = %v", found, err) + } + }) + } +} diff --git a/repotests/agentplugins_opencode_native_e2e_test.go b/repotests/agentplugins_opencode_native_e2e_test.go index 0f0bd1652..e5c17bd09 100644 --- a/repotests/agentplugins_opencode_native_e2e_test.go +++ b/repotests/agentplugins_opencode_native_e2e_test.go @@ -65,8 +65,8 @@ func newOpenCodeNativeFixture(t *testing.T) *openCodeNativeFixture { // env returns a bounded, isolated environment for both the `opencode` CLI and // the `agentplugins` installer: disposable HOME/XDG roots, no inherited -// ambient config, OPENCODE_PURE disables any implicit project/global merge -// that could hide the owned entry. +// ambient config. OPENCODE_PURE skips configured external plugins; project and +// global config still merge, so effective native source remains an observation. func (f *openCodeNativeFixture) env(clientDir string) []string { return append(nativePlatformEnvironment(f.Root, f.Home, clientDir), "AGENTPLUGINS_HOME="+f.StateHome, "OPENCODE_PURE=1") } diff --git a/repotests/agentplugins_opencode_project_native_contract_test.go b/repotests/agentplugins_opencode_project_native_contract_test.go new file mode 100644 index 000000000..cd1818ee5 --- /dev/null +++ b/repotests/agentplugins_opencode_project_native_contract_test.go @@ -0,0 +1,321 @@ +package pluginkitairepo_test + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestOpenCodeProjectNativeStrictJSONAndSelectedSkill(t *testing.T) { + for _, bad := range [][]byte{[]byte(`{"mcp":{},"mcp":{}}`), []byte(`{"mcp":{},"MCP":{}}`), []byte(`{} {}`), []byte(`{"x":NaN}`), {0xff}, []byte(strings.Repeat("[", 66) + strings.Repeat("]", 66))} { + if _, err := openCodeProjectJSON(bad); err == nil { + t.Fatalf("malformed whole JSON accepted: %q", bad) + } + } + root := t.TempDir() + selected := filepath.Join(root, "project", ".opencode", "skills", "guide", "SKILL.md") + want := map[string]openCodeProjectSkill{"guide": {Name: "guide", Location: selected, Content: "inert body"}} + for _, bad := range []string{`[]`, `[{"name":"guide","location":12,"content":"inert body"}]`, `[{"name":"guide","location":"x","content":"inert body"},{"name":"guide","location":"y","content":"inert body"}]`} { + found, err := openCodeProjectSkills([]byte(bad)) + if err == nil && openCodeProjectCheckSkills(found, want, nil) == nil { + t.Fatal("empty/malformed listing became discovery") + } + } + for _, location := range []string{filepath.Join(root, "other", "SKILL.md"), strings.Join([]string{filepath.Dir(selected), "..", "guide", "SKILL.md"}, string(os.PathSeparator))} { + got := map[string]openCodeProjectSkill{"guide": {Name: "guide", Location: location, Content: "inert body"}} + if err := openCodeProjectCheckSkills(got, want, nil); err == nil { + t.Fatal("foreign/traversing output normalized into selected ownership") + } + } + if err := openCodeProjectCheckSkills(want, want, nil); err != nil { + t.Fatal(err) + } + if err := openCodeProjectCheckSkills(want, want, []string{"guide"}); err == nil { + t.Fatal("withheld skill accepted") + } + body, err := openCodeProjectSkillContent([]byte("---\nname: guide\n---\n\ninert body\n")) + if err != nil || body != "inert body" { + t.Fatalf("native markdown body: %q %v", body, err) + } +} +func TestOpenCodeProjectNativeConfigSourceChecks(t *testing.T) { + good, err := openCodeProjectConfig([]byte(`{"mcp":{"marker":{"type":"remote","url":"http://127.0.0.1:9/A","enabled":true}}}`)) + if err != nil { + t.Fatal(err) + } + if err := openCodeProjectCheckConfig(good, map[string]string{"marker": "http://127.0.0.1:9/A"}, nil); err != nil { + t.Fatal(err) + } + for _, url := range []string{"http://127.0.0.1:9/B", ""} { + if err := openCodeProjectCheckConfig(good, map[string]string{"marker": url}, nil); err == nil { + t.Fatal("other source accepted") + } + } + if err := openCodeProjectCheckConfig(good, nil, []string{"marker"}); err == nil { + t.Fatal("removed member accepted") + } +} +func TestOpenCodeProjectNativeFiniteTableAndEnvironment(t *testing.T) { + table := openCodeProjectCommandTable() + counts := map[string]int{} + for _, class := range table { + counts[class]++ + } + if len(table) != 43 || counts["git"] != 4 || counts["native"] != 25 || counts["uap"] != 14 { + t.Fatalf("finite table changed: %v", counts) + } + root := t.TempDir() + f := &openCodeProjectFixture{work: root, home: filepath.Join(root, "home"), state: filepath.Join(root, "installer-state"), client: filepath.Join(root, "bin", "opencode"), git: filepath.Join(root, "git", "git")} + t.Setenv("OPENCODE_CONFIG", "ambient source") + t.Setenv("OPENCODE_CONFIG_CONTENT", "ambient inline") + t.Setenv("OPENCODE_CONFIG_DIR", "ambient dir") + t.Setenv("TOKEN", "secret") + env := map[string]string{} + for _, item := range f.env() { + key, value, ok := strings.Cut(item, "=") + if !ok || env[key] != "" { + t.Fatal("duplicate environment") + } + env[key] = value + } + for _, key := range []string{"OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_CONFIG_DIR", "OPENCODE_DISABLE_PROJECT_CONFIG", "TOKEN", "HTTPS_PROXY"} { + if env[key] != "" { + t.Fatal("ambient authority inherited", key) + } + } + if env["HOME"] != f.home || env["OPENCODE_PURE"] != "1" || env["OPENCODE_DISABLE_DEFAULT_PLUGINS"] != "1" || env["GIT_CONFIG_NOSYSTEM"] != "1" { + t.Fatal("fresh authority/control lost") + } +} +func TestOpenCodePassiveRecorderDetectsTCPAndHTTPWithoutMCP(t *testing.T) { + r := newOpenCodePassiveRecorder(t) + defer func() { + if err := r.close(); err != nil { + t.Error(err) + } + }() + if r.counts() != [2]int64{} { + t.Fatal("recorder not initially empty") + } + client := &http.Client{Timeout: time.Second} + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + request, err := http.NewRequestWithContext(ctx, http.MethodGet, r.url("inert"), http.NoBody) + if err != nil { + t.Fatal(err) + } + response, err := client.Do(request) + if err != nil { + t.Fatal(err) + } + if err := response.Body.Close(); err != nil { + t.Error(err) + } + if response.StatusCode != http.StatusServiceUnavailable || r.counts()[0] < 1 || r.counts()[1] != 1 { + t.Fatalf("passive audit missed connection/request: %v", r.counts()) + } +} +func TestProjectNativeCaptureChild(t *testing.T) { + mode := os.Getenv("UAP_PROJECT_CAPTURE_CHILD") + if mode == "" { + t.Skip("inert test subprocess only") + } + switch mode { + case "empty": + body, err := io.ReadAll(io.LimitReader(os.Stdin, 256)) + if err != nil || len(body) != 0 { + os.Exit(8) + } + if _, err := fmt.Fprint(os.Stdout, `{"ok":true}`); err != nil { + os.Exit(7) + } + case "overflow": + if _, err := fmt.Fprint(os.Stdout, strings.Repeat("x", 8192)); err != nil { + os.Exit(7) + } + case "stderr": + if _, err := fmt.Fprint(os.Stderr, strings.Repeat("x", 8192)); err != nil { + os.Exit(7) + } + case "deadline": + time.Sleep(time.Second) + default: + os.Exit(9) + } + os.Exit(0) +} +func TestProjectNativeCaptureCapsEveryChildClass(t *testing.T) { + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + for _, mode := range []string{"empty", "overflow", "stderr", "deadline"} { + t.Run(mode, func(t *testing.T) { + timeout := time.Second + if mode == "deadline" { + timeout = 30 * time.Millisecond + } + result := captureProjectNative(context.Background(), executable, t.TempDir(), []string{"UAP_PROJECT_CAPTURE_CHILD=" + mode}, []string{"-test.run=^TestProjectNativeCaptureChild$"}, 128, timeout) + if len(result.Stdout) > 128 || len(result.Stderr) > 128 { + t.Fatal("live output cap exceeded") + } + switch mode { + case "empty": + if result.Exit != 0 || string(result.Stdout) != `{"ok":true}` || len(result.Stderr) != 0 { + t.Fatalf("fixed child/empty stdin: %+v", result) + } + case "overflow": + if !result.StdoutLimit { + t.Fatal("stdout overflow not reported") + } + case "stderr": + if !result.StderrLimit { + t.Fatal("stderr overflow not reported") + } + case "deadline": + if !result.Deadline || result.ElapsedMS > 1500 { + t.Fatalf("deadline/reap not bounded: %+v", result) + } + } + }) + } +} + +func TestOpenCodeProjectCaptureRefusesWaitDelayAndRecognizesOnlyExactAbsence(t *testing.T) { + if projectNativeErrorKind(exec.ErrWaitDelay, nil) != "capture" || projectNativeErrorKind(&exec.ExitError{}, nil) != "exit" || projectNativeErrorKind(errors.Join(&exec.ExitError{}, exec.ErrWaitDelay), nil) != "capture" || projectNativeErrorKind(&exec.ExitError{}, context.Canceled) != "context" { + t.Fatal("capture/exit/context causes were conflated") + } + partial := projectNativeCaptureResult{Exit: 0, Stdout: []byte(`{"mcp":{}}`), Error: exec.ErrWaitDelay.Error(), ErrorKind: "capture"} + if projectNativeCaptureAdmission(partial, false) == nil || projectNativeCaptureAdmission(partial, true) == nil { + t.Fatal("valid partial JSON after WaitDelay was admitted") + } + first := "agentplugins: installation \"" + commandsPluginName + "\" was not found\n" + second := "agentplugins: plugin is not installed for target \"opencode\" in project scope; no target was changed\n" + for _, line := range []string{first, second} { + result := projectNativeCaptureResult{Exit: 1, Error: "exit status 1", ErrorKind: "exit", Stderr: []byte(line)} + if projectNativeCaptureAdmission(result, true) != nil || !openCodeProjectAbsentRemoval(result) { + t.Fatal("exact source absence refused") + } + } + for _, kind := range []string{"other-stderr", "extra-stdout", "extra-stderr", "signal", "capture", "context", "no-typed-cause"} { + result := projectNativeCaptureResult{Exit: 1, Error: "exit status 1", ErrorKind: "exit", Stderr: []byte(first)} + switch kind { + case "other-stderr": + result.Stderr = []byte("not found somewhere else\n") + case "extra-stdout": + result.Stdout = []byte(`{}`) + case "extra-stderr": + result.Stderr = append(result.Stderr, []byte("warning\n")...) + case "signal": + result.Exit = -1 + case "capture": + result.ErrorKind = "capture" + case "context": + result.ErrorKind = "context" + case "no-typed-cause": + result.ErrorKind = "" + } + if openCodeProjectAbsentRemoval(result) { + t.Fatal("unrecognized failed operation became absence", kind) + } + } +} +func TestOpenCodeProjectSnapshotIsBoundedStableAndDoesNotFollowLeafLinks(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "owned.json") + if err := os.WriteFile(path, []byte("inert"), 0600); err != nil { + t.Fatal(err) + } + before, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + body, err := readProjectNativeSnapshot(path, before, 5) + if err != nil || string(body) != "inert" { + t.Fatalf("stable snapshot %q %v", body, err) + } + if _, err := readProjectNativeSnapshot(path, before, 4); err == nil { + t.Fatal("snapshot cap ignored") + } + if err := os.WriteFile(path, []byte("larger replacement"), 0600); err != nil { + t.Fatal(err) + } + if _, err := readProjectNativeSnapshot(path, before, 5); err == nil { + t.Fatal("post-stat growth accepted") + } + foreign := filepath.Join(root, "foreign") + if err := os.WriteFile(foreign, []byte("inert"), 0600); err != nil { + t.Fatal(err) + } + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + if err := os.Symlink(foreign, path); err != nil { + t.Log("symlink unavailable", err) + return + } + if _, err := readProjectNativeSnapshot(path, before, 5); err == nil { + t.Fatal("leaf alias accepted") + } +} +func TestOpenCodeProjectConfigRejectsMalformedEnabled(t *testing.T) { + for _, flag := range []string{`null`, `"true"`, `0`, `false`} { + config, err := openCodeProjectConfig([]byte(`{"mcp":{"marker":{"type":"remote","url":"http://127.0.0.1:9/A","enabled":` + flag + `}}}`)) + if err != nil { + t.Fatal(err) + } + if openCodeProjectCheckConfig(config, map[string]string{"marker": "http://127.0.0.1:9/A"}, nil) == nil { + t.Fatal("malformed or disabled entry qualified", flag) + } + } +} + +func TestOpenCodeProjectPublicProgressIsExactAndLabelBound(t *testing.T) { + resolve := "Resolving and validating one Agent Plugin package for every selected target...\n" + cases := map[string]string{ + "add-user": resolve + "Installing uap-opencode-user-control for 1 selected clients...\n", + "add-A": resolve + "Installing uap-commands-fixture for 1 selected clients...\n", + "add-B": resolve + "Installing uap-commands-fixture for 1 selected clients...\n", + "add-same-user": resolve + "Installing uap-commands-fixture for 1 selected clients...\n", + "add-same-A": resolve + "Installing uap-commands-fixture for 1 selected clients...\n", + "add-json": resolve + "Installing uap-commands-fixture for 1 selected clients...\n", + "repeat-add-A": resolve + "Applying transactional client package...\n", + "unchanged-update-A": "Resolving and validating one updated Agent Plugin package for every selected target...\n", + "update-A": "Resolving and validating one updated Agent Plugin package for every selected target...\n", + "intact-repair-A": "Resolving and validating each unique exact installed package revision once...\n", + "missing-repair-A": "Resolving and validating each unique exact installed package revision once...\n", + "remove-A": "", + "remove-json": "", + } + for label, expected := range cases { + t.Run(label, func(t *testing.T) { + if !openCodeProjectProgress(label, []byte(expected)) { + t.Fatal("source-established exact progress refused") + } + for _, dirty := range []string{expected + "warning\n", " " + expected, expected + "\r\n", strings.ReplaceAll(expected, "\n", "\r\n")} { + if dirty != expected && openCodeProjectProgress(label, []byte(dirty)) { + t.Fatal("changed/extra progress bytes accepted") + } + } + if expected != "" && openCodeProjectProgress(label, nil) { + t.Fatal("missing required progress admitted") + } + }) + } + for _, label := range []string{"version", "initial-A-config", "git-A", "repeat-remove-A", "unknown"} { + if openCodeProjectProgress(label, nil) || openCodeProjectProgress(label, []byte(cases["add-A"])) { + t.Fatal("non-ordinary UAP lane acquired progress allowance", label) + } + } + if openCodeProjectProgress("repeat-add-A", []byte(cases["add-A"])) || openCodeProjectProgress("add-A", []byte(cases["repeat-add-A"])) { + t.Fatal("single/group imperative add progress conflated") + } +} diff --git a/repotests/agentplugins_opencode_project_native_e2e_test.go b/repotests/agentplugins_opencode_project_native_e2e_test.go new file mode 100644 index 000000000..3c5d87ec4 --- /dev/null +++ b/repotests/agentplugins_opencode_project_native_e2e_test.go @@ -0,0 +1,243 @@ +package pluginkitairepo_test + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +// Current-source only. Debug observations never change the persisted Prepared +// lifecycle and never claim MCP initialization, model use or trust behavior. +func TestAgentpluginsOpenCodeNativeProjectComponentsLifecycle(t *testing.T) { + if os.Getenv("AGENTPLUGINS_OPENCODE_PROJECT_NATIVE_E2E") != "1" { + t.Skip("opt-in current-source project native discovery") + } + nativeRequireDisposable(t) + f := newOpenCodeProjectFixture(t) + defer f.finish(t) + f.setup(t) + f.run(t, "add-user", "", "add", f.sourceControl, "--no-save") + added := f.run(t, "add-A", f.A, "add", f.sourceA, "--no-save") + if err := commandsPlanSelection(added, "echo", "wizard"); err != nil { + t.Fatal(err) + } + f.run(t, "add-B", f.B, "add", f.sourceB, "--no-save") + nativeAttempt(f.stages, "positive_project_discovery", "opencode-project-evidence.json") + f.observe(t, "initial-A", f.A, f.sourceA, "A-one", "echo", false) + f.observe(t, "initial-B", f.B, f.sourceB, "B-one", "echo", false) + f.binding(t, f.A) + f.binding(t, f.B) + f.stages["positive_project_discovery"] = nativeStage{Status: "passed", Reason: "fresh native config/skill JSON selects exact A/B managed marker and skill paths/bodies and retains distinct user control"} + // Snapshot only original user ownership/config/state and B's installed state, + // native files and package receipts; vendor caches are separate bootstrap effects. + userRoots := []string{filepath.Join(f.work, "xdg-config", "opencode", "opencode.jsonc"), filepath.Join(f.work, "xdg-config", "opencode", "skills", "uap-user-control"), filepath.Join(f.state, "state-v2.json")} + otherRoots := []string{filepath.Join(f.B, ".uap"), filepath.Join(f.B, ".opencode", "skills"), filepath.Join(f.B, "opencode.jsonc")} + userBefore, otherBefore := f.snapshot(t, userRoots...), f.snapshot(t, otherRoots...) + nativeAttempt(f.stages, "unchanged_lifecycle", "opencode-project-evidence.json") + stable := f.snapshot(t, filepath.Join(f.A, ".uap", "state-v2.json"), filepath.Join(f.A, ".opencode", "skills"), filepath.Join(f.A, "opencode.jsonc")) + for _, step := range []struct{ label, verb, selector string }{{"repeat-add-A", "add", f.sourceA}, {"unchanged-update-A", "update", commandsPluginName}} { + args := []string{step.verb, step.selector} + if step.verb == "add" { + args = append(args, "--no-save") + } + result := f.run(t, step.label, f.A, args...) + commandsRequireMutated(t, step.label, result, false) + if !reflect.DeepEqual(stable, f.snapshot(t, filepath.Join(f.A, ".uap", "state-v2.json"), filepath.Join(f.A, ".opencode", "skills"), filepath.Join(f.A, "opencode.jsonc"))) { + t.Fatal("unchanged Prepared operation changed owned files/state") + } + } + openCodeProjectIntactRepair(t, f) + f.observe(t, "stable-A", f.A, f.sourceA, "A-one", "echo", false) + f.stages["unchanged_lifecycle"] = nativeStage{Status: "passed", Reason: "repeat add/update preserve exact state/config/skills; intact group repair records actual restaging with identical package/native bytes and stable Prepared ownership"} + nativeAttempt(f.stages, "update", "opencode-project-evidence.json") + commandsSwapInvocation(t, f.sourceA) + path := filepath.Join(f.sourceA, "skills", commandsGuideSkill, "SKILL.md") + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + nativeWrite(t, path, []byte(strings.ReplaceAll(string(body), "A-one", "A-two")), 0600) + f.writeMCP(t, f.sourceA, "scope-marker", "A-two") + updated := f.run(t, "update-A", f.A, "update", f.sourceA) + commandsRequireMutated(t, "update", updated, true) + if err := commandsPlanSelection(updated, "wizard", "echo"); err != nil { + t.Fatal(err) + } + f.observe(t, "updated-A", f.A, f.sourceA, "A-two", "wizard", false) + f.binding(t, f.A) + f.stages["update"] = nativeStage{Status: "passed", Reason: "same-version update advances exact skill body, ordinary command selection and MCP marker; receipt stays Prepared"} + nativeAttempt(f.stages, "missing_repair", "opencode-project-evidence.json") + missing := filepath.Join(f.A, ".opencode", "skills", commandsGuideSkill) + nativeRequireContained(t, f.A, missing) + if err := os.RemoveAll(missing); err != nil { + t.Fatal(err) + } + // Local directory repair requires the source to reproduce the applied + // revision. Keep A-two unchanged while restoring the missing owned skill. + repaired := f.run(t, "missing-repair-A", f.A, "repair", commandsPluginName) + commandsRequireMutated(t, "missing repair", repaired, true) + // Expected source bytes come from the last owned receipt's managed package. + binding := f.binding(t, f.A) + f.observe(t, "repaired-A", f.A, binding.TargetLocator, "A-two", "wizard", false) + f.stages["missing_repair"] = nativeStage{Status: "passed", Reason: "missing owned skill restored with the local source unchanged at the applied A-two revision; fresh native JSON proves exact installed body/path"} + nativeAttempt(f.stages, "remove", "opencode-project-evidence.json") + f.run(t, "remove-A", f.A, "remove", commandsPluginName) + for _, name := range []string{commandsGuideSkill, commandsSkill("echo"), commandsSkill("wizard")} { + if _, err := os.Lstat(filepath.Join(f.A, ".opencode", "skills", name)); !os.IsNotExist(err) { + t.Fatal("removed skill remains on disk") + } + } + f.observe(t, "removed-A", f.A, "", "", "", true) + afterRemove := f.snapshot(t, filepath.Join(f.A, ".uap"), filepath.Join(f.A, "opencode.jsonc"), filepath.Join(f.A, ".opencode", "skills")) + f.run(t, "repeat-remove-A", f.A, "remove", commandsPluginName) + if !reflect.DeepEqual(afterRemove, f.snapshot(t, filepath.Join(f.A, ".uap"), filepath.Join(f.A, "opencode.jsonc"), filepath.Join(f.A, ".opencode", "skills"))) { + t.Fatal("repeat removal refusal changed owned state/files") + } + f.stages["remove"] = nativeStage{Status: "passed", Reason: "owned native keys/skills absent by fresh native JSON and filesystem; repeated removal gives recognized absent-installation refusal with exact retained bytes"} + if !reflect.DeepEqual(userBefore, f.snapshot(t, userRoots...)) || !reflect.DeepEqual(otherBefore, f.snapshot(t, otherRoots...)) { + t.Fatal("A lifecycle changed original user or B ownership/state") + } + for _, root := range []string{f.A, f.B} { + body, err := os.ReadFile(filepath.Join(root, "opencode.jsonc")) + if err != nil || !strings.Contains(string(body), openCodeJSONCComment) || !strings.Contains(string(body), "uap-inert-project") { + t.Fatal("foreign JSONC preference/comment changed") + } + if string(nativeReadProject(t, filepath.Join(root, ".uap", "data", "caller-owned", "keep.txt"))) != "caller-owned persistent data\n" { + t.Fatal("caller-owned data changed") + } + } + f.keepJSON(t, "foreign-snapshots.json", map[string]any{"original_user": userBefore, "project_B": otherBefore}) + f.stages["foreign_preservation"] = nativeStage{Status: "passed", Reason: "exact original-user state/native control and B state/native/package bytes unchanged; foreign JSONC comments/settings and caller-owned data retained"} + openCodeProjectSameName(t, f) + openCodeProjectJSONCase(t, f) + if len(f.used) != len(f.table) { + t.Fatalf("finite command table incomplete: used%d want%d", len(f.used), len(f.table)) + } + f.evidence["native_discovery_qualified"] = true +} +func nativeReadProject(t *testing.T, path string) []byte { + t.Helper() + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return body +} +func (f *openCodeProjectFixture) keepJSON(t *testing.T, name string, value any) { + t.Helper() + body, err := json.MarshalIndent(value, "", " ") + if err != nil { + t.Fatal(err) + } + f.keep(t, name, append(body, '\n')) +} + +func openCodeProjectSameName(t *testing.T, f *openCodeProjectFixture) { + t.Helper() + nativeAttempt(f.stages, "same_name_precedence", "opencode-project-evidence.json") + f.run(t, "add-same-user", "", "add", f.sourceUser, "--no-save") + f.run(t, "add-same-A", f.A, "add", f.sourceA, "--no-save") + // The mutable source intentionally carries the later body in this secondary + // phase. User/A/B receipts and files all survive regardless of native winner. + snapshots := f.snapshot(t, filepath.Join(f.state, "state-v2.json"), filepath.Join(f.A, ".uap"), filepath.Join(f.B, ".uap"), filepath.Join(f.work, "xdg-config", "opencode", "skills"), filepath.Join(f.A, ".opencode", "skills"), filepath.Join(f.B, ".opencode", "skills")) + winners := map[string]any{} + known := map[string]map[string]openCodeProjectSkill{} + known["user"] = f.expected(t, f.user, f.sourceUser, "echo") + known["A"] = f.expected(t, f.A, f.sourceA, "wizard") + known["B"] = f.expected(t, f.B, f.sourceB, "echo") + for _, item := range []struct{ label, root string }{{"user", f.user}, {"A", f.A}, {"B", f.B}} { + config := f.config(t, "same-"+item.label, item.root, nil) + skills := f.skills(t, "same-"+item.label, item.root, true) + guide, ok := skills[commandsGuideSkill] + if !ok { + t.Fatal("same-name phase has no valid winner") + } + origin := "" + for label, expected := range known { + want := expected[commandsGuideSkill] + if openCodeProjectSamePath(guide.Location, want.Location) && strings.TrimSpace(guide.Content) == strings.TrimSpace(want.Content) { + origin = label + } + } + if origin == "" { + t.Fatal("same-name winner is outside exact fixture paths/bodies") + } + mcp, ok := config["mcp"].(map[string]any) + if !ok { + t.Fatal("same-name config lacks MCP object") + } + server, ok := mcp["scope-marker"].(map[string]any) + if !ok { + t.Fatal("same-name config lacks marker") + } + url, ok := server["url"].(string) + if !ok { + t.Fatal("same-name marker is malformed") + } + allowed := false + for _, marker := range []string{"USER-same", "A-two", "B-one"} { + allowed = allowed || url == f.recorder.url(marker) + } + if !allowed { + t.Fatal("same-name MCP winner outside fixture values") + } + winners[item.label] = map[string]any{"skill_origin": origin, "skill": guide, "mcp_url": url} + } + if f.stages["same_name_precedence"].Status != "blocked" { + f.stages["same_name_precedence"] = nativeStage{Status: "blocked", Reason: "valid actual same-name winners retained; simultaneous user/A/B activation and general precedence remain unqualified"} + } + f.keepJSON(t, "same-name-winners.json", winners) + nativeAttempt(f.stages, "inline_precedence", "opencode-project-evidence.json") + inline, err := json.Marshal(map[string]any{"mcp": map[string]any{"scope-marker": map[string]any{"type": "remote", "url": f.recorder.url("INLINE"), "enabled": true}}}) + if err != nil { + t.Fatal(err) + } + if err := openCodeProjectCheckConfig(f.config(t, "inline-A", f.A, []string{"OPENCODE_CONFIG_CONTENT=" + string(inline)}), map[string]string{"scope-marker": f.recorder.url("INLINE")}, nil); err != nil { + t.Fatal(err) + } + restarted := f.config(t, "restart-A", f.A, nil) + if err := openCodeProjectCheckConfig(restarted, map[string]string{"scope-marker": f.recorder.url("A-two")}, nil); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(snapshots, f.snapshot(t, filepath.Join(f.state, "state-v2.json"), filepath.Join(f.A, ".uap"), filepath.Join(f.B, ".uap"), filepath.Join(f.work, "xdg-config", "opencode", "skills"), filepath.Join(f.A, ".opencode", "skills"), filepath.Join(f.B, ".opencode", "skills"))) { + t.Fatal("secondary observation changed receipts/owned skill files") + } + f.stages["inline_precedence"] = nativeStage{Status: "passed", Reason: "fixed process-only MCP marker wins inline; fresh ordinary restart returns to selected project file, without owned receipt/skill mutation"} +} +func openCodeProjectJSONCase(t *testing.T, f *openCodeProjectFixture) { + t.Helper() + nativeAttempt(f.stages, "root_json", "opencode-project-evidence.json") + f.run(t, "add-json", f.short, "add", f.sourceJSON, "--no-save") + // Same-name user skills are present in this secondary case: config route and + // exact UAP file ownership are mandatory; native winner limits are retained. + if err := openCodeProjectCheckConfig(f.config(t, "json-installed", f.short, nil), map[string]string{"scope-marker": f.recorder.url("JSON-one")}, nil); err != nil { + t.Fatal(err) + } + f.skills(t, "json-installed", f.short, true) + f.binding(t, f.short) + if _, err := os.Lstat(filepath.Join(f.short, "opencode.jsonc")); !os.IsNotExist(err) { + t.Fatal("JSON route invented a JSONC sibling") + } + f.run(t, "remove-json", f.short, "remove", commandsPluginName) + // User same-name components remain, so native lists must not be treated as a + // filesystem deletion oracle. Exact project paths are separately absent. + config := f.config(t, "json-removed", f.short, nil) + if err := openCodeProjectCheckConfig(config, map[string]string{"scope-marker": f.recorder.url("USER-same")}, nil); err != nil { + t.Fatal(err) + } + rows := f.skills(t, "json-removed", f.short, true) + for _, row := range rows { + if strings.HasPrefix(row.Location, f.short+string(os.PathSeparator)) { + t.Fatal("removed JSON project skill remains natively listed") + } + } + for _, name := range []string{commandsGuideSkill, commandsSkill("echo")} { + if _, err := os.Lstat(filepath.Join(f.short, ".opencode", "skills", name)); !os.IsNotExist(err) { + t.Fatal("JSON project skill survives removal") + } + } + f.stages["root_json"] = nativeStage{Status: "passed", Reason: "actual UAP root JSON install/config observation/removal; exact project files absent, same-name user runtime winner not treated as project ownership"} +} diff --git a/repotests/agentplugins_opencode_project_native_fixture_test.go b/repotests/agentplugins_opencode_project_native_fixture_test.go new file mode 100644 index 000000000..7090308ed --- /dev/null +++ b/repotests/agentplugins_opencode_project_native_fixture_test.go @@ -0,0 +1,527 @@ +package pluginkitairepo_test + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "reflect" + "sort" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/statev2" + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +const openCodeProjectLimit = 128 << 10 +const openCodeProjectEnvelopeLimit = 32 << 20 + +type openCodePassiveRecorder struct { + server *http.Server + listener net.Listener + accepted atomic.Int64 + requests atomic.Int64 + mu sync.Mutex + samples []string + done chan struct{} +} + +func newOpenCodePassiveRecorder(t *testing.T) *openCodePassiveRecorder { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + r := &openCodePassiveRecorder{listener: listener, done: make(chan struct{})} + r.server = &http.Server{ReadHeaderTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, IdleTimeout: time.Second, + ConnState: func(c net.Conn, s http.ConnState) { + if s == http.StateNew { + r.accepted.Add(1) + _ = c.SetDeadline(time.Now().Add(time.Second)) + } + }, + Handler: http.HandlerFunc(func(w http.ResponseWriter, q *http.Request) { + r.requests.Add(1) + r.mu.Lock() + if len(r.samples) < 8 { + sample := q.Method + " " + q.URL.EscapedPath() + if len(sample) > 256 { + sample = sample[:256] + } + r.samples = append(r.samples, sample) + } + r.mu.Unlock() + w.WriteHeader(http.StatusServiceUnavailable) + })} + go func() { defer close(r.done); _ = r.server.Serve(listener) }() + return r +} +func (r *openCodePassiveRecorder) counts() [2]int64 { + return [2]int64{r.accepted.Load(), r.requests.Load()} +} +func (r *openCodePassiveRecorder) url(marker string) string { + return "http://" + r.listener.Addr().String() + "/" + marker +} +func (r *openCodePassiveRecorder) close() error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + err := r.server.Shutdown(ctx) + if err != nil { + _ = r.server.Close() + } + select { + case <-r.done: + return err + case <-time.After(time.Second): + return fmt.Errorf("passive recorder Serve did not stop") + } +} + +// Every direct child has a predeclared label/class. The complete table is 43 +// commands: 4 Git setup, 14 public UAP and 25 native read observations. +func openCodeProjectCommandTable() map[string]string { + table := map[string]string{} + for _, label := range []string{"A", "B", "user", "json"} { + table["git-"+label] = "git" + } + table["version"] = "native" + for _, label := range []string{"add-user", "add-A", "add-B", "repeat-add-A", "unchanged-update-A", "intact-repair-A", "update-A", "missing-repair-A", "remove-A", "repeat-remove-A", "add-same-user", "add-same-A", "add-json", "remove-json"} { + table[label] = "uap" + } + for _, stage := range []string{"initial-A", "initial-B", "stable-A", "updated-A", "repaired-A", "removed-A", "same-user", "same-A", "same-B", "json-installed", "json-removed"} { + table[stage+"-config"] = "native" + table[stage+"-skill"] = "native" + } + table["inline-A-config"] = "native" + table["restart-A-config"] = "native" + return table +} + +type openCodeProjectFixture struct { + root, work, home, user, A, B, short, sourceA, sourceB, sourceUser, sourceControl, sourceJSON, state, client, installer, git string + ctx context.Context + cancel context.CancelFunc + recorder *openCodePassiveRecorder + files map[string][]byte + bytes int + records []map[string]any + table, used map[string]string + stages map[string]nativeStage + evidence map[string]any + failed bool +} + +func newOpenCodeProjectFixture(t *testing.T) *openCodeProjectFixture { + t.Helper() + nativeRequireDisposable(t) + root, err := os.MkdirTemp("", "uap-opencode-project-work-") + if err != nil { + t.Fatal(err) + } + root, err = filepath.EvalSymlinks(root) + if err != nil { + t.Fatal(err) + } + f := &openCodeProjectFixture{root: root, work: filepath.Join(root, "work"), files: map[string][]byte{}, table: openCodeProjectCommandTable(), used: map[string]string{}, stages: map[string]nativeStage{}} + f.ctx, f.cancel = context.WithTimeout(context.Background(), 6*time.Minute) + f.home = filepath.Join(f.work, "home") + f.user = filepath.Join(f.work, "user-cwd") + f.A = filepath.Join(f.work, "A") + f.B = filepath.Join(f.work, "B") + f.short = filepath.Join(f.work, "json") + f.state = filepath.Join(f.work, "installer-state") + f.sourceA = filepath.Join(f.work, "source-A") + f.sourceB = filepath.Join(f.work, "source-B") + f.sourceUser = filepath.Join(f.work, "source-user") + f.sourceControl = filepath.Join(f.work, "source-control") + f.sourceJSON = filepath.Join(f.work, "source-json") + for _, p := range []string{f.home, f.user, f.A, f.B, f.short, f.state, filepath.Join(f.work, "tmp"), filepath.Join(f.work, "xdg-config"), filepath.Join(f.work, "xdg-data"), filepath.Join(f.work, "xdg-cache"), filepath.Join(f.work, "xdg-state")} { + if err := os.MkdirAll(p, 0700); err != nil { + t.Fatal(err) + } + } + f.client = openCodeNativeBinary(t, "AGENTPLUGINS_OPENCODE_BIN") + f.installer = openCodeNativeBinary(t, "AGENTPLUGINS_INSTALLER_BIN") + f.git, err = exec.LookPath("git") + if err != nil { + t.Fatal(err) + } + f.git, err = filepath.Abs(f.git) + if err != nil { + t.Fatal(err) + } + f.recorder = newOpenCodePassiveRecorder(t) + f.evidence = map[string]any{"schema_version": 1, "client": "opencode", "started_utc": time.Now().UTC().Format(time.RFC3339Nano), "native_discovery_qualified": false, "native_mcp_initialization_qualified": false, "native_model_use_qualified": false, "activation": "prepared", "network": "not isolated; passive loopback recorder measures accepted TCP/HTTP only; vendor bootstrap may download/cache dependencies", "fixture_root": root} + for _, name := range []string{"positive_project_discovery", "unchanged_lifecycle", "update", "missing_repair", "remove", "same_name_precedence", "inline_precedence", "root_json", "foreign_preservation"} { + f.stages[name] = nativeStage{Status: "not_evaluated", Reason: "not reached"} + } + for _, name := range []string{"native_mcp_initialization", "model_use", "authentication", "trust", "ide", "default_plugins", "enterprise_ancestor_sources"} { + f.stages[name] = nativeStage{Status: "not_evaluated", Reason: "outside fixed debug observation contract"} + } + t.Logf("OpenCode project working authority: %s; only admitted envelope is uploaded", root) + return f +} +func (f *openCodeProjectFixture) env() []string { + return append(nativePlatformEnvironment(f.work, f.home, filepath.Dir(f.client), filepath.Dir(f.git)), "AGENTPLUGINS_HOME="+f.state, "OPENCODE_PURE=1", "OPENCODE_DISABLE_AUTOUPDATE=1", "OPENCODE_DISABLE_MODELS_FETCH=1", "OPENCODE_DISABLE_DEFAULT_PLUGINS=1", "OPENCODE_DISABLE_LSP_DOWNLOAD=1", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL="+filepath.Join(f.home, ".gitconfig"), "GIT_TERMINAL_PROMPT=0") +} +func (f *openCodeProjectFixture) keep(t *testing.T, name string, body []byte) { + t.Helper() + if filepath.Base(name) != name || f.files[name] != nil || len(f.files) >= 127 || f.bytes+len(body) > openCodeProjectEnvelopeLimit-(1<<20) { + f.failed = true + t.Fatal("project evidence cumulative admission refused") + } + f.files[name] = append([]byte(nil), body...) + f.bytes += len(body) +} +func (f *openCodeProjectFixture) command(t *testing.T, class, label, exe, cwd string, env, args []string) projectNativeCaptureResult { + t.Helper() + if f.table[label] != class || f.used[label] != "" || len(f.used) >= 60 { + f.failed = true + t.Fatal("command not in finite project table or repeated") + } + f.used[label] = class + if class == "native" && !reflect.DeepEqual(args, []string{"--version"}) && !reflect.DeepEqual(args, []string{"debug", "config"}) && !reflect.DeepEqual(args, []string{"debug", "skill"}) { + f.failed = true + t.Fatal("unreviewed native argv") + } + timeout := 10 * time.Second + if class == "uap" { + timeout = 30 * time.Second + } + before := f.recorder.counts() + result := captureProjectNative(f.ctx, exe, cwd, env, args, openCodeProjectLimit, timeout) + after := f.recorder.counts() + outName, errName := label+".stdout.log", label+".stderr.log" + f.keep(t, outName, result.Stdout) + f.keep(t, errName, result.Stderr) + digest := func(body []byte) string { sum := sha256.Sum256(body); return hex.EncodeToString(sum[:]) } + f.records = append(f.records, map[string]any{"class": class, "label": label, "argv": append([]string{exe}, args...), "cwd": cwd, "exit": result.Exit, "error": result.Error, "error_kind": result.ErrorKind, "deadline": result.Deadline, "stdout_limit": result.StdoutLimit, "stderr_limit": result.StderrLimit, "elapsed_ms": result.ElapsedMS, "stdout_file": outName, "stderr_file": errName, "stdout_bytes": len(result.Stdout), "stderr_bytes": len(result.Stderr), "stdout_sha256": digest(result.Stdout), "stderr_sha256": digest(result.Stderr), "recorder_before": before, "recorder_after": after}) + if before != [2]int64{} || after != [2]int64{} || projectNativeCaptureAdmission(result, class == "uap" && label == "repeat-remove-A") != nil { + f.failed = true + t.Fatal("child deadline/output limit or unexpected MCP connection; see bounded evidence") + } + return result +} +func (f *openCodeProjectFixture) setup(t *testing.T) { + t.Helper() + identity, err := nativeSourceIdentity(os.Getenv("AGENTPLUGINS_INSTALLER_COMMIT"), os.Getenv("AGENTPLUGINS_INSTALLER_TREE"), os.Getenv("AGENTPLUGINS_INSTALLER_PATCH_SHA256")) + if err != nil { + t.Fatal(err) + } + f.evidence["source_identity"] = identity + if os.Getenv("AGENTPLUGINS_OPENCODE_VERSION") != "1.18.34" || nativeSHA(t, f.client) != os.Getenv("AGENTPLUGINS_OPENCODE_SHA256") { + t.Fatal("missing/exact OpenCode pin mismatch") + } + f.evidence["client_sha256"] = nativeSHA(t, f.client) + f.evidence["installer_sha256"] = nativeSHA(t, f.installer) + f.evidence["scanner"] = nativeProvisionScanner(t, &nativeFixture{Root: f.work}) + for _, item := range []struct{ label, root string }{{"A", f.A}, {"B", f.B}, {"user", f.user}, {"json", f.short}} { + r := f.command(t, "git", "git-"+item.label, f.git, item.root, f.env(), []string{"-c", "core.hooksPath=/dev/null", "init", "--quiet"}) + if r.Exit != 0 || len(bytesTrim(r.Stderr)) != 0 { + t.Fatal("fresh Git boundary failed") + } + info, err := os.Lstat(filepath.Join(item.root, ".git")) + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + t.Fatal("expected fresh Git boundary missing") + } + } + r := f.command(t, "native", "version", f.client, f.user, f.env(), []string{"--version"}) + if r.Exit != 0 || len(bytesTrim(r.Stderr)) != 0 || string(bytesTrim(r.Stdout)) != "1.18.34" { + t.Fatal("native version is not exact pin") + } + f.evidence["client_version_measured"] = string(bytesTrim(r.Stdout)) + for _, item := range []struct{ root, marker string }{{f.sourceA, "A-one"}, {f.sourceB, "B-one"}, {f.sourceUser, "USER-same"}, {f.sourceJSON, "JSON-one"}} { + f.source(t, item.root, item.marker) + } + nativeWrite(t, filepath.Join(f.sourceControl, "plugin.json"), []byte(`{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"uap-opencode-user-control","version":"1.0.0"}`), 0600) + nativeWrite(t, filepath.Join(f.sourceControl, "skills", "uap-user-control", "SKILL.md"), []byte("---\nname: uap-user-control\ndescription: Inert user control\n---\nUAP-USER-CONTROL\n"), 0600) + f.writeMCP(t, f.sourceControl, "uap-user-control", "USER-control") + for _, root := range []string{f.A, f.B} { + nativeWrite(t, filepath.Join(root, "opencode.jsonc"), []byte(openCodeJSONCComment+`{"mcp":{},"default_agent":"uap-inert-project"}`), 0600) + nativeWrite(t, filepath.Join(root, ".uap", "data", "caller-owned", "keep.txt"), []byte("caller-owned persistent data\n"), 0600) + } + nativeWrite(t, filepath.Join(f.short, "opencode.json"), []byte(`{"mcp":{},"default_agent":"uap-inert-json"}`), 0600) + nativeWrite(t, filepath.Join(f.work, "xdg-config", "opencode", "opencode.jsonc"), []byte(openCodeJSONCComment+`{"mcp":{},"default_agent":"uap-inert-user"}`), 0600) +} +func bytesTrim(body []byte) []byte { return []byte(strings.TrimSpace(string(body))) } +func (f *openCodeProjectFixture) source(t *testing.T, root, marker string) { + t.Helper() + copyTree(t, commandsFixtureSource(t), root) + path := filepath.Join(root, "skills", commandsGuideSkill, "SKILL.md") + body, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + nativeWrite(t, path, append(body, []byte("\nUAP-OPENCODE-"+marker+"\n")...), 0600) + f.writeMCP(t, root, "scope-marker", marker) +} +func (f *openCodeProjectFixture) writeMCP(t *testing.T, root, key, marker string) { + t.Helper() + body, err := json.Marshal(map[string]any{"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", "mcpServers": map[string]any{key: map[string]any{"type": "streamable-http", "url": f.recorder.url(marker)}}}) + if err != nil { + t.Fatal(err) + } + nativeWrite(t, filepath.Join(root, "mcp.json"), body, 0600) +} +func (f *openCodeProjectFixture) run(t *testing.T, label, project string, args ...string) map[string]any { + t.Helper() + cwd := f.user + if project != "" { + cwd = project + args = append(args, "--scope", "project", "--project-root", project) + } + args = append(args, "--target", "opencode", "--format", "json") + r := f.command(t, "uap", label, f.installer, cwd, f.env(), args) + if label == "repeat-remove-A" { + if !openCodeProjectAbsentRemoval(r) { + t.Fatal("repeat removal did not give exact source-established absence refusal") + } + f.evidence["repeat_remove_contract"] = "empty stdout; exact fixed stderr; pure ExitError exit1; unchanged owned state/files" + return nil + } + if !openCodeProjectProgress(label, r.Stderr) { + t.Fatal("public UAP emitted unexpected progress bytes; see evidence") + } + result, err := openCodeProjectConfig(r.Stdout) + if err != nil { + t.Fatalf("public UAP whole JSON: %v", err) + } + if r.Exit != 0 || result["result"] != "success" { + t.Fatal("public UAP operation failed; see exact bounded evidence") + } + if args[0] != "remove" { + if project != "" { + result, err = openCodeProjectAssertionEnvelope(result) + if err != nil { + t.Fatalf("%s project result: %v", label, err) + } + codexNativeProjectPrepared(t, result) + } else { + openCodeAssertCompleted(t, label, result) + } + } + return result +} +func (f *openCodeProjectFixture) binding(t *testing.T, project string) domain.ClientBinding { + t.Helper() + s, err := (statev2.Store{Path: filepath.Join(project, ".uap", "state-v2.json"), ProjectRoot: project}).Load() + if err != nil || len(s.Installations) != 1 || len(s.Installations[0].Clients) != 1 { + t.Fatalf("project installed state: %v", err) + } + for _, b := range s.Installations[0].Clients { + if b.ClientID != "opencode" || b.Scope != "project" || b.NativeProfileRoot != project || b.Activation != domain.ActivationPrepared || b.Verification != domain.VerificationInstalled { + t.Fatal("project receipt lost exact Prepared authority") + } + return b + } + t.Fatal("no project receipt") + return domain.ClientBinding{} +} +func (f *openCodeProjectFixture) config(t *testing.T, label, project string, extra []string) map[string]any { + t.Helper() + env := append(f.env(), extra...) + r := f.command(t, "native", label+"-config", f.client, project, env, []string{"debug", "config"}) + if r.Exit != 0 || len(bytesTrim(r.Stderr)) != 0 { + t.Fatal("native config failed or warned; see exact evidence") + } + object, err := openCodeProjectConfig(r.Stdout) + if err != nil { + t.Fatal(err) + } + return object +} +func (f *openCodeProjectFixture) skills(t *testing.T, label, project string, precedence bool) map[string]openCodeProjectSkill { + t.Helper() + r := f.command(t, "native", label+"-skill", f.client, project, f.env(), []string{"debug", "skill"}) + if r.Exit != 0 { + t.Fatal("native skill command failed; see exact evidence") + } + if len(bytesTrim(r.Stderr)) != 0 { + if !precedence { + t.Fatal("positive skill observation warned") + } + f.evidence[label+"_warning"] = string(r.Stderr) + f.stages["same_name_precedence"] = nativeStage{Status: "blocked", Reason: "unrecognized secondary warning retained verbatim; no warning/precedence qualification"} + } + found, err := openCodeProjectSkills(r.Stdout) + if err != nil { + t.Fatal(err) + } + return found +} +func (f *openCodeProjectFixture) expected(t *testing.T, project, source, command string) map[string]openCodeProjectSkill { + t.Helper() + skills := map[string]openCodeProjectSkill{} + root := filepath.Join(project, ".opencode", "skills") + if project == f.user { + root = filepath.Join(f.work, "xdg-config", "opencode", "skills") + } + guide, err := os.ReadFile(filepath.Join(source, "skills", commandsGuideSkill, "SKILL.md")) + if err != nil { + t.Fatal(err) + } + content, err := openCodeProjectSkillContent(guide) + if err != nil { + t.Fatal(err) + } + skills[commandsGuideSkill] = openCodeProjectSkill{Name: commandsGuideSkill, Location: filepath.Join(root, commandsGuideSkill, "SKILL.md"), Content: content} + content, err = openCodeProjectSkillContent([]byte(commandsSkillBytes[command])) + if err != nil { + t.Fatal(err) + } + name := commandsSkill(command) + skills[name] = openCodeProjectSkill{Name: name, Location: filepath.Join(root, name, "SKILL.md"), Content: content} + skills["uap-user-control"] = openCodeProjectSkill{Name: "uap-user-control", Location: filepath.Join(f.work, "xdg-config", "opencode", "skills", "uap-user-control", "SKILL.md"), Content: "UAP-USER-CONTROL"} + return skills +} +func (f *openCodeProjectFixture) observe(t *testing.T, label, project, source, marker, command string, removed bool) { + t.Helper() + urls := map[string]string{"uap-user-control": f.recorder.url("USER-control")} + absent := []string{} + expected := map[string]openCodeProjectSkill{"uap-user-control": {Name: "uap-user-control", Location: filepath.Join(f.work, "xdg-config", "opencode", "skills", "uap-user-control", "SKILL.md"), Content: "UAP-USER-CONTROL"}} + forbidden := []string{commandsGuideSkill, commandsSkill("echo"), commandsSkill("wizard")} + if !removed { + urls["scope-marker"] = f.recorder.url(marker) + expected = f.expected(t, project, source, command) + forbidden = []string{commandsSkill(map[string]string{"echo": "wizard", "wizard": "echo"}[command])} + } else { + absent = append(absent, "scope-marker") + } + if err := openCodeProjectCheckConfig(f.config(t, label, project, nil), urls, absent); err != nil { + t.Fatal(err) + } + if err := openCodeProjectCheckSkills(f.skills(t, label, project, false), expected, forbidden); err != nil { + t.Fatal(err) + } +} +func (f *openCodeProjectFixture) snapshot(t *testing.T, roots ...string) map[string]string { + t.Helper() + result := map[string]string{} + total := int64(0) + entries := 0 + for _, root := range roots { + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + entries++ + if entries > 1024 || len(result) > 512 { + return fmt.Errorf("snapshot entry cap") + } + info, err := d.Info() + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !info.Mode().IsRegular() || info.Size() > 1<<20 || total+info.Size() > 4<<20 { + return fmt.Errorf("snapshot refuses type/size") + } + body, err := readProjectNativeSnapshot(path, info, min(int64(1<<20), (4<<20)-total)) + if err != nil { + return err + } + total += int64(len(body)) + sum := sha256.Sum256(body) + result[path] = fmt.Sprintf("%o:%d:%x", info.Mode(), len(body), sum) + return nil + }) + if err != nil { + t.Fatal(err) + } + } + return result +} +func (f *openCodeProjectFixture) finish(t *testing.T) { + t.Helper() + f.cancel() + closed := make(chan error, 1) + go func() { closed <- f.recorder.close() }() + select { + case err := <-closed: + if err != nil { + f.failed = true + t.Error(err) + } + case <-time.After(5 * time.Second): + f.failed = true + t.Error("passive recorder cleanup exceeded independent deadline") + } + if f.recorder.counts() != [2]int64{} { + f.failed = true + f.stages["unexpected_mcp_connection"] = nativeStage{Status: "failed", Reason: "passive endpoint observed accepted TCP/HTTP; no handshake qualification"} + t.Error("late passive recorder connection/request observed") + } + f.evidence["finished_utc"] = time.Now().UTC().Format(time.RFC3339Nano) + f.evidence["success"] = !t.Failed() && !f.failed + if t.Failed() || f.failed { + f.evidence["native_discovery_qualified"] = false + } + f.evidence["commands"] = f.records + f.evidence["command_table"] = f.table + f.evidence["used_command_count"] = len(f.used) + f.evidence["recorder_counts"] = f.recorder.counts() + f.recorder.mu.Lock() + f.evidence["recorder_samples"] = append([]string(nil), f.recorder.samples...) + f.recorder.mu.Unlock() + f.evidence["stages"] = f.stages + f.evidence["cleanup"] = "passive listener closed with independent five-second deadline; working authority retained for disposable runner deletion; no recursive cleanup in fixture" + body, err := json.MarshalIndent(f.evidence, "", " ") + if err != nil || len(body) > 1<<20 { + f.files = map[string][]byte{} + body = []byte(`{"success":false,"native_discovery_qualified":false,"error":"summary admission refused"}`) + f.failed = true + } + f.files["opencode-project-evidence.json"] = append(body, '\n') + names := make([]string, 0, len(f.files)) + size := 0 + for name, body := range f.files { + names = append(names, name) + size += len(body) + } + sort.Strings(names) + if len(names) > 128 || size > openCodeProjectEnvelopeLimit { + f.files = map[string][]byte{"opencode-project-evidence.json": []byte(`{"success":false,"native_discovery_qualified":false,"error":"aggregate envelope refused"}`)} + names = []string{"opencode-project-evidence.json"} + t.Error("aggregate project envelope refused") + } + pending, err := os.MkdirTemp(f.root, "pending-") + if err != nil { + t.Error(err) + return + } + for _, name := range names { + if err := os.WriteFile(filepath.Join(pending, name), f.files[name], 0600); err != nil { + t.Error(err) + return + } + } + // Atomic directory publication leaves working sources/cache/raw trees nested + // and invisible to the wrapper's flat evidence collector. + admitted, err := os.MkdirTemp(filepath.Dir(f.root), "uap-opencode-project-admitted-") + if err != nil { + t.Error(err) + return + } + if err = os.Remove(admitted); err == nil { + err = os.Rename(pending, admitted) + } + if err != nil { + t.Error(err) + return + } + t.Logf("OpenCode project admitted envelope: %s", admitted) +} diff --git a/repotests/agentplugins_opencode_project_native_parse_test.go b/repotests/agentplugins_opencode_project_native_parse_test.go new file mode 100644 index 000000000..59bc2cb8b --- /dev/null +++ b/repotests/agentplugins_opencode_project_native_parse_test.go @@ -0,0 +1,225 @@ +package pluginkitairepo_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "runtime" + "strings" + "unicode/utf8" +) + +// Refuse duplicate/case-ambiguous keys, excessive depth and trailing documents. +func openCodeProjectJSON(body []byte) (any, error) { + if !utf8.Valid(body) { + return nil, fmt.Errorf("invalid UTF-8 JSON") + } + decoder := json.NewDecoder(bytes.NewReader(body)) + decoder.UseNumber() + var read func(int) (any, error) + read = func(depth int) (any, error) { + if depth > 64 { + return nil, fmt.Errorf("JSON nesting exceeds limit") + } + token, err := decoder.Token() + if err != nil { + return nil, err + } + delimiter, ok := token.(json.Delim) + if !ok { + return token, nil + } + switch delimiter { + case '{': + object := map[string]any{} + seen := map[string]bool{} + for decoder.More() { + key, err := decoder.Token() + if err != nil { + return nil, err + } + name, ok := key.(string) + if !ok { + return nil, fmt.Errorf("non-string key") + } + fold := strings.ToLower(name) + if seen[fold] { + return nil, fmt.Errorf("duplicate or ambiguous JSON key") + } + seen[fold] = true + value, err := read(depth + 1) + if err != nil { + return nil, err + } + object[name] = value + } + end, err := decoder.Token() + if err != nil || end != json.Delim('}') { + return nil, fmt.Errorf("invalid object end") + } + return object, nil + case '[': + array := []any{} + for decoder.More() { + value, err := read(depth + 1) + if err != nil { + return nil, err + } + array = append(array, value) + } + end, err := decoder.Token() + if err != nil || end != json.Delim(']') { + return nil, fmt.Errorf("invalid array end") + } + return array, nil + default: + return nil, fmt.Errorf("unexpected JSON delimiter") + } + } + value, err := read(0) + if err != nil { + return nil, err + } + if _, err = decoder.Token(); !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("JSON has trailing bytes") + } + return value, nil +} + +type openCodeProjectSkill struct{ Name, Location, Content string } + +// Public progress is a closed source-derived contract. Native and Git stderr +// never use this allowance; repeated removal has its separate exact refusal. +func openCodeProjectProgress(label string, body []byte) bool { + resolve := "Resolving and validating one Agent Plugin package for every selected target...\n" + var expected string + switch label { + case "add-user": + expected = resolve + "Installing uap-opencode-user-control for 1 selected clients...\n" + case "add-A", "add-B", "add-same-user", "add-same-A", "add-json": + expected = resolve + "Installing uap-commands-fixture for 1 selected clients...\n" + case "repeat-add-A": + // Existing same-scope binding dispatches runAddLoaded before the group + // Installing line. The real public-source fixture measures this branch. + expected = resolve + "Applying transactional client package...\n" + case "unchanged-update-A", "update-A": + expected = "Resolving and validating one updated Agent Plugin package for every selected target...\n" + case "intact-repair-A", "missing-repair-A": + expected = "Resolving and validating each unique exact installed package revision once...\n" + case "remove-A", "remove-json": + expected = "" + default: + return false + } + return string(body) == expected +} + +func openCodeProjectSkills(body []byte) (map[string]openCodeProjectSkill, error) { + value, err := openCodeProjectJSON(body) + if err != nil { + return nil, err + } + rows, ok := value.([]any) + if !ok { + return nil, fmt.Errorf("native skill listing is not an array") + } + found := map[string]openCodeProjectSkill{} + for _, row := range rows { + object, ok := row.(map[string]any) + if !ok { + return nil, fmt.Errorf("native skill row is not an object") + } + name, a := object["name"].(string) + location, b := object["location"].(string) + content, c := object["content"].(string) + if !a || !b || !c || name == "" || location == "" || content == "" || found[name].Name != "" { + return nil, fmt.Errorf("malformed or duplicate native skill row") + } + found[name] = openCodeProjectSkill{Name: name, Location: location, Content: content} + } + return found, nil +} +func openCodeProjectSamePath(actual, expected string) bool { + if !filepath.IsAbs(actual) || filepath.Clean(actual) != actual || strings.ContainsRune(actual, 0) { + return false + } + if runtime.GOOS == "windows" { + return strings.EqualFold(actual, expected) + } + return actual == expected +} +func openCodeProjectSkillContent(body []byte) (string, error) { + text := strings.ReplaceAll(string(body), "\r\n", "\n") + if !strings.HasPrefix(text, "---\n") { + return "", fmt.Errorf("fixture frontmatter missing") + } + end := strings.Index(text[4:], "\n---\n") + if end < 0 { + return "", fmt.Errorf("fixture frontmatter end missing") + } + return strings.TrimSpace(text[4+end+5:]), nil +} +func openCodeProjectCheckSkills(found map[string]openCodeProjectSkill, expected map[string]openCodeProjectSkill, forbidden []string) error { + for name, want := range expected { + got, ok := found[name] + if !ok || !openCodeProjectSamePath(got.Location, want.Location) || strings.TrimSpace(got.Content) != strings.TrimSpace(want.Content) { + return fmt.Errorf("native skill %q does not match exact selected path/body", name) + } + } + for _, name := range forbidden { + if _, ok := found[name]; ok { + return fmt.Errorf("native lists withheld/removed fixture skill %q", name) + } + } + return nil +} +func openCodeProjectConfig(body []byte) (map[string]any, error) { + value, err := openCodeProjectJSON(body) + if err != nil { + return nil, err + } + object, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("native config is not an object") + } + return object, nil +} +func openCodeProjectCheckConfig(object map[string]any, urls map[string]string, absent []string) error { + mcp, ok := object["mcp"].(map[string]any) + if !ok { + return fmt.Errorf("native config has no MCP object") + } + for name, url := range urls { + server, ok := mcp[name].(map[string]any) + if !ok || server["type"] != "remote" || server["url"] != url { + return fmt.Errorf("native MCP source %q differs", name) + } + if enabled, present := server["enabled"]; present { + flag, valid := enabled.(bool) + if !valid || !flag { + return fmt.Errorf("native MCP enabled flag %q malformed or false", name) + } + } + } + for _, name := range absent { + if _, ok := mcp[name]; ok { + return fmt.Errorf("removed MCP key %q remains", name) + } + } + return nil +} + +// Early public remove errors have no JSON envelope: prepare returns before the +// renderer and main emits one exact fixed stderr line. This is not a fallback +// parser for failed native JSON. +func openCodeProjectAbsentRemoval(result projectNativeCaptureResult) bool { + if result.Exit != 1 || result.ErrorKind != "exit" || result.Error == "" || len(result.Stdout) != 0 || result.Deadline || result.StdoutLimit || result.StderrLimit { + return false + } + first := "agentplugins: installation \"" + commandsPluginName + "\" was not found\n" + second := "agentplugins: plugin is not installed for target \"opencode\" in project scope; no target was changed\n" + return string(result.Stderr) == first || string(result.Stderr) == second +} diff --git a/repotests/agentplugins_opencode_project_repair_contract_test.go b/repotests/agentplugins_opencode_project_repair_contract_test.go new file mode 100644 index 000000000..75d00b418 --- /dev/null +++ b/repotests/agentplugins_opencode_project_repair_contract_test.go @@ -0,0 +1,95 @@ +package pluginkitairepo_test + +import ( + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func TestOpenCodeProjectIntactRepairRequiresActualMutation(t *testing.T) { + for _, change := range []string{"absent no_change", "false no_change", "true no_change", "malformed no_change", "null no_change", "false mutated", "malformed mutated", "missing mutated", "single", "mixed", "failed batch"} { + t.Run(change, func(t *testing.T) { + raw := openCodeProjectResultFixture(true) + outcome, _ := commandsTargetOutcome(raw) + outcome["mutated"] = true + delete(outcome, "no_change") + switch change { + case "false no_change": + outcome["no_change"] = false + case "true no_change": + outcome["no_change"] = true + case "malformed no_change": + outcome["no_change"] = "false" + case "null no_change": + outcome["no_change"] = nil + case "false mutated": + outcome["mutated"] = false + case "malformed mutated": + outcome["mutated"] = "true" + case "missing mutated": + delete(outcome, "mutated") + case "single": + raw = openCodeProjectResultFixture(false) + case "mixed": + raw["data"].(map[string]any)["result"] = outcome + case "failed batch": + raw["data"].(map[string]any)["status"] = "failed" + } + want := change == "absent no_change" || change == "false no_change" + if err := openCodeProjectRepairResult(raw); (err == nil) != want { + t.Fatalf("repair mutation admission=%v want=%v", err, want) + } + }) + } +} + +func TestOpenCodeProjectIntactRepairPreservesOwnedBinding(t *testing.T) { + for _, change := range []string{"valid", "missing history", "altered history", "extra history", "wrong binding", "wrong path", "uncommitted", "removal", "empty digest", "wrong before digest", "wrong after digest", "projection", "native ownership", "lifecycle", "authentication", "attempt", "intent"} { + t.Run(change, func(t *testing.T) { + before := domain.ClientBinding{ClientBindingID: "binding", TargetLocator: "managed", Activation: domain.ActivationPrepared, Authentication: domain.AuthenticationNotChecked, + Receipts: []domain.MutationReceipt{{AfterDigest: "owned"}}, NativeObjects: []domain.NativeObjectOwnership{{Kind: "managed_package_directory", ManagedDigest: "owned"}}} + after := before + after.Receipts = append([]domain.MutationReceipt{}, before.Receipts...) + after.Receipts = append(after.Receipts, domain.MutationReceipt{Phase: "committed", MutationType: "directory_swap", ClientBindingID: "binding", ActivePath: "managed", BeforeDigest: "owned", AfterDigest: "owned"}) + after.UpdatedAt = "new timestamp" + switch change { + case "missing history": + before.Receipts = nil + case "altered history": + after.Receipts[0].AfterDigest = "foreign" + case "extra history": + after.Receipts = append(after.Receipts, after.Receipts[1]) + case "wrong binding": + after.Receipts[1].ClientBindingID = "foreign" + case "wrong path": + after.Receipts[1].ActivePath = "foreign" + case "uncommitted": + after.Receipts[1].Phase = "state_committed" + case "removal": + after.Receipts[1].MutationType = "directory_remove" + case "empty digest": + before.Receipts[0].AfterDigest, after.Receipts[0].AfterDigest = "", "" + case "wrong before digest": + after.Receipts[1].BeforeDigest = "foreign" + case "wrong after digest": + after.Receipts[1].AfterDigest = "foreign" + case "projection": + after.PhysicalArtifact = "foreign" + case "native ownership": + after.NativeObjects = nil + case "lifecycle": + after.Activation = domain.ActivationActive + case "authentication": + after.Authentication = domain.AuthenticationComplete + case "attempt": + before.NativeActivationAttempt, after.NativeActivationAttempt = "pending", "pending" + case "intent": + before.PendingNativeIntent = &domain.PendingNativeIntent{} + after.PendingNativeIntent = before.PendingNativeIntent + } + if err := openCodeProjectRepairBinding(before, after); (err == nil) != (change == "valid") { + t.Fatalf("repair binding admission=%v", err) + } + }) + } +} diff --git a/repotests/agentplugins_opencode_project_repair_test.go b/repotests/agentplugins_opencode_project_repair_test.go new file mode 100644 index 000000000..3eae634fb --- /dev/null +++ b/repotests/agentplugins_opencode_project_repair_test.go @@ -0,0 +1,73 @@ +package pluginkitairepo_test + +import ( + "fmt" + "path/filepath" + "reflect" + "testing" + + "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain" +) + +func openCodeProjectIntactRepair(t *testing.T, f *openCodeProjectFixture) { + t.Helper() + before := f.binding(t, f.A) + nativeRequireContained(t, f.A, before.TargetLocator) + statePath := filepath.Join(f.A, ".uap", "state-v2.json") + nativePaths := []string{before.TargetLocator, filepath.Join(f.A, ".opencode", "skills"), filepath.Join(f.A, "opencode.jsonc")} + stateBefore, nativeBefore := f.snapshot(t, statePath), f.snapshot(t, nativePaths...) + facts := map[string]any{"state_before": stateBefore, "native_before": nativeBefore, "receipt_count_before": len(before.Receipts)} + f.evidence["intact_repair"] = facts + result := f.run(t, "intact-repair-A", f.A, "repair", commandsPluginName) + if err := openCodeProjectRepairResult(result); err != nil { + t.Fatal(err) + } + after := f.binding(t, f.A) + stateAfter, nativeAfter := f.snapshot(t, statePath), f.snapshot(t, nativePaths...) + facts["state_after"], facts["native_after"], facts["receipt_count_after"] = stateAfter, nativeAfter, len(after.Receipts) + if reflect.DeepEqual(stateBefore, stateAfter) || !reflect.DeepEqual(nativeBefore, nativeAfter) { + t.Fatal("intact group repair did not record restaging with identical package/native files") + } + if err := openCodeProjectRepairBinding(before, after); err != nil { + t.Fatal(err) + } + facts["binding_invariants_preserved"] = true +} + +func openCodeProjectRepairResult(result map[string]any) error { + view, err := openCodeProjectAssertionEnvelope(result) + if err != nil { + return err + } + data, _ := view["data"].(map[string]any) + outcome, err := commandsTargetOutcome(view) + if err != nil || data["status"] != "completed" || outcome["mutated"] != true { + return fmt.Errorf("intact group repair did not report an actual completed mutation") + } + if value, exists := outcome["no_change"]; exists && value != false { + return fmt.Errorf("intact group repair carries contradictory unchanged status") + } + return nil +} + +func openCodeProjectRepairBinding(before, after domain.ClientBinding) error { + if before.NativeActivationAttempt != "" || after.NativeActivationAttempt != "" || before.PendingNativeIntent != nil || after.PendingNativeIntent != nil { + return fmt.Errorf("intact repair has unresolved native authority") + } + count := len(before.Receipts) + if count == 0 || len(after.Receipts) != count+1 || !reflect.DeepEqual(before.Receipts, after.Receipts[:count]) { + return fmt.Errorf("intact repair did not preserve and append exact receipt history") + } + previous, receipt := before.Receipts[count-1], after.Receipts[count] + if receipt.Phase != "committed" || receipt.MutationType != "directory_swap" || receipt.ClientBindingID != before.ClientBindingID || receipt.ActivePath != before.TargetLocator || + previous.AfterDigest == "" || receipt.BeforeDigest != previous.AfterDigest || receipt.AfterDigest != previous.AfterDigest { + return fmt.Errorf("intact repair receipt lost prior owned package identity") + } + // The kernel appends a new physical restaging receipt and updates the + // timestamp. Every other binding field, including native ownership, is stable. + after.Receipts, after.UpdatedAt = before.Receipts, before.UpdatedAt + if !reflect.DeepEqual(before, after) { + return fmt.Errorf("intact repair changed binding authority, projection or lifecycle") + } + return nil +} diff --git a/repotests/agentplugins_opencode_project_result_contract_test.go b/repotests/agentplugins_opencode_project_result_contract_test.go new file mode 100644 index 000000000..a3b250740 --- /dev/null +++ b/repotests/agentplugins_opencode_project_result_contract_test.go @@ -0,0 +1,104 @@ +package pluginkitairepo_test + +import ( + "bytes" + "encoding/json" + "testing" +) + +func TestOpenCodeProjectAssertionEnvelopePreservesActualResults(t *testing.T) { + for _, batch := range []bool{false, true} { + raw := openCodeProjectResultFixture(batch) + if batch { + outcome, _ := commandsTargetOutcome(raw) + outcome["mutated"] = true + delete(outcome, "no_change") + } + before, _ := json.Marshal(raw) + view, err := openCodeProjectAssertionEnvelope(raw) + if err != nil { + t.Fatal(err) + } + outcome, err := commandsTargetOutcome(view) + if err != nil || outcome["mutated"] != batch || !batch && outcome["no_change"] != true { + t.Fatal("assertion view lost actual replay fields", err) + } + if _, exists := outcome["no_change"]; batch && exists { + t.Fatal("assertion view invented a batch replay flag") + } + after, _ := json.Marshal(raw) + if !bytes.Equal(before, after) { + t.Fatal("assertion adapter mutated captured input") + } + } +} + +func TestOpenCodeProjectAssertionEnvelopeRefusesContradictoryResults(t *testing.T) { + for _, name := range []string{"mixed", "missing", "failed", "status", "error", "top-level error", "targets", "multiple", "target", "target status", "client", "scope", "activation", "verification", "attestation", "malformed attestation", "missing no_change", "typed no_change", "missing mutated", "typed mutated"} { + t.Run(name, func(t *testing.T) { + batch := name == "targets" || name == "multiple" || name == "target" || name == "target status" + raw := openCodeProjectResultFixture(batch) + data := raw["data"].(map[string]any) + view := codexProjectAssertionEnvelope(raw) + outcome, _ := commandsTargetOutcome(view) + plan := outcome["plan"].(map[string]any) + activation := outcome["activation"].(map[string]any) + switch name { + case "mixed": + data["targets"] = []any{} + case "missing": + delete(data, "result") + case "failed": + raw["result"] = "error" + case "status": + data["status"] = "managed_committed_activation_failed" + case "error": + data["error"] = "actual failure" + case "top-level error": + raw["error"] = "actual failure" + case "targets": + data["targets"] = "malformed" + case "multiple": + data["targets"] = append(data["targets"].([]any), data["targets"].([]any)[0]) + case "target": + data["targets"].([]any)[0].(map[string]any)["target"] = "codex" + case "target status": + data["targets"].([]any)[0].(map[string]any)["status"] = "external_failed" + case "client": + plan["client_id"] = "codex" + case "scope": + plan["scope"] = "user" + case "activation": + activation["activation"] = "active" + case "verification": + activation["verification"] = "package_validated" + case "attestation": + activation["activation_attested"] = true + case "malformed attestation": + activation["activation_attested"] = "false" + case "missing no_change": + delete(outcome, "no_change") + case "typed no_change": + outcome["no_change"] = "true" + case "missing mutated": + delete(outcome, "mutated") + case "typed mutated": + outcome["mutated"] = "false" + } + if _, err := openCodeProjectAssertionEnvelope(raw); err == nil { + t.Fatal("contradictory project result gained assertion authority") + } + }) + } +} + +func openCodeProjectResultFixture(batch bool) map[string]any { + outcome := map[string]any{"no_change": true, "mutated": false, + "plan": map[string]any{"client_id": "opencode", "scope": "project"}, + "activation": map[string]any{"activation": "prepared", "verification": "installation_verified"}} + data := map[string]any{"result": outcome} + if batch { + data = map[string]any{"status": "completed", "targets": []any{map[string]any{"target": "opencode", "status": "external_completed", "output": map[string]any{"result": outcome}}}} + } + return map[string]any{"result": "success", "data": data} +} diff --git a/repotests/agentplugins_opencode_project_result_test.go b/repotests/agentplugins_opencode_project_result_test.go new file mode 100644 index 000000000..5db33571c --- /dev/null +++ b/repotests/agentplugins_opencode_project_result_test.go @@ -0,0 +1,53 @@ +package pluginkitairepo_test + +import "fmt" + +// The CLI can reuse one prepared target through its single-result envelope. +// Adapt assertions only; captured raw JSON and the input map remain unchanged. +func openCodeProjectAssertionEnvelope(result map[string]any) (map[string]any, error) { + data, ok := result["data"].(map[string]any) + if result["result"] != "success" || result["error"] != nil || !ok || data["error"] != nil { + return nil, fmt.Errorf("project result is not explicit success") + } + _, batch := data["targets"] + _, single := data["result"] + if batch == single { + return nil, fmt.Errorf("project result envelope is mixed or missing") + } + if status, exists := data["status"]; batch && status != "completed" || exists && status != "completed" { + return nil, fmt.Errorf("project result has missing or contradictory completion status") + } + if batch { + targets, ok := data["targets"].([]any) + if !ok || len(targets) != 1 { + return nil, fmt.Errorf("project batch requires exactly one target") + } + target, ok := targets[0].(map[string]any) + if !ok || target["target"] != "opencode" || target["status"] != "external_completed" || target["error"] != nil { + return nil, fmt.Errorf("project batch target did not complete") + } + output, _ := target["output"].(map[string]any) + if output["error"] != nil { + return nil, fmt.Errorf("project batch output carries an explicit error") + } + } + view := codexProjectAssertionEnvelope(result) + outcome, err := commandsTargetOutcome(view) + if err != nil || outcome["error"] != nil { + return nil, fmt.Errorf("project result has no successful target outcome") + } + if single && (outcome["no_change"] != true || outcome["mutated"] != false) { + return nil, fmt.Errorf("single project replay lacks actual unchanged flags") + } + plan, _ := outcome["plan"].(map[string]any) + activation, _ := outcome["activation"].(map[string]any) + if plan["client_id"] != "opencode" || plan["scope"] != "project" || activation["activation"] != "prepared" || activation["verification"] != "installation_verified" { + return nil, fmt.Errorf("project result has contradictory client, scope or Prepared identity") + } + for _, key := range []string{"activation_attested", "authentication_attested"} { + if value, exists := activation[key]; exists && value != false { + return nil, fmt.Errorf("project result carries malformed or positive attestation") + } + } + return view, nil +} diff --git a/repotests/agentplugins_project_native_capture_test.go b/repotests/agentplugins_project_native_capture_test.go new file mode 100644 index 000000000..5dac2bea3 --- /dev/null +++ b/repotests/agentplugins_project_native_capture_test.go @@ -0,0 +1,107 @@ +package pluginkitairepo_test + +import ( + "context" + "errors" + "os/exec" + "reflect" + "sync" + "time" +) + +// These are the reviewed diagnostic capture mechanics with explicit limits. +// Direct-child cancellation and WaitDelay do not constitute a process sandbox. +type projectNativeStream struct { + mu sync.Mutex + body []byte + limit int + overflow bool + cancel context.CancelFunc +} + +func (s *projectNativeStream) Write(body []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + n := len(body) + if remaining := s.limit - len(s.body); n > remaining { + n = remaining + s.overflow = true + } + s.body = append(s.body, body[:n]...) + if s.overflow { + s.cancel() + return n, errors.New("project native output limit exceeded") + } + return n, nil +} +func (s *projectNativeStream) snapshot() ([]byte, bool) { + s.mu.Lock() + defer s.mu.Unlock() + return append([]byte(nil), s.body...), s.overflow +} + +type projectNativeCaptureResult struct { + Stdout []byte `json:"-"` + Stderr []byte `json:"-"` + Exit int `json:"exit"` + Error string `json:"error,omitempty"` + ErrorKind string `json:"error_kind,omitempty"` + Deadline bool `json:"deadline"` + StdoutLimit bool `json:"stdout_limit"` + StderrLimit bool `json:"stderr_limit"` + ElapsedMS int64 `json:"elapsed_ms"` +} + +func captureProjectNative(parent context.Context, executable, cwd string, env, args []string, limit int, timeout time.Duration) projectNativeCaptureResult { + ctx, cancel := context.WithTimeout(parent, timeout) + defer cancel() + stdout := &projectNativeStream{limit: limit, cancel: cancel} + stderr := &projectNativeStream{limit: limit, cancel: cancel} + command := exec.CommandContext(ctx, executable, args...) + command.Dir = cwd + command.Env = append([]string(nil), env...) + command.Stdin = nil + command.Stdout, command.Stderr = stdout, stderr + command.WaitDelay = 250 * time.Millisecond + start := time.Now() + err := command.Run() + result := projectNativeCaptureResult{Exit: -1, ElapsedMS: time.Since(start).Milliseconds(), Deadline: errors.Is(ctx.Err(), context.DeadlineExceeded)} + result.Stdout, result.StdoutLimit = stdout.snapshot() + result.Stderr, result.StderrLimit = stderr.snapshot() + if command.ProcessState != nil { + result.Exit = command.ProcessState.ExitCode() + } + if err != nil { + result.ErrorKind = projectNativeErrorKind(err, ctx.Err()) + result.Error = err.Error() + if len(result.Error) > 512 { + result.Error = result.Error[:512] + } + } + return result +} + +func projectNativeErrorKind(err, contextErr error) string { + if contextErr != nil { + return "context" + } + if err == nil { + return "" + } + if reflect.TypeOf(err) == reflect.TypeFor[*exec.ExitError]() { + return "exit" + } + return "capture" +} +func projectNativeCaptureAdmission(result projectNativeCaptureResult, expectedAbsent bool) error { + if result.Deadline || result.StdoutLimit || result.StderrLimit { + return errors.New("capture deadline or stream limit") + } + if result.Error == "" && result.ErrorKind == "" && result.Exit == 0 { + return nil + } + if expectedAbsent && result.Error != "" && result.ErrorKind == "exit" && result.Exit == 1 { + return nil + } + return errors.New("process/capture did not complete cleanly") +} diff --git a/repotests/agentplugins_project_native_file_test.go b/repotests/agentplugins_project_native_file_test.go new file mode 100644 index 000000000..f4023d6ff --- /dev/null +++ b/repotests/agentplugins_project_native_file_test.go @@ -0,0 +1,48 @@ +package pluginkitairepo_test + +import ( + "errors" + "fmt" + "io" + "os" +) + +func readProjectNativeSnapshot(path string, before os.FileInfo, limit int64) (result []byte, err error) { + if limit < 0 || !before.Mode().IsRegular() || before.Size() < 0 || before.Size() > limit { + return nil, fmt.Errorf("snapshot pre-read type/size refused") + } + file, err := openProjectNativeSnapshot(path) + if err != nil { + return nil, err + } + defer func() { err = errors.Join(err, file.Close()) }() + same := func(info os.FileInfo) bool { + return info != nil && info.Mode().IsRegular() && os.SameFile(before, info) && info.Mode() == before.Mode() && info.Size() == before.Size() && info.ModTime().Equal(before.ModTime()) + } + opened, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("snapshot opened identity: %w", err) + } + if !same(opened) { + return nil, fmt.Errorf("snapshot opened identity differs") + } + body, err := io.ReadAll(io.LimitReader(file, limit+1)) + if err != nil { + return nil, err + } + after, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("snapshot held identity: %w", err) + } + if len(body) > int(limit) || int64(len(body)) != before.Size() || !same(after) { + return nil, fmt.Errorf("snapshot bytes/held identity changed") + } + current, err := os.Lstat(path) + if err != nil { + return nil, fmt.Errorf("snapshot path identity: %w", err) + } + if !same(current) { + return nil, fmt.Errorf("snapshot path identity changed") + } + return body, nil +} diff --git a/repotests/agentplugins_project_native_file_unix_test.go b/repotests/agentplugins_project_native_file_unix_test.go new file mode 100644 index 000000000..1a4443c9b --- /dev/null +++ b/repotests/agentplugins_project_native_file_unix_test.go @@ -0,0 +1,17 @@ +//go:build !windows + +package pluginkitairepo_test + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func openProjectNativeSnapshot(path string) (*os.File, error) { + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) + if err != nil { + return nil, err + } + return os.NewFile(uintptr(fd), path), nil +} diff --git a/repotests/agentplugins_project_native_file_windows_test.go b/repotests/agentplugins_project_native_file_windows_test.go new file mode 100644 index 000000000..c0e63f420 --- /dev/null +++ b/repotests/agentplugins_project_native_file_windows_test.go @@ -0,0 +1,21 @@ +//go:build windows + +package pluginkitairepo_test + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func openProjectNativeSnapshot(path string) (*os.File, error) { + name, err := windows.UTF16PtrFromString(path) + if err != nil { + return nil, err + } + handle, err := windows.CreateFile(name, windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0) + if err != nil { + return nil, err + } + return os.NewFile(uintptr(handle), path), nil +} diff --git a/scripts/check-removed-contract-boundary.sh b/scripts/check-removed-contract-boundary.sh index 3c02481d1..adee62b1a 100644 --- a/scripts/check-removed-contract-boundary.sh +++ b/scripts/check-removed-contract-boundary.sh @@ -59,7 +59,13 @@ check_pattern() { } check_pattern "removed Cursor rules import" '\.cursorrules' 'docs/research/**' -check_pattern "removed OpenCode env-config compatibility" 'OPENCODE_CONFIG(_DIR)?' 'install/integrationctl/**' 'docs/UNIVERSAL_INSTALL_UPDATE_CONTROL_PLANE_PLAN.md' +# Reviewed disposable native controls and negative inheritance assertions do not +# restore a public environment-config compatibility surface. +check_pattern "removed OpenCode env-config compatibility" 'OPENCODE_CONFIG(_DIR)?' \ + 'install/integrationctl/**' 'docs/UNIVERSAL_INSTALL_UPDATE_CONTROL_PLANE_PLAN.md' \ + 'repotests/agentplugins_opencode_project_native_contract_test.go' \ + 'repotests/agentplugins_opencode_project_native_e2e_test.go' \ + 'docs/plans/project-install-e2e/opencode-project-native-gate-plan.md' check_pattern "removed Gemini binary aliases" 'PLUGIN_KIT_AI_GEMINI_BIN|GEMINI_BIN' check_pattern "Gemini migratedTo field outside research or runtime codec" 'migratedTo|migrated_to' 'docs/research/**' 'docs/UNIVERSAL_INSTALL_UPDATE_CONTROL_PLANE_PLAN.md' 'cli/internal/geminimanifest/**' 'cli/internal/platformexec/gemini*.go' 'cli/internal/validate/validate_test.go' 'install/integrationctl/**' 'repotests/plugin_manifest_lifecycle_integration_test.go' 'sdk/platformmeta/**' check_pattern "deleted maintainer docs tree" 'maintainer-docs' 'website/tools/quality/check-output.mjs' diff --git a/scripts/cline_project_envelope.py b/scripts/cline_project_envelope.py new file mode 100644 index 000000000..0aa3d6f20 --- /dev/null +++ b/scripts/cline_project_envelope.py @@ -0,0 +1,127 @@ +"""Closed pure contract for the dedicated, unreviewed Cline recovery draft.""" +import hashlib +import json +import re + +NATIVE = "cline-project-evidence.json" +FRONTEND = "cline-project-frontend.json" +FRONT_LABELS = frozenset(("frontend-commit", "frontend-tree", "frontend-clean", "frontend-go-version", "frontend-build", "frontend-gate")) +CLASSES = { + "git": ("git-user", "git-A", "git-B"), + "public_uap": ("add-user", "add-A", "add-B", "repeat-add-A", "unchanged-update-A", "intact-repair-A", "update-A", "missing-repair-A", "remove-A", "add-same-user", "add-same-A", "add-same-B"), + "native": ("version", "initial-A-skills", "initial-B-skills", "default-A", "default-B", "override-A", "override-B", "cross-A-from-B", "restart-default-A", "stable-A-skills", "stable-A-mcp", "updated-A-skills", "updated-A-mcp", "repaired-A-skills", "repaired-A-mcp", "removed-A-skills", "removed-A-mcp", "same-user-skills", "same-A-skills", "same-B-skills", "same-user-mcp", "same-A-mcp", "same-B-mcp"), + "cleanup": ("cleanup",), +} +NATIVE_LABELS = {label: kind for kind, labels in CLASSES.items() for label in labels} +EXTRAS = frozenset(("prepared-user-config.json", "prepared-A-config.json", "prepared-B-config.json", "updated-A-config.json", "repaired-A-config.json", "removed-A-config.json", "prepared-A-inspection.json", "prepared-B-inspection.json", "repaired-A-inspection.json", "foreign-snapshots.json", "same-name-winners.json", "same-user-config.json", "same-A-config.json", "same-B-config.json")) +NATIVE_FALSE = ("native_project_activation_qualified", "native_mcp_initialization_qualified", "native_runtime_qualified", "native_model_use_qualified", "native_ide_qualified", "native_trust_qualified", "same_name_active_isolation_qualified", "native_skill_execution_qualified", "native_authentication_qualified", "native_session_policy_qualified") +FRONT_FALSE = NATIVE_FALSE +STAGES = frozenset(("source_install", "unique_discovery", "stable_lifecycle", "update", "missing_repair", "remove", "same_name_precedence", "foreign_preservation")) + + +def unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate Cline evidence JSON key") + result[key] = value + return result + + +def invalid_constant(_value): + raise ValueError("nonfinite Cline evidence JSON number") + + +def document(body): + try: + value = json.loads(body.decode("utf-8"), object_pairs_hook=unique_object, parse_constant=invalid_constant) + except (UnicodeError, ValueError, RecursionError) as error: + raise ValueError("ordinary bounded Cline evidence JSON required") from error + if not isinstance(value, dict): + raise ValueError("Cline evidence object required") + return value + + +def summary(body, false_flags): + value = document(body) + if type(value.get("schema_version")) is not int or value["schema_version"] != 1 or value.get("client") != "cline" or value.get("version") != "3.0.73": + raise ValueError("closed Cline evidence identity differs") + if type(value.get("collection_complete")) is not bool: + raise ValueError("explicit Cline collection result required") + for key in false_flags: + if value.get(key) is not False: + raise ValueError("unobserved Cline qualification must remain false") + if value["collection_complete"] and (value.get("cleanup_complete") is not True or value.get("read_only_unique_marker_discovery_qualified") is not True): + raise ValueError("completed Cline evidence lacks measured discovery/cleanup") + return value + + +def streams(value, bodies, labels, kind=None): + commands = value.get("commands") + if not isinstance(commands, list) or len(commands) > len(labels): + raise ValueError("closed Cline command count refused") + seen, expected = set(), set() + for command in commands: + if not isinstance(command, dict): + raise ValueError("Cline command object required") + label = command.get("label") + if not isinstance(label, str) or label not in labels or label in seen: + raise ValueError("unknown/duplicate Cline command label") + seen.add(label) + wanted_class = kind if kind is not None else labels[label] + if command.get("class") != wanted_class: + raise ValueError("Cline command class differs") + if kind is None and command.get("in_process") is not (wanted_class == "public_uap"): + raise ValueError("Cline public Cobra/process boundary differs") + if value["collection_complete"] and (type(command.get("exit")) is not int or command["exit"] != 0 or command.get("error") not in (None, "") or command.get("overflow", False) is not False): + raise ValueError("completed Cline collection contains failed command") + for stream in ("stdout", "stderr"): + name = label + "." + stream + ".log" + body = bodies.get(name) + if command.get(stream + "_file") != name or not isinstance(body, bytes) or len(body) > 128 << 10: + raise ValueError("closed bounded Cline stream refused") + if type(command.get(stream + "_bytes")) is not int or len(body) != command[stream + "_bytes"] or hashlib.sha256(body).hexdigest() != command.get(stream + "_sha256"): + raise ValueError("Cline stream length/hash differs") + expected.add(name) + if value["collection_complete"] and seen != set(labels): + raise ValueError("completed Cline collection omitted fixed commands") + return expected + + +def validate(bodies): + if FRONTEND not in bodies: + raise ValueError("Cline frontend receipt missing") + frontend = summary(bodies[FRONTEND], FRONT_FALSE) + expected = {FRONTEND} | streams(frontend, bodies, FRONT_LABELS, "frontend") + if NATIVE in bodies: + native = summary(bodies[NATIVE], NATIVE_FALSE) + captured = streams(native, bodies, NATIVE_LABELS) + extras = set(bodies) & EXTRAS + for name in extras: + document(bodies[name]) + hashes = native.get("artifact_sha256") + if not isinstance(hashes, dict) or set(hashes) != captured | extras: + raise ValueError("Cline native artifact receipt membership differs") + for name in captured | extras: + if hashlib.sha256(bodies[name]).hexdigest() != hashes[name]: + raise ValueError("Cline native artifact SHA256 differs") + for key in ("source_commit", "source_tree"): + if not isinstance(native.get(key), str) or not re.fullmatch(r"[0-9a-f]{40}", native[key]) or native[key] != frontend.get(key): + raise ValueError("Cline source commit/tree differs") + if frontend["collection_complete"] and not native["collection_complete"]: + raise ValueError("frontend promoted incomplete native evidence") + if native["collection_complete"]: + stages = native.get("stages") + if not isinstance(stages, dict) or set(stages) != STAGES or native.get("same_name_observation_complete") is not True: + raise ValueError("completed Cline stage membership differs") + for name, stage in stages.items(): + wanted = "blocked" if name == "same_name_precedence" else "passed" + if not isinstance(stage, dict) or stage.get("status") != wanted: + raise ValueError("Cline measured stage result differs") + if extras != set(EXTRAS): + raise ValueError("completed Cline source evidence omitted") + expected |= {NATIVE} | captured | extras + elif frontend["collection_complete"]: + raise ValueError("completed Cline frontend lacks actual native receipt") + if set(bodies) != expected: + raise ValueError("unrecorded Cline artifact member refused") diff --git a/scripts/native_client_project_artifacts.py b/scripts/native_client_project_artifacts.py new file mode 100644 index 000000000..a2fecbafd --- /dev/null +++ b/scripts/native_client_project_artifacts.py @@ -0,0 +1,254 @@ +"""Admit bounded OpenCode or closed Cline project evidence; never native caches. + +Invoked by the matrix in a five-second child. Pending files stay outside the +workflow upload directory. No recursive failure cleanup or target traversal. +""" +import hashlib +import json +import os +from pathlib import Path +import re +import stat +import sys +import tempfile +import time +import zipfile + +LIMIT = 32 << 20 +FILES = 128 +NAME = re.compile(r"[A-Za-z0-9_.-]{1,128}\.(?:json|log)\Z") +WINDOWS = os.name == "nt" + + +def python_runtime(): + return {"version": sys.version[:512], "implementation": sys.implementation.name[:64], + "executable": sys.executable[:1024]} + + +def identity(info): + result = (info.st_dev, info.st_ino, info.st_mode, info.st_nlink, + info.st_size, info.st_mtime_ns, info.st_ctime_ns) + if WINDOWS: + result += (getattr(info, "st_birthtime_ns", None), getattr(info, "st_file_attributes", None), + getattr(info, "st_reparse_tag", None)) + return result + + +def windows_identity(info): + # Windows path ctime and held-fd ctime have different meanings in CPython + # 3.12. Keep both complete same-domain identities, and cross-check birth time. + birth = getattr(info, "st_birthtime_ns", None) + attributes = getattr(info, "st_file_attributes", None) + tag = getattr(info, "st_reparse_tag", None) + if (not all(type(value) is int and value > 0 for value in (birth, info.st_dev, info.st_ino, info.st_nlink)) or not isinstance(attributes, int) or attributes & 0x400 + or not isinstance(tag, int) or tag != 0): + raise ValueError("Windows envelope identity is unavailable or a reparse point") + return (info.st_dev, info.st_ino, info.st_mode, info.st_nlink, info.st_size, + info.st_mtime_ns, birth, attributes) + + +def same_opened(path_info, held_info): + if WINDOWS: + return windows_identity(path_info) == windows_identity(held_info) + return identity(path_info) == identity(held_info) + + +def open_member(path): + if not WINDOWS: + return os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + import ctypes + from ctypes import wintypes + import msvcrt + + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + create, close = kernel.CreateFileW, kernel.CloseHandle + create.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, + wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE] + create.restype = wintypes.HANDLE + close.argtypes, close.restype = [wintypes.HANDLE], wintypes.BOOL + # GENERIC_READ, share read/write/delete, OPEN_EXISTING, OPEN_REPARSE_POINT. + # A null security-attributes pointer creates a noninheritable handle. + handle = create(str(path), 0x80000000, 7, None, 3, 0x00200000, None) + if handle is None or handle == ctypes.c_void_p(-1).value: + raise ctypes.WinError(ctypes.get_last_error()) + try: + descriptor = msvcrt.open_osfhandle(handle, os.O_RDONLY | os.O_BINARY) + if descriptor < 0: + raise OSError("Windows file descriptor transfer failed") + except BaseException: + if not close(handle): + raise ctypes.WinError(ctypes.get_last_error()) + raise + # Only this descriptor now owns the handle; the caller closes it once. + return descriptor + + +def check_deadline(started): + if time.monotonic() - started > 4: + raise TimeoutError("project envelope deadline") + + +def confirm_paths(packet): + source, root, members, started = packet + check_deadline(started) + current = source.lstat() + if WINDOWS: + windows_identity(current) + if source.resolve() != source or identity(current) != identity(root): + raise ValueError("project envelope root changed") + for path, before in members: + check_deadline(started) + current = path.lstat() + if WINDOWS: + windows_identity(current) + if identity(current) != identity(before): + raise ValueError("project envelope member replaced") + check_deadline(started) + + +def read_packet(source, *, client="opencode"): + if client not in ("opencode", "cline"): + raise ValueError("unknown closed project evidence client") + started = time.monotonic() + source = Path(source) + root = source.lstat() + if not stat.S_ISDIR(root.st_mode) or source.resolve() != source: + raise ValueError("project envelope root is not an exact real directory") + if WINDOWS: + windows_identity(root) + names = [] + with os.scandir(source) as entries: + for entry in entries: + names.append(entry.name) + if len(names) > FILES: + raise ValueError("project envelope file count refused") + bodies, members = {}, [] + total = 0 + for name in sorted(names): + check_deadline(started) + if not NAME.fullmatch(name): + raise ValueError("project envelope member name refused") + path = source / name + before = path.lstat() + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or not 0 <= before.st_size <= 1 << 20: + raise ValueError("project envelope member type/size refused") + if WINDOWS: + windows_identity(before) + if total + before.st_size > LIMIT - 65536: + raise ValueError("project envelope aggregate refused") + descriptor = open_member(path) + try: + held = os.fstat(descriptor) + if not stat.S_ISREG(held.st_mode) or not same_opened(before, held): + raise ValueError("project envelope opened identity differs") + chunks = [] + left = before.st_size + 1 + while left: + check_deadline(started) + chunk = os.read(descriptor, min(left, 65536)) + if not chunk: + break + chunks.append(chunk) + left -= len(chunk) + body = b"".join(chunks) + after = os.fstat(descriptor) + if len(body) != before.st_size or identity(after) != identity(held): + raise ValueError("project envelope bytes/held identity changed") + finally: + os.close(descriptor) + if identity(path.lstat()) != identity(before): + raise ValueError("project envelope member replaced") + members.append((path, before)) + bodies[name] = body + total += len(body) + packet = (source, root, members, started) + confirm_paths(packet) + if client == "cline": + from cline_project_envelope import validate + validate(bodies) + return bodies, packet + if "opencode-project-evidence.json" not in bodies: + raise ValueError("project envelope summary missing") + # Capture digests are checked again independently from Go's in-memory admission. + summary = json.loads(bodies["opencode-project-evidence.json"].decode("utf-8")) + commands = summary.get("commands", []) + if not isinstance(commands, list) or len(commands) > 60: + raise ValueError("project command receipt count refused") + expected = {"opencode-project-evidence.json"} + expected.update(name for name in ("foreign-snapshots.json", "same-name-winners.json") if name in bodies) + for command in commands: + for stream in ("stdout", "stderr"): + name = command[stream + "_file"] + if name != command["label"] + "." + stream + ".log" or name in expected: + raise ValueError("project stream name/uniqueness refused") + expected.add(name) + body = bodies[name] + if len(body) > 128 << 10: + raise ValueError("project captured stream exceeds limit") + if len(body) != command[stream + "_bytes"] or hashlib.sha256(body).hexdigest() != command[stream + "_sha256"]: + raise ValueError("project envelope stream receipt differs") + if set(bodies) != expected: + raise ValueError("unrecorded project envelope member refused") + return bodies, packet + + +def read_envelope(source, *, client="opencode"): + return read_packet(source, client=client)[0] + + +def write_zip(pending, bodies): + with zipfile.ZipFile(pending, "x", compression=zipfile.ZIP_STORED) as archive: + for name, body in sorted(bodies.items()): + info = zipfile.ZipInfo(name) + info.external_attr = (stat.S_IFREG | 0o600) << 16 + archive.writestr(info, body) + if pending.stat().st_size > LIMIT: + raise ValueError("project ZIP envelope exceeds aggregate cap") + if pending.stat().st_size > LIMIT: + raise ValueError("project ZIP overhead exceeds aggregate cap") + + +def cline_destination_parent(path, expected=None): + info = path.lstat() + if not stat.S_ISDIR(info.st_mode) or path.resolve() != path: + raise ValueError("Cline upload parent is not an exact ordinary directory") + if expected is not None and identity(info) != identity(expected): + raise ValueError("Cline upload parent changed before publication") + return info + + +def write_archive(source, destination, *, client="opencode"): + if client not in ("opencode", "cline"): + raise ValueError("unknown closed project evidence client") + source, destination = Path(source), Path(destination) + if source.parent.stat().st_dev != destination.parent.stat().st_dev: + raise ValueError("project envelope publication filesystem differs") + parent = cline_destination_parent(destination.parent) if client == "cline" else None + pending = Path(tempfile.mkdtemp(prefix=client + "-envelope-pending-", dir=source.parent)) / "evidence.zip" + admitted = True + try: + bodies, packet = read_packet(source, client=client) + write_zip(pending, bodies) + confirm_paths(packet) # The closed ZIP contains only captured in-memory bytes. + except (OSError, ValueError, KeyError, TypeError, TimeoutError) as error: + admitted = False + failure = {"success": False, "native_discovery_qualified": False, + "native_mcp_initialization_qualified": False, "native_model_use_qualified": False, + "artifact_admission_error": str(error)[:512], "artifact_python_runtime": python_runtime()} + summary_name = "opencode-project-evidence.json" + if client == "cline": + from cline_project_envelope import NATIVE, NATIVE_FALSE + summary_name = NATIVE + failure.update({key: False for key in NATIVE_FALSE}) + failure.update(collection_complete=False, read_only_unique_marker_discovery_qualified=False) + bodies = {summary_name: json.dumps(failure).encode()} + pending.unlink(missing_ok=True) + write_zip(pending, bodies) + if client == "cline": + cline_destination_parent(destination.parent, parent) + os.replace(pending, destination) + return admitted + + +if __name__ == "__main__": + sys.exit(0 if write_archive(sys.argv[1], sys.argv[2]) else 2) diff --git a/scripts/native_client_research.py b/scripts/native_client_research.py new file mode 100644 index 000000000..624fcd6d0 --- /dev/null +++ b/scripts/native_client_research.py @@ -0,0 +1,105 @@ +"""Small bounded read/process utilities for pinned hosted client research. + +No acquisition, native admission guard or command selection occurs here. +""" +import hashlib +import os +import resource +import selectors +import signal +import subprocess +import time + +OUTPUT_LIMIT = 1 << 20 + + +def bounded_read(stream, limit): + chunks, size = [], 0 + while True: + chunk = stream.read(min(64 << 10, limit + 1 - size)) + if not chunk: + return b"".join(chunks) + chunks.append(chunk) + size += len(chunk) + if size > limit: + raise ValueError("input exceeds configured byte limit") + + +def child_limits(): + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + + +def kill_group(process): + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + + +def capture_output(process, selector, streams, started, timeout, limit): + """Drain bounded nonblocking streams under the original process deadline.""" + counts = {"stdout": 0, "stderr": 0} + group_closed = False + while selector.get_map() or process.poll() is None: + remaining = timeout - (time.monotonic() - started) + if remaining <= 0: + return "probe deadline exceeded" + if process.poll() is not None and not group_closed: + kill_group(process) + group_closed = True + for key, _ in selector.select(min(0.05, remaining)): + name = key.data + allowance = limit - counts[name] + try: + chunk = os.read(key.fd, min(64 << 10, allowance + 1)) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + key.fileobj.close() + continue + accepted = chunk[:allowance] + streams[name].write(accepted) + counts[name] += len(accepted) + if len(chunk) > allowance: + return "output limit reached" + return None + + +def run_probe(argv, cwd, env, output, label, timeout=30, limit=OUTPUT_LIMIT): + """Cap streams live without limiting unrelated native filesystem writes.""" + stdout_path, stderr_path = output / (label + ".stdout.log"), output / (label + ".stderr.log") + record = {"probe": label, "argv": argv, "cwd": str(cwd), "error": None} + started, process = time.monotonic(), None + if limit < 1 or timeout <= 0: + raise ValueError("positive probe stream limit and timeout are required") + with stdout_path.open("wb") as stdout, stderr_path.open("wb") as stderr, selectors.DefaultSelector() as selector: + try: + process = subprocess.Popen(argv, cwd=cwd, env=env, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True, + preexec_fn=child_limits) + for name, pipe in (("stdout", process.stdout), ("stderr", process.stderr)): + os.set_blocking(pipe.fileno(), False) + selector.register(pipe, selectors.EVENT_READ, name) + record["error"] = capture_output(process, selector, {"stdout": stdout, "stderr": stderr}, + started, timeout, limit) + except (OSError, subprocess.SubprocessError) as exc: + record["error"] = str(exc)[:512] + finally: + if process is not None: + try: + kill_group(process) + process.wait(timeout=5) + finally: + for pipe in (process.stdout, process.stderr): + if pipe is not None: + pipe.close() + stdout_body, stderr_body = stdout_path.read_bytes(), stderr_path.read_bytes() + record.update({"exit": process.returncode if process is not None else None, + "elapsed_seconds": round(time.monotonic() - started, 3), + "stdout_file": stdout_path.name, "stderr_file": stderr_path.name, + "stdout_bytes": len(stdout_body), "stderr_bytes": len(stderr_body), + "stdout_sha256": hashlib.sha256(stdout_body).hexdigest(), + "stderr_sha256": hashlib.sha256(stderr_body).hexdigest()}) + return record, stdout_body, stderr_body + diff --git a/scripts/research-cline-native.py b/scripts/research-cline-native.py new file mode 100644 index 000000000..43b4dfb73 --- /dev/null +++ b/scripts/research-cline-native.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Pinned read-only Cline config research on disposable hosted Linux only. + +No prompt/session, installer/updater, trust, account, MCP connection or payload +is requested. Collected JSON still requires independent evidence interpretation. +""" +import argparse +import gzip +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import sys +import tarfile +import tempfile +import urllib.request + +from native_client_research import bounded_read, run_probe + + +def adjacent_module(name, filename): + spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(filename)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +native = adjacent_module("cline_research_matrix", "run-native-client-matrix.py") +grok = adjacent_module("cline_research_bounds", "research-grok-native.py") +VERSION = "3.0.73" +URL = "https://registry.npmjs.org/@cline/cli-linux-x64/-/cli-linux-x64-3.0.73.tgz" +ARCHIVE_PIN = "sha512-K+T94s0kPHLoMSANOJJ7KHb/etzYXZGQUbarKDKUPoZsvy4+oD1MAqYnZlfbKFM8tpnRQWouFngD9TB3mFqqeA==" +BINARY_SHA256 = "1ff8841d17d4d83763134c193d414e4f7fcf2ef56af759e5ebbe96727fc2c8f7" +BINARY_BYTES = 132_511_200 +MAX_ARCHIVE, MAX_EXPANDED, MAX_BINARY, MAX_MEMBERS = 64 << 20, 384 << 20, 256 << 20, 256 +PROJECTS = ("project-a", "project-b") +HELP_PROBES = (("version", ("--version",)), ("help", ("--help",)), + ("config-help", ("config", "--help")), ("mcp-help", ("mcp", "--help"))) +MCP_PROBES = (("default-a", "project-a", None), ("default-b", "project-b", None), + ("override-a", "project-a", "project-a"), ("override-b", "project-b", "project-b"), + ("cross-a-from-b", "project-b", "project-a"), ("default-a-again", "project-a", None)) +EXPECTED_PROBES = ([name for name, _ in HELP_PROBES] + + [name for name, _, _ in MCP_PROBES] + ["skills-a", "skills-b"]) + + +def archive_binary(body, budget): + with gzip.GzipFile(fileobj=io.BytesIO(body)) as compressed: + expanded = bounded_read(grok.BudgetReader(compressed, budget), MAX_EXPANDED) + binary, seen = None, set() + with tarfile.open(fileobj=io.BytesIO(expanded), mode="r:") as archive: + for count, member in enumerate(archive, 1): + budget.check() + name = member.name + parts = PurePosixPath(name).parts + if (count > MAX_MEMBERS or not parts or parts[0] != "package" + or len(os.fsencode(name)) > 4096 or name.rstrip("/") != "/".join(parts) + or name.casefold() in seen or "\\" in name + or any(not re.fullmatch(r"[A-Za-z0-9._-]+", part) or part in (".", "..") for part in parts)): + raise ValueError("unsafe, duplicate or excessive archive member") + seen.add(name.casefold()) + if member.isdir() and member.size == 0: + continue + if member.type not in (tarfile.REGTYPE, tarfile.AREGTYPE) or not 0 <= member.size <= MAX_BINARY: + raise ValueError("archive member must be an ordinary bounded regular file") + if name == "package/bin/cline": + if binary is not None or member.size != BINARY_BYTES: + raise ValueError("unexpected Cline binary member size or duplicate") + with archive.extractfile(member) as source: + binary = bounded_read(grok.BudgetReader(source, budget), MAX_BINARY) + if len(binary) != member.size: + raise ValueError("truncated Cline binary") + if binary is None: + raise ValueError("missing exact package/bin/cline member") + return binary + + +def acquire_binary(directory, budget): + with urllib.request.urlopen(URL, timeout=min(90, budget.check())) as response: + body = bounded_read(grok.BudgetReader(response, budget), MAX_ARCHIVE) + native.verify_digest(body, ARCHIVE_PIN) + binary = archive_binary(body, budget) + actual = hashlib.sha256(binary).hexdigest() + if actual != BINARY_SHA256: + raise ValueError("Cline binary integrity mismatch") + path = directory / "cline" + path.write_bytes(binary) + path.chmod(0o700) + return path, {"url": URL, "version": VERSION, "archive_integrity": ARCHIVE_PIN, + "archive_bytes": len(body), "binary_sha256": actual, "binary_bytes": len(binary), + "installer_executed": False, "archived_assets_materialized": False} + + +def runtime_environment(home, temporary): + env = native.profile_environment(home, "linux-amd64") + env.update({"PATH": "/usr/bin:/bin", "TMPDIR": str(temporary), "LANG": "C.UTF-8", + "CLINE_NO_AUTO_UPDATE": "1"}) + return env + + +def write_fixture(path, text): + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("x", encoding="utf-8") as output: + output.write(text) + path.chmod(0o600) + + +def prepare_fixtures(root): + paths = {} + for owner, name, transport in (("home", "uap-cline-user", "streamableHttp"), + ("project-a", "uap-cline-a", "streamableHttp"), + ("project-b", "uap-cline-b", "sse")): + selected = root / owner + settings = (selected / ".cline/data/settings/cline_mcp_settings.json" if owner == "home" + else selected / ".uap/clients/cline/mcp.json") + document = {"mcpServers": {name: {"transport": {"type": transport, + "url": "http://127.0.0.1:9/" + name}, "disabled": True}}} + write_fixture(settings, json.dumps(document, indent=2) + "\n") + write_fixture(selected / ".cline/skills" / name / "SKILL.md", + f"---\nname: {name}\ndescription: Inert Cline source research marker\n---\n\nText only. No executable, hook, plugin, MCP command or task.\n") + paths[owner] = settings + return paths + + +def child_environment(base, paths, override): + if base.get("CLINE_NO_AUTO_UPDATE") != "1" or "CLINE_MCP_SETTINGS_PATH" in base: + raise ValueError("Cline requires explicit no-update and a clean base override environment") + selected = dict(base) + if override is not None: + if override not in PROJECTS: + raise ValueError("MCP override must select a known disposable project") + selected["CLINE_MCP_SETTINGS_PATH"] = str(paths[override]) + return selected + + +def validate_probe(record, stdout, stderr, kind): + if record["error"] or record["exit"] != 0: + return False + try: + if kind == "version": + return re.search(r"(? 32 or time.monotonic() - started > 5: + raise TimeoutError("scratch cleanup bound") + os.fchmod(held, os.fstat(held).st_mode | 0o700) + with os.scandir(held) as entries: + for entry in entries: + count[0] += 1 + if count[0] > 20000 or time.monotonic() - started > 5: + raise TimeoutError("scratch cleanup entry/deadline bound") + before = os.stat(entry.name, dir_fd=held, follow_symlinks=False) + if before.st_uid != expected.st_uid: + raise ValueError("foreign scratch owner") + if stat.S_ISDIR(before.st_mode): + child = os.open(entry.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=held) + try: + if not os.path.samestat(before, os.fstat(child)): + raise ValueError("scratch descendant replaced") + clear(child, depth + 1) + finally: + os.close(child) + os.rmdir(entry.name, dir_fd=held) + else: + os.unlink(entry.name, dir_fd=held) + clear(descriptor, 0) + if not os.path.samestat(expected, os.stat(root.name, dir_fd=parent, follow_symlinks=False)): + raise ValueError("scratch root replaced") + os.rmdir(root.name, dir_fd=parent) + finally: + if descriptor is not None: + os.close(descriptor) + os.close(parent) + + +def deny_late_imports(event, _args): + if event == "import": + raise RuntimeError("post-native publisher import refused") + + +def publish(raw, destination, work_seconds=4.0, reap_seconds=1.0): + pid = os.fork() + if pid == 0: + try: + os.chdir("/") + sys.addaudithook(deny_late_imports) + admitted = artifacts.write_archive(raw, destination, client="cline") + os._exit(0 if admitted else 2) + except BaseException: + os._exit(3) + work_deadline = time.monotonic() + work_seconds + while time.monotonic() < work_deadline: + waited, status = os.waitpid(pid, os.WNOHANG) + if waited: + return {"complete": os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, "child_reaped": True, + "exit": os.waitstatus_to_exitcode(status), "archive_failure_allowed": os.WIFEXITED(status) and os.WEXITSTATUS(status) == 2, + "error": None if status == 0 else "artifact admission refused"} + time.sleep(0.01) + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + reap_deadline = time.monotonic() + reap_seconds + while time.monotonic() < reap_deadline: + waited, status = os.waitpid(pid, os.WNOHANG) + if waited: + return {"complete": False, "child_reaped": True, "exit": os.waitstatus_to_exitcode(status), + "archive_failure_allowed": False, "error": "artifact admission deadline"} + time.sleep(0.01) + return {"complete": False, "child_reaped": False, "exit": None, + "archive_failure_allowed": False, "error": "artifact admission cleanup unproved"} + + +def quarantine_upload(upload, expected): + """Withhold only the original upload directory, without recreating it.""" + if upload.parent.resolve() != upload.parent: + raise ValueError("quarantine parent is aliased") + parent = os.open(upload.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + current = os.stat(upload.name, dir_fd=parent, follow_symlinks=False) + if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.getuid() + or not os.path.samestat(current, expected)): + raise ValueError("unproved publisher upload authority changed") + quarantine = Path(tempfile.mkdtemp(prefix="upload-unproved-", dir=upload.parent)) + held = os.open(quarantine, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + os.rename(upload.name, "withheld", src_dir_fd=parent, dst_dir_fd=held) + if not os.path.samestat(os.stat("withheld", dir_fd=held, follow_symlinks=False), expected): + raise ValueError("quarantined upload identity changed") + finally: + os.close(held) + finally: + os.close(parent) + return quarantine / "withheld" + + +def admit_publication(raw, upload, expected, work_seconds=4.0, reap_seconds=1.0): + admitted = publish(raw, upload / "research.zip", work_seconds=work_seconds, reap_seconds=reap_seconds) + if not admitted["complete"] and not (admitted["child_reaped"] and admitted["archive_failure_allowed"]): + quarantine_upload(upload, expected) + return admitted + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + try: + cline.native.require_hosted("linux-amd64") + if os.getuid() <= 0 or os.getuid() != os.geteuid() or os.getgid() <= 0 or os.getgid() != os.getegid(): + raise RuntimeError("original nonroot identity required") + except RuntimeError as error: + print(str(error)[:512], file=sys.stderr) + return 2 + output = args.output.absolute() + output.mkdir(mode=0o700, parents=True, exist_ok=False) + raw, upload = output / "raw", output / "upload" + raw.mkdir(mode=0o700) + raw_identity = raw.lstat() + upload.mkdir(mode=0o700) + upload_identity = upload.lstat() + summary = {"schema_version": 1, "client": "cline", "version": "3.0.73", "commands": [], + "collection_complete": False, "read_only_unique_marker_discovery_qualified": False, + "python_runtime": {"version": sys.version[:512], "implementation": sys.implementation.name[:64], "executable": sys.executable[:1024]}, + "isolation": "fresh task authority and bounded direct children; no OS sandbox or unrelated network claim"} + summary.update({key: False for key in cline_project_envelope.NATIVE_FALSE}) + root = Path(tempfile.mkdtemp(prefix="uap-cline-project-gate-", dir=os.environ["RUNNER_TEMP"])).resolve() + expected = root.lstat() + for name in ("utf-8", "cp437"): + codecs.lookup(name) + try: + with cline.grok.Budget(460) as budget: + selected_go = shutil.which("go") + if not selected_go: + raise ValueError("Go executable unavailable") + go = Path(selected_go).resolve(strict=True) + for name in ("binary", "bin", "fixtures"): + (root / name).mkdir(mode=0o700) + env = clean_environment(root, go) + checkout(SOURCE, env, raw, summary, budget) + build_and_observe(go, root, env, raw, summary, budget) + except BaseException as error: + summary["error"] = type(error).__name__ + ": " + str(error)[:512] + summary["collection_complete"], summary["read_only_unique_marker_discovery_qualified"] = False, False + try: + bounded_cleanup(root, expected) + summary["cleanup_complete"] = True + except BaseException as error: + summary.update(cleanup_complete=False, cleanup_error=type(error).__name__ + ": " + str(error)[:512], + collection_complete=False, read_only_unique_marker_discovery_qualified=False) + if raw.resolve() != raw or not os.path.samestat(raw.lstat(), raw_identity): + raise ValueError("raw evidence authority changed") + descriptor = os.open(raw / SUMMARY, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as destination: + destination.write(json.dumps(summary, indent=2) + "\n") + if upload.resolve() != upload or not os.path.samestat(upload.lstat(), upload_identity): + raise ValueError("upload directory authority changed") + admitted = admit_publication(raw, upload, upload_identity) + if not admitted["complete"]: + print(json.dumps({"artifact_admission": admitted, "collection_complete": False}), file=sys.stderr) + return 0 if admitted["complete"] and summary["collection_complete"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/research-grok-native.py b/scripts/research-grok-native.py new file mode 100644 index 000000000..66f64189a --- /dev/null +++ b/scripts/research-grok-native.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""Collect pinned Grok project-source observations on disposable hosted Linux. + +Text-only candidate fixtures and read-only native inventory cannot by themselves +qualify exact project ownership, installation, model use or MCP initialization. +""" +import argparse +import gzip +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import re +import signal +import stat +import sys +import tempfile +import time +import urllib.request + +from native_client_research import bounded_read, run_probe + +SPEC = importlib.util.spec_from_file_location("grok_research_matrix", Path(__file__).with_name("run-native-client-matrix.py")) +native = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(native) +VERSION = "1.0.67" +URL = "https://x.ai/cli/grok-1.0.67-linux-x86_64.gz" +ARCHIVE_PIN = "sha256:6bbea96cde807c13e4c66d9583d8acdd155e446d93131c2820999cf5523ed5a5" +BINARY_SHA256 = "ce33b1a29e37901f56fe59916cda7a1149e6fd377118f918e90d89f9abb52a51" +MAX_ARCHIVE, MAX_BINARY = 96 << 20, 256 << 20 +MAX_ENTRIES, MAX_DEPTH, MAX_FILE, MAX_TOTAL = 4096, 32, 1 << 20, 64 << 20 +MAX_METADATA = 4096 +COLLECTOR_SECONDS = 8 * 60 +PLUGIN_NAME = "uap-project-native-research" +STAGES = ("empty", "user", "project-a", "project-b") +PROJECTS = ("project-a", "project-b") +HELP_PROBES = (("version", ("--version",)), ("plugin-list-help", ("plugin", "list", "--help")), + ("inspect-help", ("inspect", "--help"))) +JSON_PROBES = (("list", ("plugin", "list", "--json")), ("inspect", ("inspect", "--json"))) +EXPECTED_PROBES = ([name for name, _ in HELP_PROBES] + + [f"{stage}-{project}-{kind}" for stage in STAGES for project in PROJECTS for kind, _ in JSON_PROBES]) + + +class CollectorDeadline(Exception): + pass + + +class Budget: + def __init__(self, seconds=COLLECTOR_SECONDS): + self.deadline = time.monotonic() + seconds + + def check(self): + remaining = self.deadline - time.monotonic() + if remaining <= 0: + raise CollectorDeadline("total collector deadline exceeded") + return remaining + + def alarm(self, _signum, _frame): + raise CollectorDeadline("total collector deadline exceeded") + + def __enter__(self): + remaining = self.check() + self.previous_handler = signal.signal(signal.SIGALRM, self.alarm) + self.previous_timer = signal.setitimer(signal.ITIMER_REAL, remaining) + return self + + def __exit__(self, *_args): + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, self.previous_handler) + if self.previous_timer[0]: + signal.setitimer(signal.ITIMER_REAL, *self.previous_timer) + + +class BudgetReader: + def __init__(self, source, budget): + self.source, self.budget = source, budget + + def read(self, count): + self.budget.check() + return self.source.read(count) + + +def acquire_binary(directory, budget): + with urllib.request.urlopen(URL, timeout=min(90, budget.check())) as response: + body = bounded_read(BudgetReader(response, budget), MAX_ARCHIVE) + native.verify_digest(body, ARCHIVE_PIN) + with gzip.GzipFile(fileobj=io.BytesIO(body)) as compressed: + binary = bounded_read(BudgetReader(compressed, budget), MAX_BINARY) + actual = hashlib.sha256(binary).hexdigest() + if not binary or actual != BINARY_SHA256: + raise ValueError("Grok binary integrity mismatch") + path = directory / "grok" + path.write_bytes(binary) + path.chmod(0o700) + return path, {"url": URL, "version": VERSION, "archive_integrity": ARCHIVE_PIN, + "integrity_source": "measured official HTTPS versioned bytes; no separate vendor SHA256 published", + "archive_bytes": len(body), "binary_sha256": actual, "binary_bytes": len(binary), + "installer_executed": False} + + +def runtime_environment(home, temporary): + env = native.profile_environment(home, "linux-amd64") + env.update({"PATH": "/usr/bin:/bin", "TMPDIR": str(temporary), "LANG": "C.UTF-8"}) + return env + + +def regular_identity(info): + return (info.st_dev, info.st_ino, info.st_mode, info.st_nlink, info.st_size, + info.st_mtime_ns, info.st_ctime_ns) + + +def inventory(roots, budget): + """Traverse descriptor-relative, no-follow paths; every read/count is bounded.""" + result, totals = {}, {"entries": 0, "bytes": 0} + + def walk(directory_fd, prefix, depth): + budget.check() + if depth > MAX_DEPTH: + raise ValueError("inventory depth limit exceeded") + names = [] + with os.scandir(directory_fd) as entries: + for entry in entries: + budget.check() + totals["entries"] += 1 + if totals["entries"] > MAX_ENTRIES or len(os.fsencode(entry.name)) > MAX_METADATA: + raise ValueError("inventory entry/name limit exceeded") + names.append(entry.name) + for name in sorted(names): + budget.check() + path = prefix + "/" + name + if len(os.fsencode(path)) > MAX_METADATA: + raise ValueError("inventory path metadata limit exceeded") + before = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) + mode = stat.S_IMODE(before.st_mode) + record = {"mode": oct(mode), "size": before.st_size} + if stat.S_ISDIR(before.st_mode): + record["kind"] = "directory" + child_fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory_fd) + try: + opened = os.fstat(child_fd) + if regular_identity(opened) != regular_identity(before): + raise ValueError("inventory directory changed before admission") + walk(child_fd, path, depth + 1) + if regular_identity(os.fstat(child_fd)) != regular_identity(opened): + raise ValueError("inventory directory changed during traversal") + finally: + os.close(child_fd) + elif stat.S_ISREG(before.st_mode): + if before.st_nlink != 1 or before.st_size > MAX_FILE: + raise ValueError("inventory regular file alias/size refused") + descriptor = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory_fd) + with os.fdopen(descriptor, "rb") as source: + opened = os.fstat(source.fileno()) + if not stat.S_ISREG(opened.st_mode) or regular_identity(opened) != regular_identity(before): + raise ValueError("inventory file changed before admission") + body = bounded_read(BudgetReader(source, budget), min(MAX_FILE, MAX_TOTAL - totals["bytes"])) + totals["bytes"] += len(body) + if len(body) != before.st_size or regular_identity(os.fstat(source.fileno())) != regular_identity(before): + raise ValueError("inventory file changed during read") + record.update({"kind": "file", "sha256": hashlib.sha256(body).hexdigest()}) + elif stat.S_ISLNK(before.st_mode): + target = os.readlink(name, dir_fd=directory_fd) + if len(os.fsencode(target)) > MAX_METADATA: + raise ValueError("inventory link metadata limit exceeded") + record.update({"kind": "symlink", "target": target}) + else: + record["kind"] = "special" + record["type"] = oct(stat.S_IFMT(before.st_mode)) + after = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) + if regular_identity(after) != regular_identity(before): + raise ValueError("inventory entry changed during observation") + result[path] = record + + for label, root in roots.items(): + budget.check() + descriptor = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + walk(descriptor, label, 0) + finally: + os.close(descriptor) + return result + + +def require_baseline(snapshot, baseline): + for name, expected in baseline.items(): + if snapshot.get(name) != expected: + raise ValueError("read-only observation changed fixture baseline: " + name[:MAX_METADATA]) + + +def fixture(root, marker, version): + """Only test-owned plain manifest/skill text; never adopt an existing package.""" + parent = root + for name in (".grok", "plugins"): + parent = parent / name + try: + parent.mkdir(mode=0o755) + except FileExistsError: + if not stat.S_ISDIR(parent.lstat().st_mode): + raise ValueError("fixture parent is not a real directory") + plugin = parent / PLUGIN_NAME + plugin.mkdir(mode=0o755) + manifest_dir, skill = plugin / ".grok-plugin", plugin / "skills" / marker + manifest_dir.mkdir(mode=0o755) + skill.mkdir(parents=True, mode=0o755) + manifest = {"name": PLUGIN_NAME, "version": version, "description": "Inert text-only native research", "skills": "./skills/"} + bodies = {manifest_dir / "plugin.json": json.dumps(manifest, indent=2) + "\n", + skill / "SKILL.md": f"---\nname: {marker}\ndescription: Inert native discovery research marker\n---\n\nText only. No commands, hooks, MCP servers or executable payload.\n"} + for path, text in bodies.items(): + with path.open("x", encoding="utf-8") as output: + output.write(text) + path.chmod(0o644) + return tuple(bodies) + + +def unique_object(pairs): + value = {} + for key, entry in pairs: + if key in value: + raise ValueError("duplicate JSON object key") + value[key] = entry + return value + + +def invalid_constant(_value): + raise ValueError("non-JSON numeric constant") + + +def validate_probe(record, stdout, stderr, kind): + if record["error"] or record["exit"] != 0: + return False + try: + if kind == "version": + return re.search(r"(? MAX_MEMBERS: + raise ValueError("too many archive members") + name = member.name + if name.startswith("./"): + name = name[2:] + parts = PurePosixPath(name).parts + if (not name or name.startswith("/") or "\\" in name or "\0" in name + or any(part in ("..", ".") or ":" in part for part in parts) + or (name != "." and name.rstrip("/") != "/".join(parts)) + or name.casefold() in seen): + raise ValueError("unsafe or duplicate archive member") + seen.add(name.casefold()) + if member.isdir() and member.size == 0: + continue + if (member.type not in (tarfile.REGTYPE, tarfile.AREGTYPE) + or PurePosixPath(name).name != "qodercli" or binary is not None + or not 0 < member.size <= MAX_BINARY): + raise ValueError("expected one bounded regular qodercli binary") + with archive.extractfile(member) as source: + binary = bounded_read(source, MAX_BINARY) + if len(binary) != member.size: + raise ValueError("truncated binary member") + if binary is None: + raise ValueError("missing qodercli binary") + return binary + + +def acquire_binary(directory): + with urllib.request.urlopen(URL, timeout=90) as response: + body = bounded_read(response, MAX_ARCHIVE) + native.verify_digest(body, ARCHIVE_PIN) + binary = archive_binary(body) + actual = hashlib.sha256(binary).hexdigest() + if actual != BINARY_SHA256: + raise ValueError("binary integrity mismatch") + path = directory / "qoder" + path.write_bytes(binary) + path.chmod(0o700) + return path, {"url": URL, "version": VERSION, "archive_integrity": ARCHIVE_PIN, + "archive_bytes": len(body), "binary_sha256": actual, + "binary_bytes": len(binary), "installer_executed": False} + + +def runtime_environment(home, temp): + env = native.profile_environment(home, "linux-amd64") + env.update({"PATH": "/usr/bin:/bin", "TMPDIR": str(temp), "LANG": "C.UTF-8"}) + return env + + +def validate_probe(record, stdout, stderr): + if record["error"] or record["exit"] != 0 or stderr: + return False + try: + if record["probe"] == "version": + return re.search(r"(? MAX_DEPTH: + raise ValueError("inventory depth limit exceeded") + names = [] + with os.scandir(fd) as entries: + for item in entries: + budget.check() + count += 1 + if count > MAX_ENTRIES or len(os.fsencode(item.name)) > MAX_METADATA: + raise ValueError("inventory entry/name bound exceeded") + names.append(item.name) + for name in sorted(names): + budget.check() + path = prefix + "/" + name + if len(os.fsencode(path)) > MAX_METADATA: + raise ValueError("inventory path bound exceeded") + before = os.stat(name, dir_fd=fd, follow_symlinks=False) + record = {"mode": oct(stat.S_IMODE(before.st_mode)), "size": before.st_size} + result[path] = record + if stat.S_ISDIR(before.st_mode): + record["kind"] = "directory" + child = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd) + try: + opened = os.fstat(child) + if identity(opened) != identity(before): + raise ValueError("directory changed before inventory admission") + walk(child, path, depth + 1) + if identity(os.fstat(child)) != identity(opened): + raise ValueError("directory changed during inventory") + finally: + os.close(child) + elif stat.S_ISREG(before.st_mode): + record["kind"] = "file" + if before.st_nlink != 1 or before.st_size > MAX_FILE or before.st_size > MAX_TOTAL - total: + raise ValueError("inventory file size/alias/aggregate bound exceeded") + child = os.open(name, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW, dir_fd=fd) + with os.fdopen(child, "rb") as source: + if identity(os.fstat(source.fileno())) != identity(before): + raise ValueError("file changed before inventory admission") + body = bounded_read(grok.BudgetReader(source, budget), min(MAX_FILE, MAX_TOTAL - total)) + if len(body) != before.st_size or identity(os.fstat(source.fileno())) != identity(before): + raise ValueError("file changed during inventory") + total += len(body) + record["sha256"] = hashlib.sha256(body).hexdigest() + elif stat.S_ISLNK(before.st_mode) and native_log_alias and path == "home/.qoder/logs/latest": + record.update({"kind": "refused", "type": oct(stat.S_IFMT(before.st_mode))}) + budget.check() + if before.st_nlink != 1 or not 0 <= before.st_size <= MAX_METADATA or before.st_size > MAX_TOTAL - total: + raise ValueError("native log alias metadata size/alias/aggregate bound exceeded") + # Read only literal link bytes relative to the already held real logs directory. + # They never become a path for an open/stat/native command. + try: + target = os.readlink(os.fsencode(name), dir_fd=fd) + if not isinstance(target, bytes): + target = os.fsencode(target) + budget.check() + except grok.CollectorDeadline as exc: + raise InventoryError(str(exc), result) from exc + if len(target) != before.st_size or len(target) > MAX_METADATA or len(target) > MAX_TOTAL - total: + raise ValueError("native log alias target byte count changed or exceeded bound") + if identity(os.stat(name, dir_fd=fd, follow_symlinks=False)) != identity(before): + raise ValueError("native log alias changed during metadata observation") + total += len(target) + record.update({"kind": "symlink_metadata_only", "target_bytes": len(target), + "target_sha256": hashlib.sha256(target).hexdigest(), "target_hex": target.hex(), + "target_followed": False}) + else: + record["kind"] = "refused" + record["type"] = oct(stat.S_IFMT(before.st_mode)) + raise ValueError("inventory symlink or special file refused") + if identity(os.stat(name, dir_fd=fd, follow_symlinks=False)) != identity(before): + raise ValueError("inventory path changed after read") + + try: + for label, root in roots.items(): + budget.check() + before = os.stat(root, follow_symlinks=False) + result[label] = {"kind": "directory", "mode": oct(stat.S_IMODE(before.st_mode)), "size": before.st_size} + fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + if identity(os.fstat(fd)) != identity(before): + raise ValueError("inventory root changed before admission") + walk(fd, label, 0) + if identity(os.fstat(fd)) != identity(before) or identity(os.stat(root, follow_symlinks=False)) != identity(before): + raise ValueError("inventory root changed during observation") + finally: + os.close(fd) + except (ValueError, OSError) as exc: + raise InventoryError(str(exc), result) from exc + return result + + +def file_digest(path, budget, limit): + before = os.stat(path, follow_symlinks=False) + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or before.st_size > limit: + raise ValueError("binary/fixture must be bounded regular unaliased file") + fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW) + digest, length = hashlib.sha256(), 0 + with os.fdopen(fd, "rb") as stream: + if identity(os.fstat(stream.fileno())) != identity(before): + raise ValueError("binary/fixture changed before reading") + while True: + budget.check() + body = stream.read(min(1 << 20, limit + 1 - length)) + if not body: + break + digest.update(body) + length += len(body) + if length > limit: + raise ValueError("binary/fixture byte bound exceeded") + if identity(os.fstat(stream.fileno())) != identity(before): + raise ValueError("binary/fixture changed during reading") + if identity(os.stat(path, follow_symlinks=False)) != identity(before): + raise ValueError("binary/fixture path changed after reading") + return length, digest.hexdigest() + + +def read_regular(path, budget, limit): + before = os.stat(path, follow_symlinks=False) + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or before.st_size > limit: + raise ValueError("bounded collector record must be regular and unaliased") + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, "rb") as stream: + if identity(os.fstat(stream.fileno())) != identity(before): + raise ValueError("collector record changed before read") + body = bounded_read(grok.BudgetReader(stream, budget), limit) + if len(body) != before.st_size or identity(os.fstat(stream.fileno())) != identity(before): + raise ValueError("collector record changed during read") + if identity(os.stat(path, follow_symlinks=False)) != identity(before): + raise ValueError("collector record changed after read") + return body + + +def fixtures(root): + sources = {} + for label, version, marker in (("user", "0.9.0", "USER"), ("A", "1.0.0", "A"), ("B", "1.1.0", "B")): + source = root / "sources" / label + manifest = source / ".qoder-plugin/plugin.json" + skill = source / "skills/marker/SKILL.md" + manifest.parent.mkdir(parents=True) + skill.parent.mkdir(parents=True) + manifest.write_text(json.dumps({"name": PLUGIN_NAME, "version": version, "description": "Inert scoped research fixture"}) + "\n") + skill.write_text(f"---\nname: marker\ndescription: Inert {marker} research marker\n---\n{marker}\n") + sources[label] = source + return sources + + +def fixture_pins(sources, budget): + return {str(path): file_digest(path, budget, MAX_FILE)[1] + for source in sources.values() for path in (source / ".qoder-plugin/plugin.json", source / "skills/marker/SKILL.md")} + + +def check_pins(request, budget): + size, digest = file_digest(Path(request["binary"]), budget, phase1.MAX_BINARY) + if size != BINARY_BYTES or digest != phase1.BINARY_SHA256: + raise ValueError("pinned binary changed before native execution") + sources = {"source-" + key: Path(value) for key, value in request["sources"].items()} + if inventory(sources, budget) != request["source_baseline"]: + raise ValueError("complete inert source fixture tree changed") + for path, expected in request["fixture_pins"].items(): + if file_digest(Path(path), budget, MAX_FILE)[1] != expected: + raise ValueError("fixed source fixture changed") + + +def isolation_facts(parent_namespace, uid, gid): + status = {} + with open("/proc/self/status", encoding="ascii") as stream: + body = stream.read(65537) + if len(body) > 65536: + raise ValueError("process status exceeds bound") + for line in body.splitlines(): + key, _, value = line.partition(":") + status[key] = value.strip() + namespace = os.readlink("/proc/self/ns/net") + interfaces = sorted(name for _, name in socket.if_nameindex()) + facts = {"uid": os.getuid(), "gid": os.getgid(), "euid": os.geteuid(), "egid": os.getegid(), "groups": os.getgroups(), "network_namespace": namespace, + "parent_network_namespace": parent_namespace, "interfaces": interfaces, + "no_new_privs": status.get("NoNewPrivs"), + "capabilities": {key: status.get(key) for key in ("CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb")}} + if uid == 0 or gid == 0 or facts["uid"] != uid or facts["gid"] != gid or facts["euid"] != uid or facts["egid"] != gid or facts["groups"]: + raise IsolationError("collector must retain original nonroot uid/gid and no supplementary groups", facts) + if namespace == parent_namespace or interfaces != ["lo"] or facts["no_new_privs"] != "1": + raise IsolationError("network namespace or no-new-privileges isolation not established", facts) + if any(value is None or not re.fullmatch("0+", value) for value in facts["capabilities"].values()): + raise IsolationError("collector retained capabilities", facts) + return facts + + +def validate_output(record, stdout, stderr, kind, stage): + if record["error"] or record["exit"] != 0 or stderr: + raise ValueError("native command failed, exceeded bounds, or emitted stderr") + if kind == "version": + if not re.search(r"(?= MAX_ARTIFACT_FILES or len(os.fsencode(entry.name)) > 128: + raise ValueError("artifact entry/name bound exceeded") + info = os.stat(entry.name, dir_fd=source_fd, follow_symlinks=False) + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise ValueError("artifact must be flat ordinary unaliased files") + total += info.st_size + if total > MAX_ARTIFACT: + raise ValueError("artifact total byte bound exceeded") + files.append((entry.name, info)) + for name, info in files: + budget.check() + descriptor = os.open(name, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW, dir_fd=source_fd) + with os.fdopen(descriptor, "rb") as source: + if identity(os.fstat(source.fileno())) != identity(info): + raise ValueError("artifact changed before copy") + target = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=target_fd) + with os.fdopen(target, "wb") as destination: + copied = 0 + while True: + budget.check() + body = source.read(min(1 << 20, info.st_size + 1 - copied)) + if not body: + break + copied += len(body) + if copied > info.st_size: + raise ValueError("artifact grew during copy") + destination.write(body) + if copied != info.st_size or identity(os.fstat(source.fileno())) != identity(info): + raise ValueError("artifact changed during copy") + if identity(os.stat(name, dir_fd=source_fd, follow_symlinks=False)) != identity(info): + raise ValueError("artifact path changed after copy") + if identity(os.fstat(source_fd)) != identity(before) or identity(output.lstat()) != identity(before): + raise ValueError("artifact root changed during copy") + return {"files": len(files), "bytes": total} + finally: + os.close(source_fd) + os.close(target_fd) + + +def stage_artifacts(output, upload, summary): + # The workflow uploads only `upload`, never raw output or this private pending tree. + staging = Path(phase1.tempfile.mkdtemp(prefix=upload.name + "-pending-", dir=upload.parent)) + held = staging.lstat() + try: + with grok.Budget(5) as budget: + admitted = copy_artifacts(output, staging, budget) + if upload.exists() or upload.is_symlink(): + raise ValueError("upload destination is not fresh") + staging.rename(upload) + return {"complete": True, **admitted} + except Exception as exc: + summary["success"] = False + summary["artifact_admission_error"] = f"{type(exc).__name__}: {str(exc)[:512]}" + # A rejected tree is never the upload source. Retain only this bounded failure record. + upload.mkdir(exist_ok=False) + fallback = {"evidence_kind": summary["evidence_kind"], "version": VERSION, "success": False, + "expected_source_commit": summary.get("expected_source_commit", "")[:40], + "observed_source_commit": summary.get("observed_source_commit", "")[:40], + "error": str(summary.get("error", ""))[:512], + "artifact_admission_error": summary["artifact_admission_error"], + "stage": str(summary.get("stage", ""))[:64], "probes_recorded": len(summary.get("probes", [])), + "cleanup_complete": summary.get("cleanup", {}).get("complete", False), **FALSE_CLAIMS} + write_summary(upload, fallback) + try: + cleanup(staging, held, output, "artifact-pending-cleanup") + except Exception: + pass # Private rejected files remain outside the workflow upload path. + return {"complete": False, "error": summary["artifact_admission_error"]} + + +def child_main(request_path): + phase1.native.require_hosted("linux-amd64") + request = strict_json(read_regular(request_path, grok.Budget(5), 1 << 20)) + output = Path(request["output"]) + summary = summary_record() + try: + with grok.Budget(max(0.001, request["deadline"] - time.monotonic())) as budget: + collect(request, summary, budget) + except Exception as exc: + summary["error"] = f"{type(exc).__name__}: {str(exc)[:512]}" + if isinstance(exc, InventoryError): + summary["partial_inventory"] = exc.partial + if isinstance(exc, IsolationError): + summary["isolation"] = exc.facts + write_summary(output, summary) + return 0 if summary["success"] else 1 + + +def run_collection(output, summary): + root = None + try: + if os.getuid() == 0 or os.getgid() == 0 or os.geteuid() != os.getuid() or os.getegid() != os.getgid(): + raise ValueError("original hosted collector must retain nonroot real/effective uid/gid") + with grok.Budget(COLLECTOR_SECONDS) as budget: + verify_checkout(output, summary, budget) + root = Path(phase1.tempfile.mkdtemp(prefix="qoder-populated-")) + held = root.lstat() + for name in ("binary", "home", "tmp", *LOCATIONS): + (root / name).mkdir() + binary, summary["acquisition"] = acquire(root / "binary", budget) + sources = fixtures(root) + request = {"root": str(root), "output": str(output), "binary": str(binary), + "sources": {key: str(path) for key, path in sources.items()}, + "fixture_pins": fixture_pins(sources, budget), + "source_baseline": inventory({"source-" + key: value for key, value in sources.items()}, budget), + "native_environment": phase1.runtime_environment(root / "home", root / "tmp"), + "uid": os.getuid(), "gid": os.getgid(), "parent_namespace": os.readlink("/proc/self/ns/net"), + "deadline": budget.deadline} + request_path = root / "request.json" + request_path.write_text(json.dumps(request)) + record, _, _ = run_probe(wrapper_command(request_path, request), root, {}, output, + "isolated-collector", timeout=budget.check()) + summary["wrapper"] = record + child_path = output / "qoder-populated-research.json" + if child_path.exists(): + child = strict_json(read_regular(child_path, budget, 64 << 20)) + child["acquisition"], child["wrapper"], child["checkout"] = summary["acquisition"], record, summary["checkout"] + child["source_commit"] = child["observed_source_commit"] = summary["source_commit"] + summary.clear() + summary.update(child) + if record["error"] or record["exit"] != 0: + summary["success"] = False + summary.setdefault("error", "isolated collector failed") + except Exception as exc: + summary["success"] = False + summary["error"] = f"{type(exc).__name__}: {str(exc)[:512]}" + finally: + if root is not None: + try: + summary["cleanup"] = cleanup(root, held, output) + except Exception as exc: + summary["cleanup"] = {"complete": False, "error": f"{type(exc).__name__}: {str(exc)[:512]}"} + if not summary["cleanup"]["complete"]: + summary["success"] = False + write_summary(output, summary) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path) + parser.add_argument("--upload-output", type=Path) + parser.add_argument("--collect", type=Path, help=argparse.SUPPRESS) + args = parser.parse_args(argv) + try: + phase1.native.require_hosted("linux-amd64") + except RuntimeError as exc: + print(str(exc)[:512], file=sys.stderr) + return 2 + if args.collect: + return child_main(args.collect) + if args.output is None or args.upload_output is None: + parser.error("--output and --upload-output are required") + output = args.output.absolute() + upload = args.upload_output.absolute() + if upload == output or upload.is_relative_to(output) or output.is_relative_to(upload) or upload.exists() or upload.is_symlink(): + parser.error("raw and fresh upload directories must be separate") + output.mkdir(parents=True, exist_ok=False) + summary = summary_record() + run_collection(output, summary) + admission = stage_artifacts(output, upload, summary) + return 0 if summary["success"] and admission["complete"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/research-vscode-native.py b/scripts/research-vscode-native.py new file mode 100644 index 000000000..7663f6d2c --- /dev/null +++ b/scripts/research-vscode-native.py @@ -0,0 +1,547 @@ +#!/usr/bin/env python3 +"""Pinned VS Code help/version research; genuine disposable hosted Linux only. + +This collector never opens an editor, extension host, workspace, model or MCP +session. Successful collection is not project installation qualification. +""" +import argparse +import gzip +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +import time +import urllib.parse +import urllib.request +import zipfile + +from native_client_research import bounded_read, run_probe + + +def adjacent(name, filename): + spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(filename)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +native = adjacent("vscode_research_matrix", "run-native-client-matrix.py") +bounds = adjacent("vscode_research_bounds", "research-grok-native.py") +VERSION = "1.143.1" +COMMIT = "e646868227c3e3a9e3f24277c8c7220be42c0be7" +URL = "https://vscode.download.prss.microsoft.com/dbazure/download/stable/" + COMMIT + "/code-stable-x64-1792691793.tar.gz" +ARCHIVE_BYTES = 350_844_741 +ARCHIVE_SHA256 = "48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be" +PREFIX = "VSCode-linux-x64" +REQUIRED = ("code", "resources/app/out/cli.js", "resources/app/product.json") +MAX_EXPANDED, MAX_FILE, MAX_MEMBERS = 2 << 30, 512 << 20, 100_000 +MAX_NAME, MAX_DEPTH, MAX_METADATA = 512, 32, 4096 +MAX_ARTIFACT = 32 << 20 +MIN_FREE = 4 << 30 +PROBES = (("version", "--version"), ("help", "--help")) + + +class PublisherRedirects(urllib.request.HTTPRedirectHandler): + def __init__(self): + self.count = 0 + + def redirect_request(self, request, response, code, message, headers, newurl): + self.count += 1 + parsed = urllib.parse.urlsplit(newurl) + if self.count > 3 or parsed.scheme != "https" or parsed.hostname != "vscode.download.prss.microsoft.com" or parsed.username or parsed.password or parsed.port not in (None, 443): + raise ValueError("publisher redirect refused") + return super().redirect_request(request, response, code, message, headers, newurl) + + +def acquire_archive(root, budget): + opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), PublisherRedirects()) + path, digest, count = root / "publisher.tar.gz", hashlib.sha256(), 0 + with opener.open(URL, timeout=min(90, budget.check())) as response, path.open("xb") as output: + while True: + budget.check() + chunk = response.read(min(64 << 10, ARCHIVE_BYTES + 1 - count)) + if not chunk: + break + count += len(chunk) + if count > ARCHIVE_BYTES: + raise ValueError("publisher archive exceeds exact byte bound") + digest.update(chunk) + output.write(chunk) + if count != ARCHIVE_BYTES or digest.hexdigest() != ARCHIVE_SHA256: + raise ValueError("publisher archive integrity/size mismatch") + return path + + +class Expanded: + def __init__(self, source, budget): + self.source, self.budget, self.count = source, budget, 0 + + def read(self, count): + self.budget.check() + body = self.source.read(min(count, MAX_EXPANDED + 1 - self.count)) + self.count += len(body) + if self.count > MAX_EXPANDED: + raise ValueError("expanded archive byte bound exceeded") + return body + + def exact(self, count): + chunks = [] + while count: + part = self.read(min(count, 64 << 10)) + if not part: + raise ValueError("truncated publisher archive") + chunks.append(part) + count -= len(part) + return b"".join(chunks) + + +def path_parts(name, directory=False): + if directory: + name = name.removesuffix("/") + if not name or len(name.encode("utf-8")) > MAX_NAME or "\\" in name or ":" in name or any(ord(c) < 32 or ord(c) == 127 for c in name): + raise ValueError("unsafe archive member name") + parts = name.split("/") + if len(parts) > MAX_DEPTH or parts[0] != PREFIX or any(part in ("", ".", "..") for part in parts): + raise ValueError("archive member escapes fixed publisher root") + return parts + + +def pax_values(body): + result, offset = {}, 0 + while offset < len(body): + space = body.find(b" ", offset) + if space < 0 or space - offset > 8 or not body[offset:space].isdigit(): + raise ValueError("invalid bounded PAX record") + length = int(body[offset:space]) + end = offset + length + if length <= space - offset + 2 or end > len(body) or body[end - 1:end] != b"\n": + raise ValueError("invalid PAX record length") + text = body[space + 1:end - 1].decode("utf-8", "strict") + key, separator, value = text.partition("=") + if not separator or not key or len(key.encode()) > MAX_NAME or len(value.encode()) > MAX_NAME or key in result or "sparse" in key.lower(): + raise ValueError("unsupported or oversized PAX metadata") + result[key] = value + offset = end + return result + + +def directory_fd(root_fd, parts): + current = os.dup(root_fd) + try: + for part in parts: + try: + os.mkdir(part, mode=0o700, dir_fd=current) + except FileExistsError: + pass + child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=current) + os.close(current) + current = child + return current + except BaseException: + os.close(current) + raise + + +def write_member(root_fd, parts, stream, size): + parent = directory_fd(root_fd, parts[:-1]) + try: + descriptor = os.open(parts[-1], os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=parent) + with os.fdopen(descriptor, "wb") as output: + remaining = size + while remaining: + body = stream.exact(min(remaining, 64 << 10)) + output.write(body) + remaining -= len(body) + finally: + os.close(parent) + + +def materialize_archive(path, destination, budget): + """Inspect fixed-size headers before metadata allocation; materialize no links.""" + seen, spellings, global_pax, local_pax = {}, {}, {}, {} + omitted, omitted_hash, count, regular_bytes = 0, hashlib.sha256(), 0, 0 + root_fd = os.open(destination, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + with path.open("rb") as compressed, gzip.GzipFile(fileobj=compressed) as source: + stream = Expanded(source, budget) + while True: + block = stream.exact(512) + if block == bytes(512): + if stream.exact(512) != bytes(512): + raise ValueError("invalid TAR ending") + while True: + tail = stream.read(64 << 10) + if not tail: + break + if any(tail): + raise ValueError("nonzero trailing expanded archive data") + if local_pax: + raise ValueError("dangling archive metadata") + break + count += 1 + if count > MAX_MEMBERS: + raise ValueError("publisher member count exceeded") + # frombuf parses only this fixed header; no archive extension hooks run. + member = tarfile.TarInfo.frombuf(block, "utf-8", "strict") + if len(member.name.encode("utf-8")) > MAX_NAME or len(member.linkname.encode("utf-8")) > MAX_NAME: + raise ValueError("archive header metadata exceeds bound") + if member.type == tarfile.GNUTYPE_SPARSE: + raise ValueError("GNU sparse archive member refused") + if member.type in (tarfile.XHDTYPE, tarfile.XGLTYPE, tarfile.GNUTYPE_LONGNAME, tarfile.GNUTYPE_LONGLINK): + if not 0 < member.size <= MAX_METADATA: + raise ValueError("archive extension metadata exceeds bound") + body = stream.exact(member.size) + padding = stream.exact((-member.size) % 512) + if any(padding): + raise ValueError("nonzero metadata padding") + if member.type in (tarfile.XHDTYPE, tarfile.XGLTYPE): + values = pax_values(body) + if member.type == tarfile.XGLTYPE: + global_pax.update(values) + else: + local_pax.update(values) + else: + if not body.endswith(b"\0") or b"\0" in body[:-1] or len(body) - 1 > MAX_NAME: + raise ValueError("invalid GNU long metadata") + local_pax["path" if member.type == tarfile.GNUTYPE_LONGNAME else "linkpath"] = body[:-1].decode("utf-8", "strict") + continue + metadata = {**global_pax, **local_pax} + local_pax = {} + name = metadata.get("path", member.name) + target = metadata.get("linkpath", member.linkname) + size_text = metadata.get("size", str(member.size)) + if not re.fullmatch(r"[0-9]{1,12}", size_text): + raise ValueError("invalid bounded archive size") + size = int(size_text) + parts = path_parts(name, member.isdir()) + for index in range(1, len(parts) + 1): + spelling = "/".join(parts[:index]) + folded = spelling.casefold() + if folded in spellings and spellings[folded] != spelling: + raise ValueError("case-colliding archive parent") + spellings[folded] = spelling + key = "/".join(parts).casefold() + if key in seen or any(seen.get("/".join(parts[:i]).casefold()) == "file" for i in range(1, len(parts))): + raise ValueError("duplicate/case-colliding or file-parent archive member") + if member.type in (tarfile.REGTYPE, tarfile.AREGTYPE): + if size > MAX_FILE or any(existing.startswith(key + "/") for existing in seen): + raise ValueError("regular archive member bound/parent conflict") + regular_bytes += size + if regular_bytes > MAX_EXPANDED: + raise ValueError("publisher aggregate file bytes exceeded") + seen[key] = "file" + write_member(root_fd, parts, stream, size) + elif member.isdir() and size == 0: + seen[key] = "directory" + descriptor = directory_fd(root_fd, parts) + os.close(descriptor) + elif member.issym() and size == 0: + if len(target.encode("utf-8")) > MAX_NAME or any(ord(c) < 32 or ord(c) == 127 for c in target): + raise ValueError("invalid omitted link metadata") + seen[key] = "omitted-link" + omitted += 1 + omitted_hash.update(json.dumps([name, target], separators=(",", ":")).encode()) + else: + raise ValueError("nonregular publisher archive member refused") + if any(stream.exact((-size) % 512)): + raise ValueError("nonzero archive member padding") + return {"members": count, "expanded_bytes": stream.count, "regular_bytes": regular_bytes, + "omitted_symlinks": omitted, "omitted_symlink_metadata_sha256": omitted_hash.hexdigest()} + finally: + os.close(root_fd) + + +def required_files(root, budget=None): + result = {} + for name in REQUIRED: + path = root / PREFIX / name + current = root + for part in (PREFIX, *name.split("/")): + current = current / part + info = current.lstat() + if stat.S_ISLNK(info.st_mode): + raise ValueError("required publisher path traverses a link") + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, "rb") as source: + before = os.fstat(source.fileno()) + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or before.st_size > MAX_FILE: + raise ValueError("required publisher file is not a bounded regular file") + digest, size, first = hashlib.sha256(), 0, b"" + while True: + if budget is not None: + budget.check() + body = source.read(64 << 10) + if not body: + break + if not first: + first = body[:4] + size += len(body) + if size > MAX_FILE: + raise ValueError("required publisher file grew beyond bound") + digest.update(body) + after = os.fstat(source.fileno()) + if bounds.regular_identity(before) != bounds.regular_identity(after) or bounds.regular_identity(path.lstat()) != bounds.regular_identity(after) or size != before.st_size: + raise ValueError("required publisher file changed while hashing") + if name == "code" and first != b"\x7fELF": + raise ValueError("publisher code member is not ELF") + result[name] = {"sha256": digest.hexdigest(), "bytes": size, "identity": [before.st_dev, before.st_ino]} + return result + + +ISOLATION_VERIFIER = r''' +import json,os,socket,sys +parent,uid,gid,output,env,argv=sys.argv[1:] +observed={"network_namespace":os.stat("/proc/self/ns/net").st_ino,"uid":os.getuid(),"gid":os.getgid(),"euid":os.geteuid(),"egid":os.getegid(),"groups":os.getgroups(),"interfaces":[name for _,name in socket.if_nameindex()]} +if observed["network_namespace"]==int(parent) or observed["uid"]!=int(uid) or observed["gid"]!=int(gid) or observed["uid"]==0 or observed["gid"]==0 or observed["euid"]!=int(uid) or observed["egid"]!=int(gid) or observed["groups"] or observed["interfaces"]!=["lo"]: + raise SystemExit("isolated native process admission failed") +with open("/proc/self/status") as source: + status=dict(line.rstrip().split(":",1) for line in source if ":" in line) +if any(int(status[key].strip(),16) for key in ("CapInh","CapPrm","CapEff","CapBnd","CapAmb")) or status["NoNewPrivs"].strip()!="1": + raise SystemExit("native privilege drop failed") +observed["capabilities_zero"]=True +observed["no_new_privileges"]=True +with open(output,"x") as stream:json.dump(observed,stream,sort_keys=True) +os.execve(json.loads(argv)[0],json.loads(argv),json.loads(env)) +''' + +CLEANUP_CODE = r''' +import os,stat,sys +path,device,inode=sys.argv[1:] +parent,name=os.path.split(path) +pfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW) +root=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=pfd) +identity=os.fstat(root) +if (identity.st_dev,identity.st_ino)!=(int(device),int(inode)):raise SystemExit("cleanup root identity changed") +def clear(fd): + for name in os.listdir(fd): + item=os.stat(name,dir_fd=fd,follow_symlinks=False) + if stat.S_ISDIR(item.st_mode): + child=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd) + try: + if not os.path.samestat(item,os.fstat(child)):raise RuntimeError("cleanup directory changed") + clear(child) + finally:os.close(child) + os.rmdir(name,dir_fd=fd) + else:os.unlink(name,dir_fd=fd) +clear(root) +if not os.path.samestat(identity,os.stat(name,dir_fd=pfd,follow_symlinks=False)):raise SystemExit("cleanup root replaced") +os.close(root) +os.rmdir(name,dir_fd=pfd) +os.close(pfd) +''' + + +def cleanup(root, identity, output, *, isolated=False): + interpreter = ["/usr/bin/python3", "-I"] if isolated else [sys.executable] + cwd = Path("/") if isolated else output + record, _, _ = run_probe(interpreter + ["-c", CLEANUP_CODE, str(root), str(identity.st_dev), str(identity.st_ino)], + cwd, {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, output, "cleanup", timeout=5) + return {"complete": record["exit"] == 0 and record["error"] is None, "probe": record} + + +def probe_environment(root): + for name in ("home", "tmp", "user-data", "extensions", "cwd"): + (root / name).mkdir() + env = native.profile_environment(root / "home", "linux-amd64") + env.update({"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", "TMPDIR": str(root / "tmp"), + "VSCODE_AGENT_FOLDER": str(root / "user-data"), "ELECTRON_RUN_AS_NODE": "1"}) + return env + + +def isolated_command(root, label, env, flag, parent_ns): + if os.getuid() == 0 or os.getgid() == 0 or os.geteuid() != os.getuid() or os.getegid() != os.getgid(): + raise ValueError("hosted native research requires the original nonroot user") + argv = [str(root / "publisher" / PREFIX / "code"), str(root / "publisher" / PREFIX / REQUIRED[1]), flag, + "--user-data-dir", str(root / label / "user-data"), "--extensions-dir", str(root / label / "extensions")] + facts = root / label / "isolation.json" + command = ["/usr/bin/sudo", "-n", "/usr/bin/unshare", "--net", "/usr/bin/setpriv", + "--reuid=" + str(os.getuid()), "--regid=" + str(os.getgid()), "--clear-groups", + "--bounding-set=-all", "--inh-caps=-all", "--ambient-caps=-all", "--no-new-privs", + "/usr/bin/env", "-i", "/usr/bin/python3", "-c", ISOLATION_VERIFIER, + str(parent_ns), str(os.getuid()), str(os.getgid()), str(facts), json.dumps(env), json.dumps(argv)] + return command, argv, facts + + +def validate_probe(record, stdout, stderr, label): + if record["error"] is not None or record["exit"] != 0: + return False + try: + text = stdout.decode("utf-8", "strict") + stderr.decode("utf-8", "strict") + except UnicodeError: + return False + if label == "version": + return VERSION in text.split() and COMMIT in text.split() + return bool(text.strip()) + + +def artifact_bound(output): + files, size = [], 0 + for path in output.iterdir(): + info = path.lstat() + if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 + or not re.fullmatch(r"[A-Za-z0-9._-]{1,128}", path.name)): + raise ValueError("research artifact is not a flat ordinary file") + size += info.st_size + files.append((path, info)) + # Reserve final ZIP directory/header overhead; raw output is never uploaded. + if size > MAX_ARTIFACT - (64 << 10) or len(files) > 64: + raise ValueError("research artifact exceeds total byte/member bound") + return files + + +def publish_artifacts(output, summary, allowed_names=None): + """Publish one bounded ZIP, or only a bounded failure ZIP, to upload/.""" + stage = output / "upload" + error = None + descriptor, name = tempfile.mkstemp(prefix=".upload-", dir=output) + temporary = Path(name) + # Take the source list before the ZIP temporary or upload directory exists. + os.close(descriptor) + temporary.unlink() + try: + files = artifact_bound(output) + if allowed_names is not None and any(path.name not in allowed_names for path, _ in files): + raise ValueError("research artifact name is outside the closed workspace table") + descriptor = os.open(temporary, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(descriptor, "w+b") as container, zipfile.ZipFile(container, "w", compression=zipfile.ZIP_STORED) as archive: + total = 0 + for path, recorded in files: + source_fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(source_fd, "rb") as source, archive.open(path.name, "w") as target: + if bounds.regular_identity(os.fstat(source.fileno())) != bounds.regular_identity(recorded): + raise ValueError("research artifact changed before upload staging") + size = 0 + while True: + body = source.read(min(64 << 10, recorded.st_size + 1 - size)) + if not body: + break + size += len(body) + total += len(body) + if size > recorded.st_size or total > MAX_ARTIFACT - (64 << 10): + raise ValueError("research artifact grew while staging") + target.write(body) + if (size != recorded.st_size + or bounds.regular_identity(os.fstat(source.fileno())) != bounds.regular_identity(recorded) + or bounds.regular_identity(path.lstat()) != bounds.regular_identity(recorded)): + raise ValueError("research artifact changed during upload staging") + if temporary.stat().st_size > MAX_ARTIFACT: + raise ValueError("staged ZIP exceeds upload bound") + except Exception as exc: + error = str(exc)[:512] + temporary.unlink(missing_ok=True) + failure = {"evidence_kind": "vscode-research-upload-refusal", "version": VERSION, + "source_commit": str(summary.get("source_commit", ""))[:128], + "collection_complete": False, "native_project_qualified": False, + "artifact_admission_error": error, "collection_error": str(summary.get("error", ""))[:512]} + descriptor = os.open(temporary, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(descriptor, "w+b") as container, zipfile.ZipFile(container, "w", compression=zipfile.ZIP_STORED) as archive: + archive.writestr("collection-failure.json", json.dumps(failure, indent=2) + "\n") + if temporary.stat().st_size > MAX_ARTIFACT: + raise ValueError("bounded failure ZIP cannot fit configured upload bound") + # upload/ did not exist during native execution and contains only this envelope. + stage.mkdir(mode=0o700, exist_ok=False) + os.link(temporary, stage / "research.zip", follow_symlinks=False) + temporary.unlink() + return error is None + + +def collect(root, output, summary, budget): + if (os.getuid() == 0 or os.getgid() == 0 or os.geteuid() != os.getuid() or os.getegid() != os.getgid() + or shutil.disk_usage(root).free < MIN_FREE): + raise ValueError("nonroot hosted user and at least four GiB free required") + archive = acquire_archive(root, budget) + publisher = root / "publisher" + publisher.mkdir() + summary["archive"] = {"url": URL, "bytes": ARCHIVE_BYTES, "sha256": ARCHIVE_SHA256} + summary["materialization"] = materialize_archive(archive, publisher, budget) + before = required_files(publisher, budget) + (publisher / PREFIX / "code").chmod(0o700) + parent_ns = os.stat("/proc/self/ns/net").st_ino + for label, flag in PROBES: + probe_root = root / label + probe_root.mkdir() + env = probe_environment(probe_root) + if required_files(publisher, budget) != before: + raise ValueError("publisher identity changed before probe") + command, argv, facts = isolated_command(root, label, env, flag, parent_ns) + record, stdout, stderr = run_probe(command, probe_root / "cwd", {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, + output, label, timeout=min(20, budget.check())) + record.update({"native_argv": argv, "native_environment": env, "native_cwd": str(probe_root / "cwd")}) + record["accepted"] = validate_probe(record, stdout, stderr, label) + if facts.is_file() and not facts.is_symlink(): + descriptor = os.open(facts, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, "rb") as source: + identity = os.fstat(source.fileno()) + if not stat.S_ISREG(identity.st_mode) or identity.st_nlink != 1 or identity.st_size > MAX_METADATA: + raise ValueError("isolation facts are not a bounded ordinary file") + body = bounded_read(source, MAX_METADATA) + if bounds.regular_identity(identity) != bounds.regular_identity(os.fstat(source.fileno())): + raise ValueError("isolation facts changed while reading") + facts_record = json.loads(body, object_pairs_hook=bounds.unique_object, parse_constant=bounds.invalid_constant) + if (facts_record.get("network_namespace") == parent_ns or facts_record.get("uid") != os.getuid() + or facts_record.get("gid") != os.getgid() or facts_record.get("euid") != os.getuid() + or facts_record.get("egid") != os.getgid() or facts_record.get("groups") != [] + or facts_record.get("interfaces") != ["lo"] or facts_record.get("capabilities_zero") is not True + or facts_record.get("no_new_privileges") is not True): + raise ValueError("reported isolation facts do not match required boundary") + record["isolation"] = facts_record + else: + record["accepted"] = False + summary["probes"].append(record) + if required_files(publisher, budget) != before: + raise ValueError("publisher identity changed during probe") + if not record["accepted"]: + raise ValueError("pinned publisher probe or isolation failed") + summary["required_files"] = before + summary["collection_complete"] = True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + native.require_hosted("linux-amd64") + except RuntimeError as exc: + print(str(exc)[:512], file=sys.stderr) + return 2 + output = args.output.absolute() + output.mkdir(parents=True, exist_ok=False) + root = None + summary = {"evidence_kind": "vscode-pinned-help-version-research", "version": VERSION, + "publisher_commit": COMMIT, "source_commit": os.environ.get("EXPECTED_COMMIT", ""), + "collection_complete": False, "native_project_qualified": False, + "interpretation": "Independent publisher observation review required; no workspace contract inferred", + "probes": [], "unobserved": ["workspace skills/MCP", "UAP installation ownership", "trust", "extension host/UI", "model/auth", "native restart"]} + try: + with bounds.Budget() as budget: + root = Path(tempfile.mkdtemp(prefix="vscode-native-research-")) + identity = root.lstat() + collect(root, output, summary, budget) + except Exception as exc: + summary["collection_complete"] = False + summary["error"] = f"{type(exc).__name__}: {str(exc)[:512]}" + finally: + if root is not None: + try: + summary["cleanup"] = cleanup(root, identity, output) + except Exception as exc: + summary["cleanup"] = {"complete": False, "error": str(exc)[:512]} + if not summary["cleanup"]["complete"]: + summary["collection_complete"] = False + (output / "vscode-precode-research.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + uploaded = publish_artifacts(output, summary) + return 0 if summary["collection_complete"] and uploaded else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/research-vscode-workspace.py b/scripts/research-vscode-workspace.py new file mode 100644 index 000000000..6a4361a4c --- /dev/null +++ b/scripts/research-vscode-workspace.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +"""Pinned untrusted-workbench metadata research; genuine hosted Linux only. + +No installation, chat, server start, trust grant, sandbox fallback or project +adapter qualification. The privileged fixed helper owns strict native trees. +""" +import argparse +import encodings.ascii +import encodings.cp437 +import encodings.utf_8 +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import selectors +import shutil +import signal +import stat +import subprocess +import sys +import tempfile +import time + +from native_client_research import run_probe + + +def adjacent(name, filename): + spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(filename)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +publisher = adjacent("vscode_workspace_publisher", "research-vscode-native.py") +PINNED_FILES = { + "code": (223685880, "cf8508cc7ddde30cfccc70caeda877a9a99119953c4476dcb30a3eb1a2c415e9"), + "resources/app/out/cli.js": (275169, "bd393ad6f0d28ba0aa7cd79cc685617f5187096f092009e315a4f098349f7976"), + "resources/app/product.json": (68251, "a141a1de8ed44415c0c527b1882362e6abc25e674be052d67c09da71a27fbd7e"), +} +MAX_CONTROL = 64 << 10 +ROOT_TIMEOUT = 460 +ROOT_KILL = 470 +SOURCE_RE = re.compile(r"[0-9a-f]{40}\Z") +GUARD_KEYS = ("GITHUB_ACTIONS", "RUNNER_ENVIRONMENT", "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED", "EXPECTED_COMMIT") + + +def guard(): + publisher.native.require_hosted("linux-amd64") + if (os.getuid() <= 0 or os.getgid() <= 0 or os.getuid() != os.geteuid() + or os.getgid() != os.getegid() or not SOURCE_RE.fullmatch(os.environ.get("EXPECTED_COMMIT", ""))): + raise ValueError("exact-source original nonroot hosted identity required") + + +def clean_environment(root): + return {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", "HOME": str(root)} + + +FRONTEND_CLEANUP_CODE = r''' +import os,stat,sys +path,device,inode,owner=sys.argv[1:] +device,inode,owner=int(device),int(inode),int(owner) +parent,name=os.path.split(path) +if parent!="/tmp" or not name.startswith("vscode-workspace-input-") or not name or owner<=0 or os.getuid()!=owner or os.geteuid()!=owner: + raise SystemExit("frontend cleanup authority refused") +def same(a,b): + return (a.st_dev,a.st_ino,a.st_uid,stat.S_IFMT(a.st_mode))==(b.st_dev,b.st_ino,b.st_uid,stat.S_IFMT(b.st_mode)) +def owned_directory(info): + if not stat.S_ISDIR(info.st_mode) or info.st_uid!=owner or info.st_dev!=device: + raise RuntimeError("frontend cleanup directory authority differs") +parent_fd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW) +root=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent_fd) +identity=os.fstat(root) +if (identity.st_dev,identity.st_ino,identity.st_uid)!=(device,inode,owner) or not same(identity,os.stat(name,dir_fd=parent_fd,follow_symlinks=False)): + raise SystemExit("frontend cleanup root changed") +def clear(fd,expected): + current=os.fstat(fd) + owned_directory(current) + if not same(current,expected):raise RuntimeError("frontend cleanup opened directory changed") + os.fchmod(fd,(current.st_mode&0o777)|0o700) + for entry in os.listdir(fd): + item=os.stat(entry,dir_fd=fd,follow_symlinks=False) + if item.st_uid!=owner or item.st_dev!=device:raise RuntimeError("frontend cleanup leaf authority differs") + if stat.S_ISDIR(item.st_mode): + child=os.open(entry,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd) + try: + opened=os.fstat(child) + if not same(item,opened) or not same(opened,os.stat(entry,dir_fd=fd,follow_symlinks=False)): + raise RuntimeError("frontend cleanup directory replaced") + clear(child,opened) + if not same(opened,os.stat(entry,dir_fd=fd,follow_symlinks=False)): + raise RuntimeError("frontend cleanup directory replaced before removal") + finally:os.close(child) + os.rmdir(entry,dir_fd=fd) + else: + if not (stat.S_ISREG(item.st_mode) or stat.S_ISLNK(item.st_mode)): + raise RuntimeError("frontend cleanup unsupported leaf") + if not same(item,os.stat(entry,dir_fd=fd,follow_symlinks=False)): + raise RuntimeError("frontend cleanup leaf replaced") + os.unlink(entry,dir_fd=fd) +clear(root,identity) +if not same(identity,os.stat(name,dir_fd=parent_fd,follow_symlinks=False)): + raise SystemExit("frontend cleanup root replaced before removal") +os.close(root) +os.rmdir(name,dir_fd=parent_fd) +os.close(parent_fd) +''' + + +def cleanup_frontend(root, identity, output): + record, _, _ = run_probe(["/usr/bin/python3", "-I", "-c", FRONTEND_CLEANUP_CODE, + str(root), str(identity.st_dev), str(identity.st_ino), str(identity.st_uid)], + Path("/"), {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, output, "cleanup", timeout=5) + return {"complete": record["exit"] == 0 and record["error"] is None, "probe": record} + + +def digest(path): + h = hashlib.sha256() + with path.open("rb") as stream: + while body := stream.read(64 << 10): + h.update(body) + return h.hexdigest() + + +def source_checkout(repo, output): + record, stdout, stderr = run_probe(["/usr/bin/git", "rev-parse", "HEAD"], repo, + clean_environment(output), output, "source-checkout", timeout=5, limit=MAX_CONTROL) + if record["error"] is not None or record["exit"] != 0 or stderr or stdout.decode("ascii", "strict").strip() != os.environ["EXPECTED_COMMIT"]: + raise ValueError("actual checkout does not equal exact expected source") + return record + + +def verify_publisher(root, budget): + found = publisher.required_files(root, budget) + for name, (size, expected) in PINNED_FILES.items(): + if found[name]["bytes"] != size or found[name]["sha256"] != expected: + raise ValueError("previously measured exact publisher identity differs") + return found + + +def build_helper(repo, root, output, budget): + go = shutil.which("go") + if not go: + raise ValueError("pinned workflow Go toolchain missing") + home = root / "build-home" + home.mkdir() + env = clean_environment(home) + env.update({"PATH": str(Path(go).parent) + ":/usr/bin:/bin", "GOTOOLCHAIN": "local", "GOMAXPROCS": "2", "GOMEMLIMIT": "512MiB"}) + version, stdout, stderr = run_probe([go, "version"], repo, env, output, "go-version", timeout=5, limit=MAX_CONTROL) + if version["error"] is not None or version["exit"] != 0 or stdout != b"go version go1.26.10 linux/amd64\n" or stderr: + raise ValueError("exact pinned Go version differs") + executable = root / "controller" + argv = [go, "build", "-trimpath", "-p=1", "-ldflags", "-X main.buildCommit=" + os.environ["EXPECTED_COMMIT"], + "-o", str(executable), "./install/integrationctl/research/vscodeworkspace"] + record, _, _ = run_probe(argv, repo, env, output, "helper-build", timeout=min(60, budget.check()), limit=1 << 20) + if record["error"] is not None or record["exit"] != 0: + raise ValueError("exact-source helper build failed or exceeded its bound") + info = executable.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 64 << 20: + raise ValueError("helper is not an ordinary bounded executable") + return executable, {"version_probe": version, "build": record, "bytes": info.st_size, "sha256": digest(executable)} + + +def signal_controller(process, sig, errors): + try: + os.killpg(process.pid, sig) + except ProcessLookupError: + pass + except PermissionError: + errors.append("controller signal permission refused; cleanup not proved") + + +def capture_controller(argv, repo, output, origin_ns): + env = {key: os.environ[key] for key in GUARD_KEYS} + env.update({"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}) + record = {"argv": argv, "cwd": str(repo), "error": None, "controller_reaped": False} + errors, buffers = [], {"stdout": bytearray(), "stderr": bytearray()} + process, stopping = None, False + try: + process = subprocess.Popen(argv, cwd=repo, env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True) + with selectors.DefaultSelector() as selector: + for name, pipe in (("stdout", process.stdout), ("stderr", process.stderr)): + os.set_blocking(pipe.fileno(), False) + selector.register(pipe, selectors.EVENT_READ, name) + while selector.get_map() or process.poll() is None: + elapsed = (time.monotonic_ns() - origin_ns) / 1e9 + if elapsed >= ROOT_TIMEOUT and not stopping: + errors.append("controller exceeded reserved return deadline") + process.stdin.close() + signal_controller(process, signal.SIGTERM, errors) + stopping = True + if elapsed >= ROOT_KILL: + signal_controller(process, signal.SIGKILL, errors) + errors.append("controller terminal watchdog reached; cleanup unproved") + break + for key, _ in selector.select(0.05): + allowance = MAX_CONTROL - len(buffers[key.data]) + chunk = os.read(key.fd, min(64 << 10, allowance + 1)) + if not chunk: + selector.unregister(key.fileobj) + key.fileobj.close() + else: + buffers[key.data].extend(chunk[:allowance]) + if len(chunk) > allowance: + errors.append("controller stream limit reached") + if not stopping: + process.stdin.close() + signal_controller(process, signal.SIGTERM, errors) + stopping = True + try: + process.wait(timeout=0.1) + record["controller_reaped"] = True + except subprocess.TimeoutExpired: + errors.append("controller did not reap within reserved boundary") + except (OSError, subprocess.SubprocessError) as exc: + errors.append(type(exc).__name__ + ": " + str(exc)[:512]) + finally: + if process is not None: + for pipe in (process.stdin, process.stdout, process.stderr): + if pipe is not None: + pipe.close() + record["exit"] = None if process is None else process.returncode + for stream, body in buffers.items(): + path = output / ("controller." + stream + ".log") + path.write_bytes(body) + record.update({stream + "_file": path.name, stream + "_bytes": len(body), stream + "_sha256": hashlib.sha256(body).hexdigest()}) + if errors or not record["controller_reaped"] or record["exit"] != 0: + record["error"] = "; ".join(errors or ["fixed controller returned failure"])[:2048] + return record + + +def collect(root, output, summary, origin_ns): + repo = Path(__file__).resolve().parent.parent + summary["checkout"] = source_checkout(repo, output) + with publisher.bounds.Budget(seconds=max(1, 420 - (time.monotonic_ns() - origin_ns) / 1e9)) as budget: + if shutil.disk_usage(root).free < publisher.MIN_FREE: + raise ValueError("at least four GiB free required") + archive = publisher.acquire_archive(root, budget) + materialized = root / "publisher" + materialized.mkdir() + summary["archive"] = {"url": publisher.URL, "bytes": publisher.ARCHIVE_BYTES, "sha256": publisher.ARCHIVE_SHA256} + summary["materialization"] = publisher.materialize_archive(archive, materialized, budget) + summary["publisher_files"] = verify_publisher(materialized, budget) + helper, summary["helper"] = build_helper(repo, root, output, budget) + request = {"uid": os.getuid(), "gid": os.getgid(), "euid": os.geteuid(), "egid": os.getegid(), "start_monotonic_ns": origin_ns, + "source_commit": os.environ["EXPECTED_COMMIT"], "observer_root": str(repo / "scripts" / "vscode-workspace-observer"), + "output_root": str(output), "helper_sha256": summary["helper"]["sha256"]} + (root / "request.json").write_text(json.dumps(request), encoding="utf-8") + argv = ["/usr/bin/sudo", "-n", "--preserve-env=" + ",".join(GUARD_KEYS), str(helper), "--input-root", str(root)] + summary["controller"] = capture_controller(argv, repo, output, origin_ns) + if summary["controller"]["error"] is not None: + raise ValueError("fixed root controller failed; retain bounded evidence") + with (output / "vscode-workspace-controller.json").open("rb") as stream: + body = publisher.bounded_read(stream, 1 << 20) + result = json.loads(body, object_pairs_hook=publisher.bounds.unique_object, parse_constant=publisher.bounds.invalid_constant) + if (result.get("source_commit") != os.environ["EXPECTED_COMMIT"] or result.get("collection_complete") is not True + or result.get("native_project_qualified") is not False or result.get("native_mcp_initialization_qualified") is not False + or result.get("native_runtime_qualified") is not False or result.get("cleanup", {}).get("complete") is not True): + raise ValueError("controller did not prove complete bounded collection/cleanup") + summary["collection_complete"] = True + + +def allowed_artifact_names(): + names = {"vscode-workspace-research.json", "vscode-workspace-controller.json", "system-executables.json", "xauth.json"} + for label in ("source-checkout", "go-version", "helper-build", "controller", "cleanup", "xauth", "root-cleanup"): + names.update({label + ".stdout.log", label + ".stderr.log"}) + for label in ("Xvfb", "A", "B", "A-restart", "A-after-removal"): + names.add(label + ".merged.log") + if label != "Xvfb": + names.add(label + ".observation.json") + return names + + +class RefuseNewImports: + """The post-native child may execute only its already preloaded closure.""" + def find_spec(self, fullname, path=None, target=None): + raise ImportError("post-native import refused: " + fullname) + + +def publish_from_memory(output, summary, work_seconds=4.0, reap_seconds=1.0): + pid = os.fork() + if pid == 0: + try: + sys.meta_path = [RefuseNewImports()] + accepted = publisher.publish_artifacts(output, summary, allowed_names=allowed_artifact_names()) + os._exit(0 if accepted else 10) + except BaseException: + os._exit(2) + work_deadline = time.monotonic() + work_seconds + while time.monotonic() < work_deadline: + child, status = os.waitpid(pid, os.WNOHANG) + if child: + return {"complete": os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, "child_reaped": True, + "exit": os.waitstatus_to_exitcode(status), "archive_failure_allowed": os.WIFEXITED(status) and os.WEXITSTATUS(status) == 10, + "error": None if status == 0 else "artifact admission refused"} + time.sleep(0.01) + os.kill(pid, signal.SIGKILL) + reap_deadline = time.monotonic() + reap_seconds + while time.monotonic() < reap_deadline: + child, status = os.waitpid(pid, os.WNOHANG) + if child: + return {"complete": False, "child_reaped": True, "exit": os.waitstatus_to_exitcode(status), "error": "artifact admission deadline"} + time.sleep(0.01) + return {"complete": False, "child_reaped": False, "error": "artifact admission cleanup unproved"} + + + +def quarantine_output(output, expected): + """Withhold the original upload namespace; never recreate its parent.""" + if output.parent.resolve() != output.parent: + raise ValueError("quarantine parent is aliased") + parent = os.open(output.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + current = os.stat(output.name, dir_fd=parent, follow_symlinks=False) + if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.getuid() + or not os.path.samestat(current, expected)): + raise ValueError("quarantine output authority changed") + quarantine = Path(tempfile.mkdtemp(prefix=".vscode-refused-", dir=output.parent)) + held = os.open(quarantine, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + os.rename(output.name, "evidence", src_dir_fd=parent, dst_dir_fd=held) + moved = os.stat("evidence", dir_fd=held, follow_symlinks=False) + if not os.path.samestat(moved, expected): + raise ValueError("quarantine output identity changed") + finally: + os.close(held) + finally: + os.close(parent) + return quarantine / "evidence" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + guard() + except (RuntimeError, ValueError) as exc: + print(str(exc)[:512], file=sys.stderr) + return 2 + output = args.output.absolute() + origin_ns, root = time.monotonic_ns(), None + output.mkdir(parents=True, exist_ok=False) + output_identity = output.lstat() + summary = {"evidence_kind": "vscode-untrusted-workspace-metadata-research", "source_commit": os.environ["EXPECTED_COMMIT"], + "version": publisher.VERSION, "publisher_commit": publisher.COMMIT, "collection_complete": False, + "native_project_qualified": False, "native_mcp_initialization_qualified": False, "native_runtime_qualified": False} + try: + root = Path(tempfile.mkdtemp(prefix="vscode-workspace-input-", dir="/tmp")) + identity = root.lstat() + collect(root, output, summary, origin_ns) + except Exception as exc: + summary["collection_complete"] = False + summary["error"] = type(exc).__name__ + ": " + str(exc)[:2048] + finally: + if root is not None: + try: + summary["frontend_cleanup"] = cleanup_frontend(root, identity, output) + except Exception as exc: + summary["frontend_cleanup"] = {"complete": False, "error": str(exc)[:512]} + if not summary["frontend_cleanup"]["complete"]: + summary["collection_complete"] = False + summary["elapsed_seconds"] = (time.monotonic_ns() - origin_ns) / 1e9 + (output / "vscode-workspace-research.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + admission = publish_from_memory(output, summary) + if not admission["complete"]: + summary["collection_complete"] = False + if not (admission.get("child_reaped") and admission.get("archive_failure_allowed")): + quarantine_output(output, output_identity) + + return 0 if summary["collection_complete"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/scripts/run-native-client-matrix.py b/scripts/run-native-client-matrix.py index 6947dc0d1..442e6abc6 100644 --- a/scripts/run-native-client-matrix.py +++ b/scripts/run-native-client-matrix.py @@ -72,6 +72,54 @@ "claude": {"TestAgentpluginsClaudeNativeLifecycle", "TestAgentpluginsClaudeNativeRuntimeLifecycle", "TestAgentpluginsClaudeNativeCommandsLifecycle"}, "opencode": {"TestAgentpluginsOpenCodeNativeLifecycle", "TestAgentpluginsOpenCodeNativeToolCollision", "TestAgentpluginsOpenCodeNativeRuntimeExtended", "TestAgentpluginsOpenCodeNativeCommandsLifecycle"}, } +PROJECT_TESTS = { + "codex": {"TestAgentpluginsCodexNativeProjectComponentsLifecycle", "TestAgentpluginsCodexNativeTrustedProjectMCP"}, + "claude": {"TestAgentpluginsClaudeNativeProjectPreparedLifecycle", "TestAgentpluginsClaudeNativeTrustedProjectDiscovery"}, + "opencode": {"TestAgentpluginsOpenCodeNativeProjectComponentsLifecycle"}, +} + + +def select_native_tests(client, project_components=False, released=False): + """Keep current-source project proof separate from released user evidence.""" + if not project_components: + return PATTERNS[client], REQUIRED_TESTS[client] + if released or client not in PROJECT_TESTS: + raise ValueError("project proof requires a qualified current-source client, not a released/draft installer") + required = REQUIRED_TESTS[client] | PROJECT_TESTS[client] + return "^(?:" + "|".join(re.escape(name) for name in sorted(required)) + ")$", required + + +def collect_opencode_project_artifact(fixture, output): + """Only the new admitted envelope, in a finite child; never copy its raw tree.""" + destination = output / (fixture.name + ".zip") + error = None + try: + result = subprocess.run([sys.executable, str(Path(__file__).with_name("native_client_project_artifacts.py")), + str(fixture), str(destination)], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=5, check=False) + if result.returncode: + error = "project envelope child refused (exit " + str(result.returncode) + ")" + except (OSError, subprocess.TimeoutExpired) as failure: + error = type(failure).__name__ + ": project envelope child failed or exceeded five seconds" + try: + info = destination.lstat() + valid = not destination.is_symlink() and destination.is_file() and info.st_nlink == 1 and info.st_size <= 32 << 20 + except OSError: + valid = False + if error or not valid: + error = error or "project envelope archive admission refused" + # Write outside upload and atomically replace the final leaf, including + # a refused symlink; never open its target. No recursive failure cleanup. + pending_root = Path(tempfile.mkdtemp(prefix="opencode-fallback-", dir=fixture.parent)) + pending = pending_root / "failure.zip" + with zipfile.ZipFile(pending, "x", compression=zipfile.ZIP_STORED) as archive: + archive.writestr("opencode-project-evidence.json", json.dumps({"success": False, + "native_discovery_qualified": False, "native_mcp_initialization_qualified": False, + "native_model_use_qualified": False, "artifact_admission_error": error, "artifact_python_runtime": {"version": sys.version[:512], "implementation": sys.implementation.name[:64], "executable": sys.executable[:1024]}})) + if destination.exists() and destination.is_dir() and not destination.is_symlink(): + os.replace(destination, pending_root / "refused-directory") + os.replace(pending, destination) + return error def require_verified_pin(pin): @@ -321,6 +369,7 @@ def main(): parser.add_argument("--prepare-only", action="store_true") parser.add_argument("--prepared-input", type=Path) parser.add_argument("--target-scope", choices=('historical-nine', 'linux-amd64'), default='historical-nine') + parser.add_argument("--project-components", action="store_true", help="also require current-source project lifecycle evidence with explicit discovery limits; never released/draft proof") args = parser.parse_args() draft.validate(args) if args.release_state == "draft" and args.producer_source is None: @@ -329,6 +378,11 @@ def main(): parser.error("public mode must not receive --producer-source") if bool(args.release_tag) != bool(args.release_commit): parser.error("--release-tag and --release-commit must be supplied together") + try: + pattern, required_tests = select_native_tests(args.client, args.project_components, + bool(args.release_tag) or args.release_state == "draft" or bool(args.prepared_input) or args.prepare_only) + except ValueError as error: + parser.error(str(error)) require_hosted(args.target) source = Path(__file__).resolve().parents[1] if args.prepare_only: @@ -342,7 +396,7 @@ def main(): output = args.output.resolve() output.mkdir(parents=True, exist_ok=False) scratch = Path(tempfile.mkdtemp(prefix="uap-native-hosted-", dir=os.environ["RUNNER_TEMP"])).resolve() - identity = {"schema_version": 1, "client": args.client, "target": args.target, "started_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof", "status": "failed", "scratch": str(scratch)} + identity = {"schema_version": 1, "python_runtime": {"version": sys.version[:512], "implementation": sys.implementation.name[:64], "executable": sys.executable[:1024]}, "client": args.client, "target": args.target, "started_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "isolation": "disposable GitHub-hosted machine; explicit subprocess environments and fresh profiles; no OS/process isolation from runner or later steps", "network": "not blocked; scripted loopback model endpoints; no real-model or OAuth proof", "status": "failed", "scratch": str(scratch)} try: for key, rev in [("commit", "HEAD"), ("tree", "HEAD^{tree}")]: identity[key] = subprocess.check_output(["git", "rev-parse", rev], cwd=source, encoding="utf-8", errors="strict").strip() @@ -430,11 +484,15 @@ def main(): raise RuntimeError(f"released installer version mismatch: {measured!r}") prefix = "AGENTPLUGINS_" + args.client.upper() env[prefix + "_NATIVE_E2E"] = "1" + if args.project_components: + env[prefix + "_PROJECT_NATIVE_E2E"] = "1" env[prefix + "_BIN"] = str(client) env[prefix + "_SHA256"] = client_evidence["binary_sha256"] env[prefix + "_VERSION"] = {"codex": "0.153.4", "claude": "2.1.263 (Claude Code)", "opencode": "1.18.34"}[args.client] - command = [str(tests), "-test.v", "-test.timeout=15m", "-test.run=" + PATTERNS[args.client]] + command = [str(tests), "-test.v", "-test.timeout=15m", "-test.run=" + pattern] identity["test_command"] = command + identity["required_tests"] = sorted(required_tests) + identity["project_components"] = args.project_components with (output / "native-tests.log").open("w", encoding="utf-8") as transcript: result = subprocess.run(command, cwd=project, env=env, stdout=transcript, stderr=subprocess.STDOUT, timeout=930) identity["exit_code"] = result.returncode @@ -444,17 +502,27 @@ def main(): skipped = re.findall(r"^\s*--- SKIP: ([^\s]+)", log, re.M) identity["passed_tests"] = sorted(passed) identity["skipped_tests"] = skipped - if result.returncode or skipped or not REQUIRED_TESTS[args.client].issubset(passed): + if result.returncode or skipped or not required_tests.issubset(passed): raise RuntimeError("native suite failed, skipped, or omitted a required test; inspect transcript") identity["status"] = "passed" finally: identity["finished_utc"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) # Keep structured evidence and transcripts, never copy client caches, # profile databases, source payload binaries, or ephemeral client auth. + native_passed = identity["status"] == "passed" + project_artifact_errors = [] evidence_dir = scratch / "evidence" allowed = {".json", ".jsonl", ".log"} if evidence_dir.exists(): for fixture in evidence_dir.iterdir(): + if fixture.name.startswith("uap-opencode-project-work-"): + continue + if fixture.name.startswith("uap-opencode-project-admitted-"): + if args.client == "opencode" and args.project_components: + error = collect_opencode_project_artifact(fixture, output) + if error: + project_artifact_errors.append(error) + continue if not fixture.is_dir(): continue for path in fixture.iterdir(): @@ -462,8 +530,13 @@ def main(): destination = output / fixture.name / path.name destination.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(path, destination) + if project_artifact_errors: + identity["status"] = "failed" + identity["project_artifact_admission_errors"] = project_artifact_errors identity["artifact_sha256"] = {p.relative_to(output).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest() for p in output.rglob("*") if p.is_file()} (output / "runner-evidence.json").write_text(json.dumps(identity, indent=2) + "\n", encoding="utf-8") + if native_passed and project_artifact_errors: + raise RuntimeError("new OpenCode project artifact admission failed; bounded failure evidence retained") if __name__ == "__main__": diff --git a/scripts/test_cline_native_research.py b/scripts/test_cline_native_research.py new file mode 100644 index 000000000..6140c7b09 --- /dev/null +++ b/scripts/test_cline_native_research.py @@ -0,0 +1,198 @@ +"""Inert contracts only. Never acquires/executes Cline or forges hosted admission.""" +import gzip +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tarfile +import tempfile +import unittest +from unittest.mock import patch + +SPEC = importlib.util.spec_from_file_location("cline_research", Path(__file__).with_name("research-cline-native.py")) +research = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(research) + + +def archive_bytes(entries): + output = io.BytesIO() + with tarfile.open(fileobj=output, mode="w:gz") as archive: + for name, kind, body in entries: + member = tarfile.TarInfo(name) + member.type = kind + member.size = len(body) if kind in (tarfile.REGTYPE, tarfile.AREGTYPE) else 0 + archive.addfile(member, io.BytesIO(body)) + return output.getvalue() + + +class ClineResearchTests(unittest.TestCase): + def test_only_exact_binary_is_read_and_assets_are_never_materialized(self): + entries = [("package/package.json", tarfile.REGTYPE, b"{}"), + ("package/bin/cline", tarfile.REGTYPE, b"inert"), + ("package/extensions/not-executed.js", tarfile.REGTYPE, b"never execute")] + with patch.object(research, "BINARY_BYTES", 5): + self.assertEqual(research.archive_binary(archive_bytes(entries), research.grok.Budget()), b"inert") + for unsafe in ("/package/bin/cline", "package/../bin/cline", "package/./bin/cline", + "package/bin/../cline", "package\\bin\\cline", "package/bin/cline\n"): + with self.subTest(name=unsafe), self.assertRaises(ValueError): + research.archive_binary(archive_bytes([(unsafe, tarfile.REGTYPE, b"inert")]), research.grok.Budget()) + for kind in (tarfile.SYMTYPE, tarfile.LNKTYPE, tarfile.FIFOTYPE): + with self.subTest(kind=kind), self.assertRaises(ValueError): + research.archive_binary(archive_bytes([("package/bin/cline", kind, b"")]), research.grok.Budget()) + for invalid in (entries + [("package/bin/cline", tarfile.REGTYPE, b"inert")], + entries + [("PACKAGE/bin/CLINE", tarfile.REGTYPE, b"inert")], + [("package/bin/cline", tarfile.REGTYPE, b"wrong size")], entries[:1]): + with self.assertRaises(ValueError): + research.archive_binary(archive_bytes(invalid), research.grok.Budget()) + for key, limit in (("MAX_MEMBERS", 1), ("MAX_BINARY", 2), ("MAX_EXPANDED", 32)): + with patch.object(research, key, limit), self.assertRaises(ValueError): + research.archive_binary(archive_bytes(entries), research.grok.Budget()) + + def test_archive_pin_and_actual_read_limit_precede_parse_or_output(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for body, cap in ((b"wrong pinned archive", 100), (b"12345", 4)): + with patch.object(research.urllib.request, "urlopen", return_value=io.BytesIO(body)), \ + patch.object(research, "MAX_ARCHIVE", cap), patch.object(research, "archive_binary") as parse: + with self.assertRaises(ValueError): + research.acquire_binary(root, research.grok.Budget()) + parse.assert_not_called() + self.assertEqual(list(root.iterdir()), []) + inert = gzip.compress(b"inert bytes; never executed") + pin = "sha256:" + hashlib.sha256(inert).hexdigest() + with patch.object(research.urllib.request, "urlopen", return_value=io.BytesIO(inert)), \ + patch.object(research, "ARCHIVE_PIN", pin), \ + patch.object(research, "archive_binary", return_value=b"untrusted inert binary"): + with self.assertRaisesRegex(ValueError, "binary integrity"): + research.acquire_binary(root, research.grok.Budget()) + self.assertEqual(list(root.iterdir()), []) + + def test_explicit_environment_suppresses_updater_and_rejects_ambient_authority(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + home, temp = root / "home", root / "tmp" + home.mkdir(); temp.mkdir() + ambient = {"GH_TOKEN": "inert", "GITHUB_TOKEN": "inert", "HTTP_PROXY": "foreign", + "CLINE_DIR": "/foreign", "CLINE_DATA_DIR": "/foreign", "CLINE_API_KEY": "inert", + "CLINE_MCP_SETTINGS_PATH": "/foreign", "CLINE_NO_AUTO_UPDATE": "0"} + with patch.dict(os.environ, ambient): + env = research.runtime_environment(home, temp) + self.assertEqual(env["CLINE_NO_AUTO_UPDATE"], "1") + self.assertEqual(env["HOME"], str(home)) + self.assertEqual(env["PATH"], "/usr/bin:/bin") + for key in set(ambient) - {"CLINE_NO_AUTO_UPDATE"}: + self.assertNotIn(key, env) + paths = {name: root / name / "mcp.json" for name in research.PROJECTS} + for override in research.PROJECTS: + selected = research.child_environment(env, paths, override) + self.assertEqual(selected["CLINE_MCP_SETTINGS_PATH"], str(paths[override])) + self.assertEqual(selected["CLINE_NO_AUTO_UPDATE"], "1") + self.assertNotIn("CLINE_MCP_SETTINGS_PATH", research.child_environment(env, paths, None)) + for bad in ({}, {**env, "CLINE_NO_AUTO_UPDATE": "0"}, {**env, "CLINE_MCP_SETTINGS_PATH": "/foreign"}): + with self.assertRaises(ValueError): + research.child_environment(bad, paths, None) + with self.assertRaises(ValueError): + research.child_environment(env, paths, "foreign") + + def test_fixtures_are_disabled_remote_config_and_plain_skill_text_only(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for name in ("home", *research.PROJECTS): + (root / name).mkdir() + paths = research.prepare_fixtures(root) + files = [path for path in root.rglob("*") if path.is_file()] + self.assertEqual(len(files), 6) + for path in files: + self.assertEqual(path.stat().st_mode & 0o111, 0) + self.assertIn(path.suffix, (".md", ".json")) + for owner, path in paths.items(): + entries = json.loads(path.read_text())["mcpServers"] + self.assertEqual(len(entries), 1) + entry = next(iter(entries.values())) + self.assertIs(entry["disabled"], True) + self.assertIn(entry["transport"]["type"], ("streamableHttp", "sse")) + self.assertTrue(entry["transport"]["url"].startswith("http://127.0.0.1:9/")) + self.assertNotIn("command", entry["transport"]) + self.assertNotIn("oauth", entry) + self.assertTrue(path.is_relative_to(root / owner)) + with self.assertRaises(FileExistsError): + research.prepare_fixtures(root) + + def test_finite_argv_cwd_and_process_override_sequence_are_explicit(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for name in ("home", "tmp", "project-a", "project-b", "output"): + (root / name).mkdir() + env = research.runtime_environment(root / "home", root / "tmp") + calls = [] + def inert_probe(argv, cwd, child_env, output, label, **kwargs): + calls.append((argv, cwd, child_env, label, kwargs)) + body = b"3.0.73" if label == "version" else b"[]" if "--json" in argv else b"inert help" + return {"probe": label, "exit": 0, "error": None}, body, b"" + summary = {"probes": [], "fixture_preparation": [], "inventories": {}} + with patch.object(research, "run_probe", side_effect=inert_probe): + research.collect(Path("/never-executed-cline"), root, env, root / "output", summary, research.grok.Budget()) + self.assertEqual([row["probe"] for row in summary["probes"]], research.EXPECTED_PROBES) + self.assertEqual(len(summary["probes"]), 12) + self.assertEqual(len(summary["fixture_preparation"]), 2) + self.assertTrue(all(row["accepted"] for row in summary["probes"])) + expected_overrides = {name: override for name, _, override in research.MCP_PROBES} + for argv, cwd, child_env, label, kwargs in calls: + self.assertIn(cwd, (root / "project-a", root / "project-b")) + self.assertLessEqual(kwargs["timeout"], 30) + if label.startswith("git-init"): + self.assertEqual(argv, ["/usr/bin/git", "init", "--quiet", "--template="]) + continue + self.assertEqual(argv[:5], ["/never-executed-cline", "--auto-approve", "false", "--cwd", str(cwd)]) + self.assertEqual(child_env["CLINE_NO_AUTO_UPDATE"], "1") + override = expected_overrides.get(label) + if override is None: + self.assertNotIn("CLINE_MCP_SETTINGS_PATH", child_env) + else: + self.assertEqual(child_env["CLINE_MCP_SETTINGS_PATH"], str(root / override / ".uap/clients/cline/mcp.json")) + self.assertEqual(summary["inventories"]["before"], summary["inventories"]["after"]) + + def test_each_read_only_probe_must_preserve_the_fixture_baseline(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for name in ("home", "tmp", "project-a", "project-b", "output"): + (root / name).mkdir() + env = research.runtime_environment(root / "home", root / "tmp") + def changing_probe(_argv, _cwd, _env, _output, label, **_kwargs): + if label == "version": + (root / "project-a/.uap/clients/cline/mcp.json").write_text("changed by inert probe") + return {"probe": label, "exit": 0, "error": None}, b"3.0.73", b"" + with patch.object(research, "run_probe", side_effect=changing_probe), self.assertRaisesRegex(ValueError, "baseline"): + research.collect(Path("/never-executed"), root, env, root / "output", + {"probes": [], "fixture_preparation": [], "inventories": {}}, research.grok.Budget()) + + def test_failed_or_ambiguous_output_is_not_collection_evidence(self): + good = {"error": None, "exit": 0} + self.assertTrue(research.validate_probe(good, b"Cline 3.0.73", b"", "version")) + for body in (b"3.0.730", b"13.0.73", b"3.0.73-alpha", b"missing"): + self.assertFalse(research.validate_probe(good, body, b"", "version")) + self.assertTrue(research.validate_probe(good, b'[{"name":"inert"}]', b"", "json")) + for body in (b"", b"{}", b"null", b"[1]", b"[] trailing", b'[{"x":1,"x":2}]', b'[{"x":NaN}]', b"\xff"): + self.assertFalse(research.validate_probe(good, body, b"", "json")) + self.assertFalse(research.validate_probe(good, b"[]", b"native error", "json")) + self.assertFalse(research.validate_probe({"error": "deadline", "exit": 0}, b"[]", b"", "json")) + self.assertFalse(research.validate_probe({"error": None, "exit": 1}, b"[]", b"", "json")) + + def test_real_unmodified_hosted_guard_has_zero_requested_output_effects(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "not-created" / "research" + result = subprocess.run([sys.executable, str(Path(research.__file__).resolve()), "--output", str(output)], + env={"PATH": "/usr/bin:/bin"}, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10, check=False) + self.assertEqual(result.returncode, 2) + self.assertIn(b"disposable GitHub-hosted", result.stderr) + self.assertEqual(result.stdout, b"") + self.assertFalse(output.parent.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_cline_project_envelope.py b/scripts/test_cline_project_envelope.py new file mode 100644 index 000000000..fd11d5e02 --- /dev/null +++ b/scripts/test_cline_project_envelope.py @@ -0,0 +1,150 @@ +"""Inert closed Cline evidence checks, not native qualification.""" +import copy +import hashlib +import json +from pathlib import Path +import sys +import unittest +sys.path.insert(0,str(Path(__file__).parent)) +import cline_project_envelope as contract + +def summary(flags): + value={"schema_version":1,"client":"cline","version":"3.0.73","collection_complete":False,"commands":[], + "source_commit":"a"*40,"source_tree":"b"*40} + value.update({key:False for key in flags});return value + +def command(label,kind,bodies,in_process=False): + record={"label":label,"class":kind,"in_process":in_process,"exit":0,"error":"","overflow":False} + for stream in ("stdout","stderr"): + name=label+"."+stream+".log";body=b"inert" if stream=="stdout" else b"";bodies[name]=body + record.update({stream+"_file":name,stream+"_bytes":len(body),stream+"_sha256":hashlib.sha256(body).hexdigest()}) + return record + +def encoded(front,native,bodies): + result=dict(bodies);result[contract.FRONTEND]=json.dumps(front).encode() + if native is not None:result[contract.NATIVE]=json.dumps(native).encode() + return result + +class ClosedClineEnvelopeTests(unittest.TestCase): + def fixture(self,complete=False): + bodies={};front=summary(contract.FRONT_FALSE);native=summary(contract.NATIVE_FALSE) + front["commands"]=[command(label,"frontend",bodies) for label in sorted(contract.FRONT_LABELS)] + native["commands"]=[command(label,kind,bodies,kind=="public_uap") for label,kind in contract.NATIVE_LABELS.items()] + native_names=set(bodies)-{row[stream+"_file"] for row in front["commands"] for stream in ("stdout","stderr")} + for name in contract.EXTRAS:bodies[name]=b'{}' + native["artifact_sha256"]={name:hashlib.sha256(bodies[name]).hexdigest() for name in native_names|set(contract.EXTRAS)} + if complete: + for value in (front,native):value.update(collection_complete=True,cleanup_complete=True,read_only_unique_marker_discovery_qualified=True) + native["same_name_observation_complete"]=True + native["stages"]={name:{"status":"blocked" if name=="same_name_precedence" else "passed"} for name in contract.STAGES} + return front,native,bodies + + def test_complete_and_partial_fixed_bodies_are_admitted_without_rewriting(self): + for complete in (False,True): + front,native,bodies=self.fixture(complete);selected=encoded(front,native,bodies);before=copy.deepcopy(selected) + contract.validate(selected);self.assertEqual(selected,before) + contract.validate(encoded(summary(contract.FRONT_FALSE),None,{})) + + def test_unknown_profiles_labels_classes_and_process_claims_refuse(self): + for kind in ("unknown-member","unknown-label","duplicate-label","wrong-class","wrong-boundary"): + front,native,bodies=self.fixture() + if kind=="unknown-member":bodies["native-cache.json"]=b'{}' + elif kind=="unknown-label":native["commands"][0]["label"]="private-profile" + elif kind=="duplicate-label":native["commands"].append(native["commands"][0]) + elif kind=="wrong-class":native["commands"][0]["class"]="native" + else:native["commands"][0]["in_process"]=True + with self.subTest(kind=kind),self.assertRaises(ValueError):contract.validate(encoded(front,native,bodies)) + + def test_capture_hash_size_and_extra_authentication_refuse_drift(self): + for kind in ("stream","extra","hash-membership","oversized","stream-name","stream-size-bool"): + front,native,bodies=self.fixture();first=native["commands"][0];name=first["stdout_file"] + if kind=="stream":bodies[name]+=b"changed" + elif kind=="extra":bodies["prepared-A-config.json"]=b'{"changed":true}' + elif kind=="hash-membership":native["artifact_sha256"]["foreign.json"]="0"*64 + elif kind=="oversized":bodies[name]=b"x"*((128<<10)+1) + elif kind=="stream-name":first["stdout_file"]="profile.log" + else:first["stdout_bytes"]=True + with self.subTest(kind=kind),self.assertRaises(ValueError):contract.validate(encoded(front,native,bodies)) + + def test_source_unobserved_flags_and_ambiguous_json_refuse(self): + for kind in ("source","runtime","active","schema-bool","duplicate-json","nonfinite-json","missing-native"): + front,native,bodies=self.fixture(True) + if kind=="source":native["source_tree"]="c"*40 + elif kind=="runtime":native["native_runtime_qualified"]=True + elif kind=="active":native["same_name_active_isolation_qualified"]=True + elif kind=="schema-bool":front["schema_version"]=True + selected=encoded(front,None if kind=="missing-native" else native,bodies) + if kind=="duplicate-json":selected[contract.FRONTEND]=b'{"client":"cline","client":"cline"}' + elif kind=="nonfinite-json":selected["prepared-A-config.json"]=b'{"number":NaN}' + with self.subTest(kind=kind),self.assertRaises(ValueError):contract.validate(selected) + + def test_complete_requires_every_command_stage_source_file_and_cleanup(self): + for kind in ("command","stage","extra","cleanup","empty","failed","true-exit"): + front,native,bodies=self.fixture(True) + if kind=="command":native["commands"].pop() + elif kind=="stage":native["stages"]["same_name_precedence"]["status"]="passed" + elif kind=="extra":bodies.pop("prepared-B-config.json") + elif kind=="cleanup":front["cleanup_complete"]=False + elif kind=="empty":native["read_only_unique_marker_discovery_qualified"]=False + elif kind=="failed":front["commands"][0]["error"]="deadline" + else:front["commands"][0]["exit"]=False + with self.subTest(kind=kind),self.assertRaises((ValueError,KeyError)):contract.validate(encoded(front,native,bodies)) + +if __name__=="__main__":unittest.main() + +class ActualClineArchiveTests(unittest.TestCase): + """Actual filesystem/ZIP admission, with synthetic FALSE native claims.""" + def fixture(self,root): + import native_client_project_artifacts as artifacts + front,native,bodies=ClosedClineEnvelopeTests().fixture(False) + source=root/'raw';source.mkdir() + for name,body in encoded(front,native,bodies).items():(source/name).write_bytes(body) + upload=root/'upload';upload.mkdir() + return artifacts,source,upload/'proof.zip' + + def test_real_cline_archive_contains_only_original_flat_bytes(self): + import tempfile,zipfile + with tempfile.TemporaryDirectory() as temporary: + artifacts,source,destination=self.fixture(Path(temporary).resolve()) + self.assertTrue(artifacts.write_archive(source,destination,client='cline')) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(set(archive.namelist()),{p.name for p in source.iterdir()}) + for member in archive.infolist(): + self.assertEqual(member.compress_type,zipfile.ZIP_STORED) + self.assertEqual(archive.read(member),(source/member.filename).read_bytes()) + self.assertLess(destination.stat().st_size,32<<20) + + def test_unknown_member_yields_only_false_cline_failure_not_opencode(self): + import tempfile,zipfile + with tempfile.TemporaryDirectory() as temporary: + artifacts,source,destination=self.fixture(Path(temporary).resolve()) + (source/'private-profile.json').write_bytes(b'{}') + self.assertFalse(artifacts.write_archive(source,destination,client='cline')) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.namelist(),[contract.NATIVE]) + failure=json.loads(archive.read(contract.NATIVE)) + self.assertFalse(failure['collection_complete']) + for key in contract.NATIVE_FALSE:self.assertIs(failure[key],False) + + def test_unknown_client_and_alias_upload_parent_fail_before_publication(self): + import os,tempfile + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve();artifacts,source,destination=self.fixture(root) + with self.assertRaises(ValueError):artifacts.write_archive(source,destination,client='arbitrary') + foreign=root/'foreign';foreign.mkdir();alias=root/'alias';alias.symlink_to(foreign,target_is_directory=True) + with self.assertRaises(ValueError):artifacts.write_archive(source,alias/'proof.zip',client='cline') + self.assertFalse((foreign/'proof.zip').exists()) + + def test_parent_replacement_after_zip_assembly_refuses_final_publication(self): + import os,tempfile + from unittest.mock import patch + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve();artifacts,source,destination=self.fixture(root) + original=artifacts.write_zip + def replace(pending,bodies): + original(pending,bodies) + os.rename(destination.parent,root/'old-upload') + destination.parent.mkdir() + with patch.object(artifacts,'write_zip',side_effect=replace): + with self.assertRaises(ValueError):artifacts.write_archive(source,destination,client='cline') + self.assertFalse(destination.exists()) diff --git a/scripts/test_cline_project_research.py b/scripts/test_cline_project_research.py new file mode 100644 index 000000000..fe06b7bba --- /dev/null +++ b/scripts/test_cline_project_research.py @@ -0,0 +1,294 @@ +"""Inert Cline frontend recovery tests; no vendor or hosted happy-path.""" +import importlib.util +import os +import select +from pathlib import Path +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + +SPEC=importlib.util.spec_from_file_location("cline_project_gate",Path(__file__).with_name("research-cline-project.py")) +gate=importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(gate) + +class ClineProjectFrontendTests(unittest.TestCase): + def test_real_guard_refuses_before_any_output(self): + with tempfile.TemporaryDirectory() as temporary: + output=Path(temporary)/"no-output" + with patch.dict(os.environ,{},clear=True),patch.object(sys,"argv",[str(SPEC.origin),"--output",str(output)]),patch.object(gate.cline,"acquire_binary",side_effect=AssertionError("vendor acquisition")): + self.assertEqual(gate.main(),2) + self.assertFalse(output.exists()) + + def test_environment_has_only_explicit_disposable_authority(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve() + with patch.dict(os.environ,{"GITHUB_TOKEN":"inert","CLINE_MCP_SETTINGS_PATH":"/foreign","PYTHONPATH":"/foreign"}): + env=gate.clean_environment(root,Path("/known/go/bin/go")) + self.assertEqual(env["CLINE_NO_AUTO_UPDATE"],"1") + self.assertEqual(env["HOME"],str(root/"frontend-home")) + for key in ("GITHUB_TOKEN","CLINE_MCP_SETTINGS_PATH","PYTHONPATH","CLINE_DIR","CLINE_DATA_DIR"):self.assertNotIn(key,env) + self.assertEqual(env["GOTOOLCHAIN"],"local") + + def test_closed_capture_table_and_duplicate_refuse_before_child(self): + summary={"commands":[]} + budget=type("Budget",(),{"check":lambda _self:5})() + result={"exit":0,"error":None} + with patch.object(gate,"run_probe",return_value=(result,b"bounded",b"")) as probe: + self.assertEqual(gate.capture("frontend-commit",["/usr/bin/git","rev-parse","HEAD"],"/",{},Path("/inert"),summary,budget,10),(b"bounded",b"")) + self.assertEqual(probe.call_args.kwargs["limit"],128<<10) + self.assertEqual(probe.call_args.kwargs["timeout"],5) + for label in ("frontend-commit","unknown"): + with self.assertRaises(ValueError):gate.capture(label,["unreviewed"],"/",{},Path("/inert"),summary,budget,10) + self.assertEqual(probe.call_count,1) + + def test_executable_hash_never_follows_alias_or_multilink(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve();ordinary=root/"inert-binary";ordinary.write_bytes(b"inert bytes") + self.assertEqual(gate.pinned_file(ordinary,64)["bytes"],11) + alias=root/"alias";alias.symlink_to(ordinary) + with self.assertRaises(ValueError):gate.pinned_file(alias,64) + alias.unlink();os.link(ordinary,alias) + with self.assertRaises(ValueError):gate.pinned_file(ordinary,64) + alias.unlink() + with self.assertRaises(ValueError):gate.pinned_file(ordinary,1) + + def test_cleanup_deletes_only_exact_owned_root_and_never_alias_target(self): + with tempfile.TemporaryDirectory() as temporary: + parent=Path(temporary).resolve();foreign=parent/"foreign";foreign.mkdir();(foreign/"keep").write_bytes(b"caller owned") + root=parent/"scratch";root.mkdir();expected=root.lstat();(root/"alias").symlink_to(foreign,target_is_directory=True) + sealed=root/"readonly";sealed.mkdir();(sealed/"file").write_bytes(b"inert");sealed.chmod(0o500) + gate.bounded_cleanup(root,expected) + self.assertFalse(root.exists());self.assertEqual((foreign/"keep").read_bytes(),b"caller owned") + root.mkdir() + with self.assertRaises(ValueError):gate.bounded_cleanup(root,foreign.lstat()) + self.assertTrue(root.exists()) + + def test_cleanup_deadline_refuses_without_broad_fallback(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve()/"scratch";root.mkdir();(root/"keep").write_bytes(b"inert");expected=root.lstat() + with patch.object(gate.time,"monotonic",side_effect=[0,6]): + with self.assertRaises(TimeoutError):gate.bounded_cleanup(root,expected) + self.assertEqual((root/"keep").read_bytes(),b"inert") + + @unittest.skipUnless(hasattr(os,"fork"),"forked preloaded publisher is Linux-only") + def test_publication_calls_loaded_function_and_denies_late_import(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve();destination=root/"proof" + def already_loaded(_raw,selected,*,client): + if client!="cline":raise AssertionError("unreviewed client") + fd=os.open(selected,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) + try:os.write(fd,b"loaded closure") + finally:os.close(fd) + return True + with patch.object(gate.artifacts,"write_archive",already_loaded):self.assertTrue(gate.publish(root,destination)["complete"]) + self.assertEqual(destination.read_bytes(),b"loaded closure") + def late_import(_raw,_selected,*,client): + if client!="cline":raise AssertionError("unreviewed client") + __import__("cline_gate_deliberately_absent_module") + return True + with patch.object(gate.artifacts,"write_archive",late_import):self.assertEqual(gate.publish(root,root/"refused")["exit"],3) + self.assertFalse((root/"refused").exists()) + + @unittest.skipUnless(hasattr(os,"fork"),"forked admission is Linux-only") + def test_stuck_publisher_is_killed_and_reaped_within_reserved_window(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve() + def stuck(_raw,_selected,*,client): + if client!="cline":raise AssertionError("unreviewed client") + time.sleep(30);return True + started=time.monotonic() + with patch.object(gate.artifacts,"write_archive",stuck):result=gate.publish(root,root/"absent",work_seconds=0.01,reap_seconds=1) + self.assertFalse(result["complete"]);self.assertTrue(result["child_reaped"]) + self.assertEqual(result["error"],"artifact admission deadline") + self.assertLess(time.monotonic()-started,1.5);self.assertFalse((root/"absent").exists()) + + def test_unreapable_child_reports_uncertainty_without_blocking_wait(self): + with patch.object(gate.os,"fork",return_value=12345),patch.object(gate.os,"waitpid",return_value=(0,0)) as waited,patch.object(gate.os,"kill") as killed,patch.object(gate.time,"monotonic",side_effect=[0,0.1,4.1,4.1,4.2,5.2]): + result=gate.publish(Path("/inert"),Path("/inert.zip")) + self.assertFalse(result["complete"]);self.assertFalse(result["child_reaped"]) + self.assertEqual(result["error"],"artifact admission cleanup unproved") + self.assertEqual(waited.call_count,2) + for call in waited.call_args_list:self.assertEqual(call.args,(12345,os.WNOHANG)) + killed.assert_called_once_with(12345,gate.signal.SIGKILL) + +class PublicationWithholdingTests(unittest.TestCase): + def paths(self, temporary): + root = Path(temporary).resolve() + raw, upload = root / "raw", root / "upload" + raw.mkdir() + upload.mkdir() + return root, raw, upload, upload.lstat() + + def withheld(self, root, upload): + self.assertFalse(upload.exists()) + quarantines = list(root.glob("upload-unproved-*/withheld")) + self.assertEqual(len(quarantines), 1) + return quarantines[0] + + @unittest.skipUnless(hasattr(os, "fork"), "Linux-only preloaded publisher") + def test_exception_after_zip_withholds_reaped_positive_archive(self): + with tempfile.TemporaryDirectory() as temporary: + root, raw, upload, expected = self.paths(temporary) + def write_then_raise(_raw, selected, *, client): + selected.write_bytes(b"positive bytes before unexpected exception") + raise RuntimeError("inert post-publication failure") + with patch.object(gate.artifacts, "write_archive", write_then_raise): + result = gate.admit_publication(raw, upload, expected) + self.assertFalse(result["complete"]) + self.assertTrue(result["child_reaped"]) + self.assertEqual(result["exit"], 3) + self.assertFalse(result["archive_failure_allowed"]) + self.assertEqual((self.withheld(root, upload) / "research.zip").read_bytes(), + b"positive bytes before unexpected exception") + + @unittest.skipUnless(hasattr(os, "fork"), "Linux-only preloaded publisher") + def test_reaped_deadline_withholds_already_written_archive(self): + with tempfile.TemporaryDirectory() as temporary: + root, raw, upload, expected = self.paths(temporary) + ready_read, ready_write = os.pipe() + original_fork = os.fork + def fork_after_written_barrier(): + pid = original_fork() + if pid > 0: + if not select.select([ready_read], [], [], 2)[0]: + os.kill(pid, 9) + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if os.waitpid(pid, os.WNOHANG)[0]: + break + time.sleep(0.01) + else: + raise AssertionError("inert barrier cleanup unproved") + raise AssertionError("inert writer did not reach archive barrier") + os.read(ready_read, 1) + return pid + def write_then_wait(_raw, selected, *, client): + selected.write_bytes(b"positive bytes before deadline") + os.write(ready_write, b"x") + time.sleep(30) + return True + try: + with patch.object(gate.os, "fork", fork_after_written_barrier), patch.object(gate.artifacts, "write_archive", write_then_wait): + result = gate.admit_publication(raw, upload, expected, work_seconds=0.02, reap_seconds=1) + finally: + os.close(ready_read) + os.close(ready_write) + self.assertFalse(result["complete"]) + self.assertTrue(result["child_reaped"]) + self.assertEqual(result["error"], "artifact admission deadline") + self.assertFalse(result["archive_failure_allowed"]) + self.assertEqual((self.withheld(root, upload) / "research.zip").read_bytes(), b"positive bytes before deadline") + + @unittest.skipUnless(hasattr(os, "fork"), "Linux-only preloaded publisher") + def test_closed_normal_failure_retains_only_its_failure_path(self): + with tempfile.TemporaryDirectory() as temporary: + _root, raw, upload, expected = self.paths(temporary) + def normal_failure(_raw, selected, *, client): + selected.write_bytes(b"closed synthetic false failure envelope") + return False + with patch.object(gate.artifacts, "write_archive", normal_failure): + result = gate.admit_publication(raw, upload, expected) + self.assertFalse(result["complete"]) + self.assertTrue(result["child_reaped"]) + self.assertEqual(result["exit"], 2) + self.assertTrue(result["archive_failure_allowed"]) + self.assertEqual((upload / "research.zip").read_bytes(), b"closed synthetic false failure envelope") + + def test_deadline_exit_two_never_inherits_normal_failure_admission(self): + # An exit2 observed only after the work deadline remains unqualified. + with patch.object(gate.os, "fork", return_value=12345), patch.object(gate.os, "waitpid", side_effect=[(0, 0), (12345, 512)]), patch.object(gate.os, "kill"), patch.object(gate.time, "monotonic", side_effect=[0, 0.1, 4.1, 4.1, 4.2]): + result = gate.publish(Path("/inert"), Path("/inert.zip")) + self.assertFalse(result["complete"]) + self.assertTrue(result["child_reaped"]) + self.assertEqual(result["exit"], 2) + self.assertFalse(result["archive_failure_allowed"]) + + @unittest.skipUnless(hasattr(os, "fork"), "Linux-only preloaded publisher") + def test_unreaped_late_writer_cannot_recreate_withheld_upload_parent(self): + with tempfile.TemporaryDirectory() as temporary: + root, raw, upload, expected = self.paths(temporary) + release_read, release_write = os.pipe() + child_pids = [] + original_fork, original_kill = os.fork, os.kill + def fork(): + pid = original_fork() + if pid > 0: + child_pids.append(pid) + return pid + def late_write(_raw, selected, *, client): + os.read(release_read, 1) + try: + selected.write_bytes(b"late positive bytes") + except FileNotFoundError: + (root / "late-refused").write_bytes(b"parent remains withheld") + return True + joined = False + try: + with patch.object(gate.os, "fork", fork), patch.object(gate.os, "kill"), patch.object(gate.artifacts, "write_archive", late_write): + result = gate.admit_publication(raw, upload, expected, work_seconds=0.02, reap_seconds=0.02) + self.assertFalse(result["child_reaped"]) + self.assertFalse(result["archive_failure_allowed"]) + self.withheld(root, upload) + os.write(release_write, b"x") + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if os.waitpid(child_pids[0], os.WNOHANG)[0]: + joined = True + break + time.sleep(0.01) + self.assertTrue(joined, "inert late publisher did not join") + self.assertEqual((root / "late-refused").read_bytes(), b"parent remains withheld") + self.assertFalse(upload.exists()) + finally: + os.close(release_read) + os.close(release_write) + if child_pids and not joined: + original_kill(child_pids[0], 9) + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if os.waitpid(child_pids[0], os.WNOHANG)[0]: + joined = True + break + time.sleep(0.01) + if not joined: + self.fail("inert publisher cleanup remains unproved") + + def test_quarantine_refuses_foreign_or_alias_authority(self): + with tempfile.TemporaryDirectory() as temporary: + root, _raw, upload, expected = self.paths(temporary) + foreign = root / "foreign" + foreign.mkdir() + with self.assertRaises(ValueError): + gate.quarantine_upload(upload, foreign.lstat()) + self.assertTrue(upload.is_dir()) + alias = root / "alias" + alias.symlink_to(upload, target_is_directory=True) + with self.assertRaises(ValueError): + gate.quarantine_upload(alias, expected) + self.assertTrue(upload.is_dir()) + + +if __name__=="__main__":unittest.main() + +class LoadedRealClinePublisherTests(unittest.TestCase): + @unittest.skipUnless(hasattr(os,'fork'),'Linux-only preloaded publisher') + def test_actual_loaded_cline_archive_has_no_post_native_import_or_reexec(self): + import json,zipfile + # This deliberately records only a failed/partial synthetic stage. It + # proves the loaded file-admission closure, not native observations. + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary).resolve();source=root/'raw';source.mkdir();upload=root/'upload';upload.mkdir() + summary={'schema_version':1,'client':'cline','version':'3.0.73','collection_complete':False,'commands':[]} + summary.update({key:False for key in gate.cline_project_envelope.NATIVE_FALSE}) + (source/gate.SUMMARY).write_text(json.dumps(summary)) + for name in ('utf-8','cp437'):gate.codecs.lookup(name) + # Publish uses the module objects loaded above; an exec/import of + # any mutable original source is neither requested nor permitted. + result=gate.publish(source,upload/'proof.zip') + self.assertTrue(result['complete'],result) + self.assertTrue(result['child_reaped']) + with zipfile.ZipFile(upload/'proof.zip') as archive: + self.assertEqual(archive.namelist(),[gate.SUMMARY]) + self.assertFalse(json.loads(archive.read(gate.SUMMARY))['collection_complete']) diff --git a/scripts/test_grok_native_research.py b/scripts/test_grok_native_research.py new file mode 100644 index 000000000..90cc97dea --- /dev/null +++ b/scripts/test_grok_native_research.py @@ -0,0 +1,167 @@ +"""Inert contracts only: no Grok acquisition/execution or forged hosted guards.""" +import importlib.util +import io +import json +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + +SPEC = importlib.util.spec_from_file_location("grok_research", Path(__file__).with_name("research-grok-native.py")) +research = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(research) + + +class GrokResearchTests(unittest.TestCase): + def test_total_deadline_interrupts_blocked_work_and_restores_signal_state(self): + old_handler = signal.getsignal(signal.SIGALRM) + with self.assertRaises(research.CollectorDeadline): + with research.Budget(.03): + time.sleep(2) + self.assertEqual(signal.getsignal(signal.SIGALRM), old_handler) + self.assertEqual(signal.getitimer(signal.ITIMER_REAL)[0], 0) + class Expired: + def check(self): + raise research.CollectorDeadline("inert expired budget") + source = io.BytesIO(b"must not be read") + with self.assertRaises(research.CollectorDeadline): + research.BudgetReader(source, Expired()).read(1) + self.assertEqual(source.tell(), 0) + + def test_inventory_records_alias_and_special_metadata_without_following(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inside, outside = root / "inside", root / "outside" + inside.mkdir(); outside.mkdir() + (outside / "secret").write_text("outside must not be read") + (inside / "file").write_text("inert bytes") + (inside / "alias").symlink_to(outside, target_is_directory=True) + os.mkfifo(inside / "fifo") + measured = research.inventory({"project-a": inside}, research.Budget()) + self.assertEqual(measured["project-a/alias"]["kind"], "symlink") + self.assertEqual(measured["project-a/alias"]["target"], str(outside)) + self.assertEqual(measured["project-a/fifo"]["kind"], "special") + self.assertNotIn("project-a/alias/secret", measured) + self.assertEqual(measured["project-a/file"]["kind"], "file") + os.link(inside / "file", inside / "hard-alias") + with self.assertRaisesRegex(ValueError, "alias/size"): + research.inventory({"project-a": inside}, research.Budget()) + + def test_inventory_limits_and_baseline_changes_fail_without_partial_proof(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "one").write_bytes(b"12") + (root / "two").write_bytes(b"34") + before = research.inventory({"home": root}, research.Budget()) + for key, value in [("MAX_FILE", 1), ("MAX_TOTAL", 3), ("MAX_ENTRIES", 1), ("MAX_METADATA", 2)]: + with self.subTest(limit=key), patch.object(research, key, value), self.assertRaises(ValueError): + research.inventory({"home": root}, research.Budget()) + (root / "nested").mkdir() + with patch.object(research, "MAX_DEPTH", 0), self.assertRaises(ValueError): + research.inventory({"home": root}, research.Budget()) + (root / "one").write_bytes(b"changed") + after = research.inventory({"home": root}, research.Budget()) + with self.assertRaisesRegex(ValueError, "baseline"): + research.require_baseline(after, before) + + def test_fixture_contains_only_unexecutable_manifest_and_skill_text(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + files = research.fixture(root, "uap-inert-marker", "1.0.0") + self.assertEqual(len(files), 2) + for path in files: + self.assertEqual(path.stat().st_mode & 0o777, 0o644) + manifest = json.loads(next(path for path in files if path.name == "plugin.json").read_text()) + self.assertEqual(set(manifest), {"name", "version", "description", "skills"}) + with self.assertRaises(FileExistsError): + research.fixture(root, "another-marker", "2.0.0") + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + outside, inside = root / "outside", root / "inside" + outside.mkdir(); inside.mkdir() + (inside / ".grok").symlink_to(outside, target_is_directory=True) + with self.assertRaises(ValueError): + research.fixture(inside, "marker", "1.0.0") + self.assertEqual(list(outside.iterdir()), []) + + def test_acquisition_rejects_limits_and_wrong_pin_before_gzip_or_output(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for body, cap in [(b"wrong official bytes", 100), (b"12345", 4)]: + with patch.object(research.urllib.request, "urlopen", return_value=io.BytesIO(body)), \ + patch.object(research, "MAX_ARCHIVE", cap), patch.object(research.gzip, "GzipFile") as parse: + with self.assertRaises(ValueError): + research.acquire_binary(root, research.Budget()) + parse.assert_not_called() + self.assertEqual(list(root.iterdir()), []) + + def test_validation_does_not_upgrade_failed_or_ambiguous_json(self): + good = {"error": None, "exit": 0} + self.assertTrue(research.validate_probe(good, b"grok 1.0.67 (inert fixture)", b"", "version")) + self.assertFalse(research.validate_probe(good, b"grok 1.0.670", b"", "version")) + self.assertFalse(research.validate_probe(good, b"grok 1.0.67-alpha", b"", "version")) + self.assertTrue(research.validate_probe(good, b"[]", b"", "json")) + for body in [b"", b"null", b"true", b"[] trailing", b'{"x":1,"x":2}', b'{"x":NaN}', b"\xff"]: + self.assertFalse(research.validate_probe(good, body, b"", "json")) + self.assertFalse(research.validate_probe(good, b"[]", b"error", "json")) + self.assertFalse(research.validate_probe({"error": "deadline", "exit": 0}, b"[]", b"", "json")) + self.assertFalse(research.validate_probe({"error": None, "exit": 1}, b"[]", b"", "json")) + + def test_all_stages_use_fixed_read_only_commands_and_selected_fresh_cwd(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for name in ("home", "tmp", "project-a", "project-b", "output"): + (root / name).mkdir() + with patch.dict(os.environ, {"GROK_API_KEY": "inert secret", "GH_TOKEN": "inert secret", "HTTP_PROXY": "foreign"}): + env = research.runtime_environment(root / "home", root / "tmp") + self.assertNotIn("GROK_API_KEY", env) + self.assertNotIn("GH_TOKEN", env) + self.assertNotIn("HTTP_PROXY", env) + calls = [] + def inert_probe(argv, cwd, child_env, output, label, **kwargs): + calls.append((argv, cwd, child_env, label)) + record = {"probe": label, "exit": 0, "error": None} + # Deliberately inert test observations, never native proof. + body = b"grok 1.0.67" if label == "version" else b'{"inert fixture":true}' + return record, body, b"" + summary = {"probes": [], "fixture_preparation": [], "inventories": []} + with patch.object(research, "run_probe", side_effect=inert_probe): + research.collect(Path("/never-executed-grok"), root, env, root / "output", summary, research.Budget()) + self.assertEqual([entry["probe"] for entry in summary["probes"]], research.EXPECTED_PROBES) + self.assertEqual(len(summary["probes"]), 19) + self.assertEqual(len(summary["fixture_preparation"]), 2) + self.assertEqual([entry["stage"] for entry in summary["inventories"]], list(research.STAGES)) + allowed = {tuple(args) for _, args in (*research.HELP_PROBES, *research.JSON_PROBES)} + for argv, cwd, child_env, label in calls: + self.assertEqual(child_env, env) + self.assertIn(cwd, [root / "project-a", root / "project-b"]) + if label.startswith("git-init"): + self.assertEqual(argv, ["/usr/bin/git", "init", "--quiet", "--template="]) + else: + self.assertEqual(argv[0], "/never-executed-grok") + self.assertIn(tuple(argv[1:]), allowed) + for owner, marker, version in [("home", "uap-research-user", "0.9.0"), ("project-a", "uap-research-a", "1.0.0"), + ("project-b", "uap-research-b", "2.0.0")]: + plugin = root / owner / ".grok/plugins" / research.PLUGIN_NAME + self.assertEqual(json.loads((plugin / ".grok-plugin/plugin.json").read_text())["version"], version) + self.assertTrue((plugin / "skills" / marker / "SKILL.md").is_file()) + + def test_real_guard_refuses_without_any_output_or_native_admission(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "not-created" / "research" + result = subprocess.run([sys.executable, str(Path(research.__file__).resolve()), "--output", str(output)], + env={"PATH": "/usr/bin:/bin"}, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10, check=False) + self.assertEqual(result.returncode, 2) + self.assertIn(b"disposable GitHub-hosted", result.stderr) + self.assertEqual(result.stdout, b"") + self.assertFalse(output.parent.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_native_client_matrix.py b/scripts/test_native_client_matrix.py index 04dfb42a6..f60ef5c5c 100644 --- a/scripts/test_native_client_matrix.py +++ b/scripts/test_native_client_matrix.py @@ -142,6 +142,45 @@ def test_runtime_rejects_ambient_and_self_hosted_machine(self): with self.assertRaises(RuntimeError): matrix.require_hosted("darwin-arm64") + def test_source_project_proof_selects_exact_required_codex_tests(self): + pattern, required = matrix.select_native_tests("codex", project_components=True) + declared = set() + for path in (Path(__file__).resolve().parents[1] / "repotests").glob("*_test.go"): + declared.update(re.findall(r"^func (Test\w+)\(t \*testing\.T\)", path.read_text(encoding="utf-8"), re.M)) + self.assertEqual(required, matrix.REQUIRED_TESTS["codex"] | matrix.PROJECT_TESTS["codex"]) + self.assertEqual({name for name in declared if re.search(pattern, name)}, required) + self.assertNotIn("TestAgentpluginsCodexNativeProjectComponentsLifecycle", released.CLIENT_TESTS["codex"]) + self.assertNotIn("TestAgentpluginsCodexNativeTrustedProjectMCP", released.CLIENT_TESTS["codex"]) + + def test_source_project_proof_selects_exact_required_claude_tests(self): + pattern, required = matrix.select_native_tests("claude", project_components=True) + declared = set() + for path in (Path(__file__).resolve().parents[1] / "repotests").glob("*_test.go"): + declared.update(re.findall(r"^func (Test\w+)\(t \*testing\.T\)", path.read_text(encoding="utf-8"), re.M)) + self.assertEqual(required, matrix.REQUIRED_TESTS["claude"] | matrix.PROJECT_TESTS["claude"]) + self.assertEqual({name for name in declared if re.search(pattern, name)}, required) + self.assertNotIn("TestAgentpluginsClaudeNativeProjectPreparedLifecycle", released.CLIENT_TESTS["claude"]) + self.assertNotIn("TestAgentpluginsClaudeNativeTrustedProjectDiscovery", released.CLIENT_TESTS["claude"]) + + def test_source_project_proof_selects_exact_required_opencode_tests(self): + pattern, required = matrix.select_native_tests("opencode", project_components=True) + declared = set() + for path in (Path(__file__).resolve().parents[1] / "repotests").glob("*_test.go"): + declared.update(re.findall(r"^func (Test\w+)\(t \*testing\.T\)", path.read_text(encoding="utf-8"), re.M)) + self.assertEqual(required, matrix.REQUIRED_TESTS["opencode"] | {"TestAgentpluginsOpenCodeNativeProjectComponentsLifecycle"}) + self.assertEqual({name for name in declared if re.search(pattern, name)}, required) + self.assertNotIn("TestAgentpluginsOpenCodeNativeProjectComponentsLifecycle", released.CLIENT_TESTS["opencode"]) + + def test_source_project_proof_rejects_released_and_unsupported_clients(self): + for client in matrix.PROJECT_TESTS: + with self.assertRaises(ValueError): + matrix.select_native_tests(client, project_components=True, released=True) + for client in set(matrix.REQUIRED_TESTS) - set(matrix.PROJECT_TESTS): + with self.assertRaises(ValueError): + matrix.select_native_tests(client, project_components=True) + for client in matrix.REQUIRED_TESTS: + self.assertEqual(matrix.select_native_tests(client), (matrix.PATTERNS[client], matrix.REQUIRED_TESTS[client])) + def test_target_must_be_native(self): env = {"GITHUB_ACTIONS": "true", "RUNNER_ENVIRONMENT": "github-hosted", "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED": "1"} with patch.dict(matrix.os.environ, env, clear=True), patch.object(matrix.platform, "system", return_value="Darwin"), patch.object(matrix.platform, "machine", return_value="arm64"): diff --git a/scripts/test_native_client_project_artifacts.py b/scripts/test_native_client_project_artifacts.py new file mode 100644 index 000000000..8dcb7ee93 --- /dev/null +++ b/scripts/test_native_client_project_artifacts.py @@ -0,0 +1,122 @@ +"""Inert admission tests. No vendor acquisition or native process.""" +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +import zipfile + +SPEC = importlib.util.spec_from_file_location("project_artifacts", Path(__file__).with_name("native_client_project_artifacts.py")) +artifacts = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(artifacts) + + +class ProjectEnvelopeTests(unittest.TestCase): + def fixture(self, root): + source = root / "source" + source.mkdir() + body = b'{"inert":true}' + (source / "one.stdout.log").write_bytes(body) + (source / "one.stderr.log").write_bytes(b"") + command = {"label": "one"} + for stream, value in (("stdout", body), ("stderr", b"")): + command.update({stream + "_file": "one." + stream + ".log", stream + "_bytes": len(value), + stream + "_sha256": hashlib.sha256(value).hexdigest()}) + (source / "opencode-project-evidence.json").write_text(json.dumps({"success": True, "commands": [command]})) + return source + + def test_actual_archive_only_flat_admitted_bytes(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + source = self.fixture(root) + destination = root / "upload.zip" + admitted = artifacts.write_archive(source, destination) + if not admitted: + with zipfile.ZipFile(destination) as archive: + self.fail(archive.read("opencode-project-evidence.json").decode("utf-8")) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(set(archive.namelist()), {p.name for p in source.iterdir()}) + for member in archive.infolist(): + self.assertEqual(member.compress_type, zipfile.ZIP_STORED) + self.assertEqual(archive.read(member), (source / member.filename).read_bytes()) + self.assertLess(destination.stat().st_size, artifacts.LIMIT) + + def test_refused_source_yields_only_false_failure_record(self): + for kind in ("directory", "symlink", "hardlink", "oversize", "added-name", "added-log", "changed-stream"): + with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + source = self.fixture(root) + if kind == "directory": (source / "directory.json").mkdir() + elif kind == "symlink": + try: (source / "alias.log").symlink_to(source / "one.stdout.log") + except OSError: continue + elif kind == "hardlink": os.link(source / "one.stdout.log", source / "linked.log") + elif kind == "oversize": (source / "large.log").write_bytes(b"x" * ((1 << 20) + 1)) + elif kind == "added-name": (source / "not-evidence.bin").write_bytes(b"refused") + elif kind == "added-log": (source / "unrecorded.log").write_bytes(b"refused") + else: (source / "one.stdout.log").write_bytes(b"changed") + destination = root / "upload.zip" + self.assertFalse(artifacts.write_archive(source, destination)) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.namelist(), ["opencode-project-evidence.json"]) + summary = json.loads(archive.read(archive.namelist()[0])) + self.assertFalse(summary["success"]) + self.assertFalse(summary["native_discovery_qualified"]) + + def test_live_count_and_aggregate_refusal(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + source = self.fixture(root) + for i in range(129): (source / (str(i) + ".log")).write_bytes(b"") + with self.assertRaises(ValueError): artifacts.read_envelope(source) + with tempfile.TemporaryDirectory() as temp: + source = self.fixture(Path(temp).resolve()) + with patch.object(artifacts, "LIMIT", 65536): + with self.assertRaises(ValueError): artifacts.read_envelope(source) + + def test_deadline_and_replacement_refuse(self): + with tempfile.TemporaryDirectory() as temp: + source = self.fixture(Path(temp).resolve()) + with patch.object(artifacts.time, "monotonic", side_effect=[0, 5]): + with self.assertRaises(TimeoutError): artifacts.read_envelope(source) + real = artifacts.os.fstat + def replaced(fd): + info = real(fd) + return os.stat_result((info.st_mode, info.st_ino + 1, info.st_dev, info.st_nlink, + info.st_uid, info.st_gid, info.st_size, info.st_atime, info.st_mtime, info.st_ctime)) + with patch.object(artifacts.os, "fstat", side_effect=replaced): + with self.assertRaises(ValueError): artifacts.read_envelope(source) + + +if __name__ == "__main__": + unittest.main() + +class ProjectMatrixAdmissionTests(unittest.TestCase): + def test_child_timeout_cannot_leave_success_archive_or_follow_fallback_link(self): + spec = importlib.util.spec_from_file_location("project_matrix", Path(__file__).with_name("run-native-client-matrix.py")) + matrix = importlib.util.module_from_spec(spec) + spec.loader.exec_module(matrix) + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + source = root / "uap-opencode-project-admitted-test" + source.mkdir() + output = root / "output" + output.mkdir() + foreign = root / "foreign.txt" + foreign.write_bytes(b"foreign bytes") + destination = output / (source.name + ".zip") + try: + destination.symlink_to(foreign) + except OSError: + destination.write_bytes(b"incomplete archive") + with patch.object(matrix.subprocess, "run", side_effect=matrix.subprocess.TimeoutExpired("inert", 5)): + error = matrix.collect_opencode_project_artifact(source, output) + self.assertIn("TimeoutExpired", error) + self.assertEqual(foreign.read_bytes(), b"foreign bytes") + self.assertFalse(destination.is_symlink()) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.namelist(), ["opencode-project-evidence.json"]) + self.assertFalse(json.loads(archive.read(archive.namelist()[0]))["success"]) diff --git a/scripts/test_native_client_project_artifacts_platform.py b/scripts/test_native_client_project_artifacts_platform.py new file mode 100644 index 000000000..73e7c7913 --- /dev/null +++ b/scripts/test_native_client_project_artifacts_platform.py @@ -0,0 +1,173 @@ +"""Platform admission tests; no vendor process or platform success claim.""" +import ctypes +import os +from pathlib import Path +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch +import zipfile +import json + +import test_native_client_project_artifacts as base_tests + +artifacts = base_tests.artifacts + + +def win_info(info, held=False, **changes): + values = {name: getattr(info, name) for name in ( + "st_dev", "st_ino", "st_mode", "st_nlink", "st_size", "st_mtime_ns", "st_ctime_ns")} + values.update(st_birthtime_ns=1234567, st_file_attributes=0, st_reparse_tag=0) + if held: + values["st_ctime_ns"] += 100000 + values.update(changes) + return SimpleNamespace(**values) + + +class PlatformIdentityTests(unittest.TestCase): + fixture = base_tests.ProjectEnvelopeTests.fixture + + def test_separate_path_and_held_ctime_domains_still_require_stability(self): + with tempfile.TemporaryDirectory() as temp: + source = self.fixture(Path(temp).resolve()) + real_lstat, real_fstat, real_open = Path.lstat, os.fstat, artifacts.open_member + path_stat = lambda path: win_info(real_lstat(path)) + held_stat = lambda fd: win_info(real_fstat(fd), held=True) + with patch.object(artifacts, "WINDOWS", True), patch.object(Path, "lstat", path_stat), patch.object(artifacts.os, "fstat", held_stat), patch.object(artifacts, "open_member", real_open): + # open_member sees WINDOWS at call time; use the explicit POSIX + # opener for this in-memory Windows metadata simulation only. + with patch.object(artifacts, "open_member", lambda p: os.open(p, os.O_RDONLY)): + bodies, packet = artifacts.read_packet(source) + self.assertIn("one.stdout.log", bodies) + artifacts.confirm_paths(packet) + first = packet[2][0][0] + def changed_path(path): + value = path_stat(path) + if path == first: + value.st_ctime_ns += 1 + return value + with patch.object(Path, "lstat", changed_path): + with self.assertRaisesRegex(ValueError, "member replaced"): + artifacts.confirm_paths(packet) + counter = {} + def changed_held(fd): + value = held_stat(fd) + counter[fd] = counter.get(fd, 0) + 1 + if counter[fd] % 2 == 0: + value.st_ctime_ns += 1 + return value + with patch.object(artifacts, "WINDOWS", True), patch.object(Path, "lstat", path_stat), patch.object(artifacts.os, "fstat", changed_held), patch.object(artifacts, "open_member", lambda p: os.open(p, os.O_RDONLY)): + with self.assertRaisesRegex(ValueError, "held identity changed"): + artifacts.read_packet(source) + + def test_positive_cross_identity_fields_and_reparse_refusal(self): + with tempfile.TemporaryDirectory() as temp: + info = win_info(Path(temp).stat()) + with patch.object(artifacts, "WINDOWS", True): + held = SimpleNamespace(**vars(info)) + held.st_ctime_ns += 100 + self.assertTrue(artifacts.same_opened(info, held)) + for field in ("st_dev", "st_ino", "st_mode", "st_nlink", "st_size", "st_mtime_ns", "st_birthtime_ns", "st_file_attributes"): + with self.subTest(field=field): + other = SimpleNamespace(**vars(held)) + setattr(other, field, getattr(other, field) + 1) + self.assertFalse(artifacts.same_opened(info, other)) + for changes in ({"st_dev": 0}, {"st_ino": 0}, {"st_birthtime_ns": None}, {"st_file_attributes": 0x400}, {"st_reparse_tag": 1}): + with self.subTest(changes=changes), self.assertRaises(ValueError): + artifacts.windows_identity(SimpleNamespace(**(vars(info) | changes))) + + def test_root_member_and_held_reparse_are_refused(self): + for where in ("root", "member", "held"): + with self.subTest(where=where), tempfile.TemporaryDirectory() as temp: + source = self.fixture(Path(temp).resolve()) + real_lstat, real_fstat = Path.lstat, os.fstat + opened = [] + def path_stat(path): + value = win_info(real_lstat(path)) + if (where == "root" and path == source) or (where == "member" and path.name == "one.stdout.log"): + value.st_file_attributes = 0x400 + return value + def held_stat(fd): + return win_info(real_fstat(fd), held=True, st_reparse_tag=1 if where == "held" else 0) + def open_file(path): + descriptor = os.open(path, os.O_RDONLY) + opened.append(descriptor) + return descriptor + real_close = os.close + closed = [] + def close_file(fd): + closed.append(fd) + real_close(fd) + with patch.object(artifacts, "WINDOWS", True), patch.object(Path, "lstat", path_stat), patch.object(artifacts.os, "fstat", held_stat), patch.object(artifacts, "open_member", open_file), patch.object(artifacts.os, "close", close_file): + with self.assertRaises(ValueError): + artifacts.read_packet(source) + self.assertEqual(opened, closed) + if where == "root": + self.assertEqual(opened, []) + + def test_binary_bytes_and_post_zip_path_recheck(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp).resolve() + source = self.fixture(root) + binary = b"CR\r\nLF\x1a\x00" + # Preserve the exact command receipt when replacing fixture bytes. + (source / "one.stdout.log").write_bytes(binary) + summary_path = source / "opencode-project-evidence.json" + summary = json.loads(summary_path.read_text()) + import hashlib + summary["commands"][0]["stdout_bytes"] = len(binary) + summary["commands"][0]["stdout_sha256"] = hashlib.sha256(binary).hexdigest() + summary_path.write_text(json.dumps(summary)) + destination = root / "admitted.zip" + self.assertTrue(artifacts.write_archive(source, destination)) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.read("one.stdout.log"), binary) + real_write = artifacts.write_zip + def changed_after_zip(pending, bodies): + real_write(pending, bodies) + (source / "one.stdout.log").write_bytes(b"replaced after capture") + with patch.object(artifacts, "write_zip", changed_after_zip): + self.assertFalse(artifacts.write_archive(source, destination)) + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.namelist(), ["opencode-project-evidence.json"]) + failure = json.loads(archive.read(archive.namelist()[0])) + self.assertFalse(failure["success"]) + self.assertIn("version", failure["artifact_python_runtime"]) + self.assertIn("member replaced", failure["artifact_admission_error"]) + + def test_typed_windows_open_binary_transfer_and_exact_close_ownership(self): + create, close = Mock(return_value=123), Mock(return_value=1) + kernel = SimpleNamespace(CreateFileW=create, CloseHandle=close) + transfer = Mock(return_value=17) + msvcrt = SimpleNamespace(open_osfhandle=transfer) + def win_error(code): + return OSError(code, "inert Windows error") + with patch.object(artifacts, "WINDOWS", True), patch.object(ctypes, "WinDLL", return_value=kernel, create=True), patch.object(ctypes, "WinError", side_effect=win_error, create=True), patch.object(ctypes, "get_last_error", return_value=5, create=True), patch.dict(sys.modules, {"msvcrt": msvcrt}), patch.object(os, "O_BINARY", 0x8000, create=True): + self.assertEqual(artifacts.open_member(Path("inert.json")), 17) + self.assertIs(create.restype, ctypes.c_void_p) + self.assertEqual(create.call_args.args, ("inert.json", 0x80000000, 7, None, 3, 0x00200000, None)) + transfer.assert_called_once_with(123, os.O_RDONLY | 0x8000) + close.assert_not_called() + for error in (OSError("transfer failed"), None): + transfer.side_effect, transfer.return_value = error, -1 + close.reset_mock() + with self.assertRaises(OSError): + artifacts.open_member(Path("inert.json")) + close.assert_called_once_with(123) + close.return_value = 0 + with self.assertRaises(OSError) as raised: + artifacts.open_member(Path("inert.json")) + self.assertEqual(raised.exception.errno, 5) + close.return_value = 1 + create.return_value = ctypes.c_void_p(-1).value + close.reset_mock() + transfer.reset_mock() + with self.assertRaises(OSError): + artifacts.open_member(Path("inert.json")) + close.assert_not_called() + transfer.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_native_client_research.py b/scripts/test_native_client_research.py new file mode 100644 index 000000000..329637a83 --- /dev/null +++ b/scripts/test_native_client_research.py @@ -0,0 +1,141 @@ +"""Inert process-capture regressions; no native client or hosted guard spoofing.""" +import importlib.util +import os +from pathlib import Path +import sys +import tempfile +import time +import unittest +from unittest.mock import patch + +research = None +if os.name == "posix": + SPEC = importlib.util.spec_from_file_location("bounded_research", Path(__file__).with_name("native_client_research.py")) + research = importlib.util.module_from_spec(SPEC) + SPEC.loader.exec_module(research) + + + +@unittest.skipUnless(os.name == "posix", "requires POSIX resource limits, selectable pipes, sessions and killpg") +class CaptureTests(unittest.TestCase): + def run_child(self, root, body, label="probe", **kwargs): + return research.run_probe([sys.executable, "-c", body], root, {}, root, label, **kwargs) + + def test_exact_cap_eof_and_each_stream_overflow(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for stream in ("stdout", "stderr"): + for size in (127, 128, 129): + with self.subTest(stream=stream, size=size): + record, stdout, stderr = self.run_child( + root, f"import sys; sys.{stream}.buffer.write(b'x'*{size}); sys.{stream}.flush()", + label=f"{stream}-{size}", limit=128) + body = stdout if stream == "stdout" else stderr + self.assertEqual(body, b"x" * min(size, 128)) + self.assertEqual(stderr if stream == "stdout" else stdout, b"") + self.assertEqual(record["error"], "output limit reached" if size > 128 else None) + if size <= 128: + self.assertEqual(record["exit"], 0) + self.assertLessEqual((root / record[stream + "_file"]).stat().st_size, 128) + + def test_live_caps_do_not_limit_unrelated_native_cache_files(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + record, stdout, stderr = self.run_child( + root, "from pathlib import Path; import sys; " + "Path('native-cache.bin').write_bytes(b'c'*(2*1024*1024)); " + "print(repr(sys.stdin.read()))", limit=64) + self.assertIsNone(record["error"]) + self.assertEqual(record["exit"], 0) + self.assertEqual(stdout, b"''\n") + self.assertEqual(stderr, b"") + self.assertEqual((root / "native-cache.bin").stat().st_size, 2 * 1024 * 1024) + + def test_simultaneous_separate_streams_larger_than_pipe_capacity(self): + with tempfile.TemporaryDirectory() as temporary: + body = ("import sys,threading; " + "t=threading.Thread(target=lambda:sys.stderr.buffer.write(b'e'*262144)); " + "t.start(); sys.stdout.buffer.write(b'o'*262144); t.join()") + record, stdout, stderr = self.run_child(Path(temporary), body, limit=262144) + self.assertIsNone(record["error"]) + self.assertEqual(record["exit"], 0) + self.assertEqual(stdout, b"o" * 262144) + self.assertEqual(stderr, b"e" * 262144) + + def descendant_body(self, root, exit_leader): + ready, survived = root / "descendant-ready", root / "descendant-survived" + child = ("from pathlib import Path; import time; " + f"Path({str(ready)!r}).write_text('ready'); time.sleep(.5); " + f"Path({str(survived)!r}).write_text('escaped')") + parent = ("import subprocess,time; from pathlib import Path; " + f"subprocess.Popen([{sys.executable!r},'-c',{child!r}]); " + f"ready=Path({str(ready)!r}); " + "\nwhile not ready.exists(): time.sleep(.005)\n") + if not exit_leader: + parent += "time.sleep(10)\n" + return parent, ready, survived + + def test_leader_exit_closes_descendant_inherited_pipes(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + body, ready, survived = self.descendant_body(root, True) + started = time.monotonic() + record, _, _ = self.run_child(root, body, timeout=2) + self.assertTrue(ready.exists(), "descendant must start before leader exit") + self.assertIsNone(record["error"]) + self.assertEqual(record["exit"], 0) + self.assertLess(time.monotonic() - started, 1) + time.sleep(.6) + self.assertFalse(survived.exists()) + + def test_capture_error_and_parent_interruption_reap_started_child(self): + for failure in (OSError("inert capture failure"), KeyboardInterrupt("inert interruption")): + with self.subTest(failure=type(failure).__name__), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + body, ready, survived = self.descendant_body(root, False) + processes = [] + + def interrupt(process, *_args): + processes.append(process) + deadline = time.monotonic() + 2 + while not ready.exists() and time.monotonic() < deadline: + time.sleep(.005) + self.assertTrue(ready.exists(), "actual startup required before injected failure") + raise failure + + with patch.object(research, "capture_output", side_effect=interrupt): + if isinstance(failure, OSError): + record, _, _ = self.run_child(root, body) + self.assertEqual(record["error"], "inert capture failure") + else: + with self.assertRaises(KeyboardInterrupt): + self.run_child(root, body) + self.assertIsNotNone(processes[0].poll()) + self.assertTrue(processes[0].stdout.closed) + self.assertTrue(processes[0].stderr.closed) + time.sleep(.6) + self.assertFalse(survived.exists()) + + def test_failed_startup_preserves_bounded_empty_logs(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + record, stdout, stderr = research.run_probe([str(root / "missing-inert-command")], root, {}, root, "start") + self.assertIsNone(record["exit"]) + self.assertTrue(record["error"]) + self.assertLessEqual(len(record["error"]), 512) + self.assertEqual((stdout, stderr), (b"", b"")) + + +if __name__ == "__main__": + unittest.main() + + +class NonPosixImportTests(unittest.TestCase): + def test_nonposix_discovery_does_not_import_posix_capture(self): + source = Path(__file__) + spec = importlib.util.spec_from_file_location("inert_nonposix_capture_tests", source) + module = importlib.util.module_from_spec(spec) + with patch.object(os, "name", "nt"), patch.object(importlib.util, "spec_from_file_location", side_effect=AssertionError("POSIX helper import on non-POSIX")): + spec.loader.exec_module(module) + self.assertIsNone(module.research) + self.assertTrue(module.CaptureTests.__unittest_skip__) diff --git a/scripts/test_qoder_native_research.py b/scripts/test_qoder_native_research.py new file mode 100644 index 000000000..e6c0d5803 --- /dev/null +++ b/scripts/test_qoder_native_research.py @@ -0,0 +1,156 @@ +"""Inert security/contract tests; never acquires or executes a native client.""" +import gzip +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tarfile +import tempfile +import time +import unittest +from unittest.mock import patch + +SPEC = importlib.util.spec_from_file_location("qoder_research", Path(__file__).with_name("research-qoder-native.py")) +research = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(research) + + +def archive_bytes(entries): + output = io.BytesIO() + with tarfile.open(fileobj=output, mode="w:gz") as archive: + for name, kind, body in entries: + member = tarfile.TarInfo(name) + member.type = kind + member.size = len(body) if kind in (tarfile.REGTYPE, tarfile.AREGTYPE) else 0 + member.linkname = "/foreign" if kind == tarfile.SYMTYPE else "" + archive.addfile(member, io.BytesIO(body)) + return output.getvalue() + + +class QoderResearchTests(unittest.TestCase): + def test_exact_regular_binary_and_archive_admission(self): + good = [(".", tarfile.DIRTYPE, b""), ("./qodercli", tarfile.REGTYPE, b"inert bytes")] + self.assertEqual(research.archive_binary(archive_bytes(good)), b"inert bytes") + for entries in [ + [("../qodercli", tarfile.REGTYPE, b"x")], + [("/qodercli", tarfile.REGTYPE, b"x")], + [("nested/../qodercli", tarfile.REGTYPE, b"x")], + [("qodercli", tarfile.SYMTYPE, b"")], + [("qodercli", tarfile.FIFOTYPE, b"")], + [("qodercli", tarfile.REGTYPE, b"")], + good + [("other/qodercli", tarfile.REGTYPE, b"x")], + good + [("install.sh", tarfile.REGTYPE, b"never execute")], + [(".", tarfile.DIRTYPE, b"")], + ]: + with self.subTest(entries=entries), self.assertRaises(ValueError): + research.archive_binary(archive_bytes(entries)) + with patch.object(research, "MAX_MEMBERS", 1), self.assertRaises(ValueError): + research.archive_binary(archive_bytes(good)) + with patch.object(research, "MAX_BINARY", 2), self.assertRaises(ValueError): + research.archive_binary(archive_bytes(good)) + with patch.object(research, "MAX_EXPANDED", 32), self.assertRaises(ValueError): + research.archive_binary(gzip.compress(b"x" * 100)) + + def test_download_limit_and_vendor_pin_precede_parsing_or_materialization(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for body, limit in [(b"not the pinned archive", 1024), (b"x" * 11, 10)]: + with patch.object(research.urllib.request, "urlopen", return_value=io.BytesIO(body)), \ + patch.object(research, "MAX_ARCHIVE", limit), \ + patch.object(research, "archive_binary") as parse: + with self.assertRaises(ValueError): + research.acquire_binary(root) + parse.assert_not_called() + self.assertEqual(list(root.iterdir()), []) + + def test_runtime_has_explicit_fresh_profile_and_no_ambient_authority(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + home, temp = root / "home", root / "tmp" + home.mkdir() + temp.mkdir() + ambient = {"GH_TOKEN": "inert secret", "GITHUB_TOKEN": "inert secret", "HTTP_PROXY": "http://foreign.invalid", + "QODER_API_KEY": "inert secret", "CLAUDE_CONFIG_DIR": "/foreign", "PATH": "/foreign/bin"} + with patch.dict(os.environ, ambient): + env = research.runtime_environment(home, temp) + self.assertEqual(env["HOME"], str(home)) + self.assertEqual(env["USERPROFILE"], str(home)) + self.assertEqual(env["PATH"], "/usr/bin:/bin") + self.assertEqual(env["TMPDIR"], str(temp)) + for key in set(ambient) - {"PATH"}: + self.assertNotIn(key, env) + for key in ("XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", + "NPM_CONFIG_CACHE", "NPM_CONFIG_USERCONFIG", "NPM_CONFIG_GLOBALCONFIG", "GIT_CONFIG_GLOBAL"): + self.assertTrue(Path(env[key]).is_relative_to(home)) + self.assertTrue(Path(env[key]).exists()) + + def test_probe_closes_stdin_and_bounds_live_stdout(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + argv = [sys.executable, "-c", "import sys; print(repr(sys.stdin.read()))"] + record, stdout, stderr = research.run_probe(argv, root, {"PATH": "/usr/bin:/bin"}, root, "stdin") + self.assertEqual(record["exit"], 0) + self.assertIsNone(record["error"]) + self.assertEqual(stdout.strip(), b"''") + self.assertEqual(stderr, b"") + argv = [sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'x'*4096); sys.stdout.flush()"] + record, stdout, _ = research.run_probe(argv, root, {}, root, "limit", limit=128) + self.assertEqual(record["error"], "output limit reached") + self.assertLessEqual(len(stdout), 128) + self.assertFalse(research.validate_probe(record, stdout, b"")) + + def test_deadline_terminates_group_including_an_inert_descendant(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + ready, escaped = root / "child-ready", root / "child-survived" + child = ("from pathlib import Path; import time; Path(" + repr(str(ready)) + + ").write_text('ready'); time.sleep(2); Path(" + repr(str(escaped)) + ").write_text('survived')") + parent = ("import subprocess,time; from pathlib import Path; subprocess.Popen([" + + repr(sys.executable) + ",'-c'," + repr(child) + "]); " + + "time.sleep(10)") + record, _, _ = research.run_probe([sys.executable, "-c", parent], root, {}, root, "timeout", timeout=1) + self.assertEqual(record["error"], "probe deadline exceeded") + self.assertTrue(ready.exists(), "inert descendant must actually start before the deadline") + time.sleep(2.1) + self.assertFalse(escaped.exists(), "descendant outlived killed probe group") + + def test_exact_version_empty_schema_and_complete_command_allowlist(self): + def accepted(label, body, stderr=b"", code=0, error=None): + return research.validate_probe({"probe": label, "exit": code, "error": error}, body, stderr) + self.assertTrue(accepted("version", b"Qoder CLI v1.1.78\n")) + for body in [b"1.1.780", b"11.1.78", b"1.1.78-alpha", b"missing", b"\xff"]: + self.assertFalse(accepted("version", body)) + self.assertTrue(accepted("plugins-empty-list", b"[]\n")) + for body in [b"null", b"{}", b'[{}]', b'[] trailing', b'\xff', b'']: + self.assertFalse(accepted("plugins-empty-list", body)) + self.assertFalse(accepted("plugins-empty-list", b"[]", b"native error")) + self.assertFalse(accepted("plugins-empty-list", b"[]", code=1)) + self.assertFalse(accepted("plugins-empty-list", b"[]", error="output limit reached")) + self.assertFalse(accepted("version", b"1.1.78\n", b"native warning")) + for label, _ in research.PROBES: + self.assertFalse(accepted(label, b"Usage: inert help", b"native warning")) + self.assertFalse(accepted(label, b"[]" if label == "plugins-empty-list" else b"1.1.78\n", b" \n\t")) + self.assertEqual([args for _, args in research.PROBES], [ + ("--version",), ("plugins", "--help"), ("plugins", "list", "--help"), + ("plugins", "install", "--help"), ("plugins", "uninstall", "--help"), + ("plugins", "list", "--json")]) + + def test_real_hosted_guard_refuses_before_any_requested_output(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "not-created" / "research" + # Fresh ordinary child environment has no hosted opt-in. The real + # imported guard is neither patched nor supplied forged flags. + result = subprocess.run([sys.executable, str(Path(research.__file__).resolve()), "--output", str(output)], + env={"PATH": "/usr/bin:/bin"}, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10, check=False) + self.assertEqual(result.returncode, 2) + self.assertIn(b"disposable GitHub-hosted", result.stderr) + self.assertFalse(output.parent.exists()) + self.assertEqual(result.stdout, b"") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_qoder_populated_research.py b/scripts/test_qoder_populated_research.py new file mode 100644 index 000000000..92b331c70 --- /dev/null +++ b/scripts/test_qoder_populated_research.py @@ -0,0 +1,344 @@ +"""Inert Qoder scope collector contracts; no vendor execution or guard spoofing.""" +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import time +import unittest +from unittest.mock import mock_open, patch +from types import SimpleNamespace + +SPEC = importlib.util.spec_from_file_location("qoder_scope_research", Path(__file__).with_name("research-qoder-populated.py")) +research = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(research) + + +class CollectorTests(unittest.TestCase): + def request(self, root, output): + for name in ("home", "tmp", "binary", *research.LOCATIONS): + (root / name).mkdir(parents=True) + sources = research.fixtures(root) + return {"root": str(root), "output": str(output), "binary": str(root / "binary/qoder"), + "sources": {key: str(value) for key, value in sources.items()}, + "fixture_pins": research.fixture_pins(sources, research.grok.Budget()), + "source_baseline": research.inventory({"source-" + key: value for key, value in sources.items()}, research.grok.Budget()), + "native_environment": research.phase1.runtime_environment(root / "home", root / "tmp"), + "parent_namespace": "net:[old]", "uid": 1001, "gid": 1001} + + def fake_probe(self, calls, fail=None): + def run(argv, cwd, env, output, label, **kwargs): + calls.append((list(argv), Path(cwd), dict(env), label)) + record = {"probe": label, "exit": 0, "error": None} + if label == fail: + record["exit"] = 1 + return record, b"", b"inert injected failure" + if label == "version": + body = b"Qoder 1.1.78\n" + elif label.endswith("-help"): + body = b"Usage\n" + elif label.endswith("-install"): + body = b'{"unknownNativeFields":"preserved without interpretation"}' + else: + body = b'[]' if label.startswith("empty-") else b'[{"unknownNativeFields":true}]' + return record, body, b"" + return run + + def test_finite_stages_exact_argv_and_stop_after_first_failed_mutation(self): + for fail in (None, "user-installed-install"): + with self.subTest(fail=fail), tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); root = base / "root"; output = base / "output"; output.mkdir() + request = self.request(root, output); summary = research.summary_record(); calls = [] + with patch.object(research, "isolation_facts", return_value={"verified": True}), \ + patch.object(research, "check_pins"), patch.object(research, "run_probe", side_effect=self.fake_probe(calls, fail)): + if fail: + with self.assertRaises(ValueError): + research.collect(request, summary, research.grok.Budget()) + self.assertFalse(summary["success"]) + self.assertFalse(summary["probes"][-1]["accepted"]) + else: + research.collect(request, summary, research.grok.Budget()) + self.assertTrue(summary["success"]) + self.assertEqual(len(calls), 19) + installs = [(argv[1:], cwd.name) for argv, cwd, _, label in calls if label.endswith("-install")] + self.assertEqual(installs, [(["plugins", "install", request["sources"][key], "--scope", scope, "--json"], location) + for key, scope, location in [("user", "user", "user-cwd"), ("A", "project", "project-A"), ("B", "project", "project-B")]]) + self.assertEqual(len([x for x in calls if x[0][1:] == ["plugins", "list", "--json"]]), 12) + self.assertTrue(all(summary[key] is False for key in research.FALSE_CLAIMS)) + self.assertTrue(all(env == request["native_environment"] for _, _, env, _ in calls)) + self.assertFalse(any(any(word in argv for word in ["uninstall", "enable", "disable", "trust", "login", "run"]) for argv, _, _, _ in calls if "--help" not in argv)) + + def test_text_only_sources_and_request_rejects_foreign_authority(self): + with tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); output = base / "output"; output.mkdir(); request = self.request(base / "root", output) + research.validate_request(request) + paths = [p for p in (base / "root/sources").rglob("*") if p.is_file()] + self.assertEqual(len(paths), 6) + self.assertEqual({p.suffix for p in paths}, {".json", ".md"}) + for change in [lambda r: r["native_environment"].update({"GITHUB_TOKEN": "inert"}), + lambda r: r["native_environment"].update({"HOME": "/foreign"}), + lambda r: r["sources"].update({"A": "/foreign"}), + lambda r: r.update({"binary": "/foreign/qoder"}), + lambda r: r.update({"output": r["root"]}), + lambda r: r["fixture_pins"].clear()]: + altered = copy.deepcopy(request); change(altered) + with self.assertRaises(ValueError): research.validate_request(altered) + + def test_strict_json_and_dirty_unknown_output_refusal(self): + for body in [b'{"a":1,"a":2}', b'[] trailing', b'NaN', b'\xff']: + with self.assertRaises((ValueError, UnicodeError)): research.strict_json(body) + record = {"error": None, "exit": 0} + for body, kind, stage in [(b'{}', "list", "empty"), (b'[{}]', "list", "empty"), + (b'[]', "list", "user-installed"), (b'null', "install", "A-installed")]: + with self.assertRaises(ValueError): research.validate_output(record, body, b"", kind, stage) + for dirty in [{"error": "limit", "exit": 0}, {"error": None, "exit": 1}]: + with self.assertRaises(ValueError): research.validate_output(dirty, b'[]', b"", "list", "empty") + with self.assertRaises(ValueError): research.validate_output(record, b'[]', b"warning", "list", "empty") + self.assertEqual(research.validate_output(record, b'[{"future":true}]', b"", "list", "A-installed"), [{"future": True}]) + + def test_inventory_refuses_alias_special_bounds_and_preserves_partial_metadata(self): + for kind in ["link", "fifo", "large", "hardlink"]: + with self.subTest(kind=kind), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary); (root / "a").write_bytes(b"inert") + if kind == "link": (root / "z").symlink_to(root / "a") + if kind == "fifo": os.mkfifo(root / "z") + if kind == "large": (root / "z").write_bytes(b"x" * 33) + if kind == "hardlink": os.link(root / "a", root / "z") + start = time.monotonic() + with patch.object(research, "MAX_FILE", 32), self.assertRaises(research.InventoryError) as raised: + research.inventory({"root": root}, research.grok.Budget()) + self.assertTrue(raised.exception.partial) + self.assertLess(time.monotonic() - start, 1) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary); (root / "a").write_bytes(b"x") + for attr in ["MAX_ENTRIES", "MAX_TOTAL"]: + with patch.object(research, attr, 0), self.assertRaises(research.InventoryError): + research.inventory({"root": root}, research.grok.Budget()) + (root / "d").mkdir(); (root / "d/e").mkdir() + with patch.object(research, "MAX_DEPTH", 0), self.assertRaises(research.InventoryError): + research.inventory({"root": root}, research.grok.Budget()) + + def test_exact_native_log_alias_is_only_bounded_literal_metadata(self): + for target_kind in ("file", "directory", "fifo", "dangling", "self", "nonutf8"): + with self.subTest(target_kind=target_kind), tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); home = base / "home"; logs = home / ".qoder/logs"; logs.mkdir(parents=True) + foreign = base / "foreign" + if target_kind == "file": foreign.write_text("foreign data must never be read") + if target_kind == "directory": foreign.mkdir() + if target_kind == "fifo": os.mkfifo(foreign) + target = os.fsencode(foreign) if target_kind not in ("self", "nonutf8") else b"latest" if target_kind == "self" else b"\xff-literal" + alias = logs / "latest"; os.symlink(target, os.fsencode(alias)) + real_open, real_stat = os.open, os.stat + def no_target_open(path, *args, **kwargs): + self.assertNotIn(path, (str(foreign), os.fsencode(foreign), target)) + return real_open(path, *args, **kwargs) + def no_target_stat(path, *args, **kwargs): + self.assertNotIn(path, (str(foreign), os.fsencode(foreign), target)) + return real_stat(path, *args, **kwargs) + with patch.object(research.os, "open", side_effect=no_target_open), patch.object(research.os, "stat", side_effect=no_target_stat): + result = research.inventory({"home": home}, research.grok.Budget(), native_log_alias=True) + record = result["home/.qoder/logs/latest"] + self.assertEqual(record["kind"], "symlink_metadata_only") + self.assertFalse(record["target_followed"]) + self.assertEqual(record["target_bytes"], len(target)) + self.assertEqual(bytes.fromhex(record["target_hex"]), target) + self.assertEqual(record["target_sha256"], hashlib.sha256(target).hexdigest()) + with self.assertRaises(research.InventoryError): + research.inventory({"home": home}, research.grok.Budget()) + for label in ("source-user", "source-A", "source-B", "another-home"): + with self.assertRaises(research.InventoryError): + research.inventory({label: home}, research.grok.Budget(), native_log_alias=True) + with patch.object(research, "MAX_TOTAL", len(target) - 1), self.assertRaises(research.InventoryError): + research.inventory({"home": home}, research.grok.Budget(), native_log_alias=True) + + def test_native_log_alias_admission_refuses_before_read_and_rechecks_bytes_identity_budget(self): + for failure in ("oversize", "negative", "hardlink", "length", "replacement", "deadline"): + with self.subTest(failure=failure), tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); home = base / "home"; logs = home / ".qoder/logs"; logs.mkdir(parents=True) + alias = logs / "latest"; alias.symlink_to("inert-target") + if failure == "hardlink": os.link(alias, base / "outside-alias", follow_symlinks=False) + budget = research.grok.Budget() + real_stat, real_readlink = os.stat, os.readlink + calls = [] + def metadata(path, *args, **kwargs): + info = real_stat(path, *args, **kwargs) + if path == "latest" and failure in ("oversize", "negative"): + fields = {name: getattr(info, name) for name in ("st_dev", "st_ino", "st_mode", "st_nlink", "st_size", "st_mtime_ns", "st_ctime_ns")} + fields["st_size"] = research.MAX_METADATA + 1 if failure == "oversize" else -1 + return SimpleNamespace(**fields) + return info + def link(path, *args, **kwargs): + calls.append(path) + self.assertIsInstance(path, bytes) + if failure in ("oversize", "negative", "hardlink"): + self.fail("inadmissible metadata reached readlink") + target = real_readlink(path, *args, **kwargs) + if failure == "length": return target + b"x" + if failure == "replacement": alias.unlink(); alias.symlink_to("replacement") + if failure == "deadline": budget.deadline = time.monotonic() - 1 + return target + with patch.object(research.os, "stat", side_effect=metadata), patch.object(research.os, "readlink", side_effect=link), self.assertRaises(research.InventoryError) as raised: + research.inventory({"home": home}, budget, native_log_alias=True) + self.assertEqual(raised.exception.partial["home/.qoder/logs/latest"]["kind"], "refused") + self.assertEqual(len(calls), 0 if failure in ("oversize", "negative", "hardlink") else 1) + + def test_native_log_alias_optin_never_admits_an_intermediate_or_other_alias(self): + for intermediate in (False, True): + with self.subTest(intermediate=intermediate), tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); home = base / "home"; (home / ".qoder").mkdir(parents=True) + foreign = base / "foreign"; foreign.mkdir() + if intermediate: (home / ".qoder/logs").symlink_to(foreign, target_is_directory=True) + else: + (home / ".qoder/logs").mkdir() + (home / ".qoder/logs/not-latest").symlink_to("inert") + with patch.object(research.os, "readlink", side_effect=AssertionError("unexpected alias readlink")), self.assertRaises(research.InventoryError): + research.inventory({"home": home}, research.grok.Budget(), native_log_alias=True) + + def test_inventory_preopen_symlink_swap_never_reads_foreign_file(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary); watched = root / "a"; watched.write_bytes(b"owned") + real_open = os.open + def race(path, flags, *args, **kwargs): + if path == "a": + watched.unlink(); watched.symlink_to("/foreign-inert") + return real_open(path, flags, *args, **kwargs) + with patch.object(research.os, "open", side_effect=race), self.assertRaises(research.InventoryError): + research.inventory({"root": root}, research.grok.Budget()) + + def test_isolation_verifier_checks_namespace_user_groups_caps_and_nnp(self): + good = "Groups:\t\nNoNewPrivs:\t1\n" + "\n".join(key + ":\t0000" for key in ["CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb"]) + for failure in [None, "namespace", "interfaces", "uid", "groups", "caps", "nnp"]: + with self.subTest(failure=failure), patch("builtins.open", mock_open(read_data=good.replace("CapEff:\t0000", "CapEff:\t0001") if failure == "caps" else good.replace("NoNewPrivs:\t1", "NoNewPrivs:\t0") if failure == "nnp" else good)), \ + patch.object(research.os, "getuid", return_value=0 if failure == "uid" else 1001), \ + patch.object(research.os, "getgid", return_value=1001), \ + patch.object(research.os, "geteuid", return_value=1001), \ + patch.object(research.os, "getegid", return_value=1001), \ + patch.object(research.os, "getgroups", return_value=[1001] if failure == "groups" else []), \ + patch.object(research.os, "readlink", return_value="net:[old]" if failure == "namespace" else "net:[new]"), \ + patch.object(research.socket, "if_nameindex", return_value=[(1, "lo"), (2, "eth0")] if failure == "interfaces" else [(1, "lo")]): + if failure: + with self.assertRaises(ValueError): research.isolation_facts("net:[old]", 1001, 1001) + else: + self.assertEqual(research.isolation_facts("net:[old]", 1001, 1001)["interfaces"], ["lo"]) + + def test_fixture_binary_tamper_and_deadline_refuse_without_vendor_execution(self): + with tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); output = base / "output"; output.mkdir(); request = self.request(base / "root", output) + binary = Path(request["binary"]); binary.write_bytes(b"inert") + import hashlib + with patch.object(research, "BINARY_BYTES", 5), patch.object(research.phase1, "BINARY_SHA256", hashlib.sha256(b"inert").hexdigest()): + research.check_pins(request, research.grok.Budget()) + added = Path(request["sources"]["A"]) / "extra-hook.json" + added.write_text("inert addition") + with self.assertRaisesRegex(ValueError, "complete inert source"): + research.check_pins(request, research.grok.Budget()) + added.unlink() + source = Path(next(iter(request["fixture_pins"]))); source.write_text("tampered") + with self.assertRaises(ValueError): research.check_pins(request, research.grok.Budget()) + with self.assertRaises(ValueError): research.check_pins(request, research.grok.Budget()) + budget = research.grok.Budget(.001) + with patch.object(research.grok.time, "monotonic", return_value=budget.deadline + 1), self.assertRaises(research.grok.CollectorDeadline): + research.inventory({"root": base / "root"}, budget) + + def test_cleanup_is_anchored_bounded_and_preserves_primary_deadline_failure(self): + with tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); output = base / "raw"; output.mkdir() + root = base / "root"; root.mkdir(); foreign = base / "foreign"; foreign.write_text("preserve") + (root / "link").symlink_to(foreign) + self.assertTrue(research.cleanup(root, root.lstat(), output)["complete"]) + self.assertFalse(root.exists()); self.assertEqual(foreign.read_text(), "preserve") + root.mkdir(); held = root.lstat(); root.rename(base / "original"); root.mkdir() + self.assertFalse(research.cleanup(root, held, output)["complete"]) + self.assertTrue(root.is_dir()); self.assertTrue((base / "original").is_dir()) + root.rmdir() + summary = research.summary_record() + with patch.object(research, "verify_checkout"), \ + patch.object(research.os, "getuid", return_value=1001), \ + patch.object(research.os, "getgid", return_value=1001), \ + patch.object(research.os, "geteuid", return_value=1001), \ + patch.object(research.os, "getegid", return_value=1001), \ + patch.object(research.phase1.tempfile, "mkdtemp", return_value=str(base / "experiment")), \ + patch.object(research, "acquire", side_effect=research.grok.CollectorDeadline("inert deadline")), \ + patch.object(research, "cleanup", return_value={"complete": False, "error": "inert cleanup timeout"}) as clean: + # mkdtemp is replaced only with an inert precreated test root. + (base / "experiment").mkdir() + research.run_collection(output, summary) + self.assertFalse(summary["success"]) + self.assertIn("inert deadline", summary["error"]) + self.assertFalse(summary["cleanup"]["complete"]) + self.assertEqual(clean.call_count, 1) + self.assertEqual(json.loads((output / "qoder-populated-research.json").read_text())["error"], summary["error"]) + calls = [] + def timed(argv, cwd, env, output, label, **kwargs): + calls.append(kwargs) + return {"exit": -9, "error": "inert deadline"}, b"", b"" + with patch.object(research, "run_probe", side_effect=timed): + self.assertFalse(research.cleanup(base / "experiment", (base / "experiment").lstat(), output)["complete"]) + self.assertEqual(calls, [{"timeout": 5}]) + + def test_upload_boundary_never_exposes_rejected_oversize_or_nonregular_tree(self): + for refused in [None, "overflow", "link", "hardlink", "directory"]: + with self.subTest(refused=refused), tempfile.TemporaryDirectory() as temporary: + base = Path(temporary); raw = base / "raw"; raw.mkdir(); upload = base / "upload" + (raw / "safe.log").write_text("inert") + if refused == "overflow": (raw / "large.log").write_bytes(b"x" * 5000) + if refused == "link": (raw / "z").symlink_to("/foreign") + if refused == "hardlink": os.link(raw / "safe.log", raw / "z") + if refused == "directory": (raw / "z").mkdir() + summary = research.summary_record(); summary["success"] = True + with patch.object(research, "MAX_ARTIFACT", 4096): + result = research.stage_artifacts(raw, upload, summary) + if refused: + self.assertFalse(result["complete"]); self.assertFalse(summary["success"]) + self.assertEqual([p.name for p in upload.iterdir()], ["qoder-populated-research.json"]) + fallback = json.loads((upload / "qoder-populated-research.json").read_text()) + self.assertFalse(fallback["success"]) + self.assertTrue(all(fallback[key] is False for key in research.FALSE_CLAIMS)) + else: + self.assertTrue(result["complete"]) + self.assertEqual((upload / "safe.log").read_text(), "inert") + self.assertLess(sum(p.stat().st_size for p in upload.iterdir()), 4096) + workflow = Path(research.__file__).parent.parent / ".github/workflows/qoder-populated-research.yml" + self.assertIn("path: ${{ runner.temp }}/qoder-populated-upload/", workflow.read_text()) + self.assertNotIn("path: ${{ runner.temp }}/qoder-populated-research/", workflow.read_text()) + + def test_source_head_is_observed_and_mismatch_stops_before_acquisition(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) + for observed in ["a" * 40, "b" * 40]: + summary = research.summary_record() + with patch.dict(os.environ, {"EXPECTED_COMMIT": "a" * 40}), \ + patch.object(research, "run_probe", return_value=({"exit": 0, "error": None}, (observed + "\n").encode(), b"")) as probe: + if observed == "a" * 40: + research.verify_checkout(output, summary, research.grok.Budget()) + self.assertEqual(summary["source_commit"], observed) + else: + with self.assertRaisesRegex(ValueError, "actual git HEAD"): + research.verify_checkout(output, summary, research.grok.Budget()) + self.assertEqual(probe.call_args[0][0], ["/usr/bin/git", "rev-parse", "HEAD"]) + + def test_real_hosted_guard_refuses_before_any_effect(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / "must-not-exist" + result = subprocess.run([sys.executable, str(Path(research.__file__)), "--output", str(output)], + env={"PATH": os.environ.get("PATH", "/usr/bin:/bin"), "PYTHONDONTWRITEBYTECODE": "1"}, capture_output=True, timeout=10) + self.assertEqual(result.returncode, 2) + self.assertIn(b"disposable GitHub-hosted", result.stderr) + self.assertFalse(output.exists()) + + def test_wrapper_carries_only_actual_hosted_fields_and_drops_authority(self): + request = {"uid": 1001, "gid": 1002} + with patch.dict(os.environ, {"GITHUB_TOKEN": "inert", "HTTP_PROXY": "inert"}): + command = research.wrapper_command(Path("/inert/request"), request) + self.assertIn("--net", command); self.assertIn("--clear-groups", command) + self.assertIn("--no-new-privs", command); self.assertIn("--bounding-set=-all", command) + self.assertFalse(any("GITHUB_TOKEN=" in arg or "HTTP_PROXY=" in arg for arg in command)) + self.assertNotIn("--scope", command) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_vscode_native_research.py b/scripts/test_vscode_native_research.py new file mode 100644 index 000000000..bb8eba59d --- /dev/null +++ b/scripts/test_vscode_native_research.py @@ -0,0 +1,291 @@ +"""Synthetic archives and inert Python children; no publisher acquisition/execution.""" +import gzip +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tarfile +import tempfile +import unittest +import zipfile +from unittest.mock import patch + +SPEC = importlib.util.spec_from_file_location("vscode_research", Path(__file__).with_name("research-vscode-native.py")) +research = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(research) + + +def archive_bytes(entries, tail=b""): + output = io.BytesIO() + with tarfile.open(fileobj=output, mode="w", format=tarfile.GNU_FORMAT) as archive: + for name, kind, body in entries: + entry = tarfile.TarInfo(name) + entry.type = kind + if kind in (tarfile.REGTYPE, tarfile.AREGTYPE): + entry.size = len(body) + if kind == tarfile.SYMTYPE: + entry.linkname = body.decode() + archive.addfile(entry, io.BytesIO(body) if entry.isreg() else None) + return gzip.compress(output.getvalue() + tail) + + +def regular(name, body=b"inert"): + return (research.PREFIX + "/" + name, tarfile.REGTYPE, body) + + +def extract(root, body): + archive, destination = root / "archive.gz", root / "publisher" + archive.write_bytes(body) + destination.mkdir() + return research.materialize_archive(archive, destination, research.bounds.Budget()) + + +class VSCodeResearchTests(unittest.TestCase): + def test_actual_guard_refuses_before_output_acquisition_or_native(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "never-created" / "research" + env = {k: v for k, v in os.environ.items() if k not in ( + "GITHUB_ACTIONS", "RUNNER_ENVIRONMENT", "AGENTPLUGINS_NATIVE_DISPOSABLE_HOSTED")} + result = subprocess.run([sys.executable, str(Path(research.__file__)), "--output", str(output)], + env=env, capture_output=True, timeout=5, check=False) + self.assertEqual(result.returncode, 2) + self.assertIn(b"disposable GitHub-hosted", result.stderr) + self.assertFalse(output.parent.exists()) + + def test_exact_archive_digest_and_size_precede_materialization(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for body in (b"short", b"x" * 10, b"x" * 11): + with patch.object(research, "ARCHIVE_BYTES", 10), \ + patch.object(research.urllib.request, "build_opener") as factory: + factory.return_value.open.return_value = io.BytesIO(body) + with self.assertRaises(ValueError): + research.acquire_archive(root, research.bounds.Budget()) + self.assertFalse((root / "publisher").exists()) + (root / "publisher.tar.gz").unlink(missing_ok=True) + body = b"measured inert archive" + with patch.object(research, "ARCHIVE_BYTES", len(body)), \ + patch.object(research, "ARCHIVE_SHA256", hashlib.sha256(body).hexdigest()), \ + patch.object(research.urllib.request, "build_opener") as factory: + factory.return_value.open.return_value = io.BytesIO(body) + self.assertEqual(research.acquire_archive(root, research.bounds.Budget()).read_bytes(), body) + + def test_redirects_never_leave_exact_https_publisher(self): + for url in ("http://vscode.download.prss.microsoft.com/x", "https://foreign.test/x", + "https://user:secret@vscode.download.prss.microsoft.com/x", + "https://vscode.download.prss.microsoft.com:444/x"): + with self.subTest(url=url), self.assertRaises(ValueError): + research.PublisherRedirects().redirect_request(None, None, 302, "", {}, url) + redirects = research.PublisherRedirects() + redirects.count = 3 + with self.assertRaises(ValueError): + redirects.redirect_request(None, None, 302, "", {}, research.URL) + + def test_regular_files_and_omitted_links_have_no_alias_graph(self): + entries = [regular("code", b"\x7fELFinert"), + (research.PREFIX + "/omitted", tarfile.SYMTYPE, b"../../foreign"), + regular("omitted/child", b"contained ordinary bytes")] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + result = extract(root, archive_bytes(entries)) + self.assertEqual(result["omitted_symlinks"], 1) + path = root / "publisher" / research.PREFIX / "omitted" + self.assertTrue(path.is_dir()) + self.assertFalse(path.is_symlink()) + self.assertEqual((path / "child").read_bytes(), b"contained ordinary bytes") + + def test_archive_names_aliases_types_and_counts_are_refused(self): + cases = [ + [("/" + research.PREFIX + "/code", tarfile.REGTYPE, b"x")], + [(research.PREFIX + "/../code", tarfile.REGTYPE, b"x")], + [(research.PREFIX + "/./code", tarfile.REGTYPE, b"x")], + [(research.PREFIX + "/x\\code", tarfile.REGTYPE, b"x")], + [(research.PREFIX + "/x:code", tarfile.REGTYPE, b"x")], + [(research.PREFIX + "/control\n", tarfile.REGTYPE, b"x")], + [regular("code"), regular("code")], + [regular("Foo/one"), regular("foo/two")], + [regular("parent"), regular("parent/child")], + [regular("parent/child"), regular("parent")], + ] + for kind in (tarfile.LNKTYPE, tarfile.FIFOTYPE, tarfile.CHRTYPE, tarfile.GNUTYPE_SPARSE): + cases.append([(research.PREFIX + "/code", kind, b"")]) + for entries in cases: + with self.subTest(entries=entries), tempfile.TemporaryDirectory() as directory, self.assertRaises((ValueError, tarfile.TarError)): + extract(Path(directory), archive_bytes(entries)) + for field, limit in (("MAX_MEMBERS", 1), ("MAX_FILE", 1), ("MAX_EXPANDED", 600), ("MAX_DEPTH", 1)): + with self.subTest(limit=field), tempfile.TemporaryDirectory() as directory, \ + patch.object(research, field, limit), self.assertRaises(ValueError): + extract(Path(directory), archive_bytes([regular("one"), regular("two")])) + + def test_tar_metadata_crc_and_trailing_data_are_bounded(self): + good = archive_bytes([regular("code")]) + damaged_crc = good[:-8] + bytes([good[-8] ^ 1]) + good[-7:] + for body in (good[:-5], damaged_crc, archive_bytes([regular("code")], b"not padding")): + with tempfile.TemporaryDirectory() as directory, self.assertRaises((ValueError, EOFError, OSError)): + extract(Path(directory), body) + output = io.BytesIO() + with tarfile.open(fileobj=output, mode="w", format=tarfile.PAX_FORMAT) as archive: + entry = tarfile.TarInfo(research.PREFIX + "/code") + entry.size = 1 + entry.pax_headers = {"GNU.sparse.size": "1"} + archive.addfile(entry, io.BytesIO(b"x")) + with tempfile.TemporaryDirectory() as directory, self.assertRaisesRegex(ValueError, "PAX"): + extract(Path(directory), gzip.compress(output.getvalue())) + with tempfile.TemporaryDirectory() as directory, patch.object(research, "MAX_METADATA", 1), self.assertRaises(ValueError): + extract(Path(directory), gzip.compress(output.getvalue())) + for body in (b"999999999999 x=y\n", b"3 a", b"9 x=bad\n", b"12 path=one\n12 path=two\n"): + with self.assertRaises(ValueError): + research.pax_values(body) + + def test_descriptor_writes_refuse_existing_symlink(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + archive = root / "archive.gz" + archive.write_bytes(archive_bytes([regular("child/file")])) + destination = root / "publisher" + (destination / research.PREFIX).mkdir(parents=True) + foreign = root / "foreign" + foreign.mkdir() + (destination / research.PREFIX / "child").symlink_to(foreign, target_is_directory=True) + with self.assertRaises(OSError): + research.materialize_archive(archive, destination, research.bounds.Budget()) + self.assertEqual(list(foreign.iterdir()), []) + + def test_required_files_are_regular_hashed_and_tamper_visible(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in research.REQUIRED: + path = root / research.PREFIX / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"\x7fELFinert" if name == "code" else b"inert") + before = research.required_files(root) + path = root / research.PREFIX / research.REQUIRED[1] + path.write_bytes(b"changed") + self.assertNotEqual(before, research.required_files(root)) + path.unlink() + path.symlink_to(root / research.PREFIX / "code") + with self.assertRaises(ValueError): + research.required_files(root) + + def test_finite_native_chain_has_same_process_isolation_and_clean_env(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + probe = root / "version" + probe.mkdir() + with patch.dict(os.environ, {"GH_TOKEN": "inert", "HTTP_PROXY": "foreign", "VSCODE_IPC_HOOK_CLI": "foreign"}): + env = research.probe_environment(probe) + self.assertEqual(env["ELECTRON_RUN_AS_NODE"], "1") + self.assertNotIn("GH_TOKEN", env) + self.assertNotIn("HTTP_PROXY", env) + self.assertNotIn("VSCODE_IPC_HOOK_CLI", env) + with patch.object(research.os, "getuid", return_value=1000), patch.object(research.os, "getgid", return_value=1001), \ + patch.object(research.os, "geteuid", return_value=1000), patch.object(research.os, "getegid", return_value=1001): + chain, argv, _ = research.isolated_command(root, "version", env, "--version", 12345) + self.assertEqual(argv[:3], [str(root / "publisher" / research.PREFIX / "code"), str(root / "publisher" / research.PREFIX / research.REQUIRED[1]), "--version"]) + self.assertEqual(chain[:5], ["/usr/bin/sudo", "-n", "/usr/bin/unshare", "--net", "/usr/bin/setpriv"]) + self.assertIn("--clear-groups", chain) + self.assertIn("--bounding-set=-all", chain) + self.assertIn("--no-new-privs", chain) + self.assertIn("/usr/bin/env", chain) + self.assertIn("-i", chain) + self.assertIn("os.execve", research.ISOLATION_VERIFIER) + self.assertIn("socket.if_nameindex()", research.ISOLATION_VERIFIER) + self.assertNotIn("--no-sandbox", argv) + with patch.object(research.os, "getuid", return_value=0), self.assertRaises(ValueError): + research.isolated_command(root, "version", env, "--version", 12345) + + def test_cleanup_is_anchored_no_follow_and_refuses_replaced_root(self): + with tempfile.TemporaryDirectory() as directory: + parent = Path(directory) + output, root, foreign = parent / "output", parent / "task", parent / "foreign" + output.mkdir(); root.mkdir(); foreign.mkdir() + (foreign / "keep").write_text("foreign") + (root / "nested").mkdir() + (root / "nested" / "inert").write_text("inert") + (root / "alias").symlink_to(foreign, target_is_directory=True) + identity = root.lstat() + self.assertTrue(research.cleanup(root, identity, output)["complete"]) + self.assertFalse(root.exists()) + self.assertEqual((foreign / "keep").read_text(), "foreign") + root.mkdir() + identity = root.lstat() + root.rename(parent / "original") + root.mkdir() + result = research.cleanup(root, identity, output) + self.assertFalse(result["complete"]) + self.assertTrue(root.exists()) + self.assertTrue((parent / "original").exists()) + + def test_output_admission_cannot_qualify_workspace_or_truncation(self): + good = {"error": None, "exit": 0} + self.assertTrue(research.validate_probe(good, (research.VERSION + "\n" + research.COMMIT).encode(), b"", "version")) + self.assertFalse(research.validate_probe(good, (research.VERSION + "0\n" + research.COMMIT).encode(), b"", "version")) + self.assertFalse(research.validate_probe({"error": "overflow", "exit": 0}, b"help", b"", "help")) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) + (output / "inert").write_bytes(b"x" * 9) + with patch.object(research, "MAX_ARTIFACT", 8), self.assertRaises(ValueError): + research.artifact_bound(output) + source = Path(research.__file__).read_text() + self.assertIn('"native_project_qualified": False', source) + workflow = Path(research.__file__).parent.parent / ".github/workflows/vscode-native-research.yml" + text = workflow.read_text() + self.assertIn("persist-credentials: false", text) + self.assertIn("timeout-minutes: 10", text) + self.assertIn("github.event.pull_request.head.sha || github.sha", text) + self.assertNotIn("pull_request_target", text) + self.assertIn("if: ${{ always() }}", text) + self.assertIn("vscode-native-research/upload/research.zip", text) + self.assertNotIn("path: ${{ runner.temp }}/vscode-native-research/\n", text) + self.assertIn("steps.upload_admission.outputs.valid", text) + + def test_only_validated_bounded_envelope_is_uploaded_even_on_admission_failure(self): + for condition in ("valid", "overflow", "symlink", "directory"): + with self.subTest(condition=condition), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "raw" + raw.mkdir() + (raw / "version.stdout.log").write_bytes(b"observed finite output") + if condition == "overflow": + (raw / "large.log").write_bytes(b"x" * 100) + elif condition == "symlink": + foreign = root / "foreign" + foreign.write_text("must never upload foreign data") + (raw / "alias").symlink_to(foreign) + elif condition == "directory": + (raw / "unexpected").mkdir() + limit = (64 << 10) + 64 if condition == "overflow" else research.MAX_ARTIFACT + with patch.object(research, "MAX_ARTIFACT", limit): + accepted = research.publish_artifacts(raw, {"source_commit": "inert", "error": "original failure"}) + self.assertEqual(accepted, condition == "valid") + upload = raw / "upload" + self.assertEqual([path.name for path in upload.iterdir()], ["research.zip"]) + envelope = upload / "research.zip" + self.assertLessEqual(envelope.stat().st_size, limit) + with zipfile.ZipFile(envelope) as archive: + if accepted: + self.assertEqual(archive.namelist(), ["version.stdout.log"]) + self.assertEqual(archive.read("version.stdout.log"), b"observed finite output") + else: + self.assertEqual(archive.namelist(), ["collection-failure.json"]) + failure = json.loads(archive.read("collection-failure.json")) + self.assertFalse(failure["collection_complete"]) + self.assertFalse(failure["native_project_qualified"]) + self.assertEqual(failure["collection_error"], "original failure") + + def test_root_group_or_effective_identity_mismatch_refuses_before_exec(self): + for uid, gid, euid, egid in ((1000, 0, 1000, 0), (1000, 1001, 0, 1001), (1000, 1001, 1000, 0)): + with self.subTest(ids=(uid, gid, euid, egid)), patch.object(research.os, "getuid", return_value=uid), \ + patch.object(research.os, "getgid", return_value=gid), patch.object(research.os, "geteuid", return_value=euid), \ + patch.object(research.os, "getegid", return_value=egid), self.assertRaises(ValueError): + research.isolated_command(Path("/inert"), "version", {}, "--version", 12345) + for required in ('observed["gid"]==0', 'observed["euid"]!=int(uid)', 'observed["egid"]!=int(gid)'): + self.assertIn(required, research.ISOLATION_VERIFIER) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_vscode_workspace_research.py b/scripts/test_vscode_workspace_research.py new file mode 100644 index 000000000..32ae222d2 --- /dev/null +++ b/scripts/test_vscode_workspace_research.py @@ -0,0 +1,293 @@ +"""Inert contracts only: no vendor, sudo, acquisition or hosted guard spoofing.""" +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import time +import unittest +from unittest import mock +import zipfile + +ROOT = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location("vscode_workspace_test", ROOT / "research-vscode-workspace.py") +collector = importlib.util.module_from_spec(spec) +spec.loader.exec_module(collector) + + +class WorkspaceCollectorTests(unittest.TestCase): + def test_actual_host_guard_refuses_before_output(self): + with tempfile.TemporaryDirectory() as root: + output = Path(root) / "never-created" + with mock.patch.dict(os.environ, {}, clear=True), mock.patch.object(sys, "argv", ["collector", "--output", str(output)]): + self.assertEqual(collector.main(), 2) + self.assertFalse(output.exists()) + + def test_publisher_bytes_and_two_executable_contract_remain_fixed(self): + self.assertEqual(collector.publisher.ARCHIVE_BYTES, 350844741) + self.assertEqual(collector.publisher.ARCHIVE_SHA256, "48f658052e54e020a1c1be7b37ac0f9ae39f07e8ccda805c63f053cfda72b2be") + self.assertEqual(set(collector.PINNED_FILES), {"code", "resources/app/out/cli.js", "resources/app/product.json"}) + source = (ROOT.parent / "install/integrationctl/research/vscodeworkspace/model_linux.go").read_text() + self.assertIn("node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/linux-x64/rg", source) + + def test_control_capture_has_no_privileged_kill_fallback(self): + errors = [] + with mock.patch.object(collector.os, "killpg", side_effect=PermissionError): + collector.signal_controller(mock.Mock(pid=123), 15, errors) + self.assertEqual(len(errors), 1) + self.assertIn("cleanup not proved", errors[0]) + + def test_empty_flat_envelope_admits_only_ordinary_zip(self): + with tempfile.TemporaryDirectory() as root: + output = Path(root) + summary = {"source_commit": "a" * 40, "collection_complete": False, "native_project_qualified": False} + (output / "vscode-workspace-research.json").write_text(json.dumps(summary)) + self.assertTrue(collector.publish_from_memory(output, summary)["complete"]) + with zipfile.ZipFile(output / "upload/research.zip") as archive: + self.assertEqual(archive.namelist(), ["vscode-workspace-research.json"]) + self.assertFalse(json.loads(archive.read(archive.namelist()[0]))["native_project_qualified"]) + + def test_nonregular_raw_member_yields_only_false_failure_envelope(self): + with tempfile.TemporaryDirectory() as root: + output = Path(root) + summary = {"source_commit": "b" * 40} + (output / "vscode-workspace-research.json").write_text(json.dumps(summary)) + (output / "native-cache").mkdir() + self.assertFalse(collector.publish_from_memory(output, summary)["complete"]) + with zipfile.ZipFile(output / "upload/research.zip") as archive: + self.assertEqual(archive.namelist(), ["collection-failure.json"]) + result = json.loads(archive.read(archive.namelist()[0])) + self.assertFalse(result["collection_complete"]) + self.assertFalse(result["native_project_qualified"]) + + def test_build_and_watchdog_bounds_are_explicit(self): + source = (ROOT / "research-vscode-workspace.py").read_text() + self.assertIn('timeout=min(60, budget.check()), limit=1 << 20', source) + self.assertIn("work_seconds=4.0, reap_seconds=1.0", source) + self.assertNotIn("--publish-only", source) + self.assertEqual((collector.ROOT_TIMEOUT, collector.ROOT_KILL), (460, 470)) + self.assertEqual(collector.MAX_CONTROL, 64 << 10) + + def test_unrecorded_flat_member_is_never_uploaded(self): + with tempfile.TemporaryDirectory() as root: + output = Path(root) + summary = {"source_commit": "c" * 40, "collection_complete": False} + (output / "vscode-workspace-research.json").write_text(json.dumps(summary)) + (output / "unexpected-native-cache.json").write_text("private native data") + self.assertFalse(collector.publish_from_memory(output, summary)["complete"]) + with zipfile.ZipFile(output / "upload/research.zip") as archive: + self.assertEqual(archive.namelist(), ["collection-failure.json"]) + + def test_fork_uses_preloaded_code_after_source_file_mutation(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + module_path, marker = root / "private_admission.py", root / "mutated-source-executed" + module_path.write_text("def publish_artifacts(output,summary,allowed_names=None):\n (output/'preloaded.txt').write_text('preloaded memory')\n return True\n") + module_spec = importlib.util.spec_from_file_location("private_preloaded_admission", module_path) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + module_path.write_text("raise AssertionError('mutated source must never execute')\n") + with mock.patch.object(collector, "publisher", module): + result = collector.publish_from_memory(root, {}) + self.assertTrue(result["complete"]) + self.assertFalse(marker.exists()) + self.assertEqual((root / "preloaded.txt").read_text(), "preloaded memory") + + def test_new_post_native_import_refuses_without_executing_source(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + marker = root / "executed" + (root / "new_post_native.py").write_text("raise AssertionError('new import must never execute')\n") + def lazy_publish(*_args, **_kwargs): + __import__("new_post_native") + return True + with mock.patch.object(collector.publisher, "publish_artifacts", lazy_publish), mock.patch.object(collector.sys, "path", [str(root)] + list(collector.sys.path)): + self.assertFalse(collector.publish_from_memory(root, {})["complete"]) + self.assertFalse(marker.exists()) + + def test_fork_admission_deadline_reaps_only_owned_child(self): + def blocked(*_args, **_kwargs): + while True: + pass + with tempfile.TemporaryDirectory() as root, mock.patch.object(collector.publisher, "publish_artifacts", blocked): + result = collector.publish_from_memory(Path(root), {}, work_seconds=0.03, reap_seconds=0.5) + self.assertFalse(result["complete"]) + self.assertTrue(result["child_reaped"]) + + def test_unproved_late_publisher_cannot_recreate_canonical_upload(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + output = root / "output" + output.mkdir() + expected = output.lstat() + release_read, release_write = os.pipe() + child_pids = [] + original_fork = os.fork + def fork(): + pid = original_fork() + if pid > 0: + child_pids.append(pid) + return pid + def late_publish(selected, _summary, allowed_names=None): + os.read(release_read, 1) + try: + (selected / "upload").mkdir() + (selected / "upload/research.zip").write_bytes(b"late positive bytes") + except FileNotFoundError: + (root / "late-refused").write_bytes(b"canonical parent withheld") + return True + try: + with mock.patch.object(collector.os, "fork", fork), mock.patch.object(collector.os, "kill"), mock.patch.object(collector.publisher, "publish_artifacts", late_publish): + result = collector.publish_from_memory(output, {}, work_seconds=0.02, reap_seconds=0.02) + self.assertFalse(result["child_reaped"]) + self.assertFalse(result["complete"]) + quarantine = collector.quarantine_output(output, expected) + self.assertTrue(quarantine.is_dir()) + self.assertFalse(output.exists()) + os.write(release_write, b"x") + deadline = time.monotonic() + 2 + joined = False + while time.monotonic() < deadline: + pid, _status = os.waitpid(child_pids[0], os.WNOHANG) + if pid: + joined = True + break + time.sleep(0.01) + self.assertTrue(joined, "inert late publisher did not join") + self.assertTrue((root / "late-refused").is_file()) + self.assertFalse((output / "upload/research.zip").exists()) + finally: + os.close(release_read) + os.close(release_write) + for pid in child_pids: + try: + child, _status = os.waitpid(pid, os.WNOHANG) + if not child: + os.kill(pid, 9) + os.waitpid(pid, 0) + except ChildProcessError: + pass + + def test_unexpected_publisher_failure_withholds_existing_zip_and_foreign_root_refuses(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + output = root / "output" + output.mkdir() + expected = output.lstat() + def publish_then_fail(selected, _summary, allowed_names=None): + (selected / "upload").mkdir() + (selected / "upload/research.zip").write_bytes(b"positive bytes before error") + raise RuntimeError("inert late publication error") + with mock.patch.object(collector.publisher, "publish_artifacts", publish_then_fail): + result = collector.publish_from_memory(output, {}) + self.assertFalse(result["complete"]) + self.assertFalse(result.get("archive_failure_allowed")) + foreign = root / "foreign" + foreign.mkdir() + with self.assertRaises(ValueError): + collector.quarantine_output(output, foreign.lstat()) + quarantine = collector.quarantine_output(output, expected) + self.assertEqual((quarantine / "upload/research.zip").read_bytes(), b"positive bytes before error") + self.assertFalse(output.exists()) + + def test_workspace_cleanup_uses_isolated_fixed_python_and_trusted_cwd(self): + record = {"exit": 0, "error": None} + with tempfile.TemporaryDirectory() as root: + path = Path(root) + with mock.patch.object(collector, "run_probe", return_value=(record, b"", b"")) as run: + self.assertTrue(collector.cleanup_frontend(path, path.stat(), path)["complete"]) + argv, cwd = run.call_args.args[:2] + self.assertEqual(argv[:3], ["/usr/bin/python3", "-I", "-c"]) + self.assertEqual(cwd, Path("/")) + + def cleanup_fixture(self): + task = Path(tempfile.mkdtemp(prefix="vscode-workspace-input-", dir="/tmp")) + self.addCleanup(lambda: shutil.rmtree(task, ignore_errors=True)) + uid, gid = (65534, 65534) if os.getuid() == 0 else (os.getuid(), os.getgid()) + if os.getuid() == 0: + os.chown(task, uid, gid) + return task, uid, gid + + def cleanup_child(self, task, identity, uid, gid): + argv = ["/usr/bin/python3", "-I", "-c", collector.FRONTEND_CLEANUP_CODE, + str(task), str(identity.st_dev), str(identity.st_ino), str(uid)] + if os.getuid() == 0: + argv = ["/usr/bin/setpriv", "--reuid=" + str(uid), "--regid=" + str(gid), "--clear-groups", + "--bounding-set=-all", "--inh-caps=-all", "--ambient-caps=-all", "--no-new-privs", *argv] + return subprocess.run(argv, cwd="/", env={"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=7) + + def test_ordinary_uid_cleanup_removes_readonly_cache_and_preserves_external_link_target(self): + task, uid, gid = self.cleanup_fixture() + cache = task / "go-cache" + cache.mkdir() + script = cache / "mkall.sh" + script.write_text("inert cache asset") + with tempfile.TemporaryDirectory() as outside: + external = Path(outside) / "untouched" + external.write_text("outside bytes") + external.chmod(0o440) + before = external.stat() + link = task / "outside-link" + link.symlink_to(external) + if os.getuid() == 0: + os.chown(script, uid, gid) + os.chown(cache, uid, gid) + os.chown(link, uid, gid, follow_symlinks=False) + script.chmod(0o444) + cache.chmod(0o555) + result = self.cleanup_child(task, task.stat(), uid, gid) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(task.exists()) + self.assertEqual(external.read_text(), "outside bytes") + self.assertEqual(external.stat().st_mode, before.st_mode) + self.assertEqual(external.stat().st_ino, before.st_ino) + + def test_ordinary_uid_cleanup_refuses_replaced_root_without_mutating_replacement(self): + task, uid, gid = self.cleanup_fixture() + identity = task.stat() + saved = task.with_name(task.name + "-saved") + task.rename(saved) + self.addCleanup(lambda: shutil.rmtree(saved, ignore_errors=True)) + task.mkdir() + sentinel = task / "keep" + sentinel.write_text("replacement bytes") + if os.getuid() == 0: + os.chown(sentinel, uid, gid) + os.chown(task, uid, gid) + task.chmod(0o555) + before = task.stat() + result = self.cleanup_child(task, identity, uid, gid) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(sentinel.read_text(), "replacement bytes") + self.assertEqual(task.stat().st_mode, before.st_mode) + task.chmod(0o700) + + @unittest.skipUnless(os.getuid() == 0, "creating a genuinely foreign-owned directory requires the root test fixture") + def test_ordinary_uid_cleanup_refuses_foreign_directory_without_chmod(self): + task, uid, gid = self.cleanup_fixture() + foreign = task / "foreign-owned" + foreign.mkdir(mode=0o555) + sentinel = foreign / "keep" + sentinel.write_text("foreign bytes") + before = foreign.stat() + result = self.cleanup_child(task, task.stat(), uid, gid) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(sentinel.read_text(), "foreign bytes") + self.assertEqual(foreign.stat().st_mode, before.st_mode) + + def test_workflow_uploads_only_independently_admitted_zip(self): + source = (ROOT.parent / ".github/workflows/vscode-workspace-research.yml").read_text() + self.assertIn("steps.upload_admission.outputs.valid == 'true'", source) + self.assertIn("vscode-workspace-research/upload/research.zip", source) + self.assertIn("persist-credentials: false", source) + self.assertIn("/usr/bin/python3 -I -", source) + self.assertIn("github.event.pull_request.head.sha || github.sha", source) + + +if __name__ == "__main__": + unittest.main() + diff --git a/scripts/vscode-workspace-observer/extension.cjs b/scripts/vscode-workspace-observer/extension.cjs new file mode 100644 index 000000000..3044d4513 --- /dev/null +++ b/scripts/vscode-workspace-observer/extension.cjs @@ -0,0 +1,4 @@ +'use strict'; +exports.activate = context => Object.freeze({extensionMode: context.extensionMode}); +exports.deactivate = () => {}; + diff --git a/scripts/vscode-workspace-observer/package.json b/scripts/vscode-workspace-observer/package.json new file mode 100644 index 000000000..fccac6c78 --- /dev/null +++ b/scripts/vscode-workspace-observer/package.json @@ -0,0 +1,23 @@ +{ + "name": "workspace-observer", + "publisher": "uap-research", + "version": "0.0.0", + "private": true, + "engines": { + "vscode": "^1.143.0" + }, + "main": "./extension.cjs", + "activationEvents": [ + "*" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": true + } + }, + "enabledApiProposals": [ + "mcpServerDefinitions", + "chatPromptFiles" + ] +} + diff --git a/scripts/vscode-workspace-observer/runner.cjs b/scripts/vscode-workspace-observer/runner.cjs new file mode 100644 index 000000000..f76e7f953 --- /dev/null +++ b/scripts/vscode-workspace-observer/runner.cjs @@ -0,0 +1,66 @@ +'use strict'; +// Read proposed metadata only; never start/gateway MCP, invoke prompts or grant trust. +const fs = require('node:fs'); +const path = require('node:path'); +const shape = require('./shape.cjs'); +const LIMIT = 512 * 1024; +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +function store(output, result, fileSystem = fs) { + if (!output || !path.isAbsolute(output) || path.basename(output) !== 'observation.json') throw new Error('fixed result path required'); + const body = Buffer.from(JSON.stringify(result) + '\n', 'utf8'); + if (body.length > LIMIT) throw new Error('observation JSON exceeds bound'); + const parent = path.dirname(output); + const pending = path.join(parent, '.observation.pending'); + const ready = path.join(parent, 'observation.ready'); + const fd = fileSystem.openSync(pending, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600); + try { fileSystem.writeFileSync(fd, body); fileSystem.fsyncSync(fd); } finally { fileSystem.closeSync(fd); } + fileSystem.linkSync(pending, output); + fileSystem.unlinkSync(pending); + const gate = fileSystem.openSync(ready, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600); + fileSystem.closeSync(gate); +} +exports.storeResult = store; +async function observe() { + const vscode = require('vscode'); + const folders = vscode.workspace.workspaceFolders; + if (!Array.isArray(folders) || folders.length !== 1) throw new Error('exactly one selected folder required'); + const folder = folders[0]; + const selected = shape.uri(folder.uri); + if (selected.scheme !== 'file' || selected.authority !== '' || folder.uri.fsPath !== process.env.UAP_VSCODE_WORKSPACE_FOLDER || vscode.workspace.isTrusted !== false) throw new Error('selected folder or untrusted context differs'); + const config = vscode.workspace.getConfiguration(undefined, folder.uri); + const policy = {skills_enabled: config.get('chat.useAgentSkills'), mcp_access: config.get('chat.mcp.access'), mcp_autostart: config.get('chat.mcp.autostart')}; + shape.flag(policy.skills_enabled); shape.text(policy.mcp_access); + if (policy.mcp_autostart !== 'never') throw new Error('restrictive MCP autostart setting differs'); + const extension = vscode.extensions.getExtension('uap-research.workspace-observer'); + if (!extension || extension.id !== 'uap-research.workspace-observer') throw new Error('observer identity missing'); + const activated = await extension.activate(); + if (!activated || !Number.isInteger(activated.extensionMode)) throw new Error('actual activation context mode missing'); + const proposals = shape.texts(extension.packageJSON.enabledApiProposals); + if (JSON.stringify(proposals) !== JSON.stringify(['mcpServerDefinitions','chatPromptFiles'])) throw new Error('observer proposal set differs'); + if (typeof vscode.chat?.getSkills !== 'function') throw new Error('proposed skill API missing or denied'); + const deadline = performance.now() + 15000; + let inventory; + do { + const token = new vscode.CancellationTokenSource(); + let timer; + try { + const remaining = Math.max(1, deadline - performance.now()); + const rawSkills = await Promise.race([vscode.chat.getSkills(token.token), new Promise((_, reject) => {timer = setTimeout(() => {token.cancel(); reject(new Error('skill metadata API deadline'));}, remaining);})]); + inventory = {mcp: shape.mcp(vscode.lm.mcpServerDefinitions), skills: shape.skills(rawSkills)}; + if (Buffer.byteLength(JSON.stringify(inventory), 'utf8') > LIMIT - 16384) throw new Error('metadata inventory exceeds result bound'); + } finally { clearTimeout(timer); token.cancel(); token.dispose(); } + const remaining = deadline - performance.now(); + if (remaining > 0) await sleep(Math.min(500, remaining)); + } while (performance.now() < deadline); + if (vscode.workspace.isTrusted !== false || vscode.workspace.workspaceFolders?.length !== 1 || vscode.workspace.workspaceFolders[0].uri.fsPath !== folder.uri.fsPath || config.get('chat.mcp.autostart') !== 'never') throw new Error('workspace context or restrictive policy changed'); + return {schema_version:1, collected:true, version:shape.text(vscode.version), app_root:shape.text(vscode.env.appRoot), app_host:shape.text(vscode.env.appHost), + observer:{id:extension.id, mode:activated.extensionMode, proposals}, folder:selected, trusted:false, policy, ...inventory, + native_project_qualified:false, native_mcp_initialization_qualified:false, native_runtime_qualified:false}; +} +exports.run = async function () { + try {store(process.env.UAP_VSCODE_WORKSPACE_RESULT, await observe());} + catch (error) {store(process.env.UAP_VSCODE_WORKSPACE_RESULT, {schema_version:1, collected:false, stage_error:String(error?.message || error).slice(0,2048), + native_project_qualified:false, native_mcp_initialization_qualified:false, native_runtime_qualified:false});} + await new Promise(() => {}); +}; + diff --git a/scripts/vscode-workspace-observer/shape.cjs b/scripts/vscode-workspace-observer/shape.cjs new file mode 100644 index 000000000..e4b7c17d3 --- /dev/null +++ b/scripts/vscode-workspace-observer/shape.cjs @@ -0,0 +1,64 @@ +'use strict'; +const MAX_ROWS = 128; +function text(value) { + if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > 8192 || value.includes('\0') || !value.isWellFormed()) throw new Error('metadata string invalid or oversized'); + return value; +} +function flag(value) { + if (typeof value !== 'boolean') throw new Error('metadata boolean required'); + return value; +} +function texts(value) { + if (!Array.isArray(value) || value.length > MAX_ROWS) throw new Error('metadata string array invalid or oversized'); + return value.map(text); +} +function dictionary(value, nullable = false) { + if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length > MAX_ROWS) throw new Error('metadata dictionary required'); + const result = Object.create(null); + for (const [key, entry] of Object.entries(value)) { + result[text(key)] = nullable && (entry === null || (typeof entry === 'number' && Number.isFinite(entry))) ? entry : text(entry); + } + return result; +} +function uri(value) { + if (!value || typeof value !== 'object' || typeof value.toString !== 'function') throw new Error('metadata URI required'); + return {value: text(value.toString()), scheme: text(value.scheme), path: text(value.path), authority: text(value.authority)}; +} +function rows(value, project) { + if (!Array.isArray(value) || value.length > MAX_ROWS) throw new Error('native metadata array invalid or oversized'); + return value.map(project); +} +function mcp(value) { + return rows(value, row => { + if (!row || typeof row !== 'object') throw new Error('MCP metadata row required'); + const result = {label: text(row.label)}; + if (row.version !== undefined) result.version = text(row.version); + if (row.uri !== undefined) { + if (['command','args','env','cwd'].some(key => row[key] !== undefined)) throw new Error('mixed MCP transport fields'); + result.kind = 'http'; + result.uri = uri(row.uri); + result.headers = dictionary(row.headers === undefined ? {} : row.headers); + } else { + if (row.headers !== undefined) throw new Error('mixed MCP transport fields'); + result.kind = 'stdio'; + result.command = text(row.command); + result.args = texts(row.args === undefined ? [] : row.args); + result.env = dictionary(row.env === undefined ? {} : row.env, true); + if (row.cwd !== undefined) result.cwd = uri(row.cwd); + } + return result; + }); +} +function skills(value) { + return rows(value, row => { + if (!row || typeof row !== 'object' || !['local','user','extension','plugin','builtin'].includes(row.source)) throw new Error('unknown skill metadata source'); + const result = {uri: uri(row.uri), name: text(row.name), source: row.source, disableModelInvocation: flag(row.disableModelInvocation)}; + for (const key of ['description','extensionId']) if (row[key] !== undefined) result[key] = text(row[key]); + if (row.sessionTypes !== undefined) result.sessionTypes = texts(row.sessionTypes); + if (row.userInvocable !== undefined) result.userInvocable = flag(row.userInvocable); + if (row.pluginUri !== undefined) result.pluginUri = uri(row.pluginUri); + return result; + }); +} +module.exports = {text, flag, texts, uri, mcp, skills}; + diff --git a/scripts/vscode-workspace-observer/shape.test.cjs b/scripts/vscode-workspace-observer/shape.test.cjs new file mode 100644 index 000000000..bdefe50c4 --- /dev/null +++ b/scripts/vscode-workspace-observer/shape.test.cjs @@ -0,0 +1,100 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const shape = require('./shape.cjs'); +const runner = require('./runner.cjs'); +function uri(p) {return {scheme:'file', authority:'', path:p, toString:()=>'file://'+p};} +function temporary(t) { + const root=fs.mkdtempSync(path.join(os.tmpdir(),'uap-observer-')); + t.after(()=>fs.rmSync(root,{recursive:true,force:true})); + return root; +} +test('native order and duplicates are preserved without name collapsing',()=>{ + const rows=[{uri:uri('/A/SKILL.md'),name:'same',source:'local',description:'A',disableModelInvocation:true}, + {uri:uri('/USER/SKILL.md'),name:'same',source:'user',description:'USER',disableModelInvocation:true}]; + const actual=shape.skills(rows); + assert.deepEqual(actual.map(r=>r.uri.path),['/A/SKILL.md','/USER/SKILL.md']); + assert.deepEqual(actual.map(r=>r.name),['same','same']); + const mcp=shape.mcp([{label:'first',uri:uri('/one'),headers:{}},{label:'first',command:'/inert',args:[],env:{}}]); + assert.equal(mcp.length,2); assert.deepEqual(mcp.map(r=>r.kind),['http','stdio']); +}); +test('missing malformed and oversized shapes fail closed',()=>{ + for(const value of [undefined,null,{},Array(129).fill({})]) assert.throws(()=>shape.skills(value)); + assert.throws(()=>shape.skills([{uri:uri('/A'),name:'x',source:'future',disableModelInvocation:true}])); + assert.throws(()=>shape.skills([{uri:uri('/A'),name:'x',source:'local',disableModelInvocation:'true'}])); + assert.throws(()=>shape.text('\ud800'));assert.throws(()=>shape.text('x'.repeat(8193))); + assert.throws(()=>shape.mcp([{label:'x',command:'/inert',env:{invalid:true}}])); +}); +test('metadata dictionaries do not mutate prototypes',()=>{ + const headers=JSON.parse('{"__proto__":"raw-marker","constructor":"raw-constructor"}'); + const row=shape.mcp([{label:'x',uri:uri('/A'),headers}])[0]; + assert.equal(Object.getPrototypeOf(row.headers),null); + assert.equal(row.headers.__proto__,'raw-marker'); + assert.equal({}.polluted,undefined); +}); +test('extension preserves actual activation context mode',()=>{ + const extension=require('./extension.cjs'); + assert.equal(extension.activate({extensionMode:3}).extensionMode,3); + assert.equal(extension.activate({extensionMode:1}).extensionMode,1); + assert.ok(Object.isFrozen(extension.activate({extensionMode:3}))); + const manifest=require('./package.json'); + assert.deepEqual(manifest.enabledApiProposals,['mcpServerDefinitions','chatPromptFiles']); + assert.equal(manifest.capabilities.untrustedWorkspaces.supported,true); +}); +test('observer remains read-only and deliberately pending',()=>{ + const source=fs.readFileSync(path.join(__dirname,'runner.cjs'),'utf8'); + assert.ok(source.includes('await extension.activate()')); + assert.ok(source.includes('await new Promise(() => {})')); + for(const forbidden of ['startMcpGateway','executeCommand(','sendChatRequest(','updateConfiguration(','isTrusted =']) assert.ok(!source.includes(forbidden)); +}); +test('pending writes and transient hardlinks are not final admission signals',t=>{ + const root=temporary(t),output=path.join(root,'observation.json'),ready=path.join(root,'observation.ready'); + const fileSystem={...fs, + writeFileSync(fd,body){assert.equal(fs.existsSync(output),false);assert.equal(fs.existsSync(ready),false);fs.writeFileSync(fd,body);}, + linkSync(from,to){assert.equal(fs.existsSync(ready),false);fs.linkSync(from,to);assert.equal(fs.statSync(to).nlink,2);}, + unlinkSync(from){fs.unlinkSync(from);assert.equal(fs.statSync(output).nlink,1);assert.equal(fs.existsSync(ready),false);} + }; + runner.storeResult(output,{complete:true},fileSystem); + assert.deepEqual(JSON.parse(fs.readFileSync(output)),{complete:true}); + assert.equal(fs.statSync(ready).size,0); + assert.equal(fs.existsSync(path.join(root,'.observation.pending')),false); +}); +test('foreign protocol leaves and aliases are preserved on collision',t=>{ + for(const name of ['observation.json','.observation.pending','observation.ready']){ + const root=path.join(temporary(t),name.replaceAll('.','_'));fs.mkdirSync(root); + const foreign=path.join(root,name);fs.writeFileSync(foreign,'foreign'); + assert.throws(()=>runner.storeResult(path.join(root,'observation.json'),{complete:true})); + assert.equal(fs.readFileSync(foreign,'utf8'),'foreign'); + } + const root=temporary(t),foreign=path.join(root,'foreign'),pending=path.join(root,'.observation.pending'); + fs.writeFileSync(foreign,'outside');fs.symlinkSync(foreign,pending); + assert.throws(()=>runner.storeResult(path.join(root,'observation.json'),{complete:true})); + assert.equal(fs.readFileSync(foreign,'utf8'),'outside');assert.ok(fs.lstatSync(pending).isSymbolicLink()); +}); + + +test('MCP environment preserves finite numbers and null without widening headers',()=>{ + const raw={label:'inert',command:'/never-run',env:{TEXT:'value',PORT:42,FRACTION:0.5,UNSET:null}}; + const row=shape.mcp([raw])[0]; + assert.equal(row.env.TEXT,'value'); assert.equal(row.env.PORT,42); + assert.equal(row.env.FRACTION,0.5); assert.equal(row.env.UNSET,null); + assert.equal(raw.env.PORT,42); assert.equal(Object.getPrototypeOf(row.env),null); + for(const invalid of [NaN,Infinity,-Infinity,true,{},[]]) assert.throws(()=>shape.mcp([{label:'inert',command:'/never-run',env:{KEY:invalid}}])); + for(const invalid of [42,null,NaN,Infinity]) assert.throws(()=>shape.mcp([{label:'inert',uri:uri('/A'),headers:{KEY:invalid}}])); +}); +test('MCP optional containers default only when undefined and never normalize null',()=>{ + const stdio=shape.mcp([{label:'inert',command:'/never-run'}])[0]; + assert.deepEqual(stdio.args,[]); assert.equal(Object.keys(stdio.env).length,0); + assert.equal(Object.keys(shape.mcp([{label:'inert',uri:uri('/A')}])[0].headers).length,0); + for(const key of ['args','env']) assert.throws(()=>shape.mcp([{label:'inert',command:'/never-run',[key]:null}])); + assert.throws(()=>shape.mcp([{label:'inert',uri:uri('/A'),headers:null}])); +}); +test('MCP HTTP and stdio fields cannot silently collapse to one transport',()=>{ + for(const [key,value] of [['command','/never-run'],['args',[]],['env',{}],['cwd',uri('/inert')]]) { + assert.throws(()=>shape.mcp([{label:'inert',uri:uri('/A'),[key]:value}])); + } + assert.throws(()=>shape.mcp([{label:'inert',command:'/never-run',headers:{}}])); +}); diff --git a/website/source/en/reference/client-compatibility.md b/website/source/en/reference/client-compatibility.md index 9af853d4f..bde9b5bb9 100644 --- a/website/source/en/reference/client-compatibility.md +++ b/website/source/en/reference/client-compatibility.md @@ -260,3 +260,19 @@ has its own installer, package, and transcript digests. It proves no browser tool call, model, OAuth, or runtime claim for the current candidate. New results must identify their own source SHA and client/OS versions before replacing any pending cell. + +## Current source project scope + +**Source only / unreleased.** The historical release and native observations +above retain their original scope. Project installation has a separate per-client +contract: installed ownership, native discovery and MCP/model operation are +reported separately. ChatGPT project scope refuses explicitly; other targets +have their own component, preparation or harness restrictions. + +VS Code project preparation uses `.github/skills` and `.vscode/mcp.json`. +The [pinned VS Code 1.143.1 observation](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034) verified metadata in +separate A/B folders, a fresh A launch and removal, with workspace trust ungranted. +This is metadata evidence; the adapter reports Prepared and installed-file +verification, without claiming MCP startup, tool calls or model execution. + +[Project installation guide](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Current project support and limitations](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/en/use/install.md b/website/source/en/use/install.md index 6d9e68629..a5e7e9151 100644 --- a/website/source/en/use/install.md +++ b/website/source/en/use/install.md @@ -139,3 +139,29 @@ Installed Skills are saved like other packages. They show up in [maintaining installed plugins](./manage). Continue with [maintaining installed plugins](./manage). + +## Project installation in the current source + +**Source only / unreleased.** A compatible source build supports explicit +project selection for packages and Agent Skills: + +```sh +agentplugins add owner/repo --alias review --target cursor \ + --scope project --project-root /path/to/project +agentplugins skills add owner/repository --ui \ + --scope project --project-root /path/to/project +``` + +The directory must already exist. The default remains `user`; project scope +without `--project-root` selects the invocation directory without searching +parents. Browser and terminal selection use the same scope. Only admitted +targets can be selected; the picker can keep unavailable targets visible with a +reason. The selected client can require reload, trust or explicit +session configuration after its files are prepared. + +For VS Code, project scope prepares `.github/skills` and owned `servers` members +in `.vscode/mcp.json`. The result remains Prepared: open the selected local +folder, review workspace/server trust, and inspect its skills and MCP settings. +Remote and multi-root workspaces are outside this adapter's scope. + +[Project installation guide](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Current project support and limitations](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/en/use/manage.md b/website/source/en/use/manage.md index cd2e5698d..2073a286e 100644 --- a/website/source/en/use/manage.md +++ b/website/source/en/use/manage.md @@ -141,3 +141,23 @@ activation, authentication, or runtime use; these are different failure stages. Authoring `doctor ./my-plugin` supplies project evidence only. It cannot replace installer doctor, repair managed state, or complete OAuth. Authors and installers can compare evidence through the [handoff boundary](/en/build/handoff). + +## Maintain a project in the current source + +**Source only / unreleased.** Select the same scope and project on every +maintenance command: + +```sh +agentplugins list --scope project --project-root /path/to/project +agentplugins install --locked --scope project --project-root /path/to/project +agentplugins repair review --target cursor --scope project --project-root /path/to/project +agentplugins deps recover --scope project --project-root /path/to/project +``` + +Commit the portable `uap.toml` and `uap.lock` when you want to share dependency +intent and pins. Keep `.uap/` receipts, recovery state and retained data local; +back up data separately. Removal retains data and the shared content cache. +Repair restores missing owned components and damaged managed packages; changed +native files require review. Recovery must use the original selected project. + +[Project installation guide](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Current project support and limitations](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/es/reference/client-compatibility.md b/website/source/es/reference/client-compatibility.md index c9a43f2ed..edcc78242 100644 --- a/website/source/es/reference/client-compatibility.md +++ b/website/source/es/reference/client-compatibility.md @@ -14,3 +14,21 @@ localeDisposition: "english-fallback" La evidencia detallada, las versiones probadas y sus limitaciones se mantienen en la página canónica en inglés. [Abrir la evidencia en inglés](/en/reference/client-compatibility) + +## Ámbito de proyecto en el código actual + +**Solo código fuente; aún no publicado.** Las observaciones históricas de +versiones y clientes anteriores conservan su ámbito original. La instalación por +proyecto tiene un contrato propio para cada cliente: propiedad de archivos, +detección nativa y funcionamiento de MCP/modelos se verifican por separado. +ChatGPT rechaza explícitamente este ámbito; los demás destinos tienen sus +propias restricciones de componentes, preparación y cliente compatible. + +La preparación de proyectos para VS Code utiliza `.github/skills` y +`.vscode/mcp.json`. La [observación fijada de VS Code 1.143.1](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034) +verificó metadatos en carpetas A/B separadas, tras un nuevo inicio de A y después +de la eliminación, sin conceder confianza al espacio de trabajo. Es evidencia +de metadatos: el adaptador informa Prepared y verifica los archivos instalados; +no demuestra inicio de MCP, llamadas a herramientas ni ejecución del modelo. + +[Guía de instalación por proyecto](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Compatibilidad y límites actuales por proyecto](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/es/use/install.md b/website/source/es/use/install.md index 3e6e904f5..977389191 100644 --- a/website/source/es/use/install.md +++ b/website/source/es/use/install.md @@ -13,3 +13,25 @@ translationRequired: true [Traducción no disponible; consulte la página actual en inglés](/en/use/install). La creación de Agent Plugins está disponible mediante Agent Plugins en `universal-agent-plugins`. [Usar plugins](/es/use/) · [Crear plugins](/es/build/) + +## Instalación por proyecto en el código actual + +**Solo código fuente; aún no publicado.** Una compilación compatible permite +seleccionar explícitamente un proyecto para paquetes y Agent Skills: + +[Los comandos para la compilación desde fuentes están en la sección inglesa](/en/use/install#project-installation-in-the-current-source). + +El directorio debe existir. El ámbito predeterminado sigue siendo `user`; +`project` sin `--project-root` selecciona el directorio de ejecución sin buscar +en sus padres. El navegador y la terminal usan el mismo ámbito. Solo se pueden +seleccionar destinos admitidos; los no disponibles pueden seguir visibles con +el motivo. Tras preparar los archivos, el cliente puede requerir +reinicio, confianza del espacio de trabajo o una configuración explícita de sesión. + +Para VS Code, el ámbito de proyecto prepara `.github/skills` y las entradas +`servers` propias de la instalación en `.vscode/mcp.json`. El resultado sigue +siendo Prepared: abra la carpeta local seleccionada, revise la confianza del +espacio de trabajo y de los servidores, y compruebe sus habilidades y ajustes MCP. +Este adaptador no admite espacios remotos ni espacios con varias carpetas raíz. + +[Guía de instalación por proyecto](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Compatibilidad y límites actuales por proyecto](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/es/use/manage.md b/website/source/es/use/manage.md index 3a92c8bc2..d89435986 100644 --- a/website/source/es/use/manage.md +++ b/website/source/es/use/manage.md @@ -13,3 +13,19 @@ translationRequired: true [Traducción no disponible; consulte la página actual en inglés](/en/use/manage). La creación de Agent Plugins está disponible mediante Agent Plugins en `universal-agent-plugins`. [Usar plugins](/es/use/) · [Crear plugins](/es/build/) + +## Mantener un proyecto en el código actual + +**Solo código fuente; aún no publicado.** Selecciona el mismo ámbito y proyecto +en cada comando de mantenimiento: + +[Los comandos de mantenimiento están en la sección inglesa](/en/use/manage#maintain-a-project-in-the-current-source). + +Guarda los archivos portables `uap.toml` y `uap.lock` en el control de versiones +para compartir dependencias y revisiones. Mantén locales los registros de +propiedad, recuperación y datos de `.uap/`; copia los datos por separado. +La eliminación conserva los datos y la caché compartida. Repair restaura los +componentes ausentes y los paquetes gestionados dañados; los archivos nativos +modificados requieren revisión. Recupera siempre el proyecto seleccionado original. + +[Guía de instalación por proyecto](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Compatibilidad y límites actuales por proyecto](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/fr/reference/client-compatibility.md b/website/source/fr/reference/client-compatibility.md index 201c1fd44..664bdfbc2 100644 --- a/website/source/fr/reference/client-compatibility.md +++ b/website/source/fr/reference/client-compatibility.md @@ -14,3 +14,22 @@ localeDisposition: "english-fallback" Les preuves détaillées, les versions testées et leurs limites sont tenues à jour sur la page canonique en anglais. [Consulter les preuves en anglais](/en/reference/client-compatibility) + +## Portée projet dans le code actuel + +**Code source uniquement ; fonctionnalité non publiée.** Les observations +historiques des versions et clients ci-dessus gardent leur portée initiale. +L’installation par projet a un contrat propre à chaque client : propriété des +fichiers, découverte native et fonctionnement MCP/modèle sont vérifiés séparément. +ChatGPT refuse explicitement cette portée ; les autres cibles ont leurs propres +limites de composants, de préparation et de client compatible. + +La préparation de projets pour VS Code utilise `.github/skills` et +`.vscode/mcp.json`. L’[observation de VS Code 1.143.1 épinglé](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034) +a vérifié les métadonnées dans des dossiers A/B distincts, après un nouveau +lancement de A et après suppression, sans accorder la confiance à l’espace de +travail. Cette preuve concerne les métadonnées : l’adaptateur indique Prepared +et vérifie les fichiers installés, sans attester le démarrage de MCP, +les appels d’outils ou l’exécution du modèle. + +[Guide d’installation par projet](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Compatibilité et limites actuelles par projet](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/fr/use/install.md b/website/source/fr/use/install.md index a94f52d71..7990db2a1 100644 --- a/website/source/fr/use/install.md +++ b/website/source/fr/use/install.md @@ -13,3 +13,26 @@ translationRequired: true [Traduction indisponible ; consultez la page actuelle en anglais](/en/use/install). La création Agent Plugins est disponible via Agent Plugins dans `universal-agent-plugins`. [Utiliser des plugins](/fr/use/) · [Créer des plugins](/fr/build/) + +## Installation par projet dans le code actuel + +**Code source uniquement ; fonctionnalité non publiée.** Une compilation +compatible permet de choisir explicitement un projet pour les paquets et Agent Skills : + +[Les commandes pour la compilation depuis les sources figurent dans la section anglaise](/en/use/install#project-installation-in-the-current-source). + +Le dossier doit exister. La portée par défaut reste `user` ; `project` sans +`--project-root` choisit le dossier courant sans rechercher ses parents. +Le navigateur et le terminal utilisent la même portée. Seules les cibles admises +sont sélectionnables ; les cibles indisponibles peuvent rester visibles avec le +motif. Après la préparation des fichiers, le client peut demander +un redémarrage, la confiance de l’espace de travail ou une configuration de session. + +Pour VS Code, la portée projet prépare `.github/skills` et les entrées +`servers` appartenant à l’installation dans `.vscode/mcp.json`. Le résultat reste +Prepared : ouvrez le dossier local sélectionné, examinez la confiance accordée +à l’espace de travail et aux serveurs, puis vérifiez les compétences et les +réglages MCP. Cet adaptateur ne prend pas en charge les espaces distants ni +les espaces comportant plusieurs dossiers racines. + +[Guide d’installation par projet](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Compatibilité et limites actuelles par projet](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/fr/use/manage.md b/website/source/fr/use/manage.md index 3a8c2c073..32aea9bb6 100644 --- a/website/source/fr/use/manage.md +++ b/website/source/fr/use/manage.md @@ -13,3 +13,19 @@ translationRequired: true [Traduction indisponible ; consultez la page actuelle en anglais](/en/use/manage). La création Agent Plugins est disponible via Agent Plugins dans `universal-agent-plugins`. [Utiliser des plugins](/fr/use/) · [Créer des plugins](/fr/build/) + +## Maintenir un projet dans le code actuel + +**Code source uniquement ; fonctionnalité non publiée.** Choisissez la même +portée et le même projet pour chaque commande de maintenance : + +[Les commandes de maintenance figurent dans la section anglaise](/en/use/manage#maintain-a-project-in-the-current-source). + +Versionnez les fichiers portables `uap.toml` et `uap.lock` pour partager les +dépendances et leurs révisions. Les traces de propriété, la récupération et les +données de `.uap/` restent locales ; sauvegardez les données séparément. +La suppression conserve les données et le cache partagé. Repair restaure les +composants manquants et les paquets gérés endommagés ; les fichiers natifs modifiés +nécessitent une vérification. La récupération concerne le projet initialement choisi. + +[Guide d’installation par projet](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Compatibilité et limites actuelles par projet](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/ru/reference/client-compatibility.md b/website/source/ru/reference/client-compatibility.md index 588f2cb01..77e76d2b4 100644 --- a/website/source/ru/reference/client-compatibility.md +++ b/website/source/ru/reference/client-compatibility.md @@ -36,3 +36,21 @@ localeDisposition: "current-translation" Оба сохранённых набора прошли офлайн-проверку. Верификатор из новой копии исходников `0cfa2b7` также перепроверил сохранённый набор Linux amd64 с идентичностью `fde7298`. Это не новый нативный запуск и не проверка текущего черновика; новое время квалификации из этого не следует. Совпадение хешей архива не означает свежую проверку аттестаций или подтверждение текущего состояния черновика. Исходные журналы и архивы сохраняются приватно до проверки допустимости раскрытия; публичны только метаданные. Это наблюдения сопровождающих проекта: настоящие клиенты использовали сценарные провайдеры моделей на loopback-адресах при доступной сети. Одноразовые профили и явно заданное окружение дочерних процессов не обеспечивают изоляцию ОС или процессов. Результаты не подтверждают работу реальной модели, OAuth/вход, настольные приложения, внешние сервисы, сертификацию поставщика, официальный статус или независимое использование. Валидность пакета, установка, активация, выполнение и OAuth остаются отдельными уровнями доказательств. Эта проверка установщика не квалифицирует выпуск стандартного исполняемого инструмента авторинга (D5). + +## Область проекта в текущем исходном коде + +**Только сборка из исходников; функция ещё не выпущена.** Приведённые выше +исторические проверки релизов и агентов сохраняют свою исходную область. +Установка в проект имеет отдельный контракт для каждого агента: владение +файлами, обнаружение агентом и работа MCP/модели проверяются отдельно. Область +проекта для ChatGPT явно отклоняется; у остальных целей действуют собственные +ограничения компонентов, подготовки и поддерживаемого клиента. + +Подготовка проекта для VS Code использует `.github/skills` и `.vscode/mcp.json`. +[Наблюдение VS Code 1.143.1](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034) подтвердило метаданные в отдельных +папках A и B, после нового запуска A и после удаления; доверие к рабочей области +не предоставлялось. Это свидетельство о метаданных. Адаптер сообщает Prepared +и проверку установленных файлов, не заявляя запуск MCP, вызов инструментов +или выполнение модели. + +[Руководство по установке в проект](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Текущая поддержка проектов и ограничения](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/ru/use/install.md b/website/source/ru/use/install.md index 8f57530de..dc441d24f 100644 --- a/website/source/ru/use/install.md +++ b/website/source/ru/use/install.md @@ -13,3 +13,25 @@ translationRequired: true [Перевод недоступен; читайте актуальную английскую страницу](/en/use/install). Авторинг Agent Plugins доступен через Agent Plugins в `universal-agent-plugins`. [Использовать плагины](/ru/use/) · [Создавать плагины](/ru/build/) + +## Установка в проект в текущем исходном коде + +**Только сборка из исходников; функция ещё не выпущена.** Совместимая +сборка позволяет явно выбрать проект для пакетов и Agent Skills: + +[Команды для сборки из исходников приведены в английском разделе](/en/use/install#project-installation-in-the-current-source). + +Каталог должен существовать. По умолчанию используется `user`; область +`project` без `--project-root` выбирает текущий каталог без поиска в родительских +папках. Браузерный и терминальный выбор используют одну область. Выбирать можно +только доступные цели; недоступные агенты могут оставаться видимыми с объяснением +причины. После подготовки файлов агенту могут потребоваться +перезапуск, доверие к проекту или явный выбор конфигурации сеанса. + +Для VS Code область `project` подготавливает `.github/skills` и принадлежащие +установке записи `servers` в `.vscode/mcp.json`. Результат остаётся Prepared: +откройте выбранную локальную папку, проверьте доверие к проекту и серверам, +затем навыки и настройки MCP. Удалённые рабочие области и рабочие области +с несколькими корневыми папками этим адаптером не поддерживаются. + +[Руководство по установке в проект](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Текущая поддержка проектов и ограничения](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/ru/use/manage.md b/website/source/ru/use/manage.md index 91c9cd992..e9552ef55 100644 --- a/website/source/ru/use/manage.md +++ b/website/source/ru/use/manage.md @@ -13,3 +13,19 @@ translationRequired: true [Перевод недоступен; читайте актуальную английскую страницу](/en/use/manage). Авторинг Agent Plugins доступен через Agent Plugins в `universal-agent-plugins`. [Использовать плагины](/ru/use/) · [Создавать плагины](/ru/build/) + +## Обслуживание проекта в текущем исходном коде + +**Только сборка из исходников; функция ещё не выпущена.** В каждой команде +обслуживания указывайте ту же область и тот же проект: + +[Команды обслуживания приведены в английском разделе](/en/use/manage#maintain-a-project-in-the-current-source). + +Для совместного использования зависимостей сохраняйте переносимые `uap.toml` +и `uap.lock` в системе контроля версий. Записи владения, восстановление и данные +в `.uap/` остаются локальными; данные копируйте отдельно. Удаление сохраняет +данные и общий кэш содержимого. Repair восстанавливает отсутствующие компоненты +и повреждённые управляемые пакеты; изменённые файлы агента требуют проверки. +Восстановление выполняйте для исходного выбранного проекта. + +[Руководство по установке в проект](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [Текущая поддержка проектов и ограничения](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/zh/reference/client-compatibility.md b/website/source/zh/reference/client-compatibility.md index 2aa571f01..cff0e91e4 100644 --- a/website/source/zh/reference/client-compatibility.md +++ b/website/source/zh/reference/client-compatibility.md @@ -14,3 +14,18 @@ localeDisposition: "english-fallback" 详细证据、已测试版本及其限制均在规范英文页面中维护。 [查看英文证据](/en/reference/client-compatibility) + +## 当前源码中的项目范围 + +**仅适用于源码构建,尚未发布。** 上述历史版本和客户端观察结果保持原有范围。 +项目级安装为每个客户端提供单独的支持约定:文件所有权、客户端发现以及 MCP/模型 +运行分别验证。ChatGPT 会明确拒绝项目范围;其他目标各有组件、准备步骤和受支持 +客户端的限制。 + +VS Code 项目准备使用 `.github/skills` 和 `.vscode/mcp.json`。 +[固定版本 VS Code 1.143.1 的观察记录](https://github.com/777genius/universal-agent-plugins/actions/runs/40188310034)验证了独立 A/B 文件夹、 +A 的重新启动及移除后的元数据,且未授予工作区信任。这仅证明元数据行为; +适配器返回 Prepared 并验证已安装文件,不据此声称 MCP 已启动、工具已调用 +或模型已执行。 + +[项目安装指南](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [当前项目支持情况及限制](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/zh/use/install.md b/website/source/zh/use/install.md index 75ef2f7e4..27c4f0631 100644 --- a/website/source/zh/use/install.md +++ b/website/source/zh/use/install.md @@ -13,3 +13,22 @@ translationRequired: true [暂无译文;请阅读当前英文页面](/en/use/install). Agent Plugins 创作已通过 `universal-agent-plugins` 中的 Agent Plugins 发布。 [使用插件](/zh/use/) · [构建插件](/zh/build/) + +## 当前源码中的项目级安装 + +**仅适用于源码构建,尚未发布。** 兼容的源码构建支持为插件包和 Agent Skills +显式选择项目: + +[源码构建的命令示例见英文部分](/en/use/install#project-installation-in-the-current-source)。 + +目录必须已存在。默认范围仍为 `user`;使用 `project` 而未指定 `--project-root` +时,会选择当前执行目录,不搜索父目录。浏览器和终端使用相同的范围;只有受支持 +且可用的目标才能被选择。不可用的目标可以继续显示,并说明原因。文件准备完成后, +客户端可能仍需重启、信任工作区或显式选择会话配置。 + +对于 VS Code,项目作用域会准备 `.github/skills`,并在 `.vscode/mcp.json` +中写入由本次安装拥有的 `servers` 条目。结果仍为 Prepared:请打开所选本地文件夹, +检查工作区和服务器的信任设置,再检查技能与 MCP 配置。此适配器不支持远程工作区 +或多根工作区。 + +[项目安装指南](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [当前项目支持情况及限制](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md) diff --git a/website/source/zh/use/manage.md b/website/source/zh/use/manage.md index d9d04c14a..0c8a5aa1b 100644 --- a/website/source/zh/use/manage.md +++ b/website/source/zh/use/manage.md @@ -13,3 +13,16 @@ translationRequired: true [暂无译文;请阅读当前英文页面](/en/use/manage). Agent Plugins 创作已通过 `universal-agent-plugins` 中的 Agent Plugins 发布。 [使用插件](/zh/use/) · [构建插件](/zh/build/) + +## 在当前源码中维护项目 + +**仅适用于源码构建,尚未发布。** 每条维护命令都应选择相同的范围和项目: + +[维护命令见英文部分](/en/use/manage#maintain-a-project-in-the-current-source)。 + +如需共享依赖及其固定版本,可将可移植的 `uap.toml` 和 `uap.lock` 纳入版本控制。 +`.uap/` 中的所有权记录、恢复状态和保留数据留在本地;数据应单独备份。 +删除安装会保留数据和共享内容缓存。Repair 可恢复缺失的自有组件及损坏的受管包; +被外部修改的客户端文件需要检查。恢复操作必须选择原来的项目。 + +[项目安装指南](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_INSTALLATION.md) · [当前项目支持情况及限制](https://github.com/777genius/universal-agent-plugins/blob/main/docs/PROJECT_CLIENT_SUPPORT.md)