From d09804d201a148c00dc58c0d2798526e7b697443 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 8 Oct 2026 10:18:34 -0500 Subject: [PATCH] Preserve split UTF-8 Copilot metadata and qualify profile v32 Co-Authored-By: Paperclip --- packages/paperclip-runner/acpx-profiles.json | 4 ++-- .../crates/runner-core/src/acpx_provider_backend.rs | 2 +- .../crates/runner-core/src/generated_acpx_profiles.rs | 2 +- .../runner-core/tests/native_provider_backend.rs | 2 +- .../scripts/build-copilot-distribution.mjs | 2 +- .../scripts/copilot-profile-identity.mjs | 4 ++-- .../paperclip-runner/scripts/discover-copilot-acp.mjs | 4 +++- .../scripts/discover-copilot-acp.test.mjs | 9 +++++++++ .../paperclip-runner/scripts/qualify-copilot-acp.mjs | 4 +++- .../scripts/qualify-copilot-acp.test.mjs | 11 +++++++++-- .../src/contracts/native-execution.test.ts | 4 ++-- .../src/drivers/acpx/copilot-installation.test.ts | 4 ++-- .../src/drivers/acpx/generated-profiles.ts | 4 ++-- .../src/drivers/acpx/profile-compatibility.test.ts | 4 ++-- .../src/drivers/acpx/profile-compatibility.ts | 4 ++-- .../src/drivers/copilot-metadata-probe.test.ts | 8 ++++++++ .../src/drivers/copilot-metadata-probe.ts | 4 +++- ...dentity.json => copilot-profile-v32-identity.json} | 8 ++++---- 18 files changed, 57 insertions(+), 27 deletions(-) rename packages/paperclip-runner/test/fixtures/{copilot-profile-v31-identity.json => copilot-profile-v32-identity.json} (95%) diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index b2fe9c04fa..bf7d1c2a08 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -32,12 +32,12 @@ "requiresProviderPolicy": true }, "copilot": { - "agentProfileVersion": 31, + "agentProfileVersion": 32, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:4a22e4c50fd213c79cd02fd3c5369aef2256ac719762c62f5b70c691cd47e76b", + "commandDigest": "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc", "requiresProviderPolicy": true }, "claude": { 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 8e49e5df23..a72aa982db 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 @@ -3620,7 +3620,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-v31-identity.json" + "../../../../test/fixtures/copilot-profile-v32-identity.json" )) .unwrap()["commandDigest"] .clone(); 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 ebd275e8f2..5dd8a81bb6 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:4a22e4c50fd213c79cd02fd3c5369aef2256ac719762c62f5b70c691cd47e76b", + "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc", 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 a5169d2644..991f918e27 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 @@ -1029,7 +1029,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-v31-identity.json" + "../../../../test/fixtures/copilot-profile-v32-identity.json" )) .unwrap(); let digest = identity["commandDigest"].as_str().unwrap(); diff --git a/packages/paperclip-runner/scripts/build-copilot-distribution.mjs b/packages/paperclip-runner/scripts/build-copilot-distribution.mjs index 755322db5b..c8fd6fe537 100644 --- a/packages/paperclip-runner/scripts/build-copilot-distribution.mjs +++ b/packages/paperclip-runner/scripts/build-copilot-distribution.mjs @@ -7,7 +7,7 @@ import { COPILOT_VERSION, materializePinnedCopilotBinary, resolveCopilotDistribu const MAX_ARCHIVE_BYTES = 128 * 1024 * 1024; const MAX_EXPANDED_BYTES = 384 * 1024 * 1024; -const PROFILE_DIGEST = "sha256:4a22e4c50fd213c79cd02fd3c5369aef2256ac719762c62f5b70c691cd47e76b"; +const PROFILE_DIGEST = "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc"; /** Build-time only; outputRoot is the exact runner-owned platform asset directory. */ export async function buildPinnedCopilotDistribution({ outputRoot, platform = process.platform, architecture = process.arch }, { fetchImpl = fetch } = {}) { diff --git a/packages/paperclip-runner/scripts/copilot-profile-identity.mjs b/packages/paperclip-runner/scripts/copilot-profile-identity.mjs index 5c8ab96b8f..a3486ad628 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-v31-identity.json"); +const fixture = resolve(root, "test/fixtures/copilot-profile-v32-identity.json"); const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8")); -const declaration = { ...historical.declaration, agentProfileVersion: 31 }; +const declaration = { ...historical.declaration, agentProfileVersion: 32 }; delete declaration.permissionIdentitySourceSha256; const sources = { displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts", diff --git a/packages/paperclip-runner/scripts/discover-copilot-acp.mjs b/packages/paperclip-runner/scripts/discover-copilot-acp.mjs index dc0b1fc5fe..3cbfc825d1 100644 --- a/packages/paperclip-runner/scripts/discover-copilot-acp.mjs +++ b/packages/paperclip-runner/scripts/discover-copilot-acp.mjs @@ -1,3 +1,4 @@ +import { StringDecoder } from "node:string_decoder"; // Metadata only. Supply an explicit token; this closed protocol client never prompts. // Usage: node discover-copilot-acp.mjs [exact-model-id] import assert from "node:assert/strict"; @@ -24,6 +25,7 @@ export async function withDiscoveryWorkspace(install, callback, filesystem = { m } export function createDiscoveryRpc(child, { deadlineMs = 25_000, maxBytes = 2_097_152 } = {}) { + const decoder = new StringDecoder("utf8"); const pending = new Map(), observedMethods = {}; let nextId = 0, buffer = "", bytes = 0, failure; function fail(code) { @@ -39,7 +41,7 @@ export function createDiscoveryRpc(child, { deadlineMs = 25_000, maxBytes = 2_09 child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > maxBytes) { fatal("provider_wire_limit"); return; } - buffer += chunk.toString(); + buffer += decoder.write(chunk); while (buffer.includes("\n")) { const end = buffer.indexOf("\n"), line = buffer.slice(0, end); buffer = buffer.slice(end + 1); if (!line.trim()) continue; diff --git a/packages/paperclip-runner/scripts/discover-copilot-acp.test.mjs b/packages/paperclip-runner/scripts/discover-copilot-acp.test.mjs index d33b281700..b8602fe656 100644 --- a/packages/paperclip-runner/scripts/discover-copilot-acp.test.mjs +++ b/packages/paperclip-runner/scripts/discover-copilot-acp.test.mjs @@ -38,3 +38,12 @@ test("closed discovery protocol preserves numeric zero and rejects prompts", asy assert.deepEqual(await pending, { protocolVersion: 1 }); assert.throws(() => rpc.request("session/prompt", {}), /cannot send/); rpc.close(); }); + +test("discovery preserves multibyte model names when every byte arrives separately", async () => { + const child = childFixture(), rpc = createDiscoveryRpc(child); + const result = { models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "東京 🚀 café" }] } }; + const pending = rpc.request("session/new", {}); + const wire = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 0, result })}\n`); + for (const byte of wire) child.stdout.emit("data", Buffer.from([byte])); + assert.deepEqual(await pending, result); rpc.close(); +}); diff --git a/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs b/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs index 3031419670..c5cbaf13ca 100644 --- a/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs +++ b/packages/paperclip-runner/scripts/qualify-copilot-acp.mjs @@ -1,3 +1,4 @@ +import { StringDecoder } from "node:string_decoder"; // Real-service qualification only. One explicit scenario sends one ACP prompt. // Usage: node qualify-copilot-acp.mjs // Bind only COPILOT_GITHUB_TOKEN. Do not run without a reconciled billing reservation. @@ -75,6 +76,7 @@ export async function runProbe(packInput, output, scenario, token) { backgroundCompletionObservedBeforeTerminal: false, cleanupComplete: false }; let child, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false; let outerTimer, markerTimer; + const decoder = new StringDecoder("utf8"); const pending = new Map(); const env = { PATH: "/usr/bin:/bin", COPILOT_GITHUB_TOKEN: token, COPILOT_AUTO_UPDATE: "false", COPILOT_ALLOW_ALL: "false", NO_COLOR: "1" }; async function sampleMarker(phase) { @@ -115,7 +117,7 @@ export async function runProbe(packInput, output, scenario, token) { child.stdout.on("data", chunk => { bytes += chunk.length; if (bytes > 8_388_608) { failPending("wire_limit"); child.kill("SIGTERM"); return; } - buffer += chunk.toString(); + buffer += decoder.write(chunk); while (buffer.includes("\n")) { const index = buffer.indexOf("\n"), line = buffer.slice(0, index); buffer = buffer.slice(index + 1); if (!line.trim()) continue; diff --git a/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs b/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs index 182520dc45..fb018fe3e0 100644 --- a/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs +++ b/packages/paperclip-runner/scripts/qualify-copilot-acp.test.mjs @@ -69,13 +69,19 @@ test("full credential-free protocol fixture preserves numeric-zero denial and re await writeFile(join(moduleRoot, "copilot-events.js"), 'export const COPILOT_ACP_CLIENT_CAPABILITIES = {};'); const childScript = join(root, "child.mjs"); await writeFile(childScript, `import { createInterface } from 'node:readline'; -const send = m => process.stdout.write(JSON.stringify({jsonrpc:'2.0', ...m})+'\\n'); +const send = m => { + const wire=Buffer.from(JSON.stringify({jsonrpc:'2.0', ...m})+'\\n'); + const unicode=wire.indexOf(Buffer.from('🚀')); + if(unicode<0){process.stdout.write(wire);return;} + process.stdout.write(wire.subarray(0,unicode+2)); + setTimeout(()=>process.stdout.write(wire.subarray(unicode+2)),50); +}; let promptId; createInterface({input:process.stdin}).on('line', line => { const m=JSON.parse(line); if(!m.method){if(m.id!==0 || m.result?.outcome?.optionId!=='native-deny-0') process.exit(8); send({id:promptId,result:{stopReason:'end_turn'}});return;} let result={}; - if(m.method==='initialize')result={agentInfo:{version:'1.0.88'},agentCapabilities:{}}; + if(m.method==='initialize')result={agentInfo:{version:'1.0.88'},agentCapabilities:{label:'東京 🚀 café'}}; if(m.method==='session/new')result={sessionId:'fixture-session',models:{availableModels:[{modelId:'gpt-5.6-luna'}]}}; if(m.method==='session/set_config_option')result={configOptions:[{id:'model',currentValue:'gpt-5.6-luna'}]}; if(m.method==='session/prompt'){promptId=m.id;send({id:0,method:'session/request_permission',params:{sessionId:'fixture-session',toolCall:{kind:'edit',rawInput:{fileName:process.cwd()+'/qualification-marker.txt'}},options:${JSON.stringify(options)}}});return;} @@ -86,6 +92,7 @@ createInterface({input:process.stdin}).on('line', line => { const result = await runProbe(root, output, "deny-write", "fixture-token-never-used-for-network"); const evidence = JSON.parse(await readFile(output, "utf8")); assert.equal(result.passed, true); + assert.deepEqual(evidence.agentCapabilities, { label: "東京 🚀 café" }); assert.equal(evidence.permissions[0].requestId, 0); assert.deepEqual(evidence.permissions[0].outcome, { outcome: "selected", optionId: "native-deny-0" }); assert.equal(evidence.cleanupComplete, true); diff --git a/packages/paperclip-runner/src/contracts/native-execution.test.ts b/packages/paperclip-runner/src/contracts/native-execution.test.ts index 80d4187ac8..75f62db7ee 100644 --- a/packages/paperclip-runner/src/contracts/native-execution.test.ts +++ b/packages/paperclip-runner/src/contracts/native-execution.test.ts @@ -391,7 +391,7 @@ describe("NativeExecutionInputV1", () => { })).toThrow("does not match"); }); - it.each([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] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => { + it.each([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] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => { const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId, permissionPolicy, qualificationStatus: _qualificationStatus, modelPolicy: _modelPolicy, ...snapshot } = QUALIFIED_ACPX_PROFILES.copilot; @@ -403,7 +403,7 @@ describe("NativeExecutionInputV1", () => { const parsed = parseNativeExecutionInput(value); expect(parsed.provider).toEqual(provider); expect(parseNativeExecutionInput(parsed)).toEqual(parsed); - for (const unsupportedVersion of [0, 32, 1.5, "31", null]) { + for (const unsupportedVersion of [0, 33, 1.5, "32", null]) { expect(() => parseNativeExecutionInput({ ...value, provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } }, })).toThrow("qualified ACPX v1 profile"); 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 002b5f29b5..673a1d0c92 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -70,7 +70,7 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi describe("Copilot build-owned installation", () => { 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-v31-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v32-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); @@ -101,7 +101,7 @@ describe("Copilot build-owned installation", () => { expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]); }); 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-v31-identity.json", import.meta.url), "utf8")); + const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v32-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 a283881ca3..d97f526146 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": 31, + "agentProfileVersion": 32, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:4a22e4c50fd213c79cd02fd3c5369aef2256ac719762c62f5b70c691cd47e76b", + "commandDigest": "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc", "permissionPolicy": "interactive" }, "claude": { diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts index 13586f1fcd..5b6305a10f 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.test.ts @@ -10,10 +10,10 @@ describe("historical ACPX profile decoding", () => { } }); it("decodes retained Copilot revisions and the current profile without widening other providers", () => { - for (const version of [1, 5, 6, 12, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]) { + for (const version of [1, 5, 6, 12, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]) { expect(isSupportedAcpxProfileVersion("copilot", version)).toBe(true); } - expect(isSupportedAcpxProfileVersion("copilot", 32)).toBe(false); + expect(isSupportedAcpxProfileVersion("copilot", 33)).toBe(false); for (const agent of ["cursor", "pi", "claude", "codex", "grok"]) { expect(isSupportedAcpxProfileVersion(agent, 23)).toBe(false); expect(isSupportedAcpxProfileVersion(agent, 24)).toBe(false); diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts index a4fbcbcbe6..2834a50139 100644 --- a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts +++ b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts @@ -1,9 +1,9 @@ /** Decodable historical profile revisions, not permission to launch them. * Exact current profile/package/digest admission is checked separately. */ -export type AcpxProfileVersion = 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; +export type AcpxProfileVersion = 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; const historicalVersions: Readonly> = { pi: [1, 2, 3, 4, 5], 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], + 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], cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], }; diff --git a/packages/paperclip-runner/src/drivers/copilot-metadata-probe.test.ts b/packages/paperclip-runner/src/drivers/copilot-metadata-probe.test.ts index 014c7617d5..0b7a3f6926 100644 --- a/packages/paperclip-runner/src/drivers/copilot-metadata-probe.test.ts +++ b/packages/paperclip-runner/src/drivers/copilot-metadata-probe.test.ts @@ -48,6 +48,14 @@ describe("Copilot metadata boundary", () => { expect(await probeCopilotMetadata("token", "missing")).toMatchObject({ status: "failed", code: "COPILOT_MODEL_UNAVAILABLE", promptSent: false }); expect(methods).toEqual(["initialize", "session/new"]); expect(close).toHaveBeenCalledOnce(); }); + it("preserves model labels split inside every UTF-8 character across transport chunks", async () => { + const child = fakeChild(() => undefined), rpc = createMetadataRpc(child); + const result = { models: { availableModels: [{ modelId: "gpt-5.6-luna", name: "東京 🚀 café" }] } }; + const response = rpc.request("session/new", {}); + const wire = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 0, result })}\n`); + for (const byte of wire) child.stdout.emit("data", Buffer.from([byte])); + await expect(response).resolves.toEqual(result); + }); it("cannot dispatch semantic or prompt methods", () => { const child = fakeChild(() => undefined), rpc = createMetadataRpc(child); expect(() => rpc.request("session/prompt", {})).toThrow("cannot send"); diff --git a/packages/paperclip-runner/src/drivers/copilot-metadata-probe.ts b/packages/paperclip-runner/src/drivers/copilot-metadata-probe.ts index 40ec863653..f5c5b4289e 100644 --- a/packages/paperclip-runner/src/drivers/copilot-metadata-probe.ts +++ b/packages/paperclip-runner/src/drivers/copilot-metadata-probe.ts @@ -1,3 +1,4 @@ +import { StringDecoder } from "node:string_decoder"; import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -90,6 +91,7 @@ export async function probeCopilotMetadata(token: string, model?: string): Promi } export function createMetadataRpc(child: ChildProcessWithoutNullStreams) { + const decoder = new StringDecoder("utf8"); const pending = new Map(); let nextId = 0, bytes = 0, buffer = "", failure: Error | undefined; const fail = (error: Error) => { failure ??= error; for (const call of pending.values()) call.reject(failure); pending.clear(); }; @@ -100,7 +102,7 @@ export function createMetadataRpc(child: ChildProcessWithoutNullStreams) { child.stdout.on("data", (chunk: Buffer) => { bytes += chunk.length; if (bytes > 2 * 1024 * 1024) { fail(new Error("Copilot metadata exceeded its bound")); child.kill("SIGTERM"); return; } - buffer += chunk.toString(); + buffer += decoder.write(chunk); while (buffer.includes("\n")) { const end = buffer.indexOf("\n"), line = buffer.slice(0, end); buffer = buffer.slice(end + 1); if (!line.trim()) continue; diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v31-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v32-identity.json similarity index 95% rename from packages/paperclip-runner/test/fixtures/copilot-profile-v31-identity.json rename to packages/paperclip-runner/test/fixtures/copilot-profile-v32-identity.json index 9e8ac963db..d60f725870 100644 --- a/packages/paperclip-runner/test/fixtures/copilot-profile-v31-identity.json +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v32-identity.json @@ -1,9 +1,9 @@ { - "commandDigest": "sha256:4a22e4c50fd213c79cd02fd3c5369aef2256ac719762c62f5b70c691cd47e76b", + "commandDigest": "sha256:3dcc8de3034e32001b0a819a614beca345d314266a70a09e0050a9f5f58275fc", "declaration": { "schema": "paperclip.acpx_profile_declaration.v1", "agent": "copilot", - "agentProfileVersion": 31, + "agentProfileVersion": 32, "acpxVersion": "0.13.1", "agentServerVersion": "1.0.88", "protocolPolicyRevision": "copilot-agent-manual-v1", @@ -62,14 +62,14 @@ "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", "guardedInstallationSourceSha256": "8afd25df307772e484e179e58bf54ce1510fe92be694482111d589f9f0ae6e77", "nativeExecutionSourceSha256": "4cdd702c233bf0d086ff2aeea8f02c6db8817ba0c0bcb5e64e355ee7a1c2dd77", - "profileCompatibilitySourceSha256": "ef78b62e804473f7c1c9f5e119b4755e774f1ba1cdb7b01609b74392af2547e6", + "profileCompatibilitySourceSha256": "fe521686dbd9884eec96ba0963ec00b6b58f4d891aa0292e0dc18b0d84770646", "installationSourceSha256": "9934653d6983905930099b1691b1f0c05461e04e950bb27dd0f92e64fc8e28e1", "assetOwnershipSourceSha256": "43005b7e896e0546c1f44cbe4ff3f34ad6d71f275f4b9a8113f73a71e77930c4", "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", "credentialBindingSourceSha256": "d5c200bea2e8ab730eb56e32ce2070c8a1b5407e5907108640f76a463d9b9c9c", "sidecarErrorSourceSha256": "23f3dbdc1ce488c690d218a854c820f4b4f3edd822bad574d67707314f8c512a", "profileSourceSha256": "baf0b64371fada6c92331f9f88e915292b2761c029fd86452f6de32e26dc7fd6", - "metadataProbeSourceSha256": "e8bdefca57b9678d2b5d99b0bc604301d12993b9c9cc4ecb4b5d3df0b83cec3f", + "metadataProbeSourceSha256": "826e74747a89b83a6f546ee1355de64ae5265eb920b50ae498a41dd561a493ee", "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", "runtimeHostSourceSha256": "83a5ae07c19681ab195f76363661c2381494b243343270ba363d4db2088932a5", "runtimeAdapterSourceSha256": "a1a8784f5883d2c93df6e3695d5499b83e226efb4f26e558639c4e7dbcc2d67a",