mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
Carry final Pi credential and profile verification through the stack
This commit is contained in:
commit
306c2de351
22 files changed
+207
-45
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1060,9 +1082,8 @@ location and repeat setup. Concurrent setup is rejected. Cancellation drains the
|
||||
current bounded download/build command before removing its private staging tree;
|
||||
allow that cleanup to complete before trying again.
|
||||
|
||||
Then select Pi with the exact model
|
||||
`openrouter/deepseek/deepseek-v4-flash-0731` and bind an OpenRouter credential
|
||||
through the normal provider credential UI. Setup itself makes no model request.
|
||||
Then select Pi with an explicit provider/model ID and bind its provider credential
|
||||
through the normal agent environment configuration. Setup itself makes no model request.
|
||||
A missing host closure produces explicit setup guidance. Daytona uses the
|
||||
separately built and verified Linux provider pack in its runner image; running
|
||||
local setup does not install or qualify a remote image. Published-tar local and
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
# Rich ACP integration and qualification report
|
||||
|
||||
## Current Pi 1.0 qualification checkpoint — October 2, 2026
|
||||
## 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 qualification checkpoint — October 2, 2026
|
||||
|
||||
Pi now uses `@earendil-works/pi-coding-agent@1.0.0`, `pi-acp@0.0.33`,
|
||||
ACPX `0.13.1`, and profile **12**. Its exact model remains
|
||||
@@ -827,7 +849,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 |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -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 |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -178,6 +178,9 @@ fn pi_credential_environment_keys(binding: Option<&str>) -> Result<Vec<String>,
|
||||
| "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());
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
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 {
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<string, unknown>).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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Record<QualifiedAcpxAgent, readonly
|
||||
codex: ["OPENAI_API_KEY", "CODEX_API_KEY", "PAPERCLIP_AI_PROVIDER_KEY"],
|
||||
};
|
||||
export const CLAUDE_ROUTING_ENV_KEYS = ["ANTHROPIC_BASE_URL", "CLAUDE_CODE_USE_BEDROCK", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_EC2_METADATA_DISABLED", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "ANTHROPIC_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL"] as const;
|
||||
|
||||
/** Keep the task projection consistent when excluding general IAM from Pi. */
|
||||
function removePiIamCredentials(environment: NodeJS.ProcessEnv): void {
|
||||
const names = configuredEnvironmentKeys(environment);
|
||||
for (const name of PI_GENERAL_IAM_CREDENTIAL_NAMES) delete environment[name];
|
||||
if (environment[CONFIGURED_ENVIRONMENT_KEYS] !== undefined) {
|
||||
environment[CONFIGURED_ENVIRONMENT_KEYS] = JSON.stringify(names.filter(name => !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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string>([
|
||||
...PI_GENERAL_IAM_CREDENTIAL_NAMES,
|
||||
"PATH",
|
||||
"HOME",
|
||||
"SHELL",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -38,5 +38,8 @@
|
||||
"GLIBC_TUNABLES",
|
||||
"GCONV_PATH",
|
||||
"LOCPATH",
|
||||
"NLSPATH"
|
||||
"NLSPATH",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN"
|
||||
]
|
||||
@@ -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 }); }
|
||||
|
||||
Reference in new issue
Block a user