From 5cc87a37fe7e00b1a9362a5504d5d9e9394a565d Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 7 Oct 2026 07:23:59 -0500 Subject: [PATCH] Align Pi qualification fixtures and attachment contracts with master Co-Authored-By: Paperclip --- .../2026-10-02-pi-production-readiness.md | 24 +++++++++++++++++++ .../runner-core/src/acpx_provider_backend.rs | 2 +- .../runner-core/tests/acpx_provider_turns.rs | 6 ++++- .../tests/native_provider_backend.rs | 14 +++++++---- .../src/drivers/acpx/pi-provision.test.ts | 7 +++--- .../registered-asset-instructions.json | 2 +- scripts/pi-public-install-probe.mjs | 6 ++--- 7 files changed, 48 insertions(+), 13 deletions(-) diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index 595181a613d..75e0952718b 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -5,6 +5,30 @@ set of release gates. Pi remains a candidate until every required gate passes. Keep behavioral qualification on the accepted fixtures. Do not expand it to widgets, images, Cursor or Copilot qualification. The existing draft stack must be reviewed in dependency order. +## Final fixture reconciliation — 2026-10-07 + +Fresh Linux CI passed the server, database, UI and package checks. It exposed +the remaining stale Pi profile-15 fixtures in the native backend, provisioning +suite and public-install probe. Those checks now bind profile 16. Thinking +changes are rejected while the unchanged identity can still attach successfully. +The Rust and TypeScript instruction composers now share master's current-turn +`AGENT_HOME` guidance; the shared positive and negative suffix fixtures remain +the attachment authority. + +The corrected native backend passes 22 integration tests, the Rust core passes +350 unit tests, and the shared instruction/provisioning suites pass 13 tests. +Recursive typecheck and the full workspace build pass. Product E2E free checks +pass 2,479 Vitest tests and 128 native completion checks, and token gates pass. +The final complete Rust run and fresh Linux CI are still required. Local +PostgreSQL integration remains blocked by this host's shared-memory capacity; +the corresponding Linux CI lanes passed before this fixture-only update. + +The dedicated qualification key's fresh read records $0.430881296 lifetime usage +and $4.569118704 remaining under its $5 cap, with no reset or BYOK usage. The +signed-in Default Workspace has all 72 BYOK providers unconfigured. These +account observations made zero model calls. Current-source installed admission, +the real task, and all 26 Product plus seven Runner cases remain unqualified. + ## Master safety restoration — 2026-10-07 The earlier merge retained obsolete accounting code and removed master budget 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 e7c3f76dc04..2672163ade2 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 @@ -204,7 +204,7 @@ fn registered_asset_suffix(context: &Value) -> Option { return None; } blocks.push(if copy.get("kind").and_then(Value::as_str) == Some("agent_files") { - format!("Your persistent agent directory (AGENT_HOME) is {path}. Your instruction entry is {entry}, relative to that directory. All supported files and subfolders there are restored across tasks and sessions, and collected after this provider stops. Write task deliverables in the task working directory. Only changed or deleted files synchronize; the last sync wins for the same file. Temporary copies are cleaned up without retaining file history. Check the save receipt before claiming persistence.") + format!("Your persistent agent directory (AGENT_HOME) is {path}. This is the current turn's copy; its absolute path may change between turns. In shell commands, use the current $AGENT_HOME environment variable instead of an absolute agent-directory path from an earlier turn. Your instruction entry is {entry}, relative to that directory. All supported files and subfolders there are restored across tasks and sessions, and collected after this provider stops. Write task deliverables in the task working directory. Only changed or deleted files synchronize; the last sync wins for the same file. Temporary copies are cleaned up without retaining file history. Check the save receipt before claiming persistence.") } else { format!("Your editable agent instruction file is {path}/{entry}. Edit this registered private copy normally. After this run stops, Paperclip saves changed content as a persistent revision if your responsible user still has permission and the baseline has not changed. Check the run's instruction-save receipt before claiming persistence. Conflicts are preserved for explicit resolution. Repository instruction files, skills, and this run's loaded prompt are separate and are not collected.") }); diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_turns.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_turns.rs index 1639d49f008..973011b98f6 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_turns.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_turns.rs @@ -50,7 +50,11 @@ fn config(mode: &str) -> AcpxProviderSessionConfig { working_directory: std::env::temp_dir(), permission_mode: AcpxPermissionMode::ApproveReads, mode: None, - pi_thinking_level: None, + pi_thinking_level: if mode.starts_with("controls") { + Some(paperclip_runner_core::acpx_provider_session::PiThinkingLevel::Low) + } else { + None + }, permission_mode_pinned: true, provider_policy: if mode.starts_with("controls") { Some( diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs index 9ba6df179ab..adeaaee6a58 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs @@ -17,7 +17,7 @@ use sha2::{Digest, Sha256}; const CODEX_ACPX_DIGEST: &str = "sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3"; const PI_ACPX_DIGEST: &str = - "sha256:790f8b954be995ef63aef0ebdb0e06215e4c0d1416d40d605b33966a3d6ba053"; + "sha256:28eefeccb2556d2668e20aee2f12a90d1fef50b9be954d5f6dd97c757e2e945e"; fn temporary_directory(label: &str) -> PathBuf { let nonce = SystemTime::now() @@ -1397,11 +1397,17 @@ fn pi_thinking_change_cannot_attach_to_an_existing_provider_identity() { let error = executor .execute(&command(3 + index as u64, "run.attach", changed)) .unwrap_err(); - assert!(error - .to_string() - .contains("immutable_provider_identity_changed")); + assert!( + error + .to_string() + .contains("requires the same settled ACPX provider profile and session"), + "unexpected thinking identity rejection: {error}" + ); assert_eq!(fs::read(&state_path).unwrap(), prior); } + executor + .execute(&command(6, "run.attach", payload)) + .unwrap(); executor.shutdown().unwrap(); fs::remove_dir_all(directory).unwrap(); } diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-provision.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-provision.test.ts index c738e3e798d..b9dfcd6868d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-provision.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-provision.test.ts @@ -25,10 +25,11 @@ async function fixture(vendored = true) { await mkdir(cli, { recursive: true }); await writeFile(join(root, "package.json"), JSON.stringify({ name: vendored ? "@paperclipai/server" : "@paperclipai/paperclip-runner" })); const entry = join(cli, "provision-pi.cjs"); await writeFile(entry, "fixture"); - return { root, cli, entry, parent: join(root, "provider-assets/pi"), output: join(root, "provider-assets/pi", `${process.platform}-${process.arch}`) }; + const assetRoot = vendored ? join(root, "dist/vendor/paperclip-runner") : root; + return { root, cli, entry, assetRoot, parent: join(assetRoot, "provider-assets/pi"), output: join(assetRoot, "provider-assets/pi", `${process.platform}-${process.arch}`) }; } it("recognizes both published layouts without resolving a private npm package", async () => { - for (const vendored of [true, false]) { const f = await fixture(vendored); expect((await provisionPackageRoot(f.entry)).root).toBe(f.root); } + for (const vendored of [true, false]) { const f = await fixture(vendored); expect(await provisionPackageRoot(f.entry)).toMatchObject({ root: f.root, assetRoot: f.assetRoot }); } }); it("rejects wrong package, linked entrypoint and escaping asset roots before materialization", async () => { const f = await fixture(); const other = await fixture(); @@ -37,7 +38,7 @@ it("rejects wrong package, linked entrypoint and escaping asset roots before mat await writeFile(join(f.root, "package.json"), '{"name":"@paperclipai/server"}'); await rm(f.entry); await symlink(other.entry, f.entry); await expect(provisionPi(f.entry)).rejects.toThrow("linked"); - await rm(f.entry); await writeFile(f.entry, "fixture"); await symlink(other.root, join(f.root, "provider-assets")); + await rm(f.entry); await writeFile(f.entry, "fixture"); await symlink(other.root, join(f.assetRoot, "provider-assets")); await expect(provisionPi(f.entry)).rejects.toThrow("escapes"); expect(mocks.build).not.toHaveBeenCalled(); }); diff --git a/packages/paperclip-runner/test/fixtures/registered-asset-instructions.json b/packages/paperclip-runner/test/fixtures/registered-asset-instructions.json index d978802f879..f51a893006f 100644 --- a/packages/paperclip-runner/test/fixtures/registered-asset-instructions.json +++ b/packages/paperclip-runner/test/fixtures/registered-asset-instructions.json @@ -19,7 +19,7 @@ } }, "entry": "Custom entry quoting /registered/run-copy stays unchanged.", - "instructions": "Pinned prompt.\n\nCustom entry quoting /registered/run-copy stays unchanged.\n\nPinned connection policy.\n\nYour persistent agent directory (AGENT_HOME) is /registered/run-copy. Your instruction entry is AGENTS.md, relative to that directory. All supported files and subfolders there are restored across tasks and sessions, and collected after this provider stops. Write task deliverables in the task working directory. Only changed or deleted files synchronize; the last sync wins for the same file. Temporary copies are cleaned up without retaining file history. Check the save receipt before claiming persistence.\n\nRead-only instruction sibling root: /registered/bundle" + "instructions": "Pinned prompt.\n\nCustom entry quoting /registered/run-copy stays unchanged.\n\nPinned connection policy.\n\nYour persistent agent directory (AGENT_HOME) is /registered/run-copy. This is the current turn's copy; its absolute path may change between turns. In shell commands, use the current $AGENT_HOME environment variable instead of an absolute agent-directory path from an earlier turn. Your instruction entry is AGENTS.md, relative to that directory. All supported files and subfolders there are restored across tasks and sessions, and collected after this provider stops. Write task deliverables in the task working directory. Only changed or deleted files synchronize; the last sync wins for the same file. Temporary copies are cleaned up without retaining file history. Check the save receipt before claiming persistence.\n\nRead-only instruction sibling root: /registered/bundle" }, { "context": { diff --git a/scripts/pi-public-install-probe.mjs b/scripts/pi-public-install-probe.mjs index 78d52e0491f..890e56dcbac 100644 --- a/scripts/pi-public-install-probe.mjs +++ b/scripts/pi-public-install-probe.mjs @@ -20,8 +20,8 @@ assert.equal(JSON.parse(await readFile(join(server, 'package.json'), 'utf8')).na const { resolvePaperclipRunnerBinary } = await import(pathToFileURL(join(server, 'dist/services/native-runtime/native-codex-runner.js'))); const { createCapabilityRunnerdCodexTransport } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/live/runnerd-codex-transport.js'))); const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js'))); -assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 15); -assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:790f8b954be995ef63aef0ebdb0e06215e4c0d1416d40d605b33966a3d6ba053'); +assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 16); +assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:28eefeccb2556d2668e20aee2f12a90d1fef50b9be954d5f6dd97c757e2e945e'); assert.equal(QUALIFIED_ACPX_PROFILES.pi.qualificationModel, 'openrouter/deepseek/deepseek-v4-flash-0731'); const daemon = resolvePaperclipRunnerBinary(); assert.equal(await realpath(daemon), join(server, 'dist/vendor/paperclip-runner/bin/paperclip-runnerd')); @@ -63,7 +63,7 @@ try { assert.equal(state.activeTurnId, null); assert.equal(state.identity, null); assert.equal(state.providerExitUnconfirmed, false); - console.log(JSON.stringify({ schema: 'paperclip.pi.public-npm-install.v1', target: `${process.platform}-${process.arch}`, normalPackagedDaemon: true, exactPiProfile: 15, runtime: '1.0.0', credentials: 'none', promptCalls: 0, settledMs, cleanRunnerExit: true })); + console.log(JSON.stringify({ schema: 'paperclip.pi.public-npm-install.v1', target: `${process.platform}-${process.arch}`, normalPackagedDaemon: true, exactPiProfile: 16, runtime: '1.0.0', credentials: 'none', promptCalls: 0, settledMs, cleanRunnerExit: true })); } finally { try { await bundle?.transport.close(); } finally { clearTimeout(watchdog); await rm(root, { recursive: true, force: true }); }