diff --git a/doc/architecture/runner-cursor-capabilities.md b/doc/architecture/runner-cursor-capabilities.md index 2b28ddce4e..bd1cc62d19 100644 --- a/doc/architecture/runner-cursor-capabilities.md +++ b/doc/architecture/runner-cursor-capabilities.md @@ -1,12 +1,16 @@ # Cursor ACP capability inventory -Current source candidate (2026-09-30): **Cursor profile v8 is unqualified**. -Its declaration binds `paperclip-cursor-usage-v3`, all three newly verified native +Current source candidate (2026-09-30): **Cursor profile v9 is unqualified**. +Its declaration binds `paperclip-cursor-usage-v4`, all three newly verified native closures and the shared ACPX patch that persists bounded diagnostic observations. Every native counter receipt remains partial with unverified semantics; it cannot satisfy token or dollar accounting. The exact model remains unpriced until a verified rate is available. Prior v7 source, builds and paid observations below -are historical and do not qualify v8. Old v7 sessions must be reopened. +are historical and do not qualify v9. Old v7/v8 sessions must be reopened. +The v9 collector marks missing, invalid or reused child run ordinals as +`child_run_attribution_unverified`. The offline proof executes the pinned +vendor child creation and reuse methods on all three platforms; child counter +aggregation semantics remain unverified. Historical v7 checkpoint (2026-09-30): **Cursor profile v7 remains unqualified**. The corrected local native-plan case on controller/Product source `3d21d375de2b6249d9f5322bf001ce0ce0e052aa` and native runtime `5b8e4454ef0bf12d0bb068c2e41d8c9df9356a1c` passed all 26 checks. Reject, feedback, revision-bound acceptance, browser reconnect, exact delivery and unchanged workspace bytes passed. The single planning run succeeded while the task remained In Progress with an explicit next-message wait; no implementation or task completion is claimed. Cleanup and full end integrity passed. Result SHA-256: `685609f01291db84f40d890ef562473b95065e86907bd964b656ad732f6dd97a`. The earlier controller40064 attempt remains failed. Controller fix `c58a8881f` uses the production schema/policy/contract hash envelope; native runtime/profile bytes are unchanged. That exact Cursor PR head has green CI and Greptile 5/5. All three platform builds exist, but the remaining current-profile local/Daytona cases are still required. Native AskQuestion availability remains unverified. Cursor on-demand usage remains $0 under the approved fixed $25 account cap; native per-run USD is unknown. See the [comparative capability report](runner-rich-acp-capabilities.md) for exact source roles, evidence and remaining gates. @@ -84,24 +88,24 @@ cancelled-work accounting. A thrown JSON-RPC error returns no prompt envelope, so these observations are not retained on that error path. This is a new source candidate, not a change to the frozen v7 runtime or a regrading of historical evidence. -The `paperclip-cursor-usage-v3` candidate retains the vendor version and archive +The `paperclip-cursor-usage-v4` candidate retains the vendor version and archive pins. Private full-tree materialization verifies 446 files per macOS target and 454 files on Linux; only the ACP implementation chunk differs from each vendor tree. Its patched closures are: | Platform | Patched closure SHA-256 | | --- | --- | -| macOS ARM64 | `0681abb675b46054155e0b3f28850bf4668cf24fb1aab457dad1f24b61b3ee6b` | -| macOS x64 | `18caa80f90f70f6a7d815335062b0858327891ec854aebe90b585a42eac578e6` | -| Linux x64 | `0ee4ceb24062e991650bfb4c0b4ec5fbd7f1a01487b8c70aef0cf257509701a4` | +| macOS ARM64 | `257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30` | +| macOS x64 | `6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2` | +| Linux x64 | `eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74` | -`usage-v3-runtime-patch-offline-proof.json` retains fresh isolation/instruction -checks, and `native-usage-v3-offline-proof.json` records the usage checks. Earlier +`usage-v4-runtime-patch-offline-proof.json` retains fresh isolation/instruction +checks, and `native-usage-v4-offline-proof.json` records the usage checks. Earlier proof files remain historical. These are offline source proofs; native emission semantics, accounting completeness and live qualification remain unproven. The pinned-source offline harness executes the patched prompt method, native -invocation expression and child-update method on all three platform chunks with +invocation expression, child construction/reuse and child-update methods on all three platform chunks with transport/session dependency doubles. It proves routing and bounds, not actual backend emissions, retry/subagent inclusion, cache/reasoning overlap or billing. Shared metadata persistence and new distribution/profile identities are separate @@ -118,7 +122,7 @@ Question and plan requests lack a session ID. The shared hook binds them to the Fixed launch arguments are `--disable-project-configs --disable-auto-update acp`, using the verified bundled Node and absolute verified `index.js`. Private HOME/XDG roots are required together with CURSOR_CONFIG_DIR, CURSOR_DATA_DIR, disabled compilation caching, `AGENT_CLI_CREDENTIAL_STORE=memory`, and `NO_OPEN_BROWSER=1`. Only explicitly bound CURSOR_API_KEY or CURSOR_AUTH_TOKEN may enter. The controller creates a provider/session-scoped credential-name binding from the explicit task environment, ignoring inherited or caller-supplied markers. Rust forwards it through the closed sidecar environment boundary. Before host admission, the sidecar rejects missing, stale, wrong-provider or unbound credentials and removes the marker before provider launch. Tests cover this full boundary and preserve legacy credential behavior. Do not use `--force`, `--trust`, or `--approve-mcps` to paper over governance. -`--disable-project-configs` only suppresses `.cursor/cli.json`. It does not suppress project MCP, Cursor/Claude hooks, or installed plugins. `assertCursorWorkspacePolicy` refuses ambient project execution config from the workspace through its nearest Git root, including symlinks and unreadable configuration. Ordinary Claude settings with neither hooks nor plugins remain admissible. Enterprise system hooks are checked too. The host repeats admission checks before native launch. The vendor admission check alone cannot prevent a concurrent file mutation. The current `paperclip-cursor-usage-v3` distribution patch removes local hook loading, asynchronous team hook synchronization, ambient MCP loader initialization, and ambient-client merging from the ACP source. The retained admission check remains defense in depth. This closes those discovery paths independently of filesystem polling; authenticated validation on the patched final runtime remains required. +`--disable-project-configs` only suppresses `.cursor/cli.json`. It does not suppress project MCP, Cursor/Claude hooks, or installed plugins. `assertCursorWorkspacePolicy` refuses ambient project execution config from the workspace through its nearest Git root, including symlinks and unreadable configuration. Ordinary Claude settings with neither hooks nor plugins remain admissible. Enterprise system hooks are checked too. The host repeats admission checks before native launch. The vendor admission check alone cannot prevent a concurrent file mutation. The current `paperclip-cursor-usage-v4` distribution patch removes local hook loading, asynchronous team hook synchronization, ambient MCP loader initialization, and ambient-client merging from the ACP source. The retained admission check remains defense in depth. This closes those discovery paths independently of filesystem polling; authenticated validation on the patched final runtime remains required. Artifact paths are never automatically read or uploaded. References require a present regular file under the physical workspace, reject traversal and symbolic links, and retain `registered:false`. Private/outside paths are omitted and produce a visible warning. This is metadata validation, not durable file ownership or a replacement for artifact registration checks. diff --git a/doc/architecture/runner-rich-acp-capabilities.md b/doc/architecture/runner-rich-acp-capabilities.md index 00ebdb751f..aa64d54de8 100644 --- a/doc/architecture/runner-rich-acp-capabilities.md +++ b/doc/architecture/runner-rich-acp-capabilities.md @@ -1,20 +1,25 @@ # Rich ACP integration and qualification report -Current source checkpoint (2026-09-30): **Cursor v8, Copilot v8 and Pi v10 +Current source checkpoint (2026-09-30): **Cursor v9, Copilot v8 and Pi v10 remain unqualified**. All three declarations bind shared ACPX patch SHA-256 `bd5393058a218040d217fa85449d59a6f30507de54cd645bf0ef21422823f85e`. -Cursor additionally binds newly materialized `paperclip-cursor-usage-v3` closures; +Cursor additionally binds newly materialized `paperclip-cursor-usage-v4` closures; Copilot and Pi native distribution bytes are unchanged. The new identities reject -retained 7/7/9 sessions. Claude, Codex and Grok profile declarations stay unchanged. +retained 7/7/9 sessions, plus Cursor v8 sessions. Claude, Codex and Grok profile declarations stay unchanged. The metadata is observation-only: native counters always remain partial, absent fields stay absent, and no token totals or dollar costs are inferred. A supported schema does not prove native counter aggregation semantics. No new paid pass, build, publication or production qualification is claimed for these identities. -The external eval definitions still pin 7/7/9 and need a separate reviewed update. +External eval definitions for 8/8/10 merged in [#35](https://github.com/paperclipai/paperclip-evals/pull/35) +as `25303cf84953985b961b95f89aff0bdb864b2d65`, from head +`b4ef1aa1296f8897714325d189e5a6f51dad9e01`: 136 deterministic tests, +21 validated cells, passing CI and Apex 5/5. Models, pricing and graders are +unchanged; this adds no paid proof. Those definitions remain the historical +Cursor v8 checkpoint and require a separate reviewed Cursor v9 update. | Source candidate | Remaining production gate | | --- | --- | -| Cursor v8 | Qualify the exact new runtime locally and on Daytona; resolve native counter semantics and verified exact-model pricing before treating observations as accounting. Native input, permissions, recovery, isolation and warm continuity remain required. | +| Cursor v9 | Qualify the exact new runtime locally and on Daytona; resolve native counter semantics and verified exact-model pricing before treating observations as accounting. Native input, permissions, recovery, isolation and warm continuity remain required. | | Copilot v8 | Admit fresh exact-runtime packs/images and qualify the required local/Daytona cases. Prior v7 passes remain case-specific historical proof. Native external-tool/sampling/limits callbacks must not leave unresolved admitted requests. | | Pi v10 | Retain the enforced spending-bound requirement, then qualify the exact runtime locally and on Daytona. The optional transport-budget candidate remains unintegrated. | @@ -59,6 +64,13 @@ are awaiting a fresh commit review in [#14695](https://github.com/paperclipai/pa paid attempts remain retained; neither source tests nor session-ID reuse alone prove paid warm process continuity. +Review follow-up `84a50124a997f4256932eab15fe79dd9ce875940` bounds immediate +collection retries and preserves pending ownership after a failed stop through +generic run cleanup. A later confirmed close or independent current-run stop +proof can collect the retained files. Its 547 pure tests, 65 database-backed +cases and direct server typecheck pass; owned process, IPC and temporary-root +cleanup completed. Fresh Apex and CI are pending. This adds no paid warm proof. + Cursor's pinned native `TurnEndedUpdate` has optional input, output, cache-read, cache-write and reasoning counters. Its vendor ACP prompt response does not project them. No evidence shows Paperclip dropped a supplied ACP usage receipt in diff --git a/packages/paperclip-runner/cursor-distributions.json b/packages/paperclip-runner/cursor-distributions.json index 2f281fec15..c049c08019 100644 --- a/packages/paperclip-runner/cursor-distributions.json +++ b/packages/paperclip-runner/cursor-distributions.json @@ -5,7 +5,7 @@ "darwin-arm64": { "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/darwin/arm64/agent-cli-package.tar.gz", "archiveSha256": "538827d96a779bab854a865c8e42859e8e87db34b5f69770261b90fc8cfff191", - "closureSha256": "0681abb675b46054155e0b3f28850bf4668cf24fb1aab457dad1f24b61b3ee6b", + "closureSha256": "257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30", "executable": "node", "entrypoint": "index.js", "vendorClosureSha256": "77394184a89b0e7384971181da19c3c82d83a399b04e7944b3f94fe3d7e62b25" @@ -13,7 +13,7 @@ "darwin-x64": { "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/darwin/x64/agent-cli-package.tar.gz", "archiveSha256": "ed1771c44cbf0f8059c67cac0d939e6a60eb730bd066d7dc4cbb14de314cfb6e", - "closureSha256": "18caa80f90f70f6a7d815335062b0858327891ec854aebe90b585a42eac578e6", + "closureSha256": "6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2", "executable": "node", "entrypoint": "index.js", "vendorClosureSha256": "7c8775160af74e5fb8d25a30e8412c1a6476b0661529078a686e5118fd1c5e5f" @@ -21,11 +21,11 @@ "linux-x64": { "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/linux/x64/agent-cli-package.tar.gz", "archiveSha256": "8085fd120f5c71f4eae7fea26a043718e5644e3071e4fab3220a0e58c51f9593", - "closureSha256": "0ee4ceb24062e991650bfb4c0b4ec5fbd7f1a01487b8c70aef0cf257509701a4", + "closureSha256": "eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74", "executable": "node", "entrypoint": "index.js", "vendorClosureSha256": "bf04ef8ea6a63191067a6b3e15139aa2c793d8419daab246aa3f0a012e1bbcd9" } }, - "patchVersion": "paperclip-cursor-usage-v3" + "patchVersion": "paperclip-cursor-usage-v4" } diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index fbe2280a20..1e1001ca36 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -215,7 +215,7 @@ impl AcpxProviderDescriptor { "2026.09.26-dd393fe", None, None, - "sha256:b9e94cbcdce2783665612c85caf019c4defe37d3d245a694de590ebdf5b1f1a3", + "sha256:a76ad26878a3b3328154901563cbda857e53583e4e01787f35a797992ef76162", ), "copilot" => ( self.model.as_str(), @@ -2803,7 +2803,7 @@ mod tests { "cursor", "cursor-agent", "2026.09.26-dd393fe", - "sha256:b9e94cbcdce2783665612c85caf019c4defe37d3d245a694de590ebdf5b1f1a3", + "sha256:a76ad26878a3b3328154901563cbda857e53583e4e01787f35a797992ef76162", None, None, "explicit-model", @@ -2843,6 +2843,15 @@ mod tests { } let valid: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap(); valid.validate(&context()).unwrap(); + if agent == "cursor" { + let mut previous_v8 = value.clone(); + previous_v8["commandDigest"] = json!( + "sha256:b9e94cbcdce2783665612c85caf019c4defe37d3d245a694de590ebdf5b1f1a3" + ); + let previous_v8: AcpxProviderDescriptor = + serde_json::from_value(previous_v8).unwrap(); + assert!(previous_v8.validate(&context()).is_err()); + } let mut previous_identity = value.clone(); previous_identity["commandDigest"] = json!(match agent { "cursor" => diff --git a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs index f791bac9a7..598c9ce991 100644 --- a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs @@ -28,7 +28,7 @@ export async function materializeCandidateProviderPack({ provider, outputRoot }) if (provider === "cursor") { const result = await materializePinnedCursorDistribution({ destination: outputRoot }); return { version: result.version, - profileDigest: "sha256:b9e94cbcdce2783665612c85caf019c4defe37d3d245a694de590ebdf5b1f1a3", + profileDigest: "sha256:a76ad26878a3b3328154901563cbda857e53583e4e01787f35a797992ef76162", closureDigest: `sha256:${result.closureSha256}` }; } throw new Error(`The ${provider} candidate distribution builder is not included in this source revision`); diff --git a/packages/paperclip-runner/scripts/cursor-native-usage.mjs b/packages/paperclip-runner/scripts/cursor-native-usage.mjs index d555bc6e45..d46c65869d 100644 --- a/packages/paperclip-runner/scripts/cursor-native-usage.mjs +++ b/packages/paperclip-runner/scripts/cursor-native-usage.mjs @@ -48,7 +48,8 @@ export function createCursorNativeUsage(promptId) { return { beginParent() { return begin("parent", invocations + 1); }, observeChild(child, update) { - if (closed || !child || child.terminal || !Number.isSafeInteger(child.runs) || child.runs < 1) return; + if (closed || !child || child.terminal) return; + if (!Number.isSafeInteger(child.runs) || child.runs < 1) { reasons.add("child_run_attribution_unverified"); return; } // The native callback identifies only the child, not its run generation. // After ID reuse, delayed old-run updates cannot be distinguished safely. if (child.runs !== 1) { reasons.add("child_run_attribution_unverified"); return; } diff --git a/packages/paperclip-runner/scripts/cursor-native-usage.test.mjs b/packages/paperclip-runner/scripts/cursor-native-usage.test.mjs index 1628082adf..7a491b51aa 100644 --- a/packages/paperclip-runner/scripts/cursor-native-usage.test.mjs +++ b/packages/paperclip-runner/scripts/cursor-native-usage.test.mjs @@ -116,3 +116,16 @@ test("maximum bounded ACPX identities and envelope fit the final stored object", assert.ok(Buffer.byteLength(JSON.stringify(stored)) <= 16384); assert.ok(Buffer.byteLength(JSON.stringify(stored)) - Buffer.byteLength(JSON.stringify(receipt)) <= 640); }); + + +test("missing, invalid and reused child run ordinals are explicitly partial", () => { + for (const runs of [undefined, null, 0, -1, 1.5, "1", NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, 2]) { + const usage = createCursorNativeUsage(id); + const parent = usage.beginParent(); parent.observe(event({ inputTokens: 1 })); + usage.observeChild({ runs, terminal: false }, event({ inputTokens: 999 })); + const receipt = usage.finish(); + assert.equal(receipt.completeness, "partial"); + assert.equal(receipt.observations.length, 1); + assert.ok(receipt.reasons.includes("child_run_attribution_unverified")); + } +}); diff --git a/packages/paperclip-runner/scripts/cursor-runtime-patch.mjs b/packages/paperclip-runner/scripts/cursor-runtime-patch.mjs index 7a9e9b3991..b55d921284 100644 --- a/packages/paperclip-runner/scripts/cursor-runtime-patch.mjs +++ b/packages/paperclip-runner/scripts/cursor-runtime-patch.mjs @@ -5,11 +5,11 @@ import { createCursorNativeUsage } from "./cursor-native-usage.mjs"; // This is an owned ACP-only patch of the immutable vendor archive. The legacy // Cursor adapter and the vendor's interactive CLI are not changed. -export const CURSOR_RUNTIME_PATCH_VERSION = "paperclip-cursor-usage-v3"; +export const CURSOR_RUNTIME_PATCH_VERSION = "paperclip-cursor-usage-v4"; export const CURSOR_RUNTIME_PATCH_PINS = Object.freeze({ - "darwin-arm64": { file: "5672.index.js", before: "7784c8b16d4e639814c13b12be2a687f5dc2cd98cbf29ed0a10820778ab1bf62", after: "83fa4d462b3d8fd95b05420589cc80b8a6536dbff0cc3c5d36fec69d8a753970" }, - "darwin-x64": { file: "9841.index.js", before: "3c0aaf6ecc4fceb0f95e1731deec75384984570d14d0871cecec11972b948ac4", after: "060c8816fac3d110f5931ce626e5f2d841d8252fba7dafd52e37157ca915301d" }, - "linux-x64": { file: "1699.index.js", before: "2de420f1b31e70ca74b083a1ce5b519c2abffa5789d71cc06e84d2c38acb7c07", after: "3435f90f028eb5b08cf8cb7b289694993636f2868e6af217c167a5fd27ac03b2" }, + "darwin-arm64": { file: "5672.index.js", before: "7784c8b16d4e639814c13b12be2a687f5dc2cd98cbf29ed0a10820778ab1bf62", after: "35c5bf13b261ea884bb0e7b26ae7bd4c4999b0b4b08ab338bc22f2ccce2b8108" }, + "darwin-x64": { file: "9841.index.js", before: "3c0aaf6ecc4fceb0f95e1731deec75384984570d14d0871cecec11972b948ac4", after: "5b2510248429febe1c9aead6ad43d35d89cdedc4dd057d4f07495efa33c7da55" }, + "linux-x64": { file: "1699.index.js", before: "2de420f1b31e70ca74b083a1ce5b519c2abffa5789d71cc06e84d2c38acb7c07", after: "38fe96fa71463372bffdd9efa3e5608293ff075d079149fe20a1625783c4eb39" }, }); const digest = value => createHash("sha256").update(value).digest("hex"); diff --git a/packages/paperclip-runner/scripts/materialize-cursor-distribution.test.mjs b/packages/paperclip-runner/scripts/materialize-cursor-distribution.test.mjs index 89e9d9b8a1..7776268951 100644 --- a/packages/paperclip-runner/scripts/materialize-cursor-distribution.test.mjs +++ b/packages/paperclip-runner/scripts/materialize-cursor-distribution.test.mjs @@ -68,7 +68,7 @@ test("the isolation patch refuses unsupported, drifted, missing or repeated vend }); test("retained executable vendor proof matches every checked-in patch identity", async () => { - const proof = JSON.parse(await readFile(new URL("../test/fixtures/cursor-acp/usage-v3-runtime-patch-offline-proof.json", import.meta.url), "utf8")); + const proof = JSON.parse(await readFile(new URL("../test/fixtures/cursor-acp/usage-v4-runtime-patch-offline-proof.json", import.meta.url), "utf8")); assert.equal(proof.patchVersion, CURSOR_RUNTIME_PATCH_VERSION); assert.equal(proof.providerCalls, 0); assert.equal(proof.qualification, "offline-only"); diff --git a/packages/paperclip-runner/scripts/qualify-cursor-native-usage.mjs b/packages/paperclip-runner/scripts/qualify-cursor-native-usage.mjs index 782f68c9f6..2e21b760f2 100644 --- a/packages/paperclip-runner/scripts/qualify-cursor-native-usage.mjs +++ b/packages/paperclip-runner/scripts/qualify-cursor-native-usage.mjs @@ -33,6 +33,7 @@ export async function qualifyCursorNativeUsage(vendorRoot) { const original = await readFile(join(vendorRoot, platform, "dist-package", pin.file), "utf8"); assert.equal(hash(original), pin.before); const source = patchCursorRuntimeSource(original, platform); + assert.equal(hash(source), pin.after); assert.throws(() => patchCursorRuntimeSource(original + "\n", platform), /digest mismatch/); const exported = {}; runInNewContext(source, { exports: exported }); assert.ok(exported.modules["./src/acp/agent-session.ts"]); @@ -43,13 +44,18 @@ export async function qualifyCursorNativeUsage(vendorRoot) { const handlePrompt = runInNewContext(`({${segment(source, "handlePrompt(e){", "claimTaskToolCall(e,t){")}}).handlePrompt`, scope); const invocationSource = segment(source, "D=async e=>", ",yield D(y)"); const childUpdate = runInNewContext(`({${segment(source, "onInteractionUpdate(e,t){", "onSessionCompleted(e,t){")}}).onInteractionUpdate`); + // Execute the vendor's object construction and reuse transition, rather + // than assuming a hand-built child's fields match the pinned implementation. + const childLifecycle = runInNewContext(`(class {${segment(source, "beginOrUpdateRun(e,t){", "announce(e,t){")}}).prototype`); assert.ok(source.includes("yield this.processPrompt(e,s,l,paperclipUsage)")); assert.ok(source.includes("processPrompt(e,t,n,paperclipUsage){")); const checks = []; const createHost = backend => { const publisher = { turn: 0, children: new Map(), enabled: true, beginTurn() { return ++this.turn; }, runIdsForTurn() { return []; }, whenAllTerminal: async () => true, - enqueue(_child, fn) { return fn(); }, onInteractionUpdate: childUpdate }; + enqueue(_child, fn) { return fn(); }, turnFor() { return this.turn; }, + beginOrUpdateRun: childLifecycle.beginOrUpdateRun, resolveParentSession: childLifecycle.resolveParentSession, + startNewRun: childLifecycle.startNewRun, onInteractionUpdate: childUpdate }; const host = { subagentPublisher: publisher, subagentsEnabled: true, backgroundWorkRegistry: { abortWork() {} }, ctx: { withCancel() { const abort = new AbortController(); return [{ signal: abort.signal, get canceled() { return abort.signal.aborted; } }, () => abort.abort()]; } }, @@ -82,10 +88,17 @@ export async function qualifyCursorNativeUsage(vendorRoot) { const childHost = createHost(async () => {}); const childResult = await childHost.run({ execute: async ({ publisher, turn }) => { - const child = { turn, runs: 1, terminal: false, presenter: { presentInteractionUpdate() {} } }; - publisher.children.set("native-child", child); + const child = publisher.beginOrUpdateRun("native-child", { toolCallId: "native-tool", parentAgentId: "parent", name: "worker" }); + assert.equal(child.runs, 1); assert.equal(child.turn, turn); assert.equal(child.terminal, false); + assert.equal(publisher.children.get("native-child"), child); + child.presenter = { presentInteractionUpdate() {} }; publisher.onInteractionUpdate("native-child", event({ inputTokens: 7n })); - child.runs = 2; publisher.onInteractionUpdate("native-child", event({ inputTokens: 8n })); + child.terminal = true; + const reused = publisher.beginOrUpdateRun("native-child", { toolCallId: "native-tool-2", parentAgentId: "parent", name: "worker" }); + assert.equal(reused, child); assert.equal(reused.runs, 2); assert.equal(reused.terminal, false); + assert.equal(reused.sessionId, "native-child.2"); assert.equal(reused.turn, turn); + child.presenter = { presentInteractionUpdate() {} }; + publisher.onInteractionUpdate("native-child", event({ inputTokens: 8n })); publisher.onInteractionUpdate("unknown-history-child", event({ inputTokens: 999n })); child.terminal = true; publisher.onInteractionUpdate("native-child", event({ inputTokens: 999n })); } }); @@ -95,7 +108,7 @@ export async function qualifyCursorNativeUsage(vendorRoot) { childHost.publisher.children.get("native-child").terminal = false; childHost.publisher.onInteractionUpdate("native-child", event({ inputTokens: 999n })); assert.equal(childEnvelope.observations.length, 1); - checks.push("child_run_attribution", "history_and_late_child_isolation"); + checks.push("vendor_child_creation_and_reuse", "child_run_attribution", "history_and_late_child_isolation"); const started = deferred(), release = deferred(); let oldCallback; const overlapping = createHost(async (_context, _state, action, _model, callbacks) => { @@ -121,7 +134,7 @@ export async function qualifyCursorNativeUsage(vendorRoot) { platforms.push({ platform, vendorSha256: hash(original), candidateSha256: hash(source), checks }); } return { schema: "paperclip.cursor.native-usage-offline-proof.v1", providerCalls: 0, standardUsageEmitted: false, - boundary: "digest-pinned handlePrompt, native invocation and child update methods; surrounding session/transport doubled", platforms }; + boundary: "digest-pinned handlePrompt, native invocation, child creation/reuse and child update methods; surrounding session/transport doubled", platforms }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { assert.equal(process.argv.length, 3, "Usage: node qualify-cursor-native-usage.mjs VENDOR_RESEARCH_ROOT"); diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-installation.test.ts index 381e8c5f4c..9c5b37e4c7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-installation.test.ts @@ -22,7 +22,7 @@ it("matches build materializer pins and launches only package-owned complete dis it("rejects profile substitutions before reading any native distribution", async () => { const base = { ...QUALIFIED_ACPX_PROFILES.cursor, qualificationModel: "explicit", reportedModelId: "explicit" }; - for (const profile of [QUALIFIED_ACPX_PROFILES.cursor, { ...base, agentServerVersion: "latest" }, { ...base, commandDigest: "forged" }, { ...base, reportedModelId: "different" }, { ...base, agentProfileVersion: 7 as const }]) { + for (const profile of [QUALIFIED_ACPX_PROFILES.cursor, { ...base, agentServerVersion: "latest" }, { ...base, commandDigest: "forged" }, { ...base, reportedModelId: "different" }, { ...base, agentProfileVersion: 7 as const }, { ...base, agentProfileVersion: 8 as const }]) { await expect(verifyCursorInstallation(profile)).rejects.toThrow("exact pinned profile"); } }); diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-installation.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-installation.ts index 9783bcec93..0f3aa3d68a 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-installation.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-installation.ts @@ -8,9 +8,9 @@ import { resolveRunnerProviderAssetsRoot } from "./provider-assets-root.js"; export const CURSOR_PINNED_VERSION = "2026.09.26-dd393fe"; const CLOSURE_PINS = Object.freeze({ - "darwin-arm64": "0681abb675b46054155e0b3f28850bf4668cf24fb1aab457dad1f24b61b3ee6b", - "darwin-x64": "18caa80f90f70f6a7d815335062b0858327891ec854aebe90b585a42eac578e6", - "linux-x64": "0ee4ceb24062e991650bfb4c0b4ec5fbd7f1a01487b8c70aef0cf257509701a4", + "darwin-arm64": "257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30", + "darwin-x64": "6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2", + "linux-x64": "eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74", }); /** Trusted package assets only: no workspace, PATH, executable override or env root. */ diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-launch-policy.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-launch-policy.ts index a6c38d4cb9..68b9866b9f 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-launch-policy.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-launch-policy.ts @@ -19,7 +19,7 @@ export function cursorPrivateEnvironment(paths: { agentHomeDirectory: string; da /** * The vendor's disable-project-configs flag covers cli.json only. Keep this * admission refusal as defense in depth without mutating the user's project. - * The separately verified paperclip-cursor-usage-v3 distribution removes + * The separately verified paperclip-cursor-usage-v4 distribution removes * ambient MCP and hook discovery at its source for the entire session. This * check alone is not continuous filesystem protection or an OS sandbox. */ diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts index 4ddc6e8b2c..611f44604a 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -63,8 +63,8 @@ describe("qualified ACPX profiles", () => { }); }); -it("binds Cursor v8 to inspectable native instructions, closures and exact ACPX guard patch", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v8-identity.json", import.meta.url), "utf8")); +it("binds Cursor v9 to inspectable native instructions, closures and exact ACPX guard patch", () => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/cursor-acp/profile-v9-identity.json", import.meta.url), "utf8")); const distribution = JSON.parse(readFileSync(new URL("../../../cursor-distributions.json", import.meta.url), "utf8")); expect(identity.declaration.distribution).toEqual(distribution); expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1"); diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index 93162a4d90..1618c6bd0e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -61,10 +61,10 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< }, cursor: { driverKind: ACPX_DRIVER_KIND, protocolVersion: ACPX_DRIVER_PROTOCOL_VERSION, - acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 8, + acpxVersion: QUALIFIED_ACPX_VERSION, agent: "cursor", agentProfileVersion: 9, agentServerPackage: "cursor-agent", agentServerVersion: "2026.09.26-dd393fe", agentRuntimePackage: null, agentRuntimeVersion: null, - commandDigest: "sha256:b9e94cbcdce2783665612c85caf019c4defe37d3d245a694de590ebdf5b1f1a3", + commandDigest: "sha256:a76ad26878a3b3328154901563cbda857e53583e4e01787f35a797992ef76162", // Authenticated discovery has not established a qualification model. Never // turn this empty declaration into a default; callers must select an ID. qualificationModel: "", reportedModelId: "", permissionPolicy: "interactive", diff --git a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts index 6a95201d99..efe0d3da3f 100644 --- a/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/recovery-identity.test.ts @@ -28,9 +28,10 @@ afterEach(async () => { describe("ACPX recovery identity", () => { it.each([ ["cursor", "../../../test/fixtures/cursor-acp/profile-v7-identity.json"], + ["cursor", "../../../test/fixtures/cursor-acp/profile-v8-identity.json"], ["copilot", "../../../test/fixtures/copilot-profile-v7-identity.json"], ["pi", "../../../test-fixtures/pi-acp/profile-v9-identity.json"], - ] as const)("rejects retained %s sessions after the shared patch identity changes", async (agent, path) => { + ] as const)("rejects retained %s sessions after the execution identity changes", async (agent, path) => { const fixture = await recoveryFixture(); const historical = JSON.parse(await readFile(new URL(path, import.meta.url), "utf8")); const current = resolveQualifiedAcpxProfile(agent, agent === "pi" ? "openrouter/deepseek/deepseek-v4-flash-0731" : fixture.input.requestedModel); diff --git a/packages/paperclip-runner/test/fixtures/cursor-acp/native-usage-v4-offline-proof.json b/packages/paperclip-runner/test/fixtures/cursor-acp/native-usage-v4-offline-proof.json new file mode 100644 index 0000000000..8c2ebf9e72 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/cursor-acp/native-usage-v4-offline-proof.json @@ -0,0 +1,59 @@ +{ + "schema": "paperclip.cursor.native-usage-offline-proof.v1", + "providerCalls": 0, + "standardUsageEmitted": false, + "boundary": "digest-pinned handlePrompt, native invocation, child creation/reuse and child update methods; surrounding session/transport doubled", + "platforms": [ + { + "platform": "darwin-arm64", + "vendorSha256": "7784c8b16d4e639814c13b12be2a687f5dc2cd98cbf29ed0a10820778ab1bf62", + "candidateSha256": "35c5bf13b261ea884bb0e7b26ae7bd4c4999b0b4b08ab338bc22f2ccce2b8108", + "checks": [ + "exact_parent_invocation_capture", + "separate_followups", + "no_standard_usage", + "warm_prompt_isolation", + "vendor_child_creation_and_reuse", + "child_run_attribution", + "history_and_late_child_isolation", + "overlapping_cancellation", + "late_parent_callback_isolation", + "exception_cleanup" + ] + }, + { + "platform": "darwin-x64", + "vendorSha256": "3c0aaf6ecc4fceb0f95e1731deec75384984570d14d0871cecec11972b948ac4", + "candidateSha256": "5b2510248429febe1c9aead6ad43d35d89cdedc4dd057d4f07495efa33c7da55", + "checks": [ + "exact_parent_invocation_capture", + "separate_followups", + "no_standard_usage", + "warm_prompt_isolation", + "vendor_child_creation_and_reuse", + "child_run_attribution", + "history_and_late_child_isolation", + "overlapping_cancellation", + "late_parent_callback_isolation", + "exception_cleanup" + ] + }, + { + "platform": "linux-x64", + "vendorSha256": "2de420f1b31e70ca74b083a1ce5b519c2abffa5789d71cc06e84d2c38acb7c07", + "candidateSha256": "38fe96fa71463372bffdd9efa3e5608293ff075d079149fe20a1625783c4eb39", + "checks": [ + "exact_parent_invocation_capture", + "separate_followups", + "no_standard_usage", + "warm_prompt_isolation", + "vendor_child_creation_and_reuse", + "child_run_attribution", + "history_and_late_child_isolation", + "overlapping_cancellation", + "late_parent_callback_isolation", + "exception_cleanup" + ] + } + ] +} diff --git a/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v9-identity.json b/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v9-identity.json new file mode 100644 index 0000000000..1d968894f6 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/cursor-acp/profile-v9-identity.json @@ -0,0 +1,55 @@ +{ + "commandDigest": "sha256:a76ad26878a3b3328154901563cbda857e53583e4e01787f35a797992ef76162", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "cursor", + "agentProfileVersion": 9, + "acpxVersion": "0.13.1", + "agentServerVersion": "2026.09.26-dd393fe", + "patchVersion": "paperclip-cursor-usage-v4", + "distribution": { + "schema": "paperclip.cursor_distribution.v1", + "version": "2026.09.26-dd393fe", + "platforms": { + "darwin-arm64": { + "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/darwin/arm64/agent-cli-package.tar.gz", + "archiveSha256": "538827d96a779bab854a865c8e42859e8e87db34b5f69770261b90fc8cfff191", + "closureSha256": "257424bd48e35412091c6adfc61e4648e836757ec1d240d890bba81a24918c30", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "77394184a89b0e7384971181da19c3c82d83a399b04e7944b3f94fe3d7e62b25" + }, + "darwin-x64": { + "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/darwin/x64/agent-cli-package.tar.gz", + "archiveSha256": "ed1771c44cbf0f8059c67cac0d939e6a60eb730bd066d7dc4cbb14de314cfb6e", + "closureSha256": "6f28c799c5afdc64fbdff8a2157f565f17ae7615efe014ac389d63bf70cf2be2", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "7c8775160af74e5fb8d25a30e8412c1a6476b0661529078a686e5118fd1c5e5f" + }, + "linux-x64": { + "url": "https://downloads.cursor.com/lab/2026.09.26-dd393fe/linux/x64/agent-cli-package.tar.gz", + "archiveSha256": "8085fd120f5c71f4eae7fea26a043718e5644e3071e4fab3220a0e58c51f9593", + "closureSha256": "eadb8bb8ffb0450455b15b88c9b230307a9e149958a0c452d16dd157b4633d74", + "executable": "node", + "entrypoint": "index.js", + "vendorClosureSha256": "bf04ef8ea6a63191067a6b3e15139aa2c793d8419daab246aa3f0a012e1bbcd9" + } + }, + "patchVersion": "paperclip-cursor-usage-v4" + }, + "agentFilesBinding": "registered-runtime-context-v1", + "instructionBinding": "native-global-rules-v1", + "instructionAdmission": "synchronous-protocol-guard-v1", + "acpxPatchSha256": "bd5393058a218040d217fa85449d59a6f30507de54cd645bf0ef21422823f85e", + "sessionModeAdmission": "native-config-ack-recovery-bound-v1", + "sessionModes": [ + "agent", + "plan", + "ask" + ], + "defaultSessionMode": "agent", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "nativePlanToolIdentity": "request-item-id-bound-lifecycle-v1" + } +} diff --git a/packages/paperclip-runner/test/fixtures/cursor-acp/usage-v4-runtime-patch-offline-proof.json b/packages/paperclip-runner/test/fixtures/cursor-acp/usage-v4-runtime-patch-offline-proof.json new file mode 100644 index 0000000000..a6f5180429 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/cursor-acp/usage-v4-runtime-patch-offline-proof.json @@ -0,0 +1,80 @@ +{ + "schema": "paperclip.cursor.runtime-patch-proof.v1", + "patchVersion": "paperclip-cursor-usage-v4", + "providerCalls": 0, + "qualification": "offline-only", + "platforms": [ + { + "instructions": { + "boundedPayloadRejected": true, + "explicitEmptyInstructions": true, + "nativeGlobalRule": true, + "nativeResourceAdditionalRules": true, + "newSession": { + "acknowledgementMatches": true, + "exactComposedBytesAtRequestContext": true, + "nativeRulePrecedencePreserved": true + }, + "loadSession": { + "acknowledgementMatches": true, + "exactComposedBytesAtRequestContext": true, + "nativeRulePrecedencePreserved": true + }, + "mainSourceSha256": "f8bd1c549f844859f8aeb9f06c01420f299bee22fe136695fe891eaf18674b12", + "boundary": "native RequestContext constructor; dependencies doubled; no inference" + }, + "platform": "darwin-arm64", + "file": "5672.index.js", + "sourceSha256": "7784c8b16d4e639814c13b12be2a687f5dc2cd98cbf29ed0a10820778ab1bf62", + "patchedSha256": "35c5bf13b261ea884bb0e7b26ae7bd4c4999b0b4b08ab338bc22f2ccce2b8108", + "driftRejected": true, + "fullChunkCompiles": true, + "ambientMcpAccesses": 0, + "ownedMcpPreserved": true, + "hookConfigReads": 0, + "remoteTeamHookFetches": 0, + "typedActionErrors": true, + "assistantProseIgnored": true + }, + { + "instructions": { + "boundedPayloadRejected": true, + "explicitEmptyInstructions": true, + "nativeGlobalRule": true, + "nativeResourceAdditionalRules": true + }, + "platform": "darwin-x64", + "file": "9841.index.js", + "sourceSha256": "3c0aaf6ecc4fceb0f95e1731deec75384984570d14d0871cecec11972b948ac4", + "patchedSha256": "5b2510248429febe1c9aead6ad43d35d89cdedc4dd057d4f07495efa33c7da55", + "driftRejected": true, + "fullChunkCompiles": true, + "ambientMcpAccesses": 0, + "ownedMcpPreserved": true, + "hookConfigReads": 0, + "remoteTeamHookFetches": 0, + "typedActionErrors": true, + "assistantProseIgnored": true + }, + { + "instructions": { + "boundedPayloadRejected": true, + "explicitEmptyInstructions": true, + "nativeGlobalRule": true, + "nativeResourceAdditionalRules": true + }, + "platform": "linux-x64", + "file": "1699.index.js", + "sourceSha256": "2de420f1b31e70ca74b083a1ce5b519c2abffa5789d71cc06e84d2c38acb7c07", + "patchedSha256": "38fe96fa71463372bffdd9efa3e5608293ff075d079149fe20a1625783c4eb39", + "driftRejected": true, + "fullChunkCompiles": true, + "ambientMcpAccesses": 0, + "ownedMcpPreserved": true, + "hookConfigReads": 0, + "remoteTeamHookFetches": 0, + "typedActionErrors": true, + "assistantProseIgnored": true + } + ] +} diff --git a/server/src/services/native-runtime/native-cursor-plan-wait.test.ts b/server/src/services/native-runtime/native-cursor-plan-wait.test.ts index d6c6a30883..8745debf7d 100644 --- a/server/src/services/native-runtime/native-cursor-plan-wait.test.ts +++ b/server/src/services/native-runtime/native-cursor-plan-wait.test.ts @@ -94,7 +94,7 @@ describe("accepted Cursor plan passive-wait authority", () => { const facts = fixture(); expect(nativeCursorPlanWaitFromFacts(facts)).not.toBeNull(); const current = resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]"); - const next = { ...current, agentProfileVersion: 9, commandDigest: `sha256:${"b".repeat(64)}` } satisfies typeof current; + const next = { ...current, agentProfileVersion: 10, commandDigest: `sha256:${"b".repeat(64)}` } satisfies typeof current; expect(next.agentProfileVersion).toBeGreaterThan(current.agentProfileVersion); const resolver = vi.spyOn(runner, "resolveQualifiedAcpxProfile").mockReturnValue(next); try { expect(nativeCursorPlanWaitFromFacts(facts)).toBeNull(); }