fix(runner): attest upstream releases and bind ACPX uniformly for every harness (#18325)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native ACPX runs admit a provider only when its saved profile and
`commandDigest` match the current profile exactly.
> - For Copilot, Grok and Pi, `commandDigest` hashed dozens of Paperclip
source files. Claude, Codex, Cursor and Antigravity hash only their
upstream release.
> - Any change to a bound source therefore advanced these profiles.
#18212 alone moved Copilot from v88 to v91 and Grok from v15 to v19.
Each bump reset in-flight sessions and added immutable identity fixtures
(about 160 on master).
> - #18237 added the provider pack `contractDigest`, which now guards
Runner source compatibility at the pack boundary.
> - This pull request changes the Copilot, Grok and Pi digests to
upstream release attestations, as the other providers already use.
> - It also binds the shared ACPX layer the same way for every ACPX
harness: the profile's `acpxVersion` and the pack `contractDigest`,
which now covers the Runner's ACPX patch.
> - The benefit is that Runner source changes no longer reset these
sessions or need new profile versions and fixtures.

## Linked Issues or Issue Description

Refs #18237, #18212, #17972.

**What happened?**

A Runner change that touched a bound source file changed the Copilot,
Grok or Pi `commandDigest`. The author had to advance the profile
version, regenerate an identity fixture and keep every old fixture. On
deploy, saved sessions for that provider failed the exact admission
check and started fresh.

**Expected behavior**

A provider's `commandDigest` changes only when its upstream release
changes: an upstream version, a platform closure, the ACPX patch or the
Grok launcher. Runner source compatibility is the job of the pack
`contractDigest`.

**Steps to reproduce**

1. On master, change a comment in `src/drivers/acpx/runtime-host.ts`.
2. Run `node packages/paperclip-runner/scripts/grok-profile-identity.mjs
--check`.
3. The check fails. The Grok profile must advance, and the next deploy
resets Grok sessions.

## What Changed

- Add `packages/paperclip-runner/scripts/acpx-release-attestations.mjs`.
It computes the release attestation for each provider:
- Copilot and Pi: a canonical-JSON hash of the profile version, upstream
package versions and the platform closure pins (Pi also binds its Node
version).
- Grok: the SHA-256 of `launcher.cjs`. Admission already hashes these
bytes.
- The attestation binds no Paperclip source, shared ACPX layer, absolute
path, pack manifest digest or install layout.
- Bind the shared ACPX layer once for every ACPX harness (Claude, Codex,
Copilot, Cursor, Grok, Pi, Antigravity):
- Add the Runner's ACPX patch (`patches/acpx@<runner acpx
version>.patch`) to the pack `contractDigest`. A patch change now
retires older image packs for every harness. Other packages' ACPX
patches are not read. A build context without the Runner's patch fails.
- Remove the ACPX patch hash from Cursor's release declaration
(`cursor-contract.json`) and advance Cursor once to profile 17. Copilot,
Pi and Cursor no longer bind ACPX in their own digests, like Claude,
Codex, Grok and Antigravity.
- Saved inputs for every harness already must match the current
`acpxVersion`.
  - Claude and Codex digests do not change.
- `generate-acpx-profiles.mjs` writes these digests to
`acpx-profiles.json`. `check:acpx-profiles` refuses a stale digest.
- Move the Copilot closure pins to `copilot-closure-pins.ts`, so that
the generator can import them. `copilot-installation.ts` re-exports
them.
- Advance each profile once and then freeze it: Copilot 92, Grok 20, Pi
37. A version change at the switch keeps the rollout on the same path as
every earlier profile bump.
- Replace the historical version lists in `profile-compatibility.ts`
with a range. Every revision from 1 through the current one decodes.
Claude and Codex still decode revisions 1 through 5. Launch and recovery
admission still require the exact current profile and digest.
- Delete:
- `scripts/copilot-profile-identity.mjs`,
`scripts/grok-profile-identity.mjs` and its test
  - the Grok launcher source stamp
- 159 Copilot, Grok and Pi identity fixtures, including three
unreferenced `copilot-mainline-v*` fixtures. The Cursor fixtures stay.
- Rewrite the affected tests as behaviour checks:
- Each provider admits only the exact current profile. Every earlier
version decodes and is refused at admission.
- The last source-bound digests, and a changed digest at the current
version, are refused by recovery and by the backend factory.
- The Rust Pi descriptor test uses the generated profile instead of
fixtures.
- New `acpx-release-attestations.test.mjs`: a relocated copy of the
inputs gives the same digests, each upstream input changes only its own
providers, and the generator refreshes and checks the manifest.
- Remove the fixture exceptions from `.dockerignore`. Add the
attestation script and the generator to the Daytona image content
inputs.
- Update the Pi install probe and the Rust integration constant for the
new Pi digest.
- Update the docs to describe the current attestation model:
`doc/grok-native-runner.md`, `doc/RUNNER-INTEGRATION-PLAYBOOK.md`,
`doc/observability.md`,
`packages/paperclip-runner/docs/durable-recovery.md`, and the Copilot,
Pi and rich ACP architecture reports. Each report now opens with the
current profile identity; earlier source-bound profiles remain as dated
chronology. No doc references a deleted file.

## Verification

- In `packages/paperclip-runner`:
- `node --test scripts/acpx-release-attestations.test.mjs
scripts/contracts/contract-fingerprints.test.mjs`: 19 tests pass,
including a Runner ACPX patch edit that changes the contract digest,
other patch edits (including `acpx@0.12.0` for `adapter-utils`) that do
not, and a build context without the patch that fails.
  - `node scripts/generate-acpx-profiles.mjs --check` passes.
- `vitest run src/drivers/acpx src/backends src/contracts`: 1647 tests
pass, 9 skipped.
  - `tsc -p tsconfig.json --noEmit` passes.
- From the repository root, `vitest run --config
tests/runner-e2e/vitest.config.ts
tests/runner-e2e/daytona-image.test.ts`: 11 tests pass.
- In `server`, the 8 test files that use ACPX profiles, pack digests or
`contractDigest`: 1617 tests pass.
- Not run locally: the Rust tests and `cargo fmt`, because this machine
has no Rust toolchain. CI runs them.
- Before this change, a synthetic upgrade check built two Runner
revisions that differ only in shared source. A Claude checkpoint saved
under the first revision rebinds under the second, and a changed Claude
`commandDigest` starts fresh. This confirms that the provider JSON has
no other build-dependent field.

## Risks

- **One reset at rollout.** The Copilot, Grok, Pi and Cursor digests and
versions change once. Saved sessions for these providers start fresh on
their next run, as after any earlier profile bump. After this change,
Runner source changes no longer cause this reset.
- **Lost "source changed, so requalify" signal.** The old digest forced
a new profile version when bound source changed. Nothing enforced
requalification: Copilot was last qualified at v47. Pack-boundary
contract changes are now caught by the pack `contractDigest`.
- **Image packs are retired once for every provider.** Adding the ACPX
patch changes the pack `contractDigest`, so the controller uploads its
bundled pack (about 16 s per remote start) until images are refreshed.
Claude and Codex sessions are not reset: their digests and versions do
not change.
- **Grok launcher and closure pins.** The Grok digest covers only the
launcher bytes. Grok's runtime version is still checked separately at
admission, as before.
- **Grok digest equals early Grok profiles.** The launcher bytes have
not changed since profile 1, so the new Grok digest equals the published
profile 1 digest. The profile version still separates them: launch,
recovery and the backend factory require profile 20. An image pack from
that period has no `contractDigest`, so the pack gate refuses it.
`remote-provider-pack-verification.test.ts` now covers this case.
- **Docker context checks** run `generate-acpx-profiles.mjs --check`,
which now imports `.ts` pin files through Node type stripping.
`materialize-pi-distribution.mjs` already relies on this on Node 24.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits, local test runs).

## 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
- [x] 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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nicky LeachandClaude Opus 5.5 authored and GitHub committed 2026-10-30 09:10:50 -07:00
1 parent f5e5b05a33
commit abc270ee4c
199 files changed
+481 -11677

No files matched your search

+2 -1
View File
@@ -2460,7 +2460,8 @@ The Runner's `src/live/provider-pack-contract.json` declares interface revisions
bump only the affected interface when its runner/pack contract breaks. Image
packs must also match the bundled pack's computed `contractDigest`
(`scripts/contracts/contract-fingerprints.mjs`, over the `protocol/` and
`generated/` JSON contracts and Rust shapes), so an unbumped contract change
`generated/` JSON contracts, Rust shapes and the Runner's ACPX patch), so an
unbumped contract or ACPX patch change
still retires older image packs. Declare new runner/pack contracts in a schema
and generate their TypeScript, so the digest covers them. Qualify
and seal packs at image publication. The fast path requires root-owned,
+9 -4
View File
@@ -346,9 +346,13 @@ admission override must never become the production installation recipe.
Pass the actual registered profile through every execution-input parser, not only
the runtime registry. Derive version types from one declaration so a new profile
cannot be admitted by one layer and rejected by another. Generators must preserve
historical identity fixtures; creating today's profile must not rewrite yesterday's
digest. Also check generated catalogs against their authoring source.
cannot be admitted by one layer and rejected by another. A provider's
`commandDigest` attests only its own upstream release: upstream versions, platform
closure pins and any launcher bytes. Never bind Paperclip source, the shared ACPX
version or patch, absolute paths or the pack layout into it. Every harness gets
the ACPX layer the same way: the profile's `acpxVersion` and the provider pack's
`contractDigest`, which also covers the Runner's ACPX patch and Runner source
compatibility. Also check generated catalogs against their authoring source.
### 6.3 Qualify the ordinary setup path as its own integration
@@ -680,7 +684,8 @@ Remote packaging has both a worker and a controller side. Verify that the normal
controller package can locate the correct platform companion and provider-pack
manifest using production resolution, without extracting it manually from a
qualification image. Docker copy/include lists must include new declarations,
patches, and the **active** identity fixture. Bind the manifest to the directory
patches, and the release attestation inputs that `generate:acpx-profiles` reads.
Bind the manifest to the directory
the loader actually consumes; a valid manifest in another directory is not proof.
For remote execution, build on an appropriate host, record an immutable OCI
+14 -31
View File
@@ -1,39 +1,22 @@
# Copilot 1.0.88 rich ACP capability audit
Current source declaration (2026-10-26): **Copilot profile v61** binds controller
journal retirement and the historical profile decoder. Its digest is
`sha256:956276ebee59f880249fa6955adc224af9ca7b6a9da59bcc5da4a8667550c5b3`.
The v60 declaration remains unchanged and readable as history. This source
identity update changes no Copilot executable, arguments, permission policy,
tool receipts, or qualification flags. Offline compatibility checks supply no
new live qualification; prior live outcomes retain their original identities.
## Current profile identity
Historical source declaration (2026-10-26): **Copilot profile v60** binds the shared
sources that carry Codex's explicit hosted-search restriction. Its digest is
`sha256:ca3c2b38ed5f6e25cf1c96c1f73781a86cc90c1c0add93f64716c66f28b3f3aa`.
The v59 declaration remains unchanged and readable as history. Exact-current
launch admission still applies. The Copilot executable, arguments, permissions,
receipts, and qualification flags are unchanged. Copilot authentication probes
do not carry the Codex-only setting. Offline compatibility checks do not supply
new live qualification; prior live outcomes remain attached to their original
source identities.
Copilot runs as **profile 92** on `@github/copilot` 1.0.88. Its `commandDigest`
attests the upstream release: the package version and the complete patched
distribution closure for each qualified platform. It does not bind Paperclip
source or the shared ACPX layer. Every ACPX harness binds the shared ACPX layer the same way: saved inputs must
match the current ACPX version, and the provider pack's `contractDigest` binds
the Runner's ACPX patch with the other Runner contracts. `generate:acpx-profiles` writes the digest to `acpx-profiles.json`,
and `check:acpx-profiles` refuses drift. Launch and recovery admission require
the exact current profile and digest. Saved inputs with an earlier profile
version still decode, so recovery can refuse them.
Historical integration (2026-10-17): **Copilot profile v21 remained pending** with
digest `sha256:1dad3d26d91e66c6f911dbfa43e7963f36c0bbf0d7709f4bbb568029a8b4b46a`.
It binds the shared provider completion contract, direct driver, production sidecar, and controller
continuation resolver. Installation verification requires this generated v21
identity. The permission policy, executable closure, and qualification gate are
unchanged. This source declaration supplies no new paid qualification.
## Profile chronology
Historical integration (2026-10-15): **Copilot profile v20 remained pending** with
digest `sha256:7fa710c697582b1b4d85cdf833178b1940656cb6efb44ac4d356b2f27e6b3d4b`.
Its immutable declaration remains available for decoding historical checkpoints;
it is no longer the current launch identity.
Both profile v19 identities remain historical: upstream credential-observer
`sha256:38abf359adeef43f1f19230245c6e2051c55fdd79571882ef6f00b7cb2958d93` and Muse
`sha256:2fec061f29f9cac4f6aa70f9329e1b27ce0c9cf4825030fdbb78979ced9bc201`.
Neither is the current launch identity.
Until 2026-10-30, these digests hashed Paperclip source files, so each change
to a bound file advanced the profile version. The dated sections below record
that period; their profile versions and digests are not current.
Historical integration (2026-10-10): **Copilot profile v18 remained pending** with
digest `sha256:5e79c2b1a42b98a545bafe9e93f8c6b6c423bec1c18dc1f0a3cf48477b757389`.
+18 -1
View File
@@ -1,6 +1,23 @@
# Cursor ACP capability inventory
Current source (2026-10-06): **Cursor profile v14 combines the model-selection correction with mainline ACP resource-not-found handling**.
## Current profile identity
Cursor runs as **profile 17**. Its `commandDigest` attests the release
declaration in `cursor-contract.json`: the vendor build, the native patch and
the admitted mode, question, accounting and model-selection behaviour. It does
not bind the shared ACPX layer. Every ACPX harness binds the shared ACPX layer the same way: saved inputs must
match the current ACPX version, and the provider pack's `contractDigest` binds
the Runner's ACPX patch with the other Runner contracts. Launch and recovery
admission require the exact current profile and digest. Saved inputs with an
earlier profile version still decode, so recovery can refuse them.
## Profile chronology
Until 2026-10-30, Cursor's release declaration also bound the ACPX patch. The
dated sections below record earlier profiles; their versions and digests are not
current.
Historical source (2026-10-06): **Cursor profile v14 combines the model-selection correction with mainline ACP resource-not-found handling**.
The bundled ACPX runtime forwards unlisted model IDs unchanged, verifies model
selection responses, and reapplies a selected model when a loaded session reports
another model. Its 14 package tests exercise startup and reconnect through a real
+20 -14
View File
@@ -1,18 +1,24 @@
# Pi rich ACP runtime
## Current source integration — 2026-10-19
## Current profile identity
The composed Claude source declares **Pi profile 27**
(`sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba`)
and **Copilot profile 23**, still pending qualification
(`sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63`).
These identities bind the shared Claude effort and lifecycle changes plus
master's OpenRouter attribution. Copilot's shared bridge binding also includes
Claude-only essential tool metadata; its own tool catalog is unchanged. Provider executable closures, model/effort
options and qualification boundaries are unchanged. Current package and source
checks remain separate from retained provider execution results, which keep
their literal profile/source identities. Earlier immutable fixtures remain
historical; they are not accepted as the current source identity.
Pi runs as **profile 37** on `pi-acp` 0.0.33 and
`@earendil-works/pi-coding-agent` 1.0.0. Its `commandDigest` attests the
upstream release: the package versions, the Node version and the complete
distribution closure for each qualified platform. The closure includes the
patched wrapper, the runtime helper and the Paperclip extension, so a change to
those files changes the closure pins and the digest. Other Paperclip source does
not. Every ACPX harness binds the shared ACPX layer the same way: saved inputs must
match the current ACPX version, and the provider pack's `contractDigest` binds
the Runner's ACPX patch with the other Runner contracts. Launch and recovery admission require the exact current profile
and digest. Saved inputs with an earlier profile version still decode, so
recovery can refuse them.
## Profile chronology
Until 2026-10-30, these digests hashed Paperclip source files, so each change
to a bound file advanced the profile version. The dated sections below record
that period; their profile versions and digests are not current.
## Historical source integration — 2026-10-12
@@ -311,7 +317,7 @@ progress but cannot become a final answer. Missing or conflicting boundaries
fail closed. Loaded history has a separate display-only identity. Retry,
compaction, extension and command notices retain bounded, redacted severity and
metadata on `paperclip/pi_notice`; they are never assistant final content.
The [v8 declaration](../../packages/paperclip-runner/test-fixtures/pi-acp/profile-v8-identity.json)
The v8 declaration
also pins the shared ACPX parser and Pi host projections. Previous sessions must
be reopened. No paid profile-v8 qualification has run.
@@ -356,7 +362,7 @@ was aggregated into the final item. Three runs settled across these two cells,
all owned processes exited, and five later cells did not launch. The native
message attribution defect is repaired in v8; authenticated v8 qualification
remains pending. The canonical
[v7 declaration](../../packages/paperclip-runner/test-fixtures/pi-acp/profile-v7-identity.json)
v7 declaration
binds the exact wrapper, helper, extension and three target closures.
Credential-free regressions run the pinned SDK with synthetic model streams to
@@ -1,18 +1,27 @@
# Rich ACP integration and qualification report
## Current source integration — 2026-10-19
## Current profile identity
The composed Claude source declares **Pi profile 27**
(`sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba`)
and **Copilot profile 23**, still pending qualification
(`sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63`).
These identities bind the shared Claude effort and lifecycle changes plus
master's OpenRouter attribution. Copilot's shared bridge binding also includes
Claude-only essential tool metadata; its own tool catalog is unchanged. Provider executable closures, model/effort
options and qualification boundaries are unchanged. Current package and source
checks remain separate from retained provider execution results, which keep
their literal profile/source identities. Earlier immutable fixtures remain
historical; they are not accepted as the current source identity.
Every ACPX provider's `commandDigest` attests only its own upstream release.
Copilot (profile 92), Grok (20) and Pi (37) bind their upstream package
versions, their platform closure pins and, for Grok, the launcher bytes.
`scripts/acpx-release-attestations.mjs` computes these digests,
`generate:acpx-profiles` writes them to `acpx-profiles.json`, and
`check:acpx-profiles` refuses drift. Cursor (17) attests the release declaration
in `cursor-contract.json`; Claude, Codex and Antigravity attest their upstream
packages or distributions. Runner source changes do not change any of them.
Every ACPX harness binds the shared ACPX layer the same way: saved inputs must
match the current ACPX version, and the provider pack's `contractDigest` binds
the Runner's ACPX patch with the other Runner contracts. Launch
and recovery admission require the exact current profile and digest. Saved
inputs with an earlier profile version still decode, so recovery can refuse
them. A profile identity is not evidence of live qualification.
## Profile chronology
Until 2026-10-30, these digests hashed Paperclip source files, so each change
to a bound file advanced the profile version. The dated sections below record
that period; their profile versions and digests are not current.
## Historical source integration — 2026-10-14
@@ -25,13 +34,12 @@ profile/source identity. Copilot remains pending at profile 19. Current package
and source controls are separate from those retained provider execution results.
The actual Codex squash `adb69e8f09852c772d9901ae270e5b2d66e39c77`
already owns Copilot profile 18 for the Grok descriptor schema. Its immutable
fixture is retained. The combined Claude effort sources use profile 19
already owns Copilot profile 18 for the Grok descriptor schema. The combined
Claude effort sources use profile 19
(`sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321`),
with executable, permission policy and pending qualification unchanged. The
prior unmerged Claude profile-18 attestation is retained separately as
`copilot-profile-v18-claude-qualification-identity.json`; earlier evidence keeps
its literal source. Pi profile 25 and its source digest remain unchanged by
prior unmerged Claude profile-18 attestation was recorded separately; earlier
evidence keeps its literal source. Pi profile 25 and its source digest remain unchanged by
this reconciliation. Existing settings and effort/session admission still need
final packaged and real-provider qualification on the composed Claude source.
+9 -13
View File
@@ -67,19 +67,15 @@ its native runtime identity is `native:grok` version 1.0.13. The historical
`agentServerPackage`/`agentRuntimePackage` wire fields carry these identities,
not npm dependencies. Existing npm-backed ACP bridges retain their package pins.
Grok launch profile 7 binds the native effort, search, credential, sidecar, and
Runner admission source, including Rust backend descriptor writes. An older
installed pack with the same ACPX interface version cannot satisfy this profile.
The controller stages its current verified
pack instead. A generated source-identity comment in the launcher preserves the
existing command byte-digest check; no installation guard is relaxed.
App source changes alone still permit reuse when the selected
Grok profile matches. Historical profile versions remain decodable, but they do
not authorize a fresh launch. After changing a bound source, advance the profile
and run `node packages/paperclip-runner/scripts/grok-profile-identity.mjs`.
The normal `check:acpx-profiles` build gate verifies this source identity.
The generator refuses to replace an existing versioned identity with different
source. Advance the version and retain the previous version's recovery decoder.
Grok runs as launch profile 20. Its `commandDigest` attests the upstream
release: the SHA-256 of `launcher.cjs`, which admission hashes before launch. It
changes only when the launcher bytes change. Runner source compatibility is
guarded at the pack boundary by the provider pack's `contractDigest`, so the
controller reuses an installed pack only when that digest matches and stages its
current verified pack otherwise. Earlier profile versions decode for recovery,
but they do not authorize a fresh launch. After changing the launcher, run
`pnpm --filter @paperclipai/paperclip-runner generate:acpx-profiles`; the
`check:acpx-profiles` build gate refuses a stale digest.
Provision Grok Build 1.0.13 at
`/opt/paperclip/providers/grok/1.0.13/grok` in the selected execution environment.
+3 -6
View File
@@ -2109,9 +2109,6 @@ rank optimizations, deploy a compatible source set and inspect actual collector
exports for parentage, concurrent ownership, widths and closed failure spans.
Unit tests do not establish ingestion or a latency improvement.
The shared ACPX instrumentation advances Grok source profile 13 to 14 and
Copilot 84 to 85. Historical identities remain unchanged and decodable. Provider
packages, models and permission defaults are unchanged; Pi's bound declaration
is unchanged. These are source attestations, not evidence of fresh live provider
qualification. Re-run compatibility qualification for the new identities before
rollout.
The shared ACPX instrumentation does not change any provider profile identity:
Copilot, Grok and Pi digests attest their upstream releases, not Runner source.
Provider packages, models and permission defaults are unchanged.