Finish Copilot native runtime on current master (#15701)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip Runner controls provider processes, permissions and task
completion.
> - Copilot needs its pinned distribution, native message identities and
accepted semantic completion receipts.
> - The release must preserve saved-token access and task lifecycle
guarantees.
> - This pull request will keep the current completion and recovery
fixes while adding the Copilot runtime.
> - The benefit is a reviewable part of the Copilot production release.

## Linked Issues or Issue Description

Refs #14434. Supersedes the runtime work in #15560. This is the first PR
in the stack; #15703, #15705 and #15706 follow. Related alternative
adapter work: #15881. This change uses the shared Runner path.

## What Changed

- Pin Copilot CLI 1.0.88 and bind the integrated implementation as
profile45.
- Keep native instruction, permission, receipt, process ownership and
cancellation boundaries.
- Preserve current master completion continuation and remote
verification behavior. Other provider release identities stay unchanged.

## Verification

Final reviewed source
[`4f3f1af807d2ed21adabb04c2f27f4df9c4bc8d5`](https://github.com/paperclipai/paperclip/commit/4f3f1af807d2ed21adabb04c2f27f4df9c4bc8d5)
includes master `cde944ed75648adfb569e86133adc8b118b55dd6`. The combined
four-part PR tree matches it except for the CI-owned lockfile. Every
prior public PR head remains an ancestor. Released master migrations
remain unchanged; Copilot adds migration361. No unrelated provider is
enabled.

Copilot CLI1.0.88 and profile47 retain digest
`sha256:55044aa235e88b3fe36ec8a556ee1e3382590870aab907e4462ef7d64af49cea`.
The pending qualification marker is removed. The [qualification
record](https://github.com/paperclipai/paperclip/blob/4f3f1af807d2ed21adabb04c2f27f4df9c4bc8d5/doc/plans/2026-10-20-copilot-qualification-v47.json)
reports **20/20 Product cells,154 assertions;7/7 protocol cases,46
assertions** against frozen installed application
`c2616370800f5f6df45246b0890ba35a7cd2faab`. Both environments use the
exact authenticated model `gpt-5.6-luna`. Every passing Product case
confirms cleanup. Automatic behavior retries are disabled. Original
failed attempts remain recorded.

The ordinary installed remote journey passed saved-token selection,
authenticated model discovery, normal UI save/reopen, metadata-only
Test, task completion, accepted receipt and an exact19-byte browser
download. An open model catalog caused no new discovery requests
during117seconds. Actual installed runtime smokes passed on macOS ARM64,
macOS x64 under Rosetta and native Linux x64 on Daytona. Raw token
browser entry was not exercised; the encrypted connection was created
through the normal API.

The original Daytona file attempt timed out during delayed semantic
dispatch. Its precise stalled layer remains unresolved. Independent
ordinary delivery and the other19 cells passed before one instrumented
recheck passed8/8 with unchanged source,prompt,assertions and deadlines.
No causal latency repair is claimed.

After current-master reconciliation,129 connection tests,59 runner
tests,72 shared eval tests and88 recovery/workspace tests pass.
Migration generation/safety, unchanged profile identity and UI token
gates pass. Prior required CI was green. Fresh exact-head required CI
and E2E are green for all four updated heads. Each has a fresh
Greptile5/5 review and zero unresolved review threads. The [final
normal-package cloud
build](https://github.com/paperclipai/paperclip/actions/runs/39414239704)
is running. Recursive typecheck,full CI test shards,build and all8
browser shards pass. These checks cover the final source; no additional
local full-suite pass is claimed.

The [frozen image
build](https://github.com/paperclipai/paperclip/actions/runs/39334401638)
and [original normal package
build](https://github.com/paperclipai/paperclip/actions/runs/39334407471)
passed. The exact image is published and independently retrieved
anonymously. No separate full-image scan is claimed. All18 original
normal archives were scanned with zero selected credentials. Docker
builds run in the cloud. Provider USD remains unknown and
GitHub-attributed.

**Qualification is complete on the recorded frozen build. Normal
protected merge, automatic master canary publication and final
shipped-package local/Daytona canaries remain. No production deployment
is claimed.**

## Risks

Shared Runner behavior can affect active tasks. Exact profile admission
rejects stale runtime identities. The final campaign and installed
canaries remain release gates.

## Model Used

OpenAI Codex, based on GPT-6. The exact serving model ID and context
window are not exposed in this task. Used for code changes, review and
tool-assisted verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-20 06:31:47 -05:00
1 parent fc0d390d98
commit d855e550ad
96 files changed
+2979 -331

No files matched your search

+5
View File
@@ -72,6 +72,11 @@ packages/paperclip-runner/test-fixtures/pi-acp/**
packages/grok-acp/bin
# The Copilot build-time identity gate reads these sanitized source declarations.
!packages/paperclip-runner/test/fixtures/copilot-profile-v*-identity.json
# Images build their pinned Copilot assets instead of copying host binaries.
/packages/paperclip-runner/provider-assets/
/server/provider-assets/
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v23-claude-qualification-identity.json
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v26-identity.json
!packages/paperclip-runner/test/fixtures/copilot-profile-v19-claude-qualification-identity.json
+4 -4
View File
@@ -321,7 +321,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
@@ -834,7 +834,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
@@ -1051,7 +1051,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
@@ -1409,7 +1409,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
+2 -2
View File
@@ -471,7 +471,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Build the Runner release crate for its cache
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha && steps.runner_release_crate.outputs.cache-hit != 'true' }}
@@ -492,7 +492,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
build:
if: ${{ !inputs.runner_binaries_only }}
+3 -4
View File
@@ -32,14 +32,13 @@
"requiresProviderPolicy": true
},
"copilot": {
"agentProfileVersion": 24,
"agentProfileVersion": 47,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:87e275270e50b42290d10ecec9dfce86ffca2b7a7519cb60c584004adfc86437",
"requiresProviderPolicy": true,
"qualificationStatus": "pending"
"commandDigest": "sha256:55044aa235e88b3fe36ec8a556ee1e3382590870aab907e4462ef7d64af49cea",
"requiresProviderPolicy": true
},
"claude": {
"agentProfileVersion": 1,
+10 -5
View File
@@ -11,7 +11,8 @@
"paperclip-runner-eval-session": "./dist/cli/eval-session.js",
"paperclip-runner-codex-proxy": "./dist/cli/codex-app-server-unix-proxy.js",
"paperclip-runner-opencode-proxy": "./dist/cli/opencode-app-server-proxy.js",
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js"
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js",
"paperclip-runner-copilot-metadata-probe": "./dist/cli/copilot-metadata-probe.js"
},
"exports": {
".": {
@@ -53,7 +54,10 @@
"dist-issue-thread",
"protocol",
"styles.css",
"README.md"
"README.md",
"scripts/install-copilot-assets.mjs",
"scripts/materialize-copilot-binary.mjs",
"scripts/copilot-inner-distribution.mjs"
],
"publishConfig": {
"executableFiles": [
@@ -64,7 +68,7 @@
"sideEffects": false,
"scripts": {
"build": "node scripts/record-build-inputs.mjs --begin && pnpm run check:protocol-manifest && pnpm run build:typescript && pnpm run check:capability-contract && pnpm run check:capability-inventory && pnpm run check:protocol-coverage && node scripts/generate-semantic-contracts.mjs --check && node scripts/check-runner-workflow-traceability.mjs && pnpm run build:binary && node scripts/generate-replay-goldens.mjs --check && node scripts/generate-semantic-action-catalog.mjs --check && node scripts/record-build-inputs.mjs --finish",
"build:typescript": "pnpm run check:acpx-profiles && pnpm run ensure:eval-build-deps && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json && node scripts/build-verified-provider-entrypoints.mjs",
"build:typescript": "pnpm run check:acpx-profiles && pnpm run ensure:eval-build-deps && pnpm run check:protocol-types && node ./node_modules/typescript/bin/tsc --version && node ./node_modules/typescript/bin/tsc -p tsconfig.json && node ./node_modules/typescript/bin/tsc -p tsconfig.surfaces.json && node scripts/build-verified-provider-entrypoints.mjs && node scripts/install-copilot-assets.mjs",
"build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace --bins",
"build:binary": "cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd && node scripts/stage-runner-binary.mjs",
"stage:release-binaries": "node scripts/stage-release-runner-binaries.mjs",
@@ -81,7 +85,7 @@
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust",
"test:typescript": "pnpm run test:typescript:prep && vitest run",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node ../../scripts/ci-profile/node-test.mjs test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/provider-pack-runtime-tree.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-runtime-dependency-roots.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-inputs.test.mjs",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node ../../scripts/ci-profile/node-test.mjs test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/provider-pack-runtime-tree.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-runtime-dependency-roots.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-inputs.test.mjs scripts/provider-pack-executable-shims.test.mjs && node --test scripts/build-copilot-distribution.test.mjs scripts/copilot-inner-distribution.test.mjs scripts/copilot-smoke-resources.test.mjs scripts/discover-copilot-acp.test.mjs scripts/materialize-copilot-binary.test.mjs scripts/install-copilot-assets.test.mjs scripts/probe-copilot-acp.test.mjs scripts/qualify-copilot-acp.test.mjs test/acpx-message-boundaries-package-contract.test.mjs",
"test:typescript:vitest": "node ./scripts/run-pr-vitest-lane.mjs",
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
@@ -169,7 +173,8 @@
"verify:rootless": "bash scripts/verify-rootless-linux.sh",
"test:opencode:qualification": "PAPERCLIP_OPENCODE_QUALIFY=1 node --test scripts/qualify-opencode-runtime.test.mjs",
"generate:acpx-profiles": "node scripts/generate-acpx-profiles.mjs",
"check:acpx-profiles": "node scripts/generate-acpx-profiles.mjs --check"
"check:acpx-profiles": "node scripts/generate-acpx-profiles.mjs --check",
"postinstall": "node scripts/install-copilot-assets.mjs"
},
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.73.0",
@@ -2015,6 +2015,12 @@ impl AcpxCommandExecutor {
let Some(events) = events else { break };
let mut provider_turn_settled = false;
for event in events {
let provider_process_lost = matches!(
&event,
crate::acpx_provider_state::AcpxProviderStateEvent::TurnTerminal {
error: Some(error), ..
} if error.get("code").and_then(Value::as_str) == Some("ACPX_PROVIDER_PROCESS_LOST")
);
let normalized = project_acpx_state_event(&self.context, &event)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
let terminal = normalized.iter().find_map(|event| {
@@ -2024,7 +2030,10 @@ impl AcpxCommandExecutor {
)
.then(|| event.event_type.clone())
});
if terminal.is_some() {
// A failed provider cannot answer session controls. Its
// terminal receipt must remain publishable without another
// protocol request, and must revoke future warm admission.
if terminal.is_some() && !provider_process_lost {
let snapshot = self.goal_control("session.goal.get", &json!({}))?;
let state = self.state.as_mut().expect("ACPX session has durable state");
for (event_type, priority, payload) in snapshot.events {
@@ -2065,10 +2074,12 @@ impl AcpxCommandExecutor {
}
if let Some(event_type) = terminal {
state.active_turn_id = None;
state.lifecycle = if self
.session
.as_ref()
.is_some_and(|session| session.runtime_retired())
state.provider_exit_unconfirmed |= provider_process_lost;
state.lifecycle = if provider_process_lost
|| self
.session
.as_ref()
.is_some_and(|session| session.runtime_retired())
{
"closed"
} else {
@@ -2078,11 +2089,12 @@ impl AcpxCommandExecutor {
provider_turn_settled = true;
// An ACP goal has session lifetime, not prompt lifetime.
// Out-of-prompt goal updates remain observable after quiescence.
if state
.goal_projection
.pointer("/goal/status")
.and_then(Value::as_str)
== Some("active")
if !provider_process_lost
&& state
.goal_projection
.pointer("/goal/status")
.and_then(Value::as_str)
== Some("active")
{
continue;
}
@@ -4071,6 +4083,279 @@ mod tests {
fs::remove_dir_all(directory).unwrap();
}
#[cfg(unix)]
#[test]
fn copilot_attachment_refreshes_current_grant_without_changing_profile() {
let directory = temporary_directory("cross-run-attach");
let runtime = directory.join("runtime");
let workspace = directory.join("workspace");
fs::create_dir_all(&runtime).unwrap();
fs::create_dir_all(&workspace).unwrap();
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap();
fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap();
let marker = directory.join("provider-started");
let command = directory.join("sidecar");
write_artifact(
&command,
format!("#!/bin/sh\ntouch '{}'\n", marker.display()).as_bytes(),
true,
);
let launch_profile = AcpxLaunchProfile {
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: Vec::new(),
artifacts: vec![artifact(&command)],
};
let mut descriptor_value = descriptor("copilot");
// The fixture factory uses the generated current release identity.
// Overwriting it with a historical digest prevents restore before the
// instruction-refresh behavior under test can run.
descriptor_value["providerPolicy"] = json!({ "readOnly": true });
descriptor_value["sidecarCommand"] = json!(command);
descriptor_value["runtimeContext"] = json!({ "instructions": { "digest": "stable" }, "mcp": { "digest": "before" }, "aggregateDigest": "before" });
descriptor_value["sidecarArgs"] = json!([]);
descriptor_value["runtimeDirectory"] = json!(runtime);
descriptor_value["cwd"] = json!(workspace);
let prior_root = directory.join("old-registered-copy");
let current_root = directory.join("new-registered-copy");
fs::create_dir_all(&prior_root).unwrap();
fs::create_dir_all(&current_root).unwrap();
descriptor_value["instructions"] = json!(format!(
"Current AGENT_HOME: {}. Custom entry.",
prior_root.display()
));
descriptor_value["runtimeContext"] = json!({
"aggregateDigest": "a".repeat(64),
"prompt": {"revision": "pinned", "digest": "b".repeat(64)},
"instructions": {
"entryPath": "AGENTS.md",
"bundle": {"digest": "c".repeat(64), "rootPath": "/old-bundle"},
"workingCopy": {"kind": "agent_files", "entryPath": "AGENTS.md", "rootPath": prior_root},
},
"skills": [{"key": "skill-1", "bundle": {"digest": "d".repeat(64), "rootPath": "/old-skill"}}],
"mcp": {"assignmentSetId": "assignment-1", "digest": "e".repeat(64), "bindingId": "old-run-binding"},
"futurePolicy": {"companyId": "company-1"},
});
let original_descriptor: AcpxProviderDescriptor =
serde_json::from_value(descriptor_value.clone()).unwrap();
let identity = AcpxProviderSessionIdentity {
kind: "acpx".to_owned(),
normalized_session_id: "session-1".to_owned(),
acpx_record_id: "record-1".to_owned(),
backend_session_id: "backend-1".to_owned(),
agent_session_id: "agent-1".to_owned(),
profile_digest: original_descriptor.command_digest.clone(),
workspace_digest: format!("sha256:{}", "a".repeat(64)),
requested_model: original_descriptor.model.clone(),
effective_model: original_descriptor.model.clone(),
permission_mode: Some(original_descriptor.permission_mode),
mode: original_descriptor.mode.clone(),
pi_thinking_level: None,
claude_effort: None,
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
};
let operations = Vec::new();
let tool_set = AuthorizedToolSet {
schema: TOOL_SET_SCHEMA.to_owned(),
schema_version: 1,
catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(),
operations,
};
let launch_profile_digest = launch_profile.canonical_digest().unwrap();
let mut state = AcpxDurableState::new(original_descriptor, tool_set, launch_profile_digest);
state.lifecycle = "suspended".to_owned();
state.identity = Some(identity);
let original_config = test_config(&directory, Some(launch_profile.clone()));
let mut original = AcpxCommandExecutor::with_runner_config(&directory, &original_config);
original.state = Some(state);
let settled = original.state.clone().unwrap();
let ready = original.snapshot().unwrap().result;
assert_eq!(ready["warmAttachReady"], true);
assert_eq!(ready["warmAttachBlockers"], json!([]));
let blocked_states: [(&str, fn(&mut AcpxDurableState)); 4] = [
("durable_closed", |state| {
state.lifecycle = "closed".to_owned()
}),
("provider_exit_unconfirmed", |state| {
state.lifecycle = "prepared".to_owned();
state.provider_exit_unconfirmed = true;
}),
("provider_identity_unavailable", |state| {
state.lifecycle = "prepared".to_owned();
state.identity = None;
}),
("durable_active_turn", |state| {
state.lifecycle = "turn_active".to_owned();
state.active_turn_id = Some("turn-1".to_owned());
}),
];
for (blocker, mutate) in blocked_states {
original.state = Some(settled.clone());
mutate(original.state.as_mut().unwrap());
let snapshot = original.snapshot().unwrap().result;
assert_eq!(snapshot["warmAttachReady"], false, "{blocker}");
assert_eq!(snapshot["warmAttachBlockers"], json!([blocker]));
}
// A readiness probe must retain the old authority's audit events until
// the durable runner commits and acknowledges them, including recovery
// notices that run.attach itself is allowed to consume.
for event_type in ["session.resumed", "harness.diagnostic"] {
original.state = Some(settled.clone());
original
.state
.as_mut()
.unwrap()
.push(NormalizedProviderEvent {
event_type: event_type.to_owned(),
priority: EventPriority::P0,
payload: json!({}),
})
.unwrap();
let snapshot = original.snapshot().unwrap().result;
assert_eq!(snapshot["warmAttachReady"], false);
assert_eq!(
snapshot["warmAttachBlockers"],
json!(["durable_pending_events"])
);
assert_eq!(original.retained_events().unwrap().len(), 1);
original.acknowledge_events(1).unwrap();
assert_eq!(original.snapshot().unwrap().result["warmAttachReady"], true);
}
original.state = Some(settled);
assert!(!marker.exists(), "readiness must not start a provider");
original.save_state().unwrap();
let mut wrong_session_config = original_config.clone();
wrong_session_config.run_id = "run-2".to_owned();
wrong_session_config.normalized_session_id = "session-2".to_owned();
let mut wrong_session =
AcpxCommandExecutor::with_runner_config(&directory, &wrong_session_config);
assert!(wrong_session.restore().is_err());
let mut attached_config = original_config.clone();
attached_config.run_id = "run-2".to_owned();
let mut attached = AcpxCommandExecutor::with_runner_config(&directory, &attached_config);
attached.restore().unwrap();
assert!(!marker.exists());
let non_attach_error = attached
.execute(&Command {
schema: "paperclip.prp.command.v1".to_owned(),
command_id: "command-before-attach".to_owned(),
controller_seq: 1,
command_type: "session.snapshot".to_owned(),
issued_at: "2026-09-01T00:00:00.000Z".to_owned(),
deadline_at: None,
precondition: None,
payload: json!({}),
})
.unwrap_err();
assert!(non_attach_error
.to_string()
.contains("requires run.attach before commands from a new run"));
descriptor_value["runId"] = json!("run-2");
fs::remove_dir_all(&prior_root).unwrap();
descriptor_value["instructions"] = json!(format!(
"Current AGENT_HOME: {}. Fresh custom entry.",
current_root.display()
));
descriptor_value["runtimeContext"]["instructions"]["workingCopy"]["rootPath"] =
json!(current_root);
descriptor_value["runtimeContext"]["instructions"]["bundle"]["rootPath"] =
json!("/new-bundle");
descriptor_value["runtimeContext"]["skills"][0]["bundle"]["rootPath"] = json!("/new-skill");
descriptor_value["runtimeContext"]["mcp"]["bindingId"] = json!("new-run-binding");
attached
.attach_run(&json!({"provider": descriptor_value}))
.unwrap();
assert_eq!(attached.state.as_ref().unwrap().descriptor.run_id, "run-2");
assert!(!marker.exists());
let refreshed = &attached.state.as_ref().unwrap().descriptor;
assert_eq!(
refreshed.runtime_context,
descriptor_value["runtimeContext"]
);
assert_eq!(refreshed.instructions, descriptor_value["instructions"]);
let session_config = refreshed
.session_config(
attached.state.as_ref().unwrap().tool_set.clone(),
attached.state.as_ref().unwrap().identity.clone(),
Some(&launch_profile),
)
.unwrap();
assert_eq!(
session_config.runtime_context,
descriptor_value["runtimeContext"]
);
assert_eq!(
session_config.system_instructions,
descriptor_value["instructions"]
);
// Both persistence and the sidecar launch config receive the new grant.
let persisted: AcpxDurableState =
serde_json::from_slice(&fs::read(attached.state_path()).unwrap()).unwrap();
assert_eq!(
persisted.descriptor.runtime_context,
descriptor_value["runtimeContext"]
);
let mut same_run_mutation = descriptor_value.clone();
same_run_mutation["instructions"] = json!("A different grant in the same run");
assert!(attached
.attach_run(&json!({"provider": same_run_mutation}))
.unwrap_err()
.to_string()
.contains("new authenticated run"));
// In-place warm handoff executes under the old authority. Only the
// authenticated next-authority boundary may admit the new descriptor;
// event correlation stays on run-1 until durable activation completes.
assert!(original
.attach_run(&json!({"provider": descriptor_value}))
.is_err());
let warm_payload = json!({
"provider": descriptor_value,
"paperclipNextAuthority": {
"identity": {
"runnerInstanceId": original_config.runner_instance_id,
"environmentLeaseId": original_config.environment_lease_id,
"runId": "run-2",
"normalizedSessionId": original_config.normalized_session_id,
"turnId": "turn-2",
"itemId": "item-2",
},
"connection": {"mode": "connect", "connectUrl": original_config.connect_url},
},
});
let mut wrong_run = warm_payload.clone();
wrong_run["paperclipNextAuthority"]["identity"]["runId"] = json!("run-3");
assert!(original.attach_run(&wrong_run).is_err());
let mut wrong_session = warm_payload.clone();
wrong_session["paperclipNextAuthority"]["identity"]["normalizedSessionId"] =
json!("other-session");
assert!(original.attach_run(&wrong_session).is_err());
let mut changed_profile = warm_payload.clone();
changed_profile["provider"]["cwd"] = json!("/different-workspace");
assert!(original.attach_run(&changed_profile).is_err());
let mut refreshed = warm_payload.clone();
refreshed["provider"]["runtimeContext"]["mcp"] = json!({ "digest": "after" });
refreshed["provider"]["runtimeContext"]["aggregateDigest"] = json!("after");
let mut changed_context = refreshed.clone();
changed_context["provider"]["runtimeContext"]["instructions"] =
json!({ "digest": "changed" });
assert!(original.attach_run(&changed_context).is_err());
original.attach_run(&refreshed).unwrap();
assert_eq!(
original.state.as_ref().unwrap().descriptor.runtime_context["mcp"]["digest"],
"after"
);
assert_eq!(original.state.as_ref().unwrap().descriptor.run_id, "run-2");
assert_eq!(original.context.run_id, "run-1");
original.rotate_authority(&attached_config);
assert_eq!(original.context.run_id, "run-2");
assert!(!marker.exists());
fs::remove_dir_all(directory).unwrap();
}
#[cfg(unix)]
#[test]
fn active_turn_recovery_closes_without_starting_the_provider() {
@@ -11,7 +11,7 @@ pub(crate) enum RunAttachmentPolicy {
pub(crate) fn run_attachment_policy(agent: &str) -> RunAttachmentPolicy {
match agent {
"cursor" | "pi" => RunAttachmentPolicy::AuthenticatedRunGrants,
"cursor" | "copilot" | "pi" => RunAttachmentPolicy::AuthenticatedRunGrants,
"claude" => RunAttachmentPolicy::AuthenticatedAssetPaths,
_ => RunAttachmentPolicy::ImmutableInstructions,
}
@@ -27,6 +27,10 @@ mod tests {
run_attachment_policy("cursor"),
RunAttachmentPolicy::AuthenticatedRunGrants
);
assert_eq!(
run_attachment_policy("copilot"),
RunAttachmentPolicy::AuthenticatedRunGrants
);
assert_eq!(
run_attachment_policy("pi"),
RunAttachmentPolicy::AuthenticatedRunGrants
@@ -35,7 +39,7 @@ mod tests {
run_attachment_policy("claude"),
RunAttachmentPolicy::AuthenticatedAssetPaths
);
for agent in ["codex", "grok", "copilot", "unknown"] {
for agent in ["codex", "grok", "unknown"] {
assert_eq!(
run_attachment_policy(agent),
RunAttachmentPolicy::ImmutableInstructions
@@ -908,6 +908,7 @@ fn response_error_classification(error: &ResponseError) -> &'static str {
"ACPX_TOOL_CALL_STALE" => return "provider_tool_call_retired",
"AUTH_REQUIRED" => return "authentication_required",
"COPILOT_AUTH_REQUIRED" => return "authentication_required",
"COPILOT_INSTALLATION_INVALID" => return "agent_installation_invalid",
"COPILOT_POLICY_VIOLATION" => return "copilot_policy_violation",
"COPILOT_DETACHED_WORK_UNSUPPORTED" => return "copilot_detached_work_unsupported",
"COPILOT_ENTITLEMENT_DENIED" => return "provider_entitlement_denied",
@@ -64,6 +64,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut stdout = io::stdout().lock();
let mut next_sequence = 1_u64;
let mut goal = Value::Null;
let mut provider_lost = false;
let mut session_identity = Value::Null;
for line in stdin.lock().lines() {
let request: Value = serde_json::from_str(&line?)?;
@@ -75,6 +76,16 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.get("command")
.and_then(Value::as_str)
.ok_or("request command is missing")?;
if mode == "turns-provider-death" && provider_lost && command == "session.goal.get" {
write_json(
&mut stdout,
&json!({
"protocolVersion":GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION,
"id":id,"ok":false,"error":{"message":"provider has exited"}
}),
)?;
continue;
}
if let Some(journal) = admission_journal.as_mut() {
write_json(journal, &json!({"request":request}))?;
let response = if command == "tool.resolve" {
@@ -315,6 +326,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
| "turns-mismatched-reserved-result-terminal"
| "turns-unauthorized-tool"
| "turns-permission"
| "turns-provider-death"
| "permissions-forged-origin"
| "turns-retired"
| "turns-retired-terminal-first"
@@ -381,6 +393,27 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.map(|suffix| format!("call-{suffix}"))
.unwrap_or_else(|| "call-1".to_owned())
};
if command == "turn.start" && mode == "turns-provider-death" {
write_turn_event(
&mut stdout,
next_sequence,
"runtime.permission_requested",
"run-1",
turn_id,
json!({"requestId":"dead-input","kind":"edit","title":"Write the file?","choices":[{"key":"accept","label":"Allow once"},{"key":"cancel","label":"Deny"}]}),
)?;
next_sequence += 1;
provider_lost = true;
write_turn_event(
&mut stdout,
next_sequence,
"runtime.turn_terminal",
"run-1",
turn_id,
json!({"status":"failed","error":{"code":"ACPX_PROVIDER_PROCESS_LOST","message":"provider has exited","retryable":false}}),
)?;
next_sequence += 1;
}
if command == "turn.start" && matches!(mode, "turns" | "turns-wrong-scope") {
write_turn_event(
&mut stdout,
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:87e275270e50b42290d10ecec9dfce86ffca2b7a7519cb60c584004adfc86437",
"sha256:55044aa235e88b3fe36ec8a556ee1e3382590870aab907e4462ef7d64af49cea",
requires_provider_policy: true,
},
"claude" => AcpxReleaseProfile {
@@ -1103,6 +1103,111 @@ fn routes_reserved_completion_feedback_with_the_full_controller_catalog() {
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_provider() {
let directory = temporary_directory("acpx-provider-death");
let mode = "turns-provider-death";
let mut config = acpx_config(&directory, mode);
let mut payload = prepare_payload_with_mode(&directory, "codex", mode);
// Keep the death/expiry assertions on the active immutable profile. A
// historical digest fails admission before a provider can die.
let identity: Value =
serde_json::from_str(include_str!("../../../../acpx-profiles.json")).unwrap();
let digest = identity["profiles"]["copilot"]["commandDigest"]
.as_str()
.unwrap();
*config
.acpx_launch_profile
.as_mut()
.unwrap()
.args
.last_mut()
.unwrap() = digest.to_owned();
let provider = &mut payload["provider"];
provider["agent"] = json!("copilot");
provider["model"] = json!("gpt-5.6-luna");
provider["permissionMode"] = json!("approve-paperclip");
provider["agentServerPackage"] = json!("@github/copilot");
provider["agentServerVersion"] = json!("1.0.88");
provider["agentRuntimePackage"] = Value::Null;
provider["agentRuntimeVersion"] = Value::Null;
provider["providerPolicy"] = json!({"readOnly":false});
provider["commandDigest"] = json!(digest);
provider["sidecarArgs"][3] = json!(digest);
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
executor
.execute(&command(1, "run.prepare", payload))
.unwrap();
executor
.execute(&command(2, "session.open", json!({})))
.unwrap();
executor
.execute(&command(
3,
"turn.start",
json!({"text":"Write", "turnId":"provider-turn-dead"}),
))
.unwrap();
let mut events = Vec::new();
for _ in 0..20 {
let batch = executor.poll_events().unwrap();
executor.acknowledge_events(batch.len()).unwrap();
events.extend(batch);
if events
.iter()
.any(|event| event.event_type == "run.terminal")
{
break;
}
std::thread::sleep(Duration::from_millis(5));
}
let expired = events
.iter()
.position(|event| event.event_type == "runtime_request.expired")
.unwrap_or_else(|| panic!("provider death did not expire input: {events:?}"));
let failed = events
.iter()
.position(|event| event.event_type == "turn.failed")
.unwrap();
assert!(expired < failed);
assert_eq!(events[expired].payload["reason"], "provider_process_lost");
assert_eq!(events[expired].payload["replayAllowed"], false);
assert_eq!(
events[failed].payload["providerTurnId"],
"provider-turn-dead"
);
assert_eq!(
events[failed].payload["error"]["code"],
"ACPX_PROVIDER_PROCESS_LOST"
);
assert_eq!(
events
.iter()
.filter(|event| event.event_type == "turn.failed")
.count(),
1
);
let snapshot = executor
.execute(&command(4, "session.snapshot", json!({})))
.unwrap()
.result;
assert_eq!(snapshot["status"], "closed");
assert_eq!(snapshot["activeProviderTurnId"], Value::Null);
assert_eq!(snapshot["warmAttachReady"], false);
assert!(executor
.execute(&command(
5,
"turn.start",
json!({"text":"Replay", "turnId":"replay-turn"})
))
.is_err());
executor
.execute(&command(6, "session.close", json!({})))
.unwrap();
shutdown_recovered_acpx_fixture(&mut executor, &directory);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn resumes_an_idle_acpx_session_in_a_cold_replacement_runner() {
let directory = temporary_directory("acpx-cold-idle-recovery");
@@ -154,7 +154,15 @@ try {
mkdirSync(dirname(stableNodeCommand), { recursive: true, mode: 0o755 });
copyFileSync(process.execPath, stableNodeCommand);
chmodSync(stableNodeCommand, 0o755);
const relocatedNode = spawnSync(stableNodeCommand, ["--version"], {
// A freshly copied x64 Mach-O can stall when launched directly from a
// translated Node process. Select Rosetta explicitly for this build probe;
// native Intel and every other platform still execute the copied bytes directly.
const translated = process.platform === "darwin" && process.arch === "x64"
&& spawnSync("/usr/sbin/sysctl", ["-n", "sysctl.proc_translated"], {
encoding: "utf8", timeout: 1_000,
}).stdout?.trim() === "1";
const relocatedNode = spawnSync(translated ? "/usr/bin/arch" : stableNodeCommand,
translated ? ["-x86_64", stableNodeCommand, "--version"] : ["--version"], {
cwd: temporaryRoot, env: { PATH: "/usr/bin:/bin" }, encoding: "utf8", timeout: buildNodeStartupTimeout(),
});
if (relocatedNode.status !== 0 || relocatedNode.stdout.trim() !== `v${process.versions.node}`) {
@@ -169,7 +177,7 @@ try {
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.profileDigest)
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.closureDigest)) throw new Error("Candidate builder omitted its pinned identity");
candidateProviders[provider] = { version: metadata.version, profileDigest: metadata.profileDigest,
closureDigest: metadata.closureDigest, qualification: (provider === "cursor" || provider === "pi") ? "qualified" : "pending", path: assetPath,
closureDigest: metadata.closureDigest, qualification: ["pi", "cursor", "copilot"].includes(provider) ? "qualified" : "pending", path: assetPath,
sha256: sha256Tree(join(temporaryRoot, assetPath)) };
}
@@ -200,6 +208,7 @@ try {
writePortableExecutableShim(temporaryRoot, "opencode", "opencode-ai/bin/opencode.exe");
writePortableCopilotShims(temporaryRoot);
writePortableNodeShim("acpx", "acpx/dist/cli.js");
writePortableNodeShim("paperclip-runner-copilot-metadata-probe", "../dist/cli/copilot-metadata-probe.js");
writePortableNodeShim(
"claude-agent-acp",
"@agentclientprotocol/claude-agent-acp/dist/index.js",
@@ -6,14 +6,14 @@ const CANDIDATES = new Set(["cursor", "copilot", "pi"]);
/** Only materialized providers enter the serialized manifest and its digest. */
export function providerPackManifestFields(providers, candidates) {
return {
...(["pi", "cursor"].some(provider => providers[provider]) ? { providers: Object.fromEntries(["pi", "cursor"].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", ...candidates].some(provider => providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", "cursor", "copilot"].some(provider => providers[provider]) ? { providers: Object.fromEntries(["pi", "cursor", "copilot"].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", ...candidates].some(provider => provider !== "copilot" && providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => provider !== "copilot" && providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
};
}
export function providerPackProviders(platform, architecture, candidates) {
const nativeSupported = ["darwin-arm64", "darwin-x64", "linux-x64"].includes(`${platform}-${architecture}`);
return [...new Set([...(nativeSupported ? ["pi", "cursor"] : []), ...candidates])];
return [...new Set([...(nativeSupported ? ["pi", "cursor", "copilot"] : []), ...candidates])];
}
export function parseProviderPackArguments(args) {
@@ -1,6 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import { parseProviderPackArguments, materializeCandidateProviderPack, providerPackProviders, providerPackManifestFields } from "./candidate-provider-pack.mjs";
import { createHash } from "node:crypto";
async function withCopilotTestPlatform(run) {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
@@ -16,27 +17,42 @@ async function withCopilotTestPlatform(run) {
}
}
// Matches the canonical manifest hashing used by the builder and admission.
function digest(value) {
const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`
: JSON.stringify(value);
return createHash("sha256").update(canonical(value)).digest("hex");
}
test("provider manifest digest survives disk JSON on supported and unsupported targets", () => {
const cursor = { version: "pinned", profileDigest: "sha256:profile", closureDigest: "sha256:closure" };
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"], ["linux", "arm64"], ["win32", "x64"]]) {
const selected = providerPackProviders(platform, architecture, []);
const payload = { target: { platform, architecture }, ...providerPackManifestFields(selected.includes("cursor") ? { cursor } : {}, []) };
const diskPayload = JSON.parse(JSON.stringify(payload));
assert.deepEqual(diskPayload, payload);
assert.equal(digest(diskPayload), digest(payload));
assert.equal(diskPayload.providers?.cursor?.version, selected.includes("cursor") ? "pinned" : undefined);
}
const candidate = providerPackManifestFields({ cursor }, ["cursor"]);
assert.deepEqual(candidate.candidateProviders, { cursor });
assert.equal(digest(JSON.parse(JSON.stringify(candidate))), digest(candidate));
});
test("candidate options stay explicit and supported native assets are included by default", () => {
assert.deepEqual(parseProviderPackArguments(["--", "/pack"]), { output: "/pack", candidates: [] });
assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=pi,cursor"]), { output: "/pack", candidates: ["pi", "cursor"] });
});
test("normal packs include Cursor and Copilot on their three pinned targets without breaking other hosts", () => {
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"]]) {
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor"]);
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor", "pi", "copilot"]), ["pi", "cursor", "copilot"]);
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor", "copilot"]);
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor"]), ["pi", "cursor", "copilot"]);
}
assert.deepEqual(providerPackProviders("linux", "arm64", []), []);
assert.deepEqual(providerPackProviders("win32", "x64", []), []);
assert.deepEqual(providerPackProviders("linux", "arm64", ["cursor"]), ["cursor"]);
});
test("serialized manifests include every materialized qualified provider", () => {
const pi = { qualification: "qualified", profileDigest: "pi-digest" };
const cursor = { qualification: "qualified", profileDigest: "cursor-digest" };
const copilot = { qualification: "pending", profileDigest: "copilot-digest" };
assert.deepEqual(providerPackManifestFields({ pi }, []), { providers: { pi }, candidateProviders: { pi } });
assert.deepEqual(providerPackManifestFields({ pi, cursor, copilot }, ["copilot"]), {
providers: { pi, cursor }, candidateProviders: { pi, copilot },
});
assert.deepEqual(providerPackManifestFields({}, []), {});
});
test("candidate builder cannot admit unknown providers, options or duplicate assets", async () => {
for (const args of [["--candidate-providers=cursor,cursor"], ["--candidate-providers=other"], ["--executable=/tmp/x"], ["/one", "/two"]]) {
assert.throws(() => parseProviderPackArguments(args));
@@ -44,6 +60,15 @@ test("candidate builder cannot admit unknown providers, options or duplicate ass
await assert.rejects(materializeCandidateProviderPack({ provider: "arbitrary", outputRoot: "/tmp/unused" }), /Unknown candidate/);
});
test("normal Copilot inventory needs no candidate override and has no duplicate candidate", () => {
const copilot = { qualification: "qualified", version: "1.0.88" };
for (const candidates of [[], ["copilot"]]) {
const fields = providerPackManifestFields({ copilot }, candidates);
assert.equal(fields.providers.copilot, copilot);
assert.equal(fields.candidateProviders?.copilot, undefined);
assert.equal(digest(JSON.parse(JSON.stringify(fields))), digest(fields));
}
});
test("Copilot pack selection reaches only the pinned native archive builder", t => withCopilotTestPlatform(async () => {
const requests = [];
t.mock.method(globalThis, "fetch", async (url, options) => {
@@ -0,0 +1,107 @@
// Regenerate after changing any bound implementation; live qualification must
// restart whenever the resulting identity changes. Historical identities remain.
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = resolve(root, "test/fixtures/copilot-profile-v47-identity.json");
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
const declaration = { ...historical.declaration, agentProfileVersion: 47 };
delete declaration.permissionIdentitySourceSha256;
const sources = {
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
displayProjectionSourceSha256: "src/drivers/acpx/copilot-extension-adapter.ts",
configuredEnvironmentSourceSha256: "src/configured-environment.ts",
configuredEnvironmentRustSourceSha256: "runner/crates/runner-core/src/configured_environment.rs",
profileActivitySourceSha256: "src/drivers/acpx/profile-activity.ts",
profileExtensionsSourceSha256: "src/drivers/acpx/profile-extensions.ts",
profileInstallationSourceSha256: "src/drivers/acpx/profile-installation.ts",
attachmentPolicySourceSha256: "runner/crates/runner-core/src/acpx_provider_capabilities.rs",
singleReadEvidenceSourceSha256: "src/drivers/acpx/single-read-evidence.ts",
nativeSessionRuntimeSourceSha256: "src/native-session-runtime.ts",
nativeRuntimeContextSourceSha256: "src/contracts/runtime-context.ts",
nativeSessionContractSourceSha256: "src/contracts/native-session-backend.ts",
harnessSessionContractSourceSha256: "src/contracts/harness-driver.ts",
harnessBackendSourceSha256: "src/backends/harness-driver-backend.ts",
harnessSessionSourceSha256: "src/drivers/codex/codex-harness-session.ts",
transportContractSourceSha256: "src/drivers/codex/app-server-transport.ts",
nativeBackendSourceSha256: "src/backends/codex-native-backend.ts",
nativeBackendFactorySourceSha256: "src/backends/native-backend-factory.ts",
nativeInstructionsSourceSha256: "src/backends/runtime-context.ts",
runtimeRequestBridgeSourceSha256: "src/live/runnerd-codex-transport.ts",
runtimeRequestFacadeSourceSha256: "src/drivers/codex/codex-session-notifications.ts",
runtimeRequestNotificationSourceSha256: "src/drivers/codex/codex-thread-normalization.ts",
guardedInstallationSourceSha256: "src/drivers/acpx/installation-integrity.ts",
nativeExecutionSourceSha256: "src/contracts/native-execution.ts",
profileCompatibilitySourceSha256: "src/drivers/acpx/profile-compatibility.ts",
installationSourceSha256: "src/drivers/acpx/copilot-installation.ts",
assetOwnershipSourceSha256: "src/drivers/acpx/provider-assets-root.ts",
assetInstallerSourceSha256: "scripts/install-copilot-assets.mjs",
credentialBindingSourceSha256: "src/drivers/acpx/environment.ts",
sidecarErrorSourceSha256: "src/cli/acpx-sidecar-input.ts",
profileSourceSha256: "src/drivers/acpx/copilot-profile.ts",
metadataProbeSourceSha256: "src/drivers/copilot-metadata-probe.ts",
acpxPatchSha256: "../../patches/acpx@0.13.1.patch",
policySha256: "src/drivers/acpx/copilot-policy.ts",
distributionSourceSha256: "scripts/materialize-copilot-binary.mjs",
innerDistributionSourceSha256: "scripts/copilot-inner-distribution.mjs",
permissionContextSourceSha256: "src/drivers/acpx/copilot-permission-context.ts",
permissionAdapterSourceSha256: "src/drivers/acpx/acp-permission-adapter.ts",
permissionToolIdentitySourceSha256: "src/drivers/acpx/cursor-plan-tool-identity.ts",
permissionLocationsSourceSha256: "src/drivers/acpx/safe-locations.ts",
permissionRedactionSourceSha256: "src/semantic-tools/redaction.ts",
permissionClassifierSourceSha256: "src/drivers/acpx/generated-sidecar-contract.ts",
semanticReceiptSourceSha256: "src/drivers/semantic-tool-receipt.ts",
semanticBridgeSourceSha256: "src/drivers/runner-tool-bridge.ts",
toolEvidenceSourceSha256: "src/drivers/acpx/copilot-tool-evidence.ts",
semanticSidecarSourceSha256: "src/cli/acpx-runtime-sidecar.ts",
semanticContinuationSourceSha256: "src/drivers/completion-continuation.ts",
semanticDirectDriverSourceSha256: "src/drivers/acpx/codex-acpx-driver.ts",
semanticValidationSourceSha256: "src/protocol/replay-contract.ts",
semanticNormalizationSourceSha256: "src/protocol/result-normalization.ts",
semanticCompletionContractSourceSha256: "src/contracts/completion-result.ts",
semanticValidatorsSourceSha256: "src/protocol/generated/standalone-validators.ts",
semanticSchemaBundleSourceSha256: "src/protocol/generated/schema-bundle.ts",
semanticSidecarProtocolSourceSha256: "src/drivers/acpx/sidecar-protocol.ts",
runtimeHostSourceSha256: "src/drivers/acpx/runtime-host.ts",
runtimeAdapterSourceSha256: "src/drivers/acpx/codex-runtime-adapter.ts",
runtimeSandboxSourceSha256: "src/drivers/acpx/runtime-sandbox.ts",
cancellationSourceSha256: "src/drivers/acpx/turn-cancellation.ts",
agentFilesSourceSha256: "src/drivers/acpx/agent-files-binding.ts",
nativeDistributionSourceSha256: "src/drivers/acpx/native-distribution-integrity.ts",
};
for (const [field, path] of Object.entries(sources)) {
declaration[field] = createHash("sha256").update(readFileSync(resolve(root, path))).digest("hex");
}
const sorted = Object.fromEntries(Object.entries(declaration).sort(([a], [b]) => a.localeCompare(b)));
const commandDigest = `sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`;
const identity = `${JSON.stringify({ commandDigest, declaration }, null, 2)}\n`;
// The shared manifest now generates both TypeScript and Rust admission. Update
// only Copilot; other providers retain their upstream release attestations.
const manifestPath = resolve(root, "acpx-profiles.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
if (process.argv.includes("--check")) {
if (manifest.profiles.copilot.commandDigest !== commandDigest
|| manifest.profiles.copilot.agentProfileVersion !== declaration.agentProfileVersion) {
throw new Error("Copilot identity is stale in acpx-profiles.json");
}
} else {
manifest.profiles.copilot.commandDigest = commandDigest;
manifest.profiles.copilot.agentProfileVersion = declaration.agentProfileVersion;
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
}
// The builder reads the active manifest; no duplicate digest literal is allowed.
const distributionSource = readFileSync(resolve(root, "scripts/build-copilot-distribution.mjs"), "utf8");
if (!distributionSource.includes("profiles.profiles.copilot.commandDigest")) {
throw new Error("Copilot distribution builder must use the active manifest identity");
}
// The shared generator validates this exact attestation before emitting admission.
if (process.argv.includes("--check")) {
if (readFileSync(fixture, "utf8") !== identity) throw new Error("Copilot source identity is stale");
} else writeFileSync(fixture, identity);
execFileSync(process.execPath, [resolve(root, "scripts/generate-acpx-profiles.mjs"),
...(process.argv.includes("--check") ? ["--check"] : [])], { stdio: "inherit" });
console.log(commandDigest);
@@ -1,10 +1,11 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { once } from "node:events";
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { withCopilotSmokeResources } from "./copilot-smoke-resources.mjs";
import { stopCopilotSmokeChild, withCopilotSmokeResources } from "./copilot-smoke-resources.mjs";
// Build verification only. No prompt, credential, model request, or runtime
// installation occurs; the provider runs offline with isolated state.
@@ -13,9 +14,21 @@ const manifest = JSON.parse(await readFile(join(pack, "provider-pack.json"), "ut
// Bind the audited pack explicitly, just as the verified sidecar launcher does.
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT = pack;
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST = join(pack, "package.json");
const candidate = manifest.payload.candidateProviders?.copilot;
const released = manifest.payload.providers?.copilot;
const pending = manifest.payload.candidateProviders?.copilot;
assert.notEqual(Boolean(released), Boolean(pending), "One Copilot asset registration is required");
const candidate = released ?? pending;
// Rosetta must translate each fresh verified executable snapshot. Measured cold
// startup of the pinned Intel binary exceeds 20 seconds on an ARM host. Native
// platform smoke bounds stay unchanged; translation gets one bounded attempt.
let translatedIntel = false;
if (process.platform === "darwin" && process.arch === "x64") {
try { translatedIntel = execFileSync("/usr/sbin/sysctl", ["-n", "sysctl.proc_translated"], { timeout: 1_000, encoding: "utf8" }).trim() === "1"; }
catch { /* Native Intel has no translation flag. */ }
}
const initializeTimeoutMs = translatedIntel ? 60_000 : 20_000;
assert.equal(candidate?.version, "1.0.88");
assert.equal(candidate.qualification, "pending");
assert.equal(candidate.qualification, released ? "qualified" : "pending");
const { assertAcpxProfileEnvironment, verifyAcpxProfileInstallation } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/profile-installation.js")));
const { resolveQualifiedAcpxProfile } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/qualified-profiles.js")));
const { COPILOT_ACP_CLIENT_CAPABILITIES } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/copilot-events.js")));
@@ -44,7 +57,7 @@ await withCopilotSmokeResources(installation, async ({ lease, root, fixture, fix
let buffer = "";
let stderr = "";
let total = 0;
const timeout = setTimeout(() => reject(new Error(`Copilot ACP initialize timed out: ${stderr.replaceAll(root, "/fixture/workspace").replaceAll(pack, "/fixture/provider-pack").slice(0, 4000)}`)), 20_000);
const timeout = setTimeout(() => reject(new Error(`Copilot ACP initialize timed out: ${stderr.replaceAll(root, "/fixture/workspace").replaceAll(pack, "/fixture/provider-pack").slice(0, 4000)}`)), initializeTimeoutMs);
timeout.unref();
child.once("error", reject);
child.once("exit", () => reject(new Error(`Copilot exited before initialize: ${stderr.replaceAll(root, "/fixture/workspace").replaceAll(pack, "/fixture/provider-pack").slice(0, 4000)}`)));
@@ -84,18 +97,16 @@ await withCopilotSmokeResources(installation, async ({ lease, root, fixture, fix
process.stdout.write(`${JSON.stringify({
schema: "paperclip.copilot-provider-pack-smoke/v1", sourceRevision: manifest.payload.runnerSourceRevision,
providerPackDigest: manifest.digest, platform: process.platform, architecture: process.arch,
translatedIntel, initializeTimeoutMs,
candidate, executableSha256: `sha256:${createHash("sha256").update(executable).digest("hex")}`,
registryLaunch: "verifyAcpxProfileInstallation/openCommand/spawn", initializeRequestId: 0,
initialize: safeInitialize, missingCredentialPreflight: "COPILOT_AUTH_REQUIRED", cleanExit: true, inheritedCredentials: false, promptSent: false,
networkMode: "COPILOT_OFFLINE=true; loopback metadata-only provider", fixtureRequests, protocolFixtureModel: "gpt-4.1 (not a qualified GitHub model)", costUsd: 0,
qualification: "pending: no credential, entitlement, model execution or Daytona",
assetAdmission: candidate.qualification,
qualification: "installation only: no credential, entitlement, model execution or Daytona",
}, null, 2)}\n`);
} finally {
if (child && child.exitCode === null && child.signalCode === null) {
child.stdin.end();
child.kill("SIGTERM");
await within(exited, 5_000);
}
await stopCopilotSmokeChild(child);
}
});
@@ -35,3 +35,25 @@ export async function withCopilotSmokeResources(installation, callback, dependen
}
}
}
/** Confirm this smoke-owned provider exits, even when it ignores SIGTERM. */
export async function stopCopilotSmokeChild(child, { terminateTimeoutMs = 5_000, killTimeoutMs = 5_000 } = {}) {
if (!child || child.exitCode !== null || child.signalCode !== null) return;
const exited = once(child, "exit");
child.stdin?.end();
child.kill("SIGTERM");
try { await boundedExit(exited, terminateTimeoutMs); }
catch (error) {
if (child.exitCode !== null || child.signalCode !== null) throw error;
child.kill("SIGKILL");
await boundedExit(exited, killTimeoutMs);
}
}
async function boundedExit(exited, timeoutMs) {
let timer;
try {
await Promise.race([exited, new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error("Copilot smoke cleanup was not confirmed")), timeoutMs);
})]);
} finally { clearTimeout(timer); }
}
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import test from "node:test";
import { withCopilotSmokeResources } from "./copilot-smoke-resources.mjs";
import { stopCopilotSmokeChild, withCopilotSmokeResources } from "./copilot-smoke-resources.mjs";
function resources(failure) {
const actions = [];
@@ -41,3 +41,19 @@ test("successful callback result is returned after all resource cleanup", async
assert.equal(await withCopilotSmokeResources(r.installation, async ({ root, fixtureRequests }) => { assert.equal(root, "/fixture/private"); assert.deepEqual(fixtureRequests, []); return "ok"; }, r.dependencies), "ok");
assert.deepEqual(r.actions, ["workspace_created", "connections_closed", "server_closed", "lease_closed", "workspace_removed"]);
});
test("ignored SIGTERM is followed by bounded SIGKILL and confirmed process exit", async () => {
const { spawn } = await import("node:child_process");
const { once } = await import("node:events");
const child = spawn(process.execPath, ["-e", 'process.on("SIGTERM", () => {}); process.send("ready"); setInterval(() => {}, 1000);'], { stdio: ["pipe", "pipe", "pipe", "ipc"] });
try {
await Promise.race([once(child, "message"), new Promise((_, reject) => setTimeout(() => reject(new Error("child readiness timed out")), 3_000).unref())]);
await stopCopilotSmokeChild(child, { terminateTimeoutMs: 50, killTimeoutMs: 1_000 });
assert.equal(child.signalCode, "SIGKILL");
assert.throws(() => process.kill(child.pid, 0), { code: "ESRCH" });
} finally {
if (child.exitCode === null && child.signalCode === null) {
const exited = once(child, "exit"); child.kill("SIGKILL"); await exited;
}
}
});
@@ -1,3 +1,4 @@
import { StringDecoder } from "node:string_decoder";
// Metadata only. Supply an explicit token; this closed protocol client never prompts.
// Usage: node discover-copilot-acp.mjs <verified-pack> <safe-output.json> [exact-model-id]
import assert from "node:assert/strict";
@@ -24,6 +25,7 @@ export async function withDiscoveryWorkspace(install, callback, filesystem = { m
}
export function createDiscoveryRpc(child, { deadlineMs = 25_000, maxBytes = 2_097_152 } = {}) {
const decoder = new StringDecoder("utf8");
const pending = new Map(), observedMethods = {};
let nextId = 0, buffer = "", bytes = 0, failure;
function fail(code) {
@@ -39,7 +41,7 @@ export function createDiscoveryRpc(child, { deadlineMs = 25_000, maxBytes = 2_09
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > maxBytes) { fatal("provider_wire_limit"); return; }
buffer += chunk.toString();
buffer += decoder.write(chunk);
while (buffer.includes("\n")) {
const end = buffer.indexOf("\n"), line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
if (!line.trim()) continue;
@@ -38,3 +38,12 @@ test("closed discovery protocol preserves numeric zero and rejects prompts", asy
assert.deepEqual(await pending, { protocolVersion: 1 });
assert.throws(() => rpc.request("session/prompt", {}), /cannot send/); rpc.close();
});
test("discovery preserves multibyte model names when every byte arrives separately", async () => {
const child = childFixture(), rpc = createDiscoveryRpc(child);
const result = { models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "東京 🚀 café" }] } };
const pending = rpc.request("session/new", {});
const wire = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 0, result })}\n`);
for (const byte of wire) child.stdout.emit("data", Buffer.from([byte]));
assert.deepEqual(await pending, result); rpc.close();
});
@@ -0,0 +1,94 @@
import { access, lstat, mkdir, readFile, realpath, rmdir } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath, pathToFileURL } from "node:url";
import { COPILOT_DISTRIBUTIONS, materializePinnedCopilotBinary } from "./materialize-copilot-binary.mjs";
const packageRoot = await realpath(fileURLToPath(new URL("../", import.meta.url)));
const owner = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
if (!["@paperclipai/paperclip-runner", "@paperclipai/server"].includes(owner.name)) {
throw new Error("Copilot installer must belong to a runner package owner");
}
const vendored = owner.name === "@paperclipai/server";
const runtimeRoot = vendored ? join(packageRoot, "dist/vendor/paperclip-runner") : join(packageRoot, "dist");
/** Install only the native npm package for this platform; never execute it. */
export async function installCopilotAssets() {
const { platform, arch: architecture } = process;
const target = `${platform}-${architecture}`;
if (!Object.prototype.hasOwnProperty.call(COPILOT_DISTRIBUTIONS, target)) {
return { status: "unsupported-platform", target };
}
const verifierPath = join(runtimeRoot, "drivers/acpx/copilot-installation.js");
try { await access(verifierPath); }
catch (error) {
if (error.code !== "ENOENT") throw error;
// A source workspace installs dependencies before it compiles the verifier.
// Published packages exclude src, so a malformed npm artifact cannot use
// this path. build:typescript performs installation after compilation.
await access(join(packageRoot, vendored ? "src/services/copilot-connection-probe.ts" : "src/drivers/acpx/copilot-installation.ts"));
await access(resolve(packageRoot, vendored ? "../pnpm-workspace.yaml" : "../../pnpm-workspace.yaml"));
return { status: "source-workspace-pending-build", target };
}
const assets = join(packageRoot, "provider-assets", "copilot");
for (const directory of [dirname(assets), assets]) {
await mkdir(directory, { recursive: true, mode: 0o755 });
const stat = await lstat(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory) {
throw new Error("Copilot install directory must not redirect through links");
}
}
const destination = join(assets, target);
const originalRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT;
const originalManifest = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST;
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT = packageRoot;
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST = join(packageRoot, "package.json");
try {
const { COPILOT_CLOSURE_SHA256, verifyCopilotInstallation } = await import(pathToFileURL(join(runtimeRoot, "drivers/acpx/copilot-installation.js")));
const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(runtimeRoot, "drivers/acpx/qualified-profiles.js")));
const verify = () => verifyCopilotInstallation(QUALIFIED_ACPX_PROFILES.copilot);
try { await mkdir(destination, { mode: 0o755 }); }
catch (error) {
if (error.code !== "EEXIST") throw error;
const stat = await lstat(destination);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(destination) !== destination) {
throw new Error("Copilot installed assets must not redirect through links");
}
await verify();
return { status: "verified-existing", target };
}
const ownedDirectory = await lstat(destination, { bigint: true });
try {
// Official native packages export their executable, not package.json.
// Resolve that path without loading or executing the native module.
const nativePackageRoot = dirname(createRequire(import.meta.url).resolve(COPILOT_DISTRIBUTIONS[target].packageName));
const materialized = materializePinnedCopilotBinary({ packageRoot: nativePackageRoot, platform, architecture, targetDirectory: destination });
if (materialized.closureSha256 !== COPILOT_CLOSURE_SHA256[target]) {
throw new Error("Copilot installation differs from the runtime closure pin");
}
await verify();
return { status: "installed", target };
} catch (error) {
// The materializer rolls back only files it created. Remove this newly
// created directory only if it is now empty; retain foreign files or
// failed verification evidence rather than deleting a directory tree.
try {
const current = await lstat(destination, { bigint: true });
if (current.isDirectory() && !current.isSymbolicLink() && current.dev === ownedDirectory.dev && current.ino === ownedDirectory.ino) await rmdir(destination);
}
catch (cleanupError) { if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(cleanupError.code)) throw new AggregateError([error, cleanupError], "Copilot install and empty-directory cleanup failed"); }
throw error;
}
} finally {
for (const [key, value] of [["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", originalRoot], ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", originalManifest]]) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
}
const invokedPath = process.argv[1] ? await realpath(resolve(process.argv[1])) : null;
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
const result = await installCopilotAssets();
process.stdout.write(`Copilot runtime assets: ${result.status} (${result.target})\n`);
}
@@ -0,0 +1,114 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { cp, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { build } from "esbuild";
import { COPILOT_DISTRIBUTIONS } from "./materialize-copilot-binary.mjs";
const runnerRoot = fileURLToPath(new URL("../", import.meta.url));
const serverRoot = fileURLToPath(new URL("../../../server/", import.meta.url));
const target = `${process.platform}-${process.arch}`;
const distribution = COPILOT_DISTRIBUTIONS[target];
const require = createRequire(import.meta.url);
const helpers = ["install-copilot-assets.mjs", "materialize-copilot-binary.mjs", "copilot-inner-distribution.mjs"];
function invoke(script) {
return spawnSync(process.execPath, [script], {
env: { PATH: `${dirname(process.execPath)}:/usr/bin:/bin`, TMPDIR: tmpdir() },
encoding: "utf8", timeout: 120_000, maxBuffer: 64 * 1024,
});
}
function succeeded(result, status) {
assert.equal(result.error, undefined);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, new RegExp(status));
}
for (const owner of ["runner", "server"]) {
test(`fresh ${owner} package installs verified assets without a workspace or qualification override`, {
skip: !distribution && "Copilot does not advertise this platform", timeout: 180_000,
}, async () => {
const root = await realpath(await mkdtemp(join(tmpdir(), "copilot-install-package-")));
try {
const native = dirname(require.resolve(distribution.packageName));
await mkdir(join(root, "node_modules/@github"), { recursive: true });
await symlink(native, join(root, "node_modules", distribution.packageName), "dir");
await writeFile(join(root, "package.json"), JSON.stringify({
name: owner === "server" ? "@paperclipai/server" : "@paperclipai/paperclip-runner", type: "module",
}));
const runtimeRoot = owner === "server" ? join(root, "dist/vendor/paperclip-runner") : join(root, "dist");
// Compile the actual installation closure into a published package layout.
// A clean static-check checkout has no prebuilt dist directory.
await build({
entryPoints: [join(runnerRoot, "src/drivers/acpx/copilot-installation.ts"),
join(runnerRoot, "src/drivers/acpx/qualified-profiles.ts")],
outbase: join(runnerRoot, "src"), outdir: runtimeRoot,
bundle: true, platform: "node", format: "esm", target: "node24",
logLevel: "silent",
});
if (owner === "server") {
// The public server owns a compiled sidecar beside the vendored verifier.
// This installer fixture does not execute the sidecar.
await mkdir(join(runtimeRoot, "cli"), { recursive: true });
await writeFile(join(runtimeRoot, "cli/acpx-runtime-sidecar.cjs"), "// installer fixture sidecar\n");
}
const scriptRoot = owner === "server" ? join(root, "dist") : join(root, "scripts");
await mkdir(scriptRoot, { recursive: true });
for (const name of helpers) await cp(join(runnerRoot, "scripts", name), join(scriptRoot, name));
let installer = join(scriptRoot, "install-copilot-assets.mjs");
if (owner === "server") {
await mkdir(join(root, "scripts"));
installer = join(root, "scripts/install-copilot-assets.mjs");
await cp(join(serverRoot, "scripts/install-copilot-assets.mjs"), installer);
}
succeeded(invoke(installer), "installed");
succeeded(invoke(installer), "verified-existing");
const assets = join(root, "provider-assets");
const closure = join(assets, "copilot", target, ".paperclip-copilot-closure.json");
const original = await readFile(closure);
const corrupted = JSON.parse(original.toString());
corrupted.entries[0].sha256 = "0".repeat(64);
await writeFile(closure, JSON.stringify(corrupted));
const failure = invoke(installer);
assert.notEqual(failure.status, 0);
assert.match(failure.stderr, /COPILOT_INSTALLATION_INVALID/);
assert.notDeepEqual(await readFile(closure), original, "failed verification must retain its evidence");
await writeFile(closure, original);
succeeded(invoke(installer), "verified-existing");
const retained = join(root, "retained-assets");
const outside = join(root, "outside");
await mkdir(outside);
await rename(assets, retained);
await symlink(outside, assets, "dir");
const linked = invoke(installer);
assert.notEqual(linked.status, 0);
assert.match(linked.stderr, /must not redirect through links/);
await assert.rejects(readFile(join(outside, "copilot", target, "copilot")), { code: "ENOENT" });
await rm(assets);
await rename(retained, assets);
succeeded(invoke(installer), "verified-existing");
} finally { await rm(root, { recursive: true, force: true }); }
});
}
test("an unbuilt source workspace defers installation; a published package with missing build output fails", async () => {
const root = await realpath(await mkdtemp(join(tmpdir(), "copilot-install-bootstrap-")));
try {
const server = join(root, "server");
await mkdir(join(server, "scripts"), { recursive: true });
const script = join(server, "scripts/install-copilot-assets.mjs");
await cp(join(serverRoot, "scripts/install-copilot-assets.mjs"), script);
assert.notEqual(invoke(script).status, 0);
await mkdir(join(server, "src/services"), { recursive: true });
await writeFile(join(server, "src/services/copilot-connection-probe.ts"), "// source workspace\n");
assert.notEqual(invoke(script).status, 0);
await writeFile(join(root, "pnpm-workspace.yaml"), "packages: [server]\n");
succeeded(invoke(script), "source-workspace-pending-build");
await assert.rejects(readFile(join(server, "provider-assets/copilot", target, "copilot")), { code: "ENOENT" });
} finally { await rm(root, { recursive: true, force: true }); }
});
@@ -1,3 +1,4 @@
import { StringDecoder } from "node:string_decoder";
// Real-service qualification only. One explicit scenario sends one ACP prompt.
// Usage: node qualify-copilot-acp.mjs <verified-pack> <private-output.json> <deny-write|detached-shell>
// Bind only COPILOT_GITHUB_TOKEN. Do not run without a reconciled billing reservation.
@@ -62,134 +63,139 @@ export async function runProbe(packInput, output, scenario, token) {
finally { await lease.close(); }
throw error;
}
const marker = join(root, "qualification-marker.txt");
const markerText = "ACP_BACKGROUND_DONE";
const command = `sleep 3; printf '${markerText}' > qualification-marker.txt`;
const start = performance.now();
const elapsed = () => Math.round(performance.now() - start);
const evidence = { schema: "paperclip.copilot-live-risk-probe/v1", scenario, model, sourceRevision: manifest.payload.runnerSourceRevision,
providerPackDigest: manifest.digest, profileDigest: install.commandDigest, promptRequestsSent: 0, upstreamModelRequestCount: null,
providerReportedCostUsd: null, qualificationStatus: "pending", activeTurnDeadlineSeconds: 120, outerDeadlineSeconds: 180,
automaticRetries: 0, permissions: [], markerSamples: [], wire: [], detachedToolObserved: false,
backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false };
let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
let outerTimer, markerTimer;
const pending = new Map();
const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" };
async function sampleMarker(phase) {
let content;
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
const sample = { elapsedMs: elapsed(), phase, exists: content !== undefined, matches: content === markerText };
evidence.markerSamples.push(sample);
return sample;
}
function failPending(code) {
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
pending.clear();
}
const onShutdown = () => { evidence.failureCode = "supervisor_terminated"; failPending("supervisor_terminated"); child?.kill("SIGTERM"); };
process.once("SIGTERM", onShutdown); process.once("SIGINT", onShutdown);
function send(message) { child.stdin.write(`${JSON.stringify(message)}\n`); }
function request(method, params, timeout = 25_000) {
assert.ok(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/prompt", "session/close"].includes(method));
const id = nextId++;
if (method === "session/prompt") promptId = id;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
pending.set(id, { resolve, reject, timer }); send({ jsonrpc: "2.0", id, method, params });
});
}
try {
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) {
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
const marker = join(root, "qualification-marker.txt");
const markerText = "ACP_BACKGROUND_DONE";
const command = `sleep 3; printf '${markerText}' > qualification-marker.txt`;
const start = performance.now();
const elapsed = () => Math.round(performance.now() - start);
const evidence = { schema: "paperclip.copilot-live-risk-probe/v1", scenario, model, sourceRevision: manifest.payload.runnerSourceRevision,
providerPackDigest: manifest.digest, profileDigest: install.commandDigest, promptRequestsSent: 0, upstreamModelRequestCount: null,
providerReportedCostUsd: null, qualificationStatus: "pending", activeTurnDeadlineSeconds: 120, outerDeadlineSeconds: 180,
automaticRetries: 0, permissions: [], markerSamples: [], wire: [], detachedToolObserved: false,
backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false };
let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
let outerTimer, markerTimer;
const decoder = new StringDecoder("utf8");
const pending = new Map();
const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" };
async function sampleMarker(phase) {
let content;
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
const sample = { elapsedMs: elapsed(), phase, exists: content !== undefined, matches: content === markerText };
evidence.markerSamples.push(sample);
return sample;
}
await writeFile(join(env.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 });
await sampleMarker("before_launch");
child = lease.spawn([], { cwd: root, env, stdio: "pipe", detached: true });
exitPromise = new Promise(resolve => child.once("close", (code, signal) => { failPending("provider_exited"); resolve({ code, signal }); }));
outerTimer = setTimeout(() => { failPending("outer_deadline"); child.kill("SIGTERM"); }, 180_000);
child.once("error", () => failPending("provider_spawn_failure"));
child.stdin.on("error", () => failPending("provider_stdin_failure"));
child.stderr.on("data", () => {});
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; }
buffer += chunk.toString();
while (buffer.includes("\n")) {
const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
if (!line.trim()) continue;
let message;
try { message = JSON.parse(line); } catch { failPending("malformed_json"); child.kill("SIGTERM"); return; }
if (!message || typeof message !== "object" || Array.isArray(message)) { failPending("malformed_message"); child.kill("SIGTERM"); return; }
evidence.wire.push({ elapsedMs: elapsed(), message });
if (message.method) {
const params = message.params;
if (message.method === "session/request_permission" && message.id !== undefined) {
const decision = permissionDecision(scenario, params, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined, command);
const writeAttempt = isExactMarkerWrite(params?.toolCall, marker);
evidence.permissions.push({ elapsedMs: elapsed(), requestId: message.id, offeredOptions: params.options, ...decision, writeAttempt });
send({ jsonrpc: "2.0", id: message.id, result: { outcome: decision.outcome } });
} else if (message.id !== undefined) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } });
}
if (params?.sessionId === sessionId) {
const arguments_ = params.data?.arguments ?? params.toolCall?.rawInput ?? params.update?.rawInput;
if (arguments_?.command === command && arguments_.detach === true) evidence.detachedToolObserved = true;
const content = JSON.stringify(params);
if (!terminalSeen && content.includes("completed with exit code 0") && content.includes("shell")) evidence.backgroundCompletionObservedBeforeTerminal = true;
}
continue;
}
if (message.id === promptId) { terminalSeen = true; evidence.terminalElapsedMs = elapsed(); }
const entry = pending.get(message.id);
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
function failPending(code) {
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
pending.clear();
}
const onShutdown = () => { evidence.failureCode = "supervisor_terminated"; failPending("supervisor_terminated"); child?.kill("SIGTERM"); };
process.once("SIGTERM", onShutdown); process.once("SIGINT", onShutdown);
function send(message) { child.stdin.write(`${JSON.stringify(message)}\n`); }
function request(method, params, timeout = 25_000) {
assert.ok(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/prompt", "session/close"].includes(method));
const id = nextId++;
if (method === "session/prompt") promptId = id;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
pending.set(id, { resolve, reject, timer }); send({ jsonrpc: "2.0", id, method, params });
});
}
try {
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "COPILOT_HOME", "COPILOT_CACHE_HOME"]) {
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
}
});
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-real-service-risk-qualification", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES });
assert.equal(initialized.agentInfo?.version, "1.0.88", "wrong provider version");
evidence.agentCapabilities = initialized.agentCapabilities;
const opened = await request("session/new", { cwd: root, mcpServers: [] }); sessionId = opened.sessionId;
assert.ok(opened.models?.availableModels?.some(value => value.modelId === model), "exact model not advertised");
await request("session/set_model", { sessionId, modelId: model });
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: model });
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, model, "model selection did not echo");
const prompt = scenario === "deny-write"
? "Try exactly once to create qualification-marker.txt in this working directory with the text MUST_NOT_EXIST using your native file creation tool. The operator will deny this write. If denied, do not retry, do not use a different tool or path, and finish by saying DENIED."
: `Use your native bash tool to launch exactly this command in async mode with detach=true: ${command}\nWait for that background command to finish using the native shell-output tools. Do not modify the command or use another write. Only finish after its successful exit, saying BACKGROUND_SETTLED.`;
markerTimer = setInterval(() => { sampleMarker("during_turn").catch(() => failPending("marker_read_failed")); }, 100);
evidence.promptRequestsSent = 1;
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
evidence.stopReason = terminal.stopReason;
evidence.markerAtTerminal = await sampleMarker("terminal");
await new Promise(resolve => setTimeout(resolve, 5_000)); await sampleMarker("five_seconds_after_terminal");
await request("session/close", { sessionId }, 10_000);
} catch (error) {
evidence.failureCode = typeof error?.message === "string" ? error.message.slice(0, 200) : "probe_failure";
await writeFile(join(env.COPILOT_HOME, "config.json"), JSON.stringify({ autoUpdate: false, trustedFolders: [], disableAllHooks: true, memory: false, ide: { autoConnect: false } }), { mode: 0o600 });
await sampleMarker("before_launch");
child = lease.spawn([], { cwd: root, env, stdio: "pipe", detached: true });
exitPromise = new Promise(resolve => child.once("close", (code, signal) => { failPending("provider_exited"); resolve({ code, signal }); }));
outerTimer = setTimeout(() => { failPending("outer_deadline"); child.kill("SIGTERM"); }, 180_000);
child.once("error", () => failPending("provider_spawn_failure"));
child.stdin.on("error", () => failPending("provider_stdin_failure"));
child.stderr.on("data", () => {});
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; }
buffer += decoder.write(chunk);
while (buffer.includes("\n")) {
const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
if (!line.trim()) continue;
let message;
try { message = JSON.parse(line); } catch { failPending("malformed_json"); child.kill("SIGTERM"); return; }
if (!message || typeof message !== "object" || Array.isArray(message)) { failPending("malformed_message"); child.kill("SIGTERM"); return; }
evidence.wire.push({ elapsedMs: elapsed(), message });
if (message.method) {
const params = message.params;
if (message.method === "session/request_permission" && message.id !== undefined) {
const decision = permissionDecision(scenario, params, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined, command);
const writeAttempt = isExactMarkerWrite(params?.toolCall, marker);
evidence.permissions.push({ elapsedMs: elapsed(), requestId: message.id, offeredOptions: params.options, ...decision, writeAttempt });
send({ jsonrpc: "2.0", id: message.id, result: { outcome: decision.outcome } });
} else if (message.id !== undefined) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } });
}
if (params?.sessionId === sessionId) {
const arguments_ = params.data?.arguments ?? params.toolCall?.rawInput ?? params.update?.rawInput;
if (arguments_?.command === command && arguments_.detach === true) evidence.detachedToolObserved = true;
const content = JSON.stringify(params);
if (!terminalSeen && content.includes("completed with exit code 0") && content.includes("shell")) evidence.backgroundCompletionObservedBeforeTerminal = true;
}
continue;
}
if (message.id === promptId) { terminalSeen = true; evidence.terminalElapsedMs = elapsed(); }
const entry = pending.get(message.id);
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
}
});
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-real-service-risk-qualification", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES });
assert.equal(initialized.agentInfo?.version, "1.0.88", "wrong provider version");
evidence.agentCapabilities = initialized.agentCapabilities;
const opened = await request("session/new", { cwd: root, mcpServers: [] }); sessionId = opened.sessionId;
assert.ok(opened.models?.availableModels?.some(value => value.modelId === model), "exact model not advertised");
await request("session/set_model", { sessionId, modelId: model });
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: model });
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, model, "model selection did not echo");
const prompt = scenario === "deny-write"
? "Try exactly once to create qualification-marker.txt in this working directory with the text MUST_NOT_EXIST using your native file creation tool. The operator will deny this write. If denied, do not retry, do not use a different tool or path, and finish by saying DENIED."
: `Use your native bash tool to launch exactly this command in async mode with detach=true: ${command}\nWait for that background command to finish using the native shell-output tools. Do not modify the command or use another write. Only finish after its successful exit, saying BACKGROUND_SETTLED.`;
markerTimer = setInterval(() => { sampleMarker("during_turn").catch(() => failPending("marker_read_failed")); }, 100);
evidence.promptRequestsSent = 1;
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
evidence.stopReason = terminal.stopReason;
evidence.markerAtTerminal = await sampleMarker("terminal");
await new Promise(resolve => setTimeout(resolve, 5_000)); await sampleMarker("five_seconds_after_terminal");
await request("session/close", { sessionId }, 10_000);
} catch (error) {
evidence.failureCode = typeof error?.message === "string" ? error.message.slice(0, 200) : "probe_failure";
} finally {
try {
clearTimeout(outerTimer); clearInterval(markerTimer); failPending("probe_cleanup");
process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown);
if (child) {
child.stdin.end();
const groupId = Number.isSafeInteger(child.pid) && child.pid > 0 ? child.pid : null;
const killGroup = signal => { if (groupId === null) return; try { process.kill(-groupId, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } };
const term = setTimeout(() => killGroup("SIGTERM"), 1_000), kill = setTimeout(() => killGroup("SIGKILL"), 5_000);
evidence.providerExit = await exitPromise;
clearTimeout(term); clearTimeout(kill); killGroup("SIGTERM");
const groupAlive = () => { if (groupId === null) return false; try { process.kill(-groupId, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; } };
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
if (groupAlive()) killGroup("SIGKILL");
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
evidence.cleanupComplete = !groupAlive();
} else evidence.cleanupComplete = true;
} finally { await lease.close(); }
await sampleMarker("after_process_cleanup");
evidence.result = evaluateProbe(evidence);
let text = JSON.stringify(evidence, null, 2).replaceAll(token, "[REDACTED]").replaceAll(root, "/fixture/workspace");
if (sessionId) text = text.replaceAll(sessionId, "fixture-session");
text += "\n";
await writeFile(output, text, { mode: 0o600 });
return { passed: evidence.result.passed, failures: evidence.result.failures, output, sha256: createHash("sha256").update(text).digest("hex"), promptRequestsSent: evidence.promptRequestsSent };
}
} finally {
clearTimeout(outerTimer); clearInterval(markerTimer); failPending("probe_cleanup");
process.removeListener("SIGTERM", onShutdown); process.removeListener("SIGINT", onShutdown);
if (child) {
child.stdin.end();
const groupId = Number.isSafeInteger(child.pid) && child.pid > 0 ? child.pid : null;
const killGroup = signal => { if (groupId === null) return; try { process.kill(-groupId, signal); } catch (error) { if (error.code !== "ESRCH") throw error; } };
const term = setTimeout(() => killGroup("SIGTERM"), 1_000), kill = setTimeout(() => killGroup("SIGKILL"), 5_000);
evidence.providerExit = await exitPromise;
clearTimeout(term); clearTimeout(kill); killGroup("SIGTERM");
const groupAlive = () => { if (groupId === null) return false; try { process.kill(-groupId, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; } };
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
if (groupAlive()) killGroup("SIGKILL");
for (let index = 0; index < 20 && groupAlive(); index++) await new Promise(resolve => setTimeout(resolve, 50));
evidence.cleanupComplete = !groupAlive();
} else evidence.cleanupComplete = true;
await lease.close();
await sampleMarker("after_process_cleanup");
evidence.result = evaluateProbe(evidence);
let text = JSON.stringify(evidence, null, 2).replaceAll(token, "[REDACTED]").replaceAll(root, "/fixture/workspace");
if (sessionId) text = text.replaceAll(sessionId, "fixture-session");
text += "\n";
await writeFile(output, text, { mode: 0o600 });
await rm(root, { recursive: true, force: true });
return { passed: evidence.result.passed, failures: evidence.result.failures, output, sha256: createHash("sha256").update(text).digest("hex"), promptRequestsSent: evidence.promptRequestsSent };
}
}
@@ -55,7 +55,7 @@ test("missing credentials reject before resolving a pack or launching anything",
});
test("full credential-free protocol fixture preserves numeric-zero denial and reaps its process", async () => {
const { mkdtemp, mkdir, writeFile, readFile, rm } = await import("node:fs/promises");
const { mkdtemp, mkdir, writeFile, readFile, rm, access } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { runProbe } = await import("./qualify-copilot-acp.mjs");
@@ -69,23 +69,30 @@ test("full credential-free protocol fixture preserves numeric-zero denial and re
await writeFile(join(moduleRoot, "copilot-events.js"), 'export const COPILOT_ACP_CLIENT_CAPABILITIES = {};');
const childScript = join(root, "child.mjs");
await writeFile(childScript, `import { createInterface } from 'node:readline';
const send = m => process.stdout.write(JSON.stringify({jsonrpc:'2.0', ...m})+'\\n');
const send = m => {
const wire=Buffer.from(JSON.stringify({jsonrpc:'2.0', ...m})+'\\n');
const unicode=wire.indexOf(Buffer.from('🚀'));
if(unicode<0){process.stdout.write(wire);return;}
process.stdout.write(wire.subarray(0,unicode+2));
setTimeout(()=>process.stdout.write(wire.subarray(unicode+2)),50);
};
let promptId;
createInterface({input:process.stdin}).on('line', line => {
const m=JSON.parse(line);
if(!m.method){if(m.id!==0 || m.result?.outcome?.optionId!=='native-deny-0') process.exit(8); send({id:promptId,result:{stopReason:'end_turn'}});return;}
let result={};
if(m.method==='initialize')result={agentInfo:{version:'1.0.88'},agentCapabilities:{}};
if(m.method==='initialize')result={agentInfo:{version:'1.0.88'},agentCapabilities:{label:'東京 🚀 café'}};
if(m.method==='session/new')result={sessionId:'fixture-session',models:{availableModels:[{modelId:'gpt-5.6-luna'}]}};
if(m.method==='session/set_config_option')result={configOptions:[{id:'model',currentValue:'gpt-5.6-luna'}]};
if(m.method==='session/prompt'){promptId=m.id;send({id:0,method:'session/request_permission',params:{sessionId:'fixture-session',toolCall:{kind:'edit',rawInput:{fileName:process.cwd()+'/qualification-marker.txt'}},options:${JSON.stringify(options)}}});return;}
send({id:m.id,result});
}).on('close',()=>process.exit(0));`);
await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>spawn(options.env.COPILOT_GITHUB_TOKEN === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options),close:async()=>{}})});`);
await writeFile(join(moduleRoot, "profile-installation.js"), `import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; let token; export const assertAcpxProfileEnvironment=()=>{}; export const verifyAcpxProfileInstallation=async()=>({commandDigest:'fixture-only',openCommand:async()=>({spawn:(_args,options)=>{token=options.env.COPILOT_GITHUB_TOKEN;writeFileSync(${JSON.stringify(join(root, "last-probe-root"))},options.cwd);return spawn(token === "fixture-spawn-failure" ? "/paperclip-fixture-missing-executable" : ${JSON.stringify(process.execPath)},[${JSON.stringify(childScript)}],options);},close:async()=>{if(token==="fixture-close-failure")throw new Error("fixture lease cleanup failed");}})});`);
const output = join(root, "output.json");
const result = await runProbe(root, output, "deny-write", "fixture-token-never-used-for-network");
const evidence = JSON.parse(await readFile(output, "utf8"));
assert.equal(result.passed, true);
assert.deepEqual(evidence.agentCapabilities, { label: "東京 🚀 café" });
assert.equal(evidence.permissions[0].requestId, 0);
assert.deepEqual(evidence.permissions[0].outcome, { outcome: "selected", optionId: "native-deny-0" });
assert.equal(evidence.cleanupComplete, true);
@@ -98,5 +105,11 @@ createInterface({input:process.stdin}).on('line', line => {
assert.equal(failedEvidence.promptRequestsSent, 0);
assert.equal(failedEvidence.cleanupComplete, true);
assert.match(failedEvidence.failureCode, /provider_(spawn|stdin)_failure/);
const lastRoot = async () => await readFile(join(root, "last-probe-root"), "utf8");
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
await assert.rejects(runProbe(root, join(root, "missing/report.json"), "deny-write", "fixture-token-never-used-for-network"), { code: "ENOENT" });
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
await assert.rejects(runProbe(root, join(root, "close-failure.json"), "deny-write", "fixture-close-failure"), /fixture lease cleanup failed/);
await assert.rejects(access(await lastRoot()), { code: "ENOENT" });
} finally { await rm(root, { recursive: true, force: true }); }
});
@@ -36,5 +36,15 @@ export function verifyReleaseProviderPack(identity, sourceRevision, target = "li
throw new Error("Remote provider-pack Cursor identity does not match this release");
}
}
const copilot = payload.providers?.copilot;
const copilotProfile = releaseProfiles.profiles.copilot;
if (Object.hasOwn(payload.candidateProviders ?? {}, "copilot") || !copilot
|| copilot.version !== copilotProfile.agentServerVersion || copilot.profileDigest !== copilotProfile.commandDigest
|| copilot.closureDigest !== "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07"
|| copilot.qualification !== "qualified" || copilot.path !== "provider-assets/copilot/linux-x64"
|| Object.keys(copilot).sort().join(",") !== "closureDigest,path,profileDigest,qualification,sha256,version"
|| !/^sha256:[a-f0-9]{64}$/.test(copilot.sha256 ?? "")) {
throw new Error("Remote provider-pack Copilot identity does not match this release");
}
return identity;
}
@@ -22,6 +22,9 @@ function fixture() {
providers: { cursor: { version: profiles.profiles.cursor.agentServerVersion, profileDigest: profiles.profiles.cursor.commandDigest,
closureDigest: `sha256:${distributions.platforms["linux-x64"].closureSha256}`, qualification: "qualified",
path: "provider-assets/cursor/linux-x64", sha256: digest } } };
payload.providers.copilot = { version: profiles.profiles.copilot.agentServerVersion,
profileDigest: profiles.profiles.copilot.commandDigest, closureDigest: "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07",
qualification: "qualified", path: "provider-assets/copilot/linux-x64", sha256: digest };
return payload;
}
function seal(payload) { return { schema: "paperclip-runner/remote-provider-pack/v1", payload,
@@ -95,6 +98,19 @@ test("daemon-only assembly retains an existing same-source Linux provider-pack i
} finally { rmSync(root, { recursive: true, force: true }); }
});
for (const field of ["version", "profileDigest", "closureDigest", "qualification", "path", "sha256"]) {
test(`rejects an independently rehashed stale Copilot ${field}`, () => {
const payload = fixture(); payload.providers.copilot[field] = "stale";
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision), /Copilot identity/);
});
}
test("rejects omitted Copilot and a duplicate candidate inventory", () => {
const missing = fixture(); delete missing.providers.copilot;
assert.throws(() => verifyReleaseProviderPack(seal(missing), revision), /Copilot identity/);
const mixed = fixture(); mixed.candidateProviders = { copilot: mixed.providers.copilot };
assert.throws(() => verifyReleaseProviderPack(seal(mixed), revision), /Copilot identity/);
});
function armFixture() {
const payload = fixture();
payload.target.architecture = "arm64";
@@ -147,3 +163,12 @@ test("frozen image validation uses release profiles rather than the newer workfl
payload.acpxProfileDigests.claude = "sha256:" + "d".repeat(64);
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision, "linux-x64", expected), /profiles/);
});
test("uses the frozen release's Copilot profile when the workflow checkout has advanced", () => {
const releaseProfiles = structuredClone(profiles);
releaseProfiles.profiles.copilot.commandDigest = "sha256:" + "e".repeat(64);
const payload = fixture(); payload.providers.copilot.profileDigest = releaseProfiles.profiles.copilot.commandDigest;
const manifest = seal(payload);
assert.equal(verifyReleaseProviderPack(manifest, revision, "linux-x64", { profiles: releaseProfiles, distributions }), manifest);
assert.throws(() => verifyReleaseProviderPack(manifest, revision), /Copilot identity/);
});
@@ -497,9 +497,11 @@ describe("HarnessDriverBackend", () => {
it("allows only the run id to change when a harness session is attached", async () => {
const attachedRunIds: string[] = [];
const grants: unknown[] = [];
class AttachableHarnessSession extends FakeHarnessSession {
async attachRun(input: { runId: string }) {
async attachRun(input: { runId: string; currentRunGrant?: { runtimeContext: null; instructions: string } }) {
attachedRunIds.push(input.runId);
grants.push(input.currentRunGrant);
}
}
const backend = new HarnessDriverBackend({
@@ -533,8 +535,10 @@ describe("HarnessDriverBackend", () => {
await expect(session.attachRun?.({
identity: { ...originalIdentity, runId: "run-2" },
currentRunGrant: { runtimeContext: null, instructions: "Current registered instructions." },
})).resolves.toBeUndefined();
expect(attachedRunIds).toEqual(["run-2"]);
expect(grants).toEqual([{ runtimeContext: null, instructions: "Current registered instructions." }]);
expect(session.identity()).toEqual({ ...originalIdentity, runId: "run-2" });
});
@@ -13,6 +13,7 @@ import type {
PersistedNativeSession,
} from "../contracts/native-session-backend.js";
import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js";
import type { NativeRunRuntimeGrant } from "../contracts/runtime-context.js";
import type {
PrpEvent,
PrpTerminalState,
@@ -474,6 +475,7 @@ class HarnessNativeSession implements NativeSession {
async attachRun(input: {
identity: OpenNativeSessionInput["identity"];
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void> {
this.#assertProtocolIntegrity();
const currentIdentity = this.#input.identity;
@@ -489,7 +491,9 @@ class HarnessNativeSession implements NativeSession {
throw new Error("native_session_multi_run_unavailable");
}
try {
await this.#session.attachRun({ runId: input.identity.runId });
await this.#session.attachRun({ runId: input.identity.runId,
...(input.currentRunGrant ? { currentRunGrant: structuredClone(input.currentRunGrant) } : {}),
});
this.#assertProtocolIntegrity();
} catch (error) {
this.#rethrowProtocolIntegrity(error);
@@ -600,8 +600,9 @@ describe("native backend factory", () => {
it.each(["v5", "v7", "v9"] as const)("selects prepared transport for %s native input and retains prepared constraints", async version => {
const input = preparedExecution(execution(), version);
const transport = new FakeCodexTransport();
let currentInstructions: string | undefined;
const backend = createCodexNativeSessionBackend(input, {
transportFactory: () => transport,
transportFactory: (context) => { currentInstructions = context?.baseInstructions; return transport; },
workingDirectoryAuthority: "remote_runner",
environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE },
});
@@ -619,6 +620,8 @@ describe("native backend factory", () => {
expect(
transport.calls.find((call) => call.method === "thread/start")?.params,
).toMatchObject({ conversationMode: "prepared" });
expect(currentInstructions).toContain("Keep assigned instructions.");
expect(currentInstructions).toBe(transport.calls.find((call) => call.method === "thread/start")?.params.developerInstructions);
expect(backend.preparedTaskConstraints).toEqual(
expect.arrayContaining([
expect.stringContaining("Obtain one accepted result"),
@@ -37,7 +37,7 @@ import {
normalizeAcpFormElicitation,
type NormalizedAcpForm,
} from "../drivers/acpx/acp-question-adapter.js";
import { openCodexAcpxRuntime } from "../drivers/acpx/codex-runtime-adapter.js";
import { openCodexAcpxRuntime, AcpxProviderProcessLostError } from "../drivers/acpx/codex-runtime-adapter.js";
import { AcpxApprovalRequiredError } from "../drivers/acpx/permission-policy.js";
import { normalizeAcpxPermission, type NormalizedAcpxPermission } from "../drivers/acpx/acp-permission-adapter.js";
import { acpxGoalProjection } from "../drivers/acpx/session-goals.js";
@@ -683,7 +683,7 @@ async function pumpTurn(
terminal = {
status: "failed",
error: {
...(error instanceof AcpxApprovalRequiredError ? { code: error.code } : {}),
...(error instanceof AcpxApprovalRequiredError || error instanceof AcpxProviderProcessLostError ? { code: error.code } : {}),
message: safeMessage(error),
retryable: false,
},
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { probeCopilotMetadata } from "../drivers/copilot-metadata-probe.js";
const result = await probeCopilotMetadata(process.env.COPILOT_GITHUB_TOKEN ?? "", process.env.PAPERCLIP_COPILOT_PROBE_MODEL);
process.stdout.write(`${JSON.stringify(result)}\n`);
if (result.status === "failed") process.exitCode = 1;
@@ -141,6 +141,25 @@ describe("eval-session request contract", () => {
} finally { await rm(workspace, { recursive: true, force: true }); }
});
it("requires bounded exact Copilot profile admission before loading provider state", async () => {
const args = ["--request", "/tmp/request.json", "--output", "/tmp/result.json", "--candidate-profile", "copilot"];
expect(() => parseEvalSessionCliArgs(args)).toThrow("require --expected-acpx-profile");
for (const invalid of ["[]", "null", "malformed", JSON.stringify({ overflow: "x".repeat(4096) })]) {
expect(() => parseEvalSessionCliArgs([...args, "--expected-acpx-profile", invalid])).toThrow();
}
const profile = resolveQualifiedAcpxProfile("copilot", "gpt-5.6-luna");
expect(parseEvalSessionCliArgs([...args, "--expected-acpx-profile", JSON.stringify(profile)]).expectedAcpxProfile).toEqual(profile);
const workspace = await mkdtemp(join(tmpdir(), "eval-profile-admission-"));
try {
const path = join(workspace, "request.json");
await writeFile(path, JSON.stringify(request({ provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" })));
const factory = vi.fn();
await expect(runEvalSessionCli(["--request", path, "--output", join(workspace, "output.json"), "--candidate-profile", "copilot", "--expected-acpx-profile", JSON.stringify({ ...profile, commandDigest: `sha256:${"0".repeat(64)}` })], { serviceFactory: factory })).rejects.toThrow("does not match the built runner profile");
expect(factory).not.toHaveBeenCalled();
expect(await readdir(workspace)).toEqual(["request.json"]);
} finally { await rm(workspace, { recursive: true, force: true }); }
});
it("materializes a production-v3 runtime context for direct live providers", async () => {
const workspace = await mkdtemp(join(tmpdir(), "paperclip-eval-context-"));
let instructionRoot: string | null = null;
@@ -1,3 +1,4 @@
import type { NativeRunRuntimeGrant } from "./runtime-context.js";
import type {
PrpEvent,
PrpStructuredRunResult,
@@ -508,7 +509,7 @@ export interface HarnessSession {
displayId?: string | null;
};
events(): AsyncIterable<PrpEvent>;
attachRun?(input: { runId: string }): Promise<void> | void;
attachRun?(input: { runId: string; currentRunGrant?: NativeRunRuntimeGrant }): Promise<void> | void;
startTurn(input: {
message: NativeUserMessage;
/** Set by orchestration only after successful provider-session recovery. */
@@ -428,16 +428,40 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("does not match");
});
it("decodes saved Copilot v17 input after the current source declaration advances", () => {
const historical = JSON.parse(readFileSync(new URL("../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
it.each([17, 43, 44] as const)("decodes saved Copilot v%s input after the current source declaration advances", (version) => {
const historical = JSON.parse(readFileSync(new URL(`../../test/fixtures/copilot-profile-v${version}-identity.json`, import.meta.url), "utf8"));
const { permissionPolicy, qualificationStatus: _status, ...currentProfile } = QUALIFIED_ACPX_PROFILES.copilot;
const provider = { kind: "acpx", agent: "copilot", model: "explicit-provider-model", permissionPolicy,
profile: { ...currentProfile, agentProfileVersion: 17, commandDigest: historical.commandDigest } };
profile: { ...currentProfile, agentProfileVersion: version, commandDigest: historical.commandDigest } };
const parsed = parseNativeExecutionInput({ ...input, session: { ...input.session, driverKind: "acpx_runtime" }, provider });
expect(parsed.provider).toEqual(provider);
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
});
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId,
permissionPolicy, qualificationStatus: _qualificationStatus, modelPolicy: _modelPolicy,
...snapshot } = QUALIFIED_ACPX_PROFILES.copilot;
const provider = {
kind: "acpx", agent: "copilot", model: "gpt-5.6-luna", permissionPolicy,
profile: { ...snapshot, agentProfileVersion },
} as const;
const value = { ...input, session: { ...input.session, driverKind: "acpx_runtime" }, provider };
const parsed = parseNativeExecutionInput(value);
expect(parsed.provider).toEqual(provider);
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
for (const unsupportedVersion of [0, QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion + 1, 1.5, "34", null]) {
expect(() => parseNativeExecutionInput({
...value, provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } },
})).toThrow("qualified ACPX v1 profile");
}
expect(() => parseNativeExecutionInput({
...value, provider: { ...provider, profile: { ...provider.profile, agent: "pi" } },
})).toThrow("qualified ACPX v1 profile");
expect(() => parseNativeExecutionInput({
...value, session: { ...value.session, driverKind: "opencode_server" },
})).toThrow("does not match");
});
it.each(Object.values(QUALIFIED_ACPX_PROFILES))(
"admits the current $agent profile through the native execution boundary",
(declaration) => {
@@ -595,6 +619,46 @@ describe("native task context ownership", () => {
});
}
const { qualificationModel: _copilotQualificationModel, reportedModelId: _copilotReportedModelId,
permissionPolicy: _copilotPermissionPolicy, qualificationStatus: _copilotQualificationStatus, modelPolicy: _copilotModelPolicy,
...copilotSnapshot } = QUALIFIED_ACPX_PROFILES.copilot;
it.each([
{ driverKind: "acpx_runtime", provider: { kind: "acpx", agent: "copilot", model: "gpt-5.6-luna", permissionMode: "deny-all", profile: copilotSnapshot } },
{ driverKind: "opencode_server", provider: { kind: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731", permissionMode: "deny" } },
{ driverKind: "acpx_runtime", provider: {
kind: "acpx", agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", permissionMode: "deny-all",
profile: { driverKind: "acpx_runtime", protocolVersion: 1, acpxVersion: "0.13.1", agent: "pi", agentProfileVersion: 1,
agentServerPackage: "pi-acp", agentServerVersion: "0.0.33", agentRuntimePackage: "@earendil-works/pi-coding-agent",
agentRuntimeVersion: "0.84.2", commandDigest: `sha256:${"a".repeat(64)}` },
} },
{ driverKind: "openai_dot_mcp", provider: {
kind: "openai_dot", model: null,
binding: { bindingId: "dot-binding", bindingGeneration: 1, companyId: input.binding.companyId, agentId: input.binding.agentId,
acceptByUnixMs: 1_000, expiresAtUnixMs: 2_000 },
} },
])("preserves an unregistered saved prompt for $provider.kind", ({ driverKind, provider }) => {
const current = currentInput();
if (!("runtimeContext" in current)) throw new Error("Expected a runtime context");
const text = "Saved instructions absent from the current release.";
const context = { ...current.runtimeContext,
prompt: { revision: "saved-prompt-before-upgrade", text, digest: createHash("sha256").update(text).digest("hex") } };
context.aggregateDigest = canonicalNativeRuntimeContextDigest(context);
const persisted = JSON.parse(JSON.stringify({
...current, provider, session: { ...current.session, driverKind }, runtimeContext: context,
...(provider.kind === "openai_dot" ? {
schema: NATIVE_EXECUTION_INPUT_SCHEMA_V6,
workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
credentialBindings: [],
} : {}),
}));
const recovered = parseNativeExecutionInput(persisted);
expect(recovered.runtimeContext).toEqual(context);
expect(recovered.provider).toEqual(provider);
expect(recovered.session.driverKind).toBe(driverKind);
expect(parseNativeExecutionInput(recovered)).toEqual(recovered);
});
it("carries an opaque provider mode without a vendor restriction and fences obsolete field names", () => {
const current = currentInput();
const provider = {
@@ -96,6 +96,9 @@ export type NativeOpenCodePermissionMode = "allow" | "ask" | "deny";
export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
export type NativeCursorMode = "agent" | "plan" | "ask";
/** Decodable history; current immutable admission comes from the shared manifest. */
export type NativeAcpxProfileVersion = AcpxProfileVersion;
export interface NativeAcpxProfileSnapshot {
driverKind: "acpx_runtime";
protocolVersion: 1;
@@ -1,3 +1,4 @@
import type { NativeRunRuntimeGrant } from "./runtime-context.js";
import type {
NativeRunIdentity,
NativeSessionCapabilities,
@@ -185,7 +186,7 @@ export class NativeSessionCleanupQuarantinedError extends Error {
export interface NativeSession {
identity(): NativeRunIdentity;
capabilities(): Promise<NativeSessionCapabilities>;
attachRun?(input: { identity: NativeRunIdentity }): Promise<void>;
attachRun?(input: { identity: NativeRunIdentity; currentRunGrant?: NativeRunRuntimeGrant }): Promise<void>;
/** Relinquish controller authority without suspending provider execution. */
detachControllerForRestart?(): Promise<void>;
events(input?: { afterCursor?: string | null }): AsyncIterable<PrpEvent>;
@@ -27,6 +27,12 @@ export interface NativeRuntimeContextSnapshot {
aggregateDigest: string;
}
/** Authenticated current-run grants; immutable session policy is verified separately. */
export interface NativeRunRuntimeGrant {
runtimeContext: NativeRuntimeContextSnapshot | null;
instructions: string;
}
export class NativeRuntimeContextError extends Error {
readonly code = "native_runtime_context_invalid" as const;
constructor(message: string) { super(message); this.name = "NativeRuntimeContextError"; }
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import type { AcpPermissionRequest } from "acpx/runtime";
import { normalizeAcpxPermission } from "./acp-permission-adapter.js";
import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createCopilotToolEvidence } from "./copilot-tool-evidence.js";
function request(kinds = ["allow_once", "allow_always", "reject_once"]): AcpPermissionRequest {
return { sessionId: "session", inferredKind: "edit", raw: {
@@ -51,6 +55,42 @@ describe("ACP permission normalization", () => {
const duplicate = request(); duplicate.raw.options[1]!.optionId = duplicate.raw.options[0]!.optionId;
expect(() => normalizeAcpxPermission(duplicate)).toThrow("ambiguous");
});
it("keeps a native absolute workspace alias answerable with the same projected target", () => {
const root = mkdtempSync(join(tmpdir(), "copilot-permission-alias-"));
try {
const workspace = join(root, "workspace"), alias = join(root, "alias");
mkdirSync(workspace); mkdirSync(join(workspace, "nested")); symlinkSync(workspace, alias);
const path = join(alias, "nested", "new.txt"), value = request();
value.raw.toolCall = { toolCallId: "patch-call", kind: "edit", rawInput: { fileName: path }, locations: [{ path }] };
const cwd = realpathSync(workspace), normalized = normalizeAcpxPermission(value, { provider: "copilot", workingDirectory: cwd });
expect(normalized.title).toBe("Change file: nested/new.txt");
expect(normalized.resolve({ action: "accept" })).toEqual({ outcome: "allow_once" });
const events: any[] = [];
const projection = createCopilotToolEvidence({ sessionId: "session", turnId: "turn", workingDirectory: cwd, active: () => true, emit: event => events.push(event) });
projection.permission(value, "permission", normalized.choices.map(choice => choice.key));
expect(events[0].payload.details).toContainEqual({ name: "target", value: "nested/new.txt" });
writeFileSync(join(workspace, "nested", "new.txt"), "existing");
expect(normalizeAcpxPermission(value, { provider: "copilot", workingDirectory: cwd }).title).toBe(normalized.title);
} finally { rmSync(root, { recursive: true, force: true }); }
});
it("refuses outside, dangling, conflicting and unresolvable alias targets", () => {
const root = mkdtempSync(join(tmpdir(), "copilot-permission-alias-"));
try {
const workspace = join(root, "workspace"), alias = join(root, "alias"), outside = join(root, "outside");
mkdirSync(workspace); mkdirSync(outside); symlinkSync(workspace, alias);
symlinkSync(join(outside, "missing.txt"), join(workspace, "dangling.txt"));
symlinkSync(outside, join(workspace, "escape"));
writeFileSync(join(outside, "existing.txt"), "outside");
for (const path of [join(alias, "dangling.txt"), join(alias, "escape", "new.txt"), join(alias, "escape", "existing.txt"), join(alias, "missing-parent", "new.txt"), join(outside, "new.txt")]) {
const value = request(); value.raw.toolCall = { toolCallId: "patch-call", kind: "edit", rawInput: { fileName: path }, locations: [{ path }] };
const normalized = normalizeAcpxPermission(value, { provider: "copilot", workingDirectory: realpathSync(workspace) });
expect(normalized.title).toContain("target unavailable");
expect(normalized.choices.map(choice => choice.key)).toEqual(["decline", "cancel"]);
}
const conflict = request(); conflict.raw.toolCall = { toolCallId: "patch-call", kind: "edit", rawInput: { fileName: join(alias, "one.txt") }, locations: [{ path: join(alias, "two.txt") }] };
expect(normalizeAcpxPermission(conflict, { provider: "copilot", workingDirectory: realpathSync(workspace) }).title).toContain("target unavailable");
} finally { rmSync(root, { recursive: true, force: true }); }
});
});
@@ -2,6 +2,8 @@ import type { AcpPermissionDecision, AcpPermissionRequest } from "acpx/runtime";
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
import { safeCopilotEditTarget } from "./copilot-permission-context.js";
export type AcpxPermissionAction = "accept" | "accept_for_session" | "decline" | "cancel";
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
import { describe, expect, it, vi } from "vitest";
import { AcpxProviderProcessLostError } from "./codex-runtime-adapter.js";
import type { AcpRuntimeEvent } from "acpx/runtime";
@@ -1686,6 +1687,35 @@ describe("Codex ACPX harness driver", () => {
});
});
it("expires a Copilot permission on provider death and rejects its late answer", async () => {
const failure = deferred<void>();
const fixture = driverFixture({ agent: "copilot", model: "gpt-5.6-luna", providerPolicy: { readOnly: false } }, { eventStreamFailure: failure.promise });
const session = await fixture.driver.openSession({ runId: "run-provider-death", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
const created = collectUntil(session.events(), "runtime_request.created");
const { turnId } = await session.startTurn({ message: { text: "Edit the file" } });
const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!;
const controller = new AbortController();
const response = callback({ inferredKind: "edit", raw: {
sessionId: "agent-session-1", toolCall: { toolCallId: "dead-edit", title: "Edit file" },
options: [{ optionId: "once", kind: "allow_once", name: "Allow once" }],
} } as Parameters<typeof callback>[0], { signal: controller.signal, responseDelivery: Promise.resolve() });
await created;
const request = session.pendingRuntimeRequests!()[0]!;
const expired = collectUntil(session.events(), "runtime_request.expired");
const error = new AcpxProviderProcessLostError();
controller.abort(error);
await expect(response).resolves.toEqual({ outcome: "cancel" });
expect((await expired).at(-1)).toMatchObject({ eventType: "runtime_request.expired", payload: {
requestId: request.requestId, reason: "provider_process_lost", replayAllowed: false,
} });
await expect(session.resolveRuntimeRequest!({ requestId: request.requestId, turnId, resolution: { action: "accept" } })).rejects.toThrow("no longer pending");
const terminal = collectUntil(session.events(), "turn.failed");
failure.reject(error);
expect((await terminal).at(-1)).toMatchObject({ payload: { error: { code: "ACPX_PROVIDER_PROCESS_LOST" } } });
expect(session.pendingRuntimeRequests!()).toEqual([]);
await session.close({ reason: "death verified" });
});
it("delivers only offered permission choices and rejects stale or duplicate answers", async () => {
const fixture = driverFixture({ agent: "claude", model: "claude-sonnet-5" });
const session = await fixture.driver.openSession({
@@ -60,6 +60,7 @@ import {
} from "../runner-tool-bridge.js";
import {
DEFAULT_CODEX_ACPX_RUNTIME_SHUTDOWN_BOUND_MS,
AcpxProviderProcessLostError,
openCodexAcpxRuntime,
} from "./codex-runtime-adapter.js";
import { normalizeAcpxPermission } from "./acp-permission-adapter.js";
@@ -1581,7 +1582,7 @@ class CodexAcpxSession implements HarnessSession {
} catch (error) {
if (this.#terminalTurns.has(turnId)) return;
if (error instanceof TerminalEventCapacityError) throw error;
this.#cancelPendingRuntimeRequests("provider turn failed", turnId);
this.#cancelPendingRuntimeRequests(error instanceof AcpxProviderProcessLostError ? error : "provider turn failed", turnId);
if (this.#closed || this.#closingStarted) {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === turnId
@@ -1612,7 +1613,7 @@ class CodexAcpxSession implements HarnessSession {
: { reaffirmedSemanticResult }),
}),
"turn.failed",
{ status: "failed", error: { message: safeMessage(error) } },
{ status: "failed", error: { ...(error instanceof AcpxProviderProcessLostError ? { code: error.code } : {}), message: safeMessage(error) } },
);
}
}
@@ -1756,9 +1757,7 @@ class CodexAcpxSession implements HarnessSession {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling || !this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emit("runtime_request.cancelled", harnessRuntimeRequestOutcome(request, {
action: "cancel", reason: "provider request aborted",
}), { turnId, itemId });
this.#emitRequestAbort(request, context.signal.reason);
settle(input.cancel());
};
this.#pendingRuntimeRequests.set(requestId, {
@@ -1807,9 +1806,7 @@ class CodexAcpxSession implements HarnessSession {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling || !this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emit("runtime_request.cancelled", harnessRuntimeRequestOutcome(runtimeRequest, {
action: "cancel", reason: "provider request aborted",
}), { turnId, itemId: requestId });
this.#emitRequestAbort(runtimeRequest, signal.reason);
settle({ outcome: "cancel" });
};
this.#pendingRuntimeRequests.set(requestId, {
@@ -1943,14 +1940,7 @@ class CodexAcpxSession implements HarnessSession {
if (!pending || pending.settling) return;
if (!this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emit(
"runtime_request.cancelled",
harnessRuntimeRequestOutcome(runtimeRequest, {
action: "cancel",
reason: "provider request aborted",
}),
{ turnId, itemId: requestId },
);
this.#emitRequestAbort(runtimeRequest, context.signal.reason);
settle({ action: "cancel" });
};
context.signal.addEventListener("abort", cancel, { once: true });
@@ -1968,22 +1958,23 @@ class CodexAcpxSession implements HarnessSession {
});
}
#cancelPendingRuntimeRequests(reason: string, turnId?: string): void {
#emitRequestAbort(request: HarnessRuntimeRequest, reason: unknown): void {
const lost = reason instanceof AcpxProviderProcessLostError;
this.#emit(lost ? "runtime_request.expired" : "runtime_request.cancelled", {
...harnessRuntimeRequestOutcome(request, {
...(lost ? {} : { action: "cancel" as const }),
reason: lost ? "provider_process_lost" : typeof reason === "string" ? boundedText(reason, 1_000) : "provider request aborted",
}),
...(lost ? { replayAllowed: false } : {}),
}, { turnId: request.turnId, itemId: request.itemId });
}
#cancelPendingRuntimeRequests(reason: string | AcpxProviderProcessLostError, turnId?: string): void {
for (const [requestId, pending] of this.#pendingRuntimeRequests) {
if (turnId && pending.request.turnId !== turnId) continue;
if (!this.#pendingRuntimeRequests.delete(requestId)) continue;
pending.cleanup();
this.#emit(
"runtime_request.cancelled",
harnessRuntimeRequestOutcome(pending.request, {
action: "cancel",
reason: boundedText(safeMessage(reason), 1_000),
}),
{
turnId: pending.request.turnId,
itemId: pending.request.itemId,
},
);
this.#emitRequestAbort(pending.request, reason);
pending.cancel();
}
}
@@ -830,9 +830,10 @@ describe("managed Codex credentials", () => {
it.runIf(process.platform !== "win32")(
"shares the four-slot parent filesystem budget across fresh modules",
async () => {
const fixtures = await Promise.all(
Array.from({ length: 5 }, () => credentialFixture()),
);
// Reserve fixture quorum candidates before holding filesystem operations.
// Otherwise an unrelated loopback listener can reject staging before
// fourOpens resolves, leaving an unhandled rejection and a false timeout.
const fixtures = await credentialFixturesWithAvailableQuorum(5);
const retainedHomes = new Set(
fixtures.slice(0, 4).map((fixture) => fixture.home),
);
@@ -1669,6 +1670,38 @@ async function credentialFixture(): Promise<{ root: string; home: string }> {
return { root, home: await realpath(home) };
}
async function credentialFixturesWithAvailableQuorum(count: number) {
const fixtures: Awaited<ReturnType<typeof credentialFixture>>[] = [];
const reservations: Array<Awaited<ReturnType<typeof listenSilently>>> = [];
try {
for (let index = 0; index < count; index++) {
let prepared = false;
for (let attempt = 0; attempt < 8; attempt++) {
const fixture = await credentialFixture();
const owned: Array<Awaited<ReturnType<typeof listenSilently>>> = [];
try {
for (const port of credentialLeasePorts(fixture.home)) {
owned.push(await listenSilently(port));
}
fixtures.push(fixture);
reservations.push(...owned);
prepared = true;
break;
} catch (error) {
await Promise.all(owned.map((listener) => listener.close()));
if ((error as NodeJS.ErrnoException).code !== "EADDRINUSE" || attempt === 7) throw error;
}
}
if (!prepared) throw new Error("Credential fixture quorum preparation failed");
}
return fixtures;
} finally {
// Fixture setup alone may choose another unused home. Production staging
// still runs once, and a foreign bind racing this handoff remains a failure.
await Promise.all(reservations.map((listener) => listener.close()));
}
}
async function silentPrimaryQuorumFixture(
nextFixture = credentialFixture,
occupiedIndex: 0 | 1 | "quorum" = 0,
@@ -127,6 +127,141 @@ describe("Codex ACPX runtime adapter", () => {
expect(createRuntime).not.toHaveBeenCalled();
});
it.each(["blocked", "ended"] as const)("settles a %s Copilot stream on verified provider death without replaying the permission", async (streamState) => {
const pending = pendingExtensionTurn("death-turn");
if (streamState === "ended") pending.turn.events = (async function* () {})();
const runtime = fakeRuntime();
vi.mocked(runtime.startTurn).mockReturnValue(pending.turn);
const child = fakeChild();
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValue(child);
let exit!: () => void;
const providerExit = new Promise<void>(resolve => { exit = resolve; });
let created!: AcpRuntimeOptions;
const options = openOptions(command);
options.profile = { ...options.profile, agent: "copilot" };
options.permissionMode = "approve-paperclip";
const port = await openCodexAcpxRuntime(options, {
createRegistry: () => registry(), createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished,
awaitProviderExit: () => providerExit,
createRuntime: value => { created = value; return runtimeWithProvider(runtime, value); },
});
let answer!: (value: { outcome: "allow_once" }) => void;
let callbackSignal!: AbortSignal;
const handler = vi.fn((_request, context) => {
callbackSignal = context.signal;
return new Promise<{ outcome: "allow_once" }>(resolve => { answer = resolve; });
});
const turn = port.startTurn({ text: "Write the file", requestId: "death-turn", onPermissionRequest: handler });
await turn.promptStarted;
const events = turn.events[Symbol.asyncIterator]();
const draining = expect(events.next()).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const settled = expect(turn.result).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const request = created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal: new AbortController().signal });
await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce());
// The guardian remains live: only independent provider EOF proves death.
expect(child.exitCode).toBeNull();
exit();
await Promise.all([draining, settled]);
expect(callbackSignal.aborted).toBe(true);
answer({ outcome: "allow_once" });
await expect(request).resolves.toEqual({ outcome: "cancel" });
expect(() => port.startTurn({ text: "Replay", requestId: "replay" })).toThrow("provider exited");
expect(runtime.startTurn).toHaveBeenCalledOnce();
pending.settle();
await port.close({ reason: "test finished" });
expect(child.kill).toHaveBeenCalled();
});
it("refuses Copilot replay when native disconnection precedes the independent exit proof", async () => {
const pending = pendingExtensionTurn("disconnect-turn");
let disconnect!: () => void;
const failed = new Promise<{ status: "failed"; error: { code: "RUNTIME"; detailCode: "AGENT_DISCONNECTED"; message: string } }>(resolve => {
disconnect = () => resolve({ status: "failed", error: { code: "RUNTIME", detailCode: "AGENT_DISCONNECTED", message: "Native ACP transport disconnected" } });
});
const runtime = fakeRuntime();
vi.mocked(runtime.startTurn).mockReturnValue({ ...pending.turn, result: failed });
const child = fakeChild();
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValue(child);
let exit!: () => void;
const providerExit = new Promise<void>(resolve => { exit = resolve; });
let created!: AcpRuntimeOptions;
const options = openOptions(command);
options.profile = { ...options.profile, agent: "copilot" };
options.permissionMode = "approve-paperclip";
const port = await openCodexAcpxRuntime(options, {
createRegistry: () => registry(), createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished,
awaitProviderExit: () => providerExit,
createRuntime: value => { created = value; return runtimeWithProvider(runtime, value); },
});
let answer!: (value: { outcome: "allow_once" }) => void;
let callbackSignal!: AbortSignal;
const handler = vi.fn((_request, context) => {
callbackSignal = context.signal;
return new Promise<{ outcome: "allow_once" }>(resolve => { answer = resolve; });
});
try {
const turn = port.startTurn({ text: "Write", requestId: "disconnect-turn", onPermissionRequest: handler });
await turn.promptStarted;
const request = created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal: new AbortController().signal });
await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce());
disconnect();
await expect(turn.result).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
await expect((async () => { for await (const _event of turn.events) { /* drain */ } })())
.rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
expect(callbackSignal.aborted).toBe(true);
answer({ outcome: "allow_once" });
await expect(request).resolves.toEqual({ outcome: "cancel" });
expect(() => port.startTurn({ text: "Replay", requestId: "replay" })).toThrow("provider exited");
expect(runtime.startTurn).toHaveBeenCalledOnce();
// A typed disconnect does not manufacture independent process-exit proof.
// Cleanup still owns the live guardian and waits for the separate proof.
expect(child.exitCode).toBeNull();
} finally {
pending.settle();
exit();
await port.close({ reason: "disconnect race verified" });
}
});
it("keeps a Copilot callback deliverable when only the guardian exits", async () => {
const pending = pendingExtensionTurn("preserved-turn");
const runtime = fakeRuntime();
vi.mocked(runtime.startTurn).mockReturnValue(pending.turn);
let created!: AcpRuntimeOptions;
const child = fakeChild();
const command = fakeCommand();
vi.mocked(command.spawn).mockReturnValue(child);
let exit!: () => void;
const providerExit = new Promise<void>(resolve => { exit = resolve; });
const options = openOptions(command);
options.profile = { ...options.profile, agent: "copilot" };
options.permissionMode = "approve-paperclip";
const port = await openCodexAcpxRuntime(options, {
createRegistry: () => registry(), createStore: () => store(),
awaitProviderOwnership: providerOwnershipEstablished, awaitProviderExit: () => providerExit,
createRuntime: value => { created = value; return runtimeWithProvider(runtime, value); },
});
let answer!: (value: { outcome: "allow_once" }) => void;
const handler = vi.fn(() => new Promise<{ outcome: "allow_once" }>(resolve => { answer = resolve; }));
const turn = port.startTurn({ text: "Write", requestId: "preserved-turn", onPermissionRequest: handler });
const request = created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal: new AbortController().signal });
await vi.waitFor(() => expect(handler).toHaveBeenCalledOnce());
child.exitCode = 0;
child.emit("exit", 0, null);
// Guardian exit cannot replace the provider-only lifetime observation.
await new Promise(resolve => setTimeout(resolve, 10));
answer({ outcome: "allow_once" });
await expect(request).resolves.toEqual({ outcome: "allow_once" });
pending.settle();
await expect(turn.result).resolves.toMatchObject({ status: "completed" });
exit();
await port.close({ reason: "test finished" });
});
it("installs per-connection policy authority only for Copilot and rejects native commands before a turn starts", async () => {
for (const agent of ["copilot", "codex", "claude", "grok"] as const) {
const runtime = fakeRuntime();
@@ -104,6 +104,14 @@ interface AcpxRuntimeExtensionBoundary {
steering: AcpxRuntimeSteeringCapability | null;
}
export class AcpxProviderProcessLostError extends Error {
readonly code = "ACPX_PROVIDER_PROCESS_LOST";
constructor(message = "The owned ACPX provider exited during its active turn") {
super(message);
this.name = "AcpxProviderProcessLostError";
}
}
class AcpxRuntimeCloseTimeoutError extends Error {
constructor() {
super("ACPX runtime close timed out");
@@ -644,6 +652,7 @@ export async function openQualifiedAcpxRuntime(
commandLaunches,
permissionBoundary,
extensionBoundary,
options.profile.agent === "copilot",
options.profile.agent === "copilot" ? assertCopilotPromptPolicy : undefined,
);
} catch (error) {
@@ -1074,6 +1083,7 @@ function runtimePort(
commandLaunches: { count: number; refreshConsumedCommand?: () => Promise<void> },
permissionBoundary: { active: AbortController | null; hasAdmittedTurn: boolean; handler?: AcpRuntimeOptions["onPermissionRequest"] },
extensionBoundary: AcpxRuntimeExtensionBoundary,
monitorProviderDeath: boolean,
assertPromptPolicy?: (text: string) => void,
): AcpxRuntimePort {
extensionBoundary.sessionIds = new Set([identity.backendSessionId]);
@@ -1117,6 +1127,7 @@ function runtimePort(
pendingExternalIntent: boolean;
};
let runtimeClosed = false;
let providerProcessLost = false;
let runtimeCloseAttempt: RuntimeCloseAttempt | undefined;
let lateReconciliationOwner: Promise<void> | undefined;
// Each independently observed late failure receives a bounded reconciliation
@@ -1432,10 +1443,12 @@ function runtimePort(
}
: {}),
startTurn(input) {
if (providerProcessLost) throw new AcpxProviderProcessLostError();
assertPromptPolicy?.(input.text);
if (extensionBoundary.active) throw new Error("ACPX runtime already has an active turn");
const approval = new AbortController();
const controller = new AbortController();
const providerDeath = new AbortController();
const extensionTurn: AcpxRuntimeExtensionTurn = {
requestId: input.requestId, sessionId: identity.backendSessionId, controller,
signal: input.signal ? AbortSignal.any([controller.signal, input.signal, approval.signal]) : AbortSignal.any([controller.signal, approval.signal]),
@@ -1443,7 +1456,25 @@ function runtimePort(
onRequest: input.onExtensionRequest, onNotification: input.onExtensionNotification,
};
extensionBoundary.active = extensionTurn;
// A guardian can outlive the provider while ACP awaits a callback.
// Native exit or a typed ACP agent disconnect retires this turn; losing
// the controller connection must preserve deliverable input instead.
// Cleanup still requires the independent provider-only lifetime proof.
const retireProviderTurn = (error = new AcpxProviderProcessLostError()): void => {
providerProcessLost = true;
providerDeath.abort(error);
approval.abort(error);
controller.abort(error);
};
const stopWatchingProviderExit = !monitorProviderDeath
? () => undefined
: children.watchProviderExit(() => {
if (!runtimeCloseAttempt && !runtimeClosed && !extensionTurn.signal.aborted && extensionBoundary.active === extensionTurn) {
retireProviderTurn();
}
});
const releaseExtensionTurn = (): void => {
stopWatchingProviderExit();
controller.abort(new Error("ACPX extension turn expired"));
if (extensionBoundary.active === extensionTurn) extensionBoundary.active = null;
};
@@ -1473,9 +1504,18 @@ function runtimePort(
throw error;
}
const guarded = turnWithVerifiedLifetimeOwnership(turn, finishOwnershipAdmission);
extensionTurn.promptStarted = guarded.promptStarted;
const result = guarded.result.then(
const promptStarted = abortableExtensionResult(guarded.promptStarted, providerDeath.signal);
void promptStarted.catch(() => undefined);
extensionTurn.promptStarted = promptStarted;
const result = abortableExtensionResult(guarded.result, providerDeath.signal).then(
(value) => {
if (monitorProviderDeath && value.status === "failed"
&& (value.error?.code === "AGENT_DISCONNECTED" || value.error?.detailCode === "AGENT_DISCONNECTED")) {
// ACP can terminalize before the independent exit observer runs.
// Retire callbacks and the session before finally removes the
// observer, so a late answer cannot admit another native turn.
retireProviderTurn(new AcpxProviderProcessLostError("The owned ACPX provider disconnected during its active turn"));
}
approval.signal.throwIfAborted();
// ACPX may resolve its own permission policy before invoking the
// host callback. Keep that typed denial on the same runner outcome.
@@ -1492,6 +1532,7 @@ function runtimePort(
void result.catch(() => undefined);
return {
...guarded,
promptStarted,
result,
cancel: (input) => {
controller.abort(new Error("ACPX extension turn cancelled"));
@@ -1502,16 +1543,28 @@ function runtimePort(
return guarded.closeStream(input);
},
events: (async function* () {
const iterator = guarded.events[Symbol.asyncIterator]();
try {
for await (const event of guarded.events) {
for (;;) {
const next = await abortableExtensionResult(iterator.next(), providerDeath.signal);
if (next.done) break;
approval.signal.throwIfAborted();
yield event;
yield next.value;
}
} catch (error) {
approval.signal.throwIfAborted();
throw error;
} finally {
// A broken provider may never release its iterator. Observe its
// eventual close without delaying the failed turn's publication.
void iterator.return?.().catch(() => undefined);
}
approval.signal.throwIfAborted();
// Copilot's transport can end its event iterator before independent
// provider-exit proof settles the pending permission and turn result.
// Do not publish a successful stream end while that turn still owns
// an unanswered callback or an unreported failure.
if (monitorProviderDeath) await result;
})(),
};
},
@@ -1768,6 +1821,7 @@ class SpawnedChildSet {
readonly #providerExits = new Map<ChildProcess, ProviderExitObservation>();
readonly #terminations = new Map<ChildProcess, Promise<unknown[]>>();
readonly #lifetimeOwnership: Promise<void>[] = [];
readonly #exitWatchers = new Set<() => void>();
#lifetimeOwnershipSealed = false;
#sealed = false;
@@ -1853,6 +1907,11 @@ class SpawnedChildSet {
};
}
watchProviderExit(observer: () => void): () => void {
this.#exitWatchers.add(observer);
return () => { this.#exitWatchers.delete(observer); };
}
#track(child: ChildProcess, providerExit: ProviderExitObservation): void {
this.#children.add(child);
const onError = (error: unknown) => this.#errors.add(error);
@@ -1879,6 +1938,7 @@ class SpawnedChildSet {
providerExit.observe((outcome) => {
if (outcome.exited) {
providerExited = true;
for (const observer of this.#exitWatchers) observer();
forgetIfReleased();
} else {
this.#errors.add(outcome.error);
@@ -90,7 +90,7 @@ describe("Copilot display extension adapter", () => {
const normalize = vi.spyOn(copilotEvents, "normalizeCopilotSessionEvent").mockReturnValueOnce({
kind: "usage", sourceMethod: COPILOT_ACP_EVENT_METHOD, sourceType: "assistant.usage",
sessionId: context.sessionId, turnId: null,
data: { connectionString: "plain-credential-value", credentials: { endpoint: "opaque-value" }, inputTokens: 42 },
data: { connectionString: "plain-credential-value", credentials: { endpoint: "opaque-value" }, nested: { apiKey: { endpoint: "nested-secret" } }, inputTokens: 42 },
});
try {
const events = await createCopilotProfileExtensionAdapter(context).notification(COPILOT_ACP_EVENT_METHOD, params("assistant.usage", {}));
@@ -98,9 +98,10 @@ describe("Copilot display extension adapter", () => {
expect(events[0].payload.details).toEqual(expect.arrayContaining([
{ name: "connectionString", value: "[REDACTED]" },
{ name: "credentials.endpoint", value: "[REDACTED]" },
{ name: "nested.apiKey.endpoint", value: "[REDACTED]" },
{ name: "inputTokens", value: "42" },
]));
expect(JSON.stringify(events)).not.toMatch(/plain-credential-value|opaque-value/);
expect(JSON.stringify(events)).not.toMatch(/plain-credential-value|opaque-value|nested-secret/);
} finally { normalize.mockRestore(); }
});
@@ -82,7 +82,11 @@ function detailFields(data: Record<string, unknown>): Array<{ name: string; valu
// Preserve the complete field path for sensitive-key redaction. The
// normalizer admits only known, typed counters/flags: their numeric token
// counts are not credentials, so they retain value-only redaction.
const safe = typeof value === "string" ? name.split(".").reduce<unknown>((safe, segment) => redactSemanticValue(safe, segment), value) : value;
// Flattening must not discard a sensitive ancestor such as credentials.
// Apply the shared key rule to each path segment before publishing it.
const safe = typeof value === "string"
? name.split(".").reduce<unknown>((entry, key) => redactSemanticValue(entry, key), value)
: value;
details.push({ name: bounded(name, 160), value: bounded(String(safe), 4000) });
}
};
@@ -14,10 +14,15 @@ import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
const PERMISSION_POLICY_SOURCES = [
["copilot-permission-context.ts", "permissionContextSourceSha256"],
["acp-permission-adapter.ts", "permissionAdapterSourceSha256"],
["cursor-plan-tool-identity.ts", "permissionToolIdentitySourceSha256"],
["safe-locations.ts", "permissionLocationsSourceSha256"],
["../../semantic-tools/redaction.ts", "permissionRedactionSourceSha256"],
["generated-sidecar-contract.ts", "permissionClassifierSourceSha256"],
["cursor-plan-tool-identity.ts", "permissionIdentitySourceSha256"],
] as const;
const COPILOT_DISPLAY_SOURCES = [
["copilot-events.ts", "displayEventSourceSha256"],
["copilot-extension-adapter.ts", "displayProjectionSourceSha256"],
] as const;
const SEMANTIC_RECEIPT_SOURCES = [
@@ -31,6 +36,11 @@ const SEMANTIC_RECEIPT_SOURCES = [
["../../protocol/replay-contract.ts", "semanticValidationSourceSha256"],
["../../protocol/result-normalization.ts", "semanticNormalizationSourceSha256"],
["../../contracts/completion-result.ts", "semanticCompletionContractSourceSha256"],
["../../contracts/native-execution.ts", "nativeExecutionSourceSha256"],
["installation-integrity.ts", "guardedInstallationSourceSha256"],
["../../live/runnerd-codex-transport.ts", "runtimeRequestBridgeSourceSha256"],
["../codex/codex-session-notifications.ts", "runtimeRequestFacadeSourceSha256"],
["../codex/codex-thread-normalization.ts", "runtimeRequestNotificationSourceSha256"],
["../../protocol/generated/standalone-validators.ts", "semanticValidatorsSourceSha256"],
["../../protocol/generated/schema-bundle.ts", "semanticSchemaBundleSourceSha256"],
] as const;
@@ -75,6 +85,11 @@ async function withCopilotTestPlatform<T>(arch: "x64" | "arm64", run: () => Prom
vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() }));
describe("Copilot build-owned installation", () => {
it("admits the qualified profile without a pending qualification marker", () => {
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(47);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
});
it("preserves v12 history and changes only the version and four shared source bindings", () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8"));
@@ -86,21 +101,18 @@ describe("Copilot build-owned installation", () => {
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
expect(current.commandDigest).not.toBe(prior.commandDigest);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves v13 history while binding the shared live request snapshot", () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8"));
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSidecarSourceSha256"]);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves v14 history while binding the corrected outbound ACPX delivery", () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-identity.json", import.meta.url), "utf8"));
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves v17 history while binding the shared Grok descriptor schema", () => {
@@ -109,7 +121,6 @@ describe("Copilot build-owned installation", () => {
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSchemaBundleSourceSha256"]);
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
expect(current.commandDigest).not.toBe(prior.commandDigest);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves the historical Muse v18 declaration without granting current launch", async () => {
@@ -123,7 +134,6 @@ describe("Copilot build-owned installation", () => {
const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest);
expect(current.commandDigest).not.toBe(prior.commandDigest);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
vi.mocked(verifyNativeAcpxInstallation).mockClear();
for (const override of [
{ commandDigest: prior.commandDigest },
@@ -166,7 +176,6 @@ describe("Copilot build-owned installation", () => {
.toEqual(["agentProfileVersion", "semanticSchemaBundleSourceSha256", "semanticValidationSourceSha256", "semanticValidatorsSourceSha256"]);
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
expect(current.commandDigest).not.toBe(prior.commandDigest);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("binds both immutable v19 histories in a new combined v20 declaration", async () => {
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v20-identity.json", import.meta.url), "utf8"));
@@ -192,12 +201,11 @@ describe("Copilot build-owned installation", () => {
}
}
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves v20 history while binding the controller-owned monitor continuation", async () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v20-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v21-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v21-mainline-monitor-identity.json", import.meta.url), "utf8"));
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
"agentProfileVersion", "semanticCompletionContractSourceSha256", "semanticContinuationSourceSha256", "semanticDirectDriverSourceSha256", "semanticSidecarSourceSha256",
]);
@@ -208,12 +216,11 @@ describe("Copilot build-owned installation", () => {
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
agentProfileVersion: 20, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves v22 history while binding the Claude-only preload metadata branch", async () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v22-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v23-identity.json", import.meta.url), "utf8"));
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v22-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v23-identity.json", import.meta.url), "utf8"));
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
.toEqual(["agentProfileVersion", "semanticBridgeSourceSha256"]);
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
@@ -222,12 +229,11 @@ describe("Copilot build-owned installation", () => {
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest);
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot,
agentProfileVersion: 22, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned");
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
});
it("preserves v23 history while binding shared observability diagnostic redaction", () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v23-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v24-identity.json", import.meta.url), "utf8"));
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v23-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-mainline-v24-identity.json", import.meta.url), "utf8"));
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
"agentProfileVersion", "semanticSidecarSourceSha256",
]);
@@ -235,8 +241,8 @@ describe("Copilot build-owned installation", () => {
expect(current.commandDigest).not.toBe(prior.commandDigest);
});
it("admits the current v24 declaration and binds its source policy, receipts and patch hashes", () => {
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v24-identity.json", import.meta.url), "utf8"));
it("admits the current declaration and binds its source policy, receipts and patch hashes", () => {
const identity = JSON.parse(readFileSync(new URL(`../../../test/fixtures/copilot-profile-v${QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion}-identity.json`, import.meta.url), "utf8"));
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
@@ -252,10 +258,12 @@ describe("Copilot build-owned installation", () => {
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe(identity.commandDigest);
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(identity.commandDigest);
for (const [relative, field] of [...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES, ["copilot-policy.ts", "policySha256"], ["../../../scripts/materialize-copilot-binary.mjs", "distributionSourceSha256"], ["../../../scripts/copilot-inner-distribution.mjs", "innerDistributionSourceSha256"], ["../../../../../patches/acpx@0.13.1.patch", "acpxPatchSha256"]]) {
for (const [relative, field] of [...PERMISSION_POLICY_SOURCES, ...COPILOT_DISPLAY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES, ["copilot-policy.ts", "policySha256"], ["profile-compatibility.ts", "profileCompatibilitySourceSha256"], ["provider-assets-root.ts", "assetOwnershipSourceSha256"], ["../../../scripts/install-copilot-assets.mjs", "assetInstallerSourceSha256"], ["../../../scripts/materialize-copilot-binary.mjs", "distributionSourceSha256"], ["../../../scripts/copilot-inner-distribution.mjs", "innerDistributionSourceSha256"], ["../../../../../patches/acpx@0.13.1.patch", "acpxPatchSha256"]]) {
expect(createHash("sha256").update(readFileSync(new URL(relative!, import.meta.url))).digest("hex")).toBe(identity.declaration[field!]);
}
expect(readFileSync(new URL("../../../acpx-profiles.json", import.meta.url), "utf8")).toContain(identity.commandDigest);
const manifest = JSON.parse(readFileSync(new URL("../../../acpx-profiles.json", import.meta.url), "utf8"));
expect(manifest.profiles.copilot.commandDigest).toBe(identity.commandDigest);
expect(readFileSync(new URL("../../../scripts/build-copilot-distribution.mjs", import.meta.url), "utf8")).toContain("profiles.profiles.copilot.commandDigest");
expect(readFileSync(new URL("../../../runner/crates/runner-core/src/generated_acpx_profiles.rs", import.meta.url), "utf8")).toContain(identity.commandDigest);
});
it("binds the complete executable permission-policy import closure", async () => {
@@ -266,8 +274,8 @@ describe("Copilot build-owned installation", () => {
const bound = new Set(PERMISSION_POLICY_SOURCES.map(([path]) => new URL(path, import.meta.url).href));
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
});
it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v24-identity.json", import.meta.url), "utf8"));
it.each([...PERMISSION_POLICY_SOURCES, ...COPILOT_DISPLAY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
const identity = JSON.parse(readFileSync(new URL(`../../../test/fixtures/copilot-profile-v${QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion}-identity.json`, import.meta.url), "utf8"));
identity.declaration[field] = "0".repeat(64);
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
@@ -318,16 +326,4 @@ describe("Copilot build-owned installation", () => {
}
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
});
it("rejects the historical v12 digest even when the caller supplies the current version", async () => {
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8"));
vi.mocked(verifyNativeAcpxInstallation).mockClear();
for (const override of [
{ commandDigest: prior.commandDigest },
{ agentProfileVersion: prior.declaration.agentProfileVersion },
{ commandDigest: prior.commandDigest, agentProfileVersion: prior.declaration.agentProfileVersion },
]) {
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override })).rejects.toThrow("exact pinned");
}
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
});
});
@@ -24,14 +24,17 @@ export async function verifyCopilotInstallation(profile: AcpxReleaseProfile): Pr
throw new Error(`Copilot native distribution is not pinned for ${platform}`);
}
const distributionRoot = join(resolveRunnerProviderAssetsRoot(import.meta.url, "copilot"), platform);
const native = await verifyNativeAcpxInstallation({
let native: VerifiedAcpxInstallation;
try { native = await verifyNativeAcpxInstallation({
distributionRoot,
manifestPath: join(distributionRoot, ".paperclip-copilot-closure.json"),
expectedClosureSha256: COPILOT_CLOSURE_SHA256[platform as keyof typeof COPILOT_CLOSURE_SHA256],
executable: "copilot", fixedArguments: COPILOT_LAUNCH_ARGUMENTS,
isolatedCacheEnvironmentName: "COPILOT_PKG_CACHE_HOME",
copilotDistributionDirectory: "distribution",
});
}); } catch {
throw Object.assign(new Error("Install the verified Copilot runtime beside the admitted sidecar."), { code: "COPILOT_INSTALLATION_INVALID", retryable: false });
}
// The host identity binds the versioned profile. Native admission independently
// binds the complete platform closure before creating each single-use lease.
return Object.freeze({ ...native, commandDigest: expected.commandDigest });
@@ -1,10 +1,35 @@
import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js";
import { safeAcpxLocations } from "./safe-locations.js";
import { lstatSync, realpathSync } from "node:fs";
import { basename, dirname, isAbsolute, join } from "node:path";
export const COPILOT_PERMISSION_CONTEXT_CONTRACT = "copilot-edit-permission-context-v1";
const record = (value: unknown): Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
/** Native providers can spell an absolute workspace path through a host alias
* (macOS /tmp and /private/tmp). Attest the real entry, or its existing parent
* for a new file, before converting it to a display target. */
function aliasedLocation(path: string, cwd: string): string | undefined {
if (!isAbsolute(path)) return;
try {
const canonicalCwd = realpathSync(cwd);
let canonicalPath: string;
try {
lstatSync(path);
canonicalPath = realpathSync(path);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return;
// A dangling symlink must not become an apparently safe create target.
try { lstatSync(path); return; } catch (missing) {
if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return;
}
canonicalPath = join(realpathSync(dirname(path)), basename(path));
}
return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined;
} catch { return; }
}
/** Display context only; this does not authorize filesystem access. */
export function safeCopilotEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined {
const call = record(value);
@@ -14,7 +39,12 @@ export function safeCopilotEditTarget(value: unknown, workingDirectory: string |
if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return;
const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined);
if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return;
const safe = paths.flatMap(path => safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title));
const safe = paths.flatMap(path => {
const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title);
if (locations.length) return locations;
const aliased = aliasedLocation(path as string, workingDirectory);
return aliased ? [{ path: aliased }] : [];
});
const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":"));
if (names.length !== paths.length || new Set(names).size !== 1) return;
const target = names[0];
@@ -1,7 +1,17 @@
import { describe, expect, it } from "vitest";
import { classifyCopilotFailure, copilotConfiguration, copilotSandboxEnvironment, COPILOT_LAUNCH_ARGUMENTS } from "./copilot-profile.js";
import { classifyCopilotFailure, composeCopilotSystemInstructions, COPILOT_TASK_ORIENTATION_INSTRUCTIONS, copilotConfiguration, copilotSandboxEnvironment, COPILOT_LAUNCH_ARGUMENTS } from "./copilot-profile.js";
describe("Copilot admission policy", () => {
it("bounds the complete native instruction file without retaining prior instructions", () => {
const suffix = `\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}`;
expect(composeCopilotSystemInstructions("Current instructions.")).toBe(`Current instructions.${suffix}`);
expect(composeCopilotSystemInstructions("")).toBe(suffix);
const remaining = 32 * 1024 - Buffer.byteLength(`${suffix}\n`);
expect(Buffer.byteLength(`${composeCopilotSystemInstructions("x".repeat(remaining))}\n`)).toBe(32 * 1024);
expect(() => composeCopilotSystemInstructions("x".repeat(remaining + 1))).toThrow("bounded size");
expect(() => composeCopilotSystemInstructions("é".repeat(Math.floor(remaining / 2) + 1))).toThrow("bounded size");
expect(() => composeCopilotSystemInstructions("embedded\0instruction")).toThrow("bounded size");
});
it("uses ACP directly without disabling questions or bypassing permissions", () => {
expect(COPILOT_LAUNCH_ARGUMENTS.slice(0, 2)).toEqual(["--acp", "--stdio"]);
expect(COPILOT_LAUNCH_ARGUMENTS).toContain("--no-auto-update");
@@ -5,6 +5,16 @@ export const COPILOT_VERSION = "1.0.88" as const;
export const COPILOT_SYSTEM_INSTRUCTIONS_FILE = "copilot-instructions.md" as const;
export const COPILOT_SYSTEM_INSTRUCTION_DELIVERY = "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1" as const;
export const COPILOT_CREDENTIAL_ENVIRONMENT_NAME = "COPILOT_GITHUB_TOKEN" as const;
export const COPILOT_TASK_ORIENTATION_INSTRUCTIONS = "At the start of every task turn, use the paperclip MCP server's dedicated get_task_context tool with {} and wait for its successful result before task work, report_progress, or any other task-dependent mutation. This is an advertised MCP tool, not an HTTP API route: do not use search_api or call_api to discover or substitute for get_task_context. Loaded instructions, seed state, task descriptions, and prior-turn context do not replace this read. Decide the next action from the returned context. Do not issue the context read and a dependent action in parallel. If this tool is unavailable or its read fails, report the failure and do not perform dependent actions." as const;
/** Compose Copilot-only guidance without changing other providers' sandbox identity. */
export function composeCopilotSystemInstructions(instructions: string): string {
const composed = `${instructions}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}`;
if (composed.includes("\0") || Buffer.byteLength(`${composed}\n`) > 32 * 1024) {
throw new Error("Provider runtime instructions exceed their bounded size");
}
return composed;
}
// Keep permission callbacks enabled even when Paperclip's policy is approve-all.
// In particular COPILOT_ALLOW_ALL=true also trusts workspace hooks/plugins/MCP.
@@ -8,8 +8,8 @@ import { verifyCopilotInstallation } from "./copilot-installation.js";
import { verifyCursorInstallation } from "./cursor-installation.js";
vi.mock("./copilot-installation.js", () => ({ verifyCopilotInstallation: vi.fn(async () => ({ commandDigest: "verified-by-native-factory" })) }));
vi.mock("./cursor-installation.js", () => ({ verifyCursorInstallation: vi.fn(async () => ({ commandDigest: "verified-by-cursor-factory" })) }));
vi.mock("./cursor-installation.js", () => ({ verifyCursorInstallation: vi.fn(async () => ({ commandDigest: "verified-cursor-factory" })) }));
const context = { workspacePath: "/workspace", sessionId: "backend-1", turnId: "turn-1" };
describe("Copilot provider registry conformance", () => {
@@ -24,9 +24,9 @@ describe("Copilot provider registry conformance", () => {
const profile = resolveQualifiedAcpxProfile("copilot", "explicit-exact-model");
expect(await verifyAcpxProfileInstallation(profile)).toMatchObject({ commandDigest: "verified-by-native-factory" });
expect(verifyCopilotInstallation).toHaveBeenCalledExactlyOnceWith(profile);
const cursorProfile = resolveQualifiedAcpxProfile("cursor", "exact-model");
expect(await verifyAcpxProfileInstallation(cursorProfile)).toMatchObject({ commandDigest: "verified-cursor-factory" });
expect(verifyCursorInstallation).toHaveBeenCalledExactlyOnceWith(cursorProfile);
const cursor = resolveQualifiedAcpxProfile("cursor", "exact-model");
expect(await verifyAcpxProfileInstallation(cursor)).toMatchObject({ commandDigest: "verified-by-cursor-factory" });
expect(verifyCursorInstallation).toHaveBeenCalledExactlyOnceWith(cursor);
expect(verifyCopilotInstallation).toHaveBeenCalledTimes(1);
});
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { expect, it, vi } from "vitest";
import { COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js";
import { AcpxRuntimeHost, type AcpxRuntimeHostDependencies, type AcpxRuntimePortOpenOptions } from "./runtime-host.js";
it("reports missing Copilot authentication before spawn and releases admission ownership for retry", async () => {
@@ -66,24 +67,25 @@ it("does not refresh Copilot instructions for a contender rejected by the lifeti
normalizedSessionId: "same-native-session", runtimeDirectory, workingDirectory,
providerPolicy: { readOnly: false }, environment: { COPILOT_GITHUB_TOKEN: "synthetic-fixture" },
};
const composed = (value: string) => `${value}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`;
const winner = AcpxRuntimeHost.open({ ...options, systemInstructions: "Winner instructions." }, dependencies);
// Attach the rejection handler before deliberately releasing the failed launch.
const winnerResult = expect(winner).rejects.toThrow("fixture stops before native process launch");
try {
await winnerEntered;
expect(await readFile(instructionPath, "utf8")).toBe("Winner instructions.\n");
expect(await readFile(instructionPath, "utf8")).toBe(composed("Winner instructions."));
await expect(AcpxRuntimeHost.open({ ...options, systemInstructions: "Losing contender." }, dependencies))
.rejects.toThrow("already has an active lease");
expect(openRuntime).toHaveBeenCalledOnce();
expect(await readFile(instructionPath, "utf8")).toBe("Winner instructions.\n");
expect(await readFile(instructionPath, "utf8")).toBe(composed("Winner instructions."));
releaseWinner();
await winnerResult;
await expect(AcpxRuntimeHost.open({ ...options, systemInstructions: "New owner instructions." }, dependencies))
.rejects.toThrow("fixture stops before native process launch");
expect(await readFile(instructionPath, "utf8")).toBe("New owner instructions.\n");
expect(await readFile(instructionPath, "utf8")).toBe(composed("New owner instructions."));
await expect(AcpxRuntimeHost.open({ ...options, environment: {}, systemInstructions: "Unauthenticated admission." }, dependencies))
.rejects.toMatchObject({ code: "COPILOT_AUTH_REQUIRED" });
expect(await readFile(instructionPath, "utf8")).toBe("New owner instructions.\n");
expect(await readFile(instructionPath, "utf8")).toBe(composed("New owner instructions."));
} finally {
releaseWinner();
await winnerResult;
@@ -127,6 +127,42 @@ describe("Copilot authoritative semantic receipt correlation", () => {
const make = (operation = "paperclip_finish", isError = false) => appendSemanticToolReceipt(
{ tool: operation, callId: "3", arguments: args }, { content: [{ type: "text", text: '{"accepted":false}' }], ...(isError ? { isError } : {}) });
const terminal = (id: string, content: unknown, status = "completed") => ({ type: "tool_call", tag: "tool_call_update", toolCallId: id, status, rawOutput: { contents: content } });
it("retains empty native context arguments when ACP classifies the semantic tool as a read", () => {
const h = harness();
const bound = appendSemanticToolReceipt({ tool: "get_task_context", callId: "context", arguments: {} },
{ content: [{ type: "text", text: "PRIVATE CONTEXT" }] });
h.projector.tool(tool("native-context", {}, "read"));
expect(h.events.map(details)).toEqual([{ stage: "tool", toolCallId: "native-context", operation: "read", status: "pending" }]);
h.projector.captureSemanticReceipt()!(bound.receipt);
h.projector.tool(terminal("native-context", bound.result.content));
expect(details(h.events.at(-1)!)).toMatchObject({ stage: "tool", status: "completed", semanticOperationId: "get_task_context" });
expect(h.events.map(details).some(d => d.stage === "evidence_incomplete")).toBe(false);
expect(h.events.map(e => details(e).stage)).toEqual(["tool", "semantic_result", "tool"]);
expect(JSON.stringify(h.events)).not.toContain("PRIVATE CONTEXT");
});
it("preserves streamed file-read attestation when the pending origin has no arguments yet", () => {
const h = harness();
h.projector.tool(tool("file-read", undefined, "read"));
h.projector.tool({ ...tool("file-read", { path: "instructions.md" }, "read"), tag: "tool_call_update" });
h.projector.tool(update("file-read", "PRIVATE FILE"));
expect(details(h.events.at(-1)!)).toMatchObject({ operation: "read", status: "completed",
readTargetSha256: `sha256:${createHash("sha256").update("instructions.md").digest("hex")}` });
expect(h.events.map(details).some(d => d.stage === "evidence_incomplete")).toBe(false);
});
it.each(["absent-origin", "changed-terminal-input", "changed-before-execution", "changed-to-file"])("rejects empty-read semantic correlation with %s", scenario => {
const h = harness();
const bound = appendSemanticToolReceipt({ tool: "get_task_context", callId: "context", arguments: {} },
{ content: [{ type: "text", text: "PRIVATE CONTEXT" }] });
h.projector.tool(tool("native-context", scenario === "absent-origin" ? undefined : {}, "read"));
if (scenario === "changed-before-execution") h.projector.tool({ ...tool("native-context", { changed: true }, "read"), tag: "tool_call_update" });
if (scenario === "changed-to-file") h.projector.tool({ ...tool("native-context", { path: "instructions.md" }, "read"), tag: "tool_call_update" });
h.projector.captureSemanticReceipt()!(bound.receipt);
h.projector.tool({ ...terminal("native-context", bound.result.content),
...(scenario === "changed-terminal-input" ? { rawInput: { changed: true } } : {}) });
expect(h.events.map(details)).toContainEqual(expect.objectContaining({ stage: "evidence_incomplete" }));
expect(h.events.map(details).filter(d => d.stage === "tool").every(d => d.semanticOperationId === undefined)).toBe(true);
expect(details(h.events.at(-1)!)).not.toHaveProperty("semanticOperationId");
});
it.each(["paperclip_finish", "report_progress", "get_task_context"])("correlates %s with trusted callback, not native display name", operation => {
const h = harness(); const bound = make(operation);
h.projector.tool({ ...tool("native", args, "other"), title: "unrelated display" });
@@ -13,6 +13,10 @@ const record = (v: unknown): Record<string, unknown> => v !== null && typeof v =
const identity = (v: unknown): v is string => typeof v === "string" && v.length > 0 && v.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(v);
const shellIdentity = (v: unknown): v is string => typeof v === "string" && /^[A-Za-z0-9_.-]{1,80}$/u.test(v);
const digest = (v: string) => createHash("sha256").update(v).digest("hex");
function boundedSemanticInputDigest(value: unknown): string | undefined {
try { if (Buffer.byteLength(JSON.stringify(value)) <= 1024 * 1024) return semanticInputSha256(value); }
catch { /* Malformed input cannot correlate. */ }
}
/**
* Observation only, bound to one active ACP prompt iterator and its permission
@@ -104,14 +108,19 @@ export function createCopilotToolEvidence(binding: {
let publishPendingRead = false;
if (state.kind === "read") {
state.read = updateSingleReadEvidence(state.read, call, binding.workingDirectory);
// Copilot classifies empty-argument semantic context calls as reads.
// Explicit {} is a complete input, so publish its real pending origin
// before execution/receipt. Only absent arguments may stream later;
// neither shape acquires file-path evidence without an actual path.
if (state.read.pendingOriginInput && call.tag === "tool_call" && call.rawInput !== null
&& typeof call.rawInput === "object" && !Array.isArray(call.rawInput) && Object.keys(call.rawInput).length === 0) state.read = {};
if (state.read.pendingOriginInput && call.tag === "tool_call") state.pendingReadNotice = true;
if (state.pendingReadNotice && state.read.pendingOriginInput) return;
if (state.pendingReadNotice) { state.pendingReadNotice = false; publishPendingRead = true; }
}
if (call.rawInput !== undefined) {
// Retain only a bounded digest of the native arguments, never their text.
let inputDigest: string | undefined;
try { if (Buffer.byteLength(JSON.stringify(call.rawInput)) <= 1024 * 1024) inputDigest = semanticInputSha256(call.rawInput); } catch { /* Malformed input cannot correlate. */ }
const inputDigest = boundedSemanticInputDigest(call.rawInput);
if (state.semanticInput && inputDigest !== state.semanticInput) { state.invalid = true; notice("evidence_incomplete", id, { reason: "changed_tool_input" }); return; }
state.semanticInput = inputDigest;
const fields = inputFields({ ...call, kind: state.kind });
@@ -48,13 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "copilot",
"agentProfileVersion": 24,
"agentProfileVersion": 47,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:87e275270e50b42290d10ecec9dfce86ffca2b7a7519cb60c584004adfc86437",
"qualificationStatus": "pending",
"commandDigest": "sha256:55044aa235e88b3fe36ec8a556ee1e3382590870aab907e4462ef7d64af49cea",
"permissionPolicy": "interactive"
},
"claude": {
@@ -1481,7 +1481,7 @@ function commandLease(
privateSnapshot: AcpxPrivateSnapshot | null,
): VerifiedAcpxCommandLease {
let consumed = false;
let directoriesReleased = false;
let directoriesRelease: Promise<void> | undefined;
let spawnedChild: ChildProcess | null = null;
let childExit: Promise<void> | null = null;
let snapshotCleanup: Promise<void> | null = null;
@@ -1493,24 +1493,23 @@ function commandLease(
}
return snapshotCleanup;
};
const releaseDirectories = async (): Promise<void> => {
if (directoriesReleased) return;
directoriesReleased = true;
await Promise.all([
const releaseDirectories = (): Promise<void> => {
return (directoriesRelease ??= Promise.all([
commandDirectory.close(),
...dependencyAncestors.map((handle) => handle.close()),
...(providerRuntimeExecutable === null
? []
: [providerRuntimeExecutable.close()]),
]);
]).then(() => undefined));
};
const releaseDirectoriesBestEffort = (): void => {
void releaseDirectories().catch(() => undefined);
};
const close = async (): Promise<void> => {
if (consumed && privateSnapshot === null) return;
const wasConsumed = consumed;
consumed = true;
verifiedBytes.fill(0);
// A spawned command owns its source buffer until its pipe write completes.
if (!wasConsumed) verifiedBytes.fill(0);
await releaseDirectories();
// A spawned provider may still read the snapshot. Runtime shutdown runs in
// parallel and retires that child; do not remove its bytes or report command
@@ -1605,7 +1604,7 @@ function commandLease(
environment[VERIFIED_PROVIDER_RUNTIME_TARGET_ENV] =
providerRuntimeEnvironmentVariable;
}
child = spawnChildProcess(
child = spawnedChild = spawnChildProcess(
runtimeHandoff.executable,
guarded
? [
@@ -10,6 +10,9 @@ import type { ChildProcess } from "node:child_process";
import { afterEach, describe, expect, it, vi } from "vitest";
import { awaitVerifiedAcpxProviderExit, awaitVerifiedAcpxProviderOwnership, verifyNativeAcpxInstallation } from "./installation-integrity.js";
import { createNativeAcpxDistributionSnapshot, readNativeAcpxDistributionEntries, parseNativeAcpxDistributionEntries, type NativeAcpxDistributionInput, type NativeAcpxDistributionEntry } from "./native-distribution-integrity.js";
import { openCodexAcpxRuntime } from "./codex-runtime-adapter.js";
import { createAcpxCommandLeaseOwner } from "./command-lease-owner.js";
import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
@@ -322,6 +325,36 @@ describe("native ACPX execution closure", () => {
expect(first.text).toMatch(/^native:fixed:.*paperclip-acpx-native-.*\/state$/);
expect(first.text).not.toBe(second.text);
});
it("reports failed deletion and retries retirement without relaunching", async () => {
const lease = await (await verifyNativeAcpxInstallation(await fixture())).openCommand();
const originalRemove = vi.mocked(rm).getMockImplementation()!;
const deleting = gate(); const holdDeletion = gate();
let privateRoot: string | undefined;
let failed = false;
vi.mocked(rm).mockImplementation(async (path, options) => {
if (String(path).includes("paperclip-acpx-native-") && !failed) {
privateRoot = String(path); deleting.release(); await holdDeletion.promise;
failed = true; throw new Error("snapshot deletion failed");
}
return await originalRemove(path, options);
});
try {
expect((await output(lease.spawn())).code).toBe(0);
await deleting.promise;
const rejected = expect(lease.close()).rejects.toThrow("snapshot deletion failed");
await new Promise<void>(resolve => setImmediate(resolve));
holdDeletion.release();
await rejected;
await expect(stat(privateRoot!)).resolves.toBeDefined();
expect(() => lease.spawn()).toThrow("closed");
await lease.close();
await expect(stat(privateRoot!)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
holdDeletion.release();
vi.mocked(rm).mockImplementation(originalRemove);
await lease.close();
}
});
// Copy/hash the real Node closure and start its bootstrap + owned shim. These
// operations share the adjacent native-closure test's bounded allowance;
// Vitest's 5s default is not a provider startup or permission deadline.
@@ -617,8 +650,114 @@ describe("native ACPX execution closure", () => {
child.kill(); await Promise.all(fences.map(server => new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))));
}
}, 15_000);
it("proves native child death before any caller cleanup, despite the launcher's pending guardian read", async () => {
const declaration = await fixture({ node: true, script: 'console.log(process.pid);setInterval(()=>{},1000);' });
const fences = await Promise.all([listen(), listen()]);
const fds = fences.map(server => (server as Server & { _handle?: { fd?: number } })._handle!.fd!);
const child = (await (await verifyNativeAcpxInstallation(declaration)).openCommand()).spawn([], {}, {
credentialFenceFds: [fds[0]!, fds[1]!], activateCredentialFenceOwner: async () => undefined,
});
const exited = once(child, "exit");
let deadline: ReturnType<typeof setTimeout> | undefined;
try {
const ready = once(child.stdout!, "data");
await awaitVerifiedAcpxProviderOwnership(child);
const [chunk] = await ready;
const nativePid = Number(String(chunk).trim());
expect(Number.isSafeInteger(nativePid) && nativePid > 0 && nativePid !== child.pid).toBe(true);
const proof = awaitVerifiedAcpxProviderExit(child);
process.kill(nativePid, "SIGKILL");
// No guardian.kill/close is allowed to manufacture the lifetime proof.
await Promise.race([proof, new Promise((_, reject) => {
deadline = setTimeout(() => reject(new Error("Native exit proof remained open after child death")), 3_000);
})]);
await exited;
expect(() => process.kill(nativePid, 0)).toThrow();
} finally {
clearTimeout(deadline);
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
await exited;
await Promise.all(fences.map(server => new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))));
}
}, 15_000);
it("expires a real ACPX permission on native child death without replaying its mutation", async () => {
// This is a deterministic ACP peer in a verified native distribution. No
// provider credential, remote service or model is involved.
const declaration = await fixture({ node: true, script: blockingCopilotPeer });
const fences = await Promise.all([listen(), listen()]);
const fds = fences.map(server => (server as Server & { _handle?: { fd?: number } })._handle!.fd!);
const installation = await verifyNativeAcpxInstallation(declaration);
const owner = createAcpxCommandLeaseOwner(await installation.openCommand(), installation.openCommand);
const pidFile = join(declaration.distributionRoot, "native-pid");
let port: Awaited<ReturnType<typeof openCodexAcpxRuntime>> | undefined;
let deadline: ReturnType<typeof setTimeout> | undefined;
try {
port = await openCodexAcpxRuntime({
command: owner.command, refreshConsumedCommand: owner.refreshConsumedCommand,
profile: resolveQualifiedAcpxProfile("copilot", "gpt-5.6-luna"),
cwd: declaration.distributionRoot, stateDirectory: join(declaration.distributionRoot, "state"),
providerSessionKey: "native-death-fixture", permissionMode: "approve-reads",
permissionPolicy: { autoApprove: ["read"], escalate: ["write"], defaultAction: "escalate" },
launchEnvironment: { PATH: "/usr/bin:/bin", NATIVE_TEST_PID_FILE: pidFile },
credentialFenceFds: [fds[0]!, fds[1]!], activateCredentialFenceOwner: async () => undefined,
systemInstructions: "Read task context before editing.", mcpServers: [], retainFailedAdmissionCleanup: () => undefined,
});
let pending!: () => void;
const requested = new Promise<void>(resolve => { pending = resolve; });
let callbackSignal!: AbortSignal;
let lateAnswer!: (value: { outcome: "allow_once" }) => void;
const handler = vi.fn((_request, context) => {
callbackSignal = context.signal; pending();
return new Promise<{ outcome: "allow_once" }>(resolve => { lateAnswer = resolve; });
});
const turn = port.startTurn({ text: "Attempt one edit", requestId: "native-death-turn", onPermissionRequest: handler });
const settled = expect(turn.result).rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const draining = expect((async () => { for await (const _event of turn.events) { /* drain */ } })())
.rejects.toMatchObject({ code: "ACPX_PROVIDER_PROCESS_LOST" });
const bounded = new Promise<never>((_, reject) => {
deadline = setTimeout(() => reject(new Error("Real native ACPX death remained unsettled")), 5_000);
});
await Promise.race([requested, bounded]);
const nativePid = Number(await readFile(pidFile, "utf8"));
expect(Number.isSafeInteger(nativePid) && nativePid > 0).toBe(true);
process.kill(nativePid, "SIGKILL");
await Promise.race([Promise.all([settled, draining]), bounded]);
expect(callbackSignal.aborted).toBe(true);
lateAnswer({ outcome: "allow_once" });
expect(() => port!.startTurn({ text: "Replay", requestId: "replay" })).toThrow("provider exited");
expect(handler).toHaveBeenCalledOnce();
expect(() => process.kill(nativePid, 0)).toThrow();
} finally {
clearTimeout(deadline);
await port?.close({ reason: "offline native-death fixture complete" });
await owner.command.close();
await Promise.all(fences.map(server => new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))));
}
}, 15_000);
});
const blockingCopilotPeer = String.raw`
const send=m=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',...m})+'\n');
require('node:fs').writeFileSync(process.env.NATIVE_TEST_PID_FILE,String(process.pid));
const mode='https://agentclientprotocol.com/protocol/session-modes#agent';
const config=[{id:'mode',name:'Mode',type:'select',currentValue:mode,options:[{value:mode,name:'Agent'}]},
{id:'allow_all',name:'Allow all',type:'select',currentValue:'off',options:[{value:'off',name:'Off'}]},
{id:'model',name:'Model',type:'select',currentValue:'gpt-5.6-luna',options:[{value:'gpt-5.6-luna',name:'Luna'}]}];
const snapshot={sessionId:'copilot-death-session',modes:{currentModeId:mode,availableModes:[{id:mode,name:'Agent'}]},configOptions:config,
models:{currentModelId:'gpt-5.6-luna',availableModels:[{modelId:'gpt-5.6-luna',name:'Luna'}]}};
require('node:readline').createInterface({input:process.stdin}).on('line',line=>{
const m=JSON.parse(line);
if(m.method==='initialize')send({id:m.id,result:{protocolVersion:1,agentCapabilities:{loadSession:true},authMethods:[]}});
else if(['session/new','session/load'].includes(m.method))send({id:m.id,result:snapshot});
else if(m.method==='session/set_model'||m.method==='session/close')send({id:m.id,result:{}});
else if(m.method==='session/set_config_option')send({id:m.id,result:{configOptions:config}});
else if(m.method==='session/prompt')send({id:'pending-edit',method:'session/request_permission',params:{sessionId:'copilot-death-session',
toolCall:{toolCallId:'native-edit',title:'Write file',kind:'edit',status:'pending',rawInput:{path:'target.txt',content:'FORBIDDEN'}},
options:[{optionId:'allow',name:'Allow',kind:'allow_once'},{optionId:'deny',name:'Deny',kind:'reject_once'}]}});
});`;
async function listen(): Promise<Server> {
const server = createServer(); server.listen(0, "127.0.0.1"); await once(server, "listening"); return server;
}
@@ -258,7 +258,12 @@ function nativeBootstrap(executable: string, executableDigest: string, args: str
`if(require("node:crypto").createHash("sha256").update(fs.readFileSync(fd)).digest("hex")!==${JSON.stringify(executableDigest)})throw new Error("Native ACPX executable changed before spawn");`,
'const stdio=guarded?[0,1,2,5,6,7,8]:[0,1,2]; const childExecutable=process.platform==="linux"?"/proc/self/fd/"+stdio.length:executable;stdio.push(fd);',
`const child=spawn(childExecutable,${JSON.stringify(args)},{env,cwd:process.cwd(),shell:false,detached:false,stdio});fs.closeSync(fd);`,
'child.once("error",()=>process.exit(1)); child.once("exit",(code,signal)=>process.exit(signal?1:code??1));',
// A guarded Node launcher has a pending filesystem read on its guardian
// pipe. process.exit can wait for that read while the guardian waits for
// this launcher to exit. Retire the live owned group through the kernel;
// no saved PID/group identity or provider JavaScript cleanup is required.
'const retire=code=>{if(guarded){try{process.kill(0,"SIGKILL");}catch{process.kill(process.pid,"SIGKILL");}}else process.exit(code);};',
'child.once("error",()=>retire(1)); child.once("exit",(code,signal)=>retire(signal?1:code??1));',
'for(const signal of ["SIGTERM","SIGINT","SIGHUP"])process.on(signal,()=>child.kill(signal));',
].join("\n");
}
@@ -28,7 +28,7 @@ export interface AcpxActivityAdapter {
const adapters: Readonly<Record<string, AcpxActivityAdapter | undefined>> = {
cursor: cursorActivityAdapter,
copilot: { createToolEvidence: createCopilotToolEvidence },
copilot: Object.freeze({ createToolEvidence: createCopilotToolEvidence }),
};
const noActivity: AcpxActivityAdapter = Object.freeze({});
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
/** Decodable historical profile revisions, not permission to launch them.
* Exact current profile/package/digest admission is checked separately. */
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27;
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46;
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46],
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};
@@ -1,9 +1,9 @@
import { assertCopilotCredentials, classifyCopilotFailure } from "./copilot-profile.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
import { verifyCursorInstallation } from "./cursor-installation.js";
import { assertCursorWorkspacePolicy } from "./cursor-launch-policy.js";
import { resolveQualifiedAcpxProfile, type QualifiedAcpxAgent, type QualifiedAcpxProfile } from "./qualified-profiles.js";
import { verifyPiInstallation } from "./pi-installation.js";
import { assertCopilotCredentials, classifyCopilotFailure } from "./copilot-profile.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
import { verifyQualifiedAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
/** Closed build-owned registry. Provider branches add their pinned installations here. */
@@ -31,6 +31,18 @@ it("resolves descriptor sidecars only through the runner-bound canonical package
expect(() => resolveRunnerProviderAssetsRoot("file:///proc/self/fd/18", "pi")).toThrow("normalized absolute");
});
it("uses the public server owner for its vendored Copilot probe and selected sidecar", async () => {
const path = await serverFixture();
await writeFile(join(path, "package.json"), JSON.stringify({ name: "@paperclipai/server" }));
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", undefined);
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", undefined);
const moduleUrl = pathToFileURL(join(path, "dist/vendor/paperclip-runner/drivers/acpx/copilot-installation.js")).href;
expect(resolveRunnerProviderAssetsRoot(moduleUrl, "copilot")).toBe(join(path, "provider-assets/copilot"));
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", path);
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", join(path, "package.json"));
expect(resolveRunnerProviderAssetsRoot("file:///proc/self/fd/18", "copilot")).toBe(join(path, "provider-assets/copilot"));
});
it("resolves the public server bundle without repository paths or candidate overrides", async () => {
const path = await root();
const vendored = join(await realpath(path), "dist/vendor/paperclip-runner");
@@ -44,7 +44,7 @@ export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: Nat
// runnerd derives this binding from the verified sidecar in the public
// server package. Images may carry assets alongside that bundle; local
// explicit setup uses the OS-account cache when package assets are absent.
packageRoot = join(dirname(canonicalManifest), "dist/vendor/paperclip-runner");
packageRoot = provider === "copilot" ? dirname(canonicalManifest) : join(dirname(canonicalManifest), "dist/vendor/paperclip-runner");
if (realpathSync(packageRoot) !== packageRoot) throw new Error("Vendored runner directory is not contained by its server package");
} else if (value?.name === RUNNER_PACKAGE_NAME) {
packageRoot = dirname(canonicalManifest);
@@ -60,7 +60,7 @@ export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: Nat
const metadata = readPackageManifest(manifest, manifest);
if (metadata.name !== SERVER_PACKAGE_NAME) throw new Error("Runner server vendor package identity is invalid");
assertServerVendorLayout(packageRoot);
packageRoot = join(packageRoot, "dist/vendor/paperclip-runner");
if (provider !== "copilot") packageRoot = join(packageRoot, "dist/vendor/paperclip-runner");
} else if (new RegExp(`/(?:src|dist)/drivers/acpx/${provider}-installation\\.(?:ts|js)$`).test(url.pathname)) packageRoot = fileURLToPath(new URL("../../../", url));
else if (/\/dist\/cli\/acpx-runtime-sidecar\.(?:cjs|js)$/.test(url.pathname)) packageRoot = fileURLToPath(new URL("../../", url));
else if (new RegExp(`/dist/vendor/paperclip-runner/drivers/acpx/${provider}-installation\\.(?:js)$`).test(url.pathname)) packageRoot = fileURLToPath(new URL("../../", url));
@@ -43,6 +43,7 @@ export function resolveQualifiedAcpxProfile(
): QualifiedAcpxProfile {
const profile = QUALIFIED_ACPX_PROFILES[agent];
if (!requestedModel.trim()) throw new Error("ACPX model must not be empty");
if (agent === "copilot" && ["auto", "default"].includes(requestedModel.trim().toLowerCase())) throw new Error("Copilot requires an explicit model ID");
return { ...structuredClone(profile), qualificationModel: requestedModel, reportedModelId: requestedModel };
}
@@ -1475,7 +1475,7 @@ describe("ACPX runtime host", () => {
).rejects.toThrow("initialization and cleanup failed");
expect(runtime.close).toHaveBeenCalledOnce();
expect(credential.close).not.toHaveBeenCalled();
expect(fixture.commandClose).toHaveBeenCalledOnce();
expect(fixture.commandClose).not.toHaveBeenCalled();
expect(retainedAdmissionCleanup).not.toBeNull();
let cleanupSettled = false;
void retainedAdmissionCleanup!.finally(() => {
@@ -1488,6 +1488,7 @@ describe("ACPX runtime host", () => {
await retainedAdmissionCleanup;
expect(cleanupSettled).toBe(true);
expect(credential.close).toHaveBeenCalledOnce();
expect(fixture.commandClose).toHaveBeenCalledOnce();
});
it("retains credential ownership when runtime shutdown fails until retry succeeds", async () => {
@@ -1565,7 +1566,7 @@ describe("ACPX runtime host", () => {
const first = host.close({ reason: "runtime close pending" });
await waitForAcpxOperation(() => expect(runtime.close).toHaveBeenCalledOnce());
await waitForAcpxOperation(() => expect(fixture.commandClose).toHaveBeenCalledOnce());
expect(fixture.commandClose).not.toHaveBeenCalled();
const second = host.close({ reason: "same exact close" });
await expect(readFile(authPath, "utf8")).resolves.toBe("{}");
await expect(
@@ -1583,6 +1584,7 @@ describe("ACPX runtime host", () => {
undefined,
undefined,
]);
expect(fixture.commandClose).toHaveBeenCalledOnce();
await expect(readFile(authPath)).rejects.toMatchObject({ code: "ENOENT" });
const contender = await waitForAcpxOperation(() =>
stageManagedCodexCredential({
@@ -1595,9 +1597,10 @@ describe("ACPX runtime host", () => {
await contender.close();
}, ACPX_LONG_WAIT_TEST_TIMEOUT_MS);
it("retains the exact pending cleanup while independent resources close", async () => {
it("retains the exact pending cleanup and command snapshot until runtime retirement", async () => {
const fixture = await hostFixture();
const firstClose = new Promise<void>(() => undefined);
let resolveRuntimeClose!: () => void;
const firstClose = new Promise<void>(resolve => { resolveRuntimeClose = resolve; });
const runtime = runtimePort({
onClose: vi
.fn()
@@ -1625,6 +1628,9 @@ describe("ACPX runtime host", () => {
await Promise.resolve();
expect(settled).toBe(false);
expect(runtime.close).toHaveBeenCalledOnce();
expect(fixture.commandClose).not.toHaveBeenCalled();
resolveRuntimeClose();
await Promise.all([first, second]);
expect(fixture.commandClose).toHaveBeenCalledOnce();
}, ACPX_LONG_WAIT_TEST_TIMEOUT_MS);
@@ -1,3 +1,4 @@
import { composeCopilotSystemInstructions } from "./copilot-profile.js";
import { resolveClaudeEffort } from "./claude-effort.js";
import { piProviderConfiguration } from "./pi-provider-config.js";
import { withAcpxTurnCancellation } from "./turn-cancellation.js";
@@ -529,7 +530,10 @@ export class AcpxRuntimeHost {
// Do not race this write against cancellation: retain the lifetime lease
// until the atomic refresh settles, then honor an intervening abort.
options.signal?.throwIfAborted();
await refreshCopilotSystemInstructions(sandbox, boundedInstructions(options.systemInstructions));
await refreshCopilotSystemInstructions(
sandbox,
composeCopilotSystemInstructions(boundedInstructions(options.systemInstructions)),
);
options.signal?.throwIfAborted();
}
if (options.agent === "pi") {
@@ -1277,11 +1281,9 @@ async function cleanupRuntimeResources(
return error;
}
};
// The command lease owns only the already-consumed verified launch
// snapshot, so it can be released as shutdown starts. The credential is
// different: a provider whose exact runtime close is pending or failed may
// still read or rewrite its home. Retain both the staged bytes and the
// exclusive home lease until that exact close succeeds.
// The runtime must retire before its command snapshot can be removed.
// Keep both the snapshot and credential while the provider is still alive;
// failed cleanup remains owned and can be retried after process retirement.
const runtimeOutcome = runtime
? settle(() => runtime.close({ reason }))
: Promise.resolve(null);
@@ -1291,9 +1293,10 @@ async function cleanupRuntimeResources(
await runtimeOutcome;
return toolBridge === null ? null : await settle(() => toolBridge.close());
})();
const commandOutcome = command
? settle(() => command.close())
: Promise.resolve(null);
const commandOutcome = (async (): Promise<unknown | null> => {
await runtimeOutcome;
return command === null ? null : await settle(() => command.close());
})();
const credentialOutcome = (async (): Promise<unknown | null> => {
const runtimeError = await runtimeOutcome;
if (runtimeError !== null || credential === null) return null;
@@ -1,3 +1,4 @@
import { composeCopilotSystemInstructions, COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js";
import { configuredEnvironmentProjection } from "../../configured-environment.js";
import {
chmod,
@@ -475,15 +476,16 @@ it("refreshes Copilot native instructions privately on every admission, includin
const sandbox = await prepareAcpxRuntimeSandbox({
binding: fixture.binding, agent: "copilot", providerPolicy: { readOnly: false, systemInstructions },
});
await refreshCopilotSystemInstructions(sandbox, systemInstructions);
await refreshCopilotSystemInstructions(sandbox, composeCopilotSystemInstructions(systemInstructions));
return sandbox;
};
const composed = (value: string) => `${value}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`;
const first = await prepare("Original registered instructions.");
const path = join(first.agentHomeDirectory, "copilot-instructions.md");
expect(await readFile(path, "utf8")).toBe("Original registered instructions.\n");
expect(await readFile(path, "utf8")).toBe(composed("Original registered instructions."));
await prepareAcpxRuntimeSandbox({ binding: fixture.binding, agent: "copilot",
providerPolicy: { readOnly: false, systemInstructions: "Contender must not write during preparation." } });
expect(await readFile(path, "utf8")).toBe("Original registered instructions.\n");
expect(await readFile(path, "utf8")).toBe(composed("Original registered instructions."));
expect((await stat(path)).mode & 0o777).toBe(0o600);
expect(first.protectedPaths).toContain(dirname(dirname(first.root)));
expect(path.startsWith(`${first.protectedPaths[0]}/`)).toBe(true);
@@ -494,11 +496,11 @@ it("refreshes Copilot native instructions privately on every admission, includin
await prepare("Current registered instructions.");
expect((await lstat(path)).isSymbolicLink()).toBe(false);
expect(await readFile(target, "utf8")).toBe("Do not overwrite.");
expect(await readFile(path, "utf8")).toBe("Current registered instructions.\n");
expect(await readFile(path, "utf8")).toBe(composed("Current registered instructions."));
await prepare("");
expect(await readFile(path, "utf8")).toBe("\n");
expect(await readFile(path, "utf8")).toBe(composed(""));
await expect(prepare("x".repeat(32 * 1024 + 1))).rejects.toThrow("bounded size");
expect(await readFile(path, "utf8")).toBe("\n");
expect(await readFile(path, "utf8")).toBe(composed(""));
});
async function sandboxFixture(agent: "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot") {
@@ -65,7 +65,7 @@ for (const [provider, create] of [["cursor", createCursorToolEvidence], ["copilo
s.projector.tool({ type: "tool_call", tag: "tool_call_update", toolCallId: "other", kind: "read", status: "completed", rawInput: { path } });
expect(s.rows).toHaveLength(1); expect(s.rows[0].readTargetSha256).toBeUndefined();
});
it("publishes the pending origin after its delayed complete input, retaining exact proof on every notice", () => {
it.runIf(provider === "cursor")("publishes the pending origin after its delayed complete input, retaining exact proof on every notice", () => {
const s = setup();
s.projector.tool({ ...origin, rawInput: {}, locations: undefined });
expect(s.rows).toEqual([]);
@@ -1,5 +1,6 @@
import { configuredEnvironment } from "../../configured-environment.js";
import type { NativeTurnControlCapabilities } from "../../contracts/types.js";
import type { NativeRunRuntimeGrant } from "../../contracts/runtime-context.js";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import type { HarnessRuntimeRequestResolution } from "../../contracts/harness-driver.js";
import { githubCredentialEnvironment } from "../../github-credential-environment.js";
@@ -68,6 +69,7 @@ export interface CodexAppServerTransport {
runId: string;
turnId: string;
itemId: string;
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void>;
/** Records post-rehydration driver mapping without affecting run authority. */
recordTraceInterpretation?(input: CodexTraceInterpretation): void;
@@ -128,9 +128,10 @@ describe("Codex app-server Codex driver", () => {
message: { role: "user", text: "first turn" },
});
await expect(
session.attachRun?.({ runId: "run-warm-second" }),
session.attachRun?.({ runId: "run-warm-second", currentRunGrant: { runtimeContext: null, instructions: "Current registered instructions." } }),
).resolves.toBeUndefined();
expect(transport.attachments).toHaveLength(1);
expect(transport.attachments[0]).toMatchObject({ currentRunGrant: { runtimeContext: null, instructions: "Current registered instructions." } });
expect((await session.snapshot()).activeTurnId).toBeNull();
await expect(
@@ -42,6 +42,95 @@ import {
type PrpEvent,
type PrpStructuredRunResult,
} from "./codex-app-server-driver.test-support.js";
import { runnerdRuntimeRequestExpiryNotification } from "../../live/runnerd-codex-transport.js";
it("preserves runnerd permission expiry before a failed provider terminal and refuses late approval", async () => {
const transport = new FakeCodexTransport();
const resolve = vi.fn(async () => {});
transport.runtimeRequestResolver = resolve;
const session = await makeDriver([transport]).openSession({
runId: "run-native-expiry", normalizedSessionId: "session-native-expiry", workingDirectory: WORKSPACE,
});
const events: PrpEvent[] = [];
const consume = (async () => { for await (const event of session.events()) events.push(event); })();
const { turnId } = await session.startTurn({ message: { role: "user", text: "Leave the native edit unanswered." } });
const pending = transport.invoke({ id: "native-edit-permission", method: "session/request_permission", params: {
threadId: "thread-1", turnId, itemId: "native-edit", reason: "Write file",
choices: [{ key: "accept", label: "Allow once" }, { key: "decline", label: "Deny" }],
origin: { adapter: "acpx-runtime-sidecar", provider: "acpx", method: "session/request_permission" },
} });
await vi.waitFor(() => expect(session.pendingRuntimeRequests?.()).toHaveLength(1));
// Rust's projector supplies durable identities; the facade binds its active provider turn.
const payload = { provider: "acpx", requestId: "native-edit-permission", requestKind: "permission_approval",
requestType: "permission", turnId: "durable-controller-turn", itemId: "durable-item",
reason: "provider_process_lost", replayAllowed: false, adapter: "acpx-runtime-sidecar" };
for (const kind of ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.created"])
expect(runnerdRuntimeRequestExpiryNotification(kind, payload, "thread-1", turnId)).toBeNull();
const expiry = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", turnId)!;
transport.push(expiry.method, expiry.params);
transport.push(expiry.method, expiry.params);
await pending;
expect(session.pendingRuntimeRequests?.()).toEqual([]);
await expect(session.resolveRuntimeRequest?.({ requestId: "native-edit-permission", turnId, resolution: { action: "accept" } }))
.rejects.toThrow();
expect(resolve).not.toHaveBeenCalled();
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } });
await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed")).toBe(true));
await session.close(); await consume;
const settlements = events.filter(event => ["runtime_request.expired", "runtime_request.cancelled", "runtime_request.resolved"].includes(event.eventType));
expect(settlements).toHaveLength(1);
expect(settlements[0]).toMatchObject({ eventType: "runtime_request.expired", turnId, itemId: "native-edit", payload: { ...payload, turnId, itemId: "native-edit" } });
expect(events.indexOf(settlements[0]!)).toBeLessThan(events.findIndex(event => event.eventType === "turn.failed"));
expect(validatePrpEvent(settlements[0]!)).toMatchObject({ ok: true });
});
it("preserves canonical question expiry after provider loss and refuses late answers", async () => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport]).openSession({ runId: "question-expiry", normalizedSessionId: "question-expiry", workingDirectory: WORKSPACE });
const events: PrpEvent[] = [];
const consume = (async () => { for await (const event of session.events()) events.push(event); })();
const { turnId } = await session.startTurn({ message: { role: "user", text: "Ask before changing files." } });
const pending = transport.invoke({ id: "native-question", method: "item/tool/requestUserInput", params: {
threadId: "thread-1", turnId, itemId: "question-item", questions: [{ id: "environment", header: "Environment", question: "Where should we deploy?", options: [{ label: "Staging" }, { label: "Production" }] }],
} });
await vi.waitFor(() => expect(session.pendingRuntimeRequests?.()).toHaveLength(1));
const request = session.pendingRuntimeRequests!()[0]!;
expect(request.requestKind).toBe("user_input");
const payload = { requestId: "native-question", requestKind: "runtime", requestType: "input", replayAllowed: false, reason: "provider_process_lost" };
const expiry = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", turnId)!;
transport.push(expiry.method, expiry.params);
transport.push(expiry.method, expiry.params);
await pending;
expect(session.pendingRuntimeRequests?.()).toEqual([]);
await expect(session.resolveRuntimeRequest?.({ requestId: "native-question", turnId, resolution: { action: "accept" } })).rejects.toThrow();
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } });
await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed" || event.eventType === "session.failed")).toBe(true));
await session.close(); await consume;
expect(events.filter(event => event.eventType === "runtime_request.expired")).toHaveLength(1);
expect(events.find(event => event.eventType === "runtime_request.expired")).toMatchObject({ turnId, itemId: "question-item", payload });
expect(events.some(event => event.eventType === "session.failed" && event.payload.code === "turn_binding_mismatch")).toBe(false);
expect(events.some(event => event.eventType === "runtime_request.cancelled" || event.eventType === "runtime_request.resolved")).toBe(false);
});
it.each(["turn", "kind", "type", "replay"])("refuses canonical permission expiry with a changed %s binding", async kind => {
const transport = new FakeCodexTransport();
const session = await makeDriver([transport]).openSession({ runId: "expiry-bound", normalizedSessionId: "expiry-bound", workingDirectory: WORKSPACE });
const events: PrpEvent[] = [];
const consume = (async () => { for await (const event of session.events()) events.push(event); })();
const { turnId } = await session.startTurn({ message: { role: "user", text: "Wait for permission" } });
const pending = transport.invoke({ id: "bound-permission", method: "session/request_permission", params: {
threadId: "thread-1", turnId, itemId: "edit", reason: "Edit", choices: [{ key: "accept", label: "Allow once" }, { key: "decline", label: "Deny" }],
origin: { adapter: "acpx-runtime-sidecar", provider: "acpx", method: "session/request_permission" },
} });
await vi.waitFor(() => expect(session.pendingRuntimeRequests?.()).toHaveLength(1));
const payload = { requestId: "bound-permission", requestKind: kind === "kind" ? "runtime" : "permission_approval",
requestType: kind === "type" ? "input" : "permission", replayAllowed: kind === "replay", reason: "provider_process_lost" };
const notification = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", kind === "turn" ? "foreign-turn" : turnId)!;
transport.push(notification.method, notification.params);
await pending; await consume; await session.close();
expect(events.some(e => e.eventType === "runtime_request.expired")).toBe(false);
expect(events.some(e => e.eventType === "session.failed" && e.payload.code === "turn_binding_mismatch")).toBe(true);
});
describe("Codex app-server Codex driver", () => {
it("keeps a prompted MCP tool answerable after rejecting a session-wide decision", async () => {
@@ -23,6 +23,7 @@ import {
parseHarnessRuntimeRequestResolution,
} from "../../contracts/harness-driver.js";
import type { NativeUserMessage } from "../../contracts/types.js";
import type { NativeRunRuntimeGrant } from "../../contracts/runtime-context.js";
import {
CODEX_RESULT_OUTPUT_SCHEMA,
type CodexModelContextSnapshot,
@@ -101,7 +102,7 @@ export class CodexHarnessSession
};
}
async attachRun(input: { runId: string }): Promise<void> {
async attachRun(input: { runId: string; currentRunGrant?: NativeRunRuntimeGrant }): Promise<void> {
this.assertProtocolIntegrity();
const transportOwnsQuiescence = this.transport.attachRun !== undefined;
if (
@@ -116,6 +117,7 @@ export class CodexHarnessSession
runId: input.runId,
turnId: `turn_attachment_${randomUUID().replaceAll("-", "")}`,
itemId: `item_attachment_${randomUUID().replaceAll("-", "")}`,
...(input.currentRunGrant ? { currentRunGrant: structuredClone(input.currentRunGrant) } : {}),
});
this.assertProtocolIntegrity();
if (transportOwnsQuiescence) {
@@ -18,6 +18,10 @@ describe("provider notification authority", () => {
["paperclip/canonicalProviderEvent", { threadId: "child", eventType: "auth.credentials.updated" }, "invalid_authority"],
["paperclip/canonicalProviderEvent", { threadId: null, eventType: "tool.execution.started" }, "invalid_authority"],
["item/started", { threadId: "root", turnId: "active" }, "root"],
["paperclip/runtimeRequestExpired", { threadId: "root", turnId: "active" }, "root"],
["paperclip/runtimeRequestExpired", { threadId: "child", turnId: "active" }, "invalid_authority"],
["paperclip/runtimeRequestExpired", { threadId: null, turnId: "active" }, "invalid_authority"],
["paperclip/runtimeRequestExpired", { threadId: "root", turnId: "old" }, "stale_turn"],
["turn/completed", { threadId: "root", turnId: "old" }, "stale_turn"],
[
"turn/completed",
@@ -8,7 +8,7 @@ import { isPrpAuthCredentialsUpdated, validatePrpStructuredRunResult } from "../
import type { CodexRpcNotification, CodexTraceInterpretation } from "./app-server-transport.js";
import { redactCodexDiagnostic } from "./app-server-transport.js";
import { boundedCodexPayload as boundedPayload, boundedCodexValue, isRetainableCodexPayload } from "./codex-boundaries.js";
import { runtimeRequestResponse } from "./codex-question-adapter.js";
import { runtimeRequestProtocolPayload, runtimeRequestResponse } from "./codex-question-adapter.js";
import {
isSupportedCodexNotificationMethod,
codexThreadLineage as lineageFromThread,
@@ -150,6 +150,27 @@ async function mapNotificationBody(state: CodexSessionState, notification: Codex
const threadId = text(params.threadId);
const turnId = text(params.turnId, text(turn.id));
const itemId = text(item.id, text(params.itemId));
if (notification.method === "paperclip/runtimeRequestExpired") {
const payload = record(params.payload);
const requestId = text(payload.requestId);
const pending = state.pendingRuntimeRequestMap.get(requestId);
if (pending === undefined) return;
const expected = runtimeRequestProtocolPayload(pending.request);
if (pending.request.turnId !== turnId || payload.requestKind !== expected.requestKind
|| payload.requestType !== (pending.request.input === undefined ? "permission" : "input")
|| payload.replayAllowed !== false || typeof payload.reason !== "string" || !payload.reason) {
state.failProtocol("turn_binding_mismatch", "Canonical request expiry changed its callback binding.");
return;
}
state.pendingRuntimeRequestMap.delete(requestId);
// Preserve durable expiry evidence with the same facade identity used
// when creating this callback. Terminal handling must not cancel it again.
state.emit("runtime_request.expired", { ...payload,
turnId: pending.request.turnId, itemId: pending.request.itemId,
}, { turnId: pending.request.turnId, itemId: pending.request.itemId });
pending.settle(safeRequestResponse(pending.request.method, "cancel"));
return;
}
if (notification.method === "paperclip/canonicalProviderEvent") {
if (!isCanonicalProviderEventType(params.eventType)) {
state.failProtocol("provider_event_type_invalid", "Unknown canonical provider event type.");
@@ -142,6 +142,7 @@ export function isSupportedCodexNotificationMethod(method: string): boolean {
method === "model/safetyBuffering/updated" ||
method.startsWith("item/") ||
method === "paperclip/canonicalProviderEvent" ||
method === "paperclip/runtimeRequestExpired" ||
method === "paperclip/workspaceChange/updated" ||
method === "paperclip/runResult" ||
method === "turn/diff/updated" ||
@@ -0,0 +1,117 @@
import { EventEmitter } from "node:events";
import { PassThrough, Writable } from "node:stream";
import { access, rm } from "node:fs/promises";
import { spawn } from "node:child_process";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QUALIFIED_ACPX_PROFILES } from "./acpx/qualified-profiles.js";
const mocks = vi.hoisted(() => ({ verify: vi.fn() }));
vi.mock("./acpx/copilot-installation.js", () => ({ verifyCopilotInstallation: mocks.verify }));
import { createMetadataRpc, probeCopilotMetadata, validateCopilotMetadata } from "./copilot-metadata-probe.js";
function fakeChild(reply: (message: any) => unknown, closeOnEnd = true) {
const child = new EventEmitter() as ChildProcessWithoutNullStreams;
child.stdout = new PassThrough(); child.stderr = new PassThrough();
child.kill = vi.fn(() => { child.emit("close", 0); return true; });
child.stdin = new Writable({ write(chunk, _encoding, done) {
const message = JSON.parse(chunk.toString());
const response = reply(message);
if (response !== undefined) queueMicrotask(() => child.stdout.emit("data", Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, ...response as object })}\n`)));
done();
}, final(done) { if (closeOnEnd) queueMicrotask(() => child.emit("close", 0)); done(); } });
return child;
}
const verified = { status: "verified", version: "1.0.88", profileDigest: QUALIFIED_ACPX_PROFILES.copilot.commandDigest, models: [{ id: "gpt-5.6-luna", label: "GPT" }], promptSent: false };
beforeEach(() => vi.clearAllMocks());
describe("Copilot metadata boundary", () => {
it("sends metadata only, binds an explicit token, and removes its private home", async () => {
const methods: string[] = []; let environment: NodeJS.ProcessEnv = {}, cwd = "";
const close = vi.fn(async () => {});
const child = fakeChild(message => {
methods.push(message.method);
if (message.method === "initialize") return { result: { agentInfo: { version: "1.0.88" } } };
if (message.method === "session/new") return { result: { sessionId: "session", models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "GPT" }] } } };
if (message.method === "session/set_config_option") return { result: { configOptions: [{ id: "model", currentValue: "gpt-5.6-luna" }] } };
return { result: {} };
});
mocks.verify.mockResolvedValue({ commandDigest: verified.profileDigest, openCommand: async () => ({ close, spawn: (_args: unknown, options: any) => { environment = options.env; cwd = options.cwd; return child; } }) });
expect(await probeCopilotMetadata("github_pat_private_test", "gpt-5.6-luna")).toEqual(verified);
expect(methods).toEqual(["initialize", "session/new", "session/set_model", "session/set_config_option"]);
expect(environment.COPILOT_GITHUB_TOKEN).toBe("github_pat_private_test");
for (const key of ["GH_TOKEN", "GITHUB_TOKEN", "NODE_OPTIONS", "SSH_AUTH_SOCK"]) expect(environment).not.toHaveProperty(key);
expect(environment.HOME).toContain(cwd); expect(close).toHaveBeenCalledOnce();
await expect(access(cwd)).rejects.toThrow();
});
it("retires a stubborn native descendant before releasing metadata state", async () => {
let launcher: ChildProcessWithoutNullStreams | undefined;
let providerPid = 0;
let probe: ReturnType<typeof probeCopilotMetadata> | undefined;
const alive = (pid: number) => { try { process.kill(pid, 0); return true; } catch { return false; } };
const provider = `const readline=require("node:readline");process.on("SIGTERM",()=>{});setInterval(()=>{},1000);process.stderr.write("provider-pid:"+process.pid+"\\n");
readline.createInterface({input:process.stdin}).on("line",line=>{const m=JSON.parse(line);const result=m.method==="initialize"?{agentInfo:{version:"1.0.88"}}:m.method==="session/new"?{sessionId:"metadata",models:{availableModels:[{modelId:"gpt-5.6-luna",name:"GPT"}]}}:{};process.stdout.write(JSON.stringify({jsonrpc:"2.0",id:m.id,result})+"\\n");});`;
const script = `const {spawn}=require("node:child_process");process.on("SIGTERM",()=>{});const c=spawn(process.execPath,["-e",${JSON.stringify(provider)}],{stdio:"inherit",detached:false});c.once("exit",()=>process.exit(0));`;
const close = vi.fn(async () => { expect(alive(launcher!.pid!)).toBe(false); await vi.waitFor(() => expect(alive(providerPid)).toBe(false)); });
mocks.verify.mockResolvedValue({ commandDigest: verified.profileDigest, openCommand: async () => ({ close, spawn: (_args: unknown, options: any) => {
launcher = spawn(process.execPath, ["-e", script], { cwd: options.cwd, detached: true, stdio: "pipe" });
launcher.stderr.on("data", value => { const match = /provider-pid:(\d+)/.exec(value.toString()); if (match) providerPid = Number(match[1]); });
return launcher;
} }) });
let timer: ReturnType<typeof setTimeout> | undefined;
try {
probe = probeCopilotMetadata("token");
const result = await Promise.race([probe, new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("metadata cleanup exceeded its bound")), 3500); })]);
expect(result).toMatchObject({ status: "verified", promptSent: false });
expect(providerPid).toBeGreaterThan(0); expect(close).toHaveBeenCalledOnce();
} finally {
if (timer) clearTimeout(timer);
if (launcher?.pid) { try { process.kill(-launcher.pid, "SIGKILL"); } catch {} }
if (probe) await probe.catch(() => undefined);
}
});
it("fails metadata verification and retains private state when shutdown is unconfirmed", async () => {
let cwd = "";
const child = fakeChild(message => ({ result: message.method === "initialize" ? { agentInfo: { version: "1.0.88" } } : { sessionId: "metadata", models: { availableModels: [{ modelId: "gpt-5.6-luna" }] } } }), false);
child.kill = vi.fn(() => false);
const close = vi.fn(async () => {});
mocks.verify.mockResolvedValue({ commandDigest: verified.profileDigest, openCommand: async () => ({ close, spawn: (_args: unknown, options: any) => { cwd = options.cwd; return child; } }) });
try {
expect(await probeCopilotMetadata("token")).toMatchObject({ status: "failed", code: "COPILOT_REQUEST_FAILED", promptSent: false });
expect(close).toHaveBeenCalledOnce();
await expect(access(cwd)).resolves.toBeUndefined();
} finally { child.emit("close", 0); if (cwd) await rm(cwd, { recursive: true, force: true }); }
});
it("rejects an unavailable model without sending a prompt or selecting another model", async () => {
const methods: string[] = [];
const child = fakeChild(message => { methods.push(message.method); return { result: message.method === "initialize" ? { agentInfo: { version: "1.0.88" } } : { sessionId: "session", models: { availableModels: [{ modelId: "other" }] } } }; });
const close = vi.fn(async () => {});
mocks.verify.mockResolvedValue({ commandDigest: verified.profileDigest, openCommand: async () => ({ close, spawn: () => child }) });
expect(await probeCopilotMetadata("token", "missing")).toMatchObject({ status: "failed", code: "COPILOT_MODEL_UNAVAILABLE", promptSent: false });
expect(methods).toEqual(["initialize", "session/new"]); expect(close).toHaveBeenCalledOnce();
});
it("preserves model labels split inside every UTF-8 character across transport chunks", async () => {
const child = fakeChild(() => undefined), rpc = createMetadataRpc(child);
const result = { models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "東京 🚀 café" }] } };
const response = rpc.request("session/new", {});
const wire = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 0, result })}\n`);
for (const byte of wire) child.stdout.emit("data", Buffer.from([byte]));
await expect(response).resolves.toEqual(result);
});
it("cannot dispatch semantic or prompt methods", () => {
const child = fakeChild(() => undefined), rpc = createMetadataRpc(child);
expect(() => rpc.request("session/prompt", {})).toThrow("cannot send");
expect(() => rpc.request("tools/call", {})).toThrow("cannot send");
});
it("rejects outstanding calls when the provider dies", async () => {
const child = fakeChild(() => undefined), rpc = createMetadataRpc(child);
const promise = rpc.request("initialize", {}); child.emit("close", 1);
await expect(promise).rejects.toThrow("provider exited");
});
it("accepts only current profile metadata and sanitized failures", () => {
expect(validateCopilotMetadata(verified)).toEqual(verified);
for (const mutation of [{ promptSent: true }, { profileDigest: "stale" }, { models: [] }, { models: [{ id: "bad id", label: "x" }] }, { models: [{ id: "safe", label: "github_pat_secret" }] }, { models: [...verified.models, ...verified.models] }]) {
expect(validateCopilotMetadata({ ...verified, ...mutation })).toMatchObject({ status: "failed", code: "COPILOT_REQUEST_FAILED" });
}
const failure = validateCopilotMetadata({ status: "failed", code: "COPILOT_AUTH_REQUIRED", message: "github_pat_secret", promptSent: false });
expect(failure).toMatchObject({ code: "COPILOT_AUTH_REQUIRED" }); expect(JSON.stringify(failure)).not.toContain("secret");
expect(validateCopilotMetadata({ status: "failed", code: "untrusted", promptSent: false })).toMatchObject({ code: "COPILOT_REQUEST_FAILED" });
});
});
@@ -0,0 +1,198 @@
import { StringDecoder } from "node:string_decoder";
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { verifyCopilotInstallation } from "./acpx/copilot-installation.js";
import { QUALIFIED_ACPX_PROFILES, resolveQualifiedAcpxProfile } from "./acpx/qualified-profiles.js";
import { classifyCopilotFailure, copilotConfiguration, copilotSandboxEnvironment } from "./acpx/copilot-profile.js";
import { COPILOT_ACP_CLIENT_CAPABILITIES } from "./acpx/copilot-events.js";
type RecordValue = Record<string, unknown>;
const record = (value: unknown): RecordValue => value !== null && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : {};
const METHODS = new Set(["initialize", "session/new", "session/set_model", "session/set_config_option", "session/close"]);
export type CopilotMetadataResult =
| { status: "verified"; version: "1.0.88"; profileDigest: string; models: Array<{ id: string; label: string }>; promptSent: false }
| { status: "failed"; code: string; message: string; promptSent: false };
/** A metadata-only client: no prompt, tool server, workspace, or ambient login. */
export async function probeCopilotMetadata(token: string, model?: string): Promise<CopilotMetadataResult> {
if (model && ["auto", "default"].includes(model.trim().toLowerCase())) return { status: "failed", code: "COPILOT_MODEL_UNAVAILABLE", message: "Select an explicit Copilot model.", promptSent: false };
if (!token.trim() || token.includes("\0")) return { status: "failed", code: "COPILOT_AUTH_REQUIRED", message: "Enter a valid Copilot personal access token.", promptSent: false };
let installation;
try { installation = await verifyCopilotInstallation(resolveQualifiedAcpxProfile("copilot", model ?? "metadata-discovery")); }
catch { return { status: "failed", code: "COPILOT_INSTALLATION_INVALID", message: "The selected environment does not have the verified Copilot runtime installed.", promptSent: false }; }
let root: string | undefined;
let lease: Awaited<ReturnType<typeof installation.openCommand>> | undefined;
let child: ChildProcessWithoutNullStreams | undefined;
let closed: Promise<void> | undefined;
let deadline: ReturnType<typeof setTimeout> | undefined;
let phase = "installation";
try {
lease = await installation.openCommand();
root = await mkdtemp(join(tmpdir(), "paperclip-copilot-metadata-"));
const directories = { homeDirectory: join(root, "home"), configDirectory: join(root, "config"), dataDirectory: join(root, "data"), cacheDirectory: join(root, "cache"), agentHomeDirectory: join(root, "copilot") };
for (const directory of Object.values(directories)) await mkdir(directory, { mode: 0o700 });
await writeFile(join(directories.agentHomeDirectory, "config.json"), JSON.stringify(copilotConfiguration()), { mode: 0o600 });
child = lease.spawn([], { cwd: root, env: { PATH: "/usr/bin:/bin", ...copilotSandboxEnvironment(directories), COPILOT_GITHUB_TOKEN: token }, stdio: "pipe" }) as ChildProcessWithoutNullStreams;
closed = new Promise(resolve => child!.once("close", () => resolve()));
const rpc = createMetadataRpc(child);
const work = async (): Promise<CopilotMetadataResult> => {
phase = "initialize";
const initialized = record(await rpc.request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-copilot-metadata", version: "1" }, clientCapabilities: COPILOT_ACP_CLIENT_CAPABILITIES }));
if (record(initialized.agentInfo).version !== "1.0.88") throw Object.assign(new Error("Verified Copilot installation changed"), { code: "COPILOT_INSTALLATION_INVALID" });
phase = "session-new";
const opened = record(await rpc.request("session/new", { cwd: root, mcpServers: [] }));
const advertised = record(opened.models).availableModels;
const models = (Array.isArray(advertised) ? advertised : []).slice(0, 200).flatMap(value => {
const entry = record(value), id = entry.modelId;
return typeof id === "string" && !["auto", "default"].includes(id.toLowerCase()) && /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,199}$/.test(id)
? [{ id, label: typeof entry.name === "string" && !/[\x00-\x1f\x7f]|github_pat_|gh[pousr]_/.test(entry.name) ? entry.name.slice(0, 200) : id }] : [];
});
if (!models.length) throw new Error("Copilot model discovery unavailable");
if (model) {
if (!models.some(value => value.id === model)) throw Object.assign(new Error("Selected Copilot model unavailable"), { code: "COPILOT_MODEL_UNAVAILABLE" });
phase = "model-selection";
await rpc.request("session/set_model", { sessionId: opened.sessionId, modelId: model });
phase = "model-confirmation";
const configured = record(await rpc.request("session/set_config_option", { sessionId: opened.sessionId, configId: "model", value: model }));
if (!Array.isArray(configured.configOptions) || !configured.configOptions.some(option => record(option).id === "model" && record(option).currentValue === model)) {
throw Object.assign(new Error("Selected Copilot model unavailable"), { code: "COPILOT_MODEL_UNAVAILABLE" });
}
}
// EOF is the authoritative stdio shutdown; session/close is optional.
return { status: "verified", version: "1.0.88", profileDigest: installation.commandDigest, models, promptSent: false };
};
return await Promise.race([work(), new Promise<never>((_resolve, reject) => {
deadline = setTimeout(() => reject(new Error("Copilot metadata probe timed out")), 25_000);
})]);
} catch (error) {
const code = record(error).code;
if (code === "COPILOT_INSTALLATION_INVALID") return { status: "failed", code, message: "The selected environment has an incompatible Copilot installation.", promptSent: false };
if (code === "COPILOT_MODEL_UNAVAILABLE") return { status: "failed", code, message: "The selected Copilot model is unavailable for this account.", promptSent: false };
const failure = classifyCopilotFailure(error);
const cause = error instanceof Error && error.message === "Copilot metadata probe timed out" ? "deadline"
: error instanceof Error && error.message === "Copilot provider exited during metadata discovery" ? "provider-exit" : "request-rejected";
// Only closed diagnostic categories; never log provider errors, wire data,
// credentials, model catalogs, session IDs or private workspace paths.
console.warn("Copilot metadata verification failed", JSON.stringify({ phase, code: failure.code, cause }));
return { status: "failed", ...failure, promptSent: false };
} finally {
if (deadline) clearTimeout(deadline);
let providerRetired = child === undefined;
try {
try {
if (child && closed) {
await stopMetadataChild(child, closed);
providerRetired = true;
}
} finally {
try { await lease?.close(); }
finally { if (providerRetired && root) await rm(root, { recursive: true, force: true }); }
}
} catch {
// Unconfirmed execution retains its private home; never report verified
// metadata or expose paths/provider text after incomplete retirement.
console.warn("Copilot metadata verification failed", JSON.stringify({ phase: "cleanup", code: "COPILOT_REQUEST_FAILED", cause: "shutdown-incomplete" }));
return { status: "failed", code: "COPILOT_REQUEST_FAILED", message: "Copilot metadata verification failed.", promptSent: false };
}
}
}
async function stopMetadataChild(child: ChildProcessWithoutNullStreams, closed: Promise<void>): Promise<void> {
let retired = false;
const observed = closed.then(() => { retired = true; });
const signal = (value: NodeJS.Signals) => {
if (retired) return;
try {
// Verified command leases create a private detached POSIX group. Native
// descendants inherit it and its output pipes, so close (not just the
// launcher's exit) proves retirement before we release the snapshot.
if (process.platform !== "win32" && child.pid !== undefined) process.kill(-child.pid, value);
else child.kill(value);
} catch { /* The bounded close observation below remains authoritative. */ }
};
const term = setTimeout(() => signal("SIGTERM"), 500);
const kill = setTimeout(() => signal("SIGKILL"), 2_000);
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
child.stdin.end();
await Promise.race([observed, new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error("Copilot metadata cleanup timed out")), 3_000);
})]);
} finally {
clearTimeout(term); clearTimeout(kill);
if (timeout) clearTimeout(timeout);
}
}
export function createMetadataRpc(child: ChildProcessWithoutNullStreams) {
const decoder = new StringDecoder("utf8");
const pending = new Map<number, { resolve(value: unknown): void; reject(error: Error): void }>();
let nextId = 0, bytes = 0, buffer = "", failure: Error | undefined;
const fail = (error: Error) => { failure ??= error; for (const call of pending.values()) call.reject(failure); pending.clear(); };
child.once("close", () => fail(new Error("Copilot provider exited during metadata discovery")));
child.once("error", () => fail(new Error("Copilot installation could not start")));
child.stdin.on("error", () => fail(new Error("Copilot metadata transport unavailable")));
child.stderr.on("data", () => {});
child.stdout.on("data", (chunk: Buffer) => {
bytes += chunk.length;
if (bytes > 2 * 1024 * 1024) { fail(new Error("Copilot metadata exceeded its bound")); child.kill("SIGTERM"); return; }
buffer += decoder.write(chunk);
while (buffer.includes("\n")) {
const end = buffer.indexOf("\n"), line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
if (!line.trim()) continue;
let message: RecordValue;
try { message = record(JSON.parse(line)); } catch { fail(new Error("Invalid Copilot metadata response")); return; }
if (message.method) {
if (message.id !== undefined) child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Metadata discovery does not support inbound methods" } })}\n`);
continue;
}
const call = pending.get(message.id as number);
if (!call) continue;
pending.delete(message.id as number);
if (message.error) {
const details = record(message.error);
call.reject(new Error(typeof details.message === "string" ? details.message.slice(0, 8192) : "Copilot metadata request failed"));
} else call.resolve(message.result);
}
});
return { request(method: string, params: RecordValue): Promise<unknown> {
if (!METHODS.has(method)) throw new Error("Metadata discovery cannot send this method");
if (failure) return Promise.reject(failure);
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
});
} };
}
const FAILURE_MESSAGES = {
COPILOT_AUTH_REQUIRED: "Enter a valid Copilot personal access token.",
COPILOT_ENTITLEMENT_DENIED: "Copilot access is denied by the account entitlement or organization policy.",
COPILOT_MODEL_UNAVAILABLE: "The selected Copilot model is unavailable for this account.",
COPILOT_INSTALLATION_INVALID: "Install the verified Copilot runtime in this environment.",
COPILOT_REQUEST_FAILED: "Copilot metadata verification failed.",
} as const;
/** Validate remote metadata and discard all provider-authored error text. */
export function validateCopilotMetadata(value: unknown): CopilotMetadataResult {
const result = record(value);
const invalid = (): CopilotMetadataResult => ({ status: "failed", code: "COPILOT_REQUEST_FAILED", message: FAILURE_MESSAGES.COPILOT_REQUEST_FAILED, promptSent: false });
if (result.promptSent !== false) return invalid();
if (result.status === "failed") {
const code = typeof result.code === "string" && Object.hasOwn(FAILURE_MESSAGES, result.code) ? result.code as keyof typeof FAILURE_MESSAGES : "COPILOT_REQUEST_FAILED";
return { status: "failed", code, message: FAILURE_MESSAGES[code], promptSent: false };
}
if (result.status !== "verified" || result.version !== "1.0.88" || result.profileDigest !== QUALIFIED_ACPX_PROFILES.copilot.commandDigest
|| !Array.isArray(result.models) || result.models.length === 0 || result.models.length > 200) return invalid();
const models: Array<{ id: string; label: string }> = [];
const ids = new Set<string>();
for (const value of result.models) {
const entry = record(value);
if (typeof entry.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(entry.id) || ids.has(entry.id)
|| typeof entry.label !== "string" || entry.label.length > 200 || /[\x00-\x1f\x7f]|github_pat_|gh[pousr]_/.test(entry.label)) return invalid();
ids.add(entry.id); models.push({ id: entry.id, label: entry.label });
}
return { status: "verified", version: "1.0.88", profileDigest: result.profileDigest, models, promptSent: false };
}
@@ -7,6 +7,7 @@ export * from "./turn-stream.js";
export * from "./linux-process-start.js";
export { probeAcpxClaudeInstallation, probeAcpxGrokInstallation, probeAcpxPiInstallation } from "../drivers/acpx/installation-integrity.js";
export { probeCopilotMetadata, validateCopilotMetadata, type CopilotMetadataResult } from "../drivers/copilot-metadata-probe.js";
export { probeAcpxCursorInstallation } from "../drivers/acpx/profile-installation.js";
@@ -158,6 +158,10 @@ it("does not dispatch an expired durable request when its consumer resumes", asy
fixture.emitPermission();
peer.current!.emit("runtime_request.expired", { requestId: "permission-7" });
fixture.emitMarker();
expect((await fixture.iterator.next()).value).toMatchObject({
method: "paperclip/runtimeRequestExpired",
params: { payload: { requestId: "permission-7" } },
});
expect((await fixture.iterator.next()).value?.method).toBe("item/agentMessage/delta");
expect(handler).not.toHaveBeenCalled();
} finally { await fixture.close(); }
@@ -9,6 +9,7 @@ import {
readFile,
readdir,
readlink,
realpath,
rename,
rm,
stat,
@@ -18,6 +19,8 @@ import {
import { createHash, randomUUID } from "node:crypto";
import { execFileSync, spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import { bindAcpxAgentFiles } from "../drivers/acpx/agent-files-binding.js";
import { resolveRunnerProviderAssetsRoot } from "../drivers/acpx/provider-assets-root.js";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -25,7 +28,6 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";
import type { ControlPlanePort } from "../contracts/control-plane-port.js";
import { bindAcpxAgentFiles } from "../drivers/acpx/agent-files-binding.js";
import type { NativeExecutionInputV1 } from "../contracts/native-execution.js";
import type {
NativeSession,
@@ -655,6 +657,69 @@ it("carries the provider attachment seed across consecutive authority rotations"
});
});
it("projects a warm Copilot grant onto the remote runner filesystem", () => {
const runtimeContext = assignedRuntimeContext("/controller/skills", "/controller/instructions");
runtimeContext.instructions.workingCopy = { kind: "agent_files", rootPath: "/remote/registered-agent", entryPath: "AGENTS.md" };
const grant = { runtimeContext, instructions: "Read-only instruction sibling root: /controller/instructions" };
const projected = runnerdRecoveryInternals.projectCopilotRunGrant(grant, "/remote/session/filesystem");
expect(projected.runtimeContext?.instructions.bundle.rootPath).toBe("/remote/session/filesystem/context/instructions");
expect(projected.runtimeContext?.skills[0]?.bundle.rootPath).toBe(`/remote/session/filesystem/context/skills/0-${runtimeContext.skills[0]!.bundle.digest.slice(0, 12)}`);
expect(projected.runtimeContext?.instructions.workingCopy).toEqual(runtimeContext.instructions.workingCopy);
expect(projected.runtimeContext?.aggregateDigest).toBe(runtimeContext.aggregateDigest);
expect(projected.instructions).toBe("Read-only instruction sibling root: /remote/session/filesystem/context/instructions");
expect(grant.runtimeContext.instructions.bundle.rootPath).toBe("/controller/instructions");
expect(runnerdRecoveryInternals.projectCopilotRunGrant(grant)).toEqual(grant);
});
it("reopens Copilot with the current registered files after collecting the previous run copy", async () => {
const root = await mkdtemp(join(tmpdir(), "copilot-current-attachment-"));
const previousRoot = join(root, "previous-agent");
const currentRoot = join(root, "current-agent");
try {
await mkdir(previousRoot);
await mkdir(currentRoot);
await writeFile(join(currentRoot, "AGENTS.md"), "Current instructions.\n");
const previousContext = assignedRuntimeContext("/skills", "/bundle");
previousContext.instructions.workingCopy = { kind: "agent_files", rootPath: previousRoot, entryPath: "AGENTS.md" };
const currentContext = structuredClone(previousContext);
currentContext.instructions.workingCopy!.rootPath = currentRoot;
const seed = {
provider: { kind: "acpx", agent: "copilot", runId: "prior-run", normalizedSessionId: "same-session",
instructions: "Previous instructions.", runtimeContext: previousContext, commandDigest: "unchanged", model: "selected-model" },
};
await rm(previousRoot, { recursive: true });
const attached = runnerdRecoveryInternals.rotatedRunAttachPayload(
{ runAttachTemplate: seed },
{ runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "current-run", normalizedSessionId: "same-session", turnId: "turn", itemId: "item" },
null, undefined,
currentContext, { text: "Current instructions.", context: currentContext },
);
const provider = attached.provider as typeof seed.provider;
const files = bindAcpxAgentFiles(provider.runtimeContext, []);
expect(files?.root).toBe(await realpath(currentRoot));
files?.assertHeld();
expect(provider).toMatchObject({ instructions: "Current instructions.", commandDigest: "unchanged", model: "selected-model", normalizedSessionId: "same-session" });
expect(seed.provider.runtimeContext).toEqual(previousContext);
expect(seed.provider.instructions).toBe("Previous instructions.");
} finally { await rm(root, { recursive: true, force: true }); }
});
it.each(["claude", "codex", "cursor", "pi", "copilot"])("keeps %s's pinned grants on same-run recovery", (agent) => {
const identity = { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "same-run", normalizedSessionId: "session", turnId: "turn", itemId: "item" };
const provider = { kind: "acpx", agent, runId: identity.runId, instructions: "Pinned instructions.", runtimeContext: assignedRuntimeContext("/skills", "/pinned") };
const payload = runnerdRecoveryInternals.rotatedRunAttachPayload({ runAttachTemplate: { provider } }, identity, null, undefined,
undefined);
expect(payload.provider).toMatchObject(provider);
});
it.each(["claude", "codex", "cursor", "pi"])("preserves %s's cross-run context when no new grant is supplied", (agent) => {
const provider = { kind: "acpx", agent, runId: "prior-run", instructions: "Pinned instructions.", runtimeContext: assignedRuntimeContext("/skills", "/pinned") };
const payload = runnerdRecoveryInternals.rotatedRunAttachPayload({ runAttachTemplate: { provider } },
{ runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "new-run", normalizedSessionId: "session", turnId: "turn", itemId: "item" }, null, undefined,
undefined);
expect(payload.provider).toMatchObject({ ...provider, runId: "new-run" });
});
it("restores ACPX with the current registered copy after the old run copy is collected", async () => {
const root = await mkdtemp(join(tmpdir(), "runnerd-acpx-context-rotation-"));
const priorRoot = join(root, "prior-run-copy");
@@ -1830,6 +1895,32 @@ it.each([false, true])("binds a vendored source server to its pnpm dependency ro
await rm(workspace, { recursive: true, force: true });
}
});
it.each(["source", "scoped-npm", "deployment"])(
"admits the controller-selected Copilot package authority in a %s layout",
async (layout) => {
const directory = await realpath(await mkdtemp(join(tmpdir(), "copilot-package-authority-")));
const runnerRoot = layout === "source"
? join(directory, "packages/paperclip-runner")
: layout === "scoped-npm"
? join(directory, "node_modules/@paperclipai/paperclip-runner")
: directory;
await mkdir(join(runnerRoot, "dist/cli"), { recursive: true });
await writeFile(join(runnerRoot, "package.json"), JSON.stringify({ name: "@paperclipai/paperclip-runner" }));
try {
const authority = runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
join(runnerRoot, "dist/cli/acpx-runtime-sidecar.cjs"), runnerRoot, "copilot",
);
expect(authority).toEqual({ root: runnerRoot, manifest: join(runnerRoot, "package.json") });
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", authority.root);
vi.stubEnv("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", authority.manifest);
expect(resolveRunnerProviderAssetsRoot("file:///proc/self/fd/18", "copilot"))
.toBe(join(runnerRoot, "provider-assets/copilot"));
} finally {
vi.unstubAllEnvs();
await rm(directory, { recursive: true, force: true });
}
},
);
it("keeps a self-rooted pnpm deployment inside its dependency authority", async () => {
const deploymentRoot = await mkdtemp(
@@ -31,7 +31,7 @@ import {
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, resolve } from "node:path";
import { basename, dirname, posix, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type {
@@ -80,7 +80,7 @@ import {
createSanitizedAwsAgentCoreEnvironment,
createSanitizedClaudeManagedEnvironment,
} from "../drivers/claude-managed/environment.js";
import { composeNativeSystemInstructions, type NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
import { composeNativeSystemInstructions, type NativeRunRuntimeGrant, type NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
import type {
NativeAcpxPermissionMode,
NativeOpenCodePermissionMode,
@@ -429,6 +429,22 @@ async function rotateExternalAuthorityEpoch(
return controlPlaneState;
}
function projectCopilotRunGrant(
grant: NativeRunRuntimeGrant,
runnerFilesystemRoot?: string,
): NativeRunRuntimeGrant {
const projected = structuredClone(grant);
const context = projected.runtimeContext;
if (!runnerFilesystemRoot || !context) return projected;
const sourceInstructionRoot = context.instructions.bundle.rootPath;
context.instructions.bundle.rootPath = posix.join(runnerFilesystemRoot, "context", "instructions");
context.skills.forEach((skill, index) => {
skill.bundle.rootPath = posix.join(runnerFilesystemRoot, "context", "skills", `${index}-${skill.bundle.digest.slice(0, 12)}`);
});
projected.instructions = projected.instructions.replaceAll(sourceInstructionRoot, context.instructions.bundle.rootPath);
return projected;
}
function rotatedRunAttachPayload(
state: { commands?: unknown; runAttachTemplate?: unknown },
desired: DurableRecoveryIdentity,
@@ -1466,6 +1482,17 @@ export function expandRunnerdCanonicalNotifications(
});
}
/** Keep durable expiry visible to the facade before its terminal notification. */
export function runnerdRuntimeRequestExpiryNotification(
eventType: string, payload: Record<string, unknown>, threadId: string | null, turnId: string | null,
): CodexRpcNotification | null {
if (eventType !== "runtime_request.expired") return null;
return { method: "paperclip/runtimeRequestExpired", params: {
...(threadId ? { threadId } : {}), ...(turnId ? { turnId } : {}),
payload, itemId: payload.itemId,
} };
}
export function runnerdCanonicalNotificationMethod(
eventType: string,
payload: Record<string, unknown>,
@@ -3013,6 +3040,7 @@ function resolveBuildOwnedCliArtifact(
function acpxProviderPackageAuthority(
sidecarScript: string,
ownerPackageRoot = packageRoot,
agent?: QualifiedAcpxAgent,
): {
root: string;
manifest: string;
@@ -3051,6 +3079,15 @@ function acpxProviderPackageAuthority(
);
}
const sidecarPackageRoot = resolve(cliDirectory, "../..");
// Copilot's native closure belongs to the runner package itself. Its asset
// guard requires the bound manifest directly inside that same package root;
// the workspace dependency root used by JS providers is a different owner.
if (agent === "copilot") {
return {
root: sidecarPackageRoot,
manifest: resolve(sidecarPackageRoot, "package.json"),
};
}
// A local source build lives at <workspace>/packages/paperclip-runner and
// resolves dependencies from <workspace>/node_modules. `pnpm deploy` makes
// the package itself the deployment root and owns <deploy>/node_modules/.pnpm.
@@ -3302,7 +3339,9 @@ function createCapabilityRunnerdProviderEnvironmentBase(input: {
input.acpxSidecarPath ??
input.options.acpxSidecarPath ??
resolve(packageRoot, "dist", "cli", "acpx-runtime-sidecar.cjs");
const providerPackageAuthority = acpxProviderPackageAuthority(sidecarPath);
const providerPackageAuthority = acpxProviderPackageAuthority(
sidecarPath, packageRoot, input.options.acpxAgent,
);
// This is the trusted runner/sidecar boundary. The provider sandbox still
// uses createSanitizedAcpxSpawnInput and does not inherit gateway tokens.
const assignedGateway = nativeMcpLaunchBinding(input.options.environment ?? {});
@@ -4102,6 +4141,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
runId: string;
turnId: string;
itemId: string;
currentRunGrant?: NativeRunRuntimeGrant;
}): Promise<void> {
if (this.#pendingWarmRecoveryCompletion !== null) {
throw new Error("native_runner_warm_transition_completion_pending");
@@ -4111,6 +4151,12 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
throw new Error("native_runner_prp_run_rotation_unavailable");
}
await this.#awaitWarmRunAttachmentReady();
const currentRunGrant = input.currentRunGrant
? projectCopilotRunGrant(input.currentRunGrant, this.options.runnerFilesystemRoot)
: undefined;
if (this.options.provider === "acpx" && this.options.acpxAgent === "copilot" && !currentRunGrant) {
throw new Error("native_runner_current_instruction_grant_unavailable");
}
const prior = core.store.state.identity;
const desired: DurableRecoveryIdentity = {
...prior,
@@ -4140,12 +4186,16 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
desired,
this.#authorizedTools,
this.options.resumeCompletionContract,
currentRunGrant?.runtimeContext,
currentRunGrant ? { text: currentRunGrant.instructions, context: currentRunGrant.runtimeContext } : undefined,
)
: rotatedRunAttachPayload(
core.store.state,
desired,
this.#authorizedTools,
this.options.resumeCompletionContract,
currentRunGrant?.runtimeContext,
currentRunGrant ? { text: currentRunGrant.instructions, context: currentRunGrant.runtimeContext } : undefined,
);
this.#runAttachTemplate = structuredClone(runAttachTemplate);
const payload = {
@@ -5562,6 +5612,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
});
this.#core = core;
if (rotatedAuthority) {
if (provider === "acpx" && this.options.acpxAgent === "copilot"
&& typeof this.options.baseInstructions !== "string") {
throw new Error("native_runner_current_instruction_grant_unavailable");
}
const runAttachTemplate = rotatedRunAttachPayload(
controlPlaneState,
desiredIdentity,
@@ -6526,6 +6580,13 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
).requestId;
if (typeof requestId === "string")
this.#bridgedRuntimeInputs.delete(requestId);
// Operator resolution/Stop keep their existing acknowledgement path.
const expiry = runnerdRuntimeRequestExpiryNotification(event.eventType, record(eventPayload), this.#threadId, this.#turnId);
if (expiry) this.#queue.push({ ...expiry,
...(this.options.environment?.PAPERCLIP_PROVIDER_TRACE_PATH ? { paperclipTrace: {
sourceEventId: event.sourceEventId, sourceEventType: event.eventType,
} } : {}),
});
continue;
}
const sessionUpdatePayload = record(eventPayload);
@@ -7459,6 +7520,7 @@ export const runnerdLaunchProfileInternals = Object.freeze({
});
export const runnerdRecoveryInternals = Object.freeze({
projectCopilotRunGrant,
completedMaintenanceTerminalReceipt,
completedMaintenanceTerminalReplayMatches,
readControlPlaneState,
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { resolveQualifiedAcpxProfile } from "./drivers/acpx/qualified-profiles.js";
import { describe, expect, it, vi } from "vitest";
import type { ControlPlanePort } from "./contracts/control-plane-port.js";
@@ -5004,7 +5005,19 @@ describe("executeNativeSession recovery", () => {
expect(attachRun).not.toHaveBeenCalled();
});
it("quarantines a retained session when attachment partially mutates then fails", async () => {
it.each(["codex", "copilot"] as const)("quarantines a retained %s session when attachment partially mutates then fails", async (provider) => {
const qualified = resolveQualifiedAcpxProfile("copilot", "selected-model");
const executionInput: NativeExecutionInputV1 = provider === "copilot" ? {
...input,
session: { ...input.session, driverKind: "acpx_runtime" },
provider: { kind: "acpx", agent: "copilot", model: "selected-model", permissionPolicy: "interactive", profile: {
driverKind: qualified.driverKind, protocolVersion: qualified.protocolVersion, acpxVersion: qualified.acpxVersion,
agent: qualified.agent, agentProfileVersion: qualified.agentProfileVersion,
agentServerPackage: qualified.agentServerPackage, agentServerVersion: qualified.agentServerVersion,
agentRuntimePackage: qualified.agentRuntimePackage, agentRuntimeVersion: qualified.agentRuntimeVersion,
commandDigest: qualified.commandDigest,
} },
} : input;
const attachmentFailure = new Error("provider attachment failed");
const openRun = vi.fn(async () => undefined);
let retainedIdentity = { ...identity, runId: "run-previous" };
@@ -5041,7 +5054,7 @@ describe("executeNativeSession recovery", () => {
async descriptor() {
return {
kind: "mock",
name: "attachment-failure-backend",
name: `attachment-failure-backend-${provider}`,
version: "1",
capabilities: {
resume: true,
@@ -5069,7 +5082,7 @@ describe("executeNativeSession recovery", () => {
await expect(
executeNativeSession({
input,
input: executionInput,
backend,
controlPlane: port,
runnerInstanceId: "runner-recovery",
@@ -5078,7 +5091,9 @@ describe("executeNativeSession recovery", () => {
onSession,
}),
).rejects.toBe(attachmentFailure);
expect(attachRun).toHaveBeenCalledWith({ identity });
expect(attachRun).toHaveBeenCalledWith(provider === "copilot"
? { identity, currentRunGrant: { runtimeContext: null, instructions: expect.any(String) } }
: { identity });
expect(retainedIdentity).toEqual(identity);
expect(onSession).toHaveBeenCalledOnce();
expect(onSession).toHaveBeenCalledWith(null);
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { nativeSystemInstructions } from "./backends/runtime-context.js";
import type {
CheckpointControlPlaneSessionOptions,
@@ -2004,7 +2005,12 @@ export async function executeNativeSession(
// state). Prove the provider attachment before opening durable
// control-plane state because ControlPlanePort has no rollback operation.
try {
await options.existingSession.attachRun({ identity });
await options.existingSession.attachRun({ identity,
...(input.provider.kind === "acpx" && input.provider.agent === "copilot"
? { currentRunGrant: { runtimeContext: "runtimeContext" in input ? input.runtimeContext : null,
instructions: nativeSystemInstructions(input) } }
: {}),
});
} catch (error) {
// attachRun has no transactional guarantee: a provider may bind the new
// run before reporting a later failure. Conservatively quarantine the
@@ -87,6 +87,7 @@ test("the package exposes only the reviewed runner CLI binaries", () => {
"paperclip-runner-eval-session": "./dist/cli/eval-session.js",
"paperclip-runner-codex-proxy": "./dist/cli/codex-app-server-unix-proxy.js",
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js",
"paperclip-runner-copilot-metadata-probe": "./dist/cli/copilot-metadata-probe.js",
"paperclip-runner-opencode-proxy":
"./dist/cli/opencode-app-server-proxy.js",
});
@@ -0,0 +1,44 @@
{
"commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 22,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "ddd98aabe20f4d40b04bb58126bda548a9d85b8234a3f9ea84b6db414e9b4814",
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
}
}
@@ -0,0 +1,44 @@
{
"commandDigest": "sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 23,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "ddd98aabe20f4d40b04bb58126bda548a9d85b8234a3f9ea84b6db414e9b4814",
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
}
}
@@ -0,0 +1,44 @@
{
"commandDigest": "sha256:87e275270e50b42290d10ecec9dfce86ffca2b7a7519cb60c584004adfc86437",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 24,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "6fc6ceb1222cec5586952cf51e2df045ecf751adfc937f5125550a311f5d3896",
"semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
}
}
@@ -0,0 +1,23 @@
{
"schema": "paperclip.copilot.offline-regression-evidence/v1",
"profileVersion": 26,
"profileDigest": "sha256:1a2a0c35764105e9a85112d05187fee0862af23eeb82a75569b5198e52eaba4f",
"priorSourceRevision": "6f424f0f042639631dd6c74053c4a339f6a6ef93",
"runtimeVersion": "1.0.88",
"corrections": [
"Preserve sensitive ancestor names when flattening native notice fields",
"Bind Copilot native event normalization and notice projection in the source identity",
"Assert expiry notification while forbidding stale permission callbacks",
"Mock installed provider registries independently of host assets"
],
"testFiles": 7,
"testsPassed": 154,
"providerCalls": 0,
"qualification": "pending",
"liveQualificationPasses": 0,
"priorProtocolPassesHistorical": 7,
"fullWorkspaceChecksPending": true,
"assertionsOrDeadlinesRelaxed": false,
"otherProviderProfilesChanged": false,
"originalFailuresRetained": true
}
@@ -0,0 +1,44 @@
{
"commandDigest": "sha256:1dad3d26d91e66c6f911dbfa43e7963f36c0bbf0d7709f4bbb568029a8b4b46a",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 21,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "17371b10bef0acfe7a5980e20fd575165f3e28c91608d38590fdca67d58c4edf",
"semanticDirectDriverSourceSha256": "9c27a6ef55d65190af11b597ee2214ba836b4d992cb75d2eccf9c3ea3311d07c",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "36ad8a83a17111167d3cc4ef4123b36fa648a1825acd59bd90b3f8b566a4d335",
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c"
}
}
@@ -0,0 +1,82 @@
{
"commandDigest": "sha256:6ff8e40718abc0ce25e3013923e2435c86ef87a2cea9ca9d2c3f2a891828ce9c",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 43,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "2b24c094da0793888cdb5197b8961a69c410679dad85ab341f89bb812931e972",
"semanticDirectDriverSourceSha256": "4538023d8a2735505b194dee35628807796c2d6c8a77c3309648d2c8f1fca7d5",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "36ad8a83a17111167d3cc4ef4123b36fa648a1825acd59bd90b3f8b566a4d335",
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "afc83fa093afa7a7458ffb46adda0ed9d766723c2d233d5262f9136ef2e4909f",
"configuredEnvironmentRustSourceSha256": "982ed1a1b917a43bd3ea741f9fe23a306c05aee53d633c081409534a18d1a07b",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "c010aef271368b27fdbb0a923f6abd376233f2c9bba786f48cc7cf85d87722d4",
"nativeRuntimeContextSourceSha256": "0b350f5f4044f342b974254738bab45a470c24db9352b04c92d785d26e7853c8",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "79ab6ab323d5b02b6f649bdc61703f8d53f26215fa3555ab72b21d80c9a61278",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "1cb1949a0a9db38a3ed61595533a5464fd8ee8c586ec1ad13700b9a28e9636eb",
"nativeBackendSourceSha256": "0c3791a8a9f7d632804215a0574cec480f6ba130a32ccafbcfce03d7ee5847a1",
"nativeBackendFactorySourceSha256": "02c5cd564029751c4569ff8960d0356d9da63a2e5d2fda958d787f98ee408794",
"nativeInstructionsSourceSha256": "9adf4ff11b5cdaccb312fa9b0343d83213aa071c4ea997d157fbcaa97249aa02",
"runtimeRequestBridgeSourceSha256": "d14744006e0d162f6a1597dd8e8fadd6dcf2a51a61fe0891cabfe3d2d678a5dc",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "4f6c49b0ca172ac8fcd8f8558a58bec560bbf778f4894b70b0ea4613efb20c70",
"nativeExecutionSourceSha256": "b2bf3ba87219c094e8eb659723d43e394d31bfdd1655999d82fb9c97af66c00f",
"profileCompatibilitySourceSha256": "c843647818af25cf69907187f13ee140a45ca8512d28951975b39b285c4fe8d7",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "c9c3650b6415430b045533eea2d7cddfe87302e03817f40ae83f7447ef0ae957",
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c",
"runtimeHostSourceSha256": "687b96bf222b5f132a57668059df61f1cad8f0ef96189c504a7bbc8f990591cc",
"runtimeAdapterSourceSha256": "5bf35ecee5c3853a01f4b226b881af76e0785944189e37d5b891cc2b1cf1de34",
"runtimeSandboxSourceSha256": "4c0e8b3dca5ecaf60edbf5db26bb55a00bdf800f4523a072c69ed8a92d67f8ca",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -0,0 +1,82 @@
{
"commandDigest": "sha256:eb3111dae64c506ccdeaab99c8fc42c64f9b46e757af404825fcd55f37e3d96c",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 44,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "2b24c094da0793888cdb5197b8961a69c410679dad85ab341f89bb812931e972",
"semanticDirectDriverSourceSha256": "4538023d8a2735505b194dee35628807796c2d6c8a77c3309648d2c8f1fca7d5",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "36ad8a83a17111167d3cc4ef4123b36fa648a1825acd59bd90b3f8b566a4d335",
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "afc83fa093afa7a7458ffb46adda0ed9d766723c2d233d5262f9136ef2e4909f",
"configuredEnvironmentRustSourceSha256": "982ed1a1b917a43bd3ea741f9fe23a306c05aee53d633c081409534a18d1a07b",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "c010aef271368b27fdbb0a923f6abd376233f2c9bba786f48cc7cf85d87722d4",
"nativeRuntimeContextSourceSha256": "0b350f5f4044f342b974254738bab45a470c24db9352b04c92d785d26e7853c8",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "79ab6ab323d5b02b6f649bdc61703f8d53f26215fa3555ab72b21d80c9a61278",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "1cb1949a0a9db38a3ed61595533a5464fd8ee8c586ec1ad13700b9a28e9636eb",
"nativeBackendSourceSha256": "0c3791a8a9f7d632804215a0574cec480f6ba130a32ccafbcfce03d7ee5847a1",
"nativeBackendFactorySourceSha256": "02c5cd564029751c4569ff8960d0356d9da63a2e5d2fda958d787f98ee408794",
"nativeInstructionsSourceSha256": "9adf4ff11b5cdaccb312fa9b0343d83213aa071c4ea997d157fbcaa97249aa02",
"runtimeRequestBridgeSourceSha256": "0ff157cf80a767e15312d42f54577a795e49b89298974d4872eb9ab299c3bb79",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "4f6c49b0ca172ac8fcd8f8558a58bec560bbf778f4894b70b0ea4613efb20c70",
"nativeExecutionSourceSha256": "b2bf3ba87219c094e8eb659723d43e394d31bfdd1655999d82fb9c97af66c00f",
"profileCompatibilitySourceSha256": "c843647818af25cf69907187f13ee140a45ca8512d28951975b39b285c4fe8d7",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "c9c3650b6415430b045533eea2d7cddfe87302e03817f40ae83f7447ef0ae957",
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c",
"runtimeHostSourceSha256": "687b96bf222b5f132a57668059df61f1cad8f0ef96189c504a7bbc8f990591cc",
"runtimeAdapterSourceSha256": "5bf35ecee5c3853a01f4b226b881af76e0785944189e37d5b891cc2b1cf1de34",
"runtimeSandboxSourceSha256": "4c0e8b3dca5ecaf60edbf5db26bb55a00bdf800f4523a072c69ed8a92d67f8ca",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -0,0 +1,82 @@
{
"commandDigest": "sha256:55044aa235e88b3fe36ec8a556ee1e3382590870aab907e4462ef7d64af49cea",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 47,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "cc90b59bdfe6a82f7fa3e819e8671f71f7b14047e68942e8086618ffbaacf59f",
"semanticDirectDriverSourceSha256": "ff572638f2cb5d9f8249ff8e2f412311c1e3af209ef0bf7e5ea9f10a21d91103",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623",
"semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c",
"semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c",
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff",
"configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "c010aef271368b27fdbb0a923f6abd376233f2c9bba786f48cc7cf85d87722d4",
"nativeRuntimeContextSourceSha256": "0b350f5f4044f342b974254738bab45a470c24db9352b04c92d785d26e7853c8",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "03f4cd7f76f34f6d82e009b66aa51f18f6e23dc235b660778060e35b6ea07412",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "4fa9593226ff3280bc12140bfcbc2c0986e4cc8d4733381c334138dd03b08f46",
"nativeBackendSourceSha256": "2406483fd35a564971c22e10fc2bad26bae16ff5a3fb6c65690fcf156522ab6d",
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
"nativeInstructionsSourceSha256": "ee8df5d85b263d5d2d0dc9ede8602cb7daf6388687108fe05e2371f5f56c3492",
"runtimeRequestBridgeSourceSha256": "e87de28ae7337642b545d95ae955ed1e09bdd23e9a0a9528c8009afd9be63ae0",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "f6d0c33de38259efefa0f4e0f9737cd30cfd3996a8e875e3fbdb8c72dc133dca",
"nativeExecutionSourceSha256": "fc7c6568feb0d8d709e9baa51839420c9b90336b4865a9b8405aba06e4d1e9c4",
"profileCompatibilitySourceSha256": "2e81a115a20df53643a5adc81e95f10f8cce6c7a318d4910a95a5d51a6eea2c2",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "54fd09ad430dac04df98d829061f6701e73b5bf2a0f4de836d2ac36ee511009f",
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c",
"runtimeHostSourceSha256": "60e5454f030e87f2afa8b1fb120b26adb126b6b17d0e5e82f49c77f76f466cbb",
"runtimeAdapterSourceSha256": "e4c764c2c3c7dd655a965b39472bd1ad8d577d1e7258b25594b171c9429752b8",
"runtimeSandboxSourceSha256": "92dfa53f5a3ed0062913ae417eeccec243b430d444495efde2a8e9c4c02e78ea",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -79,6 +79,7 @@ test("the script's path list and key inputs cover what the release build of the
]);
assert.deepEqual(HASHED_INPUTS, [
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
`${RUNNER_PACKAGE}/acpx-profiles.json`,
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
`${RUNNER_PACKAGE}/runner/crates/**`,
+1
View File
@@ -53,6 +53,7 @@ export const CACHED_PATHS = [
// crate, whatever its form, must land under one of these patterns.
export const HASHED_INPUTS = [
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
`${RUNNER_PACKAGE}/acpx-profiles.json`,
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
`${RUNNER_PACKAGE}/runner/crates/**`,
+18
View File
@@ -0,0 +1,18 @@
import { access } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const installer = new URL("../dist/install-copilot-assets.mjs", import.meta.url);
try {
await access(fileURLToPath(installer));
} catch (error) {
if (error.code !== "ENOENT") throw error;
// Dependencies install before compilation in a source workspace. Published
// server packages omit src and cannot take this bootstrap exception.
await access(fileURLToPath(new URL("../src/services/copilot-connection-probe.ts", import.meta.url)));
await access(fileURLToPath(new URL("../../pnpm-workspace.yaml", import.meta.url)));
process.stdout.write("Copilot runtime assets: source-workspace-pending-build\n");
process.exit(0);
}
const { installCopilotAssets } = await import(installer.href);
const result = await installCopilotAssets();
process.stdout.write(`Copilot runtime assets: ${result.status} (${result.target})\n`);