diff --git a/doc/grok-native-runner.md b/doc/grok-native-runner.md index e9a820d1be4..851c60d23c4 100644 --- a/doc/grok-native-runner.md +++ b/doc/grok-native-runner.md @@ -17,9 +17,15 @@ Grok Build's qualified harness does not require the `enableNativeRunner` flag. Explicit thinking effort uses `reasoningEffort` (`minimal`, `low`, `medium`, `high`, or `xhigh`). The immutable v10 execution input, provider descriptor, -and recovery identity bind this value as `grokEffort`. Before a prompt, the -Runner applies ACP `reasoning_effort` and requires Grok to acknowledge the exact -value. Missing configuration, a rejected value, or effort drift stops admission. +and recovery identity bind this value as `grokEffort`. The Grok provider boundary +adds native `_meta.modelId` and `_meta.reasoningEffort` to session create, load, +and resume requests before transport writes and observation. Grok 1.0.13 reports +the selected effort in `_meta["x.ai/sessionConfig"].options`; it does not support +the standard `session/set_config_option` effort API. The Runner checks that native +acknowledgement and the effective model before a prompt. A lazy restored handle +must connect through read-only native session inspection first. Reapplying the +bound metadata on restore prevents Grok from resetting effort to its default. +Missing configuration, a rejected value, or effort drift stops admission. Omitting effort retains Grok's provider default. This setting remains outside the model-visible task prompt. Historical execution inputs retain their schemas. diff --git a/packages/paperclip-runner/acpx-profiles.json b/packages/paperclip-runner/acpx-profiles.json index 0e5443132cc..c96818e7445 100644 --- a/packages/paperclip-runner/acpx-profiles.json +++ b/packages/paperclip-runner/acpx-profiles.json @@ -32,12 +32,12 @@ "requiresProviderPolicy": true }, "copilot": { - "agentProfileVersion": 68, + "agentProfileVersion": 69, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:1ce8bb0e00df81fa9eb71cff5063ce769637f5db7b3aa0ada9ade651db089c63", + "commandDigest": "sha256:5444547ca83543bc899ae5a8a2e895c19a86559339e60a1970786ce900742719", "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 5e9a354c50c..f99ff39b7a0 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:1ce8bb0e00df81fa9eb71cff5063ce769637f5db7b3aa0ada9ade651db089c63", + "sha256:5444547ca83543bc899ae5a8a2e895c19a86559339e60a1970786ce900742719", 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 c7d9b1143b7..21737c1085a 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-v68-identity.json"); +const fixture = resolve(root, "test/fixtures/copilot-profile-v69-identity.json"); const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8")); -const declaration = { ...historical.declaration, agentProfileVersion: 68 }; +const declaration = { ...historical.declaration, agentProfileVersion: 69 }; delete declaration.permissionIdentitySourceSha256; const sources = { displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts", diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts index 1f77dc18833..76f0d7c914e 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts @@ -69,36 +69,44 @@ describe("Codex ACPX runtime adapter", () => { await port.close({ reason: "test complete" }); }); it.each([false, true])("acknowledges configured Grok effort before admission (restoring=%s)", async restoring => { - const runtime = fakeRuntime(); + const runtime = Object.assign(fakeRuntime(), { requestExtension: vi.fn(async () => ({})) }); const child = fakeChild(); const options = openOptions({ spawn: () => child, close: async () => undefined }); - options.profile = { ...options.profile, agent: "grok" }; + options.profile = resolveQualifiedAcpxProfile("grok", "grok-4.7"); options.grokEffort = "high"; options.restoringSession = restoring; options.refreshConsumedCommand = vi.fn(async () => undefined); let created!: AcpRuntimeOptions; const spawnProvider = () => created.spawnAgent!({ command: "ignored", args: [], options: {} }); - vi.mocked(runtime.ensureSession).mockImplementation(async () => { - if (!restoring) spawnProvider(); - return HANDLE; - }); - vi.mocked(runtime.setConfigOption).mockImplementation(async input => { - if (restoring) spawnProvider(); + const acknowledge = () => { const guard = created.protocolGuardFactory!(); guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } }); - guard("inbound", { id: 0, result: { configOptions: [{ id: "reasoning_effort", currentValue: "default" }] } }); - guard("outbound", { id: 1, method: "session/set_config_option", params: { sessionId: "backend-1", configId: input.key, value: input.value } }); - guard("inbound", { id: 1, result: { configOptions: [{ id: "reasoning_effort", currentValue: input.value }] } }); + guard("inbound", { id: 0, result: { models: { currentModelId: "grok-4.7" }, + _meta: { "x.ai/sessionConfig": { options: [{ id: "high", category: "mode", selected: true }] } } } }); + }; + vi.mocked(runtime.ensureSession).mockImplementation(async input => { + expect(input.sessionOptions).toMatchObject({ model: "grok-4.7" }); + if (!restoring) { spawnProvider(); acknowledge(); } + return HANDLE; + }); + runtime.requestExtension.mockImplementation(async () => { + spawnProvider(); acknowledge(); return {}; }); const port = await openCodexAcpxRuntime(options, { createRegistry: () => registry(), createStore: () => store(), createRuntime: value => { created = value; return runtime; }, awaitProviderOwnership: providerOwnershipEstablished, awaitProviderExit: providerOwnershipEstablished, }); - expect(runtime.setConfigOption).toHaveBeenCalledExactlyOnceWith({ handle: HANDLE, key: "reasoning_effort", value: "high" }); + expect(runtime.setConfigOption).not.toHaveBeenCalled(); expect(await port.identity()).toMatchObject({ grokEffort: "high" }); - if (restoring) expect(options.refreshConsumedCommand).toHaveBeenCalledOnce(); - else expect(options.refreshConsumedCommand).not.toHaveBeenCalled(); + if (restoring) { + expect(runtime.requestExtension).toHaveBeenCalledExactlyOnceWith({ handle: HANDLE, method: "_x.ai/session/info", + params: { sessionId: "backend-1" }, sessionMode: "persistent" }); + expect(options.refreshConsumedCommand).toHaveBeenCalledOnce(); + } else { + expect(runtime.requestExtension).not.toHaveBeenCalled(); + expect(options.refreshConsumedCommand).not.toHaveBeenCalled(); + } expect(runtime.startTurn).not.toHaveBeenCalled(); await port.close({ reason: "test complete" }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index b8d804d7ee1..ac4fb23f740 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -323,7 +323,7 @@ export async function openQualifiedAcpxRuntime( const selectedGrokEffort = resolveGrokEffort(options.profile.agent, options.grokEffort); const grokDisableWebSearch = resolveGrokDisableWebSearch(options.profile.agent, options.grokDisableWebSearch); const effortAdmission = selectedClaudeEffort ? createClaudeEffortAdmission(selectedClaudeEffort) - : selectedGrokEffort ? createGrokEffortAdmission(selectedGrokEffort) : undefined; + : selectedGrokEffort ? createGrokEffortAdmission(selectedGrokEffort, options.profile.reportedModelId) : undefined; const cursorInstructions = options.profile.agent === "cursor" ? createCursorInstructionAdmission(options.systemInstructions) : null; @@ -565,9 +565,15 @@ export async function openQualifiedAcpxRuntime( handle = ensuredHandle; options.signal?.throwIfAborted(); if (!effortAdmission.isReady()) { - if (!runtime.setConfigOption) throw new Error("Effort admission requires native configuration"); const spawnsBeforeControl = commandLaunches.count; - await runtime.setConfigOption({ handle: ensuredHandle, key: selectedGrokEffort ? "reasoning_effort" : "effort", value: (selectedGrokEffort ?? selectedClaudeEffort)! }); + if (selectedGrokEffort) { + if (!runtime.requestExtension) throw new Error("Grok admission requires native session inspection"); + await runtime.requestExtension({ handle: ensuredHandle, method: "_x.ai/session/info", + params: { sessionId: requireIdentity(ensuredHandle).backendSessionId }, sessionMode: "persistent" }); + } else { + if (!runtime.setConfigOption) throw new Error("Effort admission requires native configuration"); + await runtime.setConfigOption({ handle: ensuredHandle, key: "effort", value: selectedClaudeEffort! }); + } effortAdmission.assertReady(); await children.verifyLifetimeOwnership(); options.signal?.throwIfAborted(); 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 35d3225b5f5..4436f17831c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts @@ -87,9 +87,27 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi describe("Copilot build-owned installation", () => { it("admits the current profile with environment launch preparation", () => { - expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(68); + expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(69); expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined(); - expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:1ce8bb0e00df81fa9eb71cff5063ce769637f5db7b3aa0ada9ade651db089c63"); + expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:5444547ca83543bc899ae5a8a2e895c19a86559339e60a1970786ce900742719"); + }); + + it("preserves published and candidate history while binding native Grok admission in v69", async () => { + const published = readFileSync(new URL("../../../test/fixtures/copilot-profile-v68-identity.json", import.meta.url)); + const candidate = readFileSync(new URL("../../../test/fixtures/copilot-profile-v67-grok-native-effort-candidate-historical-identity.json", import.meta.url)); + expect(createHash("sha256").update(published).digest("hex")).toBe("9692e8de4e75682c74f8e635e26b8de1f03f1d3e41966dafbb7314d35b75c3cf"); + expect(createHash("sha256").update(candidate).digest("hex")).toBe("0a20ffe33c92d86f58013582b19c2371f96d93e7e4e0e8096fbf0e7236bd02f4"); + const prior = JSON.parse(published.toString("utf8")); + const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v69-identity.json", import.meta.url), "utf8")); + expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([ + "agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeAdapterSourceSha256", + ]); + expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort()); + for (const history of [prior, JSON.parse(candidate.toString("utf8"))]) { + expect(isSupportedAcpxProfileVersion("copilot", history.declaration.agentProfileVersion)).toBe(true); + await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, + agentProfileVersion: history.declaration.agentProfileVersion, commandDigest: history.commandDigest })).rejects.toThrow("exact pinned"); + } }); it("preserves both lineages while binding the merged implementation in v68", async () => { diff --git a/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts index 7b94907809f..9fcf232a819 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": 68, + "agentProfileVersion": 69, "agentServerPackage": "@github/copilot", "agentServerVersion": "1.0.88", "agentRuntimePackage": null, "agentRuntimeVersion": null, - "commandDigest": "sha256:1ce8bb0e00df81fa9eb71cff5063ce769637f5db7b3aa0ada9ade651db089c63", + "commandDigest": "sha256:5444547ca83543bc899ae5a8a2e895c19a86559339e60a1970786ce900742719", "permissionPolicy": "interactive" }, "claude": { diff --git a/packages/paperclip-runner/src/drivers/acpx/grok-effort.test.ts b/packages/paperclip-runner/src/drivers/acpx/grok-effort.test.ts index 98308f3c6e1..f51bd127bd7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/grok-effort.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/grok-effort.test.ts @@ -1,19 +1,37 @@ import { describe, expect, it } from "vitest"; import { admittedGrokEffort, createGrokEffortAdmission, resolveGrokEffort } from "./grok-effort.js"; -const response = (value: string, sessionId?: string) => ({ ...(sessionId ? { sessionId } : {}), configOptions: [{ id: "reasoning_effort", currentValue: value }] }); -function loaded(method = "session/new") { - const admission = createGrokEffortAdmission("high"); +function response(value: string, sessionId?: string) { + return { ...(sessionId ? { sessionId } : {}), models: { currentModelId: "grok-4.7" }, + _meta: { "x.ai/sessionConfig": { options: [ + { id: "grok-4.7", category: "model", selected: true }, + { id: value, category: "mode", selected: true }, + ] } } }; +} +function loaded(method = "session/new", expected: "low" | "high" = "high") { + const admission = createGrokEffortAdmission(expected, "grok-4.7"); const guard = admission.createGuard(); - guard("outbound", { id: 1, method, params: method === "session/new" ? {} : { sessionId: "s" } }); - guard("inbound", { id: 1, result: response("medium", method === "session/new" ? "s" : undefined) }); + guard("outbound", { id: 1, method, params: { sessionId: "s" } }); + guard("inbound", { id: 1, result: response(expected, method === "session/new" ? "s" : undefined) }); return { admission, guard }; } -function select(guard: ReturnType["createGuard"]>, acknowledged = "high") { - guard("outbound", { id: 2, method: "session/set_config_option", params: { sessionId: "s", configId: "reasoning_effort", value: "high" } }); - guard("inbound", { id: 2, result: response(acknowledged) }); -} + describe("Grok effort admission", () => { + it.each(["session/new", "session/load", "session/resume"])("admits the pinned Grok native metadata after %s", method => { + const { admission, guard } = loaded(method); + expect(admission.isReady()).toBe(true); + expect(() => guard("outbound", { id: 2, method: "session/prompt", params: { sessionId: "s" } })).not.toThrow(); + }); + it.each(["session/new", "session/load", "session/resume"])("binds native model and effort without replacing instructions on %s", method => { + const admission = createGrokEffortAdmission("low", "grok-4.7"), guard = admission.createGuard(); + const request = { id: 1, method, params: { sessionId: "s", _meta: { rules: "Bound task instructions" } } }; + guard("outbound", request); + expect(request.params._meta).toEqual({ rules: "Bound task instructions", modelId: "grok-4.7", reasoningEffort: "low" }); + }); + it.each([{ modelId: "other-model" }, { reasoningEffort: "high" }])("rejects conflicting native request configuration", meta => { + const admission = createGrokEffortAdmission("low", "grok-4.7"), guard = admission.createGuard(); + expect(() => guard("outbound", { id: 1, method: "session/new", params: { _meta: meta } })).toThrow("request conflicts"); + }); it.each(["minimal", "low", "medium", "high", "xhigh"] as const)("accepts exact %s without aliases", value => { expect(resolveGrokEffort("grok", value)).toBe(value); expect(admittedGrokEffort("grok", value, { grokEffort: value }, { grokEffort: value })).toBe(value); @@ -23,45 +41,72 @@ describe("Grok effort admission", () => { expect(() => resolveGrokEffort("pi", "high")).toThrow("only supported"); expect(() => admittedGrokEffort("grok", "high", { grokEffort: "high" }, { grokEffort: "medium" })).toThrow("differs"); }); - it.each(["session/new", "session/load", "session/resume"])("requires acknowledged effort before prompting after %s", method => { - const { admission, guard } = loaded(method); - expect(admission.isReady()).toBe(false); - select(guard); - expect(admission.isReady()).toBe(true); - expect(() => guard("outbound", { id: 3, method: "session/prompt", params: { sessionId: "s" } })).not.toThrow(); - }); - - it("rejects a resumed session with missing effort configuration", () => { - const admission = createGrokEffortAdmission("high"); - const guard = admission.createGuard(); - guard("outbound", { id: 1, method: "session/resume", params: { sessionId: "s" } }); - expect(() => guard("inbound", { id: 1, result: {} })).toThrow("missing or ambiguous"); - }); - it("blocks a prompt based only on a restored default", () => { - const { guard } = loaded("session/load"); - expect(() => guard("outbound", { id: 3, method: "session/prompt", params: { sessionId: "s" } })).toThrow("does not acknowledge"); - }); - - it("requires the bound effort acknowledgement on each cold connection", () => { + it("requires the native acknowledgement on each cold connection", () => { const { admission, guard } = loaded(); - select(guard); + const cold = admission.createGuard(); + expect(() => guard("inbound", { id: 1, result: response("high", "s") })).toThrow("authority was replaced"); + expect(() => cold("outbound", { id: 2, method: "session/prompt", params: { sessionId: "s" } })).toThrow("does not acknowledge"); const resumed = admission.createGuard(); - resumed("outbound", { id: 4, method: "session/load", params: { sessionId: "s" } }); - expect(() => resumed("outbound", { id: 5, method: "session/prompt", params: { sessionId: "s" } })).toThrow("does not acknowledge"); - const fresh = loaded(); select(fresh.guard); - const acknowledged = fresh.admission.createGuard(); - acknowledged("outbound", { id: 4, method: "session/load", params: { sessionId: "s" } }); - acknowledged("inbound", { id: 4, result: response("high") }); - expect(() => acknowledged("outbound", { id: 5, method: "session/prompt", params: { sessionId: "s" } })).not.toThrow(); + resumed("outbound", { id: 3, method: "session/load", params: { sessionId: "s" } }); + resumed("inbound", { id: 3, result: response("high") }); + expect(() => resumed("outbound", { id: 4, method: "session/prompt", params: { sessionId: "s" } })).not.toThrow(); }); - it("rejects a provider that ignores the requested effort", () => { + it("rejects a restored provider default that differs from the bound effort", () => { + const admission = createGrokEffortAdmission("low", "grok-4.7"); + const guard = admission.createGuard(); + guard("outbound", { id: 1, method: "session/load", params: { sessionId: "s" } }); + expect(() => guard("inbound", { id: 1, result: response("high") })).toThrow("differs"); + expect(() => admission.assertReady()).toThrow("differs"); + }); + it.each([{}, { configOptions: [{ id: "reasoning_effort", currentValue: "high" }] }, + { _meta: { "x.ai/sessionConfig": { options: [{ id: "high", category: "mode", selected: false }] } } }])("rejects missing native effort acknowledgement", result => { + const admission = createGrokEffortAdmission("high", "grok-4.7"), guard = admission.createGuard(); + guard("outbound", { id: 1, method: "session/load", params: { sessionId: "s" } }); + expect(() => guard("inbound", { id: 1, result })).toThrow("missing or ambiguous"); + }); + it("rejects ambiguous native effort selections", () => { + const admission = createGrokEffortAdmission("high", "grok-4.7"), guard = admission.createGuard(); + const result = response("high", "s"); + result._meta["x.ai/sessionConfig"].options.push({ id: "low", category: "mode", selected: true }); + guard("outbound", { id: 1, method: "session/new", params: {} }); + expect(() => guard("inbound", { id: 1, result })).toThrow("missing or ambiguous"); + }); + it("rejects an effort acknowledgement for a different effective model", () => { + const admission = createGrokEffortAdmission("high", "grok-4.7"), guard = admission.createGuard(); + const result = response("high", "s"); result.models.currentModelId = "grok-4.6"; + guard("outbound", { id: 1, method: "session/new", params: {} }); + expect(() => guard("inbound", { id: 1, result })).toThrow("model acknowledgement conflicts"); + }); + it("does not admit an uncorrelated or failed response", () => { + const admission = createGrokEffortAdmission("high", "grok-4.7"), guard = admission.createGuard(); + guard("outbound", { id: 1, method: "session/new", params: {} }); + guard("inbound", { id: 2, result: response("high", "s") }); + expect(admission.isReady()).toBe(false); + guard("inbound", { id: 1, error: { code: -32603 } }); + expect(admission.isReady()).toBe(false); + }); + it.each(["response", "detail"])("rejects a conflicting native %s session identity", field => { + const admission = createGrokEffortAdmission("high", "grok-4.7"), guard = admission.createGuard(); + const result = { ...response("high"), ...(field === "response" ? { sessionId: "other" } : {}), + _meta: { ...response("high")._meta, ...(field === "detail" ? { "x.ai/sessionDetail": { sessionId: "other" } } : {}) } }; + guard("outbound", { id: 1, method: "session/load", params: { sessionId: "s" } }); + expect(() => guard("inbound", { id: 1, result })).toThrow("different session"); + }); + it.each(["session/set_config_option", "session/set_model"])("rejects unsupported late %s controls", method => { const { guard } = loaded(); - expect(() => select(guard, "medium")).toThrow("did not apply"); + expect(() => guard("outbound", { id: 2, method, params: { sessionId: "s" } })).toThrow("when the native session opens"); }); - it("rejects drift and wrong-session prompts after admission", () => { - const { guard } = loaded(); select(guard); - expect(() => guard("inbound", { method: "session/update", params: { sessionId: "s", update: { sessionUpdate: "config_option_update", ...response("low") } } })).toThrow("drifted"); - const fresh = loaded(); select(fresh.guard); - expect(() => fresh.guard("outbound", { id: 3, method: "session/prompt", params: { sessionId: "foreign" } })).toThrow("different session"); + it("rejects a prompt for another session", () => { + const { guard } = loaded(); + expect(() => guard("outbound", { id: 2, method: "session/prompt", params: { sessionId: "other" } })).toThrow("different session"); + }); + it.each([ + { sessionId: "s", model_id: "grok-4.7", reasoning_effort: "low" }, + { sessionId: "other", model_id: "grok-4.7", reasoning_effort: "high" }, + { sessionId: "s", model_id: "grok-4.6", reasoning_effort: "high" }, + ])("rejects native model or effort drift", update => { + const { guard } = loaded(); + expect(() => guard("inbound", { method: "_x.ai/session_notification", params: { sessionId: update.sessionId, + update: { sessionUpdate: "model_changed", model_id: update.model_id, reasoning_effort: update.reasoning_effort } } })).toThrow("drifted"); }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/grok-effort.ts b/packages/paperclip-runner/src/drivers/acpx/grok-effort.ts index 19118a0472a..8dd25d3f123 100644 --- a/packages/paperclip-runner/src/drivers/acpx/grok-effort.ts +++ b/packages/paperclip-runner/src/drivers/acpx/grok-effort.ts @@ -1,5 +1,3 @@ -import { createAcpxConfigAdmission } from "./config-admission.js"; - export type GrokEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; export function resolveGrokEffort(agent: string, value: unknown): GrokEffort | undefined { if (value === undefined || value === "") return undefined; @@ -7,9 +5,83 @@ export function resolveGrokEffort(agent: string, value: unknown): GrokEffort | u if (value !== "minimal" && value !== "low" && value !== "medium" && value !== "high" && value !== "xhigh") throw new Error("Grok effort must be minimal, low, medium, high, or xhigh"); return value; } -export function createGrokEffortAdmission(expected: GrokEffort) { - return createAcpxConfigAdmission(expected, { label: "Grok effort", configId: "reasoning_effort", - supportedValues: ["default", "minimal", "low", "medium", "high", "xhigh"] }); +export function createGrokEffortAdmission(expected: GrokEffort, expectedModel: string) { + type State = { sessionId: string | null; ready: boolean; error: Error | null }; + let current: State | null = null; + const failure = (detail: string) => new Error(`Grok effort admission failed: ${detail}`); + const assertReady = () => { + if (current?.error) throw current.error; + if (!current?.sessionId || !current.ready) throw failure("native session does not acknowledge the selected effort"); + }; + const readAcknowledgement = (result: Record) => { + const config = object(object(result._meta)["x.ai/sessionConfig"]); + const options = Array.isArray(config.options) ? config.options.map(object) : []; + const selected = options.filter(option => option.category === "mode" && option.selected === true); + if (selected.length !== 1) throw failure("native effort selection is missing or ambiguous"); + if (selected[0]!.id !== expected) throw failure("native effort differs from the selected effort"); + const model = object(result.models).currentModelId; + const selectedModels = options.filter(option => option.category === "model" && option.selected === true); + if (typeof model !== "string" || !model.trim() || model !== expectedModel + || selectedModels.length > 1 || selectedModels.length === 1 && selectedModels[0]!.id !== model) { + throw failure("native model acknowledgement conflicts with the selected model"); + } + }; + return { + assertReady, + isReady() { if (current?.error) throw current.error; return current?.ready === true; }, + createGuard() { + const state: State = { sessionId: null, ready: false, error: null }; + current = state; + let pending: { id: string | number; method: string; sessionId: unknown } | null = null; + return (direction: "inbound" | "outbound", value: unknown): void => { + if (current !== state) throw failure("connection authority was replaced"); + if (state.error) throw state.error; + const message = object(value), params = object(message.params); + try { + if (direction === "outbound") { + if (message.method === "session/prompt") { + assertReady(); + if (params.sessionId !== state.sessionId) throw failure("prompt belongs to a different session"); + } else if (message.method === "session/new" || message.method === "session/load" || message.method === "session/resume") { + if (typeof message.id !== "string" && typeof message.id !== "number" || pending) throw failure("uncorrelated or overlapping session admission"); + const meta = object(params._meta); + if (meta.reasoningEffort !== undefined && meta.reasoningEffort !== expected + || meta.modelId !== undefined && meta.modelId !== expectedModel) { + throw failure("native session request conflicts with the bound configuration"); + } + params._meta = { ...meta, modelId: expectedModel, reasoningEffort: expected }; + state.ready = false; state.sessionId = null; + pending = { id: message.id, method: message.method, sessionId: params.sessionId }; + } else if (message.method === "session/set_config_option" || message.method === "session/set_model") { + throw failure("model and effort must be selected when the native session opens"); + } + return; + } + if (message.method === "_x.ai/session_notification" && object(params.update).sessionUpdate === "model_changed") { + if (!state.ready) return; + const update = object(params.update); + if (params.sessionId !== state.sessionId || update.reasoning_effort !== expected + || update.model_id !== expectedModel) throw failure("native model or effort drifted from the selected configuration"); + return; + } + if (message.method !== undefined || !pending || message.id !== pending.id) return; + const request = pending; pending = null; + if (message.error !== undefined) return; + const result = object(message.result); + const sessionId = request.method === "session/new" ? result.sessionId : request.sessionId; + if (typeof sessionId !== "string" || !sessionId.trim()) throw failure("native acknowledgement has no session identity"); + const detail = object(object(result._meta)["x.ai/sessionDetail"]); + if (result.sessionId !== undefined && result.sessionId !== sessionId + || detail.sessionId !== undefined && detail.sessionId !== sessionId) throw failure("native acknowledgement belongs to a different session"); + readAcknowledgement(result); + state.sessionId = sessionId; state.ready = true; + } catch (error) { + state.error = error instanceof Error ? error : failure("invalid native acknowledgement"); + throw state.error; + } + }; + }, + }; } export function admittedGrokEffort(agent: string, requested: unknown, descriptor: Record, identity: Record): GrokEffort | undefined { const expected = resolveGrokEffort(agent, requested); @@ -17,3 +89,7 @@ export function admittedGrokEffort(agent: string, requested: unknown, descriptor if (effective !== expected || descriptor.grokEffort !== expected) throw new Error("Grok effort differs from the admitted configuration"); return effective; } + +function object(value: unknown): Record { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} diff --git a/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts b/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts new file mode 100644 index 00000000000..6770890cf53 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts @@ -0,0 +1,89 @@ +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createAcpRuntime, createAgentRegistry, createRuntimeStore, type AcpRuntimeHandle } from "acpx/runtime"; +import { expect, it } from "vitest"; +import { createGrokEffortAdmission } from "./grok-effort.js"; + +it.each(["low", "high"] as const)("transports and restores native Grok %s effort through the real ACPX client", async effort => { + const root = await mkdtemp(join(tmpdir(), "grok-native-session-options-")); + const fixture = join(root, "grok"), wirePath = join(root, "wire.jsonl"), statePath = join(root, "native.json"); + await writeFile(fixture, String.raw` + const fs = require("node:fs"), readline = require("node:readline"); + const statePath = ${JSON.stringify(statePath)}, wirePath = ${JSON.stringify(wirePath)}; + const reply = (id, result) => process.stdout.write(JSON.stringify({jsonrpc:"2.0",id,result})+"\n"); + readline.createInterface({input:process.stdin}).on("line", line => { + const request = JSON.parse(line), params = request.params ?? {}; + fs.appendFileSync(wirePath, JSON.stringify({method:request.method,sessionId:params.sessionId,meta:params._meta})+"\n"); + if (request.method === "initialize") return reply(request.id, {protocolVersion:1,agentCapabilities:{loadSession:true},authMethods:[]}); + if (["session/new","session/load","session/resume"].includes(request.method)) { + const previous = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath,"utf8")) : {}; + const model = params._meta?.modelId ?? previous.model; + const effort = params._meta?.reasoningEffort ?? "high"; + fs.writeFileSync(statePath,JSON.stringify({model,effort})); + return reply(request.id, {...(request.method === "session/new" ? {sessionId:"grok-native-session"} : {}), + models:{currentModelId:model,availableModels:[{modelId:model,name:model}]}, + _meta:{"x.ai/sessionConfig":{options:[{id:model,category:"model",selected:true},{id:effort,category:"mode",selected:true}]}}}); + } + if (request.method === "_x.ai/session/info") return reply(request.id, {result:{sessionId:params.sessionId}}); + if (request.method === "session/prompt") { + process.stdout.write(JSON.stringify({jsonrpc:"2.0",method:"session/update",params:{sessionId:params.sessionId, + update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"GROK_RESTORED_TURN_OK"}}}})+"\n"); + return reply(request.id,{stopReason:"end_turn"}); + } + + process.stdout.write(JSON.stringify({jsonrpc:"2.0",id:request.id,error:{code:-32601,message:"Unsupported native method"}})+"\n"); + }); + `); + const store = createRuntimeStore({ stateDir: join(root, "store") }); + const model = "grok-exact-model"; + let admission = createGrokEffortAdmission(effort, model); + const options = { + cwd: root, + sessionStore: store, + agentRegistry: createAgentRegistry({ overrides: { grok: [fixture, "agent", "stdio"] } }), + permissionMode: "approve-reads" as const, + timeoutMs: 5_000, + protocolGuardFactory: () => admission.createGuard(), + spawnAgent: () => spawn(process.execPath, [fixture], { cwd: root, env: {}, stdio: "pipe" }), + }; + const input = { sessionKey: "grok-native-options", agent: "grok", mode: "persistent" as const, + sessionOptions: { model } }; + let runtime = createAcpRuntime(options); + let handle: AcpRuntimeHandle | undefined; + try { + handle = await runtime.ensureSession(input); + admission.assertReady(); + const originalSession = handle.backendSessionId; + await runtime.close({ handle, reason: "cold native restore", discardPersistentState: false }); + admission = createGrokEffortAdmission(effort, model); + runtime = createAcpRuntime(options); + handle = await runtime.ensureSession(input); + expect(handle.backendSessionId).toBe(originalSession); + expect(admission.isReady()).toBe(false); + await runtime.requestExtension({ handle, method: "_x.ai/session/info", params: { sessionId: handle.backendSessionId }, sessionMode: "persistent" }); + admission.assertReady(); + const turn = runtime.startTurn({ handle, text: "Verify the restored fixture", mode: "prompt", requestId: "grok-cold-turn", timeoutMs: 5_000 }); + const output: string[] = []; + const drain = (async () => { + for await (const event of turn.events) if (event.type === "text_delta") output.push(event.text); + })(); + const result = await turn.result; + await drain; + expect(result.status).toBe("completed"); + expect(output.join("")).toBe("GROK_RESTORED_TURN_OK"); + admission.assertReady(); + const wire = (await readFile(wirePath, "utf8")).trim().split("\n").map(line => JSON.parse(line)); + const opens = wire.filter(request => ["session/new", "session/load", "session/resume"].includes(request.method)); + expect(opens.map(request => request.method)).toEqual(["session/new", "session/load", "session/load"]); + for (const request of opens) expect(request.meta).toMatchObject({ modelId: model, reasoningEffort: effort }); + expect(wire.filter(request => request.method === "session/prompt")).toEqual([ + { method: "session/prompt", sessionId: originalSession }, + ]); + expect(wire.some(request => request.method === "session/set_config_option")).toBe(false); + } finally { + if (handle) await runtime.close({ handle, reason: "native metadata test complete", discardPersistentState: false }); + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts b/packages/paperclip-runner/src/drivers/acpx/profile-compatibility.ts index 25804b55c31..6f9990c550f 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 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67; +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 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68; 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, 28, 29], 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, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67], + 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, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68], cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], }; diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v67-grok-native-effort-candidate-historical-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v67-grok-native-effort-candidate-historical-identity.json new file mode 100644 index 00000000000..7805e82d794 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v67-grok-native-effort-candidate-historical-identity.json @@ -0,0 +1,85 @@ +{ + "commandDigest": "sha256:2c0854674e3d00fd00cb0659b5ddb9788f47fb2b11f771901f338cb384926318", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 67, + "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": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "7d6e948b7cff4df52a9afc916db46ace0f42b4b2d3134ba0c21ee0a928857e62", + "semanticDirectDriverSourceSha256": "d683429427dd8a1a5efe50952018ea486f808bdbf0a92fc5946ee267fe2c72ee", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9", + "semanticSchemaBundleSourceSha256": "4cae579aac836762fdbf92c5447425d8b756e87a5766bc0c7a1885a90541c8d6", + "semanticSidecarProtocolSourceSha256": "26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff", + "configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "3e61c4cdd4b78e38fcb16eaadae4f1fd65ce3c7127184ea7a80948c8e5d77f88", + "attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94", + "transportContractSourceSha256": "03fca93d36a89b0cc0917335e293ad097fdf04356d21b307827bb0ea4c13f523", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "4c1abd77bbed9723e669009a78dab6fe024837718ced9f22c159f9e22ce1f26c", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "9584d9801b87f67e60b0007beafa909fc5b05dc6adac71409db1c7f80a2f7916", + "runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "56a177d3e702914818dca4ca50e061f0d25f988bfc4c0582c36ce23c1696d82f", + "nativeExecutionSourceSha256": "d60b8ea63bad975c0144f0702b2749d7db9a7da50680a6147f20d4d11f2d5297", + "profileCompatibilitySourceSha256": "4b0f14e8e2a34e1b6795a5be63265451e36c7583d99a246c5471e01f26f6e170", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "7586d44b47c83395498dc8806287cd3538ef58177ba7c5949f5766ed105a0161", + "sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "runtimeHostSourceSha256": "976fef21ec9f3ae9dd5eabb133a55ae578f1c0729c657c30e42562a80db237d8", + "runtimeAdapterSourceSha256": "7c47c05306ebe7d1ca6b5d046a96f5726a48adec4d27e7c16788f505f25b5db2", + "runtimeSandboxSourceSha256": "981449a4c6d10f682ecf86d834eed07faca30a7d9b419fb8db4973a442a343ec", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/packages/paperclip-runner/test/fixtures/copilot-profile-v69-identity.json b/packages/paperclip-runner/test/fixtures/copilot-profile-v69-identity.json new file mode 100644 index 00000000000..1b84327a954 --- /dev/null +++ b/packages/paperclip-runner/test/fixtures/copilot-profile-v69-identity.json @@ -0,0 +1,85 @@ +{ + "commandDigest": "sha256:5444547ca83543bc899ae5a8a2e895c19a86559339e60a1970786ce900742719", + "declaration": { + "schema": "paperclip.acpx_profile_declaration.v1", + "agent": "copilot", + "agentProfileVersion": 69, + "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": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5", + "toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706", + "semanticNormalizedInputContract": "validated-forwarded-input-commit-v1", + "semanticSidecarSourceSha256": "7d6e948b7cff4df52a9afc916db46ace0f42b4b2d3134ba0c21ee0a928857e62", + "semanticDirectDriverSourceSha256": "d683429427dd8a1a5efe50952018ea486f808bdbf0a92fc5946ee267fe2c72ee", + "semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975", + "semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e", + "semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59", + "semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9", + "semanticSchemaBundleSourceSha256": "4cae579aac836762fdbf92c5447425d8b756e87a5766bc0c7a1885a90541c8d6", + "semanticSidecarProtocolSourceSha256": "26dd0cfbef8b2b401de6b7862a4af7f243fb703e23678a1a5ed8fa68d4784d42", + "semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1", + "displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877", + "displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e", + "configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff", + "configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994", + "profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282", + "profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571", + "profileInstallationSourceSha256": "3e61c4cdd4b78e38fcb16eaadae4f1fd65ce3c7127184ea7a80948c8e5d77f88", + "attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2", + "singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f", + "nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce", + "nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842", + "nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1", + "nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90", + "harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5", + "harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249", + "harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94", + "transportContractSourceSha256": "03fca93d36a89b0cc0917335e293ad097fdf04356d21b307827bb0ea4c13f523", + "nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586", + "nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44", + "nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b", + "providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef", + "launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296", + "runtimeRequestBridgeSourceSha256": "93a631b8439df1590b0cf442cf84a9b05f004f12bfee90291651ccfc0861effe", + "runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295", + "runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211", + "guardedInstallationSourceSha256": "56a177d3e702914818dca4ca50e061f0d25f988bfc4c0582c36ce23c1696d82f", + "nativeExecutionSourceSha256": "d60b8ea63bad975c0144f0702b2749d7db9a7da50680a6147f20d4d11f2d5297", + "profileCompatibilitySourceSha256": "3513d4b0c57a17b125f7d83e3a844be4b88de7475577dac99132aef00cbdca63", + "installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d", + "assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c", + "assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f", + "credentialBindingSourceSha256": "7586d44b47c83395498dc8806287cd3538ef58177ba7c5949f5766ed105a0161", + "sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80", + "profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456", + "metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727", + "permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1", + "semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811", + "runtimeHostSourceSha256": "976fef21ec9f3ae9dd5eabb133a55ae578f1c0729c657c30e42562a80db237d8", + "runtimeAdapterSourceSha256": "7c47c05306ebe7d1ca6b5d046a96f5726a48adec4d27e7c16788f505f25b5db2", + "runtimeSandboxSourceSha256": "981449a4c6d10f682ecf86d834eed07faca30a7d9b419fb8db4973a442a343ec", + "cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca", + "agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba", + "nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b" + } +} diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 4966526432c..56b223ab862 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -1592,7 +1592,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {

Upgrade to Paperclip Runner

- Keep {harnessType === "claude_local" ? "Claude" : "Codex"}, your model, account, and compatible settings. + Keep your harness, model, account, and compatible settings. New sessions start fresh; past runs stay available.

{canQualifyRunnerUpgrade && runnerAvailability?.defaultRunner !== "paperclip" ? (