From b866fa4bdc940469eb668d3ea7938e7039b442ec Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:58:12 -0500 Subject: [PATCH] feat(runner): add pinned Kimi Code native runtime (#18028) Add the pinned Kimi Code runtime, native instruction and model admission, scoped credentials, and pending-candidate recovery contracts. Retain current master release attestations and provider-pack compatibility. Co-Authored-By: Paperclip --- .dockerignore | 8 + .../src/paperclip-runner-permissions.test.ts | 4 + .../src/paperclip-runner-permissions.ts | 1 + packages/paperclip-runner/README.md | 28 + packages/paperclip-runner/acpx-profiles.json | 10 + .../paperclip-runner/docs/durable-recovery.md | 7 +- packages/paperclip-runner/package.json | 2 +- .../src/acpx_candidate_admission.rs | 227 ++++++- .../runner-core/src/acpx_provider_backend.rs | 640 +++++++++++++++++- .../src/acpx_provider_capabilities.rs | 2 +- .../runner-core/src/acpx_provider_state.rs | 68 +- .../runner-core/src/acpx_sidecar_transport.rs | 3 +- .../runner-core/src/configured_environment.rs | 10 + .../crates/runner-core/src/durable/runner.rs | 167 ++++- .../src/generated_acpx_profiles.rs | 22 + .../src/native_provider_backend.rs | 7 +- .../runner-core/tests/acpx_provider_state.rs | 143 ++-- .../scripts/acpx-release-attestations.mjs | 4 + .../acpx-release-attestations.test.mjs | 12 +- .../build-verified-provider-entrypoints.mjs | 30 + ...ild-verified-provider-entrypoints.test.mjs | 5 + .../scripts/candidate-provider-pack.mjs | 7 +- .../scripts/candidate-provider-pack.test.mjs | 21 + .../scripts/generate-acpx-profiles.mjs | 8 + .../kimi-distribution/package-lock.json | 606 +++++++++++++++++ .../scripts/kimi-distribution/package.json | 11 + .../scripts/kimi-instructions-runtime.mjs | 34 + .../scripts/kimi-metadata-probe.test.mjs | 112 +++ .../scripts/kimi-provider-pack-smoke.mjs | 119 ++++ .../scripts/materialize-kimi-distribution.mjs | 182 +++++ .../materialize-kimi-distribution.test.mjs | 121 ++++ .../scripts/patch-kimi-runtime.mjs | 160 +++++ .../scripts/patch-kimi-runtime.test.mjs | 576 ++++++++++++++++ .../scripts/provision-kimi-package.test.mjs | 111 +++ .../scripts/provision-kimi.mjs | 120 ++++ .../src/cli/acpx-runtime-sidecar.test.ts | 80 ++- .../src/cli/acpx-runtime-sidecar.ts | 14 +- .../src/cli/kimi-metadata-probe.ts | 30 + .../src/configured-environment.test.ts | 19 +- .../src/configured-environment.ts | 2 +- .../src/contracts/native-execution.ts | 12 +- .../src/drivers/acpx/acp-edit-context.ts | 46 ++ .../acpx/acp-permission-adapter.test.ts | 38 +- .../drivers/acpx/acp-permission-adapter.ts | 6 +- .../drivers/acpx/candidate-admission.test.ts | 57 ++ .../src/drivers/acpx/candidate-admission.ts | 24 +- .../src/drivers/acpx/capability-profiles.ts | 6 + .../drivers/acpx/codex-acpx-driver.test.ts | 39 ++ .../src/drivers/acpx/codex-acpx-driver.ts | 25 +- .../src/drivers/acpx/codex-runtime-adapter.ts | 18 +- .../acpx/copilot-permission-context.ts | 52 +- .../src/drivers/acpx/driver-profile.ts | 4 +- .../src/drivers/acpx/environment.test.ts | 34 +- .../src/drivers/acpx/environment.ts | 15 +- .../src/drivers/acpx/generated-profiles.ts | 14 + .../src/drivers/acpx/kimi-closure-pins.ts | 12 + .../src/drivers/acpx/kimi-config.ts | 44 ++ .../drivers/acpx/kimi-installation.test.ts | 110 +++ .../src/drivers/acpx/kimi-installation.ts | 71 ++ .../drivers/acpx/kimi-instructions.test.ts | 70 ++ .../src/drivers/acpx/kimi-instructions.ts | 71 ++ .../acpx/kimi-message-boundary.test.ts | 70 ++ .../src/drivers/acpx/kimi-message-boundary.ts | 47 ++ .../drivers/acpx/kimi-metadata-probe.test.ts | 141 ++++ .../src/drivers/acpx/kimi-metadata-probe.ts | 94 +++ .../src/drivers/acpx/kimi-models.test.ts | 143 ++++ .../src/drivers/acpx/kimi-models.ts | 140 ++++ .../src/drivers/acpx/kimi-runtime-files.ts | 60 ++ .../acpx/profile-compatibility.test.ts | 4 +- .../src/drivers/acpx/profile-installation.ts | 10 + .../src/drivers/acpx/provider-assets-root.ts | 4 +- .../src/drivers/acpx/runtime-host.ts | 8 +- .../src/drivers/acpx/runtime-sandbox.ts | 6 + packages/paperclip-runner/src/index.ts | 3 +- packages/paperclip-runner/src/live/index.ts | 4 +- .../src/live/kimi-candidate-startup.test.ts | 37 + .../src/live/runnerd-codex-transport.ts | 31 +- .../src/providers/kimi/README.md | 285 ++++++++ .../profile-v35-kimi-historical-identity.json | 52 ++ .../pi-acp/profile-v36-identity.json | 52 ++ ...-profile-v82-kimi-historical-identity.json | 89 +++ ...-profile-v83-kimi-historical-identity.json | 89 +++ ...v88-kimi-foundation-81fbfb44-identity.json | 89 +++ .../copilot-profile-v89-identity.json | 89 +++ .../copilot-profile-v90-identity.json | 89 +++ .../copilot-profile-v91-identity.json | 90 +++ ...-profile-v13-kimi-historical-identity.json | 35 + ...-profile-v14-kimi-historical-identity.json | 35 + ...v15-kimi-foundation-81fbfb44-identity.json | 35 + .../fixtures/grok-profile-v16-identity.json | 35 + .../fixtures/grok-profile-v17-identity.json | 35 + .../fixtures/grok-profile-v18-identity.json | 36 + .../fixtures/kimi-profile-v1-identity.json | 28 + .../fixtures/kimi-profile-v10-identity.json | 48 ++ .../fixtures/kimi-profile-v11-identity.json | 48 ++ .../fixtures/kimi-profile-v13-identity.json | 72 ++ .../fixtures/kimi-profile-v14-identity.json | 72 ++ .../fixtures/kimi-profile-v15-identity.json | 72 ++ .../fixtures/kimi-profile-v16-identity.json | 75 ++ .../fixtures/kimi-profile-v2-identity.json | 28 + .../fixtures/kimi-profile-v3-identity.json | 35 + .../fixtures/kimi-profile-v4-identity.json | 35 + .../fixtures/kimi-profile-v5-identity.json | 35 + .../fixtures/kimi-profile-v6-identity.json | 35 + .../fixtures/kimi-profile-v7-identity.json | 48 ++ .../fixtures/kimi-profile-v8-identity.json | 48 ++ .../fixtures/kimi-profile-v9-identity.json | 48 ++ .../fixtures/profile-history-lineage.json | 147 ++++ .../test/kimi-installed-runtime.test.mjs | 109 +++ packages/shared/src/agent-runner.ts | 2 +- tests/runner-e2e/pi-controls-evidence.test.ts | 6 +- 111 files changed, 7180 insertions(+), 249 deletions(-) create mode 100644 packages/paperclip-runner/scripts/kimi-distribution/package-lock.json create mode 100644 packages/paperclip-runner/scripts/kimi-distribution/package.json create mode 100644 packages/paperclip-runner/scripts/kimi-instructions-runtime.mjs create mode 100644 packages/paperclip-runner/scripts/kimi-metadata-probe.test.mjs create mode 100644 packages/paperclip-runner/scripts/kimi-provider-pack-smoke.mjs create mode 100644 packages/paperclip-runner/scripts/materialize-kimi-distribution.mjs create mode 100644 packages/paperclip-runner/scripts/materialize-kimi-distribution.test.mjs create mode 100644 packages/paperclip-runner/scripts/patch-kimi-runtime.mjs create mode 100644 packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs create mode 100644 packages/paperclip-runner/scripts/provision-kimi-package.test.mjs create mode 100644 packages/paperclip-runner/scripts/provision-kimi.mjs create mode 100644 packages/paperclip-runner/src/cli/kimi-metadata-probe.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/acp-edit-context.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-config.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-installation.test.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-installation.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-instructions.test.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-instructions.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.test.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.test.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-models.test.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-models.ts create mode 100644 packages/paperclip-runner/src/drivers/acpx/kimi-runtime-files.ts create mode 100644 packages/paperclip-runner/src/live/kimi-candidate-startup.test.ts create mode 100644 packages/paperclip-runner/src/providers/kimi/README.md create mode 100644 packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-kimi-historical-identity.json create mode 100644 packages/paperclip-runner/test-fixtures/pi-acp/profile-v36-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v82-kimi-historical-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v83-kimi-historical-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v88-kimi-foundation-81fbfb44-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v89-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v90-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v91-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/grok-profile-v13-kimi-historical-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/grok-profile-v14-kimi-historical-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/grok-profile-v15-kimi-foundation-81fbfb44-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/grok-profile-v16-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/grok-profile-v17-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/grok-profile-v18-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v1-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v10-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v11-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v13-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v14-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v15-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v16-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v2-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v3-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v4-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v5-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v6-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v7-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v8-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/kimi-profile-v9-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/profile-history-lineage.json create mode 100644 packages/paperclip-runner/test/kimi-installed-runtime.test.mjs diff --git a/.dockerignore b/.dockerignore index e630fbbe116..f0f5a3dda03 100644 --- a/.dockerignore +++ b/.dockerignore @@ -48,3 +48,11 @@ packages/grok-acp/bin # Images build their pinned Copilot assets instead of copying host binaries. /packages/paperclip-runner/provider-assets/ /server/provider-assets/ + +!packages/paperclip-runner/test/fixtures/kimi-profile-v*-identity.json + +!packages/paperclip-runner/test/fixtures/grok-profile-v*-identity.json + +!packages/paperclip-runner/test/fixtures/copilot-profile-v*-identity.json + +!packages/paperclip-runner/test-fixtures/pi-acp/profile-v34-identity.json diff --git a/packages/adapter-utils/src/paperclip-runner-permissions.test.ts b/packages/adapter-utils/src/paperclip-runner-permissions.test.ts index 19900ca47b8..5ae8e4fc1a2 100644 --- a/packages/adapter-utils/src/paperclip-runner-permissions.test.ts +++ b/packages/adapter-utils/src/paperclip-runner-permissions.test.ts @@ -12,6 +12,10 @@ import { } from "./paperclip-runner-permissions.js"; describe("Paperclip Runner permission defaults", () => { + it("keeps Kimi Code pending with an explicit runtime credential", () => { + expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "kimi")) + .toMatchObject({ qualified: false, credentialEnvironment: ["KIMI_API_KEY"] }); + }); it("marks Pi and Copilot qualified with their provider credential bindings", () => { expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "pi")) .toMatchObject({ qualified: true, credentialEnvironment: expect.arrayContaining(["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"]) }); diff --git a/packages/adapter-utils/src/paperclip-runner-permissions.ts b/packages/adapter-utils/src/paperclip-runner-permissions.ts index bed9078b502..c7e6604e507 100644 --- a/packages/adapter-utils/src/paperclip-runner-permissions.ts +++ b/packages/adapter-utils/src/paperclip-runner-permissions.ts @@ -253,6 +253,7 @@ export const PAPERCLIP_RUNNER_ACPX_PROFILES = Object.freeze([ { value: "cursor", label: "Cursor", qualified: true, credentialEnvironment: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] }, { value: "copilot", label: "GitHub Copilot", qualified: true, credentialEnvironment: ["COPILOT_GITHUB_TOKEN"] }, { value: "pi", label: "Pi", qualified: true, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] }, + { value: "kimi", label: "Kimi Code", qualified: false, credentialEnvironment: ["KIMI_API_KEY"] }, { value: "antigravity", label: "Google Antigravity", qualified: false, credentialEnvironment: ["GEMINI_API_KEY"] }, ] as const); diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md index 84f96da59df..36851cf216c 100644 --- a/packages/paperclip-runner/README.md +++ b/packages/paperclip-runner/README.md @@ -981,6 +981,34 @@ while daemon work might still be running. The socket and its parent must be priv canonical paths owned by the same account. Local stdio assignment requires Unix; other platforms retain the HTTP path. +## Kimi Code candidate + +Kimi Code uses the pinned `@moonshot-ai/kimi-code@2.3.3` distribution through +ACPX. Candidate support is pending local and Daytona Product E2E qualification. +The installation targets are macOS ARM64 and Linux x64. Platform startup, +authenticated driver checks, and product acceptance are separate evidence levels. + +Run `paperclipai runtime setup kimi` on the execution host to install its complete +verified runtime. Setup downloads public pinned dependencies without model calls +or provider credentials. An invalid existing installation fails without ambient +Kimi fallback. Bind a company secret as `KIMI_API_KEY` and select an exact model. +This release uses the global Code endpoint at `https://api.kimi.ai/coding/v1`. +Model suggestions are empty; a compatible typed model is decided by the provider. +The account needs Kimi Code entitlement. Runtime Test verifies installation and +queries the authenticated Code model catalog for the exact selected model. It +sends no model prompt; an actual task verifies inference and tool execution. + +The ordinary hiring path stays hidden while qualification is pending. Existing +legacy Kimi agents retain their runtime and history. Managed Kimi connections, +ambient login borrowing, other regions/platforms, steering, native child work, +and per-run token/dollar accounting are outside this release. Read-only Kimi work +uses scoped Paperclip tools; native filesystem and shell tools are denied. +The provider-owned patch binds native instructions, preserves permission arguments, +and removes provider auth from tool subprocesses. See the +[protocol dossier](src/providers/kimi/README.md) and +[release ledger](../../doc/plans/2026-10-28-kimi-code-runner-integration.md) for +current evidence and limits. + ## ACPX controller replacement regression tests `src/control-plane/acpx-controller-recovery.test.ts` runs the production Rust diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 46350e50676..9fc8de3b253 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -67,6 +67,16 @@ "agentRuntimeVersion": "0.160.0", "commandDigest": "sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc", "requiresProviderPolicy": false + }, + "kimi": { + "agentProfileVersion": 16, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "commandDigest": "sha256:0c1e6ce460407ec973a37a2d192d665fcde744e2b9875c791c90308afbf4c52b", + "requiresProviderPolicy": true, + "qualificationStatus": "pending" } } } diff --git a/packages/paperclip-runner/docs/durable-recovery.md b/packages/paperclip-runner/docs/durable-recovery.md index f04aab08a23..c38656f6b65 100644 --- a/packages/paperclip-runner/docs/durable-recovery.md +++ b/packages/paperclip-runner/docs/durable-recovery.md @@ -80,7 +80,7 @@ Each provider declares how in `ACPX_RELEASE_ATTESTATIONS` | --- | --- | --- | | `installed-command` | Claude, Codex | the upstream package's launch command, hashed at installation | | `launcher` | Grok | the SHA-256 of the launcher shipped verbatim | -| `closure` | Copilot, Pi | upstream versions and the complete platform closure pins | +| `closure` | Copilot, Pi, Kimi | upstream versions and the complete platform closure pins | | `declaration` | Cursor, Antigravity | a checked-in release declaration, as canonical JSON | `generate:acpx-profiles` writes the computed digests and `check:acpx-profiles` @@ -127,6 +127,11 @@ An administrator-owned read-only Runner can therefore use a user-writable cache, but this provides weaker protection against that same user than hashing every executable on each admission. Removing the cache restores full hashing. +Kimi keeps revision 16 from its pending candidate history. Its release +attestation replaces the earlier source-bound digest without advancing that +revision. Historical candidate records stay readable, while exact current +digest admission refuses their old launch identities. Kimi remains pending. + ## External controller checkpoints The optional controller persistence callback stores recovery control state, not diff --git a/packages/paperclip-runner/package.json b/packages/paperclip-runner/package.json index ce53005026a..ba573e65c6c 100644 --- a/packages/paperclip-runner/package.json +++ b/packages/paperclip-runner/package.json @@ -85,7 +85,7 @@ "typecheck:browser": "tsc -p tsconfig.browser.json --noEmit", "test": "pnpm run test:typescript && pnpm run test:rust", "test:typescript": "pnpm run test:typescript:prep && vitest run", - "test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node ../../scripts/ci-profile/node-test.mjs test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/materialize-pi-distribution.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/acpx-release-attestations.test.mjs scripts/provider-pack-runtime-tree.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-runtime-dependency-roots.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-inputs.test.mjs scripts/contracts/contract-fingerprints.test.mjs scripts/provider-pack-executable-shims.test.mjs scripts/controlled-evals/controller.test.mjs scripts/controlled-evals/portable.test.mjs && node --test scripts/build-copilot-distribution.test.mjs scripts/copilot-inner-distribution.test.mjs scripts/copilot-smoke-resources.test.mjs scripts/discover-copilot-acp.test.mjs scripts/materialize-copilot-binary.test.mjs scripts/install-copilot-assets.test.mjs scripts/probe-copilot-acp.test.mjs scripts/qualify-copilot-acp.test.mjs test/acpx-message-boundaries-package-contract.test.mjs", + "test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node ../../scripts/ci-profile/node-test.mjs test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-model-selection-package-contract.test.mjs test/acpx-message-boundaries-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/materialize-pi-distribution.test.mjs scripts/cursor-provisioner-layout.test.mjs scripts/release-provider-pack.test.mjs scripts/acpx-release-attestations.test.mjs scripts/provider-pack-runtime-tree.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs scripts/materialize-kimi-distribution.test.mjs scripts/provision-kimi-package.test.mjs scripts/kimi-metadata-probe.test.mjs scripts/patch-kimi-runtime.test.mjs test/kimi-installed-runtime.test.mjs scripts/installed-package-pack.test.mjs scripts/runner-runtime-dependency-roots.test.mjs scripts/runner-package-executable.test.mjs scripts/stage-runner-binary.test.mjs scripts/build-inputs.test.mjs scripts/contracts/contract-fingerprints.test.mjs scripts/provider-pack-executable-shims.test.mjs scripts/controlled-evals/controller.test.mjs scripts/controlled-evals/portable.test.mjs && node --test scripts/build-copilot-distribution.test.mjs scripts/copilot-inner-distribution.test.mjs scripts/copilot-smoke-resources.test.mjs scripts/discover-copilot-acp.test.mjs scripts/materialize-copilot-binary.test.mjs scripts/install-copilot-assets.test.mjs scripts/probe-copilot-acp.test.mjs scripts/qualify-copilot-acp.test.mjs test/acpx-message-boundaries-package-contract.test.mjs", "test:typescript:vitest": "node ./scripts/run-pr-vitest-lane.mjs", "test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace", "test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider", diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_candidate_admission.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_candidate_admission.rs index c9eb0de9ecb..d848dbb5740 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_candidate_admission.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_candidate_admission.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::generated_acpx_profiles::acpx_release_profile; +use crate::generated_acpx_profiles::{acpx_release_profile, kimi_native_closure}; use crate::local_runner::LocalRunnerError; use crate::stable_identity::{is_stable_id, DURABLE_STABLE_ID_CHARS, SHORT_STABLE_ID_CHARS}; @@ -63,15 +63,41 @@ impl AcpxCandidateAdmission { } fn validate_at(&self, now: u64, os: &str, architecture: &str) -> Result<(), LocalRunnerError> { - let profile = acpx_release_profile("antigravity").ok_or_else(invalid)?; - let manifest: Value = serde_json::from_str(DISTRIBUTIONS).map_err(|_| invalid())?; - let closure = manifest - .pointer("/platforms/darwin-arm64/closureSha256") - .and_then(Value::as_str) - .ok_or_else(invalid)?; + let profile = acpx_release_profile(&self.agent).ok_or_else(invalid)?; + let (closure, host_matches) = match self.agent.as_str() { + "antigravity" => { + let manifest: Value = serde_json::from_str(DISTRIBUTIONS).map_err(|_| invalid())?; + let closure = manifest + .pointer("/platforms/darwin-arm64/closureSha256") + .and_then(Value::as_str) + .ok_or_else(invalid)? + .to_owned(); + ( + closure, + self.platform == "darwin" + && self.architecture == "arm64" + && os == "macos" + && architecture == "aarch64", + ) + } + "kimi" if profile.qualification_pending => { + let closure = + kimi_native_closure(&self.platform, &self.architecture).ok_or_else(invalid)?; + let host_matches = matches!( + ( + self.platform.as_str(), + self.architecture.as_str(), + os, + architecture + ), + ("darwin", "arm64", "macos", "aarch64") | ("linux", "x64", "linux", "x86_64") + ); + (closure.to_owned(), host_matches) + } + _ => return Err(invalid()), + }; if self.schema != SCHEMA || !is_stable_id(&self.company_id, DURABLE_STABLE_ID_CHARS) - || self.agent != "antigravity" || self.model.trim().is_empty() || self.model != self.model.trim() || self.model.len() > 512 @@ -79,10 +105,7 @@ impl AcpxCandidateAdmission { || matches!(self.model.to_ascii_lowercase().as_str(), "auto" | "default") || self.profile_digest != profile.command_digest || self.native_closure_digest != format!("sha256:{closure}") - || self.platform != "darwin" - || self.architecture != "arm64" - || os != "macos" - || architecture != "aarch64" + || !host_matches || self.expires_at > MAX_JSON_SAFE_INTEGER || self.expires_at <= now || self.expires_at > now.saturating_add(MAX_LIFETIME_MS) @@ -94,6 +117,27 @@ impl AcpxCandidateAdmission { Ok(()) } + pub fn validate_next_run( + &self, + next: &Self, + agent: &str, + model: &str, + run_id: &str, + normalized_session_id: &str, + ) -> Result<(), LocalRunnerError> { + if self.agent != "kimi" || self.run_id == next.run_id { + return Err(invalid()); + } + self.validate()?; + next.validate_binding(agent, model, run_id, normalized_session_id)?; + let mut rebound = next.clone(); + rebound.run_id = self.run_id.clone(); + if &rebound != self { + return Err(invalid()); + } + Ok(()) + } + pub fn validate_binding( &self, agent: &str, @@ -208,4 +252,163 @@ mod tests { let error = AcpxCandidateAdmission::parse_json("private malformed input").unwrap_err(); assert!(!error.to_string().contains("private")); } + + fn kimi_admission(platform: &str, architecture: &str) -> AcpxCandidateAdmission { + AcpxCandidateAdmission { + agent: "kimi".to_owned(), + model: "kimi-for-coding".to_owned(), + profile_digest: acpx_release_profile("kimi") + .unwrap() + .command_digest + .to_owned(), + native_closure_digest: format!( + "sha256:{}", + kimi_native_closure(platform, architecture).unwrap() + ), + platform: platform.to_owned(), + architecture: architecture.to_owned(), + ..test_admission("kimi-run", "kimi-session") + } + } + + #[test] + fn kimi_candidate_requires_exact_two_platform_closures_scope_and_time() { + for (platform, architecture, os, host_arch) in [ + ("darwin", "arm64", "macos", "aarch64"), + ("linux", "x64", "linux", "x86_64"), + ] { + let candidate = kimi_admission(platform, architecture); + let now = candidate.expires_at - 1; + candidate.validate_at(now, os, host_arch).unwrap(); + for (wrong_os, wrong_arch) in [ + ("windows", "x86_64"), + ("linux", "aarch64"), + ("macos", "x86_64"), + ] { + assert!(candidate.validate_at(now, wrong_os, wrong_arch).is_err()); + } + let (other_os, other_arch) = if os == "macos" { + ("linux", "x86_64") + } else { + ("macos", "aarch64") + }; + assert!(candidate.validate_at(now, other_os, other_arch).is_err()); + assert!(candidate + .validate_at(candidate.expires_at, os, host_arch) + .is_err()); + assert!(candidate + .validate_at(candidate.expires_at - MAX_LIFETIME_MS - 1, os, host_arch) + .is_err()); + for (field, value) in [ + ("companyId", json!("bad company")), + ("agent", json!("copilot")), + ("model", json!("default")), + ("profileDigest", json!("sha256:wrong")), + ("nativeClosureDigest", json!("sha256:wrong")), + ("platform", json!("windows")), + ("architecture", json!("s390x")), + ("runId", json!("bad/run")), + ("normalizedSessionId", json!("bad/session")), + ] { + let mut changed = serde_json::to_value(&candidate).unwrap(); + changed[field] = value; + let changed: AcpxCandidateAdmission = serde_json::from_value(changed).unwrap(); + assert!(changed.validate_at(now, os, host_arch).is_err(), "{field}"); + } + } + } + + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_next_run_rebinds_only_the_authenticated_run_identity() { + let (platform, architecture) = if cfg!(target_os = "macos") { + ("darwin", "arm64") + } else { + ("linux", "x64") + }; + let current = kimi_admission(platform, architecture); + let mut next = current.clone(); + next.run_id = "kimi-next-run".to_owned(); + current + .validate_next_run( + &next, + "kimi", + "kimi-for-coding", + "kimi-next-run", + "kimi-session", + ) + .unwrap(); + assert!(current + .validate_next_run( + ¤t, + "kimi", + "kimi-for-coding", + "kimi-run", + "kimi-session" + ) + .is_err()); + for (field, change) in [ + ("companyId", json!("another-company")), + ("model", json!("another-model")), + ("profileDigest", json!("sha256:wrong")), + ("nativeClosureDigest", json!("sha256:wrong")), + ("normalizedSessionId", json!("another-session")), + ("expiresAt", json!(next.expires_at + 1)), + ] { + let mut value = serde_json::to_value(&next).unwrap(); + value[field] = change; + let changed = serde_json::from_value(value).unwrap(); + assert!(current + .validate_next_run( + &changed, + "kimi", + "kimi-for-coding", + "kimi-next-run", + "kimi-session" + ) + .is_err()); + } + assert!(current + .validate_next_run( + &next, + "kimi", + "kimi-for-coding", + "foreign-run", + "kimi-session" + ) + .is_err()); + assert!(current + .validate_next_run( + &next, + "kimi", + "kimi-for-coding", + "kimi-next-run", + "foreign-session" + ) + .is_err()); + let mut expired = current.clone(); + expired.expires_at = 0; + assert!(expired + .validate_next_run( + &next, + "kimi", + "kimi-for-coding", + "kimi-next-run", + "kimi-session" + ) + .is_err()); + let antigravity = test_admission("run-1", "session-1"); + assert!(antigravity + .validate_next_run( + &next, + "kimi", + "kimi-for-coding", + "kimi-next-run", + "kimi-session" + ) + .is_err()); + } } 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 1271c0f050f..d7e7abd919a 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 @@ -927,6 +927,35 @@ impl AcpxCommandExecutor { .canonical_digest() } + fn restore_launch_profile_digest( + &self, + state: &AcpxDurableState, + ) -> Result { + let Some(profile) = self.launch_profile.as_ref() else { + return self.launch_profile_digest(); + }; + let Some(current) = profile.candidate_admission.as_ref().filter(|grant| { + grant.agent == "kimi" && state.descriptor.run_id != self.context.run_id + }) else { + return profile.canonical_digest(); + }; + let mut prior = profile.clone(); + prior.candidate_admission.as_mut().unwrap().run_id = state.descriptor.run_id.clone(); + prior + .candidate_admission + .as_ref() + .unwrap() + .validate_next_run( + current, + &state.descriptor.agent, + &state.descriptor.model, + &self.context.run_id, + &self.context.normalized_session_id, + ) + .map_err(|error| DurableRunnerError::invalid(error.to_string()))?; + prior.canonical_digest() + } + fn restore(&mut self) -> Result<(), DurableRunnerError> { self.ensure_persistence_healthy()?; if self.restore_checked { @@ -958,7 +987,10 @@ impl AcpxCommandExecutor { ))) } }; - if self.candidate_admission().is_some() { + if self + .candidate_admission() + .is_some_and(|admission| admission.agent == "antigravity") + { return Err(DurableRunnerError::invalid( "ACPX candidate cannot adopt a prior durable runner activation", )); @@ -983,7 +1015,7 @@ impl AcpxCommandExecutor { let state: AcpxDurableState = serde_json::from_slice(&bytes).map_err(|error| { DurableRunnerError::invalid(format!("ACPX provider state is malformed: {error}")) })?; - let launch_profile_digest = self.launch_profile_digest()?; + let launch_profile_digest = self.restore_launch_profile_digest(&state)?; state.validate(&self.context, &launch_profile_digest)?; self.state = Some(state); self.restore_session_if_needed() @@ -1218,7 +1250,10 @@ impl AcpxCommandExecutor { } fn attach_run(&mut self, payload: &Value) -> Result<(), DurableRunnerError> { - if self.candidate_admission().is_some() { + if self + .candidate_admission() + .is_some_and(|admission| admission.agent == "antigravity") + { return Err(DurableRunnerError::invalid( "ACPX candidate cannot rotate or adopt a live runner authority", )); @@ -1258,6 +1293,62 @@ impl AcpxCommandExecutor { attachment_context.run_id = run_id.to_owned(); } descriptor.validate(&attachment_context)?; + let cold_candidate_attachment = self + .candidate_admission() + .is_some_and(|grant| grant.agent == "kimi") + && payload.get("paperclipNextAuthority").is_none() + && self + .state + .as_ref() + .is_some_and(|state| state.descriptor.run_id != self.context.run_id); + let next_candidate = if let Some(admission) = self + .candidate_admission() + .filter(|admission| admission.agent == "kimi") + { + if cold_candidate_attachment { + let state = self.state.as_ref().unwrap(); + let digest = self.restore_launch_profile_digest(state)?; + state.validate(&self.context, &digest)?; + admission + .validate_binding( + &descriptor.agent, + &descriptor.model, + &self.context.run_id, + &self.context.normalized_session_id, + ) + .map_err(|error| DurableRunnerError::invalid(error.to_string()))?; + Some(admission.clone()) + } else { + let proposed: crate::acpx_candidate_admission::AcpxCandidateAdmission = + serde_json::from_value( + payload + .pointer("/paperclipNextAuthority/candidateAdmission") + .cloned() + .ok_or_else(|| { + DurableRunnerError::invalid( + "Kimi run.attach requires exact trusted candidate admission", + ) + })?, + ) + .map_err(|_| { + DurableRunnerError::invalid( + "Kimi run.attach candidate admission is invalid", + ) + })?; + admission + .validate_next_run( + &proposed, + &descriptor.agent, + &descriptor.model, + &descriptor.run_id, + &descriptor.normalized_session_id, + ) + .map_err(|error| DurableRunnerError::invalid(error.to_string()))?; + Some(proposed) + } + } else { + None + }; let tool_set = authorized_tool_set(payload)?; let state = self .state @@ -1327,16 +1418,30 @@ impl AcpxCommandExecutor { })?; } self.session = None; - let state = self + if let Some(admission) = next_candidate { + self.launch_profile + .as_mut() + .expect("candidate launch profile remains available") + .candidate_admission = Some(admission); + } + let launch_profile_digest = self.launch_profile_digest()?; + let mut state = self .state - .as_mut() - .expect("ACPX state remains available while attaching a run"); + .as_ref() + .expect("ACPX state remains available while attaching a run") + .clone(); + state.launch_profile_digest = launch_profile_digest; state.descriptor = descriptor; state.tool_set = tool_set; state.semantic_result = None; state.pending_events.clear(); state.lifecycle = "suspended".to_owned(); - self.save_state() + if cold_candidate_attachment { + self.commit_staged_state(state) + } else { + self.state = Some(state); + self.save_state() + } } fn open_session(&mut self) -> Result { @@ -2032,7 +2137,10 @@ impl AcpxCommandExecutor { } fn suspend(&mut self) -> Result { - if self.candidate_admission().is_some() { + if self + .candidate_admission() + .is_some_and(|admission| admission.agent == "antigravity") + { return self.close_session("candidate runner.suspend retires the owned process"); } if self @@ -2270,7 +2378,9 @@ fn goal_terminal_disposition<'a>( impl CommandExecutor for AcpxCommandExecutor { fn execute(&mut self, command: &Command) -> Result { self.restore()?; - if self.candidate_admission().is_some() + if self + .candidate_admission() + .is_some_and(|admission| admission.agent == "antigravity") && command.command_type == "run.attach" && command.payload.get("paperclipNextAuthority").is_some() { @@ -2346,6 +2456,12 @@ impl CommandExecutor for AcpxCommandExecutor { self.context.normalized_session_id = config.normalized_session_id.clone(); self.context.turn_id = config.turn_id.clone(); self.context.item_id = config.item_id.clone(); + if self + .candidate_admission() + .is_some_and(|admission| admission.agent == "kimi") + { + self.launch_profile = config.acpx_launch_profile.clone(); + } } fn poll_events(&mut self) -> Result, DurableRunnerError> { @@ -4824,4 +4940,510 @@ mod tests { drop(listeners.pop()); (candidates, listeners) } + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_candidate_attachment_atomically_rebinds_exact_grant_and_profile_digest() { + let directory = temporary_directory("kimi-candidate-attachment"); + let mut grant = crate::acpx_candidate_admission::test_admission("run-1", "session-1"); + grant.agent = "kimi".to_owned(); + grant.model = "explicit-test-model".to_owned(); + grant.profile_digest = acpx_release_profile("kimi") + .unwrap() + .command_digest + .to_owned(); + grant.platform = if cfg!(target_os = "macos") { + "darwin" + } else { + "linux" + } + .to_owned(); + grant.architecture = if cfg!(target_os = "macos") { + "arm64" + } else { + "x64" + } + .to_owned(); + grant.native_closure_digest = format!( + "sha256:{}", + crate::generated_acpx_profiles::kimi_native_closure( + &grant.platform, + &grant.architecture + ) + .unwrap() + ); + let command = directory.join("owned-sidecar"); + write_artifact(&command, b"#!/bin/sh\nexit 1\n", true); + let profile = AcpxLaunchProfile { + candidate_admission: Some(grant.clone()), + authority_digest: format!("sha256:{}", "d".repeat(64)), + command: command.clone(), + args: vec![], + artifacts: vec![artifact(&command)], + }; + let runtime = directory.join("runtime"); + let workspace = directory.join("workspace"); + fs::create_dir_all(&runtime).unwrap(); + fs::create_dir_all(&workspace).unwrap(); + fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap(); + fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap(); + let mut value = descriptor("kimi"); + value["runtimeDirectory"] = json!(runtime); + value["cwd"] = json!(workspace); + value["sidecarCommand"] = json!(command); + value["sidecarArgs"] = json!([]); + let desc: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap(); + let identity = AcpxProviderSessionIdentity { + kind: "acpx".to_owned(), + normalized_session_id: "session-1".to_owned(), + acpx_record_id: "record-1".to_owned(), + backend_session_id: "backend-1".to_owned(), + agent_session_id: "agent-1".to_owned(), + profile_digest: desc.command_digest.clone(), + workspace_digest: format!("sha256:{}", "a".repeat(64)), + requested_model: desc.model.clone(), + effective_model: desc.model.clone(), + permission_mode: Some(desc.permission_mode), + mode: None, + pi_thinking_level: None, + claude_effort: None, + grok_effort: None, + grok_disable_web_search: None, + provider_lifetime_fence_candidates: [60_001, 60_002, 60_003], + }; + let operations = Vec::new(); + let tools = AuthorizedToolSet { + schema: TOOL_SET_SCHEMA.to_owned(), + schema_version: 1, + catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(), + operations, + }; + let previous_digest = profile.canonical_digest().unwrap(); + let mut state = AcpxDurableState::new(desc, tools, previous_digest.clone()); + state.lifecycle = "suspended".to_owned(); + state.identity = Some(identity); + let config = test_config(&directory, Some(profile)); + let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config); + executor.state = Some(state); + executor.save_state().unwrap(); + let before = fs::read(executor.state_path()).unwrap(); + let mut next = grant.clone(); + next.run_id = "run-2".to_owned(); + let mut provider = value; + provider["runId"] = json!("run-2"); + let payload = json!({"provider":provider,"paperclipNextAuthority":{ + "identity":{"runId":"run-2","normalizedSessionId":"session-1"},"candidateAdmission":next + }}); + for (field, changed) in [ + ("companyId", json!("foreign")), + ("model", json!("wrong")), + ("expiresAt", json!(grant.expires_at + 1)), + ("runId", json!("unbound")), + ] { + let mut invalid = payload.clone(); + invalid["paperclipNextAuthority"]["candidateAdmission"][field] = changed; + assert!(executor.attach_run(&invalid).is_err(), "{field}"); + assert_eq!(fs::read(executor.state_path()).unwrap(), before); + } + assert!(executor.attach_run(&json!({"provider":provider})).is_err()); + executor.attach_run(&payload).unwrap(); + let updated = executor.state.as_ref().unwrap(); + assert_eq!(updated.descriptor.run_id, "run-2"); + assert_eq!(executor.context.run_id, "run-1"); + let profile = executor.launch_profile.as_ref().unwrap(); + assert_eq!(profile.candidate_admission.as_ref().unwrap(), &next); + assert_ne!(updated.launch_profile_digest, previous_digest); + assert_eq!( + updated.launch_profile_digest, + profile.canonical_digest().unwrap() + ); + let launch = updated + .descriptor + .session_config( + updated.tool_set.clone(), + updated.identity.clone(), + Some(profile), + ) + .unwrap(); + assert_eq!(launch.candidate_admission.as_ref().unwrap(), &next); + let saved: AcpxDurableState = + serde_json::from_slice(&fs::read(executor.state_path()).unwrap()).unwrap(); + assert_eq!(saved.descriptor.run_id, "run-2"); + assert_eq!(saved.launch_profile_digest, updated.launch_profile_digest); + assert!(executor.session.is_none()); + fs::remove_dir_all(directory).unwrap(); + } + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_cold_restore_fixture( + label: &str, + ) -> (PathBuf, DurableRunnerConfig, Value, Vec, PathBuf) { + let directory = temporary_directory(label); + let mut grant = crate::acpx_candidate_admission::test_admission("run-1", "session-1"); + grant.agent = "kimi".to_owned(); + grant.model = "explicit-test-model".to_owned(); + grant.profile_digest = acpx_release_profile("kimi") + .unwrap() + .command_digest + .to_owned(); + grant.platform = if cfg!(target_os = "macos") { + "darwin" + } else { + "linux" + } + .to_owned(); + grant.architecture = if cfg!(target_os = "macos") { + "arm64" + } else { + "x64" + } + .to_owned(); + grant.native_closure_digest = format!( + "sha256:{}", + crate::generated_acpx_profiles::kimi_native_closure( + &grant.platform, + &grant.architecture + ) + .unwrap() + ); + let command = directory.join("owned-sidecar"); + let sentinel = directory.join("provider-opened"); + write_artifact( + &command, + format!("#!/bin/sh\ntouch '{}'\nexit 1\n", sentinel.display()).as_bytes(), + true, + ); + let profile = AcpxLaunchProfile { + candidate_admission: Some(grant.clone()), + authority_digest: format!("sha256:{}", "d".repeat(64)), + command: command.clone(), + args: vec![], + artifacts: vec![artifact(&command)], + }; + let runtime = directory.join("runtime"); + let workspace = directory.join("workspace"); + fs::create_dir_all(&runtime).unwrap(); + fs::create_dir_all(&workspace).unwrap(); + fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap(); + fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap(); + let mut value = descriptor("kimi"); + value["runtimeDirectory"] = json!(runtime); + value["cwd"] = json!(workspace); + value["sidecarCommand"] = json!(command); + value["sidecarArgs"] = json!([]); + let desc: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap(); + let identity = AcpxProviderSessionIdentity { + kind: "acpx".to_owned(), + normalized_session_id: "session-1".to_owned(), + acpx_record_id: "record-1".to_owned(), + backend_session_id: "backend-1".to_owned(), + agent_session_id: "agent-1".to_owned(), + profile_digest: desc.command_digest.clone(), + workspace_digest: format!("sha256:{}", "a".repeat(64)), + requested_model: desc.model.clone(), + effective_model: desc.model.clone(), + permission_mode: Some(desc.permission_mode), + mode: None, + pi_thinking_level: None, + claude_effort: None, + grok_effort: None, + grok_disable_web_search: None, + provider_lifetime_fence_candidates: [60_001, 60_002, 60_003], + }; + let operations = Vec::new(); + let tools = AuthorizedToolSet { + schema: TOOL_SET_SCHEMA.to_owned(), + schema_version: 1, + catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(), + operations, + }; + let previous_digest = profile.canonical_digest().unwrap(); + let mut state = AcpxDurableState::new(desc, tools, previous_digest.clone()); + state.lifecycle = "suspended".to_owned(); + state.identity = Some(identity); + let config = test_config(&directory, Some(profile)); + let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config); + executor.state = Some(state); + executor.save_state().unwrap(); + let before = fs::read(executor.state_path()).unwrap(); + let mut next_config = config.clone(); + next_config.run_id = "run-2".to_owned(); + next_config.turn_id = "turn-2".to_owned(); + next_config.item_id = "item-2".to_owned(); + let next_grant = next_config + .acpx_launch_profile + .as_mut() + .unwrap() + .candidate_admission + .as_mut() + .unwrap(); + next_grant.run_id = "run-2".to_owned(); + next_config.validate().unwrap(); + let mut next_provider = value; + next_provider["runId"] = json!("run-2"); + ( + directory, + next_config, + json!({"provider": next_provider}), + before, + sentinel, + ) + } + + fn cold_attach_command(payload: Value) -> Command { + Command { + schema: "paperclip.prp.command.v1".into(), + command_id: "cold-attach".into(), + controller_seq: 1, + command_type: "run.attach".into(), + issued_at: "2026-10-30T00:00:00.000Z".into(), + deadline_at: None, + precondition: None, + payload, + } + } + + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_candidate_governed_wait_cold_restore_uses_authenticated_next_startup() { + let (directory, next_config, payload, before, sentinel) = + kimi_cold_restore_fixture("kimi-candidate-cold-governed-wait"); + let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &next_config); + recovered.restore().expect("authorized cold Kimi restore must retain the settled prior session until authenticated attachment"); + assert!(recovered.session.is_none()); + assert!(recovered.poll_events().unwrap().is_empty()); + assert!(!sentinel.exists()); + recovered.attach_run(&payload) + .expect("cold replacement uses its already authenticated startup authority, not a second warm next-authority grant"); + let saved: AcpxDurableState = + serde_json::from_slice(&fs::read(recovered.state_path()).unwrap()).unwrap(); + assert_eq!(saved.descriptor.run_id, "run-2"); + assert_eq!( + saved.launch_profile_digest, + next_config + .acpx_launch_profile + .as_ref() + .unwrap() + .canonical_digest() + .unwrap() + ); + assert!(recovered.session.is_none()); + assert!(!sentinel.exists()); + assert_ne!(fs::read(recovered.state_path()).unwrap(), before); + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_candidate_cold_restore_rejects_changed_startup_authority_before_provider_open() { + for field in [ + "company", + "model", + "expiry", + "expired", + "profile", + "closure", + "platform", + "architecture", + "session", + "grant_run", + "context_run", + "authority", + "command", + "args", + "artifact", + "missing_grant", + "saved_digest", + ] { + let (directory, mut config, payload, before, sentinel) = + kimi_cold_restore_fixture("kimi-candidate-cold-reject"); + match field { + "context_run" => config.run_id = "foreign-run".into(), + "missing_grant" => { + config + .acpx_launch_profile + .as_mut() + .unwrap() + .candidate_admission = None + } + "saved_digest" => { + let mut state: AcpxDurableState = serde_json::from_slice(&before).unwrap(); + state.launch_profile_digest = format!("sha256:{}", "0".repeat(64)); + fs::write( + directory.join("acpx-provider-state.json"), + serde_json::to_vec(&state).unwrap(), + ) + .unwrap(); + } + _ => { + let profile = config.acpx_launch_profile.as_mut().unwrap(); + let grant = profile.candidate_admission.as_mut().unwrap(); + match field { + "company" => grant.company_id = "foreign-company".into(), + "model" => grant.model = "foreign-model".into(), + "expiry" => grant.expires_at += 1, + "expired" => grant.expires_at = 1, + "profile" => grant.profile_digest = format!("sha256:{}", "0".repeat(64)), + "closure" => { + grant.native_closure_digest = format!("sha256:{}", "0".repeat(64)) + } + "platform" => grant.platform = "foreign-platform".into(), + "architecture" => grant.architecture = "foreign-architecture".into(), + "session" => grant.normalized_session_id = "foreign-session".into(), + "grant_run" => grant.run_id = "foreign-run".into(), + "authority" => { + profile.authority_digest = format!("sha256:{}", "0".repeat(64)) + } + "command" => profile.command = directory.join("foreign-command"), + "args" => profile.args.push("foreign-argument".into()), + "artifact" => { + profile.artifacts[0].sha256 = format!("sha256:{}", "0".repeat(64)) + } + _ => unreachable!(), + } + } + } + let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config); + let retained = fs::read(recovered.state_path()).unwrap(); + assert!( + recovered.execute(&cold_attach_command(payload)).is_err(), + "{field}" + ); + assert!(recovered.session.is_none(), "{field}"); + assert!(!sentinel.exists(), "{field}"); + assert_eq!( + fs::read(recovered.state_path()).unwrap(), + retained, + "{field}" + ); + fs::remove_dir_all(directory).unwrap(); + } + } + + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_candidate_cold_attach_rejects_unsettled_or_changed_session_before_provider_open() { + for field in [ + "run", + "session", + "model", + "workspace", + "active_turn", + "pending_event", + "exit_unconfirmed", + "identity", + "closed", + ] { + let (directory, config, mut payload, before, sentinel) = + kimi_cold_restore_fixture("kimi-candidate-cold-binding"); + let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config); + match field { + "run" => payload["provider"]["runId"] = json!("foreign-run"), + "session" => payload["provider"]["normalizedSessionId"] = json!("foreign-session"), + "model" => payload["provider"]["model"] = json!("foreign-model"), + "workspace" => { + payload["provider"]["cwd"] = json!(directory.join("foreign-workspace")) + } + _ => { + let mut state: AcpxDurableState = serde_json::from_slice(&before).unwrap(); + match field { + "active_turn" => state.active_turn_id = Some("active-turn".into()), + "pending_event" => { + state + .push(NormalizedProviderEvent { + event_type: "runtime_request.created".into(), + priority: EventPriority::P0, + payload: json!({"request":pending_input_request("input-1")}), + }) + .unwrap(); + } + "exit_unconfirmed" => state.provider_exit_unconfirmed = true, + "identity" => state.identity = None, + "closed" => state.lifecycle = "closed".into(), + _ => unreachable!(), + } + fs::write(recovered.state_path(), serde_json::to_vec(&state).unwrap()).unwrap(); + } + } + let retained = fs::read(recovered.state_path()).unwrap(); + assert!( + recovered.execute(&cold_attach_command(payload)).is_err(), + "{field}" + ); + assert!(recovered.session.is_none(), "{field}"); + assert!(!sentinel.exists(), "{field}"); + assert_eq!( + fs::read(recovered.state_path()).unwrap(), + retained, + "{field}" + ); + fs::remove_dir_all(directory).unwrap(); + } + } + + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_candidate_cold_attach_post_rename_failure_quarantines_public_execution() { + let (directory, config, payload, before, sentinel) = + kimi_cold_restore_fixture("kimi-candidate-cold-write-failure"); + let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config); + recovered.restore().unwrap(); + let previous_state = recovered.state.clone(); + recovered.fail_after_state_rename = true; + let command = cold_attach_command(payload); + let error = recovered.execute(&command).unwrap_err(); + assert!(error + .to_string() + .contains("injected post-rename sync failure")); + assert_eq!(recovered.state, previous_state); + assert!(recovered.session.is_none()); + assert!(!sentinel.exists()); + let disk = fs::read(recovered.state_path()).unwrap(); + assert_ne!(disk, before); + let saved: AcpxDurableState = serde_json::from_slice(&disk).unwrap(); + assert_eq!(saved.descriptor.run_id, "run-2"); + assert_eq!( + saved.launch_profile_digest, + config + .acpx_launch_profile + .as_ref() + .unwrap() + .canonical_digest() + .unwrap() + ); + recovered.fail_after_state_rename = false; + for result in [ + recovered.retained_events().map(|_| ()), + recovered.poll_events().map(|_| ()), + recovered.acknowledge_events(0), + recovered.restore(), + recovered.save_state(), + recovered.shutdown(), + ] { + assert_eq!(result.unwrap_err().to_string(), error.to_string()); + } + assert_eq!( + recovered.execute(&command).unwrap_err().to_string(), + error.to_string() + ); + assert_eq!(fs::read(recovered.state_path()).unwrap(), disk); + assert!(!sentinel.exists()); + fs::remove_dir_all(directory).unwrap(); + } } diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_capabilities.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_capabilities.rs index 65169a5a7bd..f8c48c170f7 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_capabilities.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_capabilities.rs @@ -11,7 +11,7 @@ pub(crate) enum RunAttachmentPolicy { pub(crate) fn run_attachment_policy(agent: &str) -> RunAttachmentPolicy { match agent { - "cursor" | "copilot" | "pi" => RunAttachmentPolicy::AuthenticatedRunGrants, + "cursor" | "copilot" | "pi" | "kimi" => RunAttachmentPolicy::AuthenticatedRunGrants, "claude" => RunAttachmentPolicy::AuthenticatedAssetPaths, _ => RunAttachmentPolicy::ImmutableInstructions, } diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_state.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_state.rs index b5d860a17e8..79af2cd1440 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_state.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_state.rs @@ -109,8 +109,8 @@ pub struct AcpxProviderState { plan_revision: u64, assistant_text: String, assistant_message_id: Option, - pi_assistant_message: Option, - pi_assistant_message_ids: BTreeSet, + native_assistant_message: Option, + native_assistant_message_ids: BTreeSet, pending_tools: BTreeMap, pending_tool_input_bytes: usize, pending_permissions: BTreeMap, @@ -128,8 +128,8 @@ impl AcpxProviderState { plan_revision: 0, assistant_text: String::new(), assistant_message_id: None, - pi_assistant_message: None, - pi_assistant_message_ids: BTreeSet::new(), + native_assistant_message: None, + native_assistant_message_ids: BTreeSet::new(), pending_tools: BTreeMap::new(), pending_tool_input_bytes: 0, pending_permissions: BTreeMap::new(), @@ -222,8 +222,8 @@ impl AcpxProviderState { self.plan_revision = 0; self.assistant_text.clear(); self.assistant_message_id = None; - self.pi_assistant_message = None; - self.pi_assistant_message_ids.clear(); + self.native_assistant_message = None; + self.native_assistant_message_ids.clear(); self.semantic_result = None; self.attempted_turn_controls.clear(); Ok(()) @@ -371,9 +371,9 @@ impl AcpxProviderState { }]) } AcpxEventPayload::TurnTerminal { status, error } => { - if status == AcpxTurnStatus::Completed && self.pi_assistant_message.is_some() { + if status == AcpxTurnStatus::Completed && self.native_assistant_message.is_some() { return Err(LocalRunnerError::invalid( - "Pi assistant message ended without its native boundary", + "ACP assistant message ended without its native boundary", )); } let turn_id = event @@ -394,8 +394,8 @@ impl AcpxProviderState { self.assistant_text.clear(); } self.assistant_message_id = None; - self.pi_assistant_message = None; - self.pi_assistant_message_ids.clear(); + self.native_assistant_message = None; + self.native_assistant_message_ids.clear(); events.push(AcpxProviderStateEvent::TurnTerminal { turn_id, status, @@ -518,6 +518,10 @@ impl AcpxProviderState { }; if kind == AcpxRuntimeEventKind::TextDelta && payload.get("piMessageHistory").and_then(Value::as_bool) != Some(true) + && payload + .get("assistantMessageHistory") + .and_then(Value::as_bool) + != Some(true) { let provider_message_id = payload .get("messageId") @@ -534,55 +538,67 @@ impl AcpxProviderState { .and_then(Value::as_str) .unwrap_or_default(); let mut native_boundary_clears = false; - if let Some(boundary) = payload.get("piMessageBoundary") { + if payload.get("assistantMessageBoundary").is_some() + && payload.get("piMessageBoundary").is_some() + { + return Err(LocalRunnerError::invalid( + "ACP assistant boundary has ambiguous representations", + )); + } + if let Some(boundary) = payload + .get("assistantMessageBoundary") + .or_else(|| payload.get("piMessageBoundary")) + { let phase = boundary.get("phase").and_then(Value::as_str); let reason = boundary.get("stopReason").and_then(Value::as_str); if !raw_text.is_empty() || provider_message_id.is_none() { return Err(LocalRunnerError::invalid( - "Pi assistant boundary contains invalid content or identity", + "ACP assistant boundary contains invalid content or identity", )); } match phase { - Some("start") if self.pi_assistant_message.is_none() && reason.is_none() => { - if self.pi_assistant_message_ids.len() >= 4096 + Some("start") + if self.native_assistant_message.is_none() && reason.is_none() => + { + if self.native_assistant_message_ids.len() >= 4096 || !self - .pi_assistant_message_ids + .native_assistant_message_ids .insert(provider_message_id.clone().unwrap()) { return Err(LocalRunnerError::invalid( - "Pi assistant identity is duplicated or exceeds its bound", + "ACP assistant identity is duplicated or exceeds its bound", )); } - self.pi_assistant_message = provider_message_id.clone(); + self.native_assistant_message = provider_message_id.clone(); native_boundary_clears = true; } Some("end") - if self.pi_assistant_message.is_some() - && self.pi_assistant_message == provider_message_id => + if self.native_assistant_message.is_some() + && self.native_assistant_message == provider_message_id => { if !matches!( reason, Some("stop" | "length" | "toolUse" | "error" | "aborted") ) { return Err(LocalRunnerError::invalid( - "Pi assistant stop reason is invalid", + "ACP assistant stop reason is invalid", )); } - self.pi_assistant_message = None; + self.native_assistant_message = None; native_boundary_clears = !matches!(reason, Some("stop" | "length")); } _ => { return Err(LocalRunnerError::invalid( - "Pi assistant boundaries are out of order", + "ACP assistant boundaries are out of order", )) } } - } else if !self.pi_assistant_message_ids.is_empty() - && (self.pi_assistant_message.is_none() - || self.pi_assistant_message != provider_message_id) + } else if !self.native_assistant_message_ids.is_empty() + && (self.native_assistant_message.is_none() + || self.native_assistant_message != provider_message_id) { return Err(LocalRunnerError::invalid( - "Pi assistant delta has no matching native message", + "ACP assistant delta has no matching native message", )); } // ACP can produce more than one assistant message in a turn. All 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 c57dd9a1ebd..3fe3a59c2ee 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 @@ -216,6 +216,7 @@ impl AcpxSidecarTransport { "pi" => &[], "cursor" => &["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"], "copilot" => &["COPILOT_GITHUB_TOKEN"], + "kimi" => &["KIMI_API_KEY"], "antigravity" => &[ "GEMINI_API_KEY", "PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET", @@ -254,7 +255,7 @@ impl AcpxSidecarTransport { "PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", ]; - if matches!(agent, "pi" | "cursor" | "copilot" | "antigravity") { + if matches!(agent, "pi" | "cursor" | "copilot" | "kimi" | "antigravity") { // Credential values alone are not proof of an explicit task binding. // The sidecar checks this controller-minted provider/session marker. keys.push("PAPERCLIP_ACPX_CREDENTIAL_BINDING"); diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/configured_environment.rs b/packages/paperclip-runner/runner/crates/runner-core/src/configured_environment.rs index f9d3cecf9c5..99950e24c9a 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/configured_environment.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/configured_environment.rs @@ -52,6 +52,7 @@ const RESERVED: &[&str] = &[ "GITHUB_ENTERPRISE_TOKEN", "GITHUB_TOKEN", "HOME", + "KIMI_API_KEY", "NODE_OPTIONS", "NODE_PATH", "OPENAI_API_KEY", @@ -209,6 +210,7 @@ mod tests { "[\"PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT\"]", "[\"PAPERCLIP_RUNTIME_NOTIFICATION_HELPER\"]", "[\"OPENAI_API_KEY\"]", + "[\"KIMI_API_KEY\"]", "[\"LD_PRELOAD\"]", "[\"bad=name\"]", "[\"CUSTOM\",\"CUSTOM\"]", @@ -226,6 +228,14 @@ mod tests { assert_eq!(command.get_envs().count(), 0); } let mut command = Command::new("unused"); + assert!(apply_configured_environment(&mut command, |key| match key { + MARKER => Some("[\"KIMI_API_KEY\"]".into()), + "KIMI_API_KEY" => Some("selected-code-key".into()), + _ => None, + }) + .is_err()); + assert_eq!(command.get_envs().count(), 0); + let mut command = Command::new("unused"); assert!( apply_configured_environment(&mut command, |key| Some(if key == MARKER { "[\"CUSTOM\"]".into() diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs b/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs index ac5838d2f17..3e9d58cd771 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs @@ -199,7 +199,6 @@ pub(crate) fn next_authority_config( next.normalized_session_id = read_identity("normalizedSessionId")?; next.turn_id = read_identity("turnId")?; next.item_id = read_identity("itemId")?; - next.validate()?; if next.runner_instance_id != current.runner_instance_id || next.environment_lease_id != current.environment_lease_id || next.normalized_session_id != current.normalized_session_id @@ -209,6 +208,47 @@ pub(crate) fn next_authority_config( "run.attach authority changed an immutable session binding", )); } + if let Some(admission) = current + .acpx_launch_profile + .as_ref() + .and_then(|profile| profile.candidate_admission.as_ref()) + { + if admission.agent == "kimi" { + let proposed: crate::acpx_candidate_admission::AcpxCandidateAdmission = + serde_json::from_value(boundary.get("candidateAdmission").cloned().ok_or_else( + || { + DurableRunnerError::invalid( + "Kimi authority rotation requires exact trusted candidate admission", + ) + }, + )?) + .map_err(|_| { + DurableRunnerError::invalid("Kimi authority rotation admission is invalid") + })?; + admission + .validate_next_run( + &proposed, + "kimi", + &admission.model, + &next.run_id, + &next.normalized_session_id, + ) + .map_err(|error| DurableRunnerError::invalid(error.to_string()))?; + next.acpx_launch_profile + .as_mut() + .unwrap() + .candidate_admission = Some(proposed); + } else if boundary.get("candidateAdmission").is_some() { + return Err(DurableRunnerError::invalid( + "candidate authority rotation is not admitted", + )); + } + } else if boundary.get("candidateAdmission").is_some() { + return Err(DurableRunnerError::invalid( + "authority rotation cannot create candidate admission", + )); + } + next.validate()?; Ok(Some(next)) } @@ -5104,4 +5144,129 @@ mod tests { assert_eq!(executor.calls, 1); fs::remove_dir_all(directory).unwrap(); } + #[test] + #[cfg(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64") + ))] + fn kimi_candidate_handoff_binds_the_existing_authenticated_next_authority() { + let directory = + std::env::temp_dir().join(format!("kimi-grant-handoff-{}", std::process::id())); + let mut current = config(directory); + let mut grant = crate::acpx_candidate_admission::test_admission( + "run_1", + ¤t.normalized_session_id, + ); + grant.agent = "kimi".to_owned(); + grant.profile_digest = crate::generated_acpx_profiles::acpx_release_profile("kimi") + .unwrap() + .command_digest + .to_owned(); + grant.platform = if cfg!(target_os = "macos") { + "darwin" + } else { + "linux" + } + .to_owned(); + grant.architecture = if cfg!(target_os = "macos") { + "arm64" + } else { + "x64" + } + .to_owned(); + grant.native_closure_digest = format!( + "sha256:{}", + crate::generated_acpx_profiles::kimi_native_closure( + &grant.platform, + &grant.architecture + ) + .unwrap() + ); + current.acpx_launch_profile = Some(crate::durable::AcpxLaunchProfile { + authority_digest: format!("sha256:{}", "a".repeat(64)), + command: PathBuf::from("/fixture/node"), + args: vec!["/fixture/sidecar.cjs".to_owned()], + artifacts: vec![crate::durable::QualifiedLaunchArtifact { + path: PathBuf::from("/fixture/node"), + sha256: format!("sha256:{}", "b".repeat(64)), + }], + candidate_admission: Some(grant.clone()), + }); + current.validate().unwrap(); + let mut next_grant = grant.clone(); + next_grant.run_id = "run_2".to_owned(); + let mut attach = command("run.attach"); + attach.payload = json!({"paperclipNextAuthority":{ + "identity":{"runnerInstanceId":current.runner_instance_id,"environmentLeaseId":current.environment_lease_id, + "runId":"run_2","normalizedSessionId":current.normalized_session_id,"turnId":"turn_2","itemId":"item_2"}, + "connection":{"mode":"external"},"candidateAdmission":next_grant + }}); + let next = next_authority_config(&attach, ¤t).unwrap().unwrap(); + let actual = next + .acpx_launch_profile + .as_ref() + .unwrap() + .candidate_admission + .as_ref() + .unwrap(); + assert_eq!(actual, &next_grant); + assert_eq!( + current + .acpx_launch_profile + .as_ref() + .unwrap() + .candidate_admission + .as_ref() + .unwrap(), + &grant + ); + assert_eq!(next.runner_instance_id, current.runner_instance_id); + assert_eq!(next.environment_lease_id, current.environment_lease_id); + assert_eq!(next.normalized_session_id, current.normalized_session_id); + for (field, value) in [ + ("companyId", json!("foreign-company")), + ("agent", json!("antigravity")), + ("model", json!("different-model")), + ("profileDigest", json!(format!("sha256:{}", "f".repeat(64)))), + ( + "nativeClosureDigest", + json!(format!("sha256:{}", "e".repeat(64))), + ), + ("expiresAt", json!(grant.expires_at + 1)), + ("runId", json!("unbound-run")), + ("normalizedSessionId", json!("foreign-session")), + ] { + let mut changed = attach.clone(); + changed.payload["paperclipNextAuthority"]["candidateAdmission"][field] = value; + assert!( + next_authority_config(&changed, ¤t).is_err(), + "{field}" + ); + } + for field in [ + "runnerInstanceId", + "environmentLeaseId", + "normalizedSessionId", + ] { + let mut changed = attach.clone(); + changed.payload["paperclipNextAuthority"]["identity"][field] = json!("foreign"); + assert!( + next_authority_config(&changed, ¤t).is_err(), + "{field}" + ); + } + let mut missing = attach.clone(); + missing.payload["paperclipNextAuthority"] + .as_object_mut() + .unwrap() + .remove("candidateAdmission"); + assert!(next_authority_config(&missing, ¤t).is_err()); + let mut no_prior = current.clone(); + no_prior + .acpx_launch_profile + .as_mut() + .unwrap() + .candidate_admission = None; + assert!(next_authority_config(&attach, &no_prior).is_err()); + } } 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 8b7cb4d7a3c..efc943221c9 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 @@ -13,6 +13,18 @@ pub(crate) struct AcpxReleaseProfile { pub qualification_pending: bool, } +pub(crate) fn kimi_native_closure(platform: &str, architecture: &str) -> Option<&'static str> { + match (platform, architecture) { + ("darwin", "arm64") => { + Some("6038325abd0431c30097ff1f572a9b29816e1b4da372d8cb86b2ca5d18ea34e7") + } + ("linux", "x64") => { + Some("105d8394adbbc76a953a12cc51b5288eb940ad693291e584f6cdc6407213164d") + } + _ => None, + } +} + pub(crate) fn acpx_release_profile(agent: &str) -> Option { Some(match agent { "antigravity" => AcpxReleaseProfile { @@ -85,6 +97,16 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option { requires_provider_policy: false, qualification_pending: false, }, + "kimi" => AcpxReleaseProfile { + agent_server_package: "@moonshot-ai/kimi-code", + agent_server_version: "2.3.3", + agent_runtime_package: None, + agent_runtime_version: None, + command_digest: + "sha256:0c1e6ce460407ec973a37a2d192d665fcde744e2b9875c791c90308afbf4c52b", + requires_provider_policy: true, + qualification_pending: true, + }, _ => return None, }) } diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/native_provider_backend.rs index a3ef35a7aa3..472d559f9c5 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/native_provider_backend.rs @@ -138,7 +138,12 @@ impl NativeProviderCommandExecutor { .config .acpx_launch_profile .as_ref() - .is_some_and(|profile| profile.candidate_admission.is_some()) + .is_some_and(|profile| { + profile + .candidate_admission + .as_ref() + .is_some_and(|admission| admission.agent == "antigravity") + }) && [ CODEX_PROVIDER_STATE_FILE, ACPX_PROVIDER_STATE_FILE, diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs index ce81b986920..732af85060f 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/acpx_provider_state.rs @@ -176,92 +176,103 @@ fn promotes_only_the_latest_provider_message_as_the_terminal_reply() { } #[test] -fn pi_native_boundaries_keep_progress_but_never_promote_tool_narration_or_history() { - for final_reason in ["stop", "length", "toolUse", "error", "aborted", "empty"] { - let mut state = AcpxProviderState::new("run-1").unwrap(); - state.begin_turn("turn-1").unwrap(); - let mut messages = vec![ - json!({"type":"text_delta","messageId":"history","text":"Earlier session answer","piMessageHistory":true}), - json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}), - json!({"type":"text_delta","messageId":"first","text":"Calling finish."}), - json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"end","stopReason":"toolUse"}}), - json!({"type":"text_delta","messageId":"last","text":"","piMessageBoundary":{"phase":"start"}}), - ]; - if final_reason != "empty" { - messages.push(json!({"type":"text_delta","messageId":"last","text":"EXACT_MARKER"})); - } - messages.push(json!({"type":"text_delta","messageId":"last","text":"","piMessageBoundary":{"phase":"end","stopReason":if final_reason == "empty" { "stop" } else { final_reason }}})); - let count = messages.len() as u64; - for (index, payload) in messages.into_iter().enumerate() { - state +fn native_boundaries_keep_progress_but_never_promote_tool_narration_or_history() { + for (boundary_field, history_field) in [ + ("piMessageBoundary", "piMessageHistory"), + ("assistantMessageBoundary", "assistantMessageHistory"), + ] { + for final_reason in ["stop", "length", "toolUse", "error", "aborted", "empty"] { + let mut state = AcpxProviderState::new("run-1").unwrap(); + state.begin_turn("turn-1").unwrap(); + let mut messages = vec![ + json!({"type":"text_delta","messageId":"history","text":"Earlier session answer",(history_field):true}), + json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}), + json!({"type":"text_delta","messageId":"first","text":"Calling finish."}), + json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"end","stopReason":"toolUse"}}), + json!({"type":"text_delta","messageId":"last","text":"",(boundary_field):{"phase":"start"}}), + ]; + if final_reason != "empty" { + messages + .push(json!({"type":"text_delta","messageId":"last","text":"EXACT_MARKER"})); + } + messages.push(json!({"type":"text_delta","messageId":"last","text":"",(boundary_field):{"phase":"end","stopReason":if final_reason == "empty" { "stop" } else { final_reason }}})); + let count = messages.len() as u64; + for (index, payload) in messages.into_iter().enumerate() { + state + .accept_event(&event( + index as u64 + 1, + GeneratedAcpxSidecarEventType::RuntimeEvent, + Some("turn-1"), + payload, + )) + .unwrap(); + } + let terminal = state .accept_event(&event( - index as u64 + 1, - GeneratedAcpxSidecarEventType::RuntimeEvent, + count + 1, + GeneratedAcpxSidecarEventType::RuntimeTurnTerminal, Some("turn-1"), - payload, + json!({"status":"completed"}), )) .unwrap(); - } - let terminal = state - .accept_event(&event( - count + 1, - GeneratedAcpxSidecarEventType::RuntimeTurnTerminal, - Some("turn-1"), - json!({"status":"completed"}), - )) - .unwrap(); - let replies: Vec<_> = terminal - .iter() - .filter_map(|entry| { - if let AcpxProviderStateEvent::AssistantMessage { text, .. } = entry { - Some(text.as_str()) + let replies: Vec<_> = terminal + .iter() + .filter_map(|entry| { + if let AcpxProviderStateEvent::AssistantMessage { text, .. } = entry { + Some(text.as_str()) + } else { + None + } + }) + .collect(); + assert_eq!( + replies, + if matches!(final_reason, "stop" | "length") { + vec!["EXACT_MARKER"] } else { - None + vec![] } - }) - .collect(); - assert_eq!( - replies, - if matches!(final_reason, "stop" | "length") { - vec!["EXACT_MARKER"] - } else { - vec![] - } - ); + ); + } } } #[test] -fn pi_native_boundary_order_and_terminal_completeness_fail_closed() { - for malformed in [ - json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}), - json!({"type":"text_delta","messageId":"other","text":"","piMessageBoundary":{"phase":"end","stopReason":"stop"}}), - json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"end","stopReason":"invented"}}), - json!({"type":"text_delta","messageId":"other","text":"Out of order"}), +fn native_boundary_order_and_terminal_completeness_fail_closed() { + for (boundary_field, _history_field) in [ + ("piMessageBoundary", "piMessageHistory"), + ("assistantMessageBoundary", "assistantMessageHistory"), ] { + for malformed in [ + json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}), + json!({"type":"text_delta","messageId":"other","text":"",(boundary_field):{"phase":"end","stopReason":"stop"}}), + json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"end","stopReason":"invented"}}), + json!({"type":"text_delta","messageId":"other","text":"Out of order"}), + ] { + let mut state = AcpxProviderState::new("run-1").unwrap(); + state.begin_turn("turn-1").unwrap(); + state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}))).unwrap(); + assert!(state + .accept_event(&event( + 2, + GeneratedAcpxSidecarEventType::RuntimeEvent, + Some("turn-1"), + malformed + )) + .is_err()); + } let mut state = AcpxProviderState::new("run-1").unwrap(); state.begin_turn("turn-1").unwrap(); - state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}))).unwrap(); + state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"",(boundary_field):{"phase":"start"}}))).unwrap(); assert!(state .accept_event(&event( 2, - GeneratedAcpxSidecarEventType::RuntimeEvent, + GeneratedAcpxSidecarEventType::RuntimeTurnTerminal, Some("turn-1"), - malformed + json!({"status":"completed"}) )) .is_err()); } - let mut state = AcpxProviderState::new("run-1").unwrap(); - state.begin_turn("turn-1").unwrap(); - state.accept_event(&event(1, GeneratedAcpxSidecarEventType::RuntimeEvent, Some("turn-1"), json!({"type":"text_delta","messageId":"first","text":"","piMessageBoundary":{"phase":"start"}}))).unwrap(); - assert!(state - .accept_event(&event( - 2, - GeneratedAcpxSidecarEventType::RuntimeTurnTerminal, - Some("turn-1"), - json!({"status":"completed"}) - )) - .is_err()); } #[test] diff --git a/packages/paperclip-runner/scripts/acpx-release-attestations.mjs b/packages/paperclip-runner/scripts/acpx-release-attestations.mjs index dc58149e0e7..e6e243c4f43 100644 --- a/packages/paperclip-runner/scripts/acpx-release-attestations.mjs +++ b/packages/paperclip-runner/scripts/acpx-release-attestations.mjs @@ -28,6 +28,10 @@ export const ACPX_RELEASE_ATTESTATIONS = Object.freeze({ nodeVersion: ["pi-node-pins.ts", "PI_NODE_VERSION"], closure: ["pi-closure-pins.ts", "PI_DISTRIBUTION_CLOSURE_SHA256"], } }, + kimi: { kind: "closure", pins: { + nodeVersion: ["kimi-closure-pins.ts", "KIMI_NODE_VERSION"], + closure: ["kimi-closure-pins.ts", "KIMI_DISTRIBUTION_CLOSURE_SHA256"], + } }, cursor: { kind: "declaration", path: "cursor-contract.json", digestField: "commandDigest" }, antigravity: { kind: "declaration", path: "antigravity-distributions.json" }, }); diff --git a/packages/paperclip-runner/scripts/acpx-release-attestations.test.mjs b/packages/paperclip-runner/scripts/acpx-release-attestations.test.mjs index c95cabe00de..6d849cf2b97 100644 --- a/packages/paperclip-runner/scripts/acpx-release-attestations.test.mjs +++ b/packages/paperclip-runner/scripts/acpx-release-attestations.test.mjs @@ -16,6 +16,7 @@ const packageFiles = [ "scripts/acpx-release-attestations.mjs", "scripts/generate-acpx-profiles.mjs", "src/providers/grok/launcher.cjs", "src/drivers/acpx/copilot-closure-pins.ts", "src/drivers/acpx/pi-closure-pins.ts", "src/drivers/acpx/pi-node-pins.ts", + "src/drivers/acpx/kimi-closure-pins.ts", "src/drivers/acpx/generated-profiles.ts", "runner/crates/runner-core/src/generated_acpx_profiles.rs", ]; @@ -37,7 +38,7 @@ const generate = (root, ...args) => spawnSync(process.execPath, [join(root, "scr test("every profile declares how its digest attests the upstream release", async () => { assert.deepEqual(Object.keys(ACPX_RELEASE_ATTESTATIONS).sort(), Object.keys(manifest.profiles).sort()); - assert.deepEqual(COMPUTED.sort(), ["antigravity", "copilot", "cursor", "grok", "pi"]); + assert.deepEqual(COMPUTED.sort(), ["antigravity", "copilot", "cursor", "grok", "kimi", "pi"]); for (const agent of COMPUTED) { assert.equal(manifest.profiles[agent].commandDigest, (await acpxReleaseAttestation(agent, packageRoot)).commandDigest, agent); } @@ -66,10 +67,11 @@ test("a relocated copy of the provider inputs computes the same attestations", a }); test("the attestations bind no Paperclip source beyond the Grok launcher", async () => { - for (const agent of ["copilot", "pi"]) { + for (const agent of ["copilot", "pi", "kimi"]) { const { declaration } = await acpxReleaseAttestation(agent, packageRoot); assert.deepEqual(Object.keys(declaration).filter(key => /source/iu.test(key)), [], agent); - assert.deepEqual(Object.keys(declaration.closure).sort(), ["darwin-arm64", "darwin-x64", "linux-x64"]); + assert.deepEqual(Object.keys(declaration.closure).sort(), agent === "kimi" + ? ["darwin-arm64", "linux-x64"] : ["darwin-arm64", "darwin-x64", "linux-x64"]); } }); @@ -86,6 +88,10 @@ for (const [name, agents, edit] of [ const path = join(root, "src/drivers/acpx/pi-closure-pins.ts"); writeFileSync(path, readFileSync(path, "utf8").replace(/"linux-x64": "[a-f0-9]{64}"/u, `"linux-x64": "${"0".repeat(64)}"`)); }], + ["a Kimi platform closure", ["kimi"], root => { + const path = join(root, "src/drivers/acpx/kimi-closure-pins.ts"); + writeFileSync(path, readFileSync(path, "utf8").replace(/"linux-x64": "[a-f0-9]{64}"/u, `"linux-x64": "${"0".repeat(64)}"`)); + }], ["the Grok launcher bytes", ["grok"], root => { const path = join(root, "src/providers/grok/launcher.cjs"); writeFileSync(path, `${readFileSync(path, "utf8")}\n// changed\n`); diff --git a/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs b/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs index 28f88f3e947..05a901f803d 100644 --- a/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs +++ b/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs @@ -8,6 +8,12 @@ import { build } from "esbuild"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); export const verifiedProviderEntrypoints = Object.freeze([ + Object.freeze({ + name: "kimi-metadata-probe", + source: resolve(packageRoot, "src/cli/kimi-metadata-probe.ts"), + output: resolve(packageRoot, "dist/cli/kimi-metadata-probe.js"), + verifiedOutput: resolve(packageRoot, "dist/cli/kimi-metadata-probe.cjs"), + }), Object.freeze({ name: "provision-antigravity", source: resolve(packageRoot, "scripts/provision-antigravity.mjs"), @@ -80,6 +86,7 @@ export async function bundleVerifiedProviderEntrypoints({ write = true } = {}) { platform: "node", format, target: "node24", + absWorkingDir: entrypoint.name === "kimi-metadata-probe" ? packageRoot : undefined, packages: "bundle", splitting: false, sourcemap: false, @@ -116,6 +123,7 @@ export async function bundleVerifiedProviderEntrypoints({ write = true } = {}) { results.push({ entrypoint, result, verifiedResult }); } await bundlePiProvisioner({ write }); + await bundleKimiProvisioner({ write }); return results; } @@ -146,6 +154,28 @@ export async function bundlePiProvisioner({ write = true, outputRoot = resolve(p return result; } +export async function bundleKimiProvisioner({ write = true, outputRoot = resolve(packageRoot, "dist/cli") } = {}) { + const entrypoint = { name: "provision-kimi", source: resolve(packageRoot, "scripts/provision-kimi.mjs") }; + const result = await build({ + entryPoints: [entrypoint.source], outfile: resolve(outputRoot, "provision-kimi.cjs"), + bundle: true, platform: "node", format: "cjs", target: "node24", packages: "bundle", + splitting: false, sourcemap: false, legalComments: "none", metafile: true, + treeShaking: true, write, logLevel: "silent", + banner: { js: 'const __paperclipVerifiedEntrypointUrl = require("node:url").pathToFileURL(__filename).href;' }, + define: { "import.meta.dirname": "__dirname", "import.meta.url": "__paperclipVerifiedEntrypointUrl" }, + }); + assertSelfContainedBundle(entrypoint, result); + if (write) { + const inputs = resolve(outputRoot, "kimi-provision-inputs"); + await mkdir(inputs, { recursive: true }); + for (const [source, name] of [ + ["scripts/kimi-distribution/package.json", "package.json"], + ["scripts/kimi-distribution/package-lock.json", "package-lock.json"], + ]) await cp(resolve(packageRoot, source), resolve(inputs, name)); + } + return result; +} + const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : null; diff --git a/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.test.mjs b/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.test.mjs index cca438a9322..70e2a1e2c53 100644 --- a/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.test.mjs +++ b/packages/paperclip-runner/scripts/build-verified-provider-entrypoints.test.mjs @@ -41,6 +41,11 @@ test("written provider entrypoints satisfy qualified launch permissions", async // Package-layout regression runs in the existing verified-entrypoint test lane. import "./provision-pi-package.test.mjs"; + +import "./materialize-kimi-distribution.test.mjs"; +import "./provision-kimi-package.test.mjs"; +import "./patch-kimi-runtime.test.mjs"; +import "./kimi-metadata-probe.test.mjs"; import "./antigravity-provisioner-layout.test.mjs"; import "./materialize-antigravity-distribution.test.mjs"; import "./probe-antigravity-metadata.test.mjs"; diff --git a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs index 00a9d3bb4f5..7208eb6517c 100644 --- a/packages/paperclip-runner/scripts/candidate-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/candidate-provider-pack.mjs @@ -1,7 +1,7 @@ import { materializePiDistribution } from "./materialize-pi-distribution.mjs"; import profiles from "../acpx-profiles.json" with { type: "json" }; import { materializePinnedCursorDistribution } from "./materialize-cursor-distribution.mjs"; -const CANDIDATES = new Set(["cursor", "copilot", "pi"]); +const CANDIDATES = new Set(["cursor", "copilot", "pi", "kimi"]); /** Only materialized providers enter the serialized manifest and its digest. */ export function providerPackManifestFields(providers, candidates) { @@ -34,6 +34,11 @@ export function parseProviderPackArguments(args) { /** Closed source-owned builder registry; provider branches add their exact pins. */ export async function materializeCandidateProviderPack({ provider, outputRoot }) { if (!CANDIDATES.has(provider)) throw new Error("Unknown candidate provider"); + if (provider === "kimi") { + const { materializeKimiDistribution } = await import("./materialize-kimi-distribution.mjs"); + const result = await materializeKimiDistribution({ outputRoot }); + return { version: result.version, profileDigest: profiles.profiles.kimi.commandDigest, closureDigest: result.closureDigest }; + } if (provider === "pi") return materializePiDistribution({ outputRoot }); if (provider === "copilot") { const { buildPinnedCopilotDistribution } = await import("./build-copilot-distribution.mjs"); diff --git a/packages/paperclip-runner/scripts/candidate-provider-pack.test.mjs b/packages/paperclip-runner/scripts/candidate-provider-pack.test.mjs index 636c40d536f..24ccb68a643 100644 --- a/packages/paperclip-runner/scripts/candidate-provider-pack.test.mjs +++ b/packages/paperclip-runner/scripts/candidate-provider-pack.test.mjs @@ -43,6 +43,27 @@ test("provider manifest digest survives disk JSON on supported and unsupported t test("candidate options stay explicit and supported native assets are included by default", () => { assert.deepEqual(parseProviderPackArguments(["--", "/pack"]), { output: "/pack", candidates: [] }); assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=pi,cursor"]), { output: "/pack", candidates: ["pi", "cursor"] }); + assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=kimi"]), { output: "/pack", candidates: ["kimi"] }); +}); + +test("Kimi stays explicit, pending and separate from released provider inventory", () => { + const kimi = { version: "2.3.3", qualification: "pending", profileDigest: "sha256:profile", closureDigest: "sha256:closure" }; + for (const [platform, architecture] of [["darwin", "arm64"], ["linux", "x64"]]) { + assert.equal(providerPackProviders(platform, architecture, []).includes("kimi"), false); + assert.equal(providerPackProviders(platform, architecture, ["kimi"]).includes("kimi"), true); + } + const fields = providerPackManifestFields({ kimi }, ["kimi"]); + assert.equal(fields.providers?.kimi, undefined); + assert.equal(fields.candidateProviders.kimi, kimi); + assert.equal(providerPackManifestFields({ kimi }, []).candidateProviders?.kimi, undefined); + assert.equal(digest(JSON.parse(JSON.stringify(fields))), digest(fields)); +}); + +test("Kimi's candidate builder rejects invalid paths before any public download", async t => { + let requests = 0; + t.mock.method(globalThis, "fetch", async () => { requests++; throw new Error("Unexpected Kimi download"); }); + await assert.rejects(materializeCandidateProviderPack({ provider: "kimi", outputRoot: "relative-kimi-output" }), /Kimi distribution output must be absolute/); + assert.equal(requests, 0); }); test("normal packs include Cursor and Copilot on their three pinned targets without breaking other hosts", () => { for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"]]) { diff --git a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs index 7b8d5fb59ad..8f65f62dff3 100644 --- a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs +++ b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs @@ -1,3 +1,4 @@ +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/kimi-closure-pins.ts"; import assert from "node:assert/strict"; import { readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; @@ -90,6 +91,13 @@ pub(crate) struct AcpxReleaseProfile { pub qualification_pending: bool, } +pub(crate) fn kimi_native_closure(platform: &str, architecture: &str) -> Option<&'static str> { + match (platform, architecture) { +${Object.entries(KIMI_DISTRIBUTION_CLOSURE_SHA256).map(([target, digest]) => { const [platform, architecture] = target.split("-"); return ` (${quote(platform)}, ${quote(architecture)}) => {\n Some(${quote(digest)})\n }`; }).join("\n")} + _ => None, + } +} + pub(crate) fn acpx_release_profile(agent: &str) -> Option { Some(match agent { ${entries.map(([agent, p]) => ` ${quote(agent)} => AcpxReleaseProfile { diff --git a/packages/paperclip-runner/scripts/kimi-distribution/package-lock.json b/packages/paperclip-runner/scripts/kimi-distribution/package-lock.json new file mode 100644 index 00000000000..a998cb751c6 --- /dev/null +++ b/packages/paperclip-runner/scripts/kimi-distribution/package-lock.json @@ -0,0 +1,606 @@ +{ + "name": "paperclip-kimi-runtime-distribution", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paperclip-kimi-runtime-distribution", + "version": "1.0.0", + "dependencies": { + "@moonshot-ai/kimi-code": "2.3.3" + }, + "engines": { + "node": ">=24.11.0" + } + }, + "node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@moonshot-ai/kimi-code": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@moonshot-ai/kimi-code/-/kimi-code-2.3.3.tgz", + "integrity": "sha512-3V8bycucf9KgPRH27S8De2u0ip84/Ry3FpNn6gi5z9JmoEHt9vxiYHHZqiMdyDf5nPm4bv+YKGwKEQL4oYpRTw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "qrcode": "^1.5.4", + "ws": "^8.18.0" + }, + "bin": { + "kimi": "dist/main.mjs" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "^0.3.9", + "node-pty": "^1.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/packages/paperclip-runner/scripts/kimi-distribution/package.json b/packages/paperclip-runner/scripts/kimi-distribution/package.json new file mode 100644 index 00000000000..0250f6e30c1 --- /dev/null +++ b/packages/paperclip-runner/scripts/kimi-distribution/package.json @@ -0,0 +1,11 @@ +{ + "name": "paperclip-kimi-runtime-distribution", + "version": "1.0.0", + "private": true, + "dependencies": { + "@moonshot-ai/kimi-code": "2.3.3" + }, + "engines": { + "node": ">=24.11.0" + } +} diff --git a/packages/paperclip-runner/scripts/kimi-instructions-runtime.mjs b/packages/paperclip-runner/scripts/kimi-instructions-runtime.mjs new file mode 100644 index 00000000000..c74a7252d48 --- /dev/null +++ b/packages/paperclip-runner/scripts/kimi-instructions-runtime.mjs @@ -0,0 +1,34 @@ +export const KIMI_INSTRUCTION_RUNTIME_SOURCE = [ + "function createKimiInstructionRuntime(raw, { createHash, byteLength }) {", + " const schema = \"paperclip.kimi.instructions.v1\";", + " const fail = () => { throw new Error(\"Kimi instruction binding is missing or invalid\"); };", + " if (typeof raw !== \"string\" || byteLength(raw) > 200_000) fail();", + " let value;", + " try { value = JSON.parse(raw); } catch { fail(); }", + " if (!value || typeof value !== \"object\" || Array.isArray(value)", + " || Object.keys(value).sort().join(\",\") !== \"byteLength,content,digest,schema\"", + " || value.schema !== schema || typeof value.content !== \"string\"", + " || value.content.includes(\"\\0\") || !value.content.isWellFormed()", + " || byteLength(value.content) > 32_768 || value.byteLength !== byteLength(value.content)", + " || typeof value.digest !== \"string\") fail();", + " const digest = text => `sha256:${createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\")}`;", + " const content = value.content;", + " if (value.digest !== digest(content)) fail();", + " const suffix = content.length === 0 ? \"\" : `\\n\\n${content}`;", + " const append = base => {", + " if (typeof base !== \"string\" || byteLength(base) > 2_097_152) fail();", + " return suffix.length === 0 || base.endsWith(suffix) ? base : `${base}${suffix}`;", + " };", + " return Object.freeze({", + " append,", + " receipt(sessionId, effectivePrompt) {", + " if (typeof sessionId !== \"string\" || !sessionId.trim() || append(effectivePrompt) !== effectivePrompt) fail();", + " return Object.freeze({ schema, sessionId, digest: value.digest,", + " byteLength: byteLength(content), effectiveDigest: digest(effectivePrompt),", + " effectiveByteLength: byteLength(effectivePrompt) });", + " },", + " });", + "}", +].join("\n"); + +export const createKimiInstructionRuntime = new Function(`return (${KIMI_INSTRUCTION_RUNTIME_SOURCE});`)(); diff --git a/packages/paperclip-runner/scripts/kimi-metadata-probe.test.mjs b/packages/paperclip-runner/scripts/kimi-metadata-probe.test.mjs new file mode 100644 index 00000000000..9c1f10dbe22 --- /dev/null +++ b/packages/paperclip-runner/scripts/kimi-metadata-probe.test.mjs @@ -0,0 +1,112 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdtemp, mkdir, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { build } from "esbuild"; +import { bundleVerifiedProviderEntrypoints } from "./build-verified-provider-entrypoints.mjs"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const sha = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`; + +test("Kimi metadata CLI is self-contained and rejects incomplete assets without inheriting credentials or making requests", { timeout: 20_000 }, async t => { + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-metadata-cli-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const bundles = await bundleVerifiedProviderEntrypoints({ write: false }); + const bundle = bundles.find(value => value.entrypoint.name === "kimi-metadata-probe"); + assert.ok(bundle, "The ordinary provider pack build must include the metadata CLI"); + const bytes = bundle.verifiedResult.outputFiles[0].contents; + const relocatedBuild = spawnSync(process.execPath, ["--input-type=module", "-e", ` + import { createHash } from "node:crypto"; + import { bundleVerifiedProviderEntrypoints } from ${JSON.stringify(new URL("./build-verified-provider-entrypoints.mjs", import.meta.url).href)}; + const bundles = await bundleVerifiedProviderEntrypoints({ write: false }); + const bytes = bundles.find(value => value.entrypoint.name === "kimi-metadata-probe").verifiedResult.outputFiles[0].contents; + process.stdout.write("sha256:" + createHash("sha256").update(bytes).digest("hex")); + `], { cwd: root, env: { PATH: "/usr/bin:/bin" }, encoding: "utf8", timeout: 10_000 }); + assert.ifError(relocatedBuild.error); + assert.equal(relocatedBuild.status, 0, relocatedBuild.stderr); + assert.equal(relocatedBuild.stdout, sha(bytes), "Metadata CLI admission bytes must be independent of the build cwd"); + const entry = join(root, "kimi-metadata-probe.cjs"); + await writeFile(entry, bytes); + assert.doesNotMatch(Buffer.from(bytes).toString(), /\bimport\.meta\b/); + for (const output of Object.values(bundle.verifiedResult.metafile.outputs)) { + for (const dependency of output.imports) assert.match(dependency.path, /^node:/); + } + const manifest = join(root, "package.json"); + await writeFile(manifest, '{"name":"@paperclipai/paperclip-runner","type":"module"}'); + const preload = join(root, "deny-external-work.cjs"); + await writeFile(preload, ` + const assert = require("node:assert/strict"); + process.nextTick(() => { + for (const name of ["KIMI_API_KEY", "PAPERCLIP_KIMI_PROBE_MODEL", "KIMI_MODEL_DEFAULT", "HOME", "HTTPS_PROXY", "NODE_OPTIONS", "NODE_PATH", "ANTHROPIC_API_KEY", "PAPERCLIP_ACPX_CANDIDATE_ADMISSION"]) { + assert.equal(process.env[name], undefined, name); + } + assert.equal(process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT, ${JSON.stringify(root)}); + }); + const fail = () => { throw Error("UNEXPECTED_NETWORK_OR_CHILD"); }; + globalThis.fetch = fail; + for (const name of ["node:net", "node:tls", "node:http", "node:https"]) { + const dependency = require(name); + for (const key of ["connect", "createConnection", "request", "get"]) if (key in dependency) dependency[key] = fail; + } + const child = require("node:child_process"); + for (const key of ["spawn", "exec", "execFile"]) child[key] = fail; + `); + const pinnedNode = join(packageRoot, "provider-assets/kimi", `${process.platform}-${process.arch}`, "runtime/node/bin/node"); + const node = await stat(pinnedNode).then(() => pinnedNode, () => process.execPath); + const profile = JSON.parse(await readFile(join(packageRoot, "acpx-profiles.json"), "utf8")).profiles.kimi; + const { KIMI_DISTRIBUTION_CLOSURE_SHA256 } = await import(new URL("../src/drivers/acpx/kimi-closure-pins.ts", import.meta.url)); + const admission = { schema: "paperclip.acpx-candidate-admission.v1", companyId: "metadata-cli-company", agent: "kimi", model: "selected-model", + profileDigest: profile.commandDigest, nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[`${process.platform}-${process.arch}`]}`, + platform: process.platform, architecture: process.arch, expiresAt: Date.now() + 60_000, + runId: "metadata-operation", normalizedSessionId: "metadata-session" }; + const child = spawnSync(node, ["--require", preload, entry], { + env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model", + PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(admission), + PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest, + KIMI_MODEL_DEFAULT: "unselected-model", HOME: "/private/unselected-home", HTTPS_PROXY: "http://127.0.0.1:9", + ANTHROPIC_API_KEY: "unselected-secret-canary", NODE_OPTIONS: "", NODE_PATH: "/unselected-module-path" }, + encoding: "utf8", timeout: 10_000, + }); + assert.ifError(child.error); + assert.equal(child.status, 1); + assert.equal(child.signal, null); + assert.doesNotMatch(child.stdout + child.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary|unselected-secret-canary/); + assert.deepEqual(JSON.parse(child.stdout), { schema: "paperclip.kimi.metadata.v1", status: "failed", + code: "KIMI_INSTALLATION_INVALID", message: process.versions.node === "24.21.0" || node === pinnedNode + ? "Install the current verified Kimi Code runtime in this execution environment." + : "Kimi metadata verification requires the pinned Node runtime.", promptSent: false }); + + const unqualified = spawnSync(node, ["--require", preload, entry], { + env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model", + PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest }, + encoding: "utf8", timeout: 10_000, + }); + assert.ifError(unqualified.error); assert.equal(unqualified.status, 1); + if (process.versions.node === "24.21.0" || node === pinnedNode) { + assert.equal(JSON.parse(unqualified.stdout).code, "KIMI_QUALIFICATION_REQUIRED"); + } + assert.doesNotMatch(unqualified.stdout + unqualified.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary/); + + for (const layout of ["runner", "server"]) { + const packageDirectory = join(root, layout); + const outputRoot = layout === "server" ? join(packageDirectory, "dist/vendor/paperclip-runner") : join(packageDirectory, "dist"); + const helper = join(outputRoot, "drivers/acpx/kimi-metadata-probe.js"); + const cli = join(outputRoot, "cli/kimi-metadata-probe.cjs"); + await mkdir(dirname(cli), { recursive: true }); + await writeFile(join(packageDirectory, "package.json"), '{"type":"module"}'); + await writeFile(cli, bytes); + await build({ entryPoints: [join(packageRoot, "src/drivers/acpx/kimi-metadata-probe.ts")], outfile: helper, + bundle: true, platform: "node", format: "esm", target: "node24", logLevel: "silent" }); + const api = await import(pathToFileURL(helper).href); + assert.equal(await api.kimiMetadataProbeDigest(), sha(bytes), `${layout} fixed artifact`); + await writeFile(cli, "different artifact bytes"); + assert.equal(await api.kimiMetadataProbeDigest(), sha("different artifact bytes"), `${layout} digest is fresh`); + await rm(cli); + await symlink(entry, cli); + await assert.rejects(api.kimiMetadataProbeDigest(), /ELOOP|symbolic/i); + } +}); diff --git a/packages/paperclip-runner/scripts/kimi-provider-pack-smoke.mjs b/packages/paperclip-runner/scripts/kimi-provider-pack-smoke.mjs new file mode 100644 index 00000000000..2c7f8af8591 --- /dev/null +++ b/packages/paperclip-runner/scripts/kimi-provider-pack-smoke.mjs @@ -0,0 +1,119 @@ +import assert from "node:assert/strict"; +import { once } from "node:events"; +import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +export async function smokeKimiProviderPack(directory) { + const pack = await realpath(resolve(directory)); + const manifest = JSON.parse(await readFile(join(pack, "provider-pack.json"), "utf8")); + assert.equal(manifest.schema, "paperclip-runner/remote-provider-pack/v1"); + assert.equal(manifest.payload.target.platform, process.platform); + assert.equal(manifest.payload.target.architecture, process.arch); + if (process.env.PAPERCLIP_EXPECTED_SOURCE_REVISION) assert.equal(manifest.payload.runnerSourceRevision, process.env.PAPERCLIP_EXPECTED_SOURCE_REVISION); + const candidate = manifest.payload.candidateProviders?.kimi; + assert.equal(candidate?.qualification, "pending"); + assert.equal(candidate.version, "2.3.3"); + assert.equal(candidate.path, `provider-assets/kimi/${process.platform}-${process.arch}`); + assert.equal(manifest.payload.providers?.kimi, undefined); + const load = path => import(pathToFileURL(join(pack, "dist/drivers/acpx", path))); + const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, { kimiInstructionBinding }, { KIMI_DISTRIBUTION_CLOSURE_SHA256 }] = await Promise.all([ + load("kimi-installation.js"), load("qualified-profiles.js"), load("kimi-config.js"), load("kimi-instructions.js"), load("kimi-closure-pins.js"), + ]); + assert.equal(candidate.profileDigest, QUALIFIED_ACPX_PROFILES.kimi.commandDigest); + assert.equal(candidate.closureDigest, `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[`${process.platform}-${process.arch}`]}`); + const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"]; + const previous = keys.map(key => process.env[key]); + process.env[keys[0]] = pack; process.env[keys[1]] = join(pack, "package.json"); + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-pack-smoke-"))); + const home = join(root, "home"), workspace = join(root, "workspace"); + await mkdir(home); await mkdir(workspace); + const model = "kimi-for-coding"; + const binding = kimiInstructionBinding("Do not call a model during this installed-package smoke. Exact suffix: café.\n"); + await writeFile(join(home, "config.toml"), kimiManagedConfiguration(model), { mode: 0o600 }); + const active = new Set(); let sequence = 0; + try { + const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi); + const connect = async () => { + const lease = await installation.openCommand(); + const child = lease.spawn([], { cwd: workspace, stdio: "pipe", env: { + PATH: "/usr/bin:/bin", LANG: "C.UTF-8", KIMI_CODE_HOME: home, + KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_API_KEY: "paperclip-offline-placeholder", + PAPERCLIP_KIMI_INSTRUCTIONS: binding.payload, CI: "1", NO_COLOR: "1", TERM: "dumb", + } }); + const closed = once(child, "close"), pending = new Map(); + let buffer = "", bytes = 0; + const fail = error => { for (const request of pending.values()) request.reject(error); pending.clear(); }; + child.on("error", fail); child.on("close", () => fail(new Error("Kimi exited with a pending ACP request"))); + child.stderr.resume(); + child.stdout.on("data", chunk => { + bytes += chunk.length; + if (bytes > 1024 * 1024) { fail(new Error("Kimi ACP output exceeded its package smoke bound")); return; } + buffer += chunk.toString("utf8"); + let newline; + while ((newline = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); + if (!line.trim()) continue; + let value; + try { value = JSON.parse(line); } catch { fail(new Error("Kimi emitted malformed ACP JSON")); return; } + const request = pending.get(value.id); + if (request && value.method === undefined) { + pending.delete(value.id); + if (value.error) request.reject(new Error(`Kimi rejected ACP request (${value.error.code})`)); + else request.resolve(value.result); + } + } + }); + const request = (method, params) => new Promise((accept, reject) => { + const id = sequence++; + const timeout = setTimeout(() => { pending.delete(id); reject(new Error(`Kimi ${method} timed out`)); }, 15_000); + const settle = callback => value => { clearTimeout(timeout); callback(value); }; + pending.set(id, { resolve: settle(accept), reject: settle(reject) }); + child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`); + }); + let closing; + const close = () => closing ??= (async () => { + const started = Date.now(); child.stdin.end(); + const term = setTimeout(() => child.kill("SIGTERM"), 4_000), kill = setTimeout(() => child.kill("SIGKILL"), 5_000); + try { + const [code, signal] = await closed; + assert.equal(code, 0, "Kimi did not exit cleanly after EOF"); assert.equal(signal, null); + return Date.now() - started; + } finally { clearTimeout(term); clearTimeout(kill); await lease.close(); active.delete(close); } + })(); + active.add(close); + const initialized = await request("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "paperclip-provider-pack-smoke", version: "1" } }); + assert.equal(initialized.agentInfo?.version, "2.3.3"); + return { request, close }; + }; + const assertBinding = (result, sessionId) => { + const ack = result._meta?.paperclipKimiInstructions; + assert.equal(ack?.sessionId, sessionId); assert.equal(ack.digest, binding.digest); assert.equal(ack.byteLength, binding.byteLength); + assert.match(ack.effectiveDigest, /^sha256:[a-f0-9]{64}$/); + }; + const first = await connect(); + const created = await first.request("session/new", { cwd: workspace, mcpServers: [] }); + assertBinding(created, created.sessionId); + const selected = await first.request("session/set_config_option", { sessionId: created.sessionId, configId: "model", value: model }); + assert.equal(selected.configOptions.find(option => option.id === "model").currentValue, model); + const createExitMs = await first.close(); + const second = await connect(); + const loaded = await second.request("session/load", { sessionId: created.sessionId, cwd: workspace, mcpServers: [] }); + assertBinding(loaded, created.sessionId); + assert.equal(loaded.configOptions.find(option => option.id === "model").currentValue, model); + const loadExitMs = await second.close(); + return { schema: "paperclip.kimi-provider-pack-smoke.v1", sourceRevision: manifest.payload.runnerSourceRevision, providerPackDigest: manifest.digest, + platform: process.platform, architecture: process.arch, runtimeVersion: "2.3.3", profileDigest: candidate.profileDigest, closureDigest: candidate.closureDigest, + createAcknowledged: true, loadAcknowledged: true, model, createExitMs, loadExitMs, providerPrompts: 0, providerCredentials: false, + qualification: "installed runtime only; no model entitlement or task completion" }; + } finally { + try { await Promise.all([...active].map(close => close())); } + finally { keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; }); await rm(root, { recursive: true, force: true }); } + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + if (process.argv.length !== 3) throw new Error("Usage: node scripts/kimi-provider-pack-smoke.mjs /absolute/installed-provider-pack"); + smokeKimiProviderPack(process.argv[2]).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(error.message); process.exitCode = 1; }); +} diff --git a/packages/paperclip-runner/scripts/materialize-kimi-distribution.mjs b/packages/paperclip-runner/scripts/materialize-kimi-distribution.mjs new file mode 100644 index 00000000000..4c69f3e2601 --- /dev/null +++ b/packages/paperclip-runner/scripts/materialize-kimi-distribution.mjs @@ -0,0 +1,182 @@ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises"; +import { dirname, isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256, KIMI_DISTRIBUTION_LOCK_SHA256, KIMI_NODE_DISTRIBUTIONS, KIMI_NODE_VERSION, KIMI_RUNTIME_VERSION } from "../src/drivers/acpx/kimi-closure-pins.ts"; +import { inventoryKimiRuntimeFiles, kimiRuntimeClosureDigest } from "../src/drivers/acpx/kimi-runtime-files.ts"; +import { patchKimiRuntime } from "./patch-kimi-runtime.mjs"; + +const run = promisify(execFile); +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(packageRoot, "../.."); +const hash = bytes => createHash("sha256").update(bytes).digest("hex"); +export const KIMI_DISTRIBUTION_ENTRYPOINT = "node_modules/@moonshot-ai/kimi-code/dist/main.mjs"; + +export async function runKimiBuildCommand(command, args, options, checkCancelled = () => {}, signal) { + checkCancelled(); signal?.throwIfAborted(); + const pending = run(command, args, { ...options, signal, killSignal: "SIGKILL" }); + const closed = new Promise(resolve => pending.child.once("close", resolve)); + try { + const result = await pending; + checkCancelled(); signal?.throwIfAborted(); + return result; + } finally { await closed; } +} + +export function kimiDistributionInstallCommand() { + return ["ci", "--ignore-scripts", "--omit=optional", "--omit=dev", "--no-audit", "--no-fund", "--registry=https://registry.npmjs.org", "--userconfig=.npmrc", "--globalconfig=.npmrc-global"]; +} + +export function kimiDistributionEnvironment(source = process.env) { + return Object.fromEntries(["PATH", "LANG", "LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "https_proxy", "http_proxy", "no_proxy", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"].flatMap(key => typeof source[key] === "string" ? [[key, source[key]]] : [])); +} + +export async function verifyLockedKimiPackageGraph(root, lock) { + if (lock.lockfileVersion !== 3 || !lock.packages || lock.packages[""]?.dependencies?.["@moonshot-ai/kimi-code"] !== KIMI_RUNTIME_VERSION) throw new Error("Kimi distribution lock is invalid"); + let packages = 0; + for (const [path, entry] of Object.entries(lock.packages)) { + if (!path) continue; + if (!path.startsWith("node_modules/") || /[\\\u0000-\u001f\u007f]/.test(path) || path.split("/").some(part => !part || part === "." || part === "..") || entry.link || typeof entry.version !== "string" || !/^https:\/\/registry\.npmjs\.org\//.test(entry.resolved ?? "") || !/^sha512-[A-Za-z0-9+/]+=*$/.test(entry.integrity ?? "")) throw new Error("Kimi distribution lock contains an unpinned package"); + if (entry.optional === true) { + try { await lstat(join(root, path)); } catch (error) { if (error.code === "ENOENT") continue; throw error; } + throw new Error("Kimi headless distribution unexpectedly installed an optional TUI dependency"); + } + const file = join(root, path, "package.json"); + if (await realpath(file) !== file || !(await lstat(file)).isFile()) throw new Error("Kimi distribution package metadata is not a regular file"); + const actual = JSON.parse(await readFile(file, "utf8")); + if (actual.version !== entry.version) throw new Error(`Kimi distribution package differs from its lock: ${path}`); + packages++; + } + return packages; +} + +export function assertKimiNodeSystemDependencies(listing, platform) { + for (const line of listing.split("\n").map(value => value.trim()).filter(Boolean)) { + if (platform === "darwin") { + if (line.endsWith(":")) continue; + if (!line.startsWith("/usr/lib/") && !line.startsWith("/System/Library/")) throw new Error("Kimi Node depends on an unbundled library"); + } else if (!line.startsWith("linux-vdso.so.") && !/^(?:[^\s]+ => )?\/(?:usr\/)?lib(?:64)?\//.test(line)) throw new Error("Kimi Node depends on an unbundled library"); + } +} + +async function nodeDistribution(staging, target, archivePath, environment, runOwned, signal) { + const pin = KIMI_NODE_DISTRIBUTIONS[target]; + if (!pin) throw new Error(`Kimi distribution target is unsupported: ${target}`); + let bytes; + if (archivePath) bytes = await readFile(archivePath); + else { + const timeout = AbortSignal.timeout(60_000); + const response = await fetch(`https://nodejs.org/dist/v${KIMI_NODE_VERSION}/node-v${KIMI_NODE_VERSION}-${target}.tar.gz`, { redirect: "error", signal: signal ? AbortSignal.any([signal, timeout]) : timeout }); + if (!response.ok || !response.body) throw new Error("Pinned Kimi Node download failed"); + const chunks = []; let length = 0; + for await (const chunk of response.body) { + length += chunk.length; + if (length > 128 * 1024 * 1024) throw new Error("Kimi Node archive exceeds its bound"); + chunks.push(Buffer.from(chunk)); + } + bytes = Buffer.concat(chunks); + } + if (bytes.length > 128 * 1024 * 1024 || hash(bytes) !== pin.archiveSha256) throw new Error("Kimi Node archive does not match its release pin"); + const archive = join(staging, `${target}.tar.gz`); await writeFile(archive, bytes); + const output = join(staging, `node-${target}`); await mkdir(output); + await runOwned("tar", ["-xzf", archive, "-C", output, "--strip-components=1", `node-v${KIMI_NODE_VERSION}-${target}/bin/node`, `node-v${KIMI_NODE_VERSION}-${target}/lib/node_modules/npm`], { env: environment, timeout: 30_000 }); + const executable = join(output, "bin/node"); + if (hash(await readFile(executable)) !== pin.executableSha256 || (await lstat(executable)).size !== pin.executableSize) throw new Error("Kimi Node executable does not match its target pin"); + const npm = join(output, "lib/node_modules/npm/bin/npm-cli.js"); + if (JSON.parse(await readFile(join(output, "lib/node_modules/npm/package.json"), "utf8")).version !== "11.19.0") throw new Error("Kimi Node archive has an unexpected npm version"); + return { executable, npm }; +} + +async function removePackageManagerLinks(root) { + const visit = async directory => { + for (const name of await readdir(directory)) { + const path = join(directory, name), stat = await lstat(path); + if (stat.isSymbolicLink()) { + if (dirname(path).endsWith("/node_modules/.bin")) await unlink(path); + else throw new Error("Kimi package payload contains an unsupported symbolic link"); + } else if (stat.isDirectory()) await visit(path); + else if (!stat.isFile()) throw new Error("Kimi package payload contains a non-file resource"); + } + }; + await visit(root); + await rm(join(root, "node_modules/.package-lock.json"), { force: true }); +} + +export async function buildKimiDistribution({ outputRoot, target = `${process.platform}-${process.arch}`, lockDirectory = join(packageRoot, "scripts/kimi-distribution"), nodeArchives = {}, checkCancelled = () => {}, signal }) { + checkCancelled(); signal?.throwIfAborted(); + if (typeof outputRoot !== "string" || !isAbsolute(outputRoot)) throw new Error("Kimi distribution output must be absolute"); + const output = resolve(outputRoot); + if (["/", packageRoot, repositoryRoot].includes(output)) throw new Error("Refusing unsafe Kimi distribution output"); + if (!Object.hasOwn(KIMI_NODE_DISTRIBUTIONS, target)) throw new Error("Kimi distribution target is unsupported"); + const host = `${process.platform}-${process.arch}`; + if (!Object.hasOwn(KIMI_NODE_DISTRIBUTIONS, host)) throw new Error("Kimi distribution build host is unsupported"); + try { await lstat(output); throw new Error("Kimi distribution output already exists"); } catch (error) { if (error.code !== "ENOENT") throw error; } + const lockBytes = await readFile(join(lockDirectory, "package-lock.json")); + if (hash(lockBytes) !== KIMI_DISTRIBUTION_LOCK_SHA256) throw new Error("Kimi distribution lock differs from its source pin"); + await mkdir(dirname(output), { recursive: true }); + const staging = await mkdtemp(join(await realpath(dirname(output)), ".paperclip-kimi-distribution-")); + const environment = kimiDistributionEnvironment(); + const runOwned = (command, args, options) => runKimiBuildCommand(command, args, options, checkCancelled, signal); + const runtime = join(staging, "runtime"); + try { + const buildHome = join(staging, "build-home"); await mkdir(buildHome); environment.HOME = buildHome; + const installedNode = await nodeDistribution(staging, target, nodeArchives[target], environment, runOwned, signal); + const buildNode = host === target ? installedNode : await nodeDistribution(staging, host, nodeArchives[host], environment, runOwned, signal); + if ((await runOwned(buildNode.executable, ["--version"], { env: {}, timeout: 10_000 })).stdout.trim() !== `v${KIMI_NODE_VERSION}`) throw new Error("Kimi distribution build Node cannot execute"); + await mkdir(runtime); + for (const name of ["package.json", "package-lock.json"]) await copyFile(join(lockDirectory, name), join(runtime, name)); + await writeFile(join(runtime, ".npmrc"), "registry=https://registry.npmjs.org/\nignore-scripts=true\naudit=false\nfund=false\n"); + await writeFile(join(runtime, ".npmrc-global"), ""); + await runOwned(buildNode.executable, [buildNode.npm, ...kimiDistributionInstallCommand()], { cwd: runtime, env: environment, timeout: 300_000, maxBuffer: 4 * 1024 * 1024 }); + if (!(await readFile(join(runtime, "package-lock.json"))).equals(lockBytes)) throw new Error("Kimi installation changed its committed lock"); + const packageCount = await verifyLockedKimiPackageGraph(runtime, JSON.parse(lockBytes.toString("utf8"))); + const patch = await patchKimiRuntime(join(runtime, KIMI_DISTRIBUTION_ENTRYPOINT)); + await mkdir(join(runtime, "node/bin"), { recursive: true }); + await copyFile(installedNode.executable, join(runtime, "node/bin/node")); await chmod(join(runtime, "node/bin/node"), 0o755); + if (host === target) { + const listing = process.platform === "darwin" + ? await runOwned("/usr/bin/otool", ["-L", join(runtime, "node/bin/node")], { env: {}, timeout: 10_000 }) + : await runOwned("ldd", [join(runtime, "node/bin/node")], { env: { PATH: "/usr/bin:/bin" }, timeout: 10_000 }); + assertKimiNodeSystemDependencies(listing.stdout, process.platform); + if ((await runOwned(join(runtime, "node/bin/node"), ["--version"], { env: {}, timeout: 10_000 })).stdout.trim() !== `v${KIMI_NODE_VERSION}`) throw new Error("Kimi Node cannot execute after relocation"); + } + await removePackageManagerLinks(runtime); + const entries = await inventoryKimiRuntimeFiles(runtime); + const closure = kimiRuntimeClosureDigest(entries); + const metadata = { schema: "paperclip.kimi-distribution.v1", version: KIMI_RUNTIME_VERSION, target, nodeVersion: KIMI_NODE_VERSION, runtimeRoot: "runtime", entrypoint: KIMI_DISTRIBUTION_ENTRYPOINT, lockSha256: KIMI_DISTRIBUTION_LOCK_SHA256, patch, packageCount, nativeClosureSha256: closure }; + await writeFile(join(staging, "native-closure.json"), `${JSON.stringify({ entries })}\n`); + await writeFile(join(staging, "kimi-distribution.json"), `${JSON.stringify(metadata, null, 2)}\n`); + for (const name of await readdir(staging)) if (!["runtime", "native-closure.json", "kimi-distribution.json"].includes(name)) await rm(join(staging, name), { recursive: true, force: true }); + checkCancelled(); + await rename(staging, output); + return { outputRoot: output, runtimeRoot: join(output, "runtime"), version: KIMI_RUNTIME_VERSION, closureDigest: `sha256:${closure}`, metadata }; + } catch (error) { await rm(staging, { recursive: true, force: true }); throw error; } +} + +export async function materializeKimiDistribution(options) { + if (typeof options.outputRoot !== "string" || !isAbsolute(options.outputRoot)) throw new Error("Kimi distribution output must be absolute"); + const output = resolve(options.outputRoot); + if (["/", packageRoot, repositoryRoot].includes(output)) throw new Error("Refusing unsafe Kimi distribution output"); + const target = options.target ?? `${process.platform}-${process.arch}`; + if (!Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, target)) throw new Error("Kimi distribution target is unsupported"); + try { await lstat(output); throw new Error("Kimi distribution output already exists"); } catch (error) { if (error.code !== "ENOENT") throw error; } + await mkdir(dirname(output), { recursive: true }); + const parent = await realpath(dirname(output)); + const temporary = await mkdtemp(join(parent, ".paperclip-kimi-admission-")); + const candidate = join(temporary, "candidate"); + try { + const result = await buildKimiDistribution({ ...options, outputRoot: candidate }); + if (result.metadata.nativeClosureSha256 !== KIMI_DISTRIBUTION_CLOSURE_SHA256[target]) throw new Error("Kimi native closure differs from its trusted target pin"); + try { await lstat(options.outputRoot); throw new Error("Kimi distribution output already exists"); } catch (error) { if (error.code !== "ENOENT") throw error; } + await rename(candidate, options.outputRoot); + return { ...result, outputRoot: options.outputRoot, runtimeRoot: join(options.outputRoot, "runtime") }; + } finally { await rm(temporary, { recursive: true, force: true }); } +} + +if (import.meta.url.endsWith("/materialize-kimi-distribution.mjs") && process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { + const args = process.argv.slice(2); + if (args.length !== 1) throw new Error("Usage: node scripts/materialize-kimi-distribution.mjs /absolute/output-directory"); + materializeKimiDistribution({ outputRoot: args[0] }).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(error.message); process.exitCode = 1; }); +} diff --git a/packages/paperclip-runner/scripts/materialize-kimi-distribution.test.mjs b/packages/paperclip-runner/scripts/materialize-kimi-distribution.test.mjs new file mode 100644 index 00000000000..a3adb002918 --- /dev/null +++ b/packages/paperclip-runner/scripts/materialize-kimi-distribution.test.mjs @@ -0,0 +1,121 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { chmod, link, mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import test from "node:test"; +import { setTimeout as delay } from "node:timers/promises"; +import { assertKimiNodeSystemDependencies, buildKimiDistribution, kimiDistributionEnvironment, kimiDistributionInstallCommand, materializeKimiDistribution, runKimiBuildCommand, verifyLockedKimiPackageGraph } from "./materialize-kimi-distribution.mjs"; +import { KIMI_DISTRIBUTION_LOCK_SHA256, KIMI_NODE_DISTRIBUTIONS } from "../src/drivers/acpx/kimi-closure-pins.ts"; +import { inventoryKimiRuntimeFiles, kimiRuntimeClosureDigest } from "../src/drivers/acpx/kimi-runtime-files.ts"; + +async function fixture(t) { + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-distribution-test-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const write = async (path, content) => { const file = join(root, path); await mkdir(dirname(file), { recursive: true }); await writeFile(file, content); }; + return { root, write }; +} + +test("Kimi's portable graph pins public integrity and excludes lifecycle and optional TUI dependencies", async () => { + const bytes = await readFile(new URL("./kimi-distribution/package-lock.json", import.meta.url)); + assert.equal(createHash("sha256").update(bytes).digest("hex"), KIMI_DISTRIBUTION_LOCK_SHA256); + const lock = JSON.parse(bytes.toString("utf8")); + const pkg = JSON.parse(await readFile(new URL("./kimi-distribution/package.json", import.meta.url), "utf8")); + assert.deepEqual(pkg.dependencies, { "@moonshot-ai/kimi-code": "2.3.3" }); + assert.deepEqual(lock.packages[""].dependencies, pkg.dependencies); + assert.equal(lock.packages["node_modules/@moonshot-ai/kimi-code"].integrity, "sha512-3V8bycucf9KgPRH27S8De2u0ip84/Ry3FpNn6gi5z9JmoEHt9vxiYHHZqiMdyDf5nPm4bv+YKGwKEQL4oYpRTw=="); + assert.equal(kimiDistributionInstallCommand()[0], "ci"); + assert.ok(kimiDistributionInstallCommand().includes("--ignore-scripts")); + assert.ok(kimiDistributionInstallCommand().includes("--omit=optional")); + assert.ok(kimiDistributionInstallCommand().includes("--registry=https://registry.npmjs.org")); + assert.deepEqual(Object.keys(KIMI_NODE_DISTRIBUTIONS).sort(), ["darwin-arm64", "linux-x64"]); +}); + +test("distribution build admits network settings without provider keys or Node injection", () => { + const network = { PATH: "/usr/bin:/bin", LANG: "C.UTF-8", HTTPS_PROXY: "http://proxy.invalid", NODE_EXTRA_CA_CERTS: "/public/ca.pem" }; + assert.deepEqual(kimiDistributionEnvironment({ ...network, HOME: "/private/home", KIMI_MODEL_API_KEY: "canary-secret", NPM_TOKEN: "canary-secret", NODE_OPTIONS: "--require /foreign.js", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" }), network); +}); + +test("setup cancellation physically stops an in-flight owned child", { timeout: 5_000 }, async t => { + const { root } = await fixture(t), pidFile = join(root, "pid"); + const controller = new AbortController(); + const result = runKimiBuildCommand(process.execPath, ["-e", "require('node:fs').writeFileSync(process.argv[1],String(process.pid));setInterval(()=>{},1000)", pidFile], { env: {}, timeout: 3_000 }, undefined, controller.signal).then(() => null, error => error); + let pid; + try { + for (let attempt = 0; attempt < 100; attempt++) { + try { pid = Number(await readFile(pidFile, "utf8")); break; } + catch (error) { if (error.code !== "ENOENT") throw error; await delay(10); } + } + assert.ok(Number.isSafeInteger(pid) && pid > 0, "The real child did not start"); + controller.abort(); + assert.equal((await result).code, "ABORT_ERR"); + assert.throws(() => process.kill(pid, 0), error => error.code === "ESRCH"); + } finally { controller.abort(); await result; } +}); + +test("setup cancellation aborts the pending download and removes its staging tree", { timeout: 5_000 }, async t => { + const { root } = await fixture(t), controller = new AbortController(); + let started; + const downloading = new Promise(resolve => { started = resolve; }); + t.mock.method(globalThis, "fetch", (_url, options) => new Promise((_resolve, reject) => { + started(); options.signal.addEventListener("abort", () => reject(options.signal.reason), { once: true }); + })); + const result = buildKimiDistribution({ outputRoot: join(root, "output"), signal: controller.signal }); + await downloading; controller.abort(); + await assert.rejects(result, error => error.name === "AbortError"); + assert.deepEqual(await readdir(root), []); +}); + +test("the installed graph rejects version drift and optional packages", async t => { + const { root, write } = await fixture(t); + const entry = { version: "2.3.3", resolved: "https://registry.npmjs.org/@moonshot-ai/kimi-code/-/kimi-code-2.3.3.tgz", integrity: "sha512-YWJjZA==" }; + const lock = { lockfileVersion: 3, packages: { "": { dependencies: { "@moonshot-ai/kimi-code": "2.3.3" } }, "node_modules/@moonshot-ai/kimi-code": entry, "node_modules/optional-native": { ...entry, optional: true } } }; + await write("node_modules/@moonshot-ai/kimi-code/package.json", '{"version":"2.3.3"}'); + assert.equal(await verifyLockedKimiPackageGraph(root, lock), 1); + await write("node_modules/@moonshot-ai/kimi-code/package.json", '{"version":"2.3.4"}'); + await assert.rejects(verifyLockedKimiPackageGraph(root, lock), /differs from its lock/); + await write("node_modules/@moonshot-ai/kimi-code/package.json", '{"version":"2.3.3"}'); + await write("node_modules/optional-native/package.json", '{"version":"2.3.3"}'); + await assert.rejects(verifyLockedKimiPackageGraph(root, lock), /optional TUI dependency/); +}); + +test("inventory binds native assets, workers, permissions and bytes in stable path order", async t => { + const { root, write } = await fixture(t); + await write("node_modules/provider/worker.mjs", "worker"); + await write("node_modules/provider/native/addon.node", Buffer.from([0, 1, 2, 3])); + await write("node/bin/node", "node"); + const first = await inventoryKimiRuntimeFiles(root); + assert.deepEqual(first.map(entry => entry.path), ["node/bin/node", "node_modules/provider/native/addon.node", "node_modules/provider/worker.mjs"]); + const digest = kimiRuntimeClosureDigest(first); + await write("node_modules/provider/worker.mjs", "worker changed"); + assert.notEqual(kimiRuntimeClosureDigest(await inventoryKimiRuntimeFiles(root)), digest); + await chmod(join(root, "node/bin/node"), 0o755); + assert.equal((await inventoryKimiRuntimeFiles(root))[0].executable, true); +}); + +test("inventory rejects external links, hardlinks and special files", async t => { + const { root, write } = await fixture(t); + await write("original", "content"); + await symlink("/outside", join(root, "escape")); + await assert.rejects(inventoryKimiRuntimeFiles(root), /symbolic link/); + await rm(join(root, "escape")); + await link(join(root, "original"), join(root, "alias")); + await assert.rejects(inventoryKimiRuntimeFiles(root), /invalid file/); +}); + +test("setup refuses relative, root, unsupported and existing outputs before network work", async t => { + const { root } = await fixture(t); + for (const operation of [buildKimiDistribution, materializeKimiDistribution]) { + await assert.rejects(operation({ outputRoot: "relative" }), /must be absolute/); + await assert.rejects(operation({ outputRoot: "/" }), /unsafe/); + await assert.rejects(operation({ outputRoot: join(root, "output"), target: "linux-arm64" }), /unsupported/); + await assert.rejects(operation({ outputRoot: root }), /already exists/); + } +}); + +test("the bundled Node may depend only on platform system libraries", () => { + assert.doesNotThrow(() => assertKimiNodeSystemDependencies("node:\n /usr/lib/libSystem.B.dylib (version 0)\n", "darwin")); + assert.throws(() => assertKimiNodeSystemDependencies("node:\n /opt/homebrew/lib/libnode.dylib (version 0)\n", "darwin"), /unbundled/); + assert.doesNotThrow(() => assertKimiNodeSystemDependencies("linux-vdso.so.1 (0x0)\nlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0)\n/lib64/ld-linux-x86-64.so.2 (0x0)\n", "linux")); + assert.throws(() => assertKimiNodeSystemDependencies("libcustom.so => /opt/private/libcustom.so (0x0)\n", "linux"), /unbundled/); +}); diff --git a/packages/paperclip-runner/scripts/patch-kimi-runtime.mjs b/packages/paperclip-runner/scripts/patch-kimi-runtime.mjs new file mode 100644 index 00000000000..ab4d66d976e --- /dev/null +++ b/packages/paperclip-runner/scripts/patch-kimi-runtime.mjs @@ -0,0 +1,160 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; +import { KIMI_INSTRUCTION_RUNTIME_SOURCE } from "./kimi-instructions-runtime.mjs"; + +export const KIMI_RUNTIME_PATCH = Object.freeze({ + version: "paperclip-kimi-2.3.3-instructions-v6", + vendorSha256: "f18b711e22947d473175126d0c2da437d641187963a3bc300d66e183060703e3", + patchedSha256: "79980aa95c54f325a05cbdf883f74443705db28f3e412a1b5ae22929d939eb66", +}); + +const sha256 = value => createHash("sha256").update(value).digest("hex"); + +export function replaceKimiPatchAnchor(source, before, after) { + if (source.split(before).length !== 2) throw new Error("Kimi runtime patch requires exactly one source anchor"); + return source.replace(before, () => after); +} + +export function patchKimiRuntimeSource(input) { + if (sha256(input) !== KIMI_RUNTIME_PATCH.vendorSha256) throw new Error("Kimi runtime patch input digest mismatch"); + let source = input; + const replace = (before, after) => { source = replaceKimiPatchAnchor(source, before, after); }; + replace("#!/usr/bin/env node\n", `#!/usr/bin/env node +import { createHash as paperclipKimiCreateHash } from "node:crypto"; +let paperclipKimiInstructionSnapshot; +function paperclipKimiInstructionState() { + if (paperclipKimiInstructionSnapshot) return paperclipKimiInstructionSnapshot; + const raw = process.env.PAPERCLIP_KIMI_INSTRUCTIONS; + delete process.env.PAPERCLIP_KIMI_INSTRUCTIONS; + return paperclipKimiInstructionSnapshot = (${KIMI_INSTRUCTION_RUNTIME_SOURCE})(raw, { + createHash: paperclipKimiCreateHash, byteLength: value => Buffer.byteLength(value, "utf8") + }); +} +function paperclipKimiChildEnvironment(overrides) { + const environment = { ...process.env, ...overrides }; + delete environment.KIMI_API_KEY; + delete environment.PAPERCLIP_KIMI_INSTRUCTIONS; + return environment; +} +`); + replace('function buildEnv(overrides) {\n\tif (overrides === void 0) return;\n\treturn {\n\t\t...process.env,\n\t\t...overrides\n\t};\n}', + 'function buildEnv(overrides) {\n\treturn paperclipKimiChildEnvironment(overrides);\n}'); + replace('env: globalThis.process.env', 'env: paperclipKimiChildEnvironment()'); + replace('\t\tbindTerminalCapability(terminal) {\n\t\t\tthis._terminalEnabled = terminal === true;\n\t\t}', + '\t\tbindTerminalCapability(terminal) {\n\t\t\tthis._terminalEnabled = false;\n\t\t}'); + replace("\t\tget systemPrompt() {\n\t\t\treturn this.profileState.systemPrompt;\n\t\t}", + "\t\tget systemPrompt() {\n\t\t\treturn paperclipKimiInstructionState().append(this.profileState.systemPrompt);\n\t\t}"); + replace("async function runAcpServerWithStream(stream, opts = {}) {\n", + "async function runAcpServerWithStream(stream, opts = {}) {\n\tpaperclipKimiInstructionState();\n"); + replace('\t\tmerged() {\n\t\t\treturn {\n\t\t\t\t...Object.fromEntries(this.pluginServers),\n\t\t\t\t...Object.fromEntries(this.fileServers)\n\t\t\t};\n\t\t}', + '\t\tmerged() {\n\t\t\treturn {};\n\t\t}'); + replace('\tawait pushFirstExisting$1(roots, USER_GENERIC_DIRS$1, osHomeDir, "user");', ''); + replace('async function projectRoots(workDir, options = {}) {\n', 'async function projectRoots(workDir, options = {}) {\n\treturn [];\n'); + replace('async function projectAgentRoots(fs, workDir, warn) {\n', 'async function projectAgentRoots(fs, workDir, warn) {\n\treturn [];\n'); + replace('async function userAgentRoots(fs, homeDir, osHomeDir, warn) {\n', 'async function userAgentRoots(fs, homeDir, osHomeDir, warn) {\n\treturn [];\n'); + replace('this.byEvent = indexHooks([...configured ?? [], ...pluginHooks]);', 'this.byEvent = new Map();'); + replace('\t\tasync evaluate(context) {\n\t\t\tfor (const policy of this.policies) {', + '\t\tasync evaluate(context) {\n\t\t\treturn { policyName: "paperclip-runner", result: { kind: "ask" } };\n\t\t\tfor (const policy of this.policies) {'); + replace('if (req.display.kind !== "plan_review") return CANONICAL_OPTIONS;', + 'if (req.display.kind !== "plan_review") return CANONICAL_OPTIONS.filter(option => option.kind !== "allow_always");'); + replace('\t\t\t\ttoolName: name,\n\t\t\t\taction,\n\t\t\t\tdisplay\n\t\t\t};\n\t\t\tconst approvalContext = {', + '\t\t\t\ttoolName: name,\n\t\t\t\ttoolInput: context.args,\n\t\t\t\taction,\n\t\t\t\tdisplay\n\t\t\t};\n\t\t\tconst approvalContext = {'); + replace('\t\ttitle: req.toolName,\n\t\tcontent\n', '\t\ttitle: req.toolName,\n\t\tkind: inferToolKind(req.toolName),\n\t\trawInput: req.toolInput,\n\t\tcontent\n'); + const bindRuntime = '\t\t\thandle.accessor.get(IAgentLifecycleService).handleOf(agentContext.agentId).accessor.get(IAgentRuntimeBindingService).switch(runtimeId);'; + replace(bindRuntime, `${bindRuntime} +\t\t\tconst paperclipProfile = handle.accessor.get(IAgentLifecycleService).handleOf(agentContext.agentId).accessor.get(IAgentProfileService); +\t\t\treturn paperclipKimiInstructionState().receipt(sessionId, paperclipProfile.getSystemPrompt());`); + replace("\t\t\tawait this.bindSessionRuntime?.(sessionId);\n", + "\t\t\tconst paperclipReceipt = await this.bindSessionRuntime?.(sessionId);\n\t\t\tif (!paperclipReceipt || paperclipReceipt.sessionId !== sessionId) throw new Error(\"Kimi native instruction receipt is missing\");\n\t\t\tthis.paperclipInstructionReceipts ??= new Map();\n\t\t\tthis.paperclipInstructionReceipts.set(sessionId, paperclipReceipt);\n"); + replace("\t\t\t\t...await this.activateSession(meta.id)\n", + "\t\t\t\t...await this.activateSession(meta.id),\n\t\t\t\t_meta: { paperclipKimiInstructions: this.paperclipInstructionReceipts.get(meta.id) }\n"); + const lifecycleState = "\t\t\t\tconfigOptions: await acpSession.configOptions(),\n\t\t\t\tmodes: acpSession.modeState()\n\t\t\t};\n\t\t}\n\t\tasync "; + replace(`${lifecycleState}resumeSession(params)`, + `${lifecycleState.replace("modes: acpSession.modeState()", "modes: acpSession.modeState(),\n\t\t\t\t_meta: { paperclipKimiInstructions: this.paperclipInstructionReceipts.get(params.sessionId) }")}resumeSession(params)`); + replace(`${lifecycleState}listSessions(params)`, + `${lifecycleState.replace("modes: acpSession.modeState()", "modes: acpSession.modeState(),\n\t\t\t\t_meta: { paperclipKimiInstructions: this.paperclipInstructionReceipts.get(params.sessionId) }")}listSessions(params)`); + replace('\t\t\t\tdriver.resolve({ stopReason: turnEndReasonToStopReason(event.reason, error) });', + '\t\t\t\tif (event.reason === "failed") {\n\t\t\t\t\tdriver.reject(RequestError$1.internalError({ schema: "paperclip.kimi.provider-error.v1", kind: "provider_failed" }, "Kimi provider turn failed"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tdriver.resolve({ stopReason: turnEndReasonToStopReason(event.reason, error) });'); + const stepSchema = 'object({ type: string(), turnId: number$1().int().nonnegative(), step: number$1().int().positive(), stepId: string().min(1).max(256).optional(), finishReason: string().max(32).optional(), reason: string().max(32).optional() })'; + replace('\tagentEvents = {\n\t\t"turn.started": {', `\tagentEvents = { +\t\t"turn.step.started": { kind: "stream", name: "events", type: "turn.step.started", schema: ${stepSchema} }, +\t\t"turn.step.completed": { kind: "stream", name: "events", type: "turn.step.completed", schema: ${stepSchema} }, +\t\t"turn.step.interrupted": { kind: "stream", name: "events", type: "turn.step.interrupted", schema: ${stepSchema} }, +\t\t"turn.started": {`); + replace('\t\t\tthis.subscriptions.push(events.on("assistant.delta", (event) => {', `\t\t\tthis.subscriptions.push( +\t\t\t\tevents.on("turn.step.started", event => this.dispatchTurnEvent(event.turnId, () => this.paperclipKimiStep(event, "start"))), +\t\t\t\tevents.on("turn.step.completed", event => this.dispatchTurnEvent(event.turnId, () => this.paperclipKimiStep(event, "end"))), +\t\t\t\tevents.on("turn.step.interrupted", event => this.dispatchTurnEvent(event.turnId, () => this.paperclipKimiStep(event, "interrupted"))), +\t\t\t\tevents.on("assistant.delta", (event) => {`); + replace('\t\tonAssistantDelta(event) {\n\t\t\tif (this.driverFor(event.turnId) === void 0) return;\n\t\t\tthis.emit(assistantDeltaToSessionUpdate(this.sessionId, event));\n\t\t}', `\t\tpaperclipKimiMessageFailure(driver) { +\t\t\tthis.settleDriver(driver, () => driver.reject(RequestError$1.internalError({ schema: "paperclip.kimi.provider-error.v1", kind: "message_boundary_invalid" }, "Kimi native message boundary is invalid"))); +\t\t} +\t\tpaperclipKimiChunk(driver, text, kind, thought = false) { +\t\t\tthis.emit({ sessionId: this.sessionId, update: { +\t\t\t\tsessionUpdate: thought ? "agent_thought_chunk" : "agent_message_chunk", +\t\t\t\tmessageId: driver.paperclipKimiMessage.id, content: { type: "text", text }, +\t\t\t\t_meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind } +\t\t\t} }); +\t\t} +\t\tpaperclipKimiStep(event, phase) { +\t\t\tconst driver = this.driverFor(event.turnId); +\t\t\tif (driver === void 0) return; +\t\t\tconst current = driver.paperclipKimiMessage; +\t\t\tif (!Number.isSafeInteger(event.step) || event.step < 1 || !Number.isSafeInteger(event.turnId) || event.turnId < 0) return this.paperclipKimiMessageFailure(driver); +\t\t\tif (phase === "start") { +\t\t\t\tif (current?.active || typeof event.stepId !== "string" || !/^[A-Za-z0-9_-]{1,256}$/.test(event.stepId) || current && event.step <= current.step) return this.paperclipKimiMessageFailure(driver); +\t\t\t\tdriver.paperclipKimiMessageIds ??= new Set(); +\t\t\t\tconst id = "kimi-message-" + paperclipKimiCreateHash("sha256").update(JSON.stringify([this.sessionId, event.turnId, event.stepId])).digest("hex"); +\t\t\t\tif (driver.paperclipKimiMessageIds.size >= 4096 || driver.paperclipKimiMessageIds.has(id)) return this.paperclipKimiMessageFailure(driver); +\t\t\t\tdriver.paperclipKimiMessageIds.add(id); +\t\t\t\tdriver.paperclipKimiMessage = { id, step: event.step, stepId: event.stepId, active: true }; +\t\t\t\tthis.paperclipKimiChunk(driver, "", "start"); +\t\t\t\treturn; +\t\t\t} +\t\t\tif (!current || current.step !== event.step || event.stepId !== void 0 && event.stepId !== current.stepId) return this.paperclipKimiMessageFailure(driver); +\t\t\tconst reason = phase === "interrupted" ? event.reason : event.finishReason; +\t\t\tconst reasons = phase === "interrupted" ? ["aborted", "max_steps", "error"] : ["end_turn", "max_tokens", "tool_use", "filtered", "paused", "other"]; +\t\t\tif (!reasons.includes(reason)) return this.paperclipKimiMessageFailure(driver); +\t\t\tif (!current.active) { +\t\t\t\tif (phase === "interrupted") return; +\t\t\t\treturn this.paperclipKimiMessageFailure(driver); +\t\t\t} +\t\t\tthis.paperclipKimiChunk(driver, "", "end:" + reason); +\t\t\tcurrent.active = false; +\t\t} +\t\tonAssistantDelta(event) { +\t\t\tconst driver = this.driverFor(event.turnId); +\t\t\tif (driver === void 0) return; +\t\t\tif (!driver.paperclipKimiMessage?.active || typeof event.delta !== "string") return this.paperclipKimiMessageFailure(driver); +\t\t\tthis.paperclipKimiChunk(driver, event.delta, "delta"); +\t\t}`); + replace('\t\tonThinkingDelta(event) {\n\t\t\tif (this.driverFor(event.turnId) === void 0) return;\n\t\t\tthis.emit(thinkingDeltaToSessionUpdate(this.sessionId, event));\n\t\t}', `\t\tonThinkingDelta(event) { +\t\t\tconst driver = this.driverFor(event.turnId); +\t\t\tif (driver === void 0) return; +\t\t\tif (!driver.paperclipKimiMessage?.active || typeof event.delta !== "string") return this.paperclipKimiMessageFailure(driver); +\t\t\tthis.paperclipKimiChunk(driver, event.delta, "delta", true); +\t\t}`); + return source; +} + +export async function patchKimiRuntime(path) { + const input = await readFile(path, "utf8"); + const digest = sha256(input); + if (digest === KIMI_RUNTIME_PATCH.patchedSha256) return { ...KIMI_RUNTIME_PATCH }; + const patched = patchKimiRuntimeSource(input); + if (sha256(patched) !== KIMI_RUNTIME_PATCH.patchedSha256) throw new Error("Kimi runtime patch output digest mismatch"); + await writeFile(path, patched); + return { ...KIMI_RUNTIME_PATCH }; +} + +if (import.meta.url?.endsWith("/patch-kimi-runtime.mjs") && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const path = process.argv[2]; + if (!path || process.argv.length !== 3) throw new Error("Usage: node patch-kimi-runtime.mjs "); + patchKimiRuntime(path).then(result => { + process.stdout.write(`${JSON.stringify(result)}\n`); + }).catch(error => { + process.stderr.write(`${error instanceof Error ? error.message : "Kimi runtime patch failed"}\n`); + process.exitCode = 1; + }); +} diff --git a/packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs b/packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs new file mode 100644 index 00000000000..a661c3af39a --- /dev/null +++ b/packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs @@ -0,0 +1,576 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { access, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import test from "node:test"; +import { runInNewContext } from "node:vm"; +import { createKimiInstructionRuntime } from "./kimi-instructions-runtime.mjs"; +import { KIMI_RUNTIME_PATCH, patchKimiRuntime, patchKimiRuntimeSource, replaceKimiPatchAnchor } from "./patch-kimi-runtime.mjs"; + +const sha = text => createHash("sha256").update(text, "utf8").digest("hex"); +const binding = content => JSON.stringify({ schema: "paperclip.kimi.instructions.v1", content, + digest: `sha256:${sha(content)}`, byteLength: Buffer.byteLength(content) }); +const runtime = raw => createKimiInstructionRuntime(raw, { createHash, byteLength: text => Buffer.byteLength(text, "utf8") }); + +test("native instruction binding preserves exact literals and rejects false readback", () => { + const content = "\nGovernance: résumé 日本語 ${cwd} ${base_prompt}\n\n"; + const state = runtime(binding(content)); + const native = "Original native system prompt"; + const effective = `${native}\n\n${content}`; + assert.equal(state.append(native), effective); + assert.equal(state.append(effective), effective); + const receipt = state.receipt("native-session", effective); + assert.deepEqual(receipt, { schema: "paperclip.kimi.instructions.v1", sessionId: "native-session", + digest: `sha256:${sha(content)}`, byteLength: Buffer.byteLength(content), + effectiveDigest: `sha256:${sha(effective)}`, effectiveByteLength: Buffer.byteLength(effective) }); + assert.throws(() => state.receipt("native-session", native), /binding/); + assert.throws(() => state.receipt("", effective), /binding/); + assert.throws(() => state.append("x".repeat(2_097_153)), /binding/); + assert.ok(Object.isFrozen(receipt)); +}); + +test("explicit empty instructions retain the native prompt and acknowledge zero bytes", () => { + const state = runtime(binding("")); + assert.equal(state.append("native"), "native"); + assert.equal(state.receipt("s", "native").byteLength, 0); +}); + +test("malformed, changed, oversized, and ill-formed instruction bindings fail closed", () => { + const valid = JSON.parse(binding("test")); + for (const raw of [undefined, "{", "[]", "null", " ".repeat(200_001), + binding("x\0y"), binding("x".repeat(32_769)), binding("\ud800"), + JSON.stringify({ ...valid, byteLength: 1 }), JSON.stringify({ ...valid, digest: "sha256:" + "0".repeat(64) }), + JSON.stringify({ ...valid, extra: true }), JSON.stringify({ schema: valid.schema, content: "test", digest: valid.digest })]) { + assert.throws(() => runtime(raw), /binding/); + } +}); + +test("patch rejects unknown source and ambiguous anchors before writing", async () => { + assert.throws(() => patchKimiRuntimeSource("unverified runtime"), /input digest/); + assert.throws(() => replaceKimiPatchAnchor("absent", "needle", "replacement"), /exactly one/); + assert.throws(() => replaceKimiPatchAnchor("needle needle", "needle", "replacement"), /exactly one/); + const root = await mkdtemp(join(tmpdir(), "kimi-patch-test-")); + const path = join(root, "main.mjs"); + try { + await writeFile(path, "unverified runtime"); + await assert.rejects(patchKimiRuntime(path), /input digest/); + assert.equal(await readFile(path, "utf8"), "unverified runtime"); + } finally { await rm(root, { recursive: true, force: true }); } +}); + +function segment(source, start, end) { + const first = source.indexOf(start); + assert.ok(first >= 0, `Missing native start: ${start}`); + const last = source.indexOf(end, first + start.length); + assert.ok(last > first, `Missing native end: ${end}`); + return source.slice(first, last); +} + +test("pinned native package binds new/load and rejects failed turns", { + skip: !process.env.PAPERCLIP_KIMI_VENDOR_MAIN, +}, async () => { + const input = await readFile(process.env.PAPERCLIP_KIMI_VENDOR_MAIN, "utf8"); + const patched = patchKimiRuntimeSource(input); + assert.equal(sha(patched), KIMI_RUNTIME_PATCH.patchedSha256); + const { build } = await import("esbuild"); + const bundledRoot = await mkdtemp(join(tmpdir(), "kimi-bundled-patch-test-")); + try { + const bundledPath = join(bundledRoot, "patch-kimi-runtime.bundle.cjs"); + await build({ entryPoints: [fileURLToPath(new URL("./patch-kimi-runtime.mjs", import.meta.url))], + outfile: bundledPath, bundle: true, platform: "node", format: "cjs", target: "node24", logLevel: "silent", + define: { "import.meta.url": JSON.stringify(pathToFileURL(bundledPath).href) } }); + const bundled = (await import(pathToFileURL(bundledPath))).default; + assert.equal(sha(bundled.patchKimiRuntimeSource(input)), KIMI_RUNTIME_PATCH.patchedSha256, + "installed bundled patcher produces exactly the same pinned native bytes"); + } finally { await rm(bundledRoot, { recursive: true, force: true }); } + const content = " Exact ${cwd} instructions\n日本語\n"; + const env = { PAPERCLIP_KIMI_INSTRUCTIONS: binding(content) }; + const helper = segment(patched, "let paperclipKimiInstructionSnapshot;", "import { fileURLToPath"); + const context = { process: { env }, Buffer, paperclipKimiCreateHash: createHash }; + const state = runInNewContext(`${helper};paperclipKimiInstructionState()`, context); + assert.equal(env.PAPERCLIP_KIMI_INSTRUCTIONS, undefined); + env.PAPERCLIP_KIMI_INSTRUCTIONS = binding("replacement"); + assert.equal(runInNewContext("paperclipKimiInstructionState()", context), state); + env.KIMI_API_KEY = "fake-provider-key"; + env.PAPERCLIP_TASK_SCOPED = "assigned-task-value"; + const childEnvironment = segment(patched, "function buildEnv(overrides) {", "function waitForSpawn(child)"); + const buildEnvironment = runInNewContext(`${childEnvironment};buildEnv`, context); + for (const overrides of [undefined, { KIMI_API_KEY: "override-key", PAPERCLIP_KIMI_INSTRUCTIONS: "private", EXTRA: "assigned" }]) { + const child = buildEnvironment(overrides); + assert.equal(child.KIMI_API_KEY, undefined); + assert.equal(child.PAPERCLIP_KIMI_INSTRUCTIONS, undefined); + assert.equal(child.PAPERCLIP_TASK_SCOPED, "assigned-task-value"); + assert.equal(child.EXTRA, overrides?.EXTRA); + } + assert.equal(env.KIMI_API_KEY, "fake-provider-key"); + const terminalBinding = segment(patched, "\t\tbindTerminalCapability(terminal) {", "\n\t\tget terminalEnabled()"); + const bindTerminal = runInNewContext(`({${terminalBinding}}).bindTerminalCapability`); + const terminalHolder = {}; + bindTerminal.call(terminalHolder, true); + assert.equal(terminalHolder._terminalEnabled, false); + const getter = segment(patched, "\t\tget systemPrompt() {", "\t\tget thinkingLevel()"); + const native = runInNewContext(`({profileState:{systemPrompt:"native"},${getter}})`, context); + assert.equal(native.systemPrompt, `native\n\n${content}`); + assert.equal(native.profileState.systemPrompt, "native"); + const receipt = state.receipt("s", native.systemPrompt); + const baseline = segment(patched, "\t\tmerged() {\n\t\t\treturn {};", "\n\t\tasync watchConfigFiles()"); + const merged = runInNewContext(`({${baseline}}).merged`, context); + assert.deepEqual(Object.keys(merged.call(new Proxy({}, { get: () => assert.fail("ambient MCP was read") }))), []); + const policy = segment(patched, '\t\tasync evaluate(context) {\n\t\t\treturn { policyName: "paperclip-runner"', '\n\t};'); + const evaluate = runInNewContext(`({${policy}}).evaluate`, context); + assert.equal((await evaluate.call(new Proxy({}, { get: () => assert.fail("native heuristic was read") }), {})).result.kind, "ask"); + const userRoots = segment(patched, "async function userRoots(homeDir, osHomeDir, options = {}) {", "async function projectRoots("); + const discovered = []; + const roots = await runInNewContext(`(${userRoots})`, { USER_BRAND_DIRS$1: ["skills"], + pushBrandGroup: async (_roots, _dirs, home) => { discovered.push(home); }, + pushFirstExisting$1: () => assert.fail("global skills were read") })("/owned-kimi", "/ambient-home"); + assert.deepEqual(discovered, ["/owned-kimi"]); + assert.equal(roots.length, 0); + const host = { klient: { session: () => ({ agent: () => ({}) }) }, + bindDefaultModel: async () => {}, bindSessionRuntime: async () => undefined }; + const wire = segment(patched, "\t\tasync wireSession(sessionId) {", "\n\t\t/**"); + await assert.rejects(runInNewContext(`({${wire}}).wireSession`, context).call(host, "s"), /native instruction receipt/); + const admitted = { ensureAuthed: async () => {}, klient: { global: { sessions: { create: async () => ({ id: "s" }) } } }, + activateSession: async () => ({}), paperclipInstructionReceipts: new Map([["s", receipt]]) }; + const create = segment(patched, "\t\tasync newSession(params) {", "\n\t\t/**"); + const createContext = { ...context, acpMcpServersToConfigRecord: value => value }; + const created = await runInNewContext(`({${create}}).newSession`, createContext).call(admitted, { cwd: "/workspace", mcpServers: [] }); + assert.equal(created._meta.paperclipKimiInstructions, receipt); + for (const [name, next] of [["loadSession", "resumeSession"], ["resumeSession", "listSessions"]]) { + const method = segment(patched, `\t\tasync ${name}(params) {`, `\t\tasync ${next}(params)`); + const response = await runInNewContext(`({${method}}).${name}`, createContext).call({ ...admitted, + warnIgnoredAdditionalDirs() {}, scheduleAvailableCommandsUpdate() {}, + resumeAcpSession: async () => ({ replayHistory: async () => {}, configOptions: async () => [], modeState: () => ({}) }) }, + { sessionId: "s", mcpServers: [] }); + assert.equal(response._meta.paperclipKimiInstructions, receipt); + } + const terminal = segment(patched, "\t\tonTurnEnded(event) {", "\n\t\t/**"); + const rejection = []; + const RequestError = { authRequired: () => ({ kind: "auth" }), internalError: data => data }; + const end = runInNewContext(`({${terminal}}).onTurnEnded`, { RequestError$1: RequestError, + isAuthError: error => error?.code === "provider.auth_error", turnEndReasonToStopReason: () => "end_turn" }); + for (const code of ["provider.auth_error", "provider.server_error"]) { + end.call({ driverFor: () => ({ reject: error => rejection.push(error), resolve: () => assert.fail("failed turn resolved") }), + settleDriver: (_driver, action) => action(), emitUsageUpdate() {} }, { turnId: 1, reason: "failed", error: { code } }); + } + assert.equal(rejection[0].kind, "auth"); + assert.equal(rejection[1].kind, "provider_failed"); + const infer = segment(patched, "function inferToolKind(name) {", "\n/**"); + const approval = segment(patched, "function buildPermissionToolCallUpdate(req) {", "\n/**"); + const permission = runInNewContext(`${infer}\n${approval}\nbuildPermissionToolCallUpdate`, { + displayBlockToAcpContent: () => null, acpToolCallId: (turn, id) => `${turn}:${id}`, + }); + for (const [toolName, kind] of [["Write", "edit"], ["Edit", "edit"], ["Read", "read"], ["Bash", "execute"], ["mcp__paperclip__read_workspace_file", "other"]]) { + const args = { path: "/workspace/exact.txt", content: "body" }; + const request = permission({ toolName, toolInput: args, toolCallId: "native-tool", turnId: 1, display: {}, action: "test" }); + assert.equal(request.kind, kind); + assert.equal(request.rawInput, args); + assert.equal(request.toolCallId, "1:native-tool"); + } +}); + +test("native step identity separates identical commentary/final text and retains empty boundaries", { + skip: !process.env.PAPERCLIP_KIMI_VENDOR_MAIN, +}, async () => { + const patched = patchKimiRuntimeSource(await readFile(process.env.PAPERCLIP_KIMI_VENDOR_MAIN, "utf8")); + const methods = segment(patched, "\t\tpaperclipKimiMessageFailure(driver) {", "\n\t\tonToolCallStarted(event)"); + const host = () => { + const events = [], rejected = []; + const driver = { reject: value => rejected.push(value) }; + const session = runInNewContext(`new class {${methods}}`, { paperclipKimiCreateHash: createHash, + RequestError$1: { internalError: value => value } }); + Object.assign(session, { sessionId: "native-session", emit: value => events.push(value), + driverFor: turnId => turnId === 7 && !driver.settled ? driver : undefined, + settleDriver: (value, action) => { value.settled = true; action(); } }); + return { session, events, rejected, driver }; + }; + const h = host(); + h.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "native-step-uuid-one" }, "start"); + h.session.onAssistantDelta({ turnId: 7, delta: "SAME café" }); + h.session.onThinkingDelta({ turnId: 7, delta: "reasoning" }); + h.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "native-step-uuid-one", finishReason: "tool_use" }, "end"); + h.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "native-step-uuid-two" }, "start"); + h.session.onAssistantDelta({ turnId: 7, delta: "SAME café" }); + h.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "native-step-uuid-two", finishReason: "end_turn" }, "end"); + h.session.paperclipKimiStep({ turnId: 7, step: 3, stepId: "native-step-uuid-empty" }, "start"); + h.session.paperclipKimiStep({ turnId: 7, step: 3, stepId: "native-step-uuid-empty", finishReason: "end_turn" }, "end"); + const chunks = h.events.map(event => event.update); + assert.equal(h.rejected.length, 0); + assert.deepEqual(chunks.map(chunk => chunk._meta.kind), ["start", "delta", "delta", "end:tool_use", "start", "delta", "end:end_turn", "start", "end:end_turn"]); + assert.equal(chunks[0].messageId, `kimi-message-${sha(JSON.stringify(["native-session", 7, "native-step-uuid-one"]))}`); + assert.equal(new Set(chunks.map(chunk => chunk.messageId)).size, 3); + assert.equal(chunks[2].sessionUpdate, "agent_thought_chunk"); + assert.ok(chunks.every(chunk => chunk._meta.origin === "kimi-native-assistant" && chunk._meta.source === "kimi-turn-step-v1")); + assert.ok(chunks.filter(chunk => chunk._meta.kind !== "delta").every(chunk => chunk.content.text === "")); + h.session.onAssistantDelta({ turnId: 8, delta: "foreign turn" }); + assert.equal(h.events.length, 9); + for (const mutate of [ + value => value.session.onAssistantDelta({ turnId: 7, delta: "orphan" }), + value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "other" }, "start"); }, + value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "foreign", finishReason: "end_turn" }, "end"); }, + value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid", finishReason: "invented" }, "end"); }, + value => { value.session.paperclipKimiStep({ turnId: 7, step: 1, stepId: "uuid" }, "start"); value.session.paperclipKimiStep({ turnId: 7, step: 1, reason: "aborted" }, "interrupted"); value.session.paperclipKimiStep({ turnId: 7, step: 2, stepId: "uuid" }, "start"); }, + ]) { + const invalid = host(); mutate(invalid); + assert.equal(invalid.rejected.length, 1); + assert.equal(invalid.rejected[0].kind, "message_boundary_invalid"); + const count = invalid.events.length; + invalid.session.onAssistantDelta({ turnId: 7, delta: "late after failure" }); + assert.equal(invalid.events.length, count); + } +}); + +test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a simulated local model service", { + skip: process.env.PAPERCLIP_TEST_KIMI_MODEL_PEER !== "1", + timeout: 60_000, +}, async t => { + const packageRoot = await realpath(fileURLToPath(new URL("../", import.meta.url))); + const assets = join(packageRoot, "provider-assets/kimi", `${process.platform}-${process.arch}`); + try { await access(join(assets, "native-closure.json")); } + catch (error) { + if (error.code !== "ENOENT") throw error; + t.skip("Install the pinned Kimi runtime with paperclipai runtime setup kimi first"); + return; + } + const { tsImport } = await import("tsx/esm/api"); + const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, instructionModule, { createKimiMessageProjection }, { normalizeAcpxPermission }] = await Promise.all([ + tsImport("../src/drivers/acpx/kimi-installation.ts", import.meta.url), + tsImport("../src/drivers/acpx/qualified-profiles.ts", import.meta.url), + tsImport("../src/drivers/acpx/kimi-config.ts", import.meta.url), + tsImport("../src/drivers/acpx/kimi-instructions.ts", import.meta.url), + tsImport("../src/drivers/acpx/kimi-message-boundary.ts", import.meta.url), + tsImport("../src/drivers/acpx/acp-permission-adapter.ts", import.meta.url), + ]); + const { createAcpRuntime, createFileSessionStore } = await import(pathToFileURL(join(packageRoot, "node_modules/acpx/dist/runtime.js"))); + const root = await realpath(await mkdtemp(join(tmpdir(), "kimi-simulated-model-peer-"))); + const home = join(root, "home"), workspace = join(root, "workspace"); + await mkdir(home); await mkdir(workspace); + const instructions = "\nExact governance café 日本語 ${cwd} ${base_prompt}\nKeep the final LF.\n"; + const selectedModel = "kimi-for-coding"; + const binding = instructionModule.kimiInstructionBinding(instructions); + const argumentsObject = { payload: "café 🧪\n${literal}", nested: { count: 0, empty: {}, flag: false } }; + const toolReceipt = sha(JSON.stringify(argumentsObject)); + const deniedPath = join(workspace, "denied-peer.txt"); + const bashArguments = { command: 'if test -n "${KIMI_API_KEY+x}"; then printf "PROVIDER_KEY_PRESENT"; exit 1; fi; if test -n "${PAPERCLIP_KIMI_INSTRUCTIONS+x}"; then printf "PRIVATE_BINDING_PRESENT"; exit 1; fi; if test "$PAPERCLIP_TASK_SCOPED" != "assigned-task-canary"; then printf "TASK_VARIABLE_MISSING"; exit 1; fi; printf "KIMI_CHILD_CREDENTIAL_FENCE_OK"', timeout: 5 }; + const modelRequests = [], mcpCalls = [], permissions = [], deliveries = [], nativeMessages = [], owners = []; + let scenario = "echo", modelStep = 0, peerFailure, permissionGate; + const complete = (response, chunks) => { + response.writeHead(200, { "content-type": "text/event-stream" }); + const bytes = Buffer.from(chunks.map(chunk => `data: ${JSON.stringify({ id: "simulated-peer", ...chunk })}\n\n`).join("") + "data: [DONE]\n\n"); + const index = bytes.indexOf(Buffer.from("é")); + if (index >= 0) { response.write(bytes.subarray(0, index + 1)); response.end(bytes.subarray(index + 1)); } + else response.end(bytes); + }; + const server = createServer(async (request, response) => { + try { + if (request.method !== "POST") { response.writeHead(405); response.end(); return; } + let body = ""; + for await (const chunk of request) { body += chunk; assert.ok(Buffer.byteLength(body) < 2_097_152); } + const value = JSON.parse(body); + if (request.url === "/v1/chat/completions") { + assert.equal(request.headers.authorization, "Bearer simulated-peer-not-a-credential"); + assert.equal(value.model, selectedModel); + const system = value.messages.find(message => message.role === "system")?.content; + assert.equal(typeof system, "string"); + assert.ok(system.endsWith(`\n\n${instructions}`), "exact literal instructions reached native HTTP request"); + const ack = nativeMessages.map(entry => entry.message.result?._meta?.paperclipKimiInstructions).filter(Boolean).at(-1); + assert.equal(ack?.digest, binding.digest); + assert.equal(ack?.effectiveDigest, `sha256:${sha(system)}`, "native instruction receipt matches the full actual model-request prompt"); + modelRequests.push({ scenario, value }); + assert.ok(modelRequests.length <= 9, "bounded simulated model requests"); + modelStep++; + if (scenario === "error") { + response.writeHead(418, { "content-type": "application/json" }); + response.end(JSON.stringify({ error: { type: "simulated_provider_error", message: "private-peer-error-canary" } })); + return; + } + if (modelStep === 1) { + const tools = scenario === "echo" + ? [{ id: "peer-echo", name: "mcp__peer__echo_peer", args: argumentsObject }, { id: "peer-empty", name: "mcp__peer__empty_peer", args: {} }] + : scenario === "bash" ? [{ id: "peer-bash", name: "Bash", args: bashArguments }] + : [{ id: `peer-${scenario}`, name: "Write", args: { path: permissionGate?.path ?? deniedPath, content: "must never be written" } }]; + for (const tool of tools) assert.ok(value.tools.some(declared => declared.function?.name === tool.name), `native request discloses ${tool.name}`); + complete(response, [ + { choices: [{ index: 0, delta: { content: "Before tool café." } }] }, + { choices: [{ index: 0, delta: { tool_calls: tools.map((tool, index) => ({ index, id: tool.id, type: "function", function: { name: tool.name, arguments: JSON.stringify(tool.args).slice(0, 1) } })) } }] }, + ...tools.map((tool, index) => ({ choices: [{ index: 0, delta: { tool_calls: [{ index, function: { arguments: JSON.stringify(tool.args).slice(1) } }] } }] })), + { choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] }, + ]); + } else { + const result = value.messages.find(message => message.role === "tool" && message.tool_call_id === `peer-${scenario}`); + assert.ok(result, "the exact native tool result returned to the model"); + if (scenario === "echo") { + assert.ok(JSON.stringify(result).includes(toolReceipt), `MCP receiver result reached the next native request: ${JSON.stringify({ result, mcpCalls: mcpCalls.length, permissions })}`); + assert.ok(value.messages.some(message => message.role === "tool" && message.tool_call_id === "peer-empty" && JSON.stringify(message).includes("EMPTY_TOOL_ACCEPTED")), "zero-argument MCP result returned to the native model"); + } + else if (scenario === "bash") assert.match(result.content, /^(?:Wall time: [0-9.]+ seconds\n)?KIMI_CHILD_CREDENTIAL_FENCE_OK\n?$/, "native Bash excluded provider/private credentials and retained the assigned task variable"); + else assert.match(JSON.stringify(result), /denied|rejected|approval/i); + complete(response, [{ choices: [{ index: 0, delta: { content: scenario === "echo" ? "PEER_ECHO_OK café" : scenario === "bash" ? "PEER_BASH_OK" : "PEER_DENIED_OK" } }] }, + { choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 12, completion_tokens: 3, total_tokens: 15 } }]); + } + return; + } + assert.equal(request.url, "/mcp"); + if (value.id === undefined) { response.writeHead(202); response.end(); return; } + if (value.method === "tools/call") { + assert.ok(["echo_peer", "empty_peer"].includes(value.params.name)); + assert.deepEqual(value.params.arguments, value.params.name === "echo_peer" ? argumentsObject : {}); + mcpCalls.push(value.params); + } + const result = value.method === "initialize" + ? { protocolVersion: value.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "simulated-peer", version: "1" } } + : value.method === "tools/list" ? { tools: [{ name: "echo_peer", description: "Echo the supplied JSON through the controlled peer.", + inputSchema: { type: "object", properties: { payload: { type: "string" }, nested: { type: "object", additionalProperties: true } }, required: ["payload", "nested"] } }, + { name: "empty_peer", description: "Return a receipt from an empty object argument.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }] } + : { content: [{ type: "text", text: value.params.name === "empty_peer" ? "EMPTY_TOOL_ACCEPTED" : JSON.stringify({ echoed: argumentsObject, receipt: toolReceipt }) }] }; + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ jsonrpc: "2.0", id: value.id, result })); + } catch (error) { + peerFailure ??= error; + if (!response.headersSent) response.writeHead(400, { "content-type": "application/json" }); + response.end(JSON.stringify({ error: { type: "peer_assertion_failed", message: "simulated peer assertion failed" } })); + } + }); + const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"]; + const previous = keys.map(key => process.env[key]); + const closeOwner = async owner => { + if (owner.closed) return; + owner.closed = true; + const started = Date.now(); + const terminate = setTimeout(() => { for (const child of owner.children) if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); }, 2_000); + const kill = setTimeout(() => { for (const child of owner.children) if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); }, 3_000); + try { + if (owner.handle) await owner.runtime.close({ handle: owner.handle, reason: "simulated peer cleanup" }); + } finally { + try { + await Promise.all(owner.completions); + assert.ok(Date.now() - started < 5_000, "native process cleanup is bounded"); + } finally { clearTimeout(terminate); clearTimeout(kill); await owner.lease.close(); } + } + }; + try { + await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); + const endpoint = `http://127.0.0.1:${server.address().port}`; + const config = kimiManagedConfiguration(selectedModel).replace(/^base_url = .+$/m, `base_url = ${JSON.stringify(`${endpoint}/v1`)}`) + + "\n[loop_control]\nmax_attempts_per_step = 1\n"; + await writeFile(join(home, "config.toml"), config, { mode: 0o600 }); + process.env[keys[0]] = packageRoot; process.env[keys[1]] = join(packageRoot, "package.json"); + const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi); + const store = createFileSessionStore({ stateDir: join(root, "acpx-state") }); + const spawnNative = (lease, owner) => { + const child = lease.spawn([], { cwd: workspace, stdio: "pipe", env: { PATH: "/usr/bin:/bin", HOME: home, + LANG: "C.UTF-8", CI: "1", NO_COLOR: "1", TERM: "dumb", KIMI_CODE_HOME: home, + KIMI_API_KEY: "simulated-peer-not-a-credential", KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_DISABLE_TELEMETRY: "1", + KIMI_CODE_WATCH: "0", KIMI_DISABLE_CRON: "1", PAPERCLIP_KIMI_INSTRUCTIONS: binding.payload, + PAPERCLIP_TASK_SCOPED: "assigned-task-canary" } }); + owner.children.push(child); + owner.completions.push(new Promise((resolve, reject) => { child.once("close", resolve); child.once("error", reject); })); + return child; + }; + const connect = async (resumeSessionId, sessionKey = "scripted-kimi-peer") => { + const lease = await installation.openCommand(); + const admission = instructionModule.createKimiInstructionAdmission(instructions); + const owner = { lease, children: [], completions: [], handle: undefined, runtime: undefined, closed: false }; + owners.push(owner); + owner.runtime = createAcpRuntime({ cwd: workspace, sessionStore: store, + agentRegistry: { resolve: () => "paperclip-verified-kimi-command", list: () => ["kimi"] }, + mcpServers: [{ type: "http", name: "peer", url: `${endpoint}/mcp`, headers: [] }], + permissionMode: "approve-all", permissionPolicy: { defaultAction: "escalate" }, timeoutMs: 15_000, + protocolGuardFactory: () => admission.createGuard(), + onAcpMessage: (direction, message) => nativeMessages.push({ owner, direction, message }), + onPermissionRequest: async (request, context) => { + permissions.push(request.raw); + assert.ok(context.responseDelivery instanceof Promise); + assert.ok(request.raw.options.every(option => option.kind !== "allow_always")); + const normalized = normalizeAcpxPermission(request, { provider: "kimi", workingDirectory: workspace }); + if (request.raw.toolCall.title === "Write") { + assert.equal(request.raw.toolCall.kind, "edit", "native approval carries the actual tool's canonical kind"); + const path = request.raw.toolCall.rawInput.path; + assert.equal(normalized.title, `Change file: ${path.slice(workspace.length + 1)}`); + assert.equal(normalized.kind, "edit"); + assert.deepEqual(normalized.choices.map(choice => choice.key), ["accept", "decline", "cancel"]); + for (const workingDirectory of [undefined, home]) { + const unsafe = normalizeAcpxPermission(request, { provider: "kimi", workingDirectory }); + assert.equal(unsafe.title, "File change requested; target unavailable. Deny or cancel this request."); + assert.deepEqual(unsafe.choices.map(choice => choice.key), ["decline", "cancel"]); + assert.throws(() => unsafe.resolve({ action: "accept" }), /not an offered choice/); + } + } + if (permissionGate) { + assert.equal(request.raw.toolCall.title, "Write"); + assert.deepEqual(request.raw.toolCall.rawInput, { path: permissionGate.path, content: "must never be written" }); + permissionGate.context = context; + permissionGate.delivery = context.responseDelivery.then(() => "delivered", () => "suppressed"); + permissionGate.requestId = nativeMessages.findLast(entry => entry.owner === owner && entry.direction === "inbound" + && entry.message.method === "session/request_permission" + && entry.message.params.toolCall.rawInput?.path === permissionGate.path)?.message.id; + assert.ok(permissionGate.requestId !== undefined); + permissionGate.markSeen(); + return permissionGate.decision; + } + deliveries.push(context.responseDelivery); + if (request.raw.toolCall.title === "Write") { + assert.deepEqual(request.raw.toolCall.rawInput, { path: deniedPath, content: "must never be written" }); + await assert.rejects(access(deniedPath), { code: "ENOENT" }); + return { outcome: "reject_once" }; + } + if (request.raw.toolCall.title === "Bash") { + assert.deepEqual(request.raw.toolCall.rawInput, bashArguments); + return { outcome: "allow_once" }; + } + assert.ok(["mcp__peer__echo_peer", "mcp__peer__empty_peer"].includes(request.raw.toolCall.title)); + assert.deepEqual(request.raw.toolCall.rawInput, request.raw.toolCall.title === "mcp__peer__echo_peer" ? argumentsObject : {}); + return { outcome: "allow_once" }; + }, + spawnAgent: () => spawnNative(lease, owner), + }); + owner.handle = await owner.runtime.ensureSession({ sessionKey, agent: "kimi", mode: "persistent", cwd: workspace, + ...(resumeSessionId ? { resumeSessionId } : {}), + sessionOptions: { model: selectedModel } }); + return owner; + }; + const prompt = async (owner, name) => { + scenario = name; modelStep = 0; + const turn = owner.runtime.startTurn({ handle: owner.handle, mode: "prompt", text: `Controlled ${name} boundary.`, requestId: `peer-${name}` }); + const events = []; + const drained = (async () => { for await (const event of turn.events) events.push(event); })(); + const result = await turn.result; + await drained; + if (peerFailure) throw peerFailure; + if (name !== "error") { + const chunks = events.filter(event => event.type === "text_delta" && event.stream === "output"); + assert.deepEqual(chunks.map(event => event.meta?.kind), ["start", "delta", "end:tool_use", "start", "delta", "end:end_turn"]); + assert.ok(chunks.every(event => /^kimi-message-[a-f0-9]{64}$/.test(event.messageId) + && event.meta.origin === "kimi-native-assistant" && event.meta.source === "kimi-turn-step-v1")); + assert.equal(new Set(chunks.map(event => event.messageId)).size, 2); + assert.equal(chunks[0].messageId, chunks[2].messageId); + assert.equal(chunks[3].messageId, chunks[5].messageId); + assert.notEqual(chunks[0].messageId, chunks[3].messageId); + assert.equal(chunks[1].text, "Before tool café."); + assert.ok(chunks.filter(event => event.meta.kind !== "delta").every(event => event.text === "")); + const projection = createKimiMessageProjection(); + const projected = events.map(event => projection.normalize(event)); + projection.settle(); + assert.deepEqual(projected.filter(event => event.assistantMessageBoundary).map(event => event.assistantMessageBoundary), [ + { phase: "start" }, { phase: "end", stopReason: "toolUse" }, + { phase: "start" }, { phase: "end", stopReason: "stop" }, + ]); + } + return { result, events }; + }; + const first = await connect(); + const echoed = await prompt(first, "echo"); + assert.equal(echoed.result.status, "completed"); + assert.equal(mcpCalls.length, 2); + assert.ok(echoed.events.some(event => event.type === "text_delta" && event.text.includes("PEER_ECHO_OK"))); + const shell = await prompt(first, "bash"); + assert.equal(shell.result.status, "completed"); + assert.ok(shell.events.some(event => event.type === "text_delta" && event.text.includes("PEER_BASH_OK"))); + assert.ok(nativeMessages.some(entry => entry.direction === "outbound" && entry.message.method === "initialize" && entry.message.params.clientCapabilities.terminal === true), "actual ACPX offered its mandatory client terminal"); + assert.ok(!nativeMessages.some(entry => entry.direction === "inbound" && entry.message.method === "terminal/create"), "Kimi kept Bash in its native fenced process service despite ACPX client terminal capability"); + await closeOwner(first); + const second = await connect(first.handle.backendSessionId); + const denied = await prompt(second, "deny"); + assert.equal(denied.result.status, "completed"); + await assert.rejects(access(deniedPath), { code: "ENOENT" }); + const failed = await prompt(second, "error"); + assert.equal(failed.result.status, "failed"); + assert.match(JSON.stringify(failed.result), /Kimi provider turn failed/); + assert.ok(!JSON.stringify(failed.result).includes("private-peer-error-canary")); + assert.ok(nativeMessages.some(entry => entry.direction === "outbound" && ["session/load", "session/resume"].includes(entry.message.method)), "ACPX restored the native session before the second model turn"); + assert.ok(permissions.some(request => request.toolCall.title === "Write")); + assert.equal(permissions.length, 4); + await Promise.all(deliveries); + assert.equal(modelRequests.length, 7); + await closeOwner(second); + assert.ok(owners.every(owner => owner.children.every(child => child.exitCode !== null || child.signalCode !== null))); + await assert.rejects(access(deniedPath), { code: "ENOENT" }); + t.diagnostic("SIMULATED provider service; real pinned Kimi 2.3.3 + ACPX manager: 7 native model requests with provider auth intact, exact create/resume instruction suffix, native UUID start/end separates pre-tool commentary from final text despite reused model response IDs, actual provider projection yields toolUse/stop boundaries, two MCP JSON/result round trips including terminal {}, native Bash excludes provider/private credentials and retains an assigned task variable, rejected Write with no file, typed provider failure, owned process closure; zero external model calls"); + if (process.env.PAPERCLIP_TEST_KIMI_LIFECYCLE_PEER === "1") { + const { readProcessTable, observeDescendantProcessTree } = await tsImport("../../../tests/runner-e2e/process-tree.ts", import.meta.url); + const bounded = async (promise, label, milliseconds = 5_000) => { + let timer; + try { + return await Promise.race([promise, new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} exceeded its bound`)), milliseconds); + })]); + } finally { clearTimeout(timer); } + }; + const nativeWorker = async owner => { + assert.equal(owner.children.length, 1); + const rootPid = owner.children[0].pid; + const table = await readProcessTable(); + assert.ok(table); + const tree = observeDescendantProcessTree(table, rootPid); + const candidates = tree.members.filter(member => member.depth === 1); + assert.equal(candidates.length, 1, `one native Kimi worker belongs directly to the verified command root: ${JSON.stringify(tree.members.map(member => ({ depth: member.depth, kind: member.process.kind })))}`); + const rootProcess = table.find(row => row.pid === rootPid); + const worker = candidates[0].process; + assert.ok(rootProcess && worker.pid > 1 && worker.pid !== rootPid && worker.pid !== process.pid); + const fresh = await readProcessTable(); + assert.ok(fresh?.some(row => row.pid === rootPid && row.started === rootProcess.started && row.processGroupId === rootProcess.processGroupId)); + assert.ok(fresh?.some(row => row.pid === worker.pid && row.started === worker.started + && row.parentPid === rootPid && row.processGroupId === worker.processGroupId)); + return worker; + }; + const assertRetired = async worker => { + const deadline = Date.now() + 5_000; + while (Date.now() < deadline) { + const table = await readProcessTable(); + assert.ok(table); + if (!table.some(row => row.pid === worker.pid && row.started === worker.started)) return; + await new Promise(resolve => setTimeout(resolve, 25)); + } + assert.fail("the observed native worker survived owned cleanup"); + }; + for (const boundary of ["stop", "loss"]) { + const path = join(workspace, `${boundary}-must-not-exist.txt`); + let markSeen, decide; + const seen = new Promise(resolve => { markSeen = resolve; }); + const decision = new Promise(resolve => { decide = resolve; }); + permissionGate = { path, markSeen, decision, decide }; + const owner = await connect(undefined, `scripted-kimi-peer-${boundary}`); + scenario = boundary; modelStep = 0; + const turn = owner.runtime.startTurn({ handle: owner.handle, mode: "prompt", text: `Controlled pending permission ${boundary}.`, requestId: `peer-${boundary}` }); + const drained = (async () => { for await (const _event of turn.events) {} })(); + try { + await bounded(seen, `${boundary} native permission observation`); + await assert.rejects(access(path), { code: "ENOENT" }); + const worker = await nativeWorker(owner); + if (boundary === "stop") await bounded(turn.cancel({ reason: "controlled pending permission Stop" }), "native Stop"); + else process.kill(worker.pid, "SIGKILL"); + const result = await bounded(turn.result, `${boundary} terminal result`); + await bounded(drained, `${boundary} event drain`); + if (peerFailure) throw peerFailure; + assert.equal(result.status, boundary === "stop" ? "cancelled" : "failed"); + if (boundary === "loss") assert.equal(typeof result.error?.code, "string"); + assert.equal(permissionGate.context.signal.aborted, true, "pending host callback was revoked at settlement"); + const countAtSettlement = modelRequests.length; + permissionGate.decide({ outcome: "allow_once" }); + assert.equal(await bounded(permissionGate.delivery, `${boundary} stale permission suppression`), "suppressed"); + await new Promise(resolve => setTimeout(resolve, 100)); + assert.equal(modelRequests.length, countAtSettlement, "a stale callback cannot restart provider inference"); + assert.ok(!nativeMessages.some(entry => entry.owner === owner && entry.direction === "outbound" && entry.message.id === permissionGate.requestId && entry.message.result?.outcome?.outcome === "selected"), "no stale allow response crossed the provider connection"); + await assert.rejects(access(path), { code: "ENOENT" }); + await closeOwner(owner); + await assertRetired(worker); + await assert.rejects(access(path), { code: "ENOENT" }); + } finally { permissionGate.decide({ outcome: "reject_once" }); permissionGate = undefined; } + } + assert.equal(modelRequests.length, 9); + assert.equal(permissions.length, 6); + t.diagnostic("SIMULATED provider service; pending native Write permission Stop and exact observed native-worker SIGKILL both revoked host callbacks, suppressed stale allow responses, produced no file/model continuation, settled typed results and retired owned native processes; zero external model calls"); + } + } finally { + try { await Promise.all(owners.map(closeOwner)); } + finally { + server.closeAllConnections(); + await new Promise(resolve => server.close(resolve)); + keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; }); + await rm(root, { recursive: true, force: true }); + } + } +}); diff --git a/packages/paperclip-runner/scripts/provision-kimi-package.test.mjs b/packages/paperclip-runner/scripts/provision-kimi-package.test.mjs new file mode 100644 index 00000000000..1afbc0627b5 --- /dev/null +++ b/packages/paperclip-runner/scripts/provision-kimi-package.test.mjs @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +import { execFile, spawn, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { once } from "node:events"; +import { mkdtemp, mkdir, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { setTimeout as delay } from "node:timers/promises"; +import { promisify } from "node:util"; +import { bundleKimiProvisioner } from "./build-verified-provider-entrypoints.mjs"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/kimi-closure-pins.ts"; + +const run = promisify(execFile); + +test("installed Kimi setup is self-contained, rejects corrupt assets and cancels pending downloads", { timeout: 10_000 }, async t => { + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-public-layout-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const pkg = join(root, "package"), cli = join(pkg, "dist/vendor/paperclip-runner/cli"); + await mkdir(cli, { recursive: true }); + await writeFile(join(pkg, "package.json"), '{"name":"@paperclipai/server","type":"module"}'); + await writeFile(join(pkg, "dist/index.js"), 'throw Error("server must not start");'); + await writeFile(join(cli, "acpx-runtime-sidecar.cjs"), "// installed layout fixture"); + const entry = join(cli, "provision-kimi.cjs"); + const packageRoot = resolve(dirname(new URL(import.meta.url).pathname), ".."); + const result = await bundleKimiProvisioner({ outputRoot: cli }); + for (const output of Object.values(result.metafile.outputs)) for (const dependency of output.imports) assert.match(dependency.path, /^node:/); + assert.deepEqual((await readdir(join(cli, "kimi-provision-inputs"))).sort(), ["package-lock.json", "package.json"]); + for (const name of ["package-lock.json", "package.json"]) assert.deepEqual(await readFile(join(cli, "kimi-provision-inputs", name)), await readFile(join(packageRoot, "scripts/kimi-distribution", name))); + const setup = createRequire(import.meta.url)(entry); + const layout = await setup.provisionKimiPackageRoot(entry); + assert.equal(layout.root, pkg); + assert.equal(layout.assetRoot, join(pkg, "dist/vendor/paperclip-runner")); + const linked = join(cli, "linked.cjs"); await symlink(entry, linked); + await assert.rejects(setup.provisionKimiPackageRoot(linked), /installed entrypoint/); + assert.deepEqual(setup.provisionEnvironment({ PATH: "/usr/bin:/bin", HTTPS_PROXY: "http://127.0.0.1:9", KIMI_MODEL_API_KEY: "sensitive-canary", HOME: "/private/home", NODE_OPTIONS: "--require /foreign", NODE_PATH: "/foreign", NPM_TOKEN: "sensitive-canary" }), { PATH: "/usr/bin:/bin", LANG: "C.UTF-8", HTTPS_PROXY: "http://127.0.0.1:9" }); + const target = `${process.platform}-${process.arch}`; + const parent = join(layout.assetRoot, "provider-assets/kimi"); + await mkdir(join(parent, target, "runtime"), { recursive: true }); + const deny = join(root, "deny.cjs"); + await writeFile(deny, `process.nextTick(()=>{const assert=require('node:assert/strict');for(const key of ['KIMI_MODEL_API_KEY','NPM_TOKEN','HOME','NODE_OPTIONS','NODE_PATH'])assert.equal(process.env[key],undefined,key)});const fail=()=>{throw Error('UNEXPECTED_NETWORK_OR_CHILD')};globalThis.fetch=fail;for(const m of ['node:net','node:tls','node:http','node:https']){const x=require(m);for(const key of ['connect','createConnection','request','get'])if(key in x)x[key]=fail}const c=require('node:child_process');for(const key of ['spawn','execFile','exec'])c[key]=fail;`); + const child = spawnSync(process.execPath, ["--require", deny, entry], { env: { PATH: "/usr/bin:/bin", KIMI_MODEL_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_OPTIONS: "", NODE_PATH: "/foreign" }, encoding: "utf8", timeout: 10_000 }); + assert.ifError(child.error); assert.equal(child.status, 1); assert.match(child.stderr, /Kimi setup failed/); + assert.doesNotMatch(child.stderr + child.stdout, /UNEXPECTED_NETWORK_OR_CHILD|sensitive-canary/); + assert.deepEqual(await readdir(parent), [target]); + assert.equal(JSON.parse(await readFile(join(pkg, "package.json"), "utf8")).name, "@paperclipai/server"); + await rm(join(parent, target), { recursive: true }); + const ready = join(root, "download-ready"), aborted = join(root, "download-aborted"); + const preload = join(root, "pending-download.cjs"); + await writeFile(preload, `globalThis.fetch=(_url,{signal})=>new Promise((_resolve,reject)=>{const pending=setInterval(()=>{},1000);require('node:fs').writeFileSync(${JSON.stringify(ready)},'ready');signal.addEventListener('abort',()=>{clearInterval(pending);require('node:fs').writeFileSync(${JSON.stringify(aborted)},'aborted');reject(signal.reason)},{once:true})});`); + const cancelled = spawn(process.execPath, ["--require", preload, entry], { env: { PATH: "/usr/bin:/bin" }, stdio: ["ignore", "ignore", "pipe"] }); + const closed = once(cancelled, "close"); + let stderr = ""; cancelled.stderr.on("data", chunk => { stderr = (stderr + chunk).slice(-4096); }); + const kill = setTimeout(() => cancelled.kill("SIGKILL"), 5_000); + try { + for (let attempt = 0; attempt < 100; attempt++) { + try { await readFile(ready); break; } + catch (error) { if (error.code !== "ENOENT") throw error; await delay(10); } + } + assert.equal(await readFile(ready, "utf8"), "ready"); + cancelled.kill("SIGTERM"); + const [code, signal] = await closed; + assert.equal(code, 1); assert.equal(signal, null); + assert.match(stderr, /Kimi setup failed/); + assert.equal(await readFile(aborted, "utf8"), "aborted"); + assert.deepEqual(await readdir(parent), []); + } finally { clearTimeout(kill); if (cancelled.exitCode === null && cancelled.signalCode === null) { cancelled.kill("SIGKILL"); await closed; } } +}); + +test("fresh archived installed provisioner downloads the exact closure, reuses it offline and rejects tampering", { + skip: process.env.PAPERCLIP_TEST_KIMI_PUBLIC_SETUP !== "1" && "Set PAPERCLIP_TEST_KIMI_PUBLIC_SETUP=1 for public registry downloads; no model calls or provider credentials", + timeout: 180_000, +}, async t => { + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-public-setup-"))); + t.after(() => rm(root, { recursive: true, force: true })); + const packed = join(root, "packed"), pkg = join(packed, "package"), cli = join(pkg, "dist/vendor/paperclip-runner/cli"); + await mkdir(cli, { recursive: true }); + await writeFile(join(pkg, "package.json"), '{"name":"@paperclipai/server","type":"module"}'); + await writeFile(join(pkg, "dist/index.js"), 'throw Error("server must not start");'); + await writeFile(join(cli, "acpx-runtime-sidecar.cjs"), "// installed layout fixture"); + await bundleKimiProvisioner({ outputRoot: cli }); + const archive = join(root, "setup.tgz"), installed = join(root, "installed"); + await run("tar", ["-czf", archive, "-C", packed, "package"], { timeout: 10_000 }); + await mkdir(installed); + await run("tar", ["-xzf", archive, "-C", installed], { timeout: 10_000 }); + const entry = join(installed, "package/dist/vendor/paperclip-runner/cli/provision-kimi.cjs"); + const api = createRequire(import.meta.url)(entry); + const environment = api.provisionEnvironment(process.env); + const fresh = await run(process.execPath, ["--use-env-proxy", entry], { env: { ...environment, KIMI_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/foreign-home" }, timeout: 120_000, maxBuffer: 1024 * 1024 }); + assert.doesNotMatch(fresh.stdout + fresh.stderr, /sensitive-canary/); + const receipt = JSON.parse(fresh.stdout.trim()); + assert.equal(receipt.status, "installed_verified"); + const target = `${process.platform}-${process.arch}`; + const assets = join(installed, "package/dist/vendor/paperclip-runner/provider-assets/kimi", target); + const metadata = JSON.parse(await readFile(join(assets, "kimi-distribution.json"), "utf8")); + assert.equal(metadata.nativeClosureSha256, KIMI_DISTRIBUTION_CLOSURE_SHA256[target]); + const offline = { ...environment, HTTPS_PROXY: "http://127.0.0.1:9", HTTP_PROXY: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9", http_proxy: "http://127.0.0.1:9", NO_PROXY: "", no_proxy: "" }; + const reused = await run(process.execPath, ["--use-env-proxy", entry], { env: offline, timeout: 20_000, maxBuffer: 1024 * 1024 }); + assert.equal(JSON.parse(reused.stdout.trim()).status, "verified_existing"); + const worker = join(assets, "runtime/node_modules/@moonshot-ai/kimi-code/dist/search-worker.mjs"); + const changed = Buffer.concat([await readFile(worker), Buffer.from("\n// integrity test\n")]); + await writeFile(worker, changed); + await assert.rejects(run(process.execPath, ["--use-env-proxy", entry], { env: offline, timeout: 20_000, maxBuffer: 1024 * 1024 }), error => { + assert.match(error.stderr, /Kimi setup failed/); + assert.doesNotMatch(error.stderr, /download|ECONNREFUSED/i); + return true; + }); + assert.deepEqual(await readFile(worker), changed, "Invalid existing assets must not be silently replaced"); + t.diagnostic(JSON.stringify({ target, closure: metadata.nativeClosureSha256, patch: metadata.patch.version, provisionerSha256: createHash("sha256").update(await readFile(entry)).digest("hex"), freshInstalled: true, offlineReuse: true, tamperRejected: true, modelPrompts: 0, scope: "self-contained public-layout setup component; full Paperclip tarball is a separate proof" })); +}); diff --git a/packages/paperclip-runner/scripts/provision-kimi.mjs b/packages/paperclip-runner/scripts/provision-kimi.mjs new file mode 100644 index 00000000000..4db8a1a1037 --- /dev/null +++ b/packages/paperclip-runner/scripts/provision-kimi.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node +import { constants } from "node:fs"; +import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { materializeKimiDistribution, kimiDistributionEnvironment } from "./materialize-kimi-distribution.mjs"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/kimi-closure-pins.ts"; +import { verifyKimiInstallation } from "../src/drivers/acpx/kimi-installation.ts"; +import { QUALIFIED_ACPX_PROFILES } from "../src/drivers/acpx/qualified-profiles.ts"; + +export function provisionEnvironment(source = process.env) { + return { ...kimiDistributionEnvironment(source), LANG: "C.UTF-8", PATH: source.PATH ?? "/usr/bin:/bin" }; +} + +export async function provisionKimiPackageRoot(entrypoint) { + const canonical = await realpath(entrypoint); + if (canonical !== resolve(entrypoint) || basename(canonical) !== "provision-kimi.cjs" || !(await lstat(canonical)).isFile()) throw new Error("Kimi setup requires its installed entrypoint"); + const cli = dirname(canonical); + const vendored = cli.endsWith("/dist/vendor/paperclip-runner/cli"); + if (!vendored && !cli.endsWith("/dist/cli")) throw new Error("Kimi setup requires the installed runner or server layout"); + const root = resolve(cli, vendored ? "../../../.." : "../.."); + const manifest = join(root, "package.json"); + const file = await open(manifest, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const before = await file.stat({ bigint: true }); + if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > 64n * 1024n) throw new Error("Kimi setup package manifest is invalid"); + const bytes = await file.readFile(), after = await file.stat({ bigint: true }), named = await lstat(manifest, { bigint: true }); + if (bytes.length !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("Kimi setup package manifest changed"); + if (JSON.parse(bytes.toString("utf8")).name !== (vendored ? "@paperclipai/server" : "@paperclipai/paperclip-runner")) throw new Error("Kimi setup package identity does not match its layout"); + } finally { await file.close(); } + return { root, manifest, cli, assetRoot: vendored ? resolve(cli, "..") : root }; +} + +async function verifyInstallation(root, manifest) { + const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"], previous = keys.map(key => process.env[key]); + process.env[keys[0]] = root; process.env[keys[1]] = manifest; + try { + const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi); + const lease = await installation.openCommand(); + await lease.close(); + } finally { keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; }); } +} + +async function absent(path) { try { await lstat(path); return false; } catch (error) { if (error.code === "ENOENT") return true; throw error; } } +async function containedDirectory(root, path) { + await mkdir(path, { recursive: true, mode: 0o700 }); + if (await realpath(path) !== path || !(await lstat(path)).isDirectory() || !path.startsWith(root + "/")) throw new Error("Kimi setup asset directory escapes its package"); +} + +export async function provisionKimi(entrypoint, checkCancelled = () => {}, signal) { + checkCancelled(); signal?.throwIfAborted(); + const target = `${process.platform}-${process.arch}`; + if (!Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, target)) throw new Error(`Kimi is not supported on ${target}`); + const { root, manifest, cli, assetRoot } = await provisionKimiPackageRoot(entrypoint); + const assets = join(assetRoot, "provider-assets"), parent = join(assets, "kimi"); + await containedDirectory(root, assets); await containedDirectory(root, parent); + const lockPath = join(parent, `.setup-${target}.lock`); + const lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600); + let lockIdentity, temporary, temporaryIdentity, published, committed = false; + const output = join(parent, target); + try { + lockIdentity = await lock.stat({ bigint: true }); + checkCancelled(); + if (!(await absent(output))) { + await verifyInstallation(root, manifest); + return { status: "verified_existing", target, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest }; + } + temporary = await mkdtemp(join(parent, ".setup-private-")); temporaryIdentity = await lstat(temporary, { bigint: true }); + const stagingRoot = join(temporary, "package"); await mkdir(stagingRoot, { mode: 0o700 }); + await writeFile(join(stagingRoot, "package.json"), JSON.stringify({ name: "@paperclipai/paperclip-runner" }), { flag: "wx", mode: 0o600 }); + const stagedOutput = join(stagingRoot, "provider-assets/kimi", target); + await materializeKimiDistribution({ outputRoot: stagedOutput, lockDirectory: join(cli, "kimi-provision-inputs"), checkCancelled, signal }); + await verifyInstallation(stagingRoot, join(stagingRoot, "package.json")); + if (!(await absent(output))) throw new Error("Kimi setup destination appeared during installation; refusing replacement"); + checkCancelled(); + await mkdir(output, { mode: 0o700 }); published = await lstat(output, { bigint: true }); + for (const name of await readdir(stagedOutput)) { + const named = await lstat(output, { bigint: true }); + if (named.dev !== published.dev || named.ino !== published.ino || named.isSymbolicLink()) throw new Error("Kimi setup output ownership changed during publication"); + await rename(join(stagedOutput, name), join(output, name)); + } + await verifyInstallation(root, manifest); + checkCancelled(); committed = true; + return { status: "installed_verified", target, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest }; + } finally { + const cleanup = []; + if (published && !committed) cleanup.push((async () => { + const named = await lstat(output, { bigint: true }); + if (named.dev !== published.dev || named.ino !== published.ino || named.isSymbolicLink()) throw new Error("Kimi setup output ownership changed; refusing cleanup"); + await rm(output, { recursive: true }); + })()); + if (temporary) cleanup.push((async () => { + const named = await lstat(temporary, { bigint: true }); + if (!temporaryIdentity || named.dev !== temporaryIdentity.dev || named.ino !== temporaryIdentity.ino || named.isSymbolicLink()) throw new Error("Kimi setup staging ownership changed; refusing cleanup"); + await rm(temporary, { recursive: true }); + })()); + cleanup.push((async () => { + try { + lockIdentity ??= await lock.stat({ bigint: true }); const named = await lstat(lockPath, { bigint: true }); + if (named.dev !== lockIdentity.dev || named.ino !== lockIdentity.ino) throw new Error("Kimi setup lock ownership changed; refusing cleanup"); + await unlink(lockPath); + } finally { await lock.close(); } + })()); + const results = await Promise.allSettled(cleanup), failed = results.filter(result => result.status === "rejected"); + if (failed.length) throw new AggregateError(failed.map(result => result.reason), "Kimi setup cleanup failed; inspect the package before retrying"); + } +} + +if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { + if (process.argv.length !== 2) throw new Error("Usage: paperclipai runtime setup kimi (explicit host installation; no model calls)"); + const environment = provisionEnvironment(); + for (const key of Object.keys(process.env)) delete process.env[key]; Object.assign(process.env, environment); + const controller = new AbortController(); + const cancel = () => controller.abort(); + process.on("SIGINT", cancel); process.on("SIGTERM", cancel); + const deadline = setTimeout(cancel, 900_000); deadline.unref(); + provisionKimi(fileURLToPath(import.meta.url), () => { if (controller.signal.aborted) throw new Error("Kimi setup cancelled; no installation was admitted"); }, controller.signal).finally(() => { + clearTimeout(deadline); process.off("SIGINT", cancel); process.off("SIGTERM", cancel); + }).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(`Kimi setup failed: ${error.message}`); process.exitCode = 1; }); +} diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts index b35ca3dae1a..5543589604d 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.test.ts @@ -1,4 +1,5 @@ import { bindAcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../drivers/acpx/kimi-closure-pins.js"; import { ANTIGRAVITY_DISTRIBUTION_PINS } from "../drivers/acpx/generated-profiles.js"; import { canonicalNativeRuntimeContextDigest } from "../contracts/runtime-context.js"; import { acpxProfileActivity } from "../drivers/acpx/profile-activity.js"; @@ -15,6 +16,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { createAcpxSidecarHostEnvironment, createSanitizedAcpxSpawnInput } from "../drivers/acpx/environment.js"; +import { createKimiMessageProjection } from "../drivers/acpx/kimi-message-boundary.js"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -563,6 +565,31 @@ describe("qualified ACPX runtime sidecar", () => { expect(wire.writes).toEqual([]); expect(wire.sequence()).toBe(0); }); + it("preserves validated native assistant boundaries without exposing Kimi provenance metadata", () => { + const source = readFileSync(fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)), "utf8"); + const start = source.indexOf(' if (event.type === "text_delta") {', source.indexOf("function sanitizeRuntimeEvent")); + const end = source.indexOf(' if (event.type === "status") {', start); + const sanitize = new Function("boundedOptionalText", "stableProviderIdentity", `return event => {${source.slice(start, end)} return null;}`)( + (value: string) => value, (value: string) => `stable-${value}`, + ); + const projection = createKimiMessageProjection<{ type: string; text: string; stream: string; messageId: string; meta: Record }>(); + const event = (kind: string, text = "", stream = "output") => ({ type: "text_delta", text, stream, + messageId: `kimi-message-${"a".repeat(64)}`, + meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind, secret: "PRIVATE_PROVENANCE" } }); + const frames = [sanitize(projection.normalize(event("start"))), + sanitize(projection.normalize(event("delta", "Working."))), + sanitize(projection.normalize(event("delta", "Private reasoning", "thought"))), + sanitize(projection.normalize(event("end:tool_use")))]; + expect(frames[0]).toMatchObject({ text: "", assistantMessageBoundary: { phase: "start" } }); + expect(frames[1]).toMatchObject({ type: "text_delta", text: "Working." }); + expect(frames[2]).toMatchObject({ type: "thinking" }); + expect(frames[3]).toMatchObject({ text: "", assistantMessageBoundary: { phase: "end", stopReason: "toolUse" } }); + expect(new Set(frames.map(frame => frame.messageId)).size).toBe(1); + expect(JSON.stringify(frames)).not.toContain("PRIVATE_PROVENANCE"); + expect(JSON.stringify(frames)).not.toContain("kimi-native-assistant"); + projection.settle(); + }); + it("passes only validated Pi native boundaries and history through the real text sanitizer", () => { const source = readFileSync(fileURLToPath(new URL("./acpx-runtime-sidecar.ts", import.meta.url)), "utf8"); const start = source.indexOf(' if (event.type === "text_delta") {', source.indexOf("function sanitizeRuntimeEvent")); @@ -1258,17 +1285,56 @@ describe("qualified ACPX runtime sidecar", () => { }); it.each([ - ["cursor", "explicit-cursor-model"], - ["copilot", "explicit-copilot-model"], - ] as const)("initializes the declared %s profile with its current admission status", async (agent, model) => { + ["cursor", "explicit-cursor-model", "qualified"], + ["copilot", "explicit-copilot-model", "pending"], + ["kimi", "explicit-kimi-model", resolveQualifiedAcpxProfile("kimi", "explicit-kimi-model").qualificationStatus === "pending" ? "pending" : "qualified"], + ] as const)("initializes the declared %s profile with its current admission status", async (agent, model, qualification) => { const sidecar = startSidecar(); + const profile = resolveQualifiedAcpxProfile(agent, model); sidecar.write(initializeRequest(1, agent, model)); const frame = await sidecar.next((value) => value.id === 1); + expect(ACPX_CAPABILITY_PROFILES[agent].qualification).toBe(qualification); + if (agent === "kimi" && profile.qualificationStatus === "pending") { + expect(frame).toMatchObject({ id: 1, ok: false, + error: { message: expect.stringContaining("qualification is pending"), retryable: false } }); + return; + } expect(frame).toMatchObject({ id: 1, ok: true }); const result = frame.result as Record; - expect(result.profile).toEqual(resolveQualifiedAcpxProfile(agent, model)); + expect(result.profile).toEqual(profile); expect(result.profile).toMatchObject({ reportedModelId: model }); - expect(ACPX_CAPABILITY_PROFILES[agent].qualification).toBe(agent === "cursor" ? "qualified" : "pending"); + }); + + it.each([ + ["darwin", "arm64"], ["linux", "x64"], + ] as const)("binds Kimi startup to its declared %s/%s host and session before provider launch", async (platform, architecture) => { + const sidecar = startSidecar(), model = "explicit-kimi-model"; + const profile = resolveQualifiedAcpxProfile("kimi", model); + const request = initializeRequest(1, "kimi", model); + if (profile.qualificationStatus !== "pending") { + sidecar.write(request); + expect(await sidecar.next(frame => frame.id === 1)).toMatchObject({ ok: true, result: { profile } }); + return; + } + const target = `${platform}-${architecture}` as "darwin-arm64" | "linux-x64"; + const candidateAdmission = bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1", + companyId: "company-kimi-candidate", agent: "kimi", model, profileDigest: profile.commandDigest, + nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`, + platform, architecture, expiresAt: Date.now() + 60_000, + }, { runId: "run-kimi-candidate", normalizedSessionId: "session-kimi-candidate" }); + sidecar.write({ ...request, params: { ...request.params, candidateAdmission } }); + const frame = await sidecar.next(value => value.id === 1); + if (process.platform !== platform || process.arch !== architecture) { + expect(frame).toMatchObject({ ok: false, error: { message: expect.stringContaining("candidate admission"), retryable: false } }); + return; + } + expect(frame).toMatchObject({ ok: true, result: { profile } }); + expect((frame.result as Record).profile).toMatchObject({ reportedModelId: model }); + sidecar.write({ protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION, id: 2, command: "session.open", params: { + runtimeDirectory: "/unused", workingDirectory: "/unused", normalizedSessionId: "other-kimi-session", agent: "kimi", model, + candidateAdmission, permissionMode: "deny-all", permissionModePinned: true, providerPolicy: { readOnly: true }, systemInstructions: "", tools: [], + } }); + expect(await sidecar.next(value => value.id === 2)).toMatchObject({ ok: false, error: { message: expect.stringContaining("candidate admission") } }); }); it("refuses pending Antigravity bootstrap before any native process opens", async () => { @@ -1312,7 +1378,7 @@ describe("qualified ACPX runtime sidecar", () => { ok: false, error: { code: "acpx_sidecar_command_failed", - message: "ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity", + message: "ACPX agent must be claude, codex, grok, cursor, copilot, kimi, pi, or antigravity", retryable: false, }, }); @@ -1338,7 +1404,7 @@ function initializeRequest( id: number, agent: string, model = "gpt-5.6-sol", -): Record { +) { return { protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION, id, diff --git a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts index 1ce332b9b91..da96cf45300 100644 --- a/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts +++ b/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts @@ -27,6 +27,7 @@ import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichE import type { PaperclipQuestionSet } from "../contracts/question-set.js"; import { createAcpxToolEventNormalizer, createGrokMessageNormalizer } from "../provider-events.js"; import { createPiMessageProjection, type PiProjectedMessageEvent } from "../drivers/acpx/pi-message-projection.js"; +import { createKimiMessageProjection, type KimiProjectedMessageEvent } from "../drivers/acpx/kimi-message-boundary.js"; import { parseNativeRuntimeContext } from "../contracts/runtime-context.js"; import { PRP_BLOCK_TOOL_NAME, @@ -667,7 +668,8 @@ async function pumpTurn( // the sidecar boundary, matching the in-process ACPX driver path. const normalizeToolEvent = createAcpxToolEventNormalizer(); const piMessages = initializedAgent === "pi" ? createPiMessageProjection() : null; - const normalizeMessage = piMessages ? piMessages.normalize : initializedAgent === "grok" + const kimiMessages = initializedAgent === "kimi" ? createKimiMessageProjection() : null; + const normalizeMessage = kimiMessages ? kimiMessages.normalize : piMessages ? piMessages.normalize : initializedAgent === "grok" ? createGrokMessageNormalizer() : (event: AcpRuntimeEvent) => event; for await (const event of runtimeTurn.events) { toolEvidence?.tool(event); @@ -680,7 +682,7 @@ async function pumpTurn( ); } const result = await runtimeTurn.result; - if (result.status === "completed") piMessages?.settle(); + if (result.status === "completed") { piMessages?.settle(); kimiMessages?.settle(); } await drainExtensions(); try { const usageAfter = await readSidecarHostStatusWithin(activeHost); @@ -1037,7 +1039,7 @@ function boundRuntimeEventForNormalization( } as BoundedRuntimeToolEvent; } -function sanitizeRuntimeEvent(event: PiProjectedMessageEvent): Record { +function sanitizeRuntimeEvent(event: PiProjectedMessageEvent & KimiProjectedMessageEvent): Record { const runtimeType = text(record(event).type); if (runtimeType === "plan") { return { @@ -1053,6 +1055,8 @@ function sanitizeRuntimeEvent(event: PiProjectedMessageEvent): tag: event.tag ?? null, ...(event.piMessageBoundary ? { piMessageBoundary: event.piMessageBoundary } : {}), ...(event.piMessageHistory ? { piMessageHistory: true } : {}), + ...(event.assistantMessageBoundary ? { assistantMessageBoundary: event.assistantMessageBoundary } : {}), + ...(event.assistantMessageHistory ? { assistantMessageHistory: true } : {}), messageId: typeof event.messageId === "string" && event.messageId.length > 0 ? stableProviderIdentity(event.messageId, "message") @@ -1478,8 +1482,8 @@ function requireHost( } function requireQualifiedAgent(value: unknown, model?: string, candidateAdmission?: AcpxCandidateStartupAdmission): QualifiedAcpxAgent { - if (value !== "grok" && value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot" && value !== "antigravity") { - throw new Error("ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity"); + if (value !== "grok" && value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot" && value !== "kimi" && value !== "antigravity") { + throw new Error("ACPX agent must be claude, codex, grok, cursor, copilot, kimi, pi, or antigravity"); } if (candidateAdmission) assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile(value, model ?? ""), candidateAdmission); else assertAcpxProfileReleased(value); diff --git a/packages/paperclip-runner/src/cli/kimi-metadata-probe.ts b/packages/paperclip-runner/src/cli/kimi-metadata-probe.ts new file mode 100644 index 00000000000..e5ce509634c --- /dev/null +++ b/packages/paperclip-runner/src/cli/kimi-metadata-probe.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import { parseAcpxCandidateStartupAdmission, type AcpxCandidateStartupAdmission } from "../drivers/acpx/candidate-admission.js"; +import { probeKimiMetadata } from "../drivers/acpx/kimi-metadata-probe.js"; +import { KIMI_NODE_VERSION } from "../drivers/acpx/kimi-closure-pins.js"; + +async function main() { + const model = process.env.PAPERCLIP_KIMI_PROBE_MODEL ?? "", apiKey = process.env.KIMI_API_KEY ?? ""; + const encodedAdmission = process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION; + const bindings = { PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT, + PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST }; + for (const name of Object.keys(process.env)) delete process.env[name]; + for (const [name, value] of Object.entries(bindings)) if (value !== undefined) process.env[name] = value; + let candidateAdmission: AcpxCandidateStartupAdmission | undefined; + if (encodedAdmission !== undefined) { + if (Buffer.byteLength(encodedAdmission) > 4096) throw new Error("Invalid metadata admission"); + candidateAdmission = parseAcpxCandidateStartupAdmission(JSON.parse(encodedAdmission), { agent: "kimi", model }); + } + const result = process.versions.node !== KIMI_NODE_VERSION + ? { schema: "paperclip.kimi.metadata.v1", status: "failed", code: "KIMI_INSTALLATION_INVALID", + message: "Kimi metadata verification requires the pinned Node runtime.", promptSent: false } + : await probeKimiMetadata(model, apiKey, candidateAdmission); + process.stdout.write(`${JSON.stringify(result)}\n`); + if (result.status !== "verified") process.exitCode = 1; +} + +void main().catch(() => { + process.stdout.write(`${JSON.stringify({ schema: "paperclip.kimi.metadata.v1", status: "failed", code: "KIMI_REQUEST_FAILED", + message: "Kimi Code metadata verification failed.", promptSent: false })}\n`); + process.exitCode = 1; +}); diff --git a/packages/paperclip-runner/src/configured-environment.test.ts b/packages/paperclip-runner/src/configured-environment.test.ts index 77589d9d40b..cb500f5570a 100644 --- a/packages/paperclip-runner/src/configured-environment.test.ts +++ b/packages/paperclip-runner/src/configured-environment.test.ts @@ -72,6 +72,23 @@ describe("configured environment authority", () => { expect(projected.PAPERCLIP_API_KEY).toBeUndefined(); }); + it("reserves Kimi credentials for the selected provider instead of the generic task projection", async () => { + const { createSanitizedAcpxSpawnInput } = await import("./drivers/acpx/environment.js"); + const projected = configuredEnvironmentProjection({ KIMI_API_KEY: "selected-kimi-secret", CUSTOM_FLAG: "allowed" }); + expect(projected.KIMI_API_KEY).toBeUndefined(); + expect(JSON.parse(projected[CONFIGURED_ENVIRONMENT_KEYS]!)).toEqual(["CUSTOM_FLAG"]); + expect(() => configuredEnvironment({ [CONFIGURED_ENVIRONMENT_KEYS]: '["KIMI_API_KEY"]', KIMI_API_KEY: "selected-kimi-secret" })) + .toThrow("Invalid configured environment projection"); + const selected = { ...projected, KIMI_API_KEY: "selected-kimi-secret" }; + expect(createSanitizedCodexEnvironment(selected).KIMI_API_KEY).toBeUndefined(); + for (const agent of ["claude", "codex", "grok", "cursor", "copilot"] as const) { + expect(createSanitizedAcpxSpawnInput(selected, agent).env.KIMI_API_KEY).toBeUndefined(); + } + expect(createSanitizedAcpxSpawnInput(selected, "kimi").env).toMatchObject({ KIMI_API_KEY: "selected-kimi-secret", CUSTOM_FLAG: "allowed" }); + // Pi already supports this credential through its explicit SDK allowlist. + expect(createSanitizedAcpxSpawnInput(selected, "pi").env.KIMI_API_KEY).toBe("selected-kimi-secret"); + }); + it("does not accept an ambient projection in the provider or shell policy", () => { vi.stubEnv(CONFIGURED_ENVIRONMENT_KEYS, JSON.stringify(["PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY"])); vi.stubEnv("PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY", "ambient-secret"); @@ -81,7 +98,7 @@ describe("configured environment authority", () => { } finally { vi.unstubAllEnvs(); } }); - it.each(["not-json", "[1]", '["NODE_OPTIONS"]', '["PAPERCLIP_API_KEY"]', '["PAPERCLIP_RUNNER_BOOTSTRAP_TICKET"]', '["OPENAI_API_KEY"]', '["LD_PRELOAD"]', '["bad=name"]', '["CUSTOM","CUSTOM"]', '["MISSING"]']) + it.each(["not-json", "[1]", '["NODE_OPTIONS"]', '["PAPERCLIP_API_KEY"]', '["PAPERCLIP_RUNNER_BOOTSTRAP_TICKET"]', '["OPENAI_API_KEY"]', '["KIMI_API_KEY"]', '["LD_PRELOAD"]', '["bad=name"]', '["CUSTOM","CUSTOM"]', '["MISSING"]']) ("rejects malformed or reserved projection %s", raw => { expect(() => configuredEnvironment({ [CONFIGURED_ENVIRONMENT_KEYS]: raw })).toThrow("Invalid configured environment"); }); diff --git a/packages/paperclip-runner/src/configured-environment.ts b/packages/paperclip-runner/src/configured-environment.ts index 93e1f8a5d5e..65d8b05ad31 100644 --- a/packages/paperclip-runner/src/configured-environment.ts +++ b/packages/paperclip-runner/src/configured-environment.ts @@ -30,7 +30,7 @@ const RESERVED_KEYS = new Set([ "PYTHONPATH", "PYTHONHOME", "RUBYOPT", "RUBYLIB", "PERL5OPT", "PERL5LIB", "BASH_ENV", "ENV", "ZDOTDIR", "GH_CONFIG_DIR", "OPENAI_API_KEY", "CODEX_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "AWS_BEARER_TOKEN_BEDROCK", "OPENROUTER_API_KEY", "XAI_API_KEY", "CURSOR_API_KEY", "CURSOR_AUTH_TOKEN", - "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN", "PAPERCLIP_GIT_TOKEN", + "COPILOT_GITHUB_TOKEN", "KIMI_API_KEY", "GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN", "PAPERCLIP_GIT_TOKEN", ]); function eligibleKey(key: string): boolean { diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts index aecbdd0e86c..fe1d17a2e10 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.ts @@ -2,7 +2,7 @@ import { resolveGrokDisableWebSearch } from "../drivers/acpx/grok-web-search.js" import { resolveGrokEffort } from "../drivers/acpx/grok-effort.js"; import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js"; import type { DotBindingSnapshot, MuseBindingSnapshot, ExternalBotProviderKind } from "./external-provider.js"; -import { QUALIFIED_ACPX_VERSION } from "../drivers/acpx/generated-profiles.js"; +import { QUALIFIED_ACPX_VERSION, QUALIFIED_ACPX_PROFILE_DATA } from "../drivers/acpx/generated-profiles.js"; import { isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js"; import { isProviderMode } from "./provider-mode.js"; import { createHash } from "node:crypto"; @@ -93,7 +93,11 @@ export interface NativeAwsAgentCoreProfileSnapshot { eventExpiryDays: 90; } -export type NativeAcpxAgent = "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot" | "antigravity"; +export type NativeAcpxAgent = keyof typeof QUALIFIED_ACPX_PROFILE_DATA; + +function isNativeAcpxAgent(value: unknown): value is NativeAcpxAgent { + return typeof value === "string" && Object.hasOwn(QUALIFIED_ACPX_PROFILE_DATA, value); +} export type NativeCodexApprovalPolicy = "never" | "on-request" | "untrusted"; export type NativeOpenCodePermissionMode = "allow" | "ask" | "deny"; export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all"; @@ -709,8 +713,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput if (providerModel === null) { throw new NativeExecutionInputError("input.provider.model is required for acpx"); } - if (provider.agent !== "pi" && provider.agent !== "claude" && provider.agent !== "codex" && provider.agent !== "grok" && provider.agent !== "cursor" && provider.agent !== "copilot" && provider.agent !== "antigravity") { - throw new NativeExecutionInputError("input.provider.agent must be pi, claude, codex, grok, cursor, copilot, or antigravity"); + if (!isNativeAcpxAgent(provider.agent)) { + throw new NativeExecutionInputError("input.provider.agent must name a registered ACPX profile"); } if (isV4) { if (provider.permissionMode !== "approve-all" && provider.permissionMode !== "approve-paperclip" && provider.permissionMode !== "approve-reads" && provider.permissionMode !== "deny-all") { diff --git a/packages/paperclip-runner/src/drivers/acpx/acp-edit-context.ts b/packages/paperclip-runner/src/drivers/acpx/acp-edit-context.ts new file mode 100644 index 00000000000..57f0844f280 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/acp-edit-context.ts @@ -0,0 +1,46 @@ +import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js"; +import { safeAcpxLocations } from "./safe-locations.js"; +import { lstatSync, realpathSync } from "node:fs"; +import { basename, dirname, isAbsolute, join } from "node:path"; + +const record = (value: unknown): Record => + value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; + +function aliasedLocation(path: string, cwd: string): string | undefined { + if (!isAbsolute(path)) return; + try { + const canonicalCwd = realpathSync(cwd); + let canonicalPath: string; + try { + lstatSync(path); + canonicalPath = realpathSync(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") return; + try { lstatSync(path); return; } catch (missing) { + if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return; + } + canonicalPath = join(realpathSync(dirname(path)), basename(path)); + } + return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined; + } catch { return; } +} + +export function safeAcpxEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined { + const call = record(value); + if (call.kind !== "edit" || !workingDirectory) return; + const input = record(call.rawInput); + if (call.locations !== undefined && !Array.isArray(call.locations)) return; + if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return; + const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined); + if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return; + const safe = paths.flatMap(path => { + const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title); + if (locations.length) return locations; + const aliased = aliasedLocation(path as string, workingDirectory); + return aliased ? [{ path: aliased }] : []; + }); + const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":")); + if (names.length !== paths.length || new Set(names).size !== 1) return; + const target = names[0]; + return target && redactPaperclipSemanticValue(target) === target ? target : undefined; +} diff --git a/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.test.ts index 55f01bb9e82..70019b80728 100644 --- a/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.test.ts @@ -13,7 +13,7 @@ function request(kinds = ["allow_once", "allow_always", "reject_once"]): AcpPerm } } as AcpPermissionRequest; } describe("ACP permission normalization", () => { - it.each([undefined, "codex", "claude", "pi", "copilot"])("preserves existing %s native identity behavior", provider => { + it.each([undefined, "codex", "claude", "pi", "copilot", "kimi"])("preserves existing %s native identity behavior", provider => { const native = request(); native.raw.toolCall.toolCallId = "native\u0000tool"; expect(normalizeAcpxPermission(native, { provider }).toolCallId).toBe("native\u0000tool"); }); @@ -93,6 +93,42 @@ describe("ACP permission normalization", () => { }); }); +describe("Kimi canonical file permission context", () => { + const context = { provider: "kimi", workingDirectory: "/fixture/workspace", allowAlwaysScope: "session" as const }; + function write(call: Record = {}) { + const value = request(); + Object.assign(value.raw.toolCall, { toolCallId: "kimi-write-native-id", kind: "edit", title: "Write", rawInput: { path: "/fixture/workspace/nested/result.txt", content: "PRIVATE_KIMI_CONTENT" }, locations: [{ path: "nested/result.txt" }], ...call }); + return value; + } + it("shows the native Write target and preserves its exact operation and decision identity", () => { + const normalized = normalizeAcpxPermission(write(), context); + expect(normalized.title).toBe("Change file: nested/result.txt"); + expect(normalized.toolCallId).toBe("kimi-write-native-id"); + expect(JSON.stringify(normalized)).not.toContain("PRIVATE_KIMI_CONTENT"); + expect(normalized.resolve({ action: "accept" })).toEqual({ outcome: "allow_once" }); + expect(normalized.resolve({ action: "accept_for_session" })).toEqual({ outcome: "allow_always" }); + expect(normalized.resolve({ action: "decline" })).toEqual({ outcome: "reject_once" }); + }); + it.each([ + { name: "unknown", rawInput: {}, locations: [] }, + { name: "conflicting", rawInput: { path: "one.txt" }, locations: [{ path: "two.txt" }] }, + { name: "outside", rawInput: { path: "/outside/result.txt" }, locations: [] }, + ])("offers only denial or cancellation for a $name Write target", ({ rawInput, locations }) => { + const normalized = normalizeAcpxPermission(write({ rawInput, locations }), context); + expect(normalized.title).toContain("target unavailable"); + expect(normalized.toolCallId).toBe("kimi-write-native-id"); + expect(normalized.choices.map(choice => choice.key)).toEqual(["decline", "cancel"]); + expect(normalized.resolve({ action: "decline" })).toEqual({ outcome: "reject_once" }); + expect(normalized.resolve({ action: "cancel" })).toEqual({ outcome: "cancel" }); + for (const action of ["accept", "accept_for_session"] as const) expect(() => normalized.resolve({ action })).toThrow("not an offered choice"); + }); + it("requires a bound workspace before allowing a native Write", () => { + const normalized = normalizeAcpxPermission(write(), { provider: "kimi" }); + expect(normalized.choices.map(choice => choice.key)).toEqual(["decline", "cancel"]); + expect(() => normalized.resolve({ action: "accept" })).toThrow("not an offered choice"); + }); +}); + describe("Copilot canonical file permission context", () => { const context = { provider: "copilot", workingDirectory: "/fixture/workspace", allowAlwaysScope: "session" as const }; diff --git a/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.ts index 59e068afa9b..f02ae3137aa 100644 --- a/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/acp-permission-adapter.ts @@ -4,7 +4,7 @@ import { cursorToolIdentity } from "./cursor-plan-tool-identity.js"; -import { safeCopilotEditTarget } from "./copilot-permission-context.js"; +import { safeAcpxEditTarget } from "./acp-edit-context.js"; export type AcpxPermissionAction = "accept" | "accept_for_session" | "decline" | "cancel"; export interface NormalizedAcpxPermission { @@ -39,8 +39,8 @@ export function normalizeAcpxPermission(request: AcpPermissionRequest, options: || (options.provider === "cursor" && !call.toolCallId.trim())) { throw new Error("ACP permission request omitted its tool identity"); } - const needsEditContext = options.provider === "copilot" && call.kind === "edit"; - const target = needsEditContext ? safeCopilotEditTarget(call, options.workingDirectory) : undefined; + const needsEditContext = (options.provider === "copilot" || options.provider === "kimi") && call.kind === "edit"; + const target = needsEditContext ? safeAcpxEditTarget(call, options.workingDirectory) : undefined; const bindings = new Map(); const choices: NormalizedAcpxPermission["choices"] = []; for (const [key, kind, label] of [ diff --git a/packages/paperclip-runner/src/drivers/acpx/candidate-admission.test.ts b/packages/paperclip-runner/src/drivers/acpx/candidate-admission.test.ts index 803e28250e1..44dfdc9250f 100644 --- a/packages/paperclip-runner/src/drivers/acpx/candidate-admission.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/candidate-admission.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "./kimi-closure-pins.js"; import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js"; import { assertAcpxProfileReleased, resolveQualifiedAcpxProfile } from "./qualified-profiles.js"; import { assertAcpxExecutionAdmission, bindAcpxCandidateAdmission, parseAcpxCandidateAdmission, parseAcpxCandidateStartupAdmission, type AcpxCandidateAdmission } from "./candidate-admission.js"; @@ -95,3 +96,59 @@ describe("candidate installation remains cryptographically verified", () => { expect(openRuntime).not.toHaveBeenCalled(); }); }); + + +describe("trusted Kimi candidate admission", () => { + const kimiExpected = { companyId: "company-kimi", agent: "kimi", model: "kimi-for-coding" }; + const kimiCandidate = (target: "darwin-arm64" | "linux-x64"): AcpxCandidateAdmission => ({ + schema: "paperclip.acpx-candidate-admission.v1", ...kimiExpected, agent: "kimi", + profileDigest: resolveQualifiedAcpxProfile("kimi", kimiExpected.model).commandDigest, + nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`, + platform: target === "darwin-arm64" ? "darwin" : "linux", + architecture: target === "darwin-arm64" ? "arm64" : "x64", expiresAt: now + 60_000, + }); + + it.each(["darwin-arm64", "linux-x64"] as const)("binds only the current pending Kimi profile and exact %s closure", target => { + expect(resolveQualifiedAcpxProfile("kimi", kimiExpected.model)).toMatchObject({ agentProfileVersion: 16, qualificationStatus: "pending" }); + expect(parseAcpxCandidateAdmission(kimiCandidate(target), kimiExpected, now)).toEqual(kimiCandidate(target)); + expect(() => assertAcpxProfileReleased("kimi")).toThrow("qualification is pending"); + expect(() => assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile("kimi", kimiExpected.model))).toThrow("qualification is pending"); + const other = target === "darwin-arm64" ? "linux-x64" : "darwin-arm64"; + for (const change of [ + { companyId: "foreign-company" }, { agent: "antigravity" }, { model: "another-model" }, + { model: "auto" }, { model: "default" }, { model: " kimi-for-coding" }, + { profileDigest: `sha256:${"0".repeat(64)}` }, + { nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[other]}` }, + { platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "x64" }, + { expiresAt: now }, { expiresAt: now + 86_400_001 }, { expiresAt: "1800000060000" }, + { unknown: true }, { runId: "injected-run" }, + ]) expect(() => parseAcpxCandidateAdmission({ ...kimiCandidate(target), ...change }, kimiExpected, now)).toThrow(expect.objectContaining({ code: "ACPX_CANDIDATE_ADMISSION_INVALID" })); + }); + + it.each(["darwin-arm64", "linux-x64"] as const)("requires the exact actual host and startup identity for %s", target => { + const startup = bindAcpxCandidateAdmission({ ...kimiCandidate(target), expiresAt: Date.now() + 60_000 }, + { runId: "kimi-run", normalizedSessionId: "kimi-session" }); + const profile = resolveQualifiedAcpxProfile("kimi", kimiExpected.model); + const platform = Object.getOwnPropertyDescriptor(process, "platform")!; + const architecture = Object.getOwnPropertyDescriptor(process, "arch")!; + try { + Object.defineProperty(process, "platform", { ...platform, value: startup.platform }); + Object.defineProperty(process, "arch", { ...architecture, value: startup.architecture }); + expect(() => assertAcpxExecutionAdmission(profile, startup)).not.toThrow(); + for (const identity of [{ runId: "foreign-run" }, { normalizedSessionId: "foreign-session" }, { model: "foreign-model" }]) { + expect(() => parseAcpxCandidateStartupAdmission(startup, { ...kimiExpected, runId: "kimi-run", normalizedSessionId: "kimi-session", ...identity })).toThrow(); + } + expect(() => assertAcpxExecutionAdmission({ ...profile, commandDigest: `sha256:${"f".repeat(64)}` }, startup)).toThrow(); + expect(() => parseAcpxCandidateStartupAdmission(startup, kimiExpected, startup.expiresAt)).toThrow(); + for (const [os, arch] of [["win32", "x64"], ["linux", "arm64"], ["darwin", "x64"], + target === "darwin-arm64" ? ["linux", "x64"] : ["darwin", "arm64"]]) { + Object.defineProperty(process, "platform", { ...platform, value: os }); + Object.defineProperty(process, "arch", { ...architecture, value: arch }); + expect(() => assertAcpxExecutionAdmission(profile, startup)).toThrow(); + } + } finally { + Object.defineProperty(process, "platform", platform); + Object.defineProperty(process, "arch", architecture); + } + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/candidate-admission.ts b/packages/paperclip-runner/src/drivers/acpx/candidate-admission.ts index 67752d25cfc..1ba3d0f817d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/candidate-admission.ts +++ b/packages/paperclip-runner/src/drivers/acpx/candidate-admission.ts @@ -1,3 +1,4 @@ +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "./kimi-closure-pins.js"; import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js"; import { QUALIFIED_ACPX_PROFILES, assertAcpxProfileReleased, type QualifiedAcpxProfile } from "./qualified-profiles.js"; @@ -5,12 +6,12 @@ import { QUALIFIED_ACPX_PROFILES, assertAcpxProfileReleased, type QualifiedAcpxP export interface AcpxCandidateAdmission { schema: "paperclip.acpx-candidate-admission.v1"; companyId: string; - agent: "antigravity"; + agent: "antigravity" | "kimi"; model: string; profileDigest: string; nativeClosureDigest: string; - platform: "darwin"; - architecture: "arm64"; + platform: "darwin" | "linux"; + architecture: "arm64" | "x64"; expiresAt: number; } @@ -31,16 +32,23 @@ export function parseAcpxCandidateAdmission( ): AcpxCandidateAdmission { if (value === null || typeof value !== "object" || Array.isArray(value)) return invalid(); const entry = value as Record; + const profile = entry.agent === "antigravity" ? QUALIFIED_ACPX_PROFILES.antigravity + : entry.agent === "kimi" ? QUALIFIED_ACPX_PROFILES.kimi : undefined; + const target = `${entry.platform}-${entry.architecture}`; + const closure = entry.agent === "antigravity" && target === "darwin-arm64" + ? ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256 + : entry.agent === "kimi" && (target === "darwin-arm64" || target === "linux-x64") + ? KIMI_DISTRIBUTION_CLOSURE_SHA256[target] : undefined; if (!Number.isSafeInteger(now) || now < 0 || Object.keys(entry).length !== BASE_FIELDS.length || Object.keys(entry).some(key => !BASE_FIELDS.includes(key)) || entry.schema !== "paperclip.acpx-candidate-admission.v1" || !stableId(entry.companyId) || entry.companyId !== expected.companyId - || entry.agent !== "antigravity" || entry.agent !== expected.agent + || !profile || entry.agent !== expected.agent + || entry.agent === "kimi" && profile.qualificationStatus !== "pending" || typeof entry.model !== "string" || entry.model !== expected.model || !entry.model.trim() || entry.model !== entry.model.trim() || Buffer.byteLength(entry.model) > 512 || /[\u0000-\u001f\u007f]/.test(entry.model) || ["auto", "default"].includes(entry.model.toLowerCase()) - || entry.profileDigest !== QUALIFIED_ACPX_PROFILES.antigravity.commandDigest - || entry.nativeClosureDigest !== `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}` - || entry.platform !== "darwin" || entry.architecture !== "arm64" + || entry.profileDigest !== profile.commandDigest + || !closure || entry.nativeClosureDigest !== `sha256:${closure}` || !Number.isSafeInteger(entry.expiresAt) || Number(entry.expiresAt) <= now || Number(entry.expiresAt) > now + 24 * 60 * 60 * 1000) return invalid(); return { ...entry } as unknown as AcpxCandidateAdmission; @@ -74,7 +82,7 @@ export function parseAcpxCandidateStartupAdmission( /** This exception is deliberately absent from direct driver/config admission. */ export function assertAcpxExecutionAdmission(profile: QualifiedAcpxProfile, candidate?: AcpxCandidateStartupAdmission): void { if (candidate === undefined) return assertAcpxProfileReleased(profile.agent); - if (process.platform !== "darwin" || process.arch !== "arm64") return invalid(); parseAcpxCandidateStartupAdmission(candidate, { agent: profile.agent, model: profile.qualificationModel }); + if (candidate.platform !== process.platform || candidate.architecture !== process.arch) return invalid(); if (profile.commandDigest !== candidate.profileDigest) return invalid(); } diff --git a/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts index cc2d1eff0d6..4ed7e4486b5 100644 --- a/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/capability-profiles.ts @@ -62,6 +62,12 @@ export const ACPX_CAPABILITY_PROFILES: Readonly { await session.close({ reason: "native attribution verified" }); }); + it.runIf(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending")("rejects pending Kimi before opening a direct runtime host", () => { + const openHost = vi.fn(); + expect(() => driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { openHost })) + .toThrow(expect.objectContaining({ code: "ACPX_PROFILE_QUALIFICATION_PENDING", retryable: false })); + expect(openHost).not.toHaveBeenCalled(); + }); + + it.skipIf(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending").each(["exact-final", "tool-only", "empty-final", "missing-end", "cancelled", "filtered", "paused", "max-tokens"])("qualified Kimi direct-driver step boundaries separate commentary from the actual final response: %s", async scenario => { + const id = (n: number) => `kimi-message-${String(n).padStart(64, "0")}`; + const chunk = (n: number, kind: string, text = "", stream: "output" | "thought" = "output"): AcpRuntimeEvent => ({ + type: "text_delta", text, stream, messageId: id(n), meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind }, + }); + const runtimeEvents = [chunk(1, "start"), chunk(1, "delta", "Calling finish."), chunk(1, "end:tool_use")]; + if (scenario !== "tool-only") { + runtimeEvents.push(chunk(2, "start"), chunk(2, "delta", "private thought", "thought")); + if (scenario !== "empty-final") runtimeEvents.push(chunk(2, "delta", "EXACT_"), chunk(2, "delta", "MARKER")); + if (scenario !== "missing-end" && scenario !== "cancelled") runtimeEvents.push(chunk(2, + `end:${scenario === "filtered" || scenario === "paused" ? scenario : scenario === "max-tokens" ? "max_tokens" : "end_turn"}`)); + } + const fixture = driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { runtimeEvents }); + const session = await fixture.driver.openSession({ runId: "run-kimi-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" }); + const { turnId } = await session.startTurn({ message: { text: "Answer exactly" } }); + fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" }); + const events = await collectUntil(session.events(), scenario === "missing-end" ? "turn.failed" : scenario === "cancelled" ? "turn.interrupted" : "turn.completed"); + const progress = events.find(event => event.eventType === "item.delta" && event.payload.text === "Calling finish.")!; + expect(progress).toBeDefined(); + expect(progress.itemId).toBe(`${turnId}:assistant-message:${id(1)}`); + const commentary = events.filter(event => event.eventType === "item.completed" && event.payload.kind === "agentMessage" && event.payload.channel === "commentary"); + expect(commentary.filter(event => event.payload.text === "Calling finish.")).toHaveLength(1); + expect(commentary[0]!.itemId).toBe(progress.itemId); + const final = events.filter(event => event.eventType === "item.completed" && event.payload.kind === "agentMessage" && event.payload.channel === "final"); + expect(final.map(event => event.payload.text)).toEqual(["exact-final", "max-tokens"].includes(scenario) ? ["EXACT_MARKER"] : []); + if (final.length) expect(final[0]!.itemId).toBe(`${turnId}:assistant-message:${id(2)}`); + expect(events.filter(event => event.payload.kind === "agentMessage").some(event => event.payload.text === "private thought")).toBe(false); + expect(events.filter(event => event.eventType === "item.delta" && event.payload.text === "").length).toBeGreaterThanOrEqual(2); + await session.close({ reason: "Kimi native final attribution verified" }); + }); + it("delivers negotiated steering and follow-up distinctly, once, for the active turn", async () => { const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents: [] }); fixture.host.steeringCapability.mockReturnValue({ steering: true, queuedFollowUp: true }); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts index 8d573038487..72a4cf17d0e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts @@ -5,6 +5,7 @@ import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence } import { requireAcpxResponseDelivery } from "./response-delivery.js"; import { createPiMessageProjection, piBoundaryClearsFinal, type PiProjectedMessageEvent } from "./pi-message-projection.js"; +import { createKimiMessageProjection, kimiMessageBoundaryClears, type KimiProjectedMessageEvent } from "./kimi-message-boundary.js"; import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, type AcpxExtensionInput } from "./profile-extensions.js"; import { createHash, randomBytes } from "node:crypto"; @@ -290,7 +291,7 @@ export class CodexAcpxDriver implements HarnessDriver { dependencies: CodexAcpxDriverDependencies = {}, ) { assertAcpxProfileReleased(options.agent ?? "codex"); - if (["pi", "cursor", "copilot"].includes(options.agent ?? "codex") && typeof options.providerPolicy?.readOnly !== "boolean") { + if (["pi", "cursor", "copilot", "kimi"].includes(options.agent ?? "codex") && typeof options.providerPolicy?.readOnly !== "boolean") { throw new Error("ACP candidate requires an explicit provider read-only policy"); } this.#options = { @@ -330,7 +331,7 @@ export class CodexAcpxDriver implements HarnessDriver { }, capabilities: { ...descriptor.capabilities, - resume: true, + resume: descriptor.capabilities.resume, runtimeRequestResolution: true, runtimeRequestHandoff: true, unsupported: descriptor.capabilities.unsupported, @@ -1481,7 +1482,8 @@ class CodexAcpxSession implements HarnessSession { const normalizeToolEvent = createAcpxToolEventNormalizer(); const piMessages = this.#agent === "pi" ? createPiMessageProjection() : null; - const normalizeMessage = piMessages ? piMessages.normalize : this.#agent === "grok" + const kimiMessages = this.#agent === "kimi" ? createKimiMessageProjection() : null; + const normalizeMessage = piMessages ? piMessages.normalize : kimiMessages ? kimiMessages.normalize : this.#agent === "grok" ? createGrokMessageNormalizer() : (event: AcpRuntimeEvent) => event; for await (const event of turn.events) { toolEvidence?.tool(event); @@ -1490,7 +1492,7 @@ class CodexAcpxSession implements HarnessSession { this.#mapRuntimeEvent(normalizeMessage(normalizeToolEvent(projected)), turnId, ++index); } const result = await turn.result; - if (result.status === "completed") piMessages?.settle(); + if (result.status === "completed") { piMessages?.settle(); kimiMessages?.settle(); } await drainExtensions(); const usageAfter = await readUsageStatus(this.#host); // Diagnostic projection failures cannot replace the provider's terminal result. @@ -1676,12 +1678,12 @@ class CodexAcpxSession implements HarnessSession { } #assistantItemId(turnId: string, messageId: string | null): string { - if (this.#agent !== "copilot") return `${turnId}:assistant-message`; + if (this.#agent !== "copilot" && this.#agent !== "kimi") return `${turnId}:assistant-message`; return messageId ? `${turnId}:assistant-message:${messageId}` : `${turnId}:assistant-activity`; } #mapRuntimeEvent( - event: PiProjectedMessageEvent, + event: PiProjectedMessageEvent>, turnId: string, index: number, ): void { @@ -1691,10 +1693,15 @@ class CodexAcpxSession implements HarnessSession { const isReasoning = event.stream === "thought" || event.tag === "agent_thought_chunk"; const messageId = typeof event.messageId === "string" && event.messageId ? event.messageId : null; + let kimiCommentary: { text: string; itemId: string } | undefined; // The pinned Copilot mapper supplies actual native identity, including // empty starts. Unidentified session info/warnings remain activity only. - if (!isReasoning && !event.piMessageHistory && (this.#agent !== "copilot" || messageId)) { - if (piBoundaryClearsFinal(event)) this.#assistantText = ""; + if (!isReasoning && !event.piMessageHistory && !event.assistantMessageHistory && (this.#agent !== "copilot" || messageId)) { + const kimiClears = this.#agent === "kimi" && kimiMessageBoundaryClears(event); + if (kimiClears && this.#assistantText) { + kimiCommentary = { text: this.#assistantText, itemId: this.#assistantItemId(turnId, this.#assistantMessageId) }; + } + if (piBoundaryClearsFinal(event) || kimiClears) this.#assistantText = ""; if (messageId && this.#assistantMessageId && messageId !== this.#assistantMessageId) { if (this.#agent === "copilot" && this.#assistantText) { this.#emit("item.completed", { kind: "agentMessage", channel: "commentary", text: this.#assistantText }, @@ -1722,6 +1729,8 @@ class CodexAcpxSession implements HarnessSession { : this.#assistantItemId(turnId, messageId), }, ); + if (kimiCommentary) this.#emit("item.completed", { kind: "agentMessage", channel: "commentary", text: kimiCommentary.text }, + { turnId, itemId: kimiCommentary.itemId }); } if (event.type === "status" && event.tag === "usage_update") { this.#usage = boundedRecord({ diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index 0f74e8812bd..742050327f8 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -1,3 +1,6 @@ +import { admitAcpxSessionContext } from "./session-context-admission.js"; +import { createKimiInstructionAdmission } from "./kimi-instructions.js"; +import { kimiReadOnlyPermissionAllowed } from "./kimi-config.js"; import { resolveGrokDisableWebSearch } from "./grok-web-search.js"; import { resolveGrokEffort, createGrokEffortAdmission } from "./grok-effort.js"; import { resolveClaudeEffort, createClaudeEffortAdmission } from "./claude-effort.js"; @@ -37,7 +40,6 @@ import { AcpxApprovalRequiredError, decideAcpxPermission } from "./permission-po import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js"; import { createCursorInstructionAdmission } from "./cursor-instructions.js"; import { createAntigravityAdmission } from "./antigravity-admission.js"; -import { admitAcpxSessionContext } from "./session-context-admission.js"; import { createAcpxModeBinding } from "./provider-mode.js"; import { assertCopilotPromptPolicy, createCopilotProtocolGuard } from "./copilot-policy.js"; @@ -326,17 +328,19 @@ export async function openQualifiedAcpxRuntime( const grokDisableWebSearch = resolveGrokDisableWebSearch(options.profile.agent, options.grokDisableWebSearch); const effortAdmission = selectedClaudeEffort ? createClaudeEffortAdmission(selectedClaudeEffort) : selectedGrokEffort ? createGrokEffortAdmission(selectedGrokEffort, options.profile.reportedModelId) : undefined; - const cursorInstructions = options.profile.agent === "cursor" + const instructionAdmission = options.profile.agent === "kimi" + ? createKimiInstructionAdmission(options.systemInstructions) + : options.profile.agent === "cursor" ? createCursorInstructionAdmission(options.systemInstructions) : null; const antigravityAdmission = options.profile.agent === "antigravity" ? createAntigravityAdmission(options.systemInstructions, options.profile.reportedModelId) : null; - const contextAdmission = cursorInstructions ?? antigravityAdmission; + const contextAdmission = instructionAdmission ?? antigravityAdmission; const runtimeOptions: GoalAwareAcpRuntimeOptions = { cwd: options.cwd, - ...(cursorInstructions || modeBinding ? { protocolGuardFactory: () => { - const instructions = cursorInstructions?.createGuard(); + ...(instructionAdmission || modeBinding ? { protocolGuardFactory: () => { + const instructions = instructionAdmission?.createGuard(); const mode = modeBinding?.createGuard(); return (direction: "inbound" | "outbound", message: unknown) => { instructions?.(direction, message); mode?.(direction, message); }; } } : {}), @@ -412,6 +416,8 @@ export async function openQualifiedAcpxRuntime( } if ((permissionBoundary.hasAdmittedTurn && !extensionBoundary.active) || extensionBoundary.active?.signal.aborted || context.signal.aborted) return { outcome: "cancel" }; + if (options.profile.agent === "kimi" && options.readOnly === true + && !kimiReadOnlyPermissionAllowed(request, runnerOwnedMcpServerNames)) return { outcome: "reject_once" }; const disposition = decideAcpxPermission( options.profile.agent, options.permissionMode, @@ -532,7 +538,7 @@ export async function openQualifiedAcpxRuntime( // Its supported exact-model control forces session/load and the // current instruction acknowledgement before host admission. if (!runtime.setConfigOption) { - throw new Error("ACP context admission requires exact model configuration"); + throw new Error("Cold instruction admission requires exact model configuration"); } await runtime.setConfigOption({ handle: ensuredHandle, key: "model", value: options.profile.reportedModelId, diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-permission-context.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-permission-context.ts index 96c59036fe6..8f25b772d43 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-permission-context.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-permission-context.ts @@ -1,52 +1,2 @@ -import { redactPaperclipSemanticValue } from "../../semantic-tools/redaction.js"; -import { safeAcpxLocations } from "./safe-locations.js"; -import { lstatSync, realpathSync } from "node:fs"; -import { basename, dirname, isAbsolute, join } from "node:path"; - export const COPILOT_PERMISSION_CONTEXT_CONTRACT = "copilot-edit-permission-context-v1"; -const record = (value: unknown): Record => - value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; - -/** Native providers can spell an absolute workspace path through a host alias - * (macOS /tmp and /private/tmp). Attest the real entry, or its existing parent - * for a new file, before converting it to a display target. */ -function aliasedLocation(path: string, cwd: string): string | undefined { - if (!isAbsolute(path)) return; - try { - const canonicalCwd = realpathSync(cwd); - let canonicalPath: string; - try { - lstatSync(path); - canonicalPath = realpathSync(path); - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== "ENOENT") return; - // A dangling symlink must not become an apparently safe create target. - try { lstatSync(path); return; } catch (missing) { - if ((missing as NodeJS.ErrnoException).code !== "ENOENT") return; - } - canonicalPath = join(realpathSync(dirname(path)), basename(path)); - } - return safeAcpxLocations([{ path: canonicalPath }], canonicalCwd, "edit")[0]?.path as string | undefined; - } catch { return; } -} - -/** Display context only; this does not authorize filesystem access. */ -export function safeCopilotEditTarget(value: unknown, workingDirectory: string | undefined): string | undefined { - const call = record(value); - if (call.kind !== "edit" || !workingDirectory) return; - const input = record(call.rawInput); - if (call.locations !== undefined && !Array.isArray(call.locations)) return; - if (Array.isArray(call.locations) && (call.locations.length > 16 || call.locations.some(x => typeof record(x).path !== "string"))) return; - const paths = [input.path, input.fileName, ...(Array.isArray(call.locations) ? call.locations.map(x => record(x).path) : [])].filter(x => x !== undefined); - if (!paths.length || paths.some(x => typeof x !== "string" || !x || x.trim() !== x || x.length > 2048 || /[\\\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(x))) return; - const safe = paths.flatMap(path => { - const locations = safeAcpxLocations([{ path }], workingDirectory, call.kind, call.title); - if (locations.length) return locations; - const aliased = aliasedLocation(path as string, workingDirectory); - return aliased ? [{ path: aliased }] : []; - }); - const names = safe.map(x => x.path).filter((x): x is string => typeof x === "string" && x.length <= 1024 && x.trim() === x && !x.includes(":")); - if (names.length !== paths.length || new Set(names).size !== 1) return; - const target = names[0]; - return target && redactPaperclipSemanticValue(target) === target ? target : undefined; -} +export { safeAcpxEditTarget as safeCopilotEditTarget } from "./acp-edit-context.js"; diff --git a/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts b/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts index 854c5568c7d..52a780d8f12 100644 --- a/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts +++ b/packages/paperclip-runner/src/drivers/acpx/driver-profile.ts @@ -16,7 +16,7 @@ import { import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js"; -const ACPX_AGENTS = ["claude", "codex", "grok", "pi", "cursor", "copilot", "antigravity"] as const; +const ACPX_AGENTS = ["claude", "codex", "grok", "pi", "cursor", "copilot", "kimi", "antigravity"] as const; const ACPX_PERMISSION_MODES = [ "approve-all", "approve-paperclip", @@ -113,7 +113,7 @@ export function validateAcpxDriverConfig( return invalid( "agent", "invalid_agent", - "ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity.", + "ACPX agent must be claude, codex, grok, cursor, copilot, kimi, pi, or antigravity.", ); } if (ACPX_CAPABILITY_PROFILES[agent].qualification !== "qualified") { diff --git a/packages/paperclip-runner/src/drivers/acpx/environment.test.ts b/packages/paperclip-runner/src/drivers/acpx/environment.test.ts index c744b5d99b7..fa6db41055c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/environment.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/environment.test.ts @@ -44,9 +44,9 @@ describe("ACPX launch environment", () => { expect(launch.ACPX_CLAUDE_INCLUDE_USER_SETTINGS).toBeUndefined(); expect(environment.CLAUDE_CONFIG_DIR).toBe(configured); }); - it("retains Runner notification discovery while denying task-supplied overrides", () => { + it.each(["claude", "kimi"] as const)("retains %s Runner notification discovery while denying task-supplied overrides", agent => { for (const key of ["PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT", "PAPERCLIP_RUNTIME_NOTIFICATION_HELPER"]) { - expect(createSanitizedAcpxSpawnInput({ [key]: "/private/runner/context" }, "claude").env[key]).toBe("/private/runner/context"); + expect(createSanitizedAcpxSpawnInput({ [key]: "/private/runner/context" }, agent).env[key]).toBe("/private/runner/context"); expect(configuredEnvironmentProjection({ [key]: "/caller/override" })).not.toHaveProperty(key); } }); @@ -137,7 +137,9 @@ describe("ACPX launch environment", () => { it.each([ ["pi", "OPENROUTER_API_KEY"], ["cursor", "CURSOR_API_KEY"], - ["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], ["antigravity", "GEMINI_API_KEY"], + ["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], + ["kimi", "KIMI_API_KEY"], + ["antigravity", "GEMINI_API_KEY"], ] as const)("requires an explicit %s credential binding for %s", (agent, name) => { vi.stubEnv(name, "ambient-secret"); expect(createSanitizedAcpxSpawnInput(undefined, agent).env[name]).toBeUndefined(); @@ -157,9 +159,33 @@ describe("ACPX launch environment", () => { expect(createSanitizedAcpxSpawnInput(source, "pi").env).toEqual({ COPILOT_GITHUB_TOKEN: "copilot-key" }); }); + it("strips configured Kimi model redirects and home overrides while retaining selected task and MCP values", () => { + const overrides = { + KIMI_MODEL_NAME: "__kimi_env_model__", KIMI_MODEL_API_KEY: "unselected-model-key", + KIMI_MODEL_BASE_URL: "https://unselected.invalid/v1", KIMI_MODEL_PROVIDER_TYPE: "openai_legacy", + KIMI_MODEL_FUTURE_OVERRIDE: "cannot-bypass-the-prefix-check", KIMI_SECONDARY_MODEL: "other-model", + KIMI_CODE_HOME: "/unselected/home", + }; + const source = { ...configuredEnvironmentProjection({ ...overrides, CUSTOM_SETTING: "allowed" }), + KIMI_API_KEY: "selected-code-key", PAPERCLIP_NATIVE_MCP_NAME: "assigned", + PAPERCLIP_NATIVE_MCP_URL: "https://assigned.example/mcp", PAPERCLIP_NATIVE_MCP_TOKEN: "controller-only" }; + const binding = createAcpxCredentialBinding(source, "kimi", "session-1")!; + const sidecar = createAcpxSidecarHostEnvironment({ ...source, [ACPX_CREDENTIAL_BINDING_ENV]: binding }, "kimi", "session-1"); + for (const environment of [sidecar, createSanitizedAcpxSpawnInput(source, "kimi").env, + createSanitizedAcpxSpawnInput(sidecar, "kimi").env]) { + for (const name of Object.keys(overrides)) expect(environment).not.toHaveProperty(name); + expect(environment).toMatchObject({ KIMI_API_KEY: "selected-code-key", CUSTOM_SETTING: "allowed", + PAPERCLIP_NATIVE_MCP_NAME: "assigned", PAPERCLIP_NATIVE_MCP_URL: "https://assigned.example/mcp" }); + expect(JSON.parse(environment[CONFIGURED_ENVIRONMENT_KEYS]!)).toEqual(["CUSTOM_SETTING"]); + } + expect(createSanitizedAcpxSpawnInput(source, "kimi").env).not.toHaveProperty("PAPERCLIP_NATIVE_MCP_TOKEN"); + }); + it.each([ ["pi", "OPENROUTER_API_KEY"], ["cursor", "CURSOR_API_KEY"], - ["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], ["antigravity", "GEMINI_API_KEY"], + ["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], + ["kimi", "KIMI_API_KEY"], + ["antigravity", "GEMINI_API_KEY"], ] as const)("admits only session-bound %s sidecar credentials for %s", (agent, name) => { const source = { [name]: "task-secret" }; const binding = createAcpxCredentialBinding(source, agent, "session-1"); diff --git a/packages/paperclip-runner/src/drivers/acpx/environment.ts b/packages/paperclip-runner/src/drivers/acpx/environment.ts index 59cfd0dea07..ca2bf78e80b 100644 --- a/packages/paperclip-runner/src/drivers/acpx/environment.ts +++ b/packages/paperclip-runner/src/drivers/acpx/environment.ts @@ -10,6 +10,7 @@ export const ACPX_CREDENTIAL_BINDING_ENV = "PAPERCLIP_ACPX_CREDENTIAL_BINDING"; export const ACPX_CREDENTIAL_NAMES: Readonly> = { antigravity: ["GEMINI_API_KEY", "PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET"], grok: ["XAI_API_KEY"], + kimi: ["KIMI_API_KEY"], pi: PI_CREDENTIAL_NAMES, cursor: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"], copilot: ["COPILOT_GITHUB_TOKEN"], @@ -27,7 +28,17 @@ function removePiIamCredentials(environment: NodeJS.ProcessEnv): void { } } -const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot" || agent === "antigravity"; +function removeKimiRuntimeOverrides(environment: NodeJS.ProcessEnv): void { + const isOverride = (name: string) => /^KIMI_MODEL_/.test(name.toUpperCase()) + || ["KIMI_SECONDARY_MODEL", "KIMI_CODE_HOME"].includes(name.toUpperCase()); + const names = configuredEnvironmentKeys(environment); + for (const name of Object.keys(environment)) if (isOverride(name)) delete environment[name]; + if (environment[CONFIGURED_ENVIRONMENT_KEYS] !== undefined) { + environment[CONFIGURED_ENVIRONMENT_KEYS] = JSON.stringify(names.filter(name => !isOverride(name))); + } +} + +const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot" || agent === "kimi" || agent === "antigravity"; /** Mint only at the controller's explicit task-environment boundary, never by copying a marker. */ export function createAcpxCredentialBinding( @@ -79,6 +90,7 @@ export function createAcpxSidecarHostEnvironment( } } if (agent === "pi") removePiIamCredentials(result); + if (agent === "kimi") removeKimiRuntimeOverrides(result); if (agent === "antigravity") sanitizeAntigravityEnvironment(result); return result; } @@ -164,6 +176,7 @@ export function createSanitizedAcpxSpawnInput( result[key] = value; } if (agent === "pi") removePiIamCredentials(result); + if (agent === "kimi") removeKimiRuntimeOverrides(result); if (agent === "antigravity") sanitizeAntigravityEnvironment(result); // The controller-to-sidecar document is staged under a lifetime fence. The // native child receives the private token file, never its launch document. diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index 5a14be558bf..66f90182bc6 100644 --- a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts @@ -95,6 +95,20 @@ export const QUALIFIED_ACPX_PROFILE_DATA = { "agentRuntimeVersion": "0.160.0", "commandDigest": "sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc", "permissionPolicy": "interactive" + }, + "kimi": { + "driverKind": "acpx_runtime", + "protocolVersion": 1, + "acpxVersion": "0.13.1", + "agent": "kimi", + "agentProfileVersion": 16, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "commandDigest": "sha256:0c1e6ce460407ec973a37a2d192d665fcde744e2b9875c791c90308afbf4c52b", + "qualificationStatus": "pending", + "permissionPolicy": "interactive" } } as const; diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts new file mode 100644 index 00000000000..f7942c01f1e --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts @@ -0,0 +1,12 @@ +export const KIMI_RUNTIME_VERSION = "2.3.3" as const; +export const KIMI_NODE_VERSION = "24.21.0" as const; +export const KIMI_DISTRIBUTION_LOCK_SHA256 = "87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b"; +export const KIMI_NODE_DISTRIBUTIONS = Object.freeze({ + "darwin-arm64": Object.freeze({ archiveSha256: "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057", executableSha256: "e4b5a3af0e05c75de2eae013904145f40fe7fc2a6e6f17510128bf45cca4e79b", executableSize: 122129232 }), + "linux-x64": Object.freeze({ archiveSha256: "6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff", executableSha256: "7fde7b8afa198da66257f42ee2001d874c7355631e6d1579a5fb5ef1f246df4c", executableSize: 126595440 }), +}); + +export const KIMI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({ + "darwin-arm64": "6038325abd0431c30097ff1f572a9b29816e1b4da372d8cb86b2ca5d18ea34e7", + "linux-x64": "105d8394adbbc76a953a12cc51b5288eb940ad693291e584f6cdc6407213164d", +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-config.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-config.ts new file mode 100644 index 00000000000..5ef794f4fc2 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-config.ts @@ -0,0 +1,44 @@ +export function kimiManagedConfiguration(model: string): string { + if (!model.trim() || model.includes("\0") || Buffer.byteLength(model) > 512) throw new Error("Kimi Code requires a bounded explicit model ID"); + const quote = JSON.stringify; + return [ + `default_model = ${quote(model)}`, + "telemetry = false", + "builtin_product_skills = false", + "merge_all_available_skills = false", + "hooks = []", + "[providers.paperclip]", + 'type = "kimi"', + 'base_url = "https://api.kimi.ai/coding/v1"', + 'api_key_env = "KIMI_API_KEY"', + `[models.${quote(model)}]`, + 'provider = "paperclip"', + `model = ${quote(model)}`, + "max_context_size = 262144", + 'capabilities = ["tool_use"]', + "[tools]", + 'disabled = ["Agent", "AgentSwarm", "TaskList", "TaskOutput", "TaskStop", "CronCreate", "CronList", "CronDelete", "CreateGoal", "GetGoal", "SetGoalBudget", "UpdateGoal", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode"]', + "[background]", + "keep_alive_on_exit = false", + "bash_auto_background_on_timeout = false", + "[watch]", + "enabled = false", + "", + ].join("\n"); +} + +export function kimiReadOnlyPermissionAllowed(request: unknown, runnerOwnedMcpServerNames: ReadonlySet): boolean { + if (!request || typeof request !== "object") return false; + const raw = (request as { raw?: unknown }).raw; + if (!raw || typeof raw !== "object") return false; + const call = (raw as { toolCall?: unknown }).toolCall; + if (!call || typeof call !== "object") return false; + const title = (call as { title?: unknown }).title; + const input = (call as { rawInput?: unknown }).rawInput; + if (typeof title !== "string" || !input || typeof input !== "object" || Array.isArray(input)) return false; + for (const name of runnerOwnedMcpServerNames) { + const prefix = `mcp__${name}__`; + if (title.startsWith(prefix) && title.length > prefix.length) return true; + } + return false; +} diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-installation.test.ts new file mode 100644 index 00000000000..b89e4790307 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-installation.test.ts @@ -0,0 +1,110 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { assertKimiInstallationProfile, verifyKimiInstallation } from "./kimi-installation.js"; +import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js"; +import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js"; + +const roots: string[] = []; +const previous = new Map(); +function environment(key: string, value: string) { + if (!previous.has(key)) previous.set(key, process.env[key]); + process.env[key] = value; +} +afterEach(async () => { + for (const [key, value] of previous) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } + previous.clear(); + await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); +}); + +describe("Kimi installed runtime admission", () => { + it("rejects caller-selected package/version/command changes before filesystem resolution", () => { + const profile = QUALIFIED_ACPX_PROFILES.kimi; + expect(() => assertKimiInstallationProfile(profile)).not.toThrow(); + for (const alteration of [{ agentServerVersion: "latest" }, { agentServerPackage: "foreign" }, { commandDigest: `sha256:${"f".repeat(64)}` }, { agentProfileVersion: profile.agentProfileVersion + 1 }]) { + expect(() => assertKimiInstallationProfile({ ...profile, ...alteration })).toThrow(/trusted declaration/); + } + }); + + it("retains pending profile 10 while binding the merged entrypoint builder in profile 11", () => { + const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v10-identity.json", import.meta.url)); + expect(createHash("sha256").update(bytes).digest("hex")).toBe("b859f4aefcc36fc5e75b3aeeb02135ca9f7acc84866c856a3939effdd13cb358"); + const prior = JSON.parse(bytes.toString("utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v11-identity.json", import.meta.url), "utf8")); + expect(Object.keys(current.declaration.sources).filter(key => current.declaration.sources[key] !== prior.declaration.sources[key])).toEqual([ + "scripts/build-verified-provider-entrypoints.mjs", + ]); + expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi, + agentProfileVersion: 10, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/); + }); + + it("preserves historical profile 11 while binding the reconciled authority and event boundaries in 13", () => { + const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v11-identity.json", import.meta.url)); + expect(createHash("sha256").update(bytes).digest("hex")).toBe("17b3db5659cfbaa4d3f5dddd8a97ffeab38353795aa4dbfa6c6fe1520f415ab1"); + const prior = JSON.parse(bytes.toString("utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v13-identity.json", import.meta.url), "utf8")); + expect(prior.declaration.agentProfileVersion).toBe(11); + expect(Object.keys(prior.declaration.sources)).toHaveLength(31); + expect(current.declaration.agentProfileVersion).toBe(13); + expect(Object.keys(current.declaration.sources)).toHaveLength(55); + expect(Object.hasOwn(current.declaration.sources, "src/drivers/acpx/candidate-admission.ts")).toBe(true); + expect(Object.hasOwn(current.declaration.sources, "runner/crates/runner-core/src/provider_events.rs")).toBe(true); + expect(isSupportedAcpxProfileVersion("kimi", 11)).toBe(true); + expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi, + agentProfileVersion: 11, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/); + }); + + it("decodes pending14 history without installing it as the repaired pending15 runtime", () => { + const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v14-identity.json", import.meta.url)); + expect(createHash("sha256").update(bytes).digest("hex")).toBe("13ce8e301e6b7d622f4b3ffdca7981e18538e7a5ca2f1b2208ec466259469e7b"); + const prior = JSON.parse(bytes.toString("utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v15-identity.json", import.meta.url), "utf8")); + expect(current.declaration.agentProfileVersion).toBe(15); + expect(Object.keys(current.declaration.sources)).toHaveLength(55); + expect(Object.keys(current.declaration.sources).filter(key => current.declaration.sources[key] !== prior.declaration.sources[key])).toEqual([ + "runner/crates/runner-core/src/acpx_provider_backend.rs", + ]); + expect(isSupportedAcpxProfileVersion("kimi", 14)).toBe(true); + expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi, + agentProfileVersion: 14, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/); + expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow(); + }); + + it("decodes pending15 history without installing it as merged pending16", () => { + const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v15-identity.json", import.meta.url)); + expect(createHash("sha256").update(bytes).digest("hex")).toBe("5221f6f9f64c02ca7ee68b185e08ed46b1c82a13037b001854065b9fad28ff87"); + const prior = JSON.parse(bytes.toString("utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v16-identity.json", import.meta.url), "utf8")); + expect(current.declaration.agentProfileVersion).toBe(16); + expect(current.declaration.qualificationStatus).toBe("pending"); + expect(Object.keys(current.declaration.sources)).toHaveLength(58); + expect(isSupportedAcpxProfileVersion("kimi", 15)).toBe(true); + expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi, + agentProfileVersion: 15, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/); + expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow(); + }); + + it("decodes the old pending revision while refusing its source-bound launch digest", () => { + const historical = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v16-identity.json", import.meta.url), "utf8")); + expect(isSupportedAcpxProfileVersion("kimi", historical.declaration.agentProfileVersion)).toBe(true); + expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(historical.declaration.agentProfileVersion); + expect(QUALIFIED_ACPX_PROFILES.kimi.commandDigest).not.toBe(historical.commandDigest); + expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi, + commandDigest: historical.commandDigest })).toThrow(/trusted declaration/); + }); + + it("gives the normal setup command and never falls back to ambient kimi when assets are missing", async () => { + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-installation-test-"))); roots.push(root); + await writeFile(join(root, "package.json"), '{"name":"@paperclipai/paperclip-runner"}'); + environment("PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", root); + environment("PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST", join(root, "package.json")); + await expect(verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi)).rejects.toThrow(/paperclipai runtime setup kimi/); + const assets = join(root, "provider-assets/kimi", `${process.platform}-${process.arch}`); + await mkdir(join(assets, "runtime"), { recursive: true }); + await writeFile(join(assets, "kimi-distribution.json"), JSON.stringify({ schema: "paperclip.kimi-distribution.v1", nativeClosureSha256: "f".repeat(64) })); + await expect(verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi)).rejects.toThrow(/source-owned pin/); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-installation.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-installation.ts new file mode 100644 index 00000000000..a66954aaa46 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-installation.ts @@ -0,0 +1,71 @@ +import { constants } from "node:fs"; +import { lstat, open, realpath } from "node:fs/promises"; +import { join } from "node:path"; +import { verifyNativeAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256, KIMI_DISTRIBUTION_LOCK_SHA256, KIMI_NODE_VERSION, KIMI_RUNTIME_VERSION } from "./kimi-closure-pins.js"; +import { resolveRunnerProviderAssetsRoot } from "./provider-assets-root.js"; +import { QUALIFIED_ACPX_PROFILES, type AcpxReleaseProfile } from "./qualified-profiles.js"; + +export function assertKimiInstallationProfile(profile: AcpxReleaseProfile): void { + const trusted = QUALIFIED_ACPX_PROFILES.kimi; + for (const key of ["agent", "driverKind", "protocolVersion", "acpxVersion", "agentProfileVersion", "agentServerPackage", "agentServerVersion", "agentRuntimePackage", "agentRuntimeVersion", "commandDigest", "permissionPolicy"] as const) { + if (profile[key] !== trusted[key]) throw new Error("Kimi distribution profile differs from its trusted declaration"); + } + if (profile.agentServerPackage !== "@moonshot-ai/kimi-code" || profile.agentServerVersion !== KIMI_RUNTIME_VERSION || profile.permissionPolicy !== "interactive") throw new Error("Kimi distribution profile has an unsupported runtime"); +} + +async function readMetadata(path: string): Promise> { + const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const before = await file.stat({ bigint: true }); + if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > 64n * 1024n) throw new Error("Kimi distribution metadata is not a bounded regular file"); + const bytes = await file.readFile(); + const after = await file.stat({ bigint: true }); + const named = await lstat(path, { bigint: true }); + if (bytes.length !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("Kimi distribution metadata changed while read"); + const value: unknown = JSON.parse(bytes.toString("utf8")); + if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Kimi distribution metadata is invalid"); + return value as Record; + } finally { await file.close(); } +} + +export async function verifyKimiInstallation(profile: AcpxReleaseProfile): Promise { + assertKimiInstallationProfile(profile); + const target = `${process.platform}-${process.arch}`; + if (!Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, target)) throw new Error(`Kimi runtime target is unsupported: ${target}`); + const expectedClosureSha256 = KIMI_DISTRIBUTION_CLOSURE_SHA256[target as keyof typeof KIMI_DISTRIBUTION_CLOSURE_SHA256]; + const assets = join(resolveRunnerProviderAssetsRoot(import.meta.url, "kimi"), target); + const distributionRoot = join(assets, "runtime"); + const assertDirectories = async (): Promise => { + for (const path of [assets, distributionRoot]) { + const stat = await lstat(path); + if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(path) !== path) throw new Error("Kimi runtime escaped its fixed asset directory"); + } + }; + try { await assertDirectories(); } + catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error("Kimi runtime is not installed on this host; run paperclipai runtime setup kimi before selecting Kimi Code"); + throw error; + } + const metadata = await readMetadata(join(assets, "kimi-distribution.json")); + const entrypoint = "node_modules/@moonshot-ai/kimi-code/dist/main.mjs"; + if (metadata.schema !== "paperclip.kimi-distribution.v1" || metadata.version !== KIMI_RUNTIME_VERSION || metadata.target !== target || metadata.nodeVersion !== KIMI_NODE_VERSION || metadata.runtimeRoot !== "runtime" || metadata.entrypoint !== entrypoint || metadata.lockSha256 !== KIMI_DISTRIBUTION_LOCK_SHA256 || metadata.nativeClosureSha256 !== expectedClosureSha256) throw new Error("Kimi distribution metadata differs from its source-owned pin"); + const native = await verifyNativeAcpxInstallation({ + distributionRoot, + manifestPath: join(assets, "native-closure.json"), + expectedClosureSha256, + executable: "node/bin/node", + entrypoint, + fixedArguments: ["acp"], + }); + return Object.freeze({ + commandDigest: profile.commandDigest, + agentServerPackageJsonPath: join(distributionRoot, "node_modules/@moonshot-ai/kimi-code/package.json"), + agentRuntimePackageJsonPath: null, + async openCommand(options?: { signal?: AbortSignal }) { + options?.signal?.throwIfAborted(); + await assertDirectories(); + return native.openCommand(options); + }, + }); +} diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-instructions.test.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-instructions.test.ts new file mode 100644 index 00000000000..835dcc43083 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-instructions.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { createKimiInstructionAdmission, kimiInstructionBinding } from "./kimi-instructions.js"; +import { kimiManagedConfiguration, kimiReadOnlyPermissionAllowed } from "./kimi-config.js"; + +function acknowledgment(content: string, sessionId = "native-1") { + return { schema: "paperclip.kimi.instructions.v1", ...kimiInstructionBinding(content), sessionId, + effectiveDigest: `sha256:${"a".repeat(64)}`, effectiveByteLength: 500 }; +} + +describe("Kimi native instruction admission", () => { + it("keeps human questions and plans on the qualified semantic tool path", () => { + const config = kimiManagedConfiguration("caller-model"); + for (const name of ["AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "Agent", "AgentSwarm"]) { + expect(config).toContain(JSON.stringify(name)); + } + }); + it("admits exact UTF-8 instructions only after the correlated native reply, including ID zero", () => { + const content = "Follow café instructions 🥔\n"; + const admission = createKimiInstructionAdmission(content); + const guard = admission.createGuard(); + guard("outbound", { id: 0, method: "session/new", params: {} }); + guard("inbound", { id: 4, result: { sessionId: "native-1", _meta: { paperclipKimiInstructions: acknowledgment(content) } } }); + expect(admission.assertReady).toThrow("did not acknowledge"); + guard("inbound", { id: 0, result: { sessionId: "native-1", _meta: { paperclipKimiInstructions: acknowledgment(content) } } }); + expect(admission.assertReady).not.toThrow(); + guard("outbound", { id: 1, method: "session/prompt", params: { sessionId: "native-1" } }); + }); + + it.each(["missing", "content", "session", "length", "effective"])("rejects %s proof before a prompt", kind => { + const admission = createKimiInstructionAdmission("rules"); + const guard = admission.createGuard(); + guard("outbound", { id: "load", method: "session/load", params: { sessionId: "native-1" } }); + const ack = acknowledgment("rules"); + if (kind === "content") ack.digest = kimiInstructionBinding("different").digest; + if (kind === "session") ack.sessionId = "other"; + if (kind === "length") ack.byteLength += 1; + if (kind === "effective") ack.effectiveDigest = "unknown"; + expect(() => guard("inbound", { id: "load", result: { _meta: kind === "missing" ? {} : { paperclipKimiInstructions: ack } } })).toThrow("mismatched"); + expect(() => guard("outbound", { id: 2, method: "session/prompt", params: { sessionId: "native-1" } })).toThrow(); + }); + + it("revokes the old connection and requires proof again after load", () => { + const admission = createKimiInstructionAdmission(""); + const old = admission.createGuard(); + old("outbound", { id: 1, method: "session/new", params: {} }); + old("inbound", { id: 1, result: { sessionId: "native-1", _meta: { paperclipKimiInstructions: acknowledgment("") } } }); + const current = admission.createGuard(); + expect(() => old("outbound", { method: "session/prompt" })).toThrow("replaced"); + current("outbound", { id: 2, method: "session/load", params: { sessionId: "native-1" } }); + expect(admission.assertReady).toThrow(); + current("inbound", { id: 2, result: { _meta: { paperclipKimiInstructions: acknowledgment("") } } }); + expect(admission.assertReady).not.toThrow(); + expect(() => current("outbound", { method: "session/prompt", params: { sessionId: "other" } })).toThrow("unacknowledged"); + }); + + it("bounds exact byte content rather than JavaScript character count", () => { + expect(() => kimiInstructionBinding("🧪".repeat(8193))).toThrow("bounded"); + expect(() => kimiInstructionBinding("bad\0input")).toThrow("NUL"); + }); + + it("keeps read-only authority separate from native tool labels and auto-approval", () => { + const owned = new Set(["paperclip"]); + for (const title of ["Write", "Edit", "Bash", "Read", "mcp__other__read", "mcp__paperclip__"]) { + expect(kimiReadOnlyPermissionAllowed({ inferredKind: "read", raw: { toolCall: { title, rawInput: {} } } }, owned)).toBe(false); + } + expect(kimiReadOnlyPermissionAllowed({ raw: { toolCall: { title: "mcp__paperclip__get_task_context", rawInput: {} } } }, owned)).toBe(true); + expect(kimiReadOnlyPermissionAllowed({ raw: { toolCall: { title: "mcp__paperclip__get_task_context" } } }, owned)).toBe(false); + expect(kimiReadOnlyPermissionAllowed({ raw: { toolCall: { title: "mcp__paperclip__get_task_context", rawInput: [] } } }, owned)).toBe(false); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-instructions.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-instructions.ts new file mode 100644 index 00000000000..e167d00928a --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-instructions.ts @@ -0,0 +1,71 @@ +import { createHash } from "node:crypto"; + +const SCHEMA = "paperclip.kimi.instructions.v1"; +const MAX_BYTES = 32 * 1024; + +export function kimiInstructionBinding(content: string) { + if (typeof content !== "string" || content.includes("\0") || Buffer.from(content).toString("utf8") !== content || Buffer.byteLength(content) > MAX_BYTES) { + throw new Error("Kimi Code instructions exceed their bounded size or contain NUL"); + } + const digest = `sha256:${createHash("sha256").update(content).digest("hex")}`; + const byteLength = Buffer.byteLength(content); + return { payload: JSON.stringify({ schema: SCHEMA, content, digest, byteLength }), digest, byteLength }; +} + +function record(value: unknown): Record { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} + +export function createKimiInstructionAdmission(content: string) { + const expected = kimiInstructionBinding(content); + let current: { sessionId: string | null; error: Error | null } | null = null; + const failure = (message: string) => new Error(`Kimi Code instruction admission failed: ${message}`); + const assertReady = () => { + if (current?.error) throw current.error; + if (!current?.sessionId) throw failure("provider did not acknowledge the composed instructions"); + }; + return { + assertReady, + createGuard() { + const state = { sessionId: null as string | null, error: null as Error | null }; + current = state; + const pending = new Map(); + return (direction: "inbound" | "outbound", value: unknown): void => { + if (current !== state) throw failure("connection authority was replaced"); + if (state.error) throw state.error; + const message = record(value); + try { + if (direction === "outbound") { + if (["session/new", "session/load", "session/resume"].includes(String(message.method))) { + if ((typeof message.id !== "string" && typeof message.id !== "number") || pending.size !== 0) throw failure("session admission request is ambiguous"); + state.sessionId = null; + pending.set(message.id, { method: String(message.method), sessionId: record(message.params).sessionId }); + } else if (message.method === "session/prompt") { + assertReady(); + if (record(message.params).sessionId !== state.sessionId) throw failure("prompt belongs to an unacknowledged session"); + } + return; + } + if (message.method !== undefined || (typeof message.id !== "string" && typeof message.id !== "number")) return; + const request = pending.get(message.id); + if (!request) return; + pending.delete(message.id); + if (message.error !== undefined) return; + const result = record(message.result); + const ack = record(record(result._meta).paperclipKimiInstructions); + const sessionId = request.method === "session/new" ? result.sessionId : request.sessionId; + if (ack.schema !== SCHEMA || ack.digest !== expected.digest || ack.byteLength !== expected.byteLength + || typeof sessionId !== "string" || !sessionId.trim() || ack.sessionId !== sessionId + || typeof ack.effectiveDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(ack.effectiveDigest) + || !Number.isSafeInteger(ack.effectiveByteLength) || (ack.effectiveByteLength as number) < expected.byteLength) { + throw failure("native instruction acknowledgment is missing or mismatched"); + } + state.sessionId = sessionId; + } catch (error) { + state.error = error instanceof Error ? error : failure("invalid acknowledgment"); + throw state.error; + } + }; + }, + }; +} diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.test.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.test.ts new file mode 100644 index 00000000000..51eb3674aec --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { createKimiMessageProjection, kimiMessageBoundaryClears } from "./kimi-message-boundary.js"; + +const id = (n: number) => `kimi-message-${String(n).padStart(64, "0")}`; +const chunk = (n: number, kind: string, text = "", stream = "output") => ({ type: "text_delta", messageId: id(n), text, stream, + meta: { origin: "kimi-native-assistant", source: "kimi-turn-step-v1", kind } }); +function project(events: ReturnType[]) { + const projection = createKimiMessageProjection>(); + const progress: string[] = []; let final = ""; + for (const raw of events) { + const event = projection.normalize(raw); + if (kimiMessageBoundaryClears(event)) final = ""; + if (event.stream === "output") { final += event.text; progress.push(event.text); } + } + projection.settle(); return { final, progress }; +} + +describe("Kimi native message boundaries", () => { + it("preserves separate native steps while finalizing only the actual end-turn answer", () => { + const projection = createKimiMessageProjection>(); + const started = projection.normalize(chunk(1, "start")); + expect(started).toMatchObject({ messageId: id(1), text: "", assistantMessageBoundary: { phase: "start" } }); + expect(projection.normalize(chunk(1, "delta", "I will call the tool."))).toMatchObject({ messageId: id(1), text: "I will call the tool." }); + expect(projection.normalize(chunk(1, "end:tool_use"))).toMatchObject({ messageId: id(1), text: "", assistantMessageBoundary: { phase: "end", stopReason: "toolUse" } }); + projection.normalize(chunk(2, "start")); + expect(projection.normalize(chunk(2, "delta", "private reasoning", "thought"))).toMatchObject({ messageId: id(2), stream: "thought" }); + projection.normalize(chunk(2, "delta", "EXACT_MARKER")); + expect(projection.normalize(chunk(2, "end:end_turn"))).toMatchObject({ messageId: id(2), assistantMessageBoundary: { phase: "end", stopReason: "stop" } }); + projection.settle(); + const result = project([chunk(1, "start"), chunk(1, "delta", "I will call the tool."), chunk(1, "end:tool_use"), + chunk(2, "start"), chunk(2, "delta", "private reasoning", "thought"), chunk(2, "delta", "EXACT_"), chunk(2, "delta", "MARKER"), chunk(2, "end:end_turn")]); + expect(result.final).toBe("EXACT_MARKER"); + expect(result.progress).toContain("I will call the tool."); + expect(result.progress).not.toContain("private reasoning"); + }); + it.each([ + ["end_turn", "stop"], ["max_tokens", "length"], ["tool_use", "toolUse"], ["error", "error"], + ["filtered", "error"], ["other", "error"], ["aborted", "aborted"], ["paused", "aborted"], ["max_steps", "aborted"], + ])("maps the actual native %s reason to closed canonical %s", (native, canonical) => { + const projection = createKimiMessageProjection>(); + projection.normalize(chunk(1, "start")); + expect(projection.normalize(chunk(1, `end:${native}`)).assistantMessageBoundary).toEqual({ phase: "end", stopReason: canonical }); + projection.settle(); + expect(project([chunk(1, "start"), chunk(1, "delta", "native output"), chunk(1, `end:${native}`)]).final) + .toBe(["end_turn", "max_tokens"].includes(native) ? "native output" : ""); + }); + it("retains empty final boundaries and permits notice-only turns without inventing an answer", () => { + expect(project([chunk(1, "start"), chunk(1, "delta", "old answer"), chunk(1, "end:end_turn"), chunk(2, "start"), chunk(2, "end:end_turn")]).final).toBe(""); + const projection = createKimiMessageProjection<{ type: string }>(); + expect(projection.normalize({ type: "status" })).toEqual({ type: "status" }); projection.settle(); + }); + it.each([ + [chunk(1, "delta", "missing start")], [chunk(1, "end:end_turn")], [chunk(1, "start"), chunk(1, "start")], + [chunk(1, "start"), chunk(2, "delta", "wrong id")], [chunk(1, "start"), chunk(2, "end:end_turn")], + [chunk(1, "start"), chunk(1, "end:invented")], [chunk(1, "start"), chunk(1, "end:constructor")], + [chunk(1, "start"), chunk(1, "end:end_turn"), chunk(1, "start")], [chunk(1, "start", "nonempty boundary")], + [chunk(1, "start", "", "thought")], [chunk(1, "start"), chunk(1, "end:end_turn", "nonempty end")], [chunk(1, "start")], + [{ ...chunk(1, "start"), messageId: "fabricated-id" }], [{ ...chunk(1, "start"), meta: { origin: "forged", source: "kimi-turn-step-v1", kind: "start" } }], + [{ ...chunk(1, "start"), meta: { origin: "kimi-native-assistant", source: "unversioned", kind: "start" } }], + [{ ...chunk(1, "start"), assistantMessageBoundary: { phase: "start" } }], + ])("fails closed for missing, reused or forged native provenance %#", (...events) => { + expect(() => project(events)).toThrow("provenance"); + }); + it("poisons a malformed stream instead of accepting a later valid-looking boundary", () => { + const projection = createKimiMessageProjection>(); + expect(() => projection.normalize(chunk(1, "delta", "orphan"))).toThrow("provenance"); + expect(() => projection.normalize(chunk(2, "start"))).toThrow("provenance"); + expect(() => projection.settle()).toThrow("provenance"); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.ts new file mode 100644 index 00000000000..8f9935f08c8 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-message-boundary.ts @@ -0,0 +1,47 @@ +type RuntimeMessageEvent = { type: string; text?: string; stream?: string; messageId?: string; meta?: Record }; +export interface KimiMessageBoundary { phase: "start" | "end"; stopReason?: "stop" | "length" | "toolUse" | "error" | "aborted" } +export type KimiProjectedMessageEvent = T & { assistantMessageBoundary?: KimiMessageBoundary; assistantMessageHistory?: true }; + +const STOP_REASONS: Readonly> = Object.freeze({ + end_turn: "stop", max_tokens: "length", tool_use: "toolUse", error: "error", filtered: "error", other: "error", + aborted: "aborted", paused: "aborted", max_steps: "aborted", +}); + +export function createKimiMessageProjection() { + let active: string | null = null; + const seen = new Set(); + let poisoned = false; + const fail = (): never => { poisoned = true; throw new Error("Kimi native message provenance is invalid or incomplete"); }; + return { + normalize(event: T): KimiProjectedMessageEvent { + if (poisoned) return fail(); + if ("assistantMessageBoundary" in event || "assistantMessageHistory" in event) return fail(); + if (event.type !== "text_delta") return event; + const { messageId, meta } = event; + if (typeof messageId !== "string" || !/^kimi-message-[a-f0-9]{64}$/.test(messageId) + || meta?.origin !== "kimi-native-assistant" || meta.source !== "kimi-turn-step-v1" + || typeof event.text !== "string" || (event.stream !== "output" && event.stream !== "thought")) return fail(); + if (meta.kind === "delta") { + if (active !== messageId) return fail(); + return event; + } + if (event.text !== "" || event.stream !== "output") return fail(); + if (meta.kind === "start") { + if (active || seen.has(messageId) || seen.size >= 4096) return fail(); + seen.add(messageId); active = messageId; + return { ...event, assistantMessageBoundary: { phase: "start" } }; + } + if (active !== messageId || typeof meta.kind !== "string" || !meta.kind.startsWith("end:")) return fail(); + const reason = meta.kind.slice(4); + if (!Object.hasOwn(STOP_REASONS, reason)) return fail(); + active = null; + return { ...event, assistantMessageBoundary: { phase: "end", stopReason: STOP_REASONS[reason] } }; + }, + settle(): void { if (poisoned || active) fail(); }, + }; +} + +export function kimiMessageBoundaryClears(event: { assistantMessageBoundary?: KimiMessageBoundary }): boolean { + const boundary = event.assistantMessageBoundary; + return boundary?.phase === "start" || (boundary?.phase === "end" && !["stop", "length"].includes(boundary.stopReason ?? "")); +} diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.test.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.test.ts new file mode 100644 index 00000000000..e5df3bce3cf --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.test.ts @@ -0,0 +1,141 @@ +import { bindAcpxCandidateAdmission, type AcpxCandidateAdmission } from "./candidate-admission.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256, KIMI_NODE_DISTRIBUTIONS, KIMI_NODE_VERSION, KIMI_RUNTIME_VERSION } from "./kimi-closure-pins.js"; +import { KimiMetadataProbeError } from "./kimi-models.js"; +import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js"; + +const mocks = vi.hoisted(() => ({ installation: vi.fn(), open: vi.fn(), close: vi.fn(), metadata: vi.fn() })); +vi.mock("./kimi-installation.js", () => ({ verifyKimiInstallation: mocks.installation })); +vi.mock("./kimi-models.js", async original => ({ ...await original(), probeKimiModels: mocks.metadata })); +import { kimiMetadataProbeNodeDigest, probeKimiMetadata, validateKimiMetadata } from "./kimi-metadata-probe.js"; + +const model = "selected-model"; +const apiKey = "selected-secret-canary"; +const target = `${process.platform}-${process.arch}` as keyof typeof KIMI_DISTRIBUTION_CLOSURE_SHA256; +const admission = () => bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "metadata-company", agent: "kimi", model, + profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest, + nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`, + platform: process.platform, architecture: process.arch, expiresAt: Date.now() + 60_000 } as AcpxCandidateAdmission, + { runId: "metadata-operation", normalizedSessionId: "metadata-session" }); +const receipt = () => ({ schema: "paperclip.kimi.metadata.v1", status: "verified", version: KIMI_RUNTIME_VERSION, + nodeVersion: KIMI_NODE_VERSION, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest, target, + closureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`, model, contextLength: 1048576, promptSent: false }); + +beforeEach(() => { + vi.resetAllMocks(); + mocks.installation.mockResolvedValue({ openCommand: mocks.open }); + mocks.open.mockResolvedValue({ close: mocks.close }); + mocks.close.mockResolvedValue(undefined); + mocks.metadata.mockResolvedValue({ model, contextLength: 1048576 }); +}); +afterEach(() => { vi.unstubAllEnvs(); }); + +describe("Kimi setup metadata admission", () => { + it("verifies and closes the complete installation before authenticating the exact selected model", async () => { + const order: string[] = []; + mocks.open.mockImplementation(async () => { order.push("verify-closure"); return { close: mocks.close }; }); + mocks.close.mockImplementation(async () => { order.push("cleanup"); }); + mocks.metadata.mockImplementation(async () => { order.push("authenticate"); return { model, contextLength: 1048576 }; }); + vi.stubEnv("KIMI_API_KEY", "unselected-ambient-secret"); + vi.stubEnv("KIMI_MODEL_DEFAULT", "unselected-model"); + await expect(probeKimiMetadata(model, apiKey, admission())).resolves.toEqual(receipt()); + expect(order).toEqual(["verify-closure", "cleanup", "authenticate"]); + expect(mocks.installation).toHaveBeenCalledWith(expect.objectContaining({ + agent: "kimi", commandDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest, + qualificationModel: model, reportedModelId: model, + })); + expect(mocks.metadata).toHaveBeenCalledExactlyOnceWith(model, apiKey); + }); + + it.each(["metadata", "closure", "cleanup"])("rejects %s installation failure before authenticating", async stage => { + const error = new Error(`private runtime failure ${apiKey}`); + if (stage === "metadata") mocks.installation.mockRejectedValue(error); + if (stage === "closure") mocks.open.mockRejectedValue(error); + if (stage === "cleanup") mocks.close.mockRejectedValue(error); + const result = await probeKimiMetadata(model, apiKey, admission()); + expect(result).toMatchObject({ status: "failed", code: "KIMI_INSTALLATION_INVALID", promptSent: false }); + expect(JSON.stringify(result)).not.toContain(apiKey); + expect(mocks.metadata).not.toHaveBeenCalled(); + }); + + it("refuses pending metadata work without exact trusted startup authority before files or HTTP", async () => { + expect(await probeKimiMetadata(model, apiKey)).toMatchObject({ status: "failed", code: "KIMI_QUALIFICATION_REQUIRED", promptSent: false }); + const valid = admission(); + for (const altered of [{ ...valid, model: "other-model" }, { ...valid, agent: "antigravity" }, + { ...valid, expiresAt: 0 }, { ...valid, profileDigest: `sha256:${"f".repeat(64)}` }, + { ...valid, nativeClosureDigest: `sha256:${"e".repeat(64)}` }, { ...valid, extra: true }]) { + expect(await probeKimiMetadata(model, apiKey, altered as typeof valid)).toMatchObject({ code: "KIMI_QUALIFICATION_REQUIRED" }); + } + expect(mocks.installation).not.toHaveBeenCalled(); expect(mocks.metadata).not.toHaveBeenCalled(); + }); + + it("requires no operator grant after canonical release metadata admits Kimi", async () => { + vi.resetModules(); + vi.doMock("./generated-profiles.js", async original => { + const data = await original(); + const current = structuredClone(data.QUALIFIED_ACPX_PROFILE_DATA); + const released = { ...current.kimi } as Record; + delete released.qualificationStatus; + return { ...data, QUALIFIED_ACPX_PROFILE_DATA: { ...current, kimi: released } }; + }); + try { + const released = await import("./kimi-metadata-probe.js"); + expect(await released.probeKimiMetadata(model, apiKey)).toMatchObject({ status: "verified", model, promptSent: false }); + expect(mocks.metadata).toHaveBeenCalledExactlyOnceWith(model, apiKey); + } finally { vi.doUnmock("./generated-profiles.js"); vi.resetModules(); } + }); + + it("requires an explicit bounded credential and exact model before installation or HTTP work", async () => { + vi.stubEnv("KIMI_API_KEY", apiKey); + for (const invalid of ["", " ", "key\nheader", "key\rheader", "key\0header", "x".repeat(16385)]) { + expect(await probeKimiMetadata(model, invalid)).toMatchObject({ code: "KIMI_AUTH_REQUIRED" }); + } + for (const invalid of ["", " ", "model\0value", "x".repeat(513), "\ud800"]) { + expect(await probeKimiMetadata(invalid, apiKey)).toMatchObject({ code: "KIMI_MODEL_UNAVAILABLE" }); + } + expect(mocks.installation).not.toHaveBeenCalled(); + expect(mocks.metadata).not.toHaveBeenCalled(); + }); + + it.each(["KIMI_AUTH_REJECTED", "KIMI_MODEL_UNAVAILABLE", "KIMI_METADATA_TIMEOUT", "KIMI_METADATA_INVALID", "KIMI_PROVIDER_ERROR"] as const)("preserves the canonical %s failure without provider text", async code => { + mocks.metadata.mockRejectedValue(new KimiMetadataProbeError(code)); + expect(await probeKimiMetadata(model, apiKey, admission())).toMatchObject({ status: "failed", code, promptSent: false }); + mocks.metadata.mockRejectedValue(new Error(`provider failure ${apiKey}`)); + const result = await probeKimiMetadata(model, apiKey, admission()); + expect(result).toMatchObject({ status: "failed", code: "KIMI_REQUEST_FAILED", promptSent: false }); + expect(JSON.stringify(result)).not.toContain(apiKey); + }); +}); + +describe("untrusted Kimi metadata receipts", () => { + it("accepts only the current source identity and exact selected model", () => { + expect(validateKimiMetadata(receipt(), model)).toEqual(receipt()); + expect(kimiMetadataProbeNodeDigest()).toBe(`sha256:${KIMI_NODE_DISTRIBUTIONS["linux-x64"].executableSha256}`); + expect(validateKimiMetadata(receipt(), "different-model")).toMatchObject({ code: "KIMI_MODEL_UNAVAILABLE" }); + }); + + it.each([ + { version: "latest" }, { nodeVersion: "24.0.0" }, { profileDigest: `sha256:${"0".repeat(64)}` }, + { closureDigest: `sha256:${"0".repeat(64)}` }, { target: "linux-arm64" }, + ])("rejects an incompatible installation %#", alteration => { + expect(validateKimiMetadata({ ...receipt(), ...alteration }, model)).toMatchObject({ + status: "failed", code: "KIMI_INSTALLATION_INVALID", promptSent: false, + }); + }); + + it.each([null, [], {}, { ...receipt(), promptSent: true }, { ...receipt(), injected: true }, + { ...receipt(), schema: "other" }, { schema: "paperclip.kimi.metadata.v1", status: "failed", code: "invented", promptSent: false }, + ])("rejects malformed or open receipt contracts %#", value => { + expect(validateKimiMetadata(value, model)).toMatchObject({ status: "failed", code: "KIMI_REQUEST_FAILED" }); + }); + + it.each([0, -1, 1.5, "1048576", Number.MAX_SAFE_INTEGER + 1, undefined])("rejects invalid context length %s", contextLength => { + expect(validateKimiMetadata({ ...receipt(), contextLength }, model)).toMatchObject({ code: "KIMI_METADATA_INVALID" }); + }); + + it("replaces remote failure messages with canonical text", () => { + const result = validateKimiMetadata({ schema: "paperclip.kimi.metadata.v1", status: "failed", code: "KIMI_AUTH_REJECTED", message: apiKey, promptSent: false }, model); + expect(result).toMatchObject({ code: "KIMI_AUTH_REJECTED", promptSent: false }); + expect(JSON.stringify(result)).not.toContain(apiKey); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.ts new file mode 100644 index 00000000000..5a6a1068620 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.ts @@ -0,0 +1,94 @@ +import { assertAcpxExecutionAdmission, type AcpxCandidateStartupAdmission } from "./candidate-admission.js"; +import { createHash } from "node:crypto"; +import { constants } from "node:fs"; +import { lstat, open } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256, KIMI_NODE_DISTRIBUTIONS, KIMI_NODE_VERSION, KIMI_RUNTIME_VERSION } from "./kimi-closure-pins.js"; +import { verifyKimiInstallation } from "./kimi-installation.js"; +import { KimiMetadataProbeError, probeKimiModels, type KimiMetadataProbeErrorCode } from "./kimi-models.js"; +import { resolveQualifiedAcpxProfile, QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js"; + +const SCHEMA = "paperclip.kimi.metadata.v1"; +const providerCodes = ["KIMI_AUTH_REQUIRED", "KIMI_AUTH_REJECTED", "KIMI_MODEL_UNAVAILABLE", "KIMI_METADATA_TIMEOUT", "KIMI_METADATA_INVALID", "KIMI_PROVIDER_ERROR"] as const; +type FailureCode = KimiMetadataProbeErrorCode | "KIMI_INSTALLATION_INVALID" | "KIMI_REQUEST_FAILED" | "KIMI_QUALIFICATION_REQUIRED"; +export type KimiMetadataResult = { + schema: typeof SCHEMA; status: "verified"; version: typeof KIMI_RUNTIME_VERSION; + nodeVersion: typeof KIMI_NODE_VERSION; profileDigest: string; target: string; + closureDigest: string; model: string; contextLength: number; promptSent: false; +} | { schema: typeof SCHEMA; status: "failed"; code: FailureCode; message: string; promptSent: false }; + +function failed(code: FailureCode): Extract { + const message = code === "KIMI_QUALIFICATION_REQUIRED" + ? "Kimi Code qualification is pending; metadata verification requires trusted operator admission." + : code === "KIMI_INSTALLATION_INVALID" + ? "Install the current verified Kimi Code runtime in this execution environment." + : code === "KIMI_REQUEST_FAILED" ? "Kimi Code metadata verification failed." + : new KimiMetadataProbeError(code).message; + return { schema: SCHEMA, status: "failed", code, message, promptSent: false }; +} + +export async function probeKimiMetadata(model: string, apiKey: string, candidateAdmission?: AcpxCandidateStartupAdmission): Promise { + if (typeof apiKey !== "string" || !apiKey.trim() || /[\0\r\n]/.test(apiKey) || Buffer.byteLength(apiKey) > 16 * 1024) return failed("KIMI_AUTH_REQUIRED"); + if (typeof model !== "string" || !model.trim() || model.includes("\0") || Buffer.byteLength(model) > 512 || Buffer.from(model).toString("utf8") !== model) return failed("KIMI_MODEL_UNAVAILABLE"); + const target = `${process.platform}-${process.arch}`; + if (!Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, target)) return failed("KIMI_INSTALLATION_INVALID"); + const profile = resolveQualifiedAcpxProfile("kimi", model); + try { assertAcpxExecutionAdmission(profile, candidateAdmission); } + catch { return failed("KIMI_QUALIFICATION_REQUIRED"); } + try { + const installation = await verifyKimiInstallation(profile); + const lease = await installation.openCommand(); + await lease.close(); + } catch { return failed("KIMI_INSTALLATION_INVALID"); } + try { + const metadata = await probeKimiModels(model, apiKey); + return { schema: SCHEMA, status: "verified", version: KIMI_RUNTIME_VERSION, nodeVersion: KIMI_NODE_VERSION, + profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest, target, + closureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target as keyof typeof KIMI_DISTRIBUTION_CLOSURE_SHA256]}`, + model: metadata.model, contextLength: metadata.contextLength, promptSent: false }; + } catch (error) { return failed(error instanceof KimiMetadataProbeError ? error.code : "KIMI_REQUEST_FAILED"); } +} + +export function validateKimiMetadata(value: unknown, model: string): KimiMetadataResult { + if (!value || typeof value !== "object" || Array.isArray(value)) return failed("KIMI_REQUEST_FAILED"); + const row = value as Record; + if (row.schema !== SCHEMA || row.promptSent !== false) return failed("KIMI_REQUEST_FAILED"); + if (row.status === "failed") { + if (Object.keys(row).some(key => !["schema", "status", "code", "message", "promptSent"].includes(key)) + || typeof row.code !== "string" || ![...providerCodes, "KIMI_INSTALLATION_INVALID", "KIMI_REQUEST_FAILED", "KIMI_QUALIFICATION_REQUIRED"].includes(row.code)) return failed("KIMI_REQUEST_FAILED"); + return failed(row.code as FailureCode); + } + if (row.status !== "verified" || Object.keys(row).some(key => !["schema", "status", "version", "nodeVersion", "profileDigest", "target", "closureDigest", "model", "contextLength", "promptSent"].includes(key))) return failed("KIMI_REQUEST_FAILED"); + if (row.version !== KIMI_RUNTIME_VERSION || row.nodeVersion !== KIMI_NODE_VERSION + || row.profileDigest !== QUALIFIED_ACPX_PROFILES.kimi.commandDigest || typeof row.target !== "string" + || !Object.hasOwn(KIMI_DISTRIBUTION_CLOSURE_SHA256, row.target) + || row.closureDigest !== `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[row.target as keyof typeof KIMI_DISTRIBUTION_CLOSURE_SHA256]}`) return failed("KIMI_INSTALLATION_INVALID"); + if (row.model !== model) return failed("KIMI_MODEL_UNAVAILABLE"); + if (!Number.isSafeInteger(row.contextLength) || (row.contextLength as number) <= 0) return failed("KIMI_METADATA_INVALID"); + return row as Extract; +} + +export function kimiMetadataProbeNodeDigest(): string { + return `sha256:${KIMI_NODE_DISTRIBUTIONS["linux-x64"].executableSha256}`; +} + +export async function kimiMetadataProbeDigest(): Promise { + const url = new URL(import.meta.url); + const relative = /\/dist\/vendor\/paperclip-runner\/drivers\/acpx\/kimi-metadata-probe\.js$/.test(url.pathname) + ? "../../cli/kimi-metadata-probe.cjs" + : /\/(?:src|dist)\/drivers\/acpx\/kimi-metadata-probe\.(?:ts|js)$/.test(url.pathname) + ? "../../../dist/cli/kimi-metadata-probe.cjs" : null; + if (url.protocol !== "file:" || url.search || url.hash || relative === null) throw new Error("Kimi metadata probe requires its fixed build layout"); + const path = fileURLToPath(new URL(relative, url)); + const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const before = await file.stat({ bigint: true }); + if (!before.isFile() || before.size < 1n || before.size > 4n * 1024n * 1024n) throw new Error("Kimi metadata probe artifact is invalid"); + const bytes = await file.readFile(); + const after = await file.stat({ bigint: true }), named = await lstat(path, { bigint: true }); + if (bytes.length !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino + || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || before.size !== after.size + || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("Kimi metadata probe artifact changed during admission"); + return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; + } finally { await file.close(); } +} diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-models.test.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-models.test.ts new file mode 100644 index 00000000000..4ab3a179fcb --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-models.test.ts @@ -0,0 +1,143 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { KimiMetadataProbeError, probeKimiModels } from "./kimi-models.js"; + +const model = "kimi-for-coding"; +const key = "selected-key-canary"; +const payload = { data: [{ id: "別のモデル", limit: { context: 262144 } }, { id: model, limit: { context: 1048576 } }] }; +const response = (value: unknown) => new Response(JSON.stringify(value), { headers: { "content-type": "application/json" } }); +const dependency = (value: Response) => ({ fetch: vi.fn().mockResolvedValue(value) }); + +afterEach(() => { vi.useRealTimers(); vi.unstubAllEnvs(); }); + +describe("Kimi Code authenticated model metadata", () => { + it("uses only the selected key, pinned endpoint and official client identity without inference", async () => { + vi.stubEnv("KIMI_API_KEY", "ambient-unselected-key"); + vi.stubEnv("KIMI_CODE_BASE_URL", "https://unselected.example"); + const bytes = Buffer.from(JSON.stringify(payload)); + const split = bytes.indexOf(Buffer.from("別")) + 1; + const deps = dependency(new Response(new ReadableStream({ start(controller) { + controller.enqueue(bytes.subarray(0, split)); controller.enqueue(bytes.subarray(split)); controller.close(); + } }))); + await expect(probeKimiModels(model, key, deps)).resolves.toEqual({ + code: "kimi_metadata_verified", model, contextLength: 1048576, + message: expect.stringContaining("No model prompt was sent"), + }); + expect(deps.fetch).toHaveBeenCalledTimes(1); + const [url, init] = deps.fetch.mock.calls[0]!; + expect(url).toBe("https://api.kimi.ai/coding/v1/models"); + expect(init).toMatchObject({ method: "GET", redirect: "error", headers: { + Authorization: `Bearer ${key}`, Accept: "application/json", "User-Agent": "KimiCLI/2.3.3", + } }); + expect(init?.body).toBeUndefined(); + expect(init?.signal).toBeInstanceOf(AbortSignal); + }); + + it("rejects absent credentials before fetching and never falls back to ambient login", async () => { + vi.stubEnv("KIMI_API_KEY", key); + const deps = dependency(response(payload)); + for (const invalid of ["", " ", "x\ny", "x\ry", "x\0y", "x".repeat(16385)]) { + await expect(probeKimiModels(model, invalid, deps)).rejects.toMatchObject({ code: "KIMI_AUTH_REQUIRED" }); + } + expect(deps.fetch).not.toHaveBeenCalled(); + }); + + it.each([401, 402, 403])("preserves typed HTTP %i credential/access rejection without reading its body", async status => { + const cancel = vi.fn(); + const body = new ReadableStream({ cancel }); + const getReader = vi.spyOn(body, "getReader"); + const deps = dependency(new Response(body, { status })); + await expect(probeKimiModels(model, key, deps)).rejects.toMatchObject({ + name: "KimiMetadataProbeError", code: "KIMI_AUTH_REJECTED", status, + }); + expect(getReader).not.toHaveBeenCalled(); + expect(cancel).toHaveBeenCalled(); + }); + + it("requires the exact selected model without aliases or fallback", async () => { + for (const selected of ["KIMI-FOR-CODING", "kimi-for-coding ", "unlisted", "", "x\0y", "x".repeat(513)]) { + await expect(probeKimiModels(selected, key, dependency(response(payload)))) + .rejects.toMatchObject({ code: "KIMI_MODEL_UNAVAILABLE" }); + } + }); + + it.each([ + null, [], {}, { data: {} }, { data: [null] }, { data: [{ id: "", limit: { context: 1 } }] }, + { data: [{ id: model }] }, { data: [{ id: model, limit: { context: 0 } }] }, + { data: [{ id: model, limit: { context: -1 } }] }, { data: [{ id: model, limit: { context: 1.2 } }] }, + { data: [{ id: model, limit: { context: true } }] }, { data: [{ id: model, limit: { context: "" } }] }, + { data: [{ id: model, limit: { context: Number.MAX_SAFE_INTEGER + 1 } }] }, + { data: [{ id: model, limit: { context: 1 } }, { id: model, limit: { context: 2 } }] }, + ])("rejects invalid or ambiguous metadata %#", async value => { + await expect(probeKimiModels(model, key, dependency(response(value)))).rejects.toMatchObject({ code: "KIMI_METADATA_INVALID" }); + }); + + it("accepts the native numeric-string context representation", async () => { + await expect(probeKimiModels(model, key, dependency(response({ data: [{ id: model, limit: { context: "1048576" } }] })))) + .resolves.toMatchObject({ contextLength: 1048576 }); + }); + + it("bounds streamed bytes even when Content-Length is missing or false", async () => { + for (const headers of [{}, { "content-length": "10" }]) { + const cancel = vi.fn(); + const deps = dependency(new Response(new ReadableStream({ start(controller) { + controller.enqueue(new Uint8Array(256 * 1024)); controller.enqueue(new Uint8Array(1)); + }, cancel }), { headers })); + await expect(probeKimiModels(model, key, deps)).rejects.toMatchObject({ code: "KIMI_METADATA_INVALID" }); + expect(deps.fetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); + expect(cancel).toHaveBeenCalled(); + } + }); + + it("rejects oversized declared bodies before consuming them", async () => { + const cancel = vi.fn(); + const deps = dependency(new Response(new ReadableStream({ cancel }), { headers: { "content-length": "262145" } })); + await expect(probeKimiModels(model, key, deps)).rejects.toMatchObject({ code: "KIMI_METADATA_INVALID" }); + expect(cancel).toHaveBeenCalled(); + }); + + it.each([new Uint8Array([0xff]), Buffer.from("invalid JSON")])("rejects invalid UTF-8 or JSON %#", async bytes => { + await expect(probeKimiModels(model, key, dependency(new Response(bytes)))) + .rejects.toMatchObject({ code: "KIMI_METADATA_INVALID" }); + }); + + it("aborts an outstanding request at the fixed eight-second deadline", async () => { + vi.useFakeTimers(); + let signal: AbortSignal | undefined; + const aborted = vi.fn(); + const deps = { fetch: vi.fn((_url, init) => new Promise((_, reject) => { + signal = init?.signal ?? undefined; + signal?.addEventListener("abort", () => { aborted(); reject(new Error(`request failure ${key}`)); }); + })) }; + const rejected = expect(probeKimiModels(model, key, deps)).rejects.toMatchObject({ code: "KIMI_METADATA_TIMEOUT" }); + await vi.advanceTimersByTimeAsync(7999); + expect(aborted).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(1); + await rejected; + expect(signal?.aborted).toBe(true); + expect(aborted).toHaveBeenCalledTimes(1); + }); + + it("keeps the deadline active after headers and cancels a stalled response body", async () => { + vi.useFakeTimers(); + const cancel = vi.fn(); + const deps = dependency(new Response(new ReadableStream({ cancel }))); + const rejected = expect(probeKimiModels(model, key, deps)).rejects.toMatchObject({ code: "KIMI_METADATA_TIMEOUT" }); + await vi.advanceTimersByTimeAsync(8000); + await rejected; + expect(deps.fetch.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); + expect(cancel).toHaveBeenCalled(); + }); + + it("sanitizes provider responses and transport exceptions", async () => { + for (const deps of [dependency(new Response(`private response ${key}`, { status: 500 })), + { fetch: vi.fn().mockRejectedValue(new Error(`private request ${key}`)) }]) { + let failure: unknown; + try { await probeKimiModels(model, key, deps); } catch (error) { failure = error; } + expect(failure).toBeInstanceOf(KimiMetadataProbeError); + expect(failure).toMatchObject({ code: "KIMI_PROVIDER_ERROR" }); + expect(JSON.stringify(failure)).not.toContain(key); + expect(String(failure)).not.toContain(key); + expect(failure).not.toHaveProperty("cause"); + } + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-models.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-models.ts new file mode 100644 index 00000000000..021c11bd3c2 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-models.ts @@ -0,0 +1,140 @@ +const MODELS_URL = "https://api.kimi.ai/coding/v1/models"; +const USER_AGENT = "KimiCLI/2.3.3"; +const MAX_BYTES = 256 * 1024; +const TIMEOUT_MS = 8_000; + +export type KimiMetadataProbeErrorCode = + | "KIMI_AUTH_REQUIRED" + | "KIMI_AUTH_REJECTED" + | "KIMI_MODEL_UNAVAILABLE" + | "KIMI_METADATA_TIMEOUT" + | "KIMI_METADATA_INVALID" + | "KIMI_PROVIDER_ERROR"; + +const messages: Record = { + KIMI_AUTH_REQUIRED: "Bind a valid company secret to KIMI_API_KEY.", + KIMI_AUTH_REJECTED: "Kimi Code rejected the selected credential or account access. Check the credential and Code membership.", + KIMI_MODEL_UNAVAILABLE: "The exact selected model is unavailable in the authenticated Kimi Code catalog.", + KIMI_METADATA_TIMEOUT: "Kimi Code model verification timed out.", + KIMI_METADATA_INVALID: "Kimi Code returned invalid model metadata.", + KIMI_PROVIDER_ERROR: "Kimi Code model verification failed. Try again when the provider is available.", +}; + +export class KimiMetadataProbeError extends Error { + readonly code: KimiMetadataProbeErrorCode; + readonly status?: number; + + constructor(code: KimiMetadataProbeErrorCode, status?: number) { + super(messages[code]); + this.name = "KimiMetadataProbeError"; + this.code = code; + this.status = status; + } +} + +export interface KimiVerifiedModelMetadata { + code: "kimi_metadata_verified"; + model: string; + contextLength: number; + message: string; +} + +function record(value: unknown): Record | undefined { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; +} + +function selectedModel(payload: unknown, requestedModel: string): number { + const data = record(payload)?.data; + if (!Array.isArray(data)) throw new KimiMetadataProbeError("KIMI_METADATA_INVALID"); + const ids = new Set(); + let contextLength: number | undefined; + for (const value of data) { + const row = record(value); + const id = row?.id; + const context = record(row?.limit)?.context; + const parsed = typeof context === "number" || (typeof context === "string" && /^[0-9]+$/.test(context)) ? Number(context) : NaN; + if (typeof id !== "string" || !id.trim() || ids.has(id) || !Number.isSafeInteger(parsed) || parsed <= 0) { + throw new KimiMetadataProbeError("KIMI_METADATA_INVALID"); + } + ids.add(id); + if (id === requestedModel) contextLength = parsed; + } + if (contextLength === undefined) throw new KimiMetadataProbeError("KIMI_MODEL_UNAVAILABLE"); + return contextLength; +} + +export async function probeKimiModels( + model: string, + apiKey: string, + testDependencies?: { fetch: typeof globalThis.fetch }, +): Promise { + if (typeof apiKey !== "string" || !apiKey.trim() || Buffer.byteLength(apiKey) > 16 * 1024 || /[\0\r\n]/.test(apiKey)) { + throw new KimiMetadataProbeError("KIMI_AUTH_REQUIRED"); + } + if (typeof model !== "string" || !model.trim() || Buffer.byteLength(model) > 512 || model.includes("\0") + || Buffer.from(model).toString("utf8") !== model) throw new KimiMetadataProbeError("KIMI_MODEL_UNAVAILABLE"); + const controller = new AbortController(); + const timeoutError = new KimiMetadataProbeError("KIMI_METADATA_TIMEOUT"); + let reader: ReadableStreamDefaultReader | undefined; + let response: Response | undefined; + let rejectAbort!: (reason: unknown) => void; + const aborted = new Promise((_, reject) => { rejectAbort = reject; }); + const onAbort = () => { + rejectAbort(timeoutError); + void reader?.cancel().catch(() => {}); + }; + controller.signal.addEventListener("abort", onAbort, { once: true }); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { + const request = Promise.resolve().then(() => (testDependencies?.fetch ?? globalThis.fetch)(MODELS_URL, { + method: "GET", redirect: "error", signal: controller.signal, + headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "User-Agent": USER_AGENT }, + })).then(value => { + if (controller.signal.aborted) { + void value.body?.cancel().catch(() => {}); + throw timeoutError; + } + return value; + }); + response = await Promise.race([request, aborted]); + if (!response.ok) throw new KimiMetadataProbeError( + [401, 402, 403].includes(response.status) ? "KIMI_AUTH_REJECTED" : "KIMI_PROVIDER_ERROR", response.status, + ); + const declaredLength = response.headers.get("content-length"); + if (declaredLength !== null && (!/^[0-9]+$/.test(declaredLength) || Number(declaredLength) > MAX_BYTES)) { + controller.abort(); + throw new KimiMetadataProbeError("KIMI_METADATA_INVALID"); + } + if (!response.body) throw new KimiMetadataProbeError("KIMI_METADATA_INVALID"); + reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let bytes = 0; + for (;;) { + const chunk = await Promise.race([reader.read(), aborted]); + if (controller.signal.aborted) throw timeoutError; + if (chunk.done) break; + bytes += chunk.value.byteLength; + if (bytes > MAX_BYTES) { + controller.abort(); + throw new KimiMetadataProbeError("KIMI_METADATA_INVALID"); + } + chunks.push(chunk.value); + } + let payload: unknown; + try { payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, bytes))); } + catch { throw new KimiMetadataProbeError("KIMI_METADATA_INVALID"); } + const contextLength = selectedModel(payload, model); + return { code: "kimi_metadata_verified", model, contextLength, + message: "Kimi Code accepted the selected credential and listed the exact selected model. No model prompt was sent; inference and remaining quota were not tested." }; + } catch (error) { + if (error instanceof KimiMetadataProbeError) throw error; + throw new KimiMetadataProbeError(controller.signal.aborted ? "KIMI_METADATA_TIMEOUT" : "KIMI_PROVIDER_ERROR"); + } finally { + clearTimeout(timer); + controller.signal.removeEventListener("abort", onAbort); + if (reader) { + void reader.cancel().catch(() => {}); + try { reader.releaseLock(); } catch {} + } else { void response?.body?.cancel().catch(() => {}); } + } +} diff --git a/packages/paperclip-runner/src/drivers/acpx/kimi-runtime-files.ts b/packages/paperclip-runner/src/drivers/acpx/kimi-runtime-files.ts new file mode 100644 index 00000000000..68c42ef8fbd --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/kimi-runtime-files.ts @@ -0,0 +1,60 @@ +import { createHash } from "node:crypto"; +import { constants } from "node:fs"; +import { lstat, open, readdir, realpath } from "node:fs/promises"; +import { join } from "node:path"; + +export interface KimiRuntimeFile { path: string; sha256: string; size: number; executable: boolean } +const MAX_FILES = 30_000; +const MAX_FILE_BYTES = 384 * 1024 * 1024; +const MAX_TREE_BYTES = 1024 * 1024 * 1024; +const HASH_CONCURRENCY = 16; +const safePath = (path: string) => !/[\u0000-\u001f\u007f\\]/.test(path) && path.split("/").every(part => part !== "" && part !== "." && part !== ".."); + +export function kimiRuntimeClosureDigest(entries: readonly KimiRuntimeFile[]): string { + return createHash("sha256").update(JSON.stringify(entries)).digest("hex"); +} + +export async function inventoryKimiRuntimeFiles(root: string): Promise { + if (await realpath(root) !== root || !(await lstat(root)).isDirectory()) throw new Error("Kimi runtime root is not a physical directory"); + const files: { path: string; size: number; executable: boolean }[] = []; + let total = 0; + const visit = async (directory: string, prefix = ""): Promise => { + for (const name of (await readdir(directory)).sort()) { + const path = prefix ? `${prefix}/${name}` : name; + if (!safePath(path) || path.startsWith(".paperclip-") || path === "manifest.json") throw new Error("Kimi runtime contains an invalid path"); + const absolute = join(directory, name); + const stat = await lstat(absolute); + if (stat.isSymbolicLink() || await realpath(absolute) !== absolute) throw new Error("Kimi runtime contains a symbolic link"); + if (stat.isDirectory()) await visit(absolute, path); + else { + if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_FILE_BYTES) throw new Error("Kimi runtime contains an invalid file"); + total += stat.size; + if (files.length >= MAX_FILES || total > MAX_TREE_BYTES) throw new Error("Kimi runtime inventory exceeds its bound"); + files.push({ path, size: stat.size, executable: Boolean(stat.mode & 0o111) }); + } + } + }; + await visit(root); + files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); + const entries: KimiRuntimeFile[] = []; + for (let index = 0; index < files.length; index += HASH_CONCURRENCY) { + const batch = await Promise.allSettled(files.slice(index, index + HASH_CONCURRENCY).map(async entry => { + const path = join(root, entry.path); + const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const before = await file.stat({ bigint: true }); + if (!before.isFile() || before.nlink !== 1n || before.size !== BigInt(entry.size) || Boolean(before.mode & 0o111n) !== entry.executable) throw new Error("Kimi runtime file changed before inventory"); + const digest = createHash("sha256"); + for await (const chunk of file.createReadStream({ autoClose: false })) digest.update(chunk); + const after = await file.stat({ bigint: true }); + const named = await lstat(path, { bigint: true }); + if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("Kimi runtime file changed during inventory"); + return { path: entry.path, sha256: digest.digest("hex"), size: entry.size, executable: entry.executable }; + } finally { await file.close(); } + })); + const failed = batch.find(result => result.status === "rejected"); + if (failed?.status === "rejected") throw failed.reason; + for (const result of batch) if (result.status === "fulfilled") entries.push(result.value); + } + return entries; +} diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts index d122058e4de..49abf2327c4 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts @@ -7,6 +7,8 @@ import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js"; // at 1. Update this table, with the reason, only for a deliberate reset. const PROFILE_VERSIONS: Readonly> = { antigravity: 1, claude: 1, codex: 1, copilot: 92, cursor: 17, grok: 20, pi: 37, + // Retain the existing pending Kimi revision; only its release attestation changes. + kimi: 16, }; describe("ACPX profile versions", () => { @@ -28,7 +30,7 @@ describe("historical ACPX profile decoding", () => { } }); it("does not decode a revision newer than the current one", () => { - for (const agent of ["copilot", "grok", "pi", "cursor", "antigravity"] as const) { + for (const agent of ["copilot", "grok", "pi", "cursor", "antigravity", "kimi"] as const) { expect(isSupportedAcpxProfileVersion(agent, QUALIFIED_ACPX_PROFILE_DATA[agent].agentProfileVersion + 1)).toBe(false); } }); diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-installation.ts b/packages/paperclip-runner/src/drivers/acpx/profile-installation.ts index 2e21564352c..6c38fcba918 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-installation.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-installation.ts @@ -1,3 +1,4 @@ +import { verifyKimiInstallation } from "./kimi-installation.js"; import { assertAcpxExecutionAdmission, type AcpxCandidateStartupAdmission } from "./candidate-admission.js"; import { assertCopilotCredentials, classifyCopilotFailure } from "./copilot-profile.js"; import { verifyCopilotInstallation } from "./copilot-installation.js"; @@ -12,6 +13,7 @@ import { verifyQualifiedAcpxInstallation, type VerifiedAcpxInstallation } from " export async function verifyAcpxProfileInstallation(profile: QualifiedAcpxProfile, candidateAdmission?: AcpxCandidateStartupAdmission): Promise { assertAcpxExecutionAdmission(profile, candidateAdmission); if (profile.agent === "antigravity") return verifyAntigravityInstallation(profile, candidateAdmission); + if (profile.agent === "kimi") return verifyKimiInstallation(profile); if (profile.agent === "pi") return verifyPiInstallation(profile); if (profile.agent === "copilot") return verifyCopilotInstallation(profile); if (profile.agent === "cursor") { @@ -35,6 +37,9 @@ export async function assertAcpxProfileWorkspace(agent: QualifiedAcpxAgent, work /** Called with the sanitized launch environment, never ambient process.env. */ export function assertAcpxProfileEnvironment(agent: QualifiedAcpxAgent, environment: Readonly): void { if (agent === "copilot") assertCopilotCredentials(environment); + if (agent === "kimi" && !environment.KIMI_API_KEY?.trim()) { + throw Object.assign(new Error("Kimi Code credentials are missing. Bind a company secret to KIMI_API_KEY in the agent environment."), { code: "KIMI_CREDENTIALS_MISSING", retryable: false }); + } if (agent === "antigravity" && !environment.GEMINI_API_KEY?.trim() && !environment.PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET?.trim()) { throw Object.assign(new Error("Google Antigravity requires an explicitly bound Google runtime connection. Connect a Google account or Gemini API key before starting a task."), { code: "ANTIGRAVITY_CREDENTIALS_MISSING", retryable: false }); } @@ -77,3 +82,8 @@ export async function probeAcpxPiInstallation(model: string): Promise { const installation = await verifyAcpxProfileInstallation(resolveQualifiedAcpxProfile("pi", model)); await (await installation.openCommand()).close(); } + +export async function probeAcpxKimiInstallation(model: string): Promise { + const installation = await verifyAcpxProfileInstallation(resolveQualifiedAcpxProfile("kimi", model)); + await (await installation.openCommand()).close(); +} diff --git a/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts b/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts index 3dee493616e..78ef412f092 100644 --- a/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts +++ b/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts @@ -2,7 +2,7 @@ import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, rea import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; -type NativeProvider = "cursor" | "copilot" | "pi" | "antigravity"; +type NativeProvider = "cursor" | "copilot" | "pi" | "kimi" | "antigravity"; const RUNNER_PACKAGE_NAME = "@paperclipai/paperclip-runner"; const SERVER_PACKAGE_NAME = "@paperclipai/server"; function assertServerVendorLayout(root: string): void { @@ -17,7 +17,7 @@ function assertServerVendorLayout(root: string): void { /** Resolve only runner-owned package assets, including descriptor-loaded sidecars. */ export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: NativeProvider): string { - if (!["cursor", "copilot", "pi", "antigravity"].includes(provider)) throw new Error("Unknown native ACP provider assets"); + if (!["cursor", "copilot", "pi", "kimi", "antigravity"].includes(provider)) throw new Error("Unknown native ACP provider assets"); const boundRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT; const boundManifest = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST; let packageRoot: string; diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index 51814ff481d..c01b8125811 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -173,6 +173,7 @@ export interface AcpxRuntimePortOpenOptions { stateDirectory: string; providerSessionKey: string; permissionMode: NativeAcpxPermissionMode; + readOnly?: boolean; mode?: string; piThinkingLevel?: PiThinkingLevel; claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max"; @@ -394,7 +395,7 @@ export class AcpxRuntimeHost { grokEffort: resolveGrokEffort(options.agent, options.grokEffort), grokDisableWebSearch: resolveGrokDisableWebSearch(options.agent, options.grokDisableWebSearch), ...(preconfigured || preparedHome ? { homeBinding: preconfiguredHomeEnvironment(options.environment) } : {}), - ...(["cursor", "copilot", "pi"].includes(options.agent) && options.providerPolicy !== undefined + ...(["cursor", "copilot", "pi", "kimi"].includes(options.agent) && options.providerPolicy !== undefined ? { providerPolicy: options.providerPolicy } : {}), }), dependencies.retainAdmissionCleanup, @@ -558,7 +559,7 @@ export class AcpxRuntimeHost { // agent and run. Environment/config values cannot widen the grant. Each // resumed process receives the newly registered copy; collection already // requires verified provider shutdown in the native executor. - const agentFiles = ["cursor", "copilot", "pi"].includes(options.agent) + const agentFiles = ["cursor", "copilot", "pi", "kimi"].includes(options.agent) ? bindAcpxAgentFiles(options.runtimeContext, [sandbox.root, ...(installation.agentServerPackageJsonPath === null ? [] : [dirname(installation.agentServerPackageJsonPath)]), ]) : null; @@ -601,7 +602,7 @@ export class AcpxRuntimeHost { PAPERCLIP_PI_READ_ROOTS: JSON.stringify([...(options.providerPolicy?.readRoots ?? []), ...skills.readRoots]), }); } - if (!preconfigured && !preparedHome && (options.agent === "claude" || options.agent === "grok")) { + if (!preconfigured && !preparedHome && (options.agent === "claude" || options.agent === "grok" || options.agent === "kimi")) { // The lifetime lease proves the previous provider has stopped. Refresh // the assigned snapshot before every launch, including durable resume; // Claude discovers user skills beneath its isolated CLAUDE_CONFIG_DIR. @@ -666,6 +667,7 @@ export class AcpxRuntimeHost { providerSessionKey: binding.profileSessionKey, restoringSession: options.expectedIdentity !== undefined, permissionMode: binding.permissionMode, + readOnly: options.providerPolicy?.readOnly, mode: binding.mode, piThinkingLevel: binding.piThinkingLevel, claudeEffort: binding.claudeEffort, diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts index fec3c663280..fe12d82c715 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts @@ -1,3 +1,5 @@ +import { kimiManagedConfiguration } from "./kimi-config.js"; +import { kimiInstructionBinding } from "./kimi-instructions.js"; import { AGENT_OBSERVABILITY_ENV_KEYS, codexObservabilityToml } from "../agent-observability.js"; import { piProviderConfiguration } from "./pi-provider-config.js"; import { configuredEnvironmentKeys } from "../../configured-environment.js"; @@ -421,6 +423,9 @@ export async function prepareAcpxRuntimeSandbox(input: { workspaceRecordPath, `${input.binding.workspacePath}\n`, ); + if (input.agent === "kimi") { + await writePrivateFile(join(agentHomeDirectory, "config.toml"), kimiManagedConfiguration(input.binding.requestedModel)); + } if (input.agent === "pi") { const configuration = piProviderConfiguration(input.environment); if (configuration) await writePrivateFile(join(agentHomeDirectory, "models.json"), configuration.json); @@ -533,6 +538,7 @@ export async function prepareAcpxRuntimeSandbox(input: { XDG_CACHE_HOME: cacheDirectory, PAPERCLIP_ACPX_PROFILE: input.agent, PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1", + ...(input.agent === "kimi" ? { KIMI_CODE_HOME: agentHomeDirectory, PAPERCLIP_KIMI_INSTRUCTIONS: kimiInstructionBinding(policy.systemInstructions).payload } : {}), ...(input.agent === "antigravity" ? { GEMINI_HOME: agentHomeDirectory, AGY_ACP_FORCE_FILE_STORAGE: "1" } : {}), ...(input.agent === "grok" ? { GROK_HOME: agentHomeDirectory, NO_BROWSER: "1", GROK_DISABLE_AUTOUPDATER: "1" } : {}), ...(input.agent === "pi" diff --git a/packages/paperclip-runner/src/index.ts b/packages/paperclip-runner/src/index.ts index 6b42469e678..264fb7ed4ed 100644 --- a/packages/paperclip-runner/src/index.ts +++ b/packages/paperclip-runner/src/index.ts @@ -1,6 +1,7 @@ export * from "./configured-environment.js"; export * from "./catalog/index.js"; export { CURSOR_DISTRIBUTION_PINS } from "./drivers/acpx/generated-profiles.js"; +export { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "./drivers/acpx/kimi-closure-pins.js"; export * from "./contracts/control-plane-port.js"; export * from "./contracts/completion-result.js"; export * from "./contracts/codex.js"; @@ -101,7 +102,7 @@ export * as acceptedCapabilitySemanticTools from "./semantic-tools/index.js"; export * from "./compatibility.js"; export { probeAcpxClaudeInstallation, probeAcpxGrokInstallation } from "./drivers/acpx/installation-integrity.js"; -export { probeAcpxPiInstallation } from "./drivers/acpx/profile-installation.js"; +export { probeAcpxPiInstallation, probeAcpxKimiInstallation } from "./drivers/acpx/profile-installation.js"; export { PROVIDER_PACK_COMPATIBILITY, bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary } from "./live/bundled-remote-provider-pack.js"; export { RunnerdMuseDriver, describeRunnerdMuseDriver, type RunnerdMuseDriverOptions } from "./drivers/muse/runnerd-muse-driver.js"; diff --git a/packages/paperclip-runner/src/live/index.ts b/packages/paperclip-runner/src/live/index.ts index 2f86f2739fb..4a65609addd 100644 --- a/packages/paperclip-runner/src/live/index.ts +++ b/packages/paperclip-runner/src/live/index.ts @@ -9,6 +9,8 @@ export * from "./linux-process-start.js"; export { probeAcpxClaudeInstallation, probeAcpxGrokInstallation } from "../drivers/acpx/installation-integrity.js"; export { probeCopilotMetadata, validateCopilotMetadata, type CopilotMetadataResult } from "../drivers/copilot-metadata-probe.js"; -export { probeAcpxCursorInstallation, probeAcpxPiInstallation } from "../drivers/acpx/profile-installation.js"; +export { probeAcpxCursorInstallation, probeAcpxPiInstallation, probeAcpxKimiInstallation } from "../drivers/acpx/profile-installation.js"; +export { probeKimiModels, KimiMetadataProbeError, type KimiVerifiedModelMetadata } from "../drivers/acpx/kimi-models.js"; +export { probeKimiMetadata, validateKimiMetadata, kimiMetadataProbeDigest, kimiMetadataProbeNodeDigest, type KimiMetadataResult } from "../drivers/acpx/kimi-metadata-probe.js"; export { bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary } from "./bundled-remote-provider-pack.js"; diff --git a/packages/paperclip-runner/src/live/kimi-candidate-startup.test.ts b/packages/paperclip-runner/src/live/kimi-candidate-startup.test.ts new file mode 100644 index 00000000000..83c5b0a031f --- /dev/null +++ b/packages/paperclip-runner/src/live/kimi-candidate-startup.test.ts @@ -0,0 +1,37 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { type AcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js"; +import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../drivers/acpx/kimi-closure-pins.js"; +import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js"; +import { createRunnerdCodexTransport } from "./runnerd-codex-transport.js"; + +const candidate = (remote = false): AcpxCandidateAdmission => ({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-kimi", agent: "kimi", model: "kimi-for-coding", + profileDigest: resolveQualifiedAcpxProfile("kimi", "kimi-for-coding").commandDigest, + nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[remote ? "linux-x64" : "darwin-arm64"]}`, + platform: remote ? "linux" : "darwin", architecture: remote ? "x64" : "arm64", expiresAt: Date.now() + 60_000 }); + +describe("Kimi trusted qualification startup", () => { + it("retains exact provider and isolated home authority for a valid Kimi grant", async () => { + const root = await mkdtemp(join(tmpdir(), "kimi-candidate-startup-")); + try { + const options = { provider: "acpx" as const, acpxAgent: "kimi" as const, stateDirectory: root, acpxCandidateAdmission: candidate() }; + for (const changes of [{ provider: "codex" as const }, { provider: "opencode" as const }, { homeMode: "preconfigured" as const }, + { acpxAgent: "copilot" as const }, { acpxCandidateAdmission: { ...candidate(), profileDigest: `sha256:${"f".repeat(64)}` } }]) { + expect(() => createRunnerdCodexTransport({ ...options, ...changes })).toThrow(); + } + expect(() => createRunnerdCodexTransport({ ...options, acpxCandidateAdmission: undefined })).toThrow("evaluation opt-in"); + } finally { await rm(root, { recursive: true, force: true }); } + }); + + it.each([false, true])("keeps ordinary Kimi warm transport capability under target-bound authority: remote=%s", async remote => { + const root = await mkdtemp(join(tmpdir(), "kimi-candidate-warm-")); + try { + const owner = createRunnerdCodexTransport({ provider: "acpx", acpxAgent: "kimi", stateDirectory: root, + acpxCandidateAdmission: candidate(remote), lifecyclePolicy: { mode: "warm", idleTimeoutMs: 1000 }, + ...(remote ? { runnerFilesystemRoot: "/owned-runner" } : {}) }); + await owner.transport.close(); + } finally { await rm(root, { recursive: true, force: true }); } + }); +}); diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index bba62831ee4..c7df8fe8bc9 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1245,7 +1245,7 @@ export interface CapabilityRunnerdCodexTransportOptions extends RunnerExecutionD opencodePermissionMode?: NativeOpenCodePermissionMode; acpxAgent?: QualifiedAcpxAgent; /** Explicit evaluation-only candidate selection, never derived from persisted session input. */ - acpxCandidateProfile?: "pi" | "cursor" | "copilot"; + acpxCandidateProfile?: "pi" | "cursor" | "copilot" | "kimi"; /** Server/operator authority; never derived from persisted agent or provider input. */ acpxCandidateAdmission?: AcpxCandidateAdmission; acpxPermissionMode?: NativeAcpxPermissionMode; @@ -3097,7 +3097,7 @@ function acpxProviderPackageAuthority( // Native provider closures belong to the runner package itself. Their asset // guard requires the bound manifest directly inside that same package root; // the workspace dependency root used by JS providers is a different owner. - if (agent === "copilot" || agent === "pi" || agent === "antigravity") { + if (agent === "copilot" || agent === "pi" || agent === "kimi" || agent === "antigravity") { return { root: sidecarPackageRoot, manifest: resolve(sidecarPackageRoot, "package.json"), @@ -3146,7 +3146,7 @@ function acpxRunnerLaunchProfile( `sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`; let candidateAdmission: AcpxCandidateStartupAdmission | undefined; if (options.acpxCandidateAdmission) { - if (!identity || options.runnerFilesystemRoot) throw new Error("ACPX candidate requires an actual local startup identity"); + if (!identity || options.acpxCandidateAdmission.agent === "antigravity" && options.runnerFilesystemRoot) throw new Error("ACPX candidate requires an actual authorized startup identity"); candidateAdmission = bindAcpxCandidateAdmission(options.acpxCandidateAdmission, identity); } if (!options.runnerFilesystemRoot) { @@ -3200,6 +3200,7 @@ function acpxRunnerLaunchProfile( ); } return { + ...(candidateAdmission ? { candidateAdmission } : {}), authorityDigest, command, commandSha256, @@ -3392,7 +3393,7 @@ function createCapabilityRunnerdProviderEnvironmentBase(input: { PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: providerPackageAuthority.root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: providerPackageAuthority.manifest, - ...(!input.options.acpxCandidateAdmission && input.options.providerRecoveryPolicy === + ...(input.options.acpxCandidateAdmission?.agent !== "antigravity" && input.options.providerRecoveryPolicy === "allow_replacement_after_governed_wait" ? { PAPERCLIP_ACPX_PROVIDER_RECOVERY_POLICY: @@ -3685,18 +3686,21 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { } if (options.acpxCandidateAdmission !== undefined) { const candidate = options.acpxCandidateAdmission; + if (options.provider !== "acpx") { + throw new Error("ACPX candidate admission requires the exact ACPX provider"); + } const admitted = parseAcpxCandidateAdmission(candidate, { companyId: candidate.companyId, agent: options.acpxAgent ?? "", model: candidate.model }); - this.options = { ...this.options, acpxCandidateAdmission: Object.freeze(admitted), providerRecoveryPolicy: "same_session_only" }; - this.prepareConversationResume = params => this.#prepareCandidateConversationResume(params); - if (options.provider !== "acpx" || process.platform !== "darwin" || process.arch !== "arm64" + this.options = { ...this.options, acpxCandidateAdmission: Object.freeze(admitted), ...(candidate.agent === "antigravity" ? { providerRecoveryPolicy: "same_session_only" as const } : {}) }; + if (candidate.agent === "antigravity") this.prepareConversationResume = params => this.#prepareCandidateConversationResume(params); + if (candidate.agent === "antigravity" && (options.provider !== "acpx" || process.platform !== "darwin" || process.arch !== "arm64" || options.runnerFilesystemRoot || options.prepareLaunch || options.runnerProcessLauncher || options.homeMode === "preconfigured" || options.lifecyclePolicy?.mode === "warm" || options.adoptExistingRunner || options.resumeActiveTurnId - ) { + )) { throw new Error("ACPX candidate admission supports only a fresh local isolated per-turn execution"); } } - if (options.acpxCandidateAdmission && options.resumeProviderSession) { + if (options.acpxCandidateAdmission?.agent === "antigravity" && options.resumeProviderSession) { const prior = options.resumeProviderSession; const identity = record(prior.providerIdentity); if (!prior.driverSessionId?.trim() || !prior.providerSessionId?.trim() @@ -3709,7 +3713,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { if (options.provider === "acpx" && options.acpxAgent !== undefined && ACPX_CAPABILITY_PROFILES[options.acpxAgent].qualification === "pending" && options.acpxCandidateProfile !== options.acpxAgent - && !(options.acpxAgent === "antigravity" && options.acpxCandidateAdmission)) { + && !(options.acpxCandidateAdmission?.agent === options.acpxAgent)) { throw new Error("The candidate ACPX profile requires explicit evaluation opt-in"); } this.#failureSignal = new Promise((_resolve, reject) => { @@ -4268,7 +4272,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { this.#runAttachTemplate = structuredClone(runAttachTemplate); const payload = { ...runAttachTemplate, - paperclipNextAuthority: { identity: desired, connection }, + paperclipNextAuthority: { identity: desired, connection, + ...(this.options.acpxCandidateAdmission?.agent === "kimi" + ? { candidateAdmission: bindAcpxCandidateAdmission(this.options.acpxCandidateAdmission, desired) } : {}), + }, }; core.queueCommand("run.attach", payload, commandId, true); // ACPX run attachment checkpoints the old sidecar and starts a fresh @@ -5385,7 +5392,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { } async #resumeWork(): Promise { - if (this.options.acpxCandidateAdmission) throw new Error("ACPX candidate durable runner recovery is not admitted"); + if (this.options.acpxCandidateAdmission?.agent === "antigravity") throw new Error("ACPX candidate durable runner recovery is not admitted"); const desiredIdentity = this.options.prpIdentity; if (desiredIdentity === undefined) { throw new Error("PRP provider resume state is unavailable"); diff --git a/packages/paperclip-runner/src/providers/kimi/README.md b/packages/paperclip-runner/src/providers/kimi/README.md new file mode 100644 index 00000000000..8852e434c29 --- /dev/null +++ b/packages/paperclip-runner/src/providers/kimi/README.md @@ -0,0 +1,285 @@ +# Kimi Code native boundary + +Research date: 2026-10-28. These are source observations and deterministic +package checks. They are not authenticated Product E2E qualification. + +The maintained runtime is **`@moonshot-ai/kimi-code@2.3.3`**, release commit +[`ec495f0`](https://github.com/MoonshotAI/kimi-code/releases/tag/%40moonshot-ai%2Fkimi-code%402.3.3). +It is a Node.js/TypeScript application distributed both as a packaged native +executable and an npm package. The Python +[`MoonshotAI/kimi-cli`](https://github.com/MoonshotAI/kimi-cli) repository was +archived on September 23, 2026. Its Wire 1.10 documentation does not describe +the maintained runtime's public interface. The new npm bundle is inspectable +JavaScript; its internal ACP/core/Klient packages are private workspace packages. + +## Selected transport and instruction delivery + +[`kimi acp`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/docs/en/reference/kimi-acp.md) +speaks stdio JSON-RPC ACP. The maintained alternative is the REST/WebSocket +[`kimi web`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/docs/en/reference/server-api.md) +server. ACP reuses the existing Runner integration and avoids owning a second +HTTP-server lifecycle. + +Native `AcpServer.newSession` accepts the workspace, extra directories and MCP +servers; load/resume accept current ephemeral MCP configuration. It does not +accept system instructions. The ACP CLI only consumes its login/region flags; +ordinary `--agent-file` does not establish ACP instruction admission. + +The owned patch in `scripts/patch-kimi-runtime.mjs` verifies the entire official +npm `dist/main.mjs` SHA-256 before matching each unique source anchor. The +materializer verifies the complete patched output hash. The embedded helper +uses a single canonical owned source string so the raw patcher and installed +esbuild provisioner produce identical native bytes. The patch reads one +bounded process-owned `PAPERCLIP_KIMI_INSTRUCTIONS` JSON snapshot, validates its +content digest and byte length, then removes that variable from the environment +inherited by tools. Exact literal bytes are appended after native profile +composition through `AgentProfileService.systemPrompt`. + +The native requester uses `getSystemPrompt()` in `captureTurnRequestConfig` +and `createModelRequest`; compaction uses the same getter. A restored or +recomposed profile therefore still receives the current literal suffix. +At the ACP native-runtime binding callback, the live main-agent scope reads +its effective `getSystemPrompt()`. Only that readback produces the session +receipt returned by new/load/resume. The receipt carries the bound-content +digest/length and the effective native prompt digest/length. Runner must match +the response to the active request and connection before sending a prompt. +The receipt proves delivery; an authenticated behavior case remains required. + +Relevant source: [`profileService.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/agent/profile/profileService.ts), +[`llmRequesterService.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts), +[`server.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/server.ts), +[`start.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/start.ts). + +## Capability and loss inventory + +| Native capability | ACP boundary and release implication | +| --- | --- | +| Model selection | `session/set_config_option` with `configId: model` returns current `configOptions`; `session/set_model` returns `{}` and emits a config update. Require the exact model acknowledgment before a prompt. Model aliases and provider-native model IDs are separate config fields. | +| Thinking | Config option exposes declared effort values or on/off; unsupported values fail. It must be tested for the selected account/model before advertising it. | +| Tools | Start, streaming arguments, full input, result/error and file diffs are projected. Native `tool.progress` only projects status text; stdout/stderr/progress/custom updates are dropped. | +| Reasoning | Thinking deltas become ACP thought chunks. Apply the existing Runner reasoning visibility policy. | +| Assistant messages | Upstream ACP emits text without the native step identity, merging progress and final messages. Patch v5 forwards actual main-agent step UUID/start/end facts with their native finish reason. The provider projection preserves separate messages, clears tool-use progress from final selection, and rejects incomplete provenance. | +| Children | ACP subscribes only to the main agent. Native child activity, identity and usage are not preserved. Disable native delegation for the initial supported release. | +| Questions | `elicitation.form` carries all questions and multi-selection. Permission fallback drops every question after the first, converts multi-selection to single-selection, and cannot represent Other/free text. Do not advertise native questions from fallback parsing; semantic Paperclip questions require their own live continuation proof. | +| Plans | Todos become ACP plan updates. `ExitPlanMode` approvals use the permission channel and plan-specific options. Native plan review, feedback and post-accept behavior need separate qualification. | +| Permission | Native normal mode has auto-approval heuristics and prior approval rules. The managed patch asks Runner for every tool operation, includes the actual `toolInput` as `rawInput`, removes session-wide approval choices, and disables ambient hooks/MCP/profiles. | +| Usage | ACP's `usage_update.used` is current context occupancy and `size` is model capacity. It is not per-turn input/output usage or cost; cost is omitted. Internal Klient has richer token counters, but the selected ACP path does not publish them. Do not turn these context values into charges or a dollar-budget guarantee. | +| Failures | Upstream ACP maps non-auth failed turns to `end_turn`. The managed patch preserves typed auth failure and rejects every other native failed turn on the prompt RPC. | +| Continuity | New/load/resume/list/fork/close/delete are implemented. Load replays history; resume skips replay. A cold restore accepts fresh ephemeral MCP config; loading an already-live session ignores replacement MCP configuration. Reopen after a configuration change requires a new owned process. | +| Stop and shutdown | Native cancel is turn-addressed and handles launch races. ACP close cancels/disposes the session. Connection EOF starts a flush/dispose path. These implementations do not prove bounded process-tree cleanup; test spontaneous child/process loss and forced termination independently. | +| Steering | Native Klient and REST have steering/queued prompts. ACP has no steering method. Sending a second prompt does not prove same-turn steering. | +| Media | ACP advertises image and text embedded-resource support; blob resources are dropped and audio is unsupported. End-to-end Runner forwarding still needs its own proof. | + +Source: [`session.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/session.ts), +[`events-map.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/events-map.ts), +[`interaction-bridge.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/interaction-bridge.ts), +[`question.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/question.ts), +[`approval.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/approval.ts). + +## Configuration and authority + +An empty isolated `KIMI_CODE_HOME` does **not** make an ambient `KIMI_API_KEY` +usable automatically. A provider must explicitly declare `api_key_env`. +Use the selected provider endpoint and a configured model alias whose `model` +is the caller's exact native model ID. `max_context_size` is required by the +native schema; it is configuration metadata, not entitlement evidence. +Strip unselected `KIMI_MODEL_*` variables, which can synthesize a competing +provider/model. Do not import ambient OAuth credentials, MCP grants or hooks. + +The managed patch keeps user skills only under the owned Kimi home and drops +project/global skill directories. It empties ambient workspace MCP baseline +while leaving ACP session MCP separate. Root configuration must disable +native delegation, cron/background task tools and automatic backgrounding; +keep-alive-on-exit stays off. Removing TaskList/TaskOutput/TaskStop makes native +`Bash.allowBackground()` false and causes explicit background requests to fail. + +Native `HostProcessService.buildEnv()` otherwise inherits the complete provider +environment, including `KIMI_API_KEY`; Bash supplies only noninteractive +overrides. The patch always builds an explicit child environment and removes +`KIMI_API_KEY` and `PAPERCLIP_KIMI_INSTRUCTIONS` after merging overrides. The +native PTY path uses the same filter. Assigned task variables remain available; +the provider process retains its key for model authentication. This is a child +environment boundary, not an operating-system sandbox for arbitrary native +filesystem or shell tools. Read-only work must use the controller's scoped +semantic tools. + +Patch v4 also ignores the ACP client's terminal capability. Otherwise native +`AcpProcessService` sends Bash to ACPX's separate terminal process, bypassing +the provider-owned environment filter and dropping configured task variables. +Keeping the existing native local process service makes the filter apply on +the actual Runner execution path. The controlled-peer check requires ACPX to +advertise its normal terminal capability while observing zero `terminal/create` +requests from Kimi. + +Native OAuth resolves the access token into the same `{ apiKey }` credential +as an explicit API-key provider. A selected unexpired access token can bound +a finite test, but that explicit binding has no OAuth refresh behavior. Never +copy refresh tokens into qualification homes or describe this as managed +OAuth support. + +The setup Test can verify Code metadata without inference through +`probeKimiModels(model, apiKey)`. It sends one GET to the fixed global +`https://api.kimi.ai/coding/v1/models` endpoint, using the explicitly selected +key and pinned `KimiCLI/2.3.3` user agent. Native Kimi uses this endpoint for +both OAuth and API-key providers and treats HTTP 401/402/403 as credential or +membership-access failures. The probe requires a valid model catalog with the +exact caller-selected ID and positive context metadata. It never substitutes a +model or imports an ambient credential. Redirects are rejected, streamed JSON +is capped at 256 KiB, and the eight-second deadline aborts both the request and +response body. Errors contain fixed messages and typed codes, not provider +response text or credential values. + +A successful metadata probe establishes catalog authentication and exact model +listing. It does not establish successful inference, remaining quota, usage or +charges. The native ACP catalog alone cannot establish authentication because +it projects local configuration. Normal model and instruction admission still +applies before a task prompt. + +Source: [`overrides.md`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/docs/en/configuration/overrides.md), +[`config-files.md`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/docs/en/configuration/config-files.md), +[`credentials.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/human/credentials/credentials.ts), +[`bashTool.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/agent/tools/os/bash/bashTool.ts), +[`hostProcessService.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/os/backends/node-local/hostProcessService.ts), +[`hostTerminalService.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts), +[`acpConnection.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/acp-fs/acpConnection.ts), +[`acpTerminalRunner.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/acp-server/src/acp-terminal/acpTerminalRunner.ts), +[`managed-kimi-code.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/oauth/src/managed-kimi-code.ts), +[`refreshProviderModels.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/oauth/src/refreshProviderModels.ts). + +## Reproducible boundary check + +From a clean verified npm installation, provide its unmodified `dist/main.mjs` +to the package contract cell: + +```sh +PAPERCLIP_KIMI_VENDOR_MAIN=/absolute/path/to/dist/main.mjs \ + node --test packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs +``` + +This executes the actual pinned getter and ACP lifecycle/error methods with +engine/transport dependencies doubled. It verifies literal Unicode/whitespace, +explicit empty content, native readback, create/load/resume receipts, missing +receipts, ambient-source isolation, host permission delegation, malformed +bindings, child environment filtering, esbuild bundle reproducibility and +terminal failure mapping. It performs no inference and does not +replace installed-package, authenticated semantic-tool, browser persistence, +warm recovery or physical-cleanup acceptance. + +## Controlled model-service regression + +After installing the fixed runtime assets, a second opt-in cell runs the real +pinned Kimi command through the real ACPX manager against a loopback-only, +scripted OpenAI-compatible service and MCP receiver: + +```sh +PAPERCLIP_TEST_KIMI_MODEL_PEER=1 \ + node --test packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs +``` + +The test changes only its private native test configuration below the production +boundary. It does not add an endpoint override to production configuration, use +a provider credential, or call an external model. It captures exact instruction +bytes at the native HTTP request, fragmented/interleaved MCP JSON including a +terminal `{}`, accepted tool results in the next model request, Write denial +before any file effect, a native Bash credential fence, typed provider failure, +new/resume admission and bounded owned-process closure. The production patch +keeps Bash in the provider's own local process service even when ACPX advertises +client terminals. ACPX prefers `session/resume` when advertised; the +installed-runtime metadata smoke checks `session/load` separately. + +The first controlled-peer attempt against patch v1 failed: the native blocking +approval payload omitted `toolInput`, although a sibling native event carried +it. The ACP callback therefore lost `rawInput`, and the strict host rejected +the operation before the MCP receiver ran. That failure is retained as the +reason for patch v2: add the original arguments at native `approvalRequest` +construction before the interaction kernel, then preserve them at the ACP edge. +An empty object remains a complete valid argument. A passing simulated service +cell remains protocol-boundary evidence and does not qualify model behavior, +account entitlement, browser journeys or Daytona delivery. + +The v2 controlled-peer cell passed on 2026-10-28 against the verified +`darwin-arm64` closure through the actual pinned ACPX manager: five simulated +model requests, two accepted MCP receiver calls including `{}`, one denied +Write with independent before/after file-absence checks, typed failed-turn +settlement and two closed native processes. Both initial and resumed native +acknowledgments matched the SHA-256 of the complete system prompt observed at +the HTTP request. Permission response-delivery receipts settled. This cell +uses fake credentials and records zero external model calls. + +The v3 Bash regression exposed the ACP client-terminal bypass: native filter +unit checks passed, but normal ACPX terminal execution lost the assigned task +variable and did not exercise that filter. V4 therefore forces the native local process path. +The extended cell checks both key/private-variable absence and task-variable +presence in real Bash, while every native model HTTP request must retain the +fake provider authorization. Earlier v2 evidence remains valid for its narrower +instruction/MCP/denial/error scope; the credential fence requires v4 evidence. + +The final v4 native runtime passed the seven-request controlled-peer cell on +2026-10-28. ACPX advertised its normal client-terminal capability, Kimi sent zero +`terminal/create` requests, and native Bash returned its success marker only +after verifying both private variables were absent and an assigned task variable +was retained. All seven model requests still carried the fake provider key. +The exact native SHA-256 was +`cfd11b57b3d75419539bd16f237d73d9ca896d45ae41bbfed8c71beab4daaee9`; +the verified macOS closure was +`c05a0cd0686a4988fed6cb3c69ca1d9845ea233c53b391a84bc442241cef551b`. + +Enable the additional pending-work lifecycle checks with: + +```sh +PAPERCLIP_TEST_KIMI_MODEL_PEER=1 PAPERCLIP_TEST_KIMI_LIFECYCLE_PEER=1 \ + node --test --test-name-pattern='simulated local model service' \ + packages/paperclip-runner/scripts/patch-kimi-runtime.test.mjs +``` + +The extended nine-request cell also passed on 2026-10-28. Both cases held a real +native Write approval before any file effect. Stop settled as cancelled. For +spontaneous provider loss, the test observed the unique native worker beneath +its verified command root and revalidated its parent, process start and group +before sending SIGKILL to that worker. It settled as a typed failure. Both cases +aborted the waiting host callback, suppressed a subsequently supplied stale +`allow_once` response on that exact connection, and produced no file or further +model request. The test independently checked file absence after settlement and +cleanup, and observed physical worker retirement within five seconds. Four +owned native processes closed across the complete cell. These remain simulated +service proofs; they do not qualify authenticated behavior, browser journeys or +Daytona delivery. + +## Message-boundary regression + +The first completed local Product attempt on 2026-10-28 exposed an upstream +ACP framing loss. Campaign `local-2026-10-28T13-43-42-062Z` reached an accepted +completion report and durable Done state, but the displayed final comment +concatenated a pre-tool progress message with the final answer. The attempt +remains failed; completion state alone is insufficient user-facing acceptance. + +The engine already emits `TurnStepStarted` and `TurnStepCompleted` with the +real model-step UUID and normalized finish reason. The owned v5 patch registers +those existing events in Klient's agent event hub and subscribes through the +same active-turn dispatch gate as text deltas. Each message ID hashes the +native session, turn and step UUID. Empty start/end chunks carry bounded +provenance; text and reasoning deltas use the same identity. Public interrupted +step events close only the matching active step. No text inspection or tool +notification creates a message boundary. Restored history remains outside the +active prompt event stream. + +Source: [`loopService.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/agent/loop/loopService.ts), +[`turnEvents.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/agent-core-v2/src/agent/loop/turnEvents.ts), +[`Klient events.ts`](https://github.com/MoonshotAI/kimi-code/blob/ec495f0/packages/klient/src/contract/agent/events.ts). + +The v5 nine-request controlled-peer check passed in 13.4 seconds on 2026-10-28 +against native SHA-256 +`9ea2cf23ed4a7386de7f01147d5f7c83719568d814b9d6e1d5ebb43ca3067d86` +and verified macOS closure +`429433e76fc8889b2262b3d0739bc955e80d958da4a9aa378653d6690030c2ef`. +Each ordinary simulated turn emitted a pre-tool message ending in `tool_use` +and a separate UUID for the final message ending in `end_turn`, even though +the scripted HTTP service deliberately reused its response ID. Actual ACPX +normalization retained every empty boundary. The real provider projection +validated the native provenance and returned canonical `toolUse` and `stop` +boundaries. The same check repeated instruction admission, MCP arguments and +results, the child credential fence, denial without effects, typed failure, +pending permission Stop, exact native-worker loss, stale-answer suppression +and bounded physical cleanup. Four owned native processes retired; there were +zero external model calls. This does not replace the failed Product attempt +or prove its browser journey passed after the fix. diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-kimi-historical-identity.json b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-kimi-historical-identity.json new file mode 100644 index 00000000000..c73831841b9 --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-kimi-historical-identity.json @@ -0,0 +1,52 @@ +{ + "commandDigest": "sha256:00ce5ecc7f0e904811b70584b936c673182c1029b500e8d4823f44ae3cd755e2", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "pi", + "agentProfileVersion": 35, + "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": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "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": "75a9a13f13f224dc092bd8ebf49e8747162172943761e44476c8eb18d80c52d4", + "credentialEnvironmentSourceSha256": "a6733e28a9468b744e1e72f1af0c2236e17372e51a9a3ba254a406e085591d2d", + "runtimeSandboxSourceSha256": "d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "recoveryIdentitySourceSha256": "f74d061cc07b0c7ca04df62ca991d940a733d9ef6594b3362451769fc10030b8", + "nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1", + "dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814", + "openRouterAttributionSourceSha256": "1211191529d6073b733b311c3d69cada4973eede0f3e540c46e261f78e0b90f9", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e" + } +} diff --git a/packages/paperclip-runner/test-fixtures/pi-acp/profile-v36-identity.json b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v36-identity.json new file mode 100644 index 00000000000..8c3384b145e --- /dev/null +++ b/packages/paperclip-runner/test-fixtures/pi-acp/profile-v36-identity.json @@ -0,0 +1,52 @@ +{ + "commandDigest": "sha256:5186e5acd48dd5f951ee62656be9e266d05e880127fb1d8d6f26161e444770af", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "pi", + "agentProfileVersion": 36, + "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": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "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": "75a9a13f13f224dc092bd8ebf49e8747162172943761e44476c8eb18d80c52d4", + "credentialEnvironmentSourceSha256": "9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "runtimeSandboxSourceSha256": "86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "recoveryIdentitySourceSha256": "f74d061cc07b0c7ca04df62ca991d940a733d9ef6594b3362451769fc10030b8", + "nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1", + "dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814", + "openRouterAttributionSourceSha256": "1211191529d6073b733b311c3d69cada4973eede0f3e540c46e261f78e0b90f9", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v82-kimi-historical-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v82-kimi-historical-identity.json new file mode 100644 index 00000000000..abcf1d7d86a --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v82-kimi-historical-identity.json @@ -0,0 +1,89 @@ +{ + "commandDigest": "sha256:e0eddf258e824bb0cd7e0ec0a12dc5b8eeff72262e176249b5e6e7d5ea977caf", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 82, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a", + "upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0", + "patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679", + "messageIdentityContract": "copilot-native-message-id-v1", + "ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1", + "permissionContextContract": "copilot-edit-permission-context-v1", + "permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852", + "permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "semanticDirectDriverSourceSha256": "9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb", + "semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b", + "semanticSidecarProtocolSourceSha256": "26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "4af2280930d6382ff4acef6a09515050991746d8ad5358705f536227d548555f", + "attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94", + "transportContractSourceSha256": "03fca93d36a89b0cc0917335e293ad097fdf04356d21b307827bb0ea4c13f523", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "55aec95e7c7f204454cbd2961e4ee7e7507851da99f0ced899b61154333ae927", + "runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "56a177d3e702914818dca4ca50e061f0d25f988bfc4c0582c36ce23c1696d82f", + "nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69", + "profileCompatibilitySourceSha256": "addf190c6b561d48cbc013ba08326e9d922e49e6adc3fa4460d1fb733b06f6bb", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "b4e8c5b4e2c1757bcbf2049b5cb013723876168c00c24c28048fc80c7ca16ace", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "a6733e28a9468b744e1e72f1af0c2236e17372e51a9a3ba254a406e085591d2d", + "admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "runtimeHostSourceSha256": "981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "runtimeAdapterSourceSha256": "601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "runtimeSandboxSourceSha256": "d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v83-kimi-historical-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v83-kimi-historical-identity.json new file mode 100644 index 00000000000..bd4e7827bb7 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v83-kimi-historical-identity.json @@ -0,0 +1,89 @@ +{ + "commandDigest": "sha256:4cecc2ba4bc9674fbc6f585d81934789a98b75a36f074dfe2b40391ae9ef8a19", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 83, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a", + "upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0", + "patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679", + "messageIdentityContract": "copilot-native-message-id-v1", + "ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1", + "permissionContextContract": "copilot-edit-permission-context-v1", + "permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852", + "permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "semanticDirectDriverSourceSha256": "9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb", + "semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b", + "semanticSidecarProtocolSourceSha256": "26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "4af2280930d6382ff4acef6a09515050991746d8ad5358705f536227d548555f", + "attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94", + "transportContractSourceSha256": "03fca93d36a89b0cc0917335e293ad097fdf04356d21b307827bb0ea4c13f523", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "55aec95e7c7f204454cbd2961e4ee7e7507851da99f0ced899b61154333ae927", + "runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "56a177d3e702914818dca4ca50e061f0d25f988bfc4c0582c36ce23c1696d82f", + "nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69", + "profileCompatibilitySourceSha256": "68f12855fc4f51f31b79ee48851204cd8424449df3b7f33147813aaa21959e83", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "b4e8c5b4e2c1757bcbf2049b5cb013723876168c00c24c28048fc80c7ca16ace", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "a6733e28a9468b744e1e72f1af0c2236e17372e51a9a3ba254a406e085591d2d", + "admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "runtimeHostSourceSha256": "981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "runtimeAdapterSourceSha256": "601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "runtimeSandboxSourceSha256": "d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v88-kimi-foundation-81fbfb44-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v88-kimi-foundation-81fbfb44-identity.json new file mode 100644 index 00000000000..ad222308368 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v88-kimi-foundation-81fbfb44-identity.json @@ -0,0 +1,89 @@ +{ + "commandDigest": "sha256:0c3328c3fecbe730fa0c4d5239f1084eada804bcb140e16bb41efa0e5a4c4380", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 88, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a", + "upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0", + "patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679", + "messageIdentityContract": "copilot-native-message-id-v1", + "ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1", + "permissionContextContract": "copilot-edit-permission-context-v1", + "permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852", + "permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "semanticDirectDriverSourceSha256": "d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb", + "semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b", + "semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runtimeRequestFacadeSourceSha256": "23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6", + "nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69", + "profileCompatibilitySourceSha256": "3937581ffc20b248bb24e2e7109e8adb1035a750d1713994988986d3572a84bc", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "36064b48d889c41070091a75c1645b7c8caa987a7ed1810a931dc378ddbb1356", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "runtimeHostSourceSha256": "f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "runtimeAdapterSourceSha256": "c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "runtimeSandboxSourceSha256": "86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v89-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v89-identity.json new file mode 100644 index 00000000000..a913d244eb4 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v89-identity.json @@ -0,0 +1,89 @@ +{ + "commandDigest": "sha256:995381b2cec7a0faf3f5cefbe2421eae7936a2bfa50bb153645f57341f968db3", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 89, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a", + "upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0", + "patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679", + "messageIdentityContract": "copilot-native-message-id-v1", + "ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1", + "permissionContextContract": "copilot-edit-permission-context-v1", + "permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852", + "permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "semanticDirectDriverSourceSha256": "d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb", + "semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b", + "semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runtimeRequestFacadeSourceSha256": "23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6", + "nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69", + "profileCompatibilitySourceSha256": "6332462e69b990dcba4104389e76ba4198ba284242dc1b963aedfdc5dba93f31", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "36064b48d889c41070091a75c1645b7c8caa987a7ed1810a931dc378ddbb1356", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "runtimeHostSourceSha256": "f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "runtimeAdapterSourceSha256": "c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "runtimeSandboxSourceSha256": "86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v90-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v90-identity.json new file mode 100644 index 00000000000..f1e16b3f7fb --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v90-identity.json @@ -0,0 +1,89 @@ +{ + "commandDigest": "sha256:8922d3d41dae0a7b7f7e2de69d50b272b58283232fefb22ffdb6c9b34fa26a6b", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 90, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a", + "upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0", + "patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679", + "messageIdentityContract": "copilot-native-message-id-v1", + "ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1", + "permissionContextContract": "copilot-edit-permission-context-v1", + "permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852", + "permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "semanticDirectDriverSourceSha256": "d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb", + "semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b", + "semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runtimeRequestFacadeSourceSha256": "23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6", + "nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69", + "profileCompatibilitySourceSha256": "09eac296f1f511678ffa057b9bcc0de5321902a9607eeec0177cd1957f22d735", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "36064b48d889c41070091a75c1645b7c8caa987a7ed1810a931dc378ddbb1356", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "runtimeHostSourceSha256": "f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "runtimeAdapterSourceSha256": "c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "runtimeSandboxSourceSha256": "86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v91-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v91-identity.json new file mode 100644 index 00000000000..02568d0b4bc --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v91-identity.json @@ -0,0 +1,90 @@ +{ + "commandDigest": "sha256:75ae0628c140e51b466b898a394032849d6eca5272279fa7782192a6d316a75d", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 91, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3", + "distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107", + "agentFilesBinding": "registered-runtime-context-v1", + "systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1", + "sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1", + "innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a", + "upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0", + "patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679", + "messageIdentityContract": "copilot-native-message-id-v1", + "ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1", + "permissionContextContract": "copilot-edit-permission-context-v1", + "permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852", + "permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "b60e4b924e827e122860d2a4cc13b465ba748ee521620887c88f3c58c0df6081", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "477a05847187159f9c6f07bf90f1c0c1d432a9eb6fdb15d5620daf54443dafd8", + "semanticDirectDriverSourceSha256": "d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb", + "semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b", + "semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "nativeRuntimeContextSourceSha256": "152d7187ce1dc91a15377d368776555c9897d6e018b65f0f59a92dcdbd6497d9", + "nativeRuntimeContextFieldsSourceSha256": "069f4bc3c4dc95888f30d3fea331adb0ee0a164d09f4f70ec9c132f24f69c7dd", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runtimeRequestFacadeSourceSha256": "23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6", + "nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69", + "profileCompatibilitySourceSha256": "aaa189a1e8b921887a5d83c7576617a5b46ceb77d43ed7ec0466653eb29fc37c", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "36064b48d889c41070091a75c1645b7c8caa987a7ed1810a931dc378ddbb1356", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "runtimeHostSourceSha256": "f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "runtimeAdapterSourceSha256": "c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "runtimeSandboxSourceSha256": "86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/grok-profile-v13-kimi-historical-identity.json b/packages/paperclip-runner/test/fixtures/grok-profile-v13-kimi-historical-identity.json new file mode 100644 index 00000000000..e436c1a6d2b --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/grok-profile-v13-kimi-historical-identity.json @@ -0,0 +1,35 @@ +{ + "commandDigest": "sha256:c4beba5aff2708dbab58b00da718e112dc29b91159a4f9cf449104a5ef141a40", + "sourceDigest": "sha256:e61fd575a05d284ba10ef17122adabccd08169ae0d06004a365198a603014b1e", + "declaration": { + "schema": "paperclip.grok-profile-identity.v1", + "agent": "grok", + "agentProfileVersion": 13, + "agentServerPackage": "builtin:grok-acp", + "agentServerVersion": "1", + "agentRuntimePackage": "native:grok", + "agentRuntimeVersion": "1.0.13", + "requiresProviderPolicy": false, + "acpxVersion": "0.13.1", + "sources": { + "patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c", + "packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73", + "packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b", + "packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55", + "packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:ff22cccd54160797428e6c7affc70f7d30e3a6678d13058ca972b146c3cdd7d2", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:e97a7c4d2cf9be9c6e0ada78076a5551b01401518597ab81cb6fbd5a1faf4a5e", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:cfbbb39a2e20f449df53ceaea4a6cbd068c954c10405bea2d908b93e3f387fdf" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/grok-profile-v14-kimi-historical-identity.json b/packages/paperclip-runner/test/fixtures/grok-profile-v14-kimi-historical-identity.json new file mode 100644 index 00000000000..7994dfbb2b2 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/grok-profile-v14-kimi-historical-identity.json @@ -0,0 +1,35 @@ +{ + "commandDigest": "sha256:3140bc190a025afa2f7380a3eb1a2d8aeecd24850ac69f82871847e78c3b17a0", + "sourceDigest": "sha256:8da223681ae5deefd04e192d181e09e5446729d4caf2f96eefac76769717bb09", + "declaration": { + "schema": "paperclip.grok-profile-identity.v1", + "agent": "grok", + "agentProfileVersion": 14, + "agentServerPackage": "builtin:grok-acp", + "agentServerVersion": "1", + "agentRuntimePackage": "native:grok", + "agentRuntimeVersion": "1.0.13", + "requiresProviderPolicy": false, + "acpxVersion": "0.13.1", + "sources": { + "patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c", + "packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73", + "packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b", + "packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55", + "packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:401b2edf0678e3880eed4120137fd1fef96da423955b222f506ddf582a7cc520", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:e97a7c4d2cf9be9c6e0ada78076a5551b01401518597ab81cb6fbd5a1faf4a5e", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:cfbbb39a2e20f449df53ceaea4a6cbd068c954c10405bea2d908b93e3f387fdf" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/grok-profile-v15-kimi-foundation-81fbfb44-identity.json b/packages/paperclip-runner/test/fixtures/grok-profile-v15-kimi-foundation-81fbfb44-identity.json new file mode 100644 index 00000000000..8667845f574 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/grok-profile-v15-kimi-foundation-81fbfb44-identity.json @@ -0,0 +1,35 @@ +{ + "commandDigest": "sha256:99a7c5a38e3786fc08cc27e54902b0d36b07be15c61e3ca5645088c301c35067", + "sourceDigest": "sha256:1c16b81b5743471f4b5143943fcfc860f14f8ae0ca9255bc8b2fc8542bdede5a", + "declaration": { + "schema": "paperclip.grok-profile-identity.v1", + "agent": "grok", + "agentProfileVersion": 15, + "agentServerPackage": "builtin:grok-acp", + "agentServerVersion": "1", + "agentRuntimePackage": "native:grok", + "agentRuntimeVersion": "1.0.13", + "requiresProviderPolicy": false, + "acpxVersion": "0.13.1", + "sources": { + "patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c", + "packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73", + "packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b", + "packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55", + "packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:8c1d2bdc1cdd808b51b624b1bd1b576b8f72e72e89fbac3ff1f9beef14878480", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/grok-profile-v16-identity.json b/packages/paperclip-runner/test/fixtures/grok-profile-v16-identity.json new file mode 100644 index 00000000000..c924c1f8d6a --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/grok-profile-v16-identity.json @@ -0,0 +1,35 @@ +{ + "commandDigest": "sha256:4d6b4c857ba6e63a3eb8c5c406280e4bd0164d3109c940918392c52868bb4263", + "sourceDigest": "sha256:a32325cdbc87730f602b1b9349310fcabab03de77b3e4b07a514948a03628601", + "declaration": { + "schema": "paperclip.grok-profile-identity.v1", + "agent": "grok", + "agentProfileVersion": 16, + "agentServerPackage": "builtin:grok-acp", + "agentServerVersion": "1", + "agentRuntimePackage": "native:grok", + "agentRuntimeVersion": "1.0.13", + "requiresProviderPolicy": false, + "acpxVersion": "0.13.1", + "sources": { + "patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c", + "packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73", + "packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b", + "packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55", + "packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:da62bf0a159b6fe65c3fff5d9ed7ad58ae32cff6b22ed1b787f909e8eaa0f564", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/grok-profile-v17-identity.json b/packages/paperclip-runner/test/fixtures/grok-profile-v17-identity.json new file mode 100644 index 00000000000..8008f24963e --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/grok-profile-v17-identity.json @@ -0,0 +1,35 @@ +{ + "commandDigest": "sha256:8c9cb917026844e362e1b28da8529662a15da23cad1002ab5bcec79031b1339e", + "sourceDigest": "sha256:57da46dc476d78a55ffb61111a8ab328c879f1d2689f615ef65bc757149b24b7", + "declaration": { + "schema": "paperclip.grok-profile-identity.v1", + "agent": "grok", + "agentProfileVersion": 17, + "agentServerPackage": "builtin:grok-acp", + "agentServerVersion": "1", + "agentRuntimePackage": "native:grok", + "agentRuntimeVersion": "1.0.13", + "requiresProviderPolicy": false, + "acpxVersion": "0.13.1", + "sources": { + "patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c", + "packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73", + "packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b", + "packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55", + "packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:01836e46cba936845c5e612775a9fa5dfc277cdcb7f54cb97da9a334d9580fe1", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/grok-profile-v18-identity.json b/packages/paperclip-runner/test/fixtures/grok-profile-v18-identity.json new file mode 100644 index 00000000000..f3b3463e1a5 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/grok-profile-v18-identity.json @@ -0,0 +1,36 @@ +{ + "commandDigest": "sha256:4996686ac076be94980dc3f2e2ad58aac10b2a70e28e4507c263d7587b33dd4e", + "sourceDigest": "sha256:b10fe1bd9c5e5928d2afe984992093107ce10590e22296e1e3a91897567bfeb6", + "declaration": { + "schema": "paperclip.grok-profile-identity.v1", + "agent": "grok", + "agentProfileVersion": 18, + "agentServerPackage": "builtin:grok-acp", + "agentServerVersion": "1", + "agentRuntimePackage": "native:grok", + "agentRuntimeVersion": "1.0.13", + "requiresProviderPolicy": false, + "acpxVersion": "0.13.1", + "sources": { + "patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2", + "packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:477a05847187159f9c6f07bf90f1c0c1d432a9eb6fdb15d5620daf54443dafd8", + "packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c", + "packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af", + "packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777", + "packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c", + "packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73", + "packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e", + "packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "packages/paperclip-runner/src/drivers/acpx/generated-sidecar-contract.ts": "sha256:b60e4b924e827e122860d2a4cc13b465ba748ee521620887c88f3c58c0df6081", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b", + "packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55", + "packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:01836e46cba936845c5e612775a9fa5dfc277cdcb7f54cb97da9a334d9580fe1", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54" + } + } +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v1-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v1-identity.json new file mode 100644 index 00000000000..1617dc8d63d --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v1-identity.json @@ -0,0 +1,28 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 1, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:94a141bcd76dfa5a92ecf9ff51623474a5b46e4fe9a5955c0e030a6fbe00af62", + "scripts/kimi-instructions-runtime.mjs": "sha256:7697cd40f1123ce607ec61548dde1c31dff496f3fd7aed1afbe75bbe434e7859", + "scripts/materialize-kimi-distribution.mjs": "sha256:7d61f91d2b32a8d22e5949619faed009c2fded8e034e84a2615a76548e2aa4d0", + "scripts/provision-kimi.mjs": "sha256:d11a6320b47c1549572f192e6a3d532b4c48a7f2499afd91539626c04e917bca", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:9c291ba7a09e1dd5a6774e7c096c1e0829bb99d48aef927880890944e6676d45", + "src/drivers/acpx/kimi-config.ts": "sha256:718e238d97922211fa95e3df88a40321bf1ac2b63b395cd2e8f03e89b56616ed", + "src/drivers/acpx/kimi-installation.ts": "sha256:753a1f8505bcfaff83ac76c425e45e4ff780861a6b6b0fbcb71e96c0c482669d", + "src/drivers/acpx/kimi-instructions.ts": "sha256:a04e2d17aac37da2463c8a2c057f9e922ecdb68d9e922e9288e0445c870f9a36", + "src/drivers/acpx/instruction-admission.ts": "sha256:371c17f328abed44f90ac37f3b686e1c0a0d6b647518e62935262ecc31450b16" + } + }, + "commandDigest": "sha256:260b99cef27309297ebf1366c6f588bcd83b2c35e4374106bdae2315a0edb84c" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v10-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v10-identity.json new file mode 100644 index 00000000000..b25b0cafd51 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v10-identity.json @@ -0,0 +1,48 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 10, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "src/drivers/acpx/environment.ts": "sha256:a6733e28a9468b744e1e72f1af0c2236e17372e51a9a3ba254a406e085591d2d", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:2860a13ee316924221c3bf4944e97ecde23c2f582270924b80e1e41648e21f31", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:56151bc4c58d5ba88018edb3588cd6f22451a4d83b2a129c9e23ab3c94cf2978", + "src/cli/kimi-metadata-probe.ts": "sha256:953cb37cbf16accde2f81848ad4d64c7c30355b02952f6528f966eee1511fbf9", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324" + } + }, + "commandDigest": "sha256:c8a39af9d580cdb5fe20a3333a566e2f97a1b4ec3a0025cb647d2144df358604" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v11-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v11-identity.json new file mode 100644 index 00000000000..83b9e12caf9 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v11-identity.json @@ -0,0 +1,48 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 11, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "src/drivers/acpx/environment.ts": "sha256:a6733e28a9468b744e1e72f1af0c2236e17372e51a9a3ba254a406e085591d2d", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:70f7b314e86a22ad2df192eb6ff984e5f286002e165eec71292fff75064f7b9f", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:56151bc4c58d5ba88018edb3588cd6f22451a4d83b2a129c9e23ab3c94cf2978", + "src/cli/kimi-metadata-probe.ts": "sha256:953cb37cbf16accde2f81848ad4d64c7c30355b02952f6528f966eee1511fbf9", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324" + } + }, + "commandDigest": "sha256:6bdee2692b41fea57ac9be810a5009bb5d65650beca9c816e310b27f0974cccc" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v13-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v13-identity.json new file mode 100644 index 00000000000..aa2b85ad077 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v13-identity.json @@ -0,0 +1,72 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 13, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/session-context-admission.ts": "sha256:c4026e6c51b9a7fb731b178e03077917f6f0f71835da0cdb083dce11f2ddae93", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "src/drivers/acpx/environment.ts": "sha256:9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "src/cli/acpx-runtime-sidecar.ts": "sha256:6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:70f7b314e86a22ad2df192eb6ff984e5f286002e165eec71292fff75064f7b9f", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:0f6f2865065500d77460ec761fa13661a3149dd34422812201026709f7cb84f8", + "src/cli/kimi-metadata-probe.ts": "sha256:306e2caca87977c3ea511ec43c937fd801f4c8cf9b4337b1ed8a3e315c1cdb37", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324", + "src/drivers/acpx/candidate-admission.ts": "sha256:72a3c8b1e97f6aeedaeff4fd4a75edfbd2104000607517c190dd4c34f6d6327c", + "src/drivers/acpx/qualified-profiles.ts": "sha256:f0c3948ba291aa4b9ae54c0aebd2bd7967739ce40d48ccbe717491500fd9105b", + "src/drivers/acpx/profile-installation.ts": "sha256:d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "src/drivers/acpx/driver-profile.ts": "sha256:857d75d11cb1b82d22275443dab79ef674214b35f103dff30e1ab14915d80a47", + "src/live/runnerd-codex-transport.ts": "sha256:1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runner/crates/runner-core/src/acpx_candidate_admission.rs": "sha256:65e14dd8cd7304c92b63972801dad535d52b5a6a4ea499f6c315dbc82ce258d6", + "runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:8c1d2bdc1cdd808b51b624b1bd1b576b8f72e72e89fbac3ff1f9beef14878480", + "runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54", + "runner/crates/runner-core/src/native_provider_backend.rs": "sha256:d2aa6a65be9195e5db0fbe83103be7f6897dedb8195872191d3eb0392cb4a1cc", + "runner/crates/runner-core/src/durable/runner.rs": "sha256:7335562b0393c011fe60d7820afeea965345478dd72757dbf91f025d44d3c36c", + "runner/crates/runner-core/src/durable/mod.rs": "sha256:b5989da5304dfdfe04d0060a0a249e853c784af1cdd933bcf0d5a8fd07f0e895", + "src/provider-events.ts": "sha256:a6b77d969fd2f767dd8e583e81b907973c429b6e32d2c2d7fef0856f3568f07a", + "src/native-session-runtime.ts": "sha256:cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "src/control-plane/durable-prp-control-plane.ts": "sha256:a8a8bed373cdd18397a762b9f67b04c789d9e5d63208f06c5ebe000faff090ac", + "src/drivers/codex/codex-harness-session.ts": "sha256:bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "src/drivers/codex/codex-session-notifications.ts": "sha256:23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "src/drivers/codex/codex-session-state.ts": "sha256:c2f1059c5f1532cf3c2059792324a6f55d0c06ec8a62c3a4ab2c9451d617988a", + "runner/crates/runner-core/src/provider_backend.rs": "sha256:03a57d83901f0fbccfb7bcdaeae79baf8918f658ceb3463406211431944a461e", + "runner/crates/runner-core/src/provider_bridge.rs": "sha256:d417840ecd87e1a8a407629074f6c64bad4071c5e9add4b5a2aef0b3403b5529", + "runner/crates/runner-core/src/provider_events.rs": "sha256:91dfa4985691f86ab7f60514e46d924fb815900d6d456d78780bd94d29bff461", + "runner/crates/runner-core/src/acpx_event_scope.rs": "sha256:088f4b5c67b5663cbce2ef0b8b8cb3e6fe04d2b5c9cfa7a8139f8f0bf91e476e", + "runner/crates/runner-core/src/acpx_event_payload.rs": "sha256:f3b2f6ea9c2e9cf673790eef2f7d4e738c36a2e6347e1fd61ae501793cef1f15", + "scripts/generate-acpx-profiles.mjs": "sha256:e878a3f8afa9ce38940e61343cfbdc55d6431e9574108ca5fb17f0c72ead7423" + } + }, + "commandDigest": "sha256:8fcd50b260d61244499bb2de97be745dca7652b9901acd04aa54a135c4bf23cb" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v14-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v14-identity.json new file mode 100644 index 00000000000..f239948d95d --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v14-identity.json @@ -0,0 +1,72 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 14, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/session-context-admission.ts": "sha256:c4026e6c51b9a7fb731b178e03077917f6f0f71835da0cdb083dce11f2ddae93", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "src/drivers/acpx/environment.ts": "sha256:9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "src/cli/acpx-runtime-sidecar.ts": "sha256:6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:70f7b314e86a22ad2df192eb6ff984e5f286002e165eec71292fff75064f7b9f", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:0f6f2865065500d77460ec761fa13661a3149dd34422812201026709f7cb84f8", + "src/cli/kimi-metadata-probe.ts": "sha256:306e2caca87977c3ea511ec43c937fd801f4c8cf9b4337b1ed8a3e315c1cdb37", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324", + "src/drivers/acpx/candidate-admission.ts": "sha256:72a3c8b1e97f6aeedaeff4fd4a75edfbd2104000607517c190dd4c34f6d6327c", + "src/drivers/acpx/qualified-profiles.ts": "sha256:f0c3948ba291aa4b9ae54c0aebd2bd7967739ce40d48ccbe717491500fd9105b", + "src/drivers/acpx/profile-installation.ts": "sha256:d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "src/drivers/acpx/driver-profile.ts": "sha256:857d75d11cb1b82d22275443dab79ef674214b35f103dff30e1ab14915d80a47", + "src/live/runnerd-codex-transport.ts": "sha256:1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runner/crates/runner-core/src/acpx_candidate_admission.rs": "sha256:65e14dd8cd7304c92b63972801dad535d52b5a6a4ea499f6c315dbc82ce258d6", + "runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:da62bf0a159b6fe65c3fff5d9ed7ad58ae32cff6b22ed1b787f909e8eaa0f564", + "runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54", + "runner/crates/runner-core/src/native_provider_backend.rs": "sha256:d2aa6a65be9195e5db0fbe83103be7f6897dedb8195872191d3eb0392cb4a1cc", + "runner/crates/runner-core/src/durable/runner.rs": "sha256:7335562b0393c011fe60d7820afeea965345478dd72757dbf91f025d44d3c36c", + "runner/crates/runner-core/src/durable/mod.rs": "sha256:b5989da5304dfdfe04d0060a0a249e853c784af1cdd933bcf0d5a8fd07f0e895", + "src/provider-events.ts": "sha256:a6b77d969fd2f767dd8e583e81b907973c429b6e32d2c2d7fef0856f3568f07a", + "src/native-session-runtime.ts": "sha256:cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "src/control-plane/durable-prp-control-plane.ts": "sha256:a8a8bed373cdd18397a762b9f67b04c789d9e5d63208f06c5ebe000faff090ac", + "src/drivers/codex/codex-harness-session.ts": "sha256:bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "src/drivers/codex/codex-session-notifications.ts": "sha256:23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "src/drivers/codex/codex-session-state.ts": "sha256:c2f1059c5f1532cf3c2059792324a6f55d0c06ec8a62c3a4ab2c9451d617988a", + "runner/crates/runner-core/src/provider_backend.rs": "sha256:03a57d83901f0fbccfb7bcdaeae79baf8918f658ceb3463406211431944a461e", + "runner/crates/runner-core/src/provider_bridge.rs": "sha256:d417840ecd87e1a8a407629074f6c64bad4071c5e9add4b5a2aef0b3403b5529", + "runner/crates/runner-core/src/provider_events.rs": "sha256:91dfa4985691f86ab7f60514e46d924fb815900d6d456d78780bd94d29bff461", + "runner/crates/runner-core/src/acpx_event_scope.rs": "sha256:088f4b5c67b5663cbce2ef0b8b8cb3e6fe04d2b5c9cfa7a8139f8f0bf91e476e", + "runner/crates/runner-core/src/acpx_event_payload.rs": "sha256:f3b2f6ea9c2e9cf673790eef2f7d4e738c36a2e6347e1fd61ae501793cef1f15", + "scripts/generate-acpx-profiles.mjs": "sha256:e878a3f8afa9ce38940e61343cfbdc55d6431e9574108ca5fb17f0c72ead7423" + } + }, + "commandDigest": "sha256:d917ef7222b71c8dbe1df46c33f7a439f15492db862307aa6b5427bf82ed196b" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v15-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v15-identity.json new file mode 100644 index 00000000000..535f22daa03 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v15-identity.json @@ -0,0 +1,72 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 15, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/session-context-admission.ts": "sha256:c4026e6c51b9a7fb731b178e03077917f6f0f71835da0cdb083dce11f2ddae93", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "src/drivers/acpx/environment.ts": "sha256:9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "src/cli/acpx-runtime-sidecar.ts": "sha256:6816fe12cdd274746b5241b75823b3c347b245f3cc03cdbe70f05ec1a0250663", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:70f7b314e86a22ad2df192eb6ff984e5f286002e165eec71292fff75064f7b9f", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:0f6f2865065500d77460ec761fa13661a3149dd34422812201026709f7cb84f8", + "src/cli/kimi-metadata-probe.ts": "sha256:306e2caca87977c3ea511ec43c937fd801f4c8cf9b4337b1ed8a3e315c1cdb37", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324", + "src/drivers/acpx/candidate-admission.ts": "sha256:72a3c8b1e97f6aeedaeff4fd4a75edfbd2104000607517c190dd4c34f6d6327c", + "src/drivers/acpx/qualified-profiles.ts": "sha256:f0c3948ba291aa4b9ae54c0aebd2bd7967739ce40d48ccbe717491500fd9105b", + "src/drivers/acpx/profile-installation.ts": "sha256:d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "src/drivers/acpx/driver-profile.ts": "sha256:857d75d11cb1b82d22275443dab79ef674214b35f103dff30e1ab14915d80a47", + "src/live/runnerd-codex-transport.ts": "sha256:1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runner/crates/runner-core/src/acpx_candidate_admission.rs": "sha256:65e14dd8cd7304c92b63972801dad535d52b5a6a4ea499f6c315dbc82ce258d6", + "runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:01836e46cba936845c5e612775a9fa5dfc277cdcb7f54cb97da9a334d9580fe1", + "runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54", + "runner/crates/runner-core/src/native_provider_backend.rs": "sha256:d2aa6a65be9195e5db0fbe83103be7f6897dedb8195872191d3eb0392cb4a1cc", + "runner/crates/runner-core/src/durable/runner.rs": "sha256:7335562b0393c011fe60d7820afeea965345478dd72757dbf91f025d44d3c36c", + "runner/crates/runner-core/src/durable/mod.rs": "sha256:b5989da5304dfdfe04d0060a0a249e853c784af1cdd933bcf0d5a8fd07f0e895", + "src/provider-events.ts": "sha256:a6b77d969fd2f767dd8e583e81b907973c429b6e32d2c2d7fef0856f3568f07a", + "src/native-session-runtime.ts": "sha256:cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "src/control-plane/durable-prp-control-plane.ts": "sha256:a8a8bed373cdd18397a762b9f67b04c789d9e5d63208f06c5ebe000faff090ac", + "src/drivers/codex/codex-harness-session.ts": "sha256:bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "src/drivers/codex/codex-session-notifications.ts": "sha256:23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "src/drivers/codex/codex-session-state.ts": "sha256:c2f1059c5f1532cf3c2059792324a6f55d0c06ec8a62c3a4ab2c9451d617988a", + "runner/crates/runner-core/src/provider_backend.rs": "sha256:03a57d83901f0fbccfb7bcdaeae79baf8918f658ceb3463406211431944a461e", + "runner/crates/runner-core/src/provider_bridge.rs": "sha256:d417840ecd87e1a8a407629074f6c64bad4071c5e9add4b5a2aef0b3403b5529", + "runner/crates/runner-core/src/provider_events.rs": "sha256:91dfa4985691f86ab7f60514e46d924fb815900d6d456d78780bd94d29bff461", + "runner/crates/runner-core/src/acpx_event_scope.rs": "sha256:088f4b5c67b5663cbce2ef0b8b8cb3e6fe04d2b5c9cfa7a8139f8f0bf91e476e", + "runner/crates/runner-core/src/acpx_event_payload.rs": "sha256:f3b2f6ea9c2e9cf673790eef2f7d4e738c36a2e6347e1fd61ae501793cef1f15", + "scripts/generate-acpx-profiles.mjs": "sha256:e878a3f8afa9ce38940e61343cfbdc55d6431e9574108ca5fb17f0c72ead7423" + } + }, + "commandDigest": "sha256:606c0cfcaee89813bef5531d666eb208e839116de27cdd46035a08d38c00ecce" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v16-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v16-identity.json new file mode 100644 index 00000000000..c8b5ffdad66 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v16-identity.json @@ -0,0 +1,75 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 16, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/session-context-admission.ts": "sha256:c4026e6c51b9a7fb731b178e03077917f6f0f71835da0cdb083dce11f2ddae93", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb", + "src/drivers/acpx/environment.ts": "sha256:9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71", + "src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88", + "src/cli/acpx-runtime-sidecar.ts": "sha256:477a05847187159f9c6f07bf90f1c0c1d432a9eb6fdb15d5620daf54443dafd8", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:70f7b314e86a22ad2df192eb6ff984e5f286002e165eec71292fff75064f7b9f", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:0f6f2865065500d77460ec761fa13661a3149dd34422812201026709f7cb84f8", + "src/cli/kimi-metadata-probe.ts": "sha256:306e2caca87977c3ea511ec43c937fd801f4c8cf9b4337b1ed8a3e315c1cdb37", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:b60e4b924e827e122860d2a4cc13b465ba748ee521620887c88f3c58c0df6081", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324", + "src/drivers/acpx/candidate-admission.ts": "sha256:72a3c8b1e97f6aeedaeff4fd4a75edfbd2104000607517c190dd4c34f6d6327c", + "src/drivers/acpx/qualified-profiles.ts": "sha256:f0c3948ba291aa4b9ae54c0aebd2bd7967739ce40d48ccbe717491500fd9105b", + "src/drivers/acpx/profile-installation.ts": "sha256:d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863", + "src/drivers/acpx/driver-profile.ts": "sha256:857d75d11cb1b82d22275443dab79ef674214b35f103dff30e1ab14915d80a47", + "src/live/runnerd-codex-transport.ts": "sha256:1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49", + "runner/crates/runner-core/src/acpx_candidate_admission.rs": "sha256:65e14dd8cd7304c92b63972801dad535d52b5a6a4ea499f6c315dbc82ce258d6", + "runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:01836e46cba936845c5e612775a9fa5dfc277cdcb7f54cb97da9a334d9580fe1", + "runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e", + "runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54", + "runner/crates/runner-core/src/native_provider_backend.rs": "sha256:d2aa6a65be9195e5db0fbe83103be7f6897dedb8195872191d3eb0392cb4a1cc", + "runner/crates/runner-core/src/durable/runner.rs": "sha256:7335562b0393c011fe60d7820afeea965345478dd72757dbf91f025d44d3c36c", + "runner/crates/runner-core/src/durable/mod.rs": "sha256:b5989da5304dfdfe04d0060a0a249e853c784af1cdd933bcf0d5a8fd07f0e895", + "src/contracts/runtime-context.ts": "sha256:152d7187ce1dc91a15377d368776555c9897d6e018b65f0f59a92dcdbd6497d9", + "src/contracts/generated-runtime-context-contract.ts": "sha256:069f4bc3c4dc95888f30d3fea331adb0ee0a164d09f4f70ec9c132f24f69c7dd", + "src/contracts/runtime-context-prompts.ts": "sha256:fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "src/provider-events.ts": "sha256:a6b77d969fd2f767dd8e583e81b907973c429b6e32d2c2d7fef0856f3568f07a", + "src/native-session-runtime.ts": "sha256:cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea", + "src/control-plane/durable-prp-control-plane.ts": "sha256:a8a8bed373cdd18397a762b9f67b04c789d9e5d63208f06c5ebe000faff090ac", + "src/drivers/codex/codex-harness-session.ts": "sha256:bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851", + "src/drivers/codex/codex-session-notifications.ts": "sha256:23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa", + "src/drivers/codex/codex-session-state.ts": "sha256:c2f1059c5f1532cf3c2059792324a6f55d0c06ec8a62c3a4ab2c9451d617988a", + "runner/crates/runner-core/src/provider_backend.rs": "sha256:03a57d83901f0fbccfb7bcdaeae79baf8918f658ceb3463406211431944a461e", + "runner/crates/runner-core/src/provider_bridge.rs": "sha256:d417840ecd87e1a8a407629074f6c64bad4071c5e9add4b5a2aef0b3403b5529", + "runner/crates/runner-core/src/provider_events.rs": "sha256:91dfa4985691f86ab7f60514e46d924fb815900d6d456d78780bd94d29bff461", + "runner/crates/runner-core/src/acpx_event_scope.rs": "sha256:088f4b5c67b5663cbce2ef0b8b8cb3e6fe04d2b5c9cfa7a8139f8f0bf91e476e", + "runner/crates/runner-core/src/acpx_event_payload.rs": "sha256:f3b2f6ea9c2e9cf673790eef2f7d4e738c36a2e6347e1fd61ae501793cef1f15", + "scripts/generate-acpx-profiles.mjs": "sha256:e878a3f8afa9ce38940e61343cfbdc55d6431e9574108ca5fb17f0c72ead7423" + } + }, + "commandDigest": "sha256:e027eb4ec2f99fc485b76a3e17212810d0cefa873c55365b44b4e5ba815439bf" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v2-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v2-identity.json new file mode 100644 index 00000000000..64633f1b2c5 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v2-identity.json @@ -0,0 +1,28 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 2, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:7bea24c45b7f32a72168bf6da57cb6fd472cd2ef9fac0c196b7f050e81a8b62a", + "scripts/kimi-instructions-runtime.mjs": "sha256:7697cd40f1123ce607ec61548dde1c31dff496f3fd7aed1afbe75bbe434e7859", + "scripts/materialize-kimi-distribution.mjs": "sha256:7d61f91d2b32a8d22e5949619faed009c2fded8e034e84a2615a76548e2aa4d0", + "scripts/provision-kimi.mjs": "sha256:d11a6320b47c1549572f192e6a3d532b4c48a7f2499afd91539626c04e917bca", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:b40e709dbd3fdc04bd1bc1f2ec5a34a2eb09b353a11d3b1b86c35bbe3dcbd513", + "src/drivers/acpx/kimi-config.ts": "sha256:b927c3f2437094d4492a1499559247220e154fcae719ecfe75bb63defc7df05e", + "src/drivers/acpx/kimi-installation.ts": "sha256:753a1f8505bcfaff83ac76c425e45e4ff780861a6b6b0fbcb71e96c0c482669d", + "src/drivers/acpx/kimi-instructions.ts": "sha256:a04e2d17aac37da2463c8a2c057f9e922ecdb68d9e922e9288e0445c870f9a36", + "src/drivers/acpx/instruction-admission.ts": "sha256:371c17f328abed44f90ac37f3b686e1c0a0d6b647518e62935262ecc31450b16" + } + }, + "commandDigest": "sha256:103f3a3d84353fa6d3244fe51065bd62159dc32adb88a69d16dbde495c012360" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v3-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v3-identity.json new file mode 100644 index 00000000000..d53a06d2998 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v3-identity.json @@ -0,0 +1,35 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 3, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd3686d4eb7da9bf22f75c28ee307830f106dd0426d70d5961e8f20b8309c6c8", + "scripts/kimi-instructions-runtime.mjs": "sha256:50ae002d19276b7fd5b85fd8cf1eace033f986486645d07b8eabfcb759c378ef", + "scripts/materialize-kimi-distribution.mjs": "sha256:7d61f91d2b32a8d22e5949619faed009c2fded8e034e84a2615a76548e2aa4d0", + "scripts/provision-kimi.mjs": "sha256:d11a6320b47c1549572f192e6a3d532b4c48a7f2499afd91539626c04e917bca", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:37a526622ccecceba448254cd1bf503b4afa2a3db3ed1fd60351a5936509608f", + "src/drivers/acpx/kimi-config.ts": "sha256:b927c3f2437094d4492a1499559247220e154fcae719ecfe75bb63defc7df05e", + "src/drivers/acpx/kimi-installation.ts": "sha256:753a1f8505bcfaff83ac76c425e45e4ff780861a6b6b0fbcb71e96c0c482669d", + "src/drivers/acpx/kimi-instructions.ts": "sha256:a04e2d17aac37da2463c8a2c057f9e922ecdb68d9e922e9288e0445c870f9a36", + "src/drivers/acpx/instruction-admission.ts": "sha256:371c17f328abed44f90ac37f3b686e1c0a0d6b647518e62935262ecc31450b16", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:bab2a716764e4ea752a4bdcc71c7bdf4378633c7fb16a1a9cb158ee3724bd5c9", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:4dae5e87eca617a7f84c1fafa6c78dea4b94b4ebe8814162674d54b3b929eef1", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:7dfbf8d6e613900dcae06f0b5462dd916bd3d25cade2740c78ac0e27f7ac1157", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:827f6beb5d36bde8bec3881b41cdaffe8571ce360af22617cfee509cd03a7984", + "src/cli/acpx-runtime-sidecar.ts": "sha256:488b2615829a389866954cb4a240ddd88f7067d7940fdeaa9f443104eb4bb95f" + } + }, + "commandDigest": "sha256:a5e1790881f10503aff7423a27237427c517dc13b395a7fdc3bc0c52556a7640" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v4-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v4-identity.json new file mode 100644 index 00000000000..9d9231034a8 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v4-identity.json @@ -0,0 +1,35 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 4, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:ff2864bcb275a1c3da2c38f67865910107814ca155fd6c57b2470bcd2fcda01d", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:7b1cafde992fc54255e5d29d8fdccf7b10e71e439d08d7b9826aa5841dea8555", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:72a6eb8ae1857326dea399de3c2ec8c95075957347eda75188afef5163bdad3c", + "src/drivers/acpx/kimi-config.ts": "sha256:fc1a718d766d75058083aa6b913585d6bf2c747fffeebced88efb48d64bd2186", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:b2ff442570f314332f6010334899d9b444dab0def9343ad8269f54503232385e", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:7dfbf8d6e613900dcae06f0b5462dd916bd3d25cade2740c78ac0e27f7ac1157", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:827f6beb5d36bde8bec3881b41cdaffe8571ce360af22617cfee509cd03a7984", + "src/cli/acpx-runtime-sidecar.ts": "sha256:488b2615829a389866954cb4a240ddd88f7067d7940fdeaa9f443104eb4bb95f" + } + }, + "commandDigest": "sha256:0b05cfa4326ac517f6963a9d0701da3e42a420c5ee9c3d8e72de9c6b9b78ed68" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v5-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v5-identity.json new file mode 100644 index 00000000000..9a7d739f10d --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v5-identity.json @@ -0,0 +1,35 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 5, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:ff2864bcb275a1c3da2c38f67865910107814ca155fd6c57b2470bcd2fcda01d", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:72a6eb8ae1857326dea399de3c2ec8c95075957347eda75188afef5163bdad3c", + "src/drivers/acpx/kimi-config.ts": "sha256:fc1a718d766d75058083aa6b913585d6bf2c747fffeebced88efb48d64bd2186", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:b2ff442570f314332f6010334899d9b444dab0def9343ad8269f54503232385e", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:7dfbf8d6e613900dcae06f0b5462dd916bd3d25cade2740c78ac0e27f7ac1157", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:827f6beb5d36bde8bec3881b41cdaffe8571ce360af22617cfee509cd03a7984", + "src/cli/acpx-runtime-sidecar.ts": "sha256:488b2615829a389866954cb4a240ddd88f7067d7940fdeaa9f443104eb4bb95f" + } + }, + "commandDigest": "sha256:ca8ead6a1fc21144af5fc14934867f5b286f4c13771cee01ccd331193fff909e" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v6-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v6-identity.json new file mode 100644 index 00000000000..03d153daa62 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v6-identity.json @@ -0,0 +1,35 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 6, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:ff2864bcb275a1c3da2c38f67865910107814ca155fd6c57b2470bcd2fcda01d", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:72a6eb8ae1857326dea399de3c2ec8c95075957347eda75188afef5163bdad3c", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:b2ff442570f314332f6010334899d9b444dab0def9343ad8269f54503232385e", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:7dfbf8d6e613900dcae06f0b5462dd916bd3d25cade2740c78ac0e27f7ac1157", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:827f6beb5d36bde8bec3881b41cdaffe8571ce360af22617cfee509cd03a7984", + "src/cli/acpx-runtime-sidecar.ts": "sha256:488b2615829a389866954cb4a240ddd88f7067d7940fdeaa9f443104eb4bb95f" + } + }, + "commandDigest": "sha256:0d8b02a74558ec3486870b473fb5b07f57b59460f2698113371c1ff65c5d73cf" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v7-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v7-identity.json new file mode 100644 index 00000000000..c7aaf85375b --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v7-identity.json @@ -0,0 +1,48 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 7, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:0c2a180fe6fbe4de276d5dfc83dfd5870a5257317db62b7bf6c8afddbe063548", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:13511f26a31248e42063c49eeaf6235b7448df12049c1bf45c2170cc20767175", + "scripts/kimi-distribution/package-lock.json": "sha256:ff2cf7356dc031cac15d9f3339cf82bd1811399af59396e2d7629648c21298cf", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:1a9d512c6fe1f79a3a4b861ae5b51fbc9fc68b55249b31694515ec9f846a0332", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:b2ff442570f314332f6010334899d9b444dab0def9343ad8269f54503232385e", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:7dfbf8d6e613900dcae06f0b5462dd916bd3d25cade2740c78ac0e27f7ac1157", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:827f6beb5d36bde8bec3881b41cdaffe8571ce360af22617cfee509cd03a7984", + "src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:2860a13ee316924221c3bf4944e97ecde23c2f582270924b80e1e41648e21f31", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:56151bc4c58d5ba88018edb3588cd6f22451a4d83b2a129c9e23ab3c94cf2978", + "src/cli/kimi-metadata-probe.ts": "sha256:953cb37cbf16accde2f81848ad4d64c7c30355b02952f6528f966eee1511fbf9", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324" + } + }, + "commandDigest": "sha256:a674226081364c1e86d93e96e9ee7b268fd733a97e1613ebdca0b940990aac0c" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v8-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v8-identity.json new file mode 100644 index 00000000000..4074916451e --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v8-identity.json @@ -0,0 +1,48 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 8, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:b2ff442570f314332f6010334899d9b444dab0def9343ad8269f54503232385e", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:7dfbf8d6e613900dcae06f0b5462dd916bd3d25cade2740c78ac0e27f7ac1157", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:827f6beb5d36bde8bec3881b41cdaffe8571ce360af22617cfee509cd03a7984", + "src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:2860a13ee316924221c3bf4944e97ecde23c2f582270924b80e1e41648e21f31", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:56151bc4c58d5ba88018edb3588cd6f22451a4d83b2a129c9e23ab3c94cf2978", + "src/cli/kimi-metadata-probe.ts": "sha256:953cb37cbf16accde2f81848ad4d64c7c30355b02952f6528f966eee1511fbf9", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324" + } + }, + "commandDigest": "sha256:53c63af8722894e14d48ed55526f99fcd79e06c86ae1c7aa384ef7ee8c56efd4" +} diff --git a/packages/paperclip-runner/test/fixtures/kimi-profile-v9-identity.json b/packages/paperclip-runner/test/fixtures/kimi-profile-v9-identity.json new file mode 100644 index 00000000000..ac90ac3ef5f --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/kimi-profile-v9-identity.json @@ -0,0 +1,48 @@ +{ + "declaration": { + "schema": "paperclip.kimi-profile-identity.v1", + "agent": "kimi", + "agentProfileVersion": 9, + "agentServerPackage": "@moonshot-ai/kimi-code", + "agentServerVersion": "2.3.3", + "agentRuntimePackage": null, + "agentRuntimeVersion": null, + "requiresProviderPolicy": true, + "qualificationStatus": "pending", + "acpxVersion": "0.13.1", + "sources": { + "scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b", + "scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746", + "scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879", + "scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045", + "scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb", + "scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b", + "src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f", + "src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2", + "src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453", + "src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991", + "src/drivers/acpx/instruction-admission.ts": "sha256:adf5301e86f2b652679f1b18d00659b9b7214d16539dbd12ef9041586ee97944", + "src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b", + "src/configured-environment.ts": "sha256:b48a72c1af77c88af76b7df527fdb831c714738042c93906f4da7ae0728ef957", + "src/drivers/acpx/environment.ts": "sha256:a5209468735f73d50acc20aaefb7854eea5fd9e4ad281542e57d5aafb61b19a1", + "src/drivers/acpx/codex-runtime-adapter.ts": "sha256:601cbfe4e2811f771a3a59bf23fdbb05e33a04901af987f2ff96a8acf56374a3", + "src/drivers/acpx/runtime-host.ts": "sha256:981a18aad23e34c3c675123d1dd0a920748821bad88885bb1a2e1a060c870921", + "src/drivers/acpx/runtime-sandbox.ts": "sha256:d631dc7a3554c5eb4ed47bef966007245a133862277c96011b1fe1639135f59d", + "src/cli/acpx-runtime-sidecar.ts": "sha256:152ec3e5066695c7a28b56cb9b4d5fbd30500c8e7beee54efb9a33fc8a532c9d", + "scripts/build-verified-provider-entrypoints.mjs": "sha256:2860a13ee316924221c3bf4944e97ecde23c2f582270924b80e1e41648e21f31", + "src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92", + "src/drivers/acpx/kimi-metadata-probe.ts": "sha256:56151bc4c58d5ba88018edb3588cd6f22451a4d83b2a129c9e23ab3c94cf2978", + "src/cli/kimi-metadata-probe.ts": "sha256:953cb37cbf16accde2f81848ad4d64c7c30355b02952f6528f966eee1511fbf9", + "src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0", + "src/drivers/acpx/codex-acpx-driver.ts": "sha256:9ee8f25f1ebcd44af15f12a46e5735980083672cd317f44cd10cf2e77699e352", + "src/drivers/acpx/sidecar-protocol.ts": "sha256:26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43", + "src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0", + "src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "src/drivers/acpx/generated-sidecar-contract.ts": "sha256:1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324" + } + }, + "commandDigest": "sha256:c42fa5361d53081a8bb497c35e1ee2f85e6c26d2e0d59fad14b93d16f810796b" +} diff --git a/packages/paperclip-runner/test/fixtures/profile-history-lineage.json b/packages/paperclip-runner/test/fixtures/profile-history-lineage.json new file mode 100644 index 00000000000..37424b2874d --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/profile-history-lineage.json @@ -0,0 +1,147 @@ +{ + "schema": "paperclip.acpx-profile-history-lineage.v1", + "historicalDecodeOnly": true, + "executionRequiresExactCurrentProfile": true, + "records": [ + { + "path": "packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-identity.json", + "originRevision": "a8e07500db68f187b19de83823eb7fabf1196021", + "originPath": "packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-identity.json", + "sha256": "ea4e2c1d571657fc682ad2a75d60a1f638cf6f37d23ac203645f2e4d1115c04d", + "agent": "pi", + "version": 35, + "commandDigest": "sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-kimi-historical-identity.json", + "originRevision": "d07cfde85793ce3e5205b8cc98334a60598f305d", + "originPath": "packages/paperclip-runner/test-fixtures/pi-acp/profile-v35-identity.json", + "sha256": "fff6039d4b79f29f8d8c832ecf2f3f027164980a58911fffcc1cce1652e66dad", + "agent": "pi", + "version": 35, + "commandDigest": "sha256:00ce5ecc7f0e904811b70584b936c673182c1029b500e8d4823f44ae3cd755e2", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/copilot-profile-v82-identity.json", + "originRevision": "a8e07500db68f187b19de83823eb7fabf1196021", + "originPath": "packages/paperclip-runner/test/fixtures/copilot-profile-v82-identity.json", + "sha256": "fe04407493a608d4204403d04ef06c79904a206f3c57547a11612c068a55b99f", + "agent": "copilot", + "version": 82, + "commandDigest": "sha256:99d3a7f3853bc3f518c1579cbe165f38fd4764c020a688814d3adfd7e3d7b1a8", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/copilot-profile-v82-kimi-historical-identity.json", + "originRevision": "d07cfde85793ce3e5205b8cc98334a60598f305d", + "originPath": "packages/paperclip-runner/test/fixtures/copilot-profile-v82-identity.json", + "sha256": "0e0efd23d89c1fbfdb8b621ea17caac2c3ab0f3784f3283a4dfea686de58c4f9", + "agent": "copilot", + "version": 82, + "commandDigest": "sha256:e0eddf258e824bb0cd7e0ec0a12dc5b8eeff72262e176249b5e6e7d5ea977caf", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/copilot-profile-v83-identity.json", + "originRevision": "a8e07500db68f187b19de83823eb7fabf1196021", + "originPath": "packages/paperclip-runner/test/fixtures/copilot-profile-v83-identity.json", + "sha256": "31d024e3870f8a7278939f6f1186e2ec6d2bc331d7a89213aceae88c04b40654", + "agent": "copilot", + "version": 83, + "commandDigest": "sha256:56361a790c38c59633ad4a5adad2d3b4a9098974f15264e4b506d69807902bea", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/copilot-profile-v83-kimi-historical-identity.json", + "originRevision": "d07cfde85793ce3e5205b8cc98334a60598f305d", + "originPath": "packages/paperclip-runner/test/fixtures/copilot-profile-v83-identity.json", + "sha256": "5974cae60f76e04eed978d73593e4f5c3ec271eb0135e56e7bcc61ffe368e9ef", + "agent": "copilot", + "version": 83, + "commandDigest": "sha256:4cecc2ba4bc9674fbc6f585d81934789a98b75a36f074dfe2b40391ae9ef8a19", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/grok-profile-v13-identity.json", + "originRevision": "a8e07500db68f187b19de83823eb7fabf1196021", + "originPath": "packages/paperclip-runner/test/fixtures/grok-profile-v13-identity.json", + "sha256": "374993b170ea181470f6bc606ec568eb347f987e65338d8b980356d97c97b98b", + "agent": "grok", + "version": 13, + "commandDigest": "sha256:e311f8111dd12f0c49bf12a80345cfe5f31a1222e0189209adeabf2f9bd0e1b0", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/grok-profile-v13-kimi-historical-identity.json", + "originRevision": "d07cfde85793ce3e5205b8cc98334a60598f305d", + "originPath": "packages/paperclip-runner/test/fixtures/grok-profile-v13-identity.json", + "sha256": "ce76000eddf14e0c6caca659f40536c986e3101a72babc25c136bfbb0eee6ed5", + "agent": "grok", + "version": 13, + "commandDigest": "sha256:c4beba5aff2708dbab58b00da718e112dc29b91159a4f9cf449104a5ef141a40", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/grok-profile-v14-identity.json", + "originRevision": "a8e07500db68f187b19de83823eb7fabf1196021", + "originPath": "packages/paperclip-runner/test/fixtures/grok-profile-v14-identity.json", + "sha256": "08294ec6bdcbdbe46c8c8b711d8b368558a05f0d84d25c9f5fd0c3b67b79cd17", + "agent": "grok", + "version": 14, + "commandDigest": "sha256:ca59edc7f416cc1e86ac7d7d1e288d3d91065b7ab59e79bcb62bae5f0cc5cf49", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/grok-profile-v14-kimi-historical-identity.json", + "originRevision": "d07cfde85793ce3e5205b8cc98334a60598f305d", + "originPath": "packages/paperclip-runner/test/fixtures/grok-profile-v14-identity.json", + "sha256": "c275b8337162ff441f86e0a31eba8595ebf52e0f1a805a1c00443346719f6b56", + "agent": "grok", + "version": 14, + "commandDigest": "sha256:3140bc190a025afa2f7380a3eb1a2d8aeecd24850ac69f82871847e78c3b17a0", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/copilot-profile-v88-kimi-foundation-81fbfb44-identity.json", + "originRevision": "81fbfb44cd4c652605ea581f8a10c42433cc34c3", + "originPath": "packages/paperclip-runner/test/fixtures/copilot-profile-v88-identity.json", + "sha256": "c8acee88d0768e8bbecda50bcfd0378c7d7f3ce5e2925c1b6f1f66eeb06ae161", + "agent": "copilot", + "version": 88, + "commandDigest": "sha256:0c3328c3fecbe730fa0c4d5239f1084eada804bcb140e16bb41efa0e5a4c4380", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/copilot-profile-v88-identity.json", + "originRevision": "0c70cedce9a803bdc201324528abb35e423e7cd0", + "originPath": "packages/paperclip-runner/test/fixtures/copilot-profile-v88-identity.json", + "sha256": "81e5fbccf057c04e012f638dc7a02c5758bf6ca67f7c4ed0045f3f61525401c2", + "agent": "copilot", + "version": 88, + "commandDigest": "sha256:b9c13b83628be07086a88394ae671997291b77bbdbf45b0bd1c4e91b7121bc9d", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/grok-profile-v15-kimi-foundation-81fbfb44-identity.json", + "originRevision": "81fbfb44cd4c652605ea581f8a10c42433cc34c3", + "originPath": "packages/paperclip-runner/test/fixtures/grok-profile-v15-identity.json", + "sha256": "f99f284f966c4a17b3073c4093b4cf35cff003868eb1d8dfa80e5171c4e7e7a0", + "agent": "grok", + "version": 15, + "commandDigest": "sha256:99a7c5a38e3786fc08cc27e54902b0d36b07be15c61e3ca5645088c301c35067", + "launchAuthority": false + }, + { + "path": "packages/paperclip-runner/test/fixtures/grok-profile-v15-identity.json", + "originRevision": "0c70cedce9a803bdc201324528abb35e423e7cd0", + "originPath": "packages/paperclip-runner/test/fixtures/grok-profile-v15-identity.json", + "sha256": "54f7063c82dc5b135a9b5e2b0d8653b5c839a42bbc3a845dbcfb6cd225fa623f", + "agent": "grok", + "version": 15, + "commandDigest": "sha256:658e6d41ce6fc7080aa6655d7a97121b3cae6214f2d4c074284d958b95bbe769", + "launchAuthority": false + } + ] +} diff --git a/packages/paperclip-runner/test/kimi-installed-runtime.test.mjs b/packages/paperclip-runner/test/kimi-installed-runtime.test.mjs new file mode 100644 index 00000000000..0dedc1a4a3b --- /dev/null +++ b/packages/paperclip-runner/test/kimi-installed-runtime.test.mjs @@ -0,0 +1,109 @@ +import assert from "node:assert/strict"; +import { once } from "node:events"; +import { access, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const packageRoot = fileURLToPath(new URL("../", import.meta.url)); +const target = `${process.platform}-${process.arch}`; +const enabled = process.env.PAPERCLIP_TEST_KIMI_INSTALLED_RUNTIME === "1"; + +test("real installed Kimi acknowledges instructions/model/create/load and exits after EOF without model prompts", { skip: !enabled && "Set PAPERCLIP_TEST_KIMI_INSTALLED_RUNTIME=1 after paperclipai runtime setup kimi; uses no provider credential or model calls", timeout: 45_000 }, async t => { + const assets = join(packageRoot, "provider-assets/kimi", target); + try { await access(join(assets, "native-closure.json")); } + catch (error) { if (error.code === "ENOENT") { t.skip(`Pinned Kimi assets are not installed for ${target}; run paperclipai runtime setup kimi`); return; } throw error; } + const { tsImport } = await import("tsx/esm/api"); + const { verifyKimiInstallation } = await tsImport("../src/drivers/acpx/kimi-installation.ts", import.meta.url); + const { QUALIFIED_ACPX_PROFILES } = await tsImport("../src/drivers/acpx/qualified-profiles.ts", import.meta.url); + const { kimiManagedConfiguration } = await tsImport("../src/drivers/acpx/kimi-config.ts", import.meta.url); + const { kimiInstructionBinding } = await tsImport("../src/drivers/acpx/kimi-instructions.ts", import.meta.url); + const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-kimi-installed-runtime-"))); + const home = join(root, "home"), workspace = join(root, "workspace"); + await mkdir(home); await mkdir(workspace); + const model = "kimi-for-coding"; + await writeFile(join(home, "config.toml"), kimiManagedConfiguration(model), { mode: 0o600 }); + const binding = kimiInstructionBinding("Do not invoke a model during this package protocol check. Keep this exact suffix: café.\n"); + const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"]; + const previous = keys.map(key => process.env[key]); + process.env[keys[0]] = resolve(packageRoot); process.env[keys[1]] = join(resolve(packageRoot), "package.json"); + const connections = new Set(); + let sequence = 0; + try { + const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi); + const connect = async () => { + const lease = await installation.openCommand(); + const child = lease.spawn([], { cwd: workspace, env: { + PATH: "/usr/bin:/bin", LANG: "C.UTF-8", KIMI_CODE_HOME: home, + KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_API_KEY: "paperclip-offline-placeholder", + PAPERCLIP_KIMI_INSTRUCTIONS: binding.payload, CI: "1", NO_COLOR: "1", TERM: "dumb", + }, stdio: "pipe" }); + let buffer = "", total = 0; + const pending = new Map(); + const closed = once(child, "close"); + const fail = error => { for (const item of pending.values()) item.reject(error); pending.clear(); }; + child.on("error", fail); + child.on("close", () => fail(new Error("Kimi exited with an unresolved ACP request"))); + child.stderr.resume(); + child.stdout.on("data", chunk => { + total += chunk.length; + if (total > 1024 * 1024) { fail(new Error("Kimi ACP output exceeded the package smoke bound")); return; } + buffer += chunk.toString("utf8"); + let newline; + while ((newline = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); + if (!line.trim()) continue; + let message; + try { message = JSON.parse(line); } catch { fail(new Error("Kimi emitted malformed ACP JSON")); return; } + const item = pending.get(message.id); + if (item && message.method === undefined) { + pending.delete(message.id); + if (message.error) item.reject(new Error(`Kimi rejected ACP request (${message.error.code})`)); + else item.resolve(message.result); + } + } + }); + const request = (method, params) => new Promise((accept, reject) => { + const id = sequence++; + const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Kimi ${method} timed out`)); }, 10_000); + const settle = callback => value => { clearTimeout(timer); callback(value); }; + pending.set(id, { resolve: settle(accept), reject: settle(reject) }); + child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`); + }); + let closePromise; + const close = () => closePromise ??= (async () => { + const started = Date.now(); + child.stdin.end(); + const term = setTimeout(() => child.kill("SIGTERM"), 4_000); + const kill = setTimeout(() => child.kill("SIGKILL"), 5_000); + try { const [code, signal] = await closed; return { code, signal, elapsedMs: Date.now() - started }; } + finally { clearTimeout(term); clearTimeout(kill); await lease.close(); connections.delete(close); } + })(); + connections.add(close); + const initialize = await request("initialize", { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: "paperclip-installed-package-test", version: "1" } }); + assert.equal(initialize.agentInfo.version, "2.3.3"); + return { request, close }; + }; + const first = await connect(); + const created = await first.request("session/new", { cwd: workspace, mcpServers: [] }); + const assertBinding = (result, sessionId) => { + const ack = result._meta.paperclipKimiInstructions; + assert.equal(ack.sessionId, sessionId); assert.equal(ack.digest, binding.digest); assert.equal(ack.byteLength, binding.byteLength); + assert.match(ack.effectiveDigest, /^sha256:[a-f0-9]{64}$/); + }; + assertBinding(created, created.sessionId); + const selected = await first.request("session/set_config_option", { sessionId: created.sessionId, configId: "model", value: model }); + assert.equal(selected.configOptions.find(option => option.id === "model").currentValue, model); + const firstClose = await first.close(); assert.equal(firstClose.code, 0); assert.equal(firstClose.signal, null); + const second = await connect(); + const loaded = await second.request("session/load", { sessionId: created.sessionId, cwd: workspace, mcpServers: [] }); + assertBinding(loaded, created.sessionId); + assert.equal(loaded.configOptions.find(option => option.id === "model").currentValue, model); + const secondClose = await second.close(); assert.equal(secondClose.code, 0); assert.equal(secondClose.signal, null); + t.diagnostic(`Kimi 2.3.3 ${target}: create/load/model bound; EOF exits ${firstClose.elapsedMs}ms/${secondClose.elapsedMs}ms; zero session/prompt calls`); + } finally { + try { await Promise.all([...connections].map(close => close())); } + finally { keys.forEach((key, index) => { if (previous[index] === undefined) delete process.env[key]; else process.env[key] = previous[index]; }); await rm(root, { recursive: true, force: true }); } + } +}); diff --git a/packages/shared/src/agent-runner.ts b/packages/shared/src/agent-runner.ts index 846e27ad8ae..496c3a3c82a 100644 --- a/packages/shared/src/agent-runner.ts +++ b/packages/shared/src/agent-runner.ts @@ -42,7 +42,7 @@ export function agentHarnessType(adapterType: string, config: Record)[String(agent)] ?? `acpx:${String(agent)}`; + return ({ codex: "codex_local", claude: "claude_local", grok: "grok_local", cursor: "cursor", pi: "pi_local", kimi: "kimi_local", antigravity: "antigravity_runtime" } as Record)[String(agent)] ?? `acpx:${String(agent)}`; } return String(config.provider); } diff --git a/tests/runner-e2e/pi-controls-evidence.test.ts b/tests/runner-e2e/pi-controls-evidence.test.ts index 2dfda64f9aa..2726922acff 100644 --- a/tests/runner-e2e/pi-controls-evidence.test.ts +++ b/tests/runner-e2e/pi-controls-evidence.test.ts @@ -188,8 +188,8 @@ describe("Pi controls catalog admission", () => { // Extended v3 states the strict file oracle's task-wide Bash limit. const pi = runnerMatrix.find(c => c.profile.qualificationCandidate === "pi")!.profile; expect(pi.modelQualification?.qualificationId).toBe("pi:0.0.33:1.0.0:openrouter"); - expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 23, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 35, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "single-json-write-and-content-bound-native-read-both-runs", agentMemoryReadAuthority: "local-withheld-or-exact-remote-agent-run-file", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" }); - expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 10, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", profileVersion: 35, + expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 23, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 36, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "single-json-write-and-content-bound-native-read-both-runs", agentMemoryReadAuthority: "local-withheld-or-exact-remote-agent-run-file", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" }); + expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 10, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", profileVersion: 36, remoteProcessIdentity: "observer-pid-startTicks-bootId", controlPlaneSettlement: "required-scoped-result-and-terminal-after-runner" }); expect(runnerSuites.find(s => s.id === "extended-harnesses")!.definitionMetadata).toMatchObject({ version: 6, piFileArtifactTitle: "exact-filename", piFileCommandTransport: "fenced-bash-markdown-paste" }); @@ -199,7 +199,7 @@ describe("Pi controls catalog admission", () => { } const hashes = Object.fromEntries(runnerSuites.filter(s => ["pi-native", "native-active-stop", "extended-harnesses", "rich-acp-warm-continuity"].includes(s.id)).map(s => [s.id, suiteDefinitionHash(s)])); expect(hashes).toEqual({ - "pi-native": "8107706b60dd20aa34856728be19f3a8bf25bc21812587b3f115a4fe57da3972", + "pi-native": "b8774bd124dd39a694f3b6f26c8c9c5be9278c213b34ffba08d9fe62c21e82b7", "native-active-stop": "4654a2169bff96cf8377fdd62441f5cabea5c613f447e518830f6b1efd82c051", "rich-acp-warm-continuity": "7e8a0012f2f67fba03cd94403b096a07cfc95cdb7aa568b4c3fd18b852d735f3", "extended-harnesses": "d5bcf5de0ade3a54e4e1560e76feb5d8a787039d56d2379cd501626d85ed7418",