From 05f7079ecb2ee28d64715fd18e60b43e2d2fa989 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Tue, 20 Oct 2026 11:20:49 -0700 Subject: [PATCH] Extract Runner provider launch preparation (#16977) ## Thinking Path > - Paperclip manages work for AI agents. > - Runner starts providers in local and remote environments. > - Core selects the provider and task policy. > - The execution environment supplies paths and installed files. > - The transport currently combines these inputs in one large method. > - This PR extracts provider descriptor construction before a separate plugin preparation change. ## Linked Issues or Issue Description **What existing behavior does this improve?** Runner provider launch preparation. **Current behavior** The PRP transport constructs each provider descriptor inside startup. This couples provider configuration to transport lifecycle code. **Proposed behavior** The transport calls one built-in descriptor builder. The builder takes Core provider requirements and execution-environment locations as separate inputs. File staging, provider validation logic, and session rules keep their current behavior. The required Copilot source-identity update changes profile admission metadata. **Reason and benefit** This gives launch preparation a separate unit of code. A later PR can add plugin-owned preparation without adding ownership flags to each provider backend. Refs #15406, #16486. This replaces the earlier installed-home implementation in this PR. #16978 remains a draft. ## What Changed - Extract provider descriptor construction and its existing permission helpers. - Keep helper exports at their previous import path. - Add focused tests for selection, permissions, locations, and managed profile rejection. - Update the Copilot source identity. Bind the extracted file and retain the v49 history. The source attestation requires v50 after this move. This changes the admitted profile identity even though no execution behavior changes are intended. - Document the boundary and its current limits. ## Verification - 338 focused builder, transport, Codex security, and Copilot identity tests passed on the rebased head. - `pnpm -r typecheck` passed. `pnpm build` passed. - The full root `pnpm test:run` on the pre-rebase head is still in progress. It has reported five failures in unchanged server tests. One isolated failure rejects the word `private` from this checkout's `/private/tmp` stack path. The full root suite is not reported as passing. - All 56 CI checks passed on the rebased head. Four optional checks were skipped. The branch is mergeable. Workspace typecheck and build passed on this head. - Standard Greptile and Apex each scored commit `47f631883d8f417f6a818500ebe4e77e22cc70b8` 5/5. The rebase onto master changed only Copilot identity metadata, history handling, and related tests. `git range-diff` confirms that the launch-preparation implementation is unchanged. The existing Apex result remains the review evidence; it did not review the new commit hash. - No live provider calls or Cloud deployment were run. ## Risks - A moved field could change provider startup. Existing transport tests and the new boundary tests check the projection. - Copilot source binding requires a new identity after the move. Existing artifacts and retained sessions must match the new admitted profile. This PR does not claim fresh live qualification. - This PR does not enable plugin-owned preparation. File materialization remains in the existing SDK path. ## Model Used - OpenAI Codex, GPT-6. Reasoning and repository tools. The exact serving model identifier and context size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- doc/runner-launch-preparation.md | 12 + packages/paperclip-runner/acpx-profiles.json | 4 +- .../src/generated_acpx_profiles.rs | 2 +- .../scripts/copilot-profile-identity.mjs | 5 +- .../drivers/acpx/copilot-installation.test.ts | 17 +- .../src/drivers/acpx/generated-profiles.ts | 4 +- .../src/drivers/acpx/profile-compatibility.ts | 4 +- .../src/live/runner-provider-launch.test.ts | 63 ++++ .../src/live/runner-provider-launch.ts | 304 ++++++++++++++++++ .../src/live/runnerd-codex-transport.ts | 285 +--------------- .../copilot-profile-v50-identity.json | 83 +++++ 11 files changed, 503 insertions(+), 280 deletions(-) create mode 100644 doc/runner-launch-preparation.md create mode 100644 packages/paperclip-runner/src/live/runner-provider-launch.test.ts create mode 100644 packages/paperclip-runner/src/live/runner-provider-launch.ts create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v50-identity.json diff --git a/doc/runner-launch-preparation.md b/doc/runner-launch-preparation.md new file mode 100644 index 00000000000..b45f49b03ba --- /dev/null +++ b/doc/runner-launch-preparation.md @@ -0,0 +1,12 @@ +# Runner launch preparation + +Core selects the provider, model, instructions, tools, and task permissions. +The execution environment supplies executable paths and filesystem locations. + +The built-in TypeScript implementation constructs the provider descriptor in +`createRunnerProviderLaunch`. The transport sends this descriptor with +`run.prepare`. Provider backends keep their existing validation and session rules. + +This separation does not change file preparation. The SDK still prepares local +files and stages remote files through the existing execution driver. +It does not enable plugin-owned launch preparation. diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 38442d25355..21955036b37 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -32,12 +32,12 @@ "requiresProviderPolicy": true }, "copilot": { - "agentProfileVersion": 49, + "agentProfileVersion": 50, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:37a8f6a540a7417bb12becbf283158df8cca925c5136ecc0db14597441fff702", + "commandDigest": "sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4", "requiresProviderPolicy": true }, "claude": { 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 5c0a1555349..39f9206b27a 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 @@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option { agent_runtime_package: None, agent_runtime_version: None, command_digest: - "sha256:37a8f6a540a7417bb12becbf283158df8cca925c5136ecc0db14597441fff702", + "sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4", requires_provider_policy: true, }, "claude" => AcpxReleaseProfile { diff --git a/packages/paperclip-runner/scripts/copilot-profile-identity.mjs b/packages/paperclip-runner/scripts/copilot-profile-identity.mjs index 31a5c8da9fd..11618410c90 100644 --- a/packages/paperclip-runner/scripts/copilot-profile-identity.mjs +++ b/packages/paperclip-runner/scripts/copilot-profile-identity.mjs @@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url"; import { resolve, dirname } from "node:path"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const fixture = resolve(root, "test/fixtures/copilot-profile-v49-identity.json"); +const fixture = resolve(root, "test/fixtures/copilot-profile-v50-identity.json"); const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8")); -const declaration = { ...historical.declaration, agentProfileVersion: 49 }; +const declaration = { ...historical.declaration, agentProfileVersion: 50 }; delete declaration.permissionIdentitySourceSha256; const sources = { displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts", @@ -31,6 +31,7 @@ const sources = { nativeBackendSourceSha256: "src/backends/codex-native-backend.ts", nativeBackendFactorySourceSha256: "src/backends/native-backend-factory.ts", nativeInstructionsSourceSha256: "src/backends/runtime-context.ts", + providerLaunchSourceSha256: "src/live/runner-provider-launch.ts", runtimeRequestBridgeSourceSha256: "src/live/runnerd-codex-transport.ts", runtimeRequestFacadeSourceSha256: "src/drivers/codex/codex-session-notifications.ts", runtimeRequestNotificationSourceSha256: "src/drivers/codex/codex-thread-normalization.ts", diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts index a67e2d6b936..f736301a8ad 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -38,6 +38,7 @@ const SEMANTIC_RECEIPT_SOURCES = [ ["../../contracts/completion-result.ts", "semanticCompletionContractSourceSha256"], ["../../contracts/native-execution.ts", "nativeExecutionSourceSha256"], ["installation-integrity.ts", "guardedInstallationSourceSha256"], + ["../../live/runner-provider-launch.ts", "providerLaunchSourceSha256"], ["../../live/runnerd-codex-transport.ts", "runtimeRequestBridgeSourceSha256"], ["../codex/codex-session-notifications.ts", "runtimeRequestFacadeSourceSha256"], ["../codex/codex-thread-normalization.ts", "runtimeRequestNotificationSourceSha256"], @@ -85,10 +86,20 @@ async function withCopilotTestPlatform(arch: "x64" | "arm64", run: () => Prom vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi.fn() })); describe("Copilot build-owned installation", () => { - it("admits profile49 after bounded completion-recovery compatibility revalidation", () => { - expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(49); + it("admits the current profile after launch preparation extraction", () => { + expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(50); expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined(); - expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:37a8f6a540a7417bb12becbf283158df8cca925c5136ecc0db14597441fff702"); + expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4"); + }); + + it("preserves v49 while binding the extracted provider launch builder", () => { + const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v49-identity.json", import.meta.url), "utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v50-identity.json", import.meta.url), "utf8")); + expect(prior.commandDigest).toBe("sha256:37a8f6a540a7417bb12becbf283158df8cca925c5136ecc0db14597441fff702"); + expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([ + "agentProfileVersion", "profileCompatibilitySourceSha256", "providerLaunchSourceSha256", "runtimeRequestBridgeSourceSha256", + ]); + expect(isSupportedAcpxProfileVersion("copilot", 49)).toBe(true); }); it("preserves qualified v48 while binding completion-proposal recovery in v49", async () => { diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index 22bca29a0fc..0f88f9551d4 100644 --- a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts @@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = { "protocolVersion": 1, "acpxVersion": "0.13.1", "agent": "copilot", - "agentProfileVersion": 49, + "agentProfileVersion": 50, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:37a8f6a540a7417bb12becbf283158df8cca925c5136ecc0db14597441fff702", + "commandDigest": "sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4", "permissionPolicy": "interactive" }, "claude": { diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts index 03f9c4dd56f..d0d6837ab48 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts @@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js"; /** Decodable historical profile revisions, not permission to launch them. * Exact current profile/package/digest admission is checked separately. */ -type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48; +type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49; type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"]; export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion; const historicalVersions: Readonly> = { pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5], - grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48], + grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49], cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], }; diff --git a/packages/paperclip-runner/src/live/runner-provider-launch.test.ts b/packages/paperclip-runner/src/live/runner-provider-launch.test.ts new file mode 100644 index 00000000000..d0e27594308 --- /dev/null +++ b/packages/paperclip-runner/src/live/runner-provider-launch.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { createRunnerProviderLaunch, type RunnerProviderLaunchEnvironment, type RunnerProviderLaunchRequirements } from "./runner-provider-launch.js"; + +const requirements: RunnerProviderLaunchRequirements = { + identity: { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "run", + normalizedSessionId: "session", turnId: "turn", itemId: "item" }, + provider: "codex", model: "selected-model", approvalPolicy: "on-request", + instructions: "Follow the assigned instructions.", runtimeContext: null, + readOnly: true, preparedConversation: false, includeCollaborationModeInstructions: false, + settings: {}, +}; +const environment: RunnerProviderLaunchEnvironment = { + options: { codexArgs: ["app-server"] }, root: "/execution/state", cwd: "/execution/workspace", + codexHome: "/execution/home/.codex", providerNodeCommand: "/runtime/node", + codexCommand: "/runtime/codex", opencodeProxyPath: "/runtime/opencode-proxy.cjs", + acpxSidecarPath: "/runtime/acpx-sidecar.cjs", +}; + +describe("built-in provider launch preparation", () => { + it("keeps Core's selection and policy when the execution environment changes", () => { + const before = structuredClone(requirements); + const local = createRunnerProviderLaunch(requirements, environment); + const remote = createRunnerProviderLaunch(requirements, { ...environment, + cwd: "/guest/project", codexCommand: "/guest/bin/codex" }); + expect(local).toMatchObject({ model: "selected-model", approvalPolicy: "on-request", + readOnly: true, collaborationMode: "plan", instructions: requirements.instructions }); + expect(remote).toEqual({ ...local, cwd: "/guest/project", command: "/guest/bin/codex" }); + expect(requirements).toEqual(before); + }); + + it("preserves the ACPX profile, run identity, and permission policy", () => { + const launch = createRunnerProviderLaunch({ ...requirements, provider: "acpx", + model: "claude-selected", settings: { acpxAgent: "claude", acpxPermissionMode: "deny-all" } }, environment); + expect(launch).toMatchObject({ kind: "acpx", agent: "claude", model: "claude-selected", + sidecarCommand: "/runtime/node", sidecarArgs: ["/runtime/acpx-sidecar.cjs"], + runId: "run", normalizedSessionId: "session", permissionMode: "deny-all", + permissionModePinned: true, providerPolicy: { readOnly: true }, + instructions: requirements.instructions }); + expect(launch.commandDigest).toMatch(/^sha256:[a-f0-9]{64}$/); + }); + + it("keeps the OpenCode facade shape and prepared conversation mode", () => { + expect(createRunnerProviderLaunch({ ...requirements, provider: "opencode", preparedConversation: true }, environment)) + .toMatchObject({ kind: "opencode", driver: "opencode_server", command: "/runtime/node", + args: ["/runtime/opencode-proxy.cjs"], conversationMode: "prepared", collaborationMode: "plan" }); + expect(createRunnerProviderLaunch({ ...requirements, provider: "opencode" }, environment)) + .not.toHaveProperty("readOnly"); + }); + + it("uses the existing permission builder when the environment supplies no argument override", () => { + const launch = createRunnerProviderLaunch(requirements, { ...environment, + options: { environment: { HOME: "/execution/home" } } }); + expect(launch.args).toEqual(expect.arrayContaining(["app-server"])); + expect(JSON.stringify(launch.args)).toContain("paperclip-runner-workspace-read-only"); + }); + + it("rejects missing managed profiles before provider launch", () => { + expect(() => createRunnerProviderLaunch({ ...requirements, provider: "claude_managed" }, environment)) + .toThrow("requires a qualified managed profile"); + expect(() => createRunnerProviderLaunch({ ...requirements, provider: "aws_agentcore" }, environment)) + .toThrow("requires a qualified AgentCore profile"); + }); +}); diff --git a/packages/paperclip-runner/src/live/runner-provider-launch.ts b/packages/paperclip-runner/src/live/runner-provider-launch.ts new file mode 100644 index 00000000000..fa59eee43d0 --- /dev/null +++ b/packages/paperclip-runner/src/live/runner-provider-launch.ts @@ -0,0 +1,304 @@ +import { resolve } from "node:path"; +import { codexExecutableReadOnlyRoots } from "../drivers/codex/codex-security-config.js"; +import { createIsolatedCodexAppServerArgs } from "../drivers/codex/codex-app-server-driver.js"; +import { usesPreconfiguredHome } from "../drivers/preconfigured-home.js"; +import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js"; +import { resolveAcpxProviderMode } from "../drivers/acpx/provider-mode.js"; +import type { DurableRecoveryIdentity } from "../contracts/durable-recovery.js"; +import type { NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js"; +import type { CapabilityRunnerdCodexTransportOptions } from "./runnerd-codex-transport.js"; + +/** Core's provider inputs. Executable paths and filesystem layout are supplied separately. */ +export interface RunnerProviderLaunchRequirements { + identity: DurableRecoveryIdentity; + provider: NonNullable; + model: string | null; + approvalPolicy: "never" | "on-request" | "untrusted"; + readOnly: boolean; + preparedConversation: boolean; + includeCollaborationModeInstructions: boolean; + settings: Pick; + instructions: string; + runtimeContext: NativeRuntimeContextSnapshot | null; +} + +/** Locations and provider settings resolved by the built-in execution environment. */ +export interface RunnerProviderLaunchEnvironment { + options: Pick; + root: string; + cwd: string; + codexHome: string; + providerNodeCommand: string; + codexCommand?: string; + opencodeProxyPath: string; + acpxSidecarPath: string; +} + +export function createRunnerProviderLaunch( + requirements: RunnerProviderLaunchRequirements, + input: RunnerProviderLaunchEnvironment, +): Record { + const { identity, instructions: baseInstructions, runtimeContext, settings, provider } = requirements; + const acpxAgent = provider === "acpx" ? settings.acpxAgent ?? "codex" : null; + const requestedModel = requirements.model ?? ""; + const includeCodexCollaborationInstructions = provider === "codex" && + requirements.includeCollaborationModeInstructions; + const { codexHome, providerNodeCommand, codexCommand: effectiveCodexCommand, + opencodeProxyPath, acpxSidecarPath } = input; + const acpxProfile = + provider === "acpx" + ? resolveQualifiedAcpxProfile(acpxAgent!, requestedModel) + : null; + const managedProfile = settings.managedProfile; + const agentCoreProfile = settings.agentCoreProfile; + if (provider === "claude_managed") { + if (!managedProfile) { + throw new Error( + "Claude Managed runner transport requires a qualified managed profile", + ); + } + if (requestedModel !== managedProfile.model) { + throw new Error( + "Claude Managed requested model does not match its qualified profile", + ); + } + } + if (provider === "aws_agentcore") { + if (!agentCoreProfile) { + throw new Error( + "AWS AgentCore runner transport requires a qualified AgentCore profile", + ); + } + if (requestedModel !== agentCoreProfile.model) { + throw new Error( + "AWS AgentCore requested model does not match its qualified profile", + ); + } + } + const selectedAcpxMode = provider === "acpx" + ? resolveAcpxProviderMode(acpxProfile!.agent, settings.acpxMode) : undefined; + return provider === "acpx" + ? { + kind: "acpx", + provider: "acpx", + driver: "acpx_runtime", + providerVersion: acpxProfile!.acpxVersion, + agent: acpxProfile!.agent, + model: requestedModel, + acpxVersion: acpxProfile!.acpxVersion, + agentServerPackage: acpxProfile!.agentServerPackage, + agentServerVersion: acpxProfile!.agentServerVersion, + agentRuntimePackage: acpxProfile!.agentRuntimePackage, + agentRuntimeVersion: acpxProfile!.agentRuntimeVersion, + commandDigest: acpxProfile!.commandDigest, + sidecarCommand: providerNodeCommand, + sidecarArgs: [acpxSidecarPath], + runtimeDirectory: + input.options.acpxRuntimeDirectory ?? + resolve(input.root, "acpx"), + normalizedSessionId: identity.normalizedSessionId, + runId: identity.runId, + cwd: input.cwd, + instructions: baseInstructions, + providerPolicy: { readOnly: requirements.readOnly }, + ...(selectedAcpxMode === undefined ? {} : { mode: selectedAcpxMode }), + ...(acpxProfile!.agent === "pi" ? { piThinkingLevel: settings.piThinkingLevel } : {}), + ...(acpxProfile!.agent === "claude" && settings.claudeEffort ? { claudeEffort: settings.claudeEffort } : {}), + permissionMode: resolveRunnerdAcpxPermissionMode( + settings.acpxPermissionMode, + ), + permissionModePinned: + settings.acpxPermissionModePinned ?? true, + runtimeContext, + } + : provider === "claude_managed" + ? { + kind: "claude_managed", + model: managedProfile!.model, + profileId: managedProfile!.profileId, + anthropicAgentId: managedProfile!.anthropicAgentId, + agentVersion: managedProfile!.agentVersion, + environmentId: managedProfile!.environmentId, + betaVersion: managedProfile!.betaVersion, + maxSessionListCostUsd: managedProfile!.maxSessionListCostUsd, + instructions: baseInstructions, + runtimeContext, + } + : provider === "aws_agentcore" + ? { + kind: "aws_agentcore", + model: agentCoreProfile!.model, + profileId: agentCoreProfile!.profileId, + region: agentCoreProfile!.region, + accountId: agentCoreProfile!.accountId, + harnessArn: agentCoreProfile!.harnessArn, + harnessVersion: agentCoreProfile!.harnessVersion, + endpointArn: agentCoreProfile!.endpointArn, + endpointQualifier: agentCoreProfile!.endpointQualifier, + agentRuntimeArn: agentCoreProfile!.agentRuntimeArn, + memoryArn: agentCoreProfile!.memoryArn, + memoryId: agentCoreProfile!.memoryId, + invocationRoleArn: agentCoreProfile!.invocationRoleArn, + contextBucket: agentCoreProfile!.contextBucket, + contextPrefix: agentCoreProfile!.contextPrefix, + contextKmsKeyArn: agentCoreProfile!.contextKmsKeyArn, + qualificationRevision: + agentCoreProfile!.qualificationRevision, + eventExpiryDays: agentCoreProfile!.eventExpiryDays, + maxEstimatedSessionCostUsd: + agentCoreProfile!.maxEstimatedSessionCostUsd, + maxIterations: agentCoreProfile!.maxIterations, + maxOutputTokens: agentCoreProfile!.maxOutputTokens, + timeoutSeconds: agentCoreProfile!.timeoutSeconds, + instructions: baseInstructions, + runtimeContext, + } + : { + kind: provider, + provider, + ...(input.options.homeMode === "preconfigured" ? { preconfiguredHome: { HOME: input.options.environment!.HOME, CODEX_HOME: codexHome } } : {}), + driver: + provider === "opencode" + ? "opencode_server" + : "codex_app_server", + providerVersion: + provider === "opencode" ? "1.18.34" : "codex-app-server-v1", + command: + provider === "opencode" + ? providerNodeCommand + : effectiveCodexCommand!, + args: + provider === "opencode" + ? [opencodeProxyPath] + : (input.options.codexArgs ?? + createRunnerdCodexAppServerArgs({ + environment: input.options.environment, + codexHome, + workingDirectory: input.cwd, + runtimeStateRoot: input.options.runnerFilesystemRoot ?? input.root, + codexCommand: effectiveCodexCommand, + instructionWorkingCopyRoot: runtimeContext?.instructions.workingCopy?.rootPath, + disableProjectInstructions: input.options.disableCodexProjectInstructions, + readOnlyRoots: [ + ...trustedRuntimeReadOnlyRoots( + input.options.environment, + ), + ...(runtimeContext + ? [ + resolve(codexHome, "skills"), + runtimeContext.instructions.bundle.rootPath, + ...runtimeContext.skills.map( + (skill) => skill.bundle.rootPath, + ), + ] + : []), + ], + })), + cwd: input.cwd, + model: requirements.model, + approvalPolicy: + requirements.approvalPolicy, + externallySandboxed: + provider === "codex" && + input.options.externallySandboxed === true, + ...(provider === "codex" && requirements.readOnly ? { readOnly: true } : {}), + instructions: + provider === "codex" + ? withCodexCollaborationRuntimeInstructions( + baseInstructions, + includeCodexCollaborationInstructions, + ) + : baseInstructions, + collaborationMode: + requirements.readOnly + ? "plan" + : "default", + ...(requirements.preparedConversation + ? { conversationMode: "prepared" } + : {}), + includeCollaborationModeInstructions: + includeCodexCollaborationInstructions, + ...(provider === "codex" + ? { includeSkillInstructions: runtimeContext !== null } + : {}), + runtimeContext, + }; +} + +const CODEX_COLLABORATION_RUNTIME_INSTRUCTIONS = `## Codex-style collaboration + +- Before the first tool call in a turn, send a brief commentary update describing the immediate work you are starting. +- During tool-driven work, send concise commentary updates at meaningful transitions so the user can follow progress without opening raw logs. +- Reserve \`report_progress\` for meaningful durable milestones on longer work. Do not call it merely to create a completion comment on a short run; Paperclip materializes the final assistant response as the durable completion comment. +- Invoke the semantic completion tool exactly once before the final assistant response. After it succeeds, send one self-contained final response with the outcome and verification, then do not call another tool. The completion tool records task disposition; Paperclip keeps receiving your answer until the provider turn ends.`; + +export function withCodexCollaborationRuntimeInstructions( + instructions: string, + enabled = true, +): string { + if (!enabled) return instructions; + const base = instructions.trimEnd(); + return `${base}\n\n${CODEX_COLLABORATION_RUNTIME_INSTRUCTIONS}`; +} + +export function resolveRunnerdAcpxPermissionMode( + configured: CapabilityRunnerdCodexTransportOptions["acpxPermissionMode"], +): NonNullable { + return configured ?? "approve-all"; +} + +export function trustedRuntimeReadOnlyRoots( + environment: NodeJS.ProcessEnv | undefined, +): string[] { + const path = environment?.PATH ?? ""; + const roots = new Set(); + for (const entry of path.split(process.platform === "win32" ? ";" : ":")) { + if (entry === "/opt/homebrew" || entry.startsWith("/opt/homebrew/")) { + roots.add("/opt/homebrew"); + } else if (entry === "/usr/local" || entry.startsWith("/usr/local/")) { + roots.add("/usr/local"); + } else if (entry === "/opt/local" || entry.startsWith("/opt/local/")) { + roots.add("/opt/local"); + } else if (entry === "/nix/store" || entry.startsWith("/nix/store/")) { + roots.add("/nix/store"); + } + } + return [...roots]; +} + +export function createRunnerdCodexAppServerArgs(input: { + environment: NodeJS.ProcessEnv | undefined; + codexHome: string; + codexCommand?: string; + workingDirectory?: string; + runtimeStateRoot?: string; + readOnlyRoots?: string[]; + instructionWorkingCopyRoot?: string; + disableProjectInstructions?: boolean; +}): string[] { + // The filesystem policy denies HOME and CODEX_HOME to keep credentials and + // runner state outside provider reach. Always bind those names to the actual + // isolated runner home; a stale controller environment must never cause the + // execution workspace itself to become an explicit deny root. + const args = [ + ...(input.disableProjectInstructions ? ["-c", "project_doc_max_bytes=0"] : []), + ...createIsolatedCodexAppServerArgs( + { + ...input.environment, + HOME: usesPreconfiguredHome(input.environment) ? input.environment?.HOME : input.codexHome, + CODEX_HOME: input.codexHome, + }, + [...(input.readOnlyRoots ?? []), ...codexExecutableReadOnlyRoots(input.environment ?? {}, input.codexCommand)], + input.instructionWorkingCopyRoot, + usesPreconfiguredHome(input.environment) && input.runtimeStateRoot ? [input.runtimeStateRoot] : [], + ), + ]; + if (usesPreconfiguredHome(input.environment) && input.workingDirectory) { + args.unshift("-c", `projects={${JSON.stringify(input.workingDirectory)}={trust_level="trusted"}}`); + } + return args; +} diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 0fa9d5aeebf..391c6b0f397 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -1,3 +1,5 @@ +import { createRunnerProviderLaunch, createRunnerdCodexAppServerArgs, trustedRuntimeReadOnlyRoots } from "./runner-provider-launch.js"; +export { createRunnerdCodexAppServerArgs, resolveRunnerdAcpxPermissionMode, trustedRuntimeReadOnlyRoots, withCodexCollaborationRuntimeInstructions } from "./runner-provider-launch.js"; import { applyProviderObservabilityEnvironment, redactObservabilityDiagnostic } from "../drivers/agent-observability.js"; import type { RunnerExecutionDriver } from "../control-plane/runner-execution-driver.js"; import { resolvePinnedOpenCodeCommand } from "../drivers/opencode/opencode-server-driver.js"; @@ -11,7 +13,6 @@ import { preconfiguredHomeEnvironment, preconfiguredCodexHome, usesPreconfigured import { isAcpxCanonicalInputMethod } from "../drivers/acpx/profile-extensions.js"; import { RunnerdTraceFrameIndex } from "./runnerd-trace-frame-index.js"; import { waitForWarmAttachmentReadiness } from "./warm-attachment-readiness.js"; -import { codexExecutableReadOnlyRoots } from "../drivers/codex/codex-security-config.js"; import { resolveCodexCommand } from "../drivers/codex/codex-command.js"; import { isCanonicalProviderEventType } from "../provider-events.js"; import { execFileSync } from "node:child_process"; @@ -46,7 +47,6 @@ import { createSanitizedCodexEnvironment } from "../drivers/codex/app-server-tra import { CodexAppServerDriver, codexSemanticToolSpecs, - createIsolatedCodexAppServerArgs, } from "../drivers/codex/codex-app-server-driver.js"; import type { DurableRecoveryCommittedEvent, @@ -72,7 +72,6 @@ import { type DurablePrpControlPlaneOptions, } from "../control-plane/durable-prp-control-plane.js"; import { - resolveQualifiedAcpxProfile, type QualifiedAcpxAgent, } from "../drivers/acpx/qualified-profiles.js"; import { ACPX_CREDENTIAL_BINDING_ENV, createAcpxCredentialBinding, createSanitizedAcpxSpawnInput } from "../drivers/acpx/environment.js"; @@ -149,22 +148,6 @@ function readLocalProcessStartedAt(pid: number): string | null { return null; } -const CODEX_COLLABORATION_RUNTIME_INSTRUCTIONS = `## Codex-style collaboration - -- Before the first tool call in a turn, send a brief commentary update describing the immediate work you are starting. -- During tool-driven work, send concise commentary updates at meaningful transitions so the user can follow progress without opening raw logs. -- Reserve \`report_progress\` for meaningful durable milestones on longer work. Do not call it merely to create a completion comment on a short run; Paperclip materializes the final assistant response as the durable completion comment. -- Invoke the semantic completion tool exactly once before the final assistant response. After it succeeds, send one self-contained final response with the outcome and verification, then do not call another tool. The completion tool records task disposition; Paperclip keeps receiving your answer until the provider turn ends.`; - -export function withCodexCollaborationRuntimeInstructions( - instructions: string, - enabled = true, -): string { - if (!enabled) return instructions; - const base = instructions.trimEnd(); - return `${base}\n\n${CODEX_COLLABORATION_RUNTIME_INSTRUCTIONS}`; -} - const CONTROL_PLANE_STATE_MAX_BYTES = 256 * 1024 * 1024; function readControlPlaneState(directory: string): Record { @@ -3408,12 +3391,6 @@ export function createCapabilityRunnerdProviderEnvironment(input: Parameters { - return configured ?? "approve-all"; -} - const OPEN_CODE_RUNNER_ENVIRONMENT_KEYS = new Set([ "PAPERCLIP_AI_PROVIDER_KEY", "PAPERCLIP_AI_PROVIDER_URL", @@ -3475,58 +3452,6 @@ export function resolveSourceCodexHome( return home ? resolve(home, ".codex") : null; } -export function trustedRuntimeReadOnlyRoots( - environment: NodeJS.ProcessEnv | undefined, -): string[] { - const path = environment?.PATH ?? ""; - const roots = new Set(); - for (const entry of path.split(process.platform === "win32" ? ";" : ":")) { - if (entry === "/opt/homebrew" || entry.startsWith("/opt/homebrew/")) { - roots.add("/opt/homebrew"); - } else if (entry === "/usr/local" || entry.startsWith("/usr/local/")) { - roots.add("/usr/local"); - } else if (entry === "/opt/local" || entry.startsWith("/opt/local/")) { - roots.add("/opt/local"); - } else if (entry === "/nix/store" || entry.startsWith("/nix/store/")) { - roots.add("/nix/store"); - } - } - return [...roots]; -} - -export function createRunnerdCodexAppServerArgs(input: { - environment: NodeJS.ProcessEnv | undefined; - codexHome: string; - codexCommand?: string; - workingDirectory?: string; - runtimeStateRoot?: string; - readOnlyRoots?: string[]; - instructionWorkingCopyRoot?: string; - disableProjectInstructions?: boolean; -}): string[] { - // The filesystem policy denies HOME and CODEX_HOME to keep credentials and - // runner state outside provider reach. Always bind those names to the actual - // isolated runner home; a stale controller environment must never cause the - // execution workspace itself to become an explicit deny root. - const args = [ - ...(input.disableProjectInstructions ? ["-c", "project_doc_max_bytes=0"] : []), - ...createIsolatedCodexAppServerArgs( - { - ...input.environment, - HOME: usesPreconfiguredHome(input.environment) ? input.environment?.HOME : input.codexHome, - CODEX_HOME: input.codexHome, - }, - [...(input.readOnlyRoots ?? []), ...codexExecutableReadOnlyRoots(input.environment ?? {}, input.codexCommand)], - input.instructionWorkingCopyRoot, - usesPreconfiguredHome(input.environment) && input.runtimeStateRoot ? [input.runtimeStateRoot] : [], - ), - ]; - if (usesPreconfiguredHome(input.environment) && input.workingDirectory) { - args.unshift("-c", `projects={${JSON.stringify(input.workingDirectory)}={trust_level="trusted"}}`); - } - return args; -} - export function unwrapToolResponse(response: Record, preserveEnvelope = false): { readonly __paperclipSemanticToolOutcome: true; readonly result: unknown; @@ -4967,10 +4892,6 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { ); const acpxAgent = provider === "acpx" ? (this.options.acpxAgent ?? "codex") : null; - const requestedModel = typeof params.model === "string" ? params.model : ""; - const includeCodexCollaborationInstructions = - provider === "codex" && - record(params.config).include_collaboration_mode_instructions !== false; const unboundBaseInstructions = String( params.developerInstructions ?? params.baseInstructions ?? "You are a Paperclip agent.", ); @@ -4981,197 +4902,25 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { runtimeContext.instructions.bundle.rootPath, ) : unboundBaseInstructions; - const acpxProfile = - provider === "acpx" - ? resolveQualifiedAcpxProfile(acpxAgent!, requestedModel) - : null; - const managedProfile = this.options.managedProfile; - const agentCoreProfile = this.options.agentCoreProfile; - if (provider === "claude_managed") { - if (!managedProfile) { - throw new Error( - "Claude Managed runner transport requires a qualified managed profile", - ); - } - if (requestedModel !== managedProfile.model) { - throw new Error( - "Claude Managed requested model does not match its qualified profile", - ); - } - } - if (provider === "aws_agentcore") { - if (!agentCoreProfile) { - throw new Error( - "AWS AgentCore runner transport requires a qualified AgentCore profile", - ); - } - if (requestedModel !== agentCoreProfile.model) { - throw new Error( - "AWS AgentCore requested model does not match its qualified profile", - ); - } - } - const selectedAcpxMode = provider === "acpx" - ? resolveAcpxProviderMode(acpxProfile!.agent, this.options.acpxMode) : undefined; const completionContract = record(params.completionContract); const runAttachTemplate = { observeCredentials: this.options.observeCredentials !== false, authorizedTools: this.#authorizedTools, - ...(completionContract.revision && - Array.isArray(completionContract.criterionIds) - ? { completionContract } - : {}), - provider: - provider === "acpx" - ? { - kind: "acpx", - provider: "acpx", - driver: "acpx_runtime", - providerVersion: acpxProfile!.acpxVersion, - agent: acpxProfile!.agent, - model: requestedModel, - acpxVersion: acpxProfile!.acpxVersion, - agentServerPackage: acpxProfile!.agentServerPackage, - agentServerVersion: acpxProfile!.agentServerVersion, - agentRuntimePackage: acpxProfile!.agentRuntimePackage, - agentRuntimeVersion: acpxProfile!.agentRuntimeVersion, - commandDigest: acpxProfile!.commandDigest, - sidecarCommand: providerNodeCommand, - sidecarArgs: [acpxSidecarPath], - runtimeDirectory: - this.options.acpxRuntimeDirectory ?? - resolve(this.#root, "acpx"), - normalizedSessionId: identity.normalizedSessionId, - runId: identity.runId, - cwd: String(params.cwd ?? tmpdir()), - instructions: baseInstructions, - providerPolicy: { readOnly: params.permissions === "paperclip-runner-workspace-read-only" }, - ...(selectedAcpxMode === undefined ? {} : { mode: selectedAcpxMode }), - ...(acpxProfile!.agent === "pi" ? { piThinkingLevel: this.options.piThinkingLevel } : {}), - ...(acpxProfile!.agent === "claude" && this.options.claudeEffort ? { claudeEffort: this.options.claudeEffort } : {}), - permissionMode: resolveRunnerdAcpxPermissionMode( - this.options.acpxPermissionMode, - ), - permissionModePinned: - this.options.acpxPermissionModePinned ?? true, - runtimeContext, - } - : provider === "claude_managed" - ? { - kind: "claude_managed", - model: managedProfile!.model, - profileId: managedProfile!.profileId, - anthropicAgentId: managedProfile!.anthropicAgentId, - agentVersion: managedProfile!.agentVersion, - environmentId: managedProfile!.environmentId, - betaVersion: managedProfile!.betaVersion, - maxSessionListCostUsd: managedProfile!.maxSessionListCostUsd, - instructions: baseInstructions, - runtimeContext, - } - : provider === "aws_agentcore" - ? { - kind: "aws_agentcore", - model: agentCoreProfile!.model, - profileId: agentCoreProfile!.profileId, - region: agentCoreProfile!.region, - accountId: agentCoreProfile!.accountId, - harnessArn: agentCoreProfile!.harnessArn, - harnessVersion: agentCoreProfile!.harnessVersion, - endpointArn: agentCoreProfile!.endpointArn, - endpointQualifier: agentCoreProfile!.endpointQualifier, - agentRuntimeArn: agentCoreProfile!.agentRuntimeArn, - memoryArn: agentCoreProfile!.memoryArn, - memoryId: agentCoreProfile!.memoryId, - invocationRoleArn: agentCoreProfile!.invocationRoleArn, - contextBucket: agentCoreProfile!.contextBucket, - contextPrefix: agentCoreProfile!.contextPrefix, - contextKmsKeyArn: agentCoreProfile!.contextKmsKeyArn, - qualificationRevision: - agentCoreProfile!.qualificationRevision, - eventExpiryDays: agentCoreProfile!.eventExpiryDays, - maxEstimatedSessionCostUsd: - agentCoreProfile!.maxEstimatedSessionCostUsd, - maxIterations: agentCoreProfile!.maxIterations, - maxOutputTokens: agentCoreProfile!.maxOutputTokens, - timeoutSeconds: agentCoreProfile!.timeoutSeconds, - instructions: baseInstructions, - runtimeContext, - } - : { - kind: provider, - provider, - ...(this.options.homeMode === "preconfigured" ? { preconfiguredHome: { HOME: this.options.environment!.HOME, CODEX_HOME: codexHome } } : {}), - driver: - provider === "opencode" - ? "opencode_server" - : "codex_app_server", - providerVersion: - provider === "opencode" ? "1.18.34" : "codex-app-server-v1", - command: - provider === "opencode" - ? providerNodeCommand - : effectiveCodexCommand!, - args: - provider === "opencode" - ? [opencodeProxyPath] - : (this.options.codexArgs ?? - createRunnerdCodexAppServerArgs({ - environment: this.options.environment, - codexHome, - workingDirectory: String(params.cwd ?? tmpdir()), - runtimeStateRoot: this.options.runnerFilesystemRoot ?? this.#root, - codexCommand: effectiveCodexCommand, - instructionWorkingCopyRoot: runtimeContext?.instructions.workingCopy?.rootPath, - disableProjectInstructions: this.options.disableCodexProjectInstructions, - readOnlyRoots: [ - ...trustedRuntimeReadOnlyRoots( - this.options.environment, - ), - ...(runtimeContext - ? [ - resolve(codexHome, "skills"), - runtimeContext.instructions.bundle.rootPath, - ...runtimeContext.skills.map( - (skill) => skill.bundle.rootPath, - ), - ] - : []), - ], - })), - cwd: String(params.cwd ?? tmpdir()), - model: typeof params.model === "string" ? params.model : null, - approvalPolicy: - params.approvalPolicy === "on-request" || - params.approvalPolicy === "untrusted" - ? params.approvalPolicy - : "never", - externallySandboxed: - provider === "codex" && - this.options.externallySandboxed === true, - ...(provider === "codex" && params.permissions === "paperclip-runner-workspace-read-only" ? { readOnly: true } : {}), - instructions: - provider === "codex" - ? withCodexCollaborationRuntimeInstructions( - baseInstructions, - includeCodexCollaborationInstructions, - ) - : baseInstructions, - collaborationMode: - params.permissions === - "paperclip-runner-workspace-read-only" - ? "plan" - : "default", - ...(params.conversationMode === "prepared" - ? { conversationMode: "prepared" } - : {}), - includeCollaborationModeInstructions: - includeCodexCollaborationInstructions, - ...(provider === "codex" - ? { includeSkillInstructions: runtimeContext !== null } - : {}), - runtimeContext, - }, + ...(completionContract.revision && Array.isArray(completionContract.criterionIds) + ? { completionContract } : {}), + provider: createRunnerProviderLaunch({ + identity, provider, model: typeof params.model === "string" ? params.model : null, + instructions: baseInstructions, runtimeContext, settings: this.options, + approvalPolicy: params.approvalPolicy === "on-request" || params.approvalPolicy === "untrusted" + ? params.approvalPolicy : "never", + readOnly: params.permissions === "paperclip-runner-workspace-read-only", + preparedConversation: params.conversationMode === "prepared", + includeCollaborationModeInstructions: record(params.config).include_collaboration_mode_instructions !== false, + }, { + options: this.options, root: this.#root, cwd: String(params.cwd ?? tmpdir()), + codexHome, providerNodeCommand, codexCommand: effectiveCodexCommand, + opencodeProxyPath, acpxSidecarPath, + }), }; // Preserve the first generation's provider attachment seed independently // of bounded command history. The in-memory copy serves a live warm diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v50-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v50-identity.json new file mode 100644 index 00000000000..808fadba4e4 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v50-identity.json @@ -0,0 +1,83 @@ +{ + "commandDigest": "sha256:d8f89c8e75883a793122ed908016b7730ededa6c3df741f23298c2445d7e6fc4", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 50, + "acpxVersion": "0.13.1", + "agentServerVersion": "1.0.88", + "protocolPolicyRevision": "copilot-agent-manual-v1", + "acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1", + "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": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b", + "permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "cc90b59bdfe6a82f7fa3e819e8671f71f7b14047e68942e8086618ffbaacf59f", + "semanticDirectDriverSourceSha256": "ff572638f2cb5d9f8249ff8e2f412311c1e3af209ef0bf7e5ea9f10a21d91103", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f8b381e1ac2a880558f3e81968f0f8f05de3ded573d044fafc89f198f2ec0433", + "semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9", + "semanticSchemaBundleSourceSha256": "016391a087cc217aafed4ba1f4efafc6e4881a7525952dce824b7f4d24530603", + "semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff", + "configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382", + "attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "e8a3bee66a1ff1c6f251b5ae57453ffb908336da9c4a6d98f41f94f7ee94a8e8", + "nativeRuntimeContextSourceSha256": "28c28c850bceade8c72b3ea246485d2bc77bd6e1dab95b43e3b319e27ca944bf", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "03f4cd7f76f34f6d82e009b66aa51f18f6e23dc235b660778060e35b6ea07412", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94", + "transportContractSourceSha256": "4fa9593226ff3280bc12140bfcbc2c0986e4cc8d4733381c334138dd03b08f46", + "nativeBackendSourceSha256": "2406483fd35a564971c22e10fc2bad26bae16ff5a3fb6c65690fcf156522ab6d", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "ee8df5d85b263d5d2d0dc9ede8602cb7daf6388687108fe05e2371f5f56c3492", + "providerLaunchSourceSha256": "2bd265438b88897ab57c73a180009d9f78c382c1576b8d9c16e4bae3dd01c45a", + "runtimeRequestBridgeSourceSha256": "ba198ea55c177906d1de215d119466805452b87abefa1db47a1ffabc205d7a73", + "runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "f6d0c33de38259efefa0f4e0f9737cd30cfd3996a8e875e3fbdb8c72dc133dca", + "nativeExecutionSourceSha256": "fc7c6568feb0d8d709e9baa51839420c9b90336b4865a9b8405aba06e4d1e9c4", + "profileCompatibilitySourceSha256": "afbc27f9dcd824d14fc71552f0244614aaa7260a5b9b2cb9d148974c68a42ce6", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "54fd09ad430dac04df98d829061f6701e73b5bf2a0f4de836d2ac36ee511009f", + "sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c", + "runtimeHostSourceSha256": "60e5454f030e87f2afa8b1fb120b26adb126b6b17d0e5e82f49c77f76f466cbb", + "runtimeAdapterSourceSha256": "e4c764c2c3c7dd655a965b39472bd1ad8d577d1e7258b25594b171c9429752b8", + "runtimeSandboxSourceSha256": "92dfa53f5a3ed0062913ae417eeccec243b430d444495efde2a8e9c4c02e78ea", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +}