From 1bd7e87482f52c041f7e0bb144e79eabd5784445 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 9 Oct 2026 11:40:15 -0500 Subject: [PATCH 1/2] Repair Copilot packaging and isolate current instructions in profile 35 Keep Pi source identity intact, expose the metadata probe, synchronize current admission contracts, and restore exact instruction ownership assertions. Co-Authored-By: Paperclip --- packages/paperclip-runner/acpx-profiles.json | 4 +- packages/paperclip-runner/package.json | 3 +- .../runner-core/src/acpx_provider_backend.rs | 3 +- .../src/generated_acpx_profiles.rs | 2 +- .../tests/native_provider_backend.rs | 2 +- .../scripts/build-provider-pack.mjs | 1 + .../scripts/copilot-profile-identity.mjs | 4 +- .../scripts/generate-acpx-profiles.mjs | 2 +- .../drivers/acpx/copilot-installation.test.ts | 66 +++++---------- .../src/drivers/acpx/copilot-profile.test.ts | 12 ++- .../src/drivers/acpx/copilot-profile.ts | 9 +++ .../drivers/acpx/copilot-runtime-host.test.ts | 10 ++- .../src/drivers/acpx/generated-profiles.ts | 4 +- .../src/drivers/acpx/runtime-host.ts | 6 +- .../src/drivers/acpx/runtime-sandbox.test.ts | 14 ++-- .../src/drivers/acpx/runtime-sandbox.ts | 10 +-- .../copilot-profile-v35-identity.json | 81 +++++++++++++++++++ .../native-runtime/runtime-mode.test.ts | 11 ++- 18 files changed, 166 insertions(+), 78 deletions(-) create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v35-identity.json diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 3a79a740325..c7dd5a7f32c 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -32,12 +32,12 @@ "requiresProviderPolicy": true }, "copilot": { - "agentProfileVersion": 34, + "agentProfileVersion": 35, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed", + "commandDigest": "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327", "requiresProviderPolicy": true }, "claude": { diff --git a/packages/paperclip-runner/package.json b/packages/paperclip-runner/package.json index f1ec09e14ec..e040f8df926 100644 --- a/packages/paperclip-runner/package.json +++ b/packages/paperclip-runner/package.json @@ -11,7 +11,8 @@ "paperclip-runner-eval-session": "./dist/cli/eval-session.js", "paperclip-runner-codex-proxy": "./dist/cli/codex-app-server-unix-proxy.js", "paperclip-runner-opencode-proxy": "./dist/cli/opencode-app-server-proxy.js", - "paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js" + "paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js", + "paperclip-runner-copilot-metadata-probe": "./dist/cli/copilot-metadata-probe.js" }, "exports": { ".": { 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 8f01f705a43..79324882e5f 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 @@ -4083,7 +4083,7 @@ mod tests { descriptor_value["agentRuntimePackage"] = Value::Null; descriptor_value["agentRuntimeVersion"] = Value::Null; descriptor_value["commandDigest"] = serde_json::from_str::(include_str!( - "../../../../test/fixtures/copilot-profile-v34-identity.json" + "../../../../test/fixtures/copilot-profile-v35-identity.json" )) .unwrap()["commandDigest"] .clone(); @@ -4127,6 +4127,7 @@ mod tests { effective_model: original_descriptor.model.clone(), permission_mode: Some(original_descriptor.permission_mode), mode: original_descriptor.mode.clone(), + pi_thinking_level: None, provider_lifetime_fence_candidates: [60_001, 60_002, 60_003], }; let operations = Vec::new(); 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 dd2ed8962b7..a7988b6f44a 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:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed", + "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327", requires_provider_policy: true, }, "claude" => AcpxReleaseProfile { diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs index 2fb94b988b7..0763718e91c 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs @@ -1030,7 +1030,7 @@ fn provider_death_expires_input_and_commits_failure_without_querying_the_dead_pr let mut config = acpx_config(&directory, mode); let mut payload = prepare_payload_with_mode(&directory, "codex", mode); let identity: Value = serde_json::from_str(include_str!( - "../../../../test/fixtures/copilot-profile-v34-identity.json" + "../../../../test/fixtures/copilot-profile-v35-identity.json" )) .unwrap(); let digest = identity["commandDigest"].as_str().unwrap(); diff --git a/packages/paperclip-runner/scripts/build-provider-pack.mjs b/packages/paperclip-runner/scripts/build-provider-pack.mjs index bdf0886af4a..9ed43f2cd09 100644 --- a/packages/paperclip-runner/scripts/build-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/build-provider-pack.mjs @@ -206,6 +206,7 @@ try { writePortableExecutableShim(temporaryRoot, "opencode", "opencode-ai/bin/opencode.exe"); writePortableCopilotShims(temporaryRoot); writePortableNodeShim("acpx", "acpx/dist/cli.js"); + writePortableNodeShim("paperclip-runner-copilot-metadata-probe", "../dist/cli/copilot-metadata-probe.js"); writePortableNodeShim( "claude-agent-acp", "@agentclientprotocol/claude-agent-acp/dist/index.js", diff --git a/packages/paperclip-runner/scripts/copilot-profile-identity.mjs b/packages/paperclip-runner/scripts/copilot-profile-identity.mjs index 3b932b141be..cb6ddf39bf1 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-v34-identity.json"); +const fixture = resolve(root, "test/fixtures/copilot-profile-v35-identity.json"); const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8")); -const declaration = { ...historical.declaration, agentProfileVersion: 34 }; +const declaration = { ...historical.declaration, agentProfileVersion: 35 }; delete declaration.permissionIdentitySourceSha256; const sources = { displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts", diff --git a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs index 38322261173..358a1968140 100644 --- a/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs +++ b/packages/paperclip-runner/scripts/generate-acpx-profiles.mjs @@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract; assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`); assert.equal(contract.acpxPatchSha256, createHash("sha256") .update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex")); -for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v34-identity.json"]]) { +for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v35-identity.json"]]) { const identity = await readJson(path); assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion); assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest); diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts index 999efa6eed2..87e573f04ae 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -13,10 +13,15 @@ import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js"; const PERMISSION_POLICY_SOURCES = [ ["copilot-permission-context.ts", "permissionContextSourceSha256"], ["acp-permission-adapter.ts", "permissionAdapterSourceSha256"], + ["cursor-plan-tool-identity.ts", "permissionToolIdentitySourceSha256"], ["safe-locations.ts", "permissionLocationsSourceSha256"], ["../../semantic-tools/redaction.ts", "permissionRedactionSourceSha256"], ["generated-sidecar-contract.ts", "permissionClassifierSourceSha256"], - ["cursor-plan-tool-identity.ts", "permissionIdentitySourceSha256"], +] as const; + +const COPILOT_DISPLAY_SOURCES = [ + ["copilot-events.ts", "displayEventSourceSha256"], + ["copilot-extension-adapter.ts", "displayProjectionSourceSha256"], ] as const; const SEMANTIC_RECEIPT_SOURCES = [ @@ -29,6 +34,11 @@ const SEMANTIC_RECEIPT_SOURCES = [ ["../../protocol/replay-contract.ts", "semanticValidationSourceSha256"], ["../../protocol/result-normalization.ts", "semanticNormalizationSourceSha256"], ["../../contracts/completion-result.ts", "semanticCompletionContractSourceSha256"], + ["../../contracts/native-execution.ts", "nativeExecutionSourceSha256"], + ["installation-integrity.ts", "guardedInstallationSourceSha256"], + ["../../live/runnerd-codex-transport.ts", "runtimeRequestBridgeSourceSha256"], + ["../codex/codex-session-notifications.ts", "runtimeRequestFacadeSourceSha256"], + ["../codex/codex-thread-normalization.ts", "runtimeRequestNotificationSourceSha256"], ["../../protocol/generated/standalone-validators.ts", "semanticValidatorsSourceSha256"], ["../../protocol/generated/schema-bundle.ts", "semanticSchemaBundleSourceSha256"], ] as const; @@ -59,36 +69,8 @@ async function permissionPolicyClosure(extraClassifierImport = false): Promise ({ verifyNativeAcpxInstallation: vi.fn() })); describe("Copilot build-owned installation", () => { - it("preserves v12 history and changes only the version and four shared source bindings", () => { - const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8")); - const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8")); - const changed = Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort(); - expect(changed).toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]); - expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort()); - expect(prior.declaration.agentProfileVersion).toBe(12); - expect(current.declaration.agentProfileVersion).toBe(13); - const sortedPrior = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b))); - expect(`sha256:${createHash("sha256").update(JSON.stringify(sortedPrior)).digest("hex")}`).toBe(prior.commandDigest); - expect(current.commandDigest).not.toBe(prior.commandDigest); - expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); - }); - - it("preserves v13 history while binding the shared live request snapshot", () => { - const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v13-identity.json", import.meta.url), "utf8")); - const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8")); - expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSidecarSourceSha256"]); - expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); - }); - - it("preserves v14 history while binding the corrected outbound ACPX delivery", () => { - const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v14-identity.json", import.meta.url), "utf8")); - const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v15-identity.json", import.meta.url), "utf8")); - expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["acpxPatchSha256", "agentProfileVersion"]); - expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); - }); - - it("admits the current v17 declaration and binds its source policy, receipts and patch hashes", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8")); + it("admits the current declaration and binds its source policy, receipts and patch hashes", () => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v35-identity.json", import.meta.url), "utf8")); expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY); expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1"); expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT); @@ -104,10 +86,12 @@ describe("Copilot build-owned installation", () => { expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe(identity.commandDigest); const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b))); expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(identity.commandDigest); - for (const [relative, field] of [...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES, ["copilot-policy.ts", "policySha256"], ["../../../scripts/materialize-copilot-binary.mjs", "distributionSourceSha256"], ["../../../scripts/copilot-inner-distribution.mjs", "innerDistributionSourceSha256"], ["../../../../../patches/acpx@0.13.1.patch", "acpxPatchSha256"]]) { + for (const [relative, field] of [...PERMISSION_POLICY_SOURCES, ...COPILOT_DISPLAY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES, ["copilot-policy.ts", "policySha256"], ["profile-compatibility.ts", "profileCompatibilitySourceSha256"], ["provider-assets-root.ts", "assetOwnershipSourceSha256"], ["../../../scripts/install-copilot-assets.mjs", "assetInstallerSourceSha256"], ["../../../scripts/materialize-copilot-binary.mjs", "distributionSourceSha256"], ["../../../scripts/copilot-inner-distribution.mjs", "innerDistributionSourceSha256"], ["../../../../../patches/acpx@0.13.1.patch", "acpxPatchSha256"]]) { expect(createHash("sha256").update(readFileSync(new URL(relative!, import.meta.url))).digest("hex")).toBe(identity.declaration[field!]); } - expect(readFileSync(new URL("../../../acpx-profiles.json", import.meta.url), "utf8")).toContain(identity.commandDigest); + const manifest = JSON.parse(readFileSync(new URL("../../../acpx-profiles.json", import.meta.url), "utf8")); + expect(manifest.profiles.copilot.commandDigest).toBe(identity.commandDigest); + expect(readFileSync(new URL("../../../scripts/build-copilot-distribution.mjs", import.meta.url), "utf8")).toContain("profiles.profiles.copilot.commandDigest"); expect(readFileSync(new URL("../../../runner/crates/runner-core/src/generated_acpx_profiles.rs", import.meta.url), "utf8")).toContain(identity.commandDigest); }); it("binds the complete executable permission-policy import closure", async () => { @@ -118,8 +102,8 @@ describe("Copilot build-owned installation", () => { const bound = new Set(PERMISSION_POLICY_SOURCES.map(([path]) => new URL(path, import.meta.url).href)); expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]); }); - it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8")); + it.each([...PERMISSION_POLICY_SOURCES, ...COPILOT_DISPLAY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v35-identity.json", import.meta.url), "utf8")); identity.declaration[field] = "0".repeat(64); const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b))); expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest); @@ -165,16 +149,4 @@ describe("Copilot build-owned installation", () => { } expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled(); }); - it("rejects the historical v12 digest even when the caller supplies the current version", async () => { - const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v12-identity.json", import.meta.url), "utf8")); - vi.mocked(verifyNativeAcpxInstallation).mockClear(); - for (const override of [ - { commandDigest: prior.commandDigest }, - { agentProfileVersion: prior.declaration.agentProfileVersion }, - { commandDigest: prior.commandDigest, agentProfileVersion: prior.declaration.agentProfileVersion }, - ]) { - await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override })).rejects.toThrow("exact pinned"); - } - expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled(); - }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-profile.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-profile.test.ts index 826927436ee..581f6548e7e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-profile.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-profile.test.ts @@ -1,7 +1,17 @@ import { describe, expect, it } from "vitest"; -import { classifyCopilotFailure, copilotConfiguration, copilotSandboxEnvironment, COPILOT_LAUNCH_ARGUMENTS } from "./copilot-profile.js"; +import { classifyCopilotFailure, composeCopilotSystemInstructions, COPILOT_TASK_ORIENTATION_INSTRUCTIONS, copilotConfiguration, copilotSandboxEnvironment, COPILOT_LAUNCH_ARGUMENTS } from "./copilot-profile.js"; describe("Copilot admission policy", () => { + it("bounds the complete native instruction file without retaining prior instructions", () => { + const suffix = `\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}`; + expect(composeCopilotSystemInstructions("Current instructions.")).toBe(`Current instructions.${suffix}`); + expect(composeCopilotSystemInstructions("")).toBe(suffix); + const remaining = 32 * 1024 - Buffer.byteLength(`${suffix}\n`); + expect(Buffer.byteLength(`${composeCopilotSystemInstructions("x".repeat(remaining))}\n`)).toBe(32 * 1024); + expect(() => composeCopilotSystemInstructions("x".repeat(remaining + 1))).toThrow("bounded size"); + expect(() => composeCopilotSystemInstructions("é".repeat(Math.floor(remaining / 2) + 1))).toThrow("bounded size"); + expect(() => composeCopilotSystemInstructions("embedded\0instruction")).toThrow("bounded size"); + }); it("uses ACP directly without disabling questions or bypassing permissions", () => { expect(COPILOT_LAUNCH_ARGUMENTS.slice(0, 2)).toEqual(["--acp", "--stdio"]); expect(COPILOT_LAUNCH_ARGUMENTS).toContain("--no-auto-update"); diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-profile.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-profile.ts index 73c8d599a05..379a72b9ab4 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-profile.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-profile.ts @@ -7,6 +7,15 @@ export const COPILOT_SYSTEM_INSTRUCTION_DELIVERY = "COPILOT_HOME/copilot-instruc export const COPILOT_CREDENTIAL_ENVIRONMENT_NAME = "COPILOT_GITHUB_TOKEN" as const; export const COPILOT_TASK_ORIENTATION_INSTRUCTIONS = "At the start of every task turn, use the paperclip MCP server's dedicated get_task_context tool with {} and wait for its successful result before task work, report_progress, or any other task-dependent mutation. This is an advertised MCP tool, not an HTTP API route: do not use search_api or call_api to discover or substitute for get_task_context. Loaded instructions, seed state, task descriptions, and prior-turn context do not replace this read. Decide the next action from the returned context. Do not issue the context read and a dependent action in parallel. If this tool is unavailable or its read fails, report the failure and do not perform dependent actions." as const; +/** Compose Copilot-only guidance without changing other providers' sandbox identity. */ +export function composeCopilotSystemInstructions(instructions: string): string { + const composed = `${instructions}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}`; + if (composed.includes("\0") || Buffer.byteLength(`${composed}\n`) > 32 * 1024) { + throw new Error("Provider runtime instructions exceed their bounded size"); + } + return composed; +} + // Keep permission callbacks enabled even when Paperclip's policy is approve-all. // In particular COPILOT_ALLOW_ALL=true also trusts workspace hooks/plugins/MCP. export const COPILOT_LAUNCH_ARGUMENTS = Object.freeze([ diff --git a/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts b/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts index 2ce8ed5c2b7..536220811fa 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-runtime-host.test.ts @@ -2,6 +2,7 @@ import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { expect, it, vi } from "vitest"; +import { COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js"; import { AcpxRuntimeHost, type AcpxRuntimeHostDependencies, type AcpxRuntimePortOpenOptions } from "./runtime-host.js"; it("reports missing Copilot authentication before spawn and releases admission ownership for retry", async () => { @@ -66,24 +67,25 @@ it("does not refresh Copilot instructions for a contender rejected by the lifeti normalizedSessionId: "same-native-session", runtimeDirectory, workingDirectory, providerPolicy: { readOnly: false }, environment: { COPILOT_GITHUB_TOKEN: "synthetic-fixture" }, }; + const composed = (value: string) => `${value}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`; const winner = AcpxRuntimeHost.open({ ...options, systemInstructions: "Winner instructions." }, dependencies); // Attach the rejection handler before deliberately releasing the failed launch. const winnerResult = expect(winner).rejects.toThrow("fixture stops before native process launch"); try { await winnerEntered; - expect(await readFile(instructionPath, "utf8")).toBe("Winner instructions.\n"); + expect(await readFile(instructionPath, "utf8")).toBe(composed("Winner instructions.")); await expect(AcpxRuntimeHost.open({ ...options, systemInstructions: "Losing contender." }, dependencies)) .rejects.toThrow("already has an active lease"); expect(openRuntime).toHaveBeenCalledOnce(); - expect(await readFile(instructionPath, "utf8")).toBe("Winner instructions.\n"); + expect(await readFile(instructionPath, "utf8")).toBe(composed("Winner instructions.")); releaseWinner(); await winnerResult; await expect(AcpxRuntimeHost.open({ ...options, systemInstructions: "New owner instructions." }, dependencies)) .rejects.toThrow("fixture stops before native process launch"); - expect(await readFile(instructionPath, "utf8")).toBe("New owner instructions.\n"); + expect(await readFile(instructionPath, "utf8")).toBe(composed("New owner instructions.")); await expect(AcpxRuntimeHost.open({ ...options, environment: {}, systemInstructions: "Unauthenticated admission." }, dependencies)) .rejects.toMatchObject({ code: "COPILOT_AUTH_REQUIRED" }); - expect(await readFile(instructionPath, "utf8")).toBe("New owner instructions.\n"); + expect(await readFile(instructionPath, "utf8")).toBe(composed("New owner instructions.")); } finally { releaseWinner(); await winnerResult; diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index e63e7896fdb..593baba1e5f 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": 34, + "agentProfileVersion": 35, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:6dde98a9dbadf22f17ca28f133dbe2a65fa199827741190f569704e44bdc20ed", + "commandDigest": "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327", "permissionPolicy": "interactive" }, "claude": { diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index 0f441eb29b8..84c652d256c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -1,3 +1,4 @@ +import { composeCopilotSystemInstructions } from "./copilot-profile.js"; import { piProviderConfiguration } from "./pi-provider-config.js"; import { withAcpxTurnCancellation } from "./turn-cancellation.js"; import { resolveAcpxProviderMode } from "./provider-mode.js"; @@ -497,7 +498,10 @@ export class AcpxRuntimeHost { // Do not race this write against cancellation: retain the lifetime lease // until the atomic refresh settles, then honor an intervening abort. options.signal?.throwIfAborted(); - await refreshCopilotSystemInstructions(sandbox, boundedInstructions(options.systemInstructions)); + await refreshCopilotSystemInstructions( + sandbox, + composeCopilotSystemInstructions(boundedInstructions(options.systemInstructions)), + ); options.signal?.throwIfAborted(); } if (options.agent === "pi") { diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts index ab14aa666f3..2c4bfa3b8ea 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.test.ts @@ -1,3 +1,4 @@ +import { composeCopilotSystemInstructions, COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js"; import { configuredEnvironmentProjection } from "../../configured-environment.js"; import { chmod, @@ -429,15 +430,16 @@ it("refreshes Copilot native instructions privately on every admission, includin const sandbox = await prepareAcpxRuntimeSandbox({ binding: fixture.binding, agent: "copilot", providerPolicy: { readOnly: false, systemInstructions }, }); - await refreshCopilotSystemInstructions(sandbox, systemInstructions); + await refreshCopilotSystemInstructions(sandbox, composeCopilotSystemInstructions(systemInstructions)); return sandbox; }; + const composed = (value: string) => `${value}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`; const first = await prepare("Original registered instructions."); const path = join(first.agentHomeDirectory, "copilot-instructions.md"); - expect(await readFile(path, "utf8")).toBe("Original registered instructions.\n"); + expect(await readFile(path, "utf8")).toBe(composed("Original registered instructions.")); await prepareAcpxRuntimeSandbox({ binding: fixture.binding, agent: "copilot", providerPolicy: { readOnly: false, systemInstructions: "Contender must not write during preparation." } }); - expect(await readFile(path, "utf8")).toBe("Original registered instructions.\n"); + expect(await readFile(path, "utf8")).toBe(composed("Original registered instructions.")); expect((await stat(path)).mode & 0o777).toBe(0o600); expect(first.protectedPaths).toContain(dirname(dirname(first.root))); expect(path.startsWith(`${first.protectedPaths[0]}/`)).toBe(true); @@ -448,11 +450,11 @@ it("refreshes Copilot native instructions privately on every admission, includin await prepare("Current registered instructions."); expect((await lstat(path)).isSymbolicLink()).toBe(false); expect(await readFile(target, "utf8")).toBe("Do not overwrite."); - expect(await readFile(path, "utf8")).toBe("Current registered instructions.\n"); + expect(await readFile(path, "utf8")).toBe(composed("Current registered instructions.")); await prepare(""); - expect(await readFile(path, "utf8")).toBe("\n"); + expect(await readFile(path, "utf8")).toBe(composed("")); await expect(prepare("x".repeat(32 * 1024 + 1))).rejects.toThrow("bounded size"); - expect(await readFile(path, "utf8")).toBe("\n"); + expect(await readFile(path, "utf8")).toBe(composed("")); }); async function sandboxFixture(agent: "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot") { diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts index 3fd9b1ba8eb..3e0e52d250d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts @@ -1,6 +1,6 @@ import { piProviderConfiguration } from "./pi-provider-config.js"; import { configuredEnvironmentKeys } from "../../configured-environment.js"; -import { COPILOT_SYSTEM_INSTRUCTIONS_FILE, COPILOT_TASK_ORIENTATION_INSTRUCTIONS } from "./copilot-profile.js"; +import { COPILOT_SYSTEM_INSTRUCTIONS_FILE } from "./copilot-profile.js"; import { randomBytes } from "node:crypto"; import { constants, @@ -646,15 +646,13 @@ export async function refreshCopilotSystemInstructions( sandbox: Pick, instructions: string, ): Promise { - const current = `${instructions}\n\n# Paperclip task orientation\n${COPILOT_TASK_ORIENTATION_INSTRUCTIONS}\n`; - if (current.includes("\0") || Buffer.byteLength(current) > 32 * 1024) { + if (instructions.includes("\0") || Buffer.byteLength(instructions) > 32 * 1024) { throw new Error("Provider runtime instructions exceed their bounded size"); } - // Native Copilot reloads this file on session/load. Empty caller text clears - // old caller instructions while retaining the current task-orientation rule. + // Native Copilot reloads this file on session/load. Empty text clears old text. await writePrivateFile( join(sandbox.agentHomeDirectory, COPILOT_SYSTEM_INSTRUCTIONS_FILE), - current, + `${instructions}\n`, ); } diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v35-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v35-identity.json new file mode 100644 index 00000000000..504c2d251cc --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v35-identity.json @@ -0,0 +1,81 @@ +{ + "commandDigest": "sha256:6ecb42102c9440e5c8fe5b462b894f9457f255b29f8c584d8b713057cffe4327", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 35, + "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": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee", + "toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "7fd06053db98cc763c7205424069a5286c9d4cfefb3d197a4a765c693dccb7e6", + "semanticDirectDriverSourceSha256": "77882e21ad45b66a688fd1933dbd4fc89fad38e8c1f092805cb9485bdd5223c5", + "semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594", + "semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5", + "semanticSchemaBundleSourceSha256": "fe82ac2cbf194d31ac801de6a3cacfb165cb34146c5c26557f283b8c87a5f01e", + "semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "a03575b3a737dea60da94cf0fa1feed80719a9aaa7f09456eb57a845d9e8e087", + "configuredEnvironmentRustSourceSha256": "55d44e48e217385b5c64b1b7d8df16dda62319ca3fb524e452fd78c9ed804d3b", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382", + "attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "aac818c1cf50a25899ae95cafc70f093c8cd8af37d19f88cbb7847b4d202d2b6", + "nativeRuntimeContextSourceSha256": "0b350f5f4044f342b974254738bab45a470c24db9352b04c92d785d26e7853c8", + "nativeSessionContractSourceSha256": "93251b1eadc686e9d8339d4dfc50b995a3d0a9e6ad8aedc6f2bdf7e30c16138d", + "harnessSessionContractSourceSha256": "9e3d62b712cae91168c96d11ff850b97a144809b5cddfe08ec9a91229e9bfc40", + "harnessBackendSourceSha256": "46fd6523f986d5406eca3b420fe920f1d51a6435fb7afc2acf4d21be13af447f", + "harnessSessionSourceSha256": "4dff9bb3370c5560cfc910a7ee814a27725b469c3b1b5dc021e3c0b158ebe746", + "transportContractSourceSha256": "5cd208fe11c3bedd476494b0609fb32efbabe4a1170e2c5854d0aa5e429bd6f4", + "nativeBackendSourceSha256": "294b9d830bc58c05985087875523db133d32cf999703140fd927e843399424db", + "nativeBackendFactorySourceSha256": "ce22825a53669f8c2db5aaa890bae94af19ede22295bda4ed7987a31d804cd8a", + "nativeInstructionsSourceSha256": "ea85be2aca8dfbd6b4042dcd7b1ed9db9a57baf8c379649a3f5d7d80ca54a6a2", + "runtimeRequestBridgeSourceSha256": "a66ee14f858ccf75c69dfbce132900a30724fe1e6baffc26b02d58a6bc5924ac", + "runtimeRequestFacadeSourceSha256": "e1993906ac9f47eb4ea04ae815db869ecde72f582af460cc0080308670612c8f", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "2881b655112f165320a73753385cd54286139a0273a5fa30c1b43fc3b5981a25", + "nativeExecutionSourceSha256": "9c702e518862c396ccfa12d84d11cb8dec1fc064c595359e31cc3e299a7378cc", + "profileCompatibilitySourceSha256": "20f8aaa363496a4cbede0f33aeacf869fb50171eaee2d63a892c65b3bda4c6a1", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc", + "sidecarErrorSourceSha256": "23f3dbdc1ce488c690d218a854c820f4b4f3edd822bad574d67707314f8c512a", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "826e74747a89b83a6f546ee1355de64ae5265eb920b50ae498a41dd561a493ee", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "runtimeHostSourceSha256": "fd62912cdbaf76e73664024fcec835e899e967a4889fdfdccde7eb215c545f8a", + "runtimeAdapterSourceSha256": "3aea6448dc6474e465e5267b61798ad5449bdaec3f4a41bc4be831426893a9bb", + "runtimeSandboxSourceSha256": "1c1ddb0b24c3417250ce161cda897ba59b1adce7c7609ed553be298464e6c1c4", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/server/src/services/native-runtime/runtime-mode.test.ts b/server/src/services/native-runtime/runtime-mode.test.ts index 11eb8d808aa..a2c9de4e558 100644 --- a/server/src/services/native-runtime/runtime-mode.test.ts +++ b/server/src/services/native-runtime/runtime-mode.test.ts @@ -107,16 +107,23 @@ describe("resolveNativeRuntimeMode", () => { kind: "native", profile: { backend: "acpx_runtime" }, }); + expect(resolveNativeRuntimeMode({ + ...eligible, + adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" }, + })).toMatchObject({ + kind: "native", + profile: { backend: "acpx_runtime" }, + }); }); - it("rejects malformed OpenCode and unqualified ACPX profiles", () => { + it("rejects malformed OpenCode and unregistered ACPX profiles", () => { expect(() => resolveNativeRuntimeMode({ ...eligible, adapterConfig: { provider: "opencode", model: "gpt-5.6-sol" }, })).toThrow(expect.objectContaining({ code: "paperclip_runner_opencode_model_invalid", })); - for (const acpxAgent of ["copilot"]) { + for (const acpxAgent of ["unregistered-acpx-agent"]) { expect(() => resolveNativeRuntimeMode({ ...eligible, adapterConfig: { provider: "acpx", acpxAgent, model: "explicit-provider-model" }, From 055459e1150e0a172f962ae98669ab937a40706a Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 9 Oct 2026 11:42:44 -0500 Subject: [PATCH 2/2] Keep guarded recovery fixture synthetic under installed Codex selection Bind its fake provider at the command resolver and retain bounded diagnostics for failed terminal results. Preserve retirement and archived mutation assertions. Co-Authored-By: Paperclip --- .../native-runtime/native-session-resume.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/services/native-runtime/native-session-resume.test.ts b/server/src/services/native-runtime/native-session-resume.test.ts index 41b99c90bb7..80236ceb6a1 100644 --- a/server/src/services/native-runtime/native-session-resume.test.ts +++ b/server/src/services/native-runtime/native-session-resume.test.ts @@ -734,6 +734,7 @@ const recoveryFakeCodex = resolve( const previousStateBase = process.env.PAPERCLIP_RUNNER_STATE_DIR; const server = createServer(); let firstSession: NativeSession | undefined; + let restoreCodexCommand: (() => void) | undefined; const ownedSessions = new Set(); const ownedSpawns: Array<{ pid: number; processGroupId: number | null; startedAt: string }> = []; const onSpawn = async (meta: typeof ownedSpawns[number]) => { ownedSpawns.push(meta); }; @@ -759,6 +760,11 @@ const recoveryFakeCodex = resolve( `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(recoveryFakeCodex)} "$CODEX_HOME/fake-state.json" 16\n`, ); await chmod(command, 0o700); + // Installed dependency selection intentionally precedes PATH on mainline. + // Bind this fixture's synthetic provider explicitly at that boundary. + const codexCommandModule = await import("../../../../packages/paperclip-runner/src/drivers/codex/codex-command.js"); + const codexCommandSelection = vi.spyOn(codexCommandModule, "resolveCodexCommand").mockReturnValue(command); + restoreCodexCommand = () => codexCommandSelection.mockRestore(); await new Promise((done) => server.listen(0, "127.0.0.1", done)); const address = server.address(); if (!address || typeof address === "string") @@ -1048,6 +1054,11 @@ const recoveryFakeCodex = resolve( async onContinuityBreak(value) { continuity = value; }, + }).then((result) => { + if (result.terminal.runTerminalState !== "succeeded") { + throw new Error(`Disposable recovery fixture failed: ${JSON.stringify(result)}`); + } + return result; }).catch(async (error: unknown) => { const eventRows = await db .select({ @@ -1227,6 +1238,7 @@ const recoveryFakeCodex = resolve( Promise.resolve().then(() => session.close({ reason: "Recovery fixture cleanup" })), )); const closeFailures = closed.filter((result) => result.status === "rejected"); + restoreCodexCommand?.(); runnerPrpWebSocketInternals.resetForTests(); server.closeAllConnections(); await new Promise((done) => server.close(() => done()));