From 7a8df3095365da84b6871933fc20a29316fa8834 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:53:58 -0500 Subject: [PATCH] fix(runner): admit Grok effort through native session metadata (#17942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip lets people manage AI agents for work. > - Paperclip Runner must preserve each agent's model and thinking effort. > - The Grok migration in #17922 passes explicit effort to native Runner. > - The pinned Grok 1.0.13 CLI reports native session metadata instead of standard ACP config options. > - The standard effort guard rejects that valid session before the first prompt. > - This pull request binds model and effort on Grok session open and verifies the native reply. > - Restored sessions keep the selected effort instead of reverting to Grok's default. ## Linked Issues or Issue Description Related: https://github.com/paperclipai/paperclip/pull/17922 **What happened?** A migrated Grok 4.7 agent with High effort fails at native session.open before it can run a tool. Grok 1.0.13 has no standard configOptions response and rejects session/set_config_option. Its native session metadata supports the requested model and effort. A cold load also resets effort to High unless the request supplies it again. The upgrade banner also calls Grok “Codex”. **Expected behavior** The native session must acknowledge the selected model and effort before a prompt. A cold restore must preserve both values. The upgrade banner must describe the selected harness accurately. **Steps to reproduce** 1. Upgrade a Grok agent to Paperclip Runner with explicit High effort. 2. Assign a bounded shell task. The pre-fix native session fails during admission. 3. For the restore case, create a Low native session, close the process, and load it without effort metadata. The pinned CLI reports High. **Paperclip version or commit** Observed at f60ac29cf116015deb64eb8a1f24d02f6f35fe56 with Grok 1.0.13. **Deployment mode** Cloud, with the board operator interface. ## What Changed - Project the bound model and effort into native Grok session/new, session/load, and session/resume metadata before transport writes and observation. - Require a correlated native acknowledgement with the exact model, effort, and session identity. Reject conflicting metadata, selection drift, stale connections, and prompts before admission. - Connect lazy restored handles through read-only native session inspection. Keep the existing process ownership and launch snapshot checks. - Replace Grok's unsupported standard config setter. Keep the existing Claude effort path. - Add behavioral guard cases and a real ACPX client fixture for fresh and cold Low/High sessions. The fixture resets effort on restore if metadata is absent, as the pinned CLI does. - Publish Copilot profile v69 because its source identity includes the shared runtime adapter. Preserve published v66/v67/v68 bytes and the reviewed Grok v67 candidate as history. Keep old profiles closed for launch. - Explain the native protocol in the Grok Runner guide and use provider-independent upgrade banner text. ## Verification - Three native create/load/resume cases failed before the repair. The pre-fix guard rejected the native metadata reply. - Seven affected Runner test files: 234 tests passed after the master merge. These include native Grok admission, the real ACPX transport fixture, runtime ownership, Claude effort, and profile history. - The review follow-up extends the same Low/High fixture through a complete first turn after cold restore. It checks the repeated native load, original session id, actual response event, and completed turn. All 127 tests in the three affected Grok/adapter files passed again; Runner typecheck and source identity checks also passed. - All 167 existing agent form tests passed. Token gates and diff checks passed. - Workspace typecheck passed. Final Runner typecheck and generated profile checks passed. - Production `pnpm build` passed, including after the master conflict resolution. Runner typecheck, profile history checks, and the 234 affected tests passed on the merged source. All 55 Rust ACPX library tests also passed on the merged source. - Real pinned Grok 1.0.13 metadata probes reproduced the unsupported standard setter and native create/load/resume behavior. No model prompt was sent. An earlier actual ACPX runtime probe passed fresh and cold Low/High metadata checks. A repeat with an explicitly isolated HOME failed authentication before session creation, so it does not count as final live acceptance. The deterministic transport fixture is green; a deployed shell task remains required. - `pnpm test:run` did not complete locally. Embedded Postgres failed during bootstrap after five attempts. This is the same local fixture limitation recorded for #17922. Fresh PR CI must pass before merge. - After deployment, retry the original bounded shell task with the saved Grok 4.7/High configuration. Verify actual tool output, a successful run, and Done state after reload. - Final commit `810d84fd19728e7d798989582f26b05f69166db2`: all CI gates passed. Greptile and the formal Paperclip review both reached 5/5. No unresolved review threads remain. ## Risks - This depends on the pinned Grok 1.0.13 native metadata format. Missing or conflicting replies stop admission instead of silently changing effort. - Cold restores must make one read-only native inspection call before admission. The existing handshake timeout and process ownership checks still apply. - The shared adapter changes Copilot's source identity. The new current profile includes the final source hashes; the prior profile remains decodable but cannot launch. - This does not change the Grok CLI version, account selection, search policy, dependency patch, or lockfile. ## Model Used OpenAI Codex, GPT-6. The exact serving model ID and context window are not exposed in this session. The agent used reasoning, code editing, shell tools, and browser tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- doc/grok-native-runner.md | 12 +- packages/paperclip-runner/acpx-profiles.json | 4 +- .../src/generated_acpx_profiles.rs | 2 +- .../scripts/copilot-profile-identity.mjs | 4 +- .../acpx/codex-runtime-adapter.test.ts | 36 +++-- .../src/drivers/acpx/codex-runtime-adapter.ts | 12 +- .../drivers/acpx/copilot-installation.test.ts | 22 ++- .../src/drivers/acpx/generated-profiles.ts | 4 +- .../src/drivers/acpx/grok-effort.test.ts | 133 ++++++++++++------ .../src/drivers/acpx/grok-effort.ts | 86 ++++++++++- .../acpx/grok-native-session-options.test.ts | 89 ++++++++++++ .../src/drivers/acpx/profile-compatibility.ts | 4 +- ...-effort-candidate-historical-identity.json | 85 +++++++++++ .../copilot-profile-v69-identity.json | 85 +++++++++++ ui/src/components/AgentConfigForm.tsx | 2 +- 15 files changed, 499 insertions(+), 81 deletions(-) create mode 100644 packages/paperclip-runner/src/drivers/acpx/grok-native-session-options.test.ts create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v67-grok-native-effort-candidate-historical-identity.json create mode 100644 packages/paperclip-runner/test/fixtures/copilot-profile-v69-identity.json 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" ? (