diff --git a/.dockerignore b/.dockerignore index a863f36141c..cb547de2a60 100644 --- a/.dockerignore +++ b/.dockerignore @@ -76,6 +76,7 @@ packages/grok-acp/bin !packages/paperclip-runner/test-fixtures/pi-acp/profile-v26-identity.json !packages/paperclip-runner/test/fixtures/copilot-profile-v19-claude-qualification-identity.json !packages/paperclip-runner/test/fixtures/copilot-profile-v22-identity.json +!packages/paperclip-runner/test/fixtures/copilot-profile-v23-identity.json !packages/paperclip-runner/test-fixtures/pi-acp/profile-v24-claude-historical-identity.json diff --git a/doc/architecture/runner-pi-capabilities.md b/doc/architecture/runner-pi-capabilities.md index c367ce6acd3..365f7d3d303 100644 --- a/doc/architecture/runner-pi-capabilities.md +++ b/doc/architecture/runner-pi-capabilities.md @@ -4,10 +4,11 @@ The composed Claude source declares **Pi profile 27** (`sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba`) -and **Copilot profile 22**, still pending qualification -(`sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2`). +and **Copilot profile 23**, still pending qualification +(`sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63`). These identities bind the shared Claude effort and lifecycle changes plus -master's OpenRouter attribution. Provider executable closures, model/effort +master's OpenRouter attribution. Copilot's shared bridge binding also includes +Claude-only essential tool metadata; its own tool catalog is unchanged. Provider executable closures, model/effort options and qualification boundaries are unchanged. Current package and source checks remain separate from retained provider execution results, which keep their literal profile/source identities. Earlier immutable fixtures remain diff --git a/doc/architecture/runner-rich-acp-capabilities.md b/doc/architecture/runner-rich-acp-capabilities.md index a1d611b7423..275d5804a54 100644 --- a/doc/architecture/runner-rich-acp-capabilities.md +++ b/doc/architecture/runner-rich-acp-capabilities.md @@ -4,10 +4,11 @@ The composed Claude source declares **Pi profile 27** (`sha256:b1b75a1ae3ac498eb07b95b96bebf4da86227b83d09276076dd5c222e3964dba`) -and **Copilot profile 22**, still pending qualification -(`sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2`). +and **Copilot profile 23**, still pending qualification +(`sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63`). These identities bind the shared Claude effort and lifecycle changes plus -master's OpenRouter attribution. Provider executable closures, model/effort +master's OpenRouter attribution. Copilot's shared bridge binding also includes +Claude-only essential tool metadata; its own tool catalog is unchanged. Provider executable closures, model/effort options and qualification boundaries are unchanged. Current package and source checks remain separate from retained provider execution results, which keep their literal profile/source identities. Earlier immutable fixtures remain diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index e483929ad19..a8881081ca8 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -32,12 +32,12 @@ "requiresProviderPolicy": true }, "copilot": { - "agentProfileVersion": 22, + "agentProfileVersion": 23, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2", + "commandDigest": "sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63", "requiresProviderPolicy": true, "qualificationStatus": "pending" }, diff --git a/packages/paperclip-runner/docs/protocol-compatibility.md b/packages/paperclip-runner/docs/protocol-compatibility.md index cafc8ddb618..175d5b3b681 100644 --- a/packages/paperclip-runner/docs/protocol-compatibility.md +++ b/packages/paperclip-runner/docs/protocol-compatibility.md @@ -317,3 +317,17 @@ including master's OpenRouter attribution. The prior branch profile 24 is preserved separately from master's published profile 24. Provider executable bytes remain unchanged. These new source identities require fresh compatibility checks; earlier provider runs retain their original identities. + +## Claude essential task tools + +Claude's runner-owned MCP bridge marks `set_task_title`, `paperclip_finish` and +`paperclip_block` with `_meta["anthropic/alwaysLoad"]: true`. The pinned SDK can +include these definitions in its first request without a ToolSearch round trip. +Other public tools keep their existing deferral policy. Private operations stay +undiscoverable, and incoming tool definitions cannot set this metadata themselves. +The host enables this only for Claude; Codex and Copilot catalogs are unchanged. +No server-wide tool preloading, model change or permission change is introduced. + +The shared bridge's source binding advances pending Copilot profile 22 to 23. +Profile 22 remains decodable and its fixture is immutable. Copilot remains pending; +the revision does not qualify a new provider or change its executable closure. 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 1a72ae27b75..7ad167ce577 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:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2", + "sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63", requires_provider_policy: true, }, "claude" => AcpxReleaseProfile { 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 0a81202310b..12ba72ccf45 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -211,8 +211,22 @@ describe("Copilot build-owned installation", () => { expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); }); - it("admits the current v22 declaration and binds its source policy, receipts and patch hashes", () => { - const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v22-identity.json", import.meta.url), "utf8")); + it("preserves v22 history while binding the Claude-only preload metadata branch", async () => { + const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v22-identity.json", import.meta.url), "utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v23-identity.json", import.meta.url), "utf8")); + expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()) + .toEqual(["agentProfileVersion", "semanticBridgeSourceSha256"]); + expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort()); + expect(isSupportedAcpxProfileVersion("copilot", 22)).toBe(true); + const sorted = Object.fromEntries(Object.entries(prior.declaration).sort(([a], [b]) => a.localeCompare(b))); + expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).toBe(prior.commandDigest); + await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, + agentProfileVersion: 22, commandDigest: prior.commandDigest })).rejects.toThrow("exact pinned"); + expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending"); + }); + + it("admits the current v23 declaration and binds its source policy, receipts and patch hashes", () => { + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v23-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); @@ -243,7 +257,7 @@ describe("Copilot build-owned installation", () => { 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-v22-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v23-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); diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index 58740700729..bf7d198f0b9 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": 22, + "agentProfileVersion": 23, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:9027ee2bdf9180e3267b7d05b56b0703943251a29713c393f7a3f2ef1072efd2", + "commandDigest": "sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63", "qualificationStatus": "pending", "permissionPolicy": "interactive" }, diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts index a38fc6fed8b..213dd87a81d 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts @@ -8,7 +8,7 @@ export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfi 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], 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], + 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], cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], }; diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts index ba93401fde0..20bc11c713a 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.test.ts @@ -906,6 +906,31 @@ describe("ACPX runtime host", () => { expect(fixture.commandClose).toHaveBeenCalledOnce(); }); + it.each(["claude", "codex", "copilot"] as const)("preloads essential task tools only for Claude through the host (%s)", async agent => { + const fixture = await hostFixture(); + const model = agent === "claude" ? "claude-opus-5-5" : "gpt-5.6-sol"; + let bridge: Extract | undefined; + const environment = agent === "claude" ? { ANTHROPIC_API_KEY: "fixture-not-a-real-credential" } + : agent === "copilot" ? { COPILOT_GITHUB_TOKEN: "fixture-not-a-real-credential" } + : { PAPERCLIP_ACPX_CODEX_AUTH_JSON_SECRET: "{}" }; + const host = await AcpxRuntimeHost.open({ + ...fixture.options, agent, model, permissionMode: "deny-all", environment, + semanticTools: { tools: [{ name: "set_task_title", inputSchema: { type: "object", properties: {} } }], handler: async () => ({}) }, + }, fixture.dependencies({ openRuntime: async options => { + bridge = options.mcpServers.find((server): server is NonNullable => "url" in server && server.runnerOwned); + return runtimePort({ getStatus: async () => ({ models: { currentModelId: model, availableModelIds: [model] } }) }); + } })); + try { + const response = await fetch(bridge!.url, { + method: "POST", headers: { Authorization: `Bearer ${bridge!.bearerToken}`, "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), + }); + const listed = await response.json() as { result: { tools: Array<{ name: string; _meta?: Record }> } }; + expect(listed.result.tools.filter(tool => tool._meta?.["anthropic/alwaysLoad"] === true).map(({ name }) => name)) + .toEqual(agent === "claude" ? ["set_task_title", "paperclip_finish", "paperclip_block"] : []); + } finally { await host.close({ reason: "tool preload verified" }); } + }); + it("forwards semantic receipt capture through the real runtime host and authenticated bridge", async () => { const fixture = await hostFixture(); const events: CanonicalProviderEvent[] = []; diff --git a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts index 070dbe8d4d1..af13be285f4 100644 --- a/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts +++ b/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts @@ -194,7 +194,7 @@ export type AcpxMcpServerBinding = { name: string; command: string; args: string[]; env: Record; runnerOwned: false; }; -export type AcpxSemanticToolSession = Omit; +export type AcpxSemanticToolSession = Omit; export interface AcpxRetainedCleanupFailure { resource: @@ -582,7 +582,10 @@ export class AcpxRuntimeHost { toolBridge = options.semanticTools ? await acquireAbortableAdmissionResource({ signal: options.signal, - acquire: () => startRunnerToolBridge(options.semanticTools!), + acquire: () => startRunnerToolBridge({ + ...options.semanticTools!, + preloadClaudeTaskTools: options.agent === "claude", + }), resource: "tool_bridge", releaseLate: (lateToolBridge) => lateToolBridge.close(), reportFailure: (failure) => diff --git a/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts b/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts index b61d2c32f3a..152b8a9b6ea 100644 --- a/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts +++ b/packages/paperclip-runner/src/drivers/runner-tool-bridge.test.ts @@ -38,6 +38,27 @@ async function rpc( } describe("runner semantic MCP bridge", () => { + it.each([false, true])("preloads only essential public Claude task tools when opted in (%s)", async preloadClaudeTaskTools => { + const bridge = await startRunnerToolBridge({ + preloadClaudeTaskTools, + tools: [tool("paperclip__set_task_title"), { + ...tool("documents.read"), _meta: { "anthropic/alwaysLoad": true }, + }], + privateTools: [tool("__paperclip_permission")], + handler: async () => ({ ok: true }), + }); + bridges.push(bridge); + const listed = await (await rpc(bridge, { id: 1, method: "tools/list" })).json() as { + result: { tools: Array<{ name: string; _meta?: Record }> }; + }; + expect(listed.result.tools.map(({ name }) => name)).toEqual([ + "set_task_title", "documents.read", "paperclip_finish", "paperclip_block", + ]); + expect(listed.result.tools.filter(tool => tool._meta?.["anthropic/alwaysLoad"] === true).map(({ name }) => name)) + .toEqual(preloadClaudeTaskTools ? ["set_task_title", "paperclip_finish", "paperclip_block"] : []); + expect(listed.result.tools.find(tool => tool.name === "documents.read")?._meta).toBeUndefined(); + }); + it("binds to loopback, requires authentication, and exposes a closed catalog", async () => { const bridge = await startRunnerToolBridge({ secret: "session-secret", diff --git a/packages/paperclip-runner/src/drivers/runner-tool-bridge.ts b/packages/paperclip-runner/src/drivers/runner-tool-bridge.ts index 06c59a8a8db..4c3a49a873a 100644 --- a/packages/paperclip-runner/src/drivers/runner-tool-bridge.ts +++ b/packages/paperclip-runner/src/drivers/runner-tool-bridge.ts @@ -22,6 +22,7 @@ export interface RunnerToolDefinition { name: string; description?: string; inputSchema: Record; + _meta?: { "anthropic/alwaysLoad": true }; } export interface RunnerToolCall { @@ -36,6 +37,8 @@ export interface RunnerToolCall { export interface RunnerToolBridgeOptions { tools?: readonly Readonly>[]; + /** Claude-only: avoid tool search for task naming and terminal disposition. */ + preloadClaudeTaskTools?: boolean; /** Runner-owned operations that are callable but never model-visible. */ privateTools?: readonly Readonly>[]; handler(call: RunnerToolCall): Promise; @@ -118,6 +121,13 @@ export async function startRunnerToolBridge( } } const visibleTools = [...tools, ...structuredClone(RESERVED_TOOLS)]; + if (options.preloadClaudeTaskTools) { + for (const tool of visibleTools) { + if (tool.name === "set_task_title" || tool.name === PRP_COMPLETION_TOOL_NAME || tool.name === PRP_BLOCK_TOOL_NAME) { + tool._meta = { "anthropic/alwaysLoad": true }; + } + } + } const admittedTools = [...visibleTools, ...privateTools]; const validators = compileValidators(admittedTools); const calls = new Map(); diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v23-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v23-identity.json new file mode 100644 index 00000000000..abf8d388cc9 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v23-identity.json @@ -0,0 +1,44 @@ +{ + "commandDigest": "sha256:6ca1e2c9efa23887ba6f08d89980c91d13114640ac8a1a79f00f27aa8cd39d63", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 23, + "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": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224", + "permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce", + "permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9", + "permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f", + "permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4", + "permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2", + "semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791", + "semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580", + "toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "ddd98aabe20f4d40b04bb58126bda548a9d85b8234a3f9ea84b6db414e9b4814", + "semanticDirectDriverSourceSha256": "72d5134a9567ced685bc942adf14ec5dfba04bcd6b0eeabe77eac0cec5c434c3", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "a104278d4d0e063a69bb464938db117dc27691b5bd4590c5a359e11f4917f623", + "semanticValidatorsSourceSha256": "38ae5379d47df633564730ec1359a16d21c0d0a68addb5e488b58c697f13362c", + "semanticSchemaBundleSourceSha256": "5e957fe0f0ba164441d6a5b8a276e52cf6f2ffaf99741faca99cff05e539221c", + "semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "semanticContinuationSourceSha256": "eb877719ef32a935afa4ec93f0f1c43d58d55bc4e1042965febcac4c45ed851c" + } +}