From 883176f69c3c899775e24dd38a3b967d33f0ae44 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 8 Oct 2026 15:46:30 -0500 Subject: [PATCH] Finalize Pi credential isolation and current profile verification --- .dockerignore | 1 + .../runner-copilot-capabilities.md | 8 ++- doc/architecture/runner-pi-capabilities.md | 24 ++++++++- .../runner-rich-acp-capabilities.md | 26 +++++++++- .../2026-10-02-pi-production-readiness.md | 6 ++- packages/paperclip-runner/acpx-profiles.json | 4 +- .../runner-core/src/acpx_provider_backend.rs | 5 +- .../runner-core/src/acpx_sidecar_transport.rs | 3 ++ .../src/generated_acpx_profiles.rs | 2 +- .../tests/native_provider_backend.rs | 2 +- .../scripts/generate-acpx-profiles.mjs | 2 +- .../durable-prp-control-plane.test.ts | 10 +++- .../durable-prp-control-plane.ts | 4 +- .../src/drivers/acpx/environment.test.ts | 17 +++++++ .../src/drivers/acpx/environment.ts | 16 +++++- .../src/drivers/acpx/generated-profiles.ts | 4 +- .../src/drivers/acpx/pi-installation.test.ts | 40 +++++++++------ .../drivers/acpx/pi-provider-config.test.ts | 2 +- .../src/drivers/acpx/pi-provider-config.ts | 10 +++- .../pi-acp/profile-v22-identity.json | 50 +++++++++++++++++++ .../pi-acp/reserved-credential-names.json | 5 +- scripts/pi-public-install-probe.mjs | 6 +-- 22 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json diff --git a/.dockerignore b/.dockerignore index a81dc79e8b1..30d3d533bf0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -52,6 +52,7 @@ packages/paperclip-runner/test-fixtures/pi-acp/** !packages/paperclip-runner/test-fixtures/pi-acp/profile-v19-identity.json !packages/paperclip-runner/test-fixtures/pi-acp/profile-v20-identity.json !packages/paperclip-runner/test-fixtures/pi-acp/profile-v21-identity.json +!packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json !packages/paperclip-runner/docs/capability-contract.md # check:runner-workflow-traceability access()es every regression test the # stress-traceability spec names — those are src/**/*.test.ts files, so diff --git a/doc/architecture/runner-copilot-capabilities.md b/doc/architecture/runner-copilot-capabilities.md index d3dae77617b..ceddfb1b7d8 100644 --- a/doc/architecture/runner-copilot-capabilities.md +++ b/doc/architecture/runner-copilot-capabilities.md @@ -1,6 +1,12 @@ # Copilot 1.0.88 rich ACP capability audit -Current source candidate (2026-10-01): **Copilot profile v12 is unqualified**. +Current integration (2026-10-08): **Copilot profile v17 remains pending** with +digest `sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f`. +It binds master's shared protocol-validation sources without changing the +executable or supplying new paid qualification. Historical checkpoints below +retain their original identities and outcomes. + +Historical source candidate (2026-10-01): **Copilot profile v12 is unqualified**. Receipt v2 preserves the original `inputSha256` and separately captures `normalizedInputSha256` from the same invocation's validated outgoing body. For the native sidecar, only a successful, correlated `tool.resolve` commits diff --git a/doc/architecture/runner-pi-capabilities.md b/doc/architecture/runner-pi-capabilities.md index 1926a380193..7876c5cba45 100644 --- a/doc/architecture/runner-pi-capabilities.md +++ b/doc/architecture/runner-pi-capabilities.md @@ -1,6 +1,28 @@ # Pi rich ACP runtime -## Pi 1.0 candidate (2026-10-02, profile v12) +## Current integration — 2026-10-08 + +The experimental Runner admits **Pi profile 22** (`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`) and +**Cursor profile 15**. Copilot remains pending at **profile 17**. Legacy +`pi_local` is unchanged. Pi accepts any explicit caller-selected provider/model +ID and requires native acknowledgement; qualification models are examples, not +an allowlist or a fallback. + +The requested task execution and human-control paths have seven passing cloud +cases on profile 19 with the accepted Sonnet 4.6/low fixture. Profile 22 retains +that wrapper, helper and extension, incorporates master's environment changes, +patches brace-expansion to the official 5.0.12 payload, and excludes general AWS +IAM credentials from Pi's environment. Bedrock's provider-scoped bearer key is +supported. Those paid results retain their original profile-19 identity; +current integration is verified separately by source, package and CI checks. +Accounting and the wider platform/provider matrix remain deferred. See the +[readiness plan](../plans/2026-10-02-pi-production-readiness.md#current-delivery-scope--2026-10-08) +for the exact source/image evidence and merge status. + +All dated checkpoints below describe historical qualification attempts. Their +failures and profile numbers are retained; they are not current release gates. + +## Historical Pi 1.0 candidate (2026-10-02, profile v12) The candidate now pins **`@earendil-works/pi-coding-agent@1.0.0`** with `pi-acp@0.0.33`, ACPX `0.13.1`, and portable Node `24.21.0`. Pi v11 and older diff --git a/doc/architecture/runner-rich-acp-capabilities.md b/doc/architecture/runner-rich-acp-capabilities.md index 826478c83ed..4c905ceea05 100644 --- a/doc/architecture/runner-rich-acp-capabilities.md +++ b/doc/architecture/runner-rich-acp-capabilities.md @@ -1,6 +1,28 @@ # Rich ACP integration and qualification report -Current source checkpoint (2026-10-01): **Cursor v10, Copilot v12 and Pi v10 +## Current integration — 2026-10-08 + +The experimental Runner admits **Pi profile 22** (`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`) and +**Cursor profile 15**. Copilot remains pending at **profile 17**. Legacy +`pi_local` is unchanged. Pi accepts any explicit caller-selected provider/model +ID and requires native acknowledgement; qualification models are examples, not +an allowlist or a fallback. + +The requested task execution and human-control paths have seven passing cloud +cases on profile 19 with the accepted Sonnet 4.6/low fixture. Profile 22 retains +that wrapper, helper and extension, incorporates master's environment changes, +patches brace-expansion to the official 5.0.12 payload, and excludes general AWS +IAM credentials from Pi's environment. Bedrock's provider-scoped bearer key is +supported. Those paid results retain their original profile-19 identity; +current integration is verified separately by source, package and CI checks. +Accounting and the wider platform/provider matrix remain deferred. See the +[readiness plan](../plans/2026-10-02-pi-production-readiness.md#current-delivery-scope--2026-10-08) +for the exact source/image evidence and merge status. + +All dated checkpoints below describe historical qualification attempts. Their +failures and profile numbers are retained; they are not current release gates. + +Historical source checkpoint (2026-10-01): **Cursor v10, Copilot v12 and Pi v10 remain unqualified**. Copilot receipt v2 distinguishes original provider arguments from the validated outgoing completion input. The sidecar commits the captured normalized digest only after its exact pending call receives a @@ -766,7 +788,7 @@ vendor functions; paid and remote qualification remain required. Pi's tool polic shell commands and filesystem races require the host boundary. These are qualification gates, not claims that a JavaScript path check confines a shell. -## Explicit gaps and follow-ups +## Historical gaps and follow-ups — October 2, 2026 | Priority | Exposed but unused, partial, or unverified | Reason and next proof | | --- | --- | --- | diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index 93925ec8d4e..b7b6485ffb6 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -22,7 +22,9 @@ fallback is added. Pi profile 19 has command digest An initial master integration checkpoint used Pi profile 20 (`sha256:465ae72460f05cae961873f09cd7cd3652dc3a6949930b7ee16303925cd3dd0e`). It bound the Codex-only sandbox change. This checkpoint is retained as history and is superseded by profile 21 before merge. -The assembled merge uses Pi profile 21 (`sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1`). It restores master's explicit task-environment projection and patches the bundled `brace-expansion` dependency from 5.0.9 to the official 5.0.12 payload. Pi 1.0.0, pi-acp 0.0.33, Node 24.21.0, wrapper, helper, extension, native question/control delivery, model selection and recovery identity remain unchanged. The original three closure hashes were reproduced before calculating the patched ones. The dependency patch is hash-pinned, is included in installed setup tooling, and is checked against the complete locked package graph. The seven live results retain their original profile-19 source and are not relabeled as fresh profile-21 runs. Profile-21 integration and the narrow security correction require current-head tests, package materialization and CI before merge. Cursor keeps its existing qualified status. Copilot stays pending; its new profile-17 attestation only binds master's updated shared protocol validation sources. +The dependency correction checkpoint uses Pi profile 21 (`sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1`). It restores master's explicit task-environment projection and patches the bundled `brace-expansion` dependency from 5.0.9 to the official 5.0.12 payload. Pi 1.0.0, pi-acp 0.0.33, Node 24.21.0, wrapper, helper, extension, native question/control delivery, model selection and recovery identity remain unchanged. The original three closure hashes were reproduced before calculating the patched ones. The dependency patch is hash-pinned, is included in installed setup tooling, and is checked against the complete locked package graph. The seven live results retain their original profile-19 source and are not relabeled as fresh profile-21 runs. Profile-21 integration and the narrow security correction require current-head tests, package materialization and CI before merge. Cursor keeps its existing qualified status. Copilot stays pending; its new profile-17 attestation only binds master's updated shared protocol validation sources. + +The assembled merge uses **Pi profile 22** (`sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9`). Profile 21 remains a historical declaration. Version 22 changes only the version and credential-policy source hashes: general AWS IAM keys are excluded from both static and custom provider credentials, selected task-environment projections, and the Pi sidecar launch. `AWS_BEARER_TOKEN_BEDROCK` remains supported. The dependency closures, model selection, wrapper, helper, extension and question/control behavior are unchanged from profile 21. Rust admission uses the current declaration and rejects previous profiles. Current-head tests and CI must pass before merge; the profile-19 paid proof is not relabeled. | Core path | Canonical campaign | Retained evidence | | --- | --- | --- | @@ -113,7 +115,7 @@ harness `ee3b094d35719e4dd7cbc791d1924205c4fa474f` and immutable image Their canonical results and independent cleanup pass; their host stops normally. They do not substitute for the seven fresh profile-19 results above. -The earlier profile-20 merge checkpoint is retained above as history; current merge verification uses profile 21. +The earlier profile-20 merge checkpoint is retained above as history; current merge verification uses profile 22. | Core path | Canonical campaign | Retained evidence | | --- | --- | --- | diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 148395c548d..81a3e159050 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -14,12 +14,12 @@ "requiresProviderPolicy": false }, "pi": { - "agentProfileVersion": 21, + "agentProfileVersion": 22, "agentServerPackage": "pi-acp", "agentServerVersion": "0.0.33", "agentRuntimePackage": "@earendil-works/pi-coding-agent", "agentRuntimeVersion": "1.0.0", - "commandDigest": "sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1", + "commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9", "requiresProviderPolicy": true }, "cursor": { 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 314cca4c8f5..3757c6ea68d 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 @@ -3184,7 +3184,7 @@ mod tests { #[test] fn pi_profile_matches_published_identity_and_rejects_prior_profiles() { let published: Value = serde_json::from_str(include_str!( - "../../../../test-fixtures/pi-acp/profile-v20-identity.json" + "../../../../test-fixtures/pi-acp/profile-v22-identity.json" )) .unwrap(); let mut value = descriptor("codex"); @@ -3205,6 +3205,9 @@ mod tests { include_str!("../../../../test-fixtures/pi-acp/profile-v16-identity.json"), include_str!("../../../../test-fixtures/pi-acp/profile-v17-identity.json"), include_str!("../../../../test-fixtures/pi-acp/profile-v18-identity.json"), + include_str!("../../../../test-fixtures/pi-acp/profile-v19-identity.json"), + include_str!("../../../../test-fixtures/pi-acp/profile-v20-identity.json"), + include_str!("../../../../test-fixtures/pi-acp/profile-v21-identity.json"), ] { let prior: Value = serde_json::from_str(prior).unwrap(); value["commandDigest"] = prior["commandDigest"].clone(); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index c3e808fdd81..331f6ed17f3 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -178,6 +178,9 @@ fn pi_credential_environment_keys(binding: Option<&str>) -> Result, | "GCONV_PATH" | "LOCPATH" | "NLSPATH" + | "AWS_ACCESS_KEY_ID" + | "AWS_SECRET_ACCESS_KEY" + | "AWS_SESSION_TOKEN" ); if !valid_name || protected_name || !seen.insert(name) { return Err(invalid()); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs b/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs index ccb8fd3d88c..28f4d2a775a 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs @@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option { agent_runtime_package: Some("@earendil-works/pi-coding-agent"), agent_runtime_version: Some("1.0.0"), command_digest: - "sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1", + "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9", requires_provider_policy: true, }, "cursor" => AcpxReleaseProfile { 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 3732d81846b..cc94d4834c0 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:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1"; + "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9"; fn temporary_directory(label: &str) -> PathBuf { let nonce = SystemTime::now() diff --git a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs index 75df08f46bb..afdd2013b65 100644 --- a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs +++ b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs @@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract; assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`); assert.equal(contract.acpxPatchSha256, createHash("sha256") .update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex")); -for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v21-identity.json"], ["copilot", "test/fixtures/copilot-profile-v17-identity.json"]]) { +for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v17-identity.json"]]) { const identity = await readJson(path); assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion); assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest); diff --git a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts index 24e9fc98fc9..94f9b79505b 100644 --- a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts +++ b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts @@ -19,6 +19,7 @@ import { resolve } from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { configuredEnvironmentProjection } from "../configured-environment.js"; import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js"; import { createCapabilityRunnerdCodexTransport, createCapabilityRunnerdProviderEnvironment } from "../live/runnerd-codex-transport.js"; import { ACPX_CREDENTIAL_BINDING_ENV, createAcpxSidecarHostEnvironment } from "../drivers/acpx/environment.js"; @@ -550,7 +551,7 @@ it("preserves the controller-selected ACPX provider package root", () => { it.each([ ["pi", "OPENROUTER_API_KEY"], - ["pi", "AWS_ACCESS_KEY_ID"], + ["pi", "AWS_BEARER_TOKEN_BEDROCK"], ["pi", "CUSTOM_PI_API_KEY"], ["cursor", "CURSOR_API_KEY"], ["cursor", "CURSOR_AUTH_TOKEN"], @@ -559,6 +560,7 @@ it.each([ const launches: RunnerProcessLaunchSpec[] = []; vi.stubEnv(key, "ambient-must-not-cross"); vi.stubEnv(ACPX_CREDENTIAL_BINDING_ENV, "ambient-forged-marker"); + const iam = agent === "pi" ? configuredEnvironmentProjection({ AWS_ACCESS_KEY_ID: "general-key", AWS_SECRET_ACCESS_KEY: "general-secret", AWS_SESSION_TOKEN: "general-session", CUSTOM_SETTING: "allowed" }) : {}; const custom = key === "CUSTOM_PI_API_KEY" ? { PAPERCLIP_PI_PROVIDERS: JSON.stringify({ private: { baseUrl: "https://provider.invalid", apiKey: key } }) } : {}; try { for (const explicit of [true, false]) { @@ -566,7 +568,7 @@ it.each([ provider: "acpx", identity, codexHome: "/fixture/home", runtimeContextPath: "/fixture/context.json", hasRuntimeContext: false, options: { acpxAgent: agent, - environment: explicit ? { ...custom, [key]: "explicit-fixture-credential", [ACPX_CREDENTIAL_BINDING_ENV]: "caller-forged-marker", DATABASE_URL: "must-not-cross" } : undefined }, + environment: explicit ? { ...custom, ...iam, [key]: "explicit-fixture-credential", [ACPX_CREDENTIAL_BINDING_ENV]: "caller-forged-marker", DATABASE_URL: "must-not-cross" } : undefined }, }); const handle = spawnRunner({ connection: { mode: "connect", connectUrl: "ws://127.0.0.1:43127" }, @@ -588,6 +590,10 @@ it.each([ sessionId: identity.normalizedSessionId, names: explicit ? [...(key === "CUSTOM_PI_API_KEY" ? ["PAPERCLIP_PI_PROVIDERS"] : []), key] : [] }); expect(receipt).not.toContain("fixture-credential"); expect(launch.environment.DATABASE_URL).toBeUndefined(); + if (agent === "pi") { + for (const name of ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]) expect(launch.environment).not.toHaveProperty(name); + if (explicit) expect(launch.environment.CUSTOM_SETTING).toBe("allowed"); + } const provider = createAcpxSidecarHostEnvironment(launch.environment, agent, identity.normalizedSessionId); expect(provider[key]).toBe(explicit ? "explicit-fixture-credential" : undefined); expect(provider[ACPX_CREDENTIAL_BINDING_ENV]).toBeUndefined(); diff --git a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts index c4f5459d95c..4b444eaaa65 100644 --- a/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts +++ b/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts @@ -3463,14 +3463,16 @@ function runnerEnvironment( if (marker !== undefined && Buffer.byteLength(marker) <= 4_096) { try { binding = JSON.parse(marker); } catch { /* Sidecar admission rejects invalid markers. */ } } + let taskEnvironmentSource = explicitSource; if (binding !== null && typeof binding === "object" && !Array.isArray(binding) && (binding as Record).agent === "pi") { const bound = createAcpxSidecarHostEnvironment(explicitSource, "pi", normalizedSessionId); + taskEnvironmentSource = bound; for (const key of piCredentialNames(explicitSource)) { if (bound[key] !== undefined) environment[key] = bound[key]; } } - Object.assign(environment, githubCredentialEnvironment(explicitSource), configuredEnvironment(explicitSource)); + Object.assign(environment, githubCredentialEnvironment(explicitSource), configuredEnvironment(taskEnvironmentSource)); } return environment; } diff --git a/packages/paperclip-runner/src/drivers/acpx/environment.test.ts b/packages/paperclip-runner/src/drivers/acpx/environment.test.ts index d3feedea99c..3bc038a101e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/environment.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/environment.test.ts @@ -1,3 +1,4 @@ +import { configuredEnvironmentProjection, CONFIGURED_ENVIRONMENT_KEYS } from "../../configured-environment.js"; import { afterEach, describe, expect, it, vi } from "vitest"; afterEach(() => vi.unstubAllEnvs()); @@ -5,6 +6,22 @@ afterEach(() => vi.unstubAllEnvs()); import { ACPX_CREDENTIAL_BINDING_ENV, createAcpxCredentialBinding, createAcpxSidecarHostEnvironment, createSanitizedAcpxSpawnInput } from "./environment.js"; describe("ACPX launch environment", () => { + it("excludes general IAM from the Pi sidecar and launch, including selected task values", () => { + const iam = { AWS_ACCESS_KEY_ID: "general-key", AWS_SECRET_ACCESS_KEY: "general-secret", AWS_SESSION_TOKEN: "general-session" }; + const source = { ...iam, ...configuredEnvironmentProjection({ ...iam, CUSTOM_SETTING: "allowed" }), AWS_BEARER_TOKEN_BEDROCK: "provider-key" }; + const binding = createAcpxCredentialBinding(source, "pi", "session-1")!; + expect(JSON.parse(binding).names).toEqual(["AWS_BEARER_TOKEN_BEDROCK"]); + const sidecar = createAcpxSidecarHostEnvironment({ ...source, [ACPX_CREDENTIAL_BINDING_ENV]: binding }, "pi", "session-1"); + for (const env of [sidecar, createSanitizedAcpxSpawnInput(source, "pi").env, createSanitizedAcpxSpawnInput(sidecar, "pi").env]) { + for (const name of Object.keys(iam)) expect(env).not.toHaveProperty(name); + expect(env.AWS_BEARER_TOKEN_BEDROCK).toBe("provider-key"); + expect(env.CUSTOM_SETTING).toBe("allowed"); + expect(JSON.parse(env[CONFIGURED_ENVIRONMENT_KEYS]!)).toEqual(["CUSTOM_SETTING"]); + } + for (const name of Object.keys(iam)) { + expect(() => createAcpxSidecarHostEnvironment({ ...source, [ACPX_CREDENTIAL_BINDING_ENV]: JSON.stringify({ ...JSON.parse(binding), names: [name] }) }, "pi", "session-1")).toThrow("explicit matching session binding"); + } + }); it("keeps gateway and Bedrock settings confined to the selected harness", () => { const source = { ANTHROPIC_BASE_URL: "https://gateway.example", diff --git a/packages/paperclip-runner/src/drivers/acpx/environment.ts b/packages/paperclip-runner/src/drivers/acpx/environment.ts index 4845351d54f..b437e75c35b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/environment.ts +++ b/packages/paperclip-runner/src/drivers/acpx/environment.ts @@ -1,5 +1,5 @@ -import { configuredEnvironment } from "../../configured-environment.js"; -import { PI_CREDENTIAL_NAMES, piCredentialNames } from "./pi-provider-config.js"; +import { CONFIGURED_ENVIRONMENT_KEYS, configuredEnvironment, configuredEnvironmentKeys } from "../../configured-environment.js"; +import { PI_CREDENTIAL_NAMES, PI_GENERAL_IAM_CREDENTIAL_NAMES, piCredentialNames } from "./pi-provider-config.js"; import type { QualifiedAcpxAgent } from "./qualified-profiles.js"; export const ACPX_CREDENTIAL_BINDING_ENV = "PAPERCLIP_ACPX_CREDENTIAL_BINDING"; @@ -12,6 +12,16 @@ export const ACPX_CREDENTIAL_NAMES: Readonly !PI_GENERAL_IAM_CREDENTIAL_NAMES.includes(name))); + } +} + const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot"; /** Mint only at the controller's explicit task-environment boundary, never by copying a marker. */ @@ -63,6 +73,7 @@ export function createAcpxSidecarHostEnvironment( delete result[name]; } } + if (agent === "pi") removePiIamCredentials(result); return result; } @@ -135,6 +146,7 @@ export function createSanitizedAcpxSpawnInput( retainedBytes += entryBytes; result[key] = value; } + if (agent === "pi") removePiIamCredentials(result); return Object.freeze({ env: Object.freeze(result), }) as SanitizedAcpxSpawnInput; diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index d5caa4092d7..3a0bf38cf33 100644 --- a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts @@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = { "protocolVersion": 1, "acpxVersion": "0.13.1", "agent": "pi", - "agentProfileVersion": 21, + "agentProfileVersion": 22, "agentServerPackage": "pi-acp", "agentServerVersion": "0.0.33", "agentRuntimePackage": "@earendil-works/pi-coding-agent", "agentRuntimeVersion": "1.0.0", - "commandDigest": "sha256:514cbf86e70c1eaedebdf924bc0f3de4753c7a9e0313a0bb38614b6b9481a9a1", + "commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9", "permissionPolicy": "interactive" }, "cursor": { diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts index 0b17ae8babf..c04f4c3f71e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-installation.test.ts @@ -80,11 +80,19 @@ describe("Pi installation factory", () => { expect(current.commandDigest).not.toBe(prior.commandDigest); }); + it("preserves profile 21 and binds the restriction on general IAM credentials", async () => { + const read = async (version: number) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/profile-v${version}-identity.json`, import.meta.url), "utf8")); + const prior = await read(21), current = await read(22); + expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort()) + .toEqual(["agentProfileVersion", "credentialEnvironmentSourceSha256", "providerConfigurationSourceSha256"]); + expect(current.commandDigest).not.toBe(prior.commandDigest); + }); + it("binds the profile declaration to the reviewed patch and platform closure pins", async () => { const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex"); const canonical = (value: any): string => 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); - const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v21-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v22-identity.json", import.meta.url), "utf8")); const declaration = identity.declaration; expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1"); expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`); @@ -115,21 +123,21 @@ describe("Pi installation factory", () => { it("rejects legacy profiles and tampered executable identities", () => { expect(() => assertPiInstallationProfile(candidate())).not.toThrow(); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 21"); - expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 21"); - for (const priorVersion of [15, 16, 17, 18, 19, 20]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 21"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 22"); + expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 22"); + for (const priorVersion of [15, 16, 17, 18, 19, 20, 21]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 22"); for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) { expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration"); } diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.test.ts b/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.test.ts index 60c29152b95..7f4fa25b501 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.test.ts @@ -38,7 +38,7 @@ describe("Pi caller-selected providers and models", () => { finally { vi.unstubAllEnvs(); } }); - it.each(["!cat /private/key", "PAPERCLIP_API_KEY", "NODE_OPTIONS", "PATH"])("rejects unsafe custom-provider credential reference %s", apiKey => { + it.each(["!cat /private/key", "PAPERCLIP_API_KEY", "NODE_OPTIONS", "PATH", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"])("rejects unsafe custom-provider credential reference %s", apiKey => { expect(() => piProviderConfiguration({ PAPERCLIP_PI_PROVIDERS: JSON.stringify({ custom: { apiKey } }) })).toThrow(); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.ts b/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.ts index a8b5a56871b..eca68f7de3b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.ts +++ b/packages/paperclip-runner/src/drivers/acpx/pi-provider-config.ts @@ -13,12 +13,18 @@ export const PI_CREDENTIAL_NAMES = Object.freeze([ "XIAOMI_TOKEN_PLAN_CN_API_KEY", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "XIAOMI_TOKEN_PLAN_SGP_API_KEY", "ANT_LING_API_KEY", "QWEN_TOKEN_PLAN_API_KEY", "QWEN_TOKEN_PLAN_CN_API_KEY", "TYPESAFE_API_KEY", "RADIUS_API_KEY", "COPILOT_GITHUB_TOKEN", "AWS_BEARER_TOKEN_BEDROCK", - "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "PAPERCLIP_PI_PROVIDERS", ]); -// These names alter process startup rather than identify a provider credential. +// General IAM credentials authorize services beyond the model provider. Bedrock +// uses its provider-scoped bearer credential instead. +export const PI_GENERAL_IAM_CREDENTIAL_NAMES = Object.freeze([ + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", +]); + +// Process startup controls and general IAM credentials are never provider inputs. const RESERVED_PI_CREDENTIAL_NAMES = new Set([ + ...PI_GENERAL_IAM_CREDENTIAL_NAMES, "PATH", "HOME", "SHELL", diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json new file mode 100644 index 00000000000..b30a12edfdc --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json @@ -0,0 +1,50 @@ +{ + "commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "pi", + "agentProfileVersion": 22, + "acpxVersion": "0.13.1", + "agentServerVersion": "0.0.33", + "agentRuntimeVersion": "1.0.0", + "nodeVersion": "24.21.0", + "closure": { + "darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf", + "darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e", + "linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410" + }, + "wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36", + "helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d", + "extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4", + "agentFilesBinding": "registered-runtime-context-v1", + "nativeQuestions": "select-confirm-input-editor-v4-root-field-types", + "pathPolicy": "sdk-normalization-v1", + "mcpToolNames": "provider-safe-exact-source-v1", + "acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1", + "nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts", + "nativeNotices": "session-extension-v2-bounded-runtime-failure", + "messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd", + "noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "nativeInputDisposition": "queued-only-v1", + "cacheWarming": "disabled-owned-decision-v1", + "thinkingLevel": { + "supported": [ + "off", + "low", + "high", + "max" + ], + "newConfigurationDefault": "low", + "admission": "explicit-native-effective-before-prompt-v1", + "recovery": "mode-bound-v1" + }, + "modelSelection": "caller-selected-native-verified-v1", + "providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865", + "credentialEnvironmentSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc", + "runtimeSandboxSourceSha256": "1c1ddb0b24c3417250ce161cda897ba59b1adce7c7609ed553be298464e6c1c4", + "recoveryIdentitySourceSha256": "f333a24b19ef1b5b5d23ea9778e753f7051690e835ef9e4476a962d8a231c375", + "nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1", + "dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814" + } +} diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/reserved-credential-names.json b/packages/paperclip-runner/test-fixtures/pi-acp/reserved-credential-names.json index 5f2dd1c718f..8c917d959d8 100644 --- a/packages/paperclip-runner/test-fixtures/pi-acp/reserved-credential-names.json +++ b/packages/paperclip-runner/test-fixtures/pi-acp/reserved-credential-names.json @@ -38,5 +38,8 @@ "GLIBC_TUNABLES", "GCONV_PATH", "LOCPATH", - "NLSPATH" + "NLSPATH", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN" ] diff --git a/scripts/pi-public-install-probe.mjs b/scripts/pi-public-install-probe.mjs index c41d1543535..663754fcbc8 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, 19); -assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:b7647ebf97f802ca053ec3384c912bf0e8d18eba308d27397bb1d95a37220825'); +assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 22); +assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9'); assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'qualificationModel'), false); assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'reportedModelId'), false); const daemon = resolvePaperclipRunnerBinary(); @@ -64,7 +64,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: 19, 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: 22, 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 }); }