From 2e8859ab762e5b2ce97f2ea774e385fd333f9cee Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 21:19:34 -0500 Subject: [PATCH] Preserve controller-owned provider pack authority across cold resume --- .../src/live/runnerd-codex-transport.test.ts | 10 ++++++++ .../src/live/runnerd-codex-transport.ts | 1 + .../remote-provider-pack-cache.test.ts | 19 +++++++++++++++ .../remote-provider-pack-cache.ts | 24 ++++++++++++++++--- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index 1b00e118cf4..243a5c3c781 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -2285,6 +2285,16 @@ it.each(["missing", "changed"] as const)("reuses the recorded Codex command when expect(JSON.stringify(template)).not.toContain(replacement); expect(resolver).not.toHaveBeenCalled(); expect(launch).not.toHaveBeenCalled(); + const persisted = JSON.parse(await readFile(join(root, "control-plane", "control-plane-state.json"), "utf8")); + expect(persisted.identity).toMatchObject(nextIdentity); + expect(persisted.runAttachTemplate).toEqual(template); + // Later command compaction must not erase the resumed epoch's launch authority. + expect(runnerdRecoveryInternals.rotatedRunAttachPayload( + { ...persisted, commands: [] }, + { ...nextIdentity, runId: "run-after-compaction" }, + null, + undefined, + )).toMatchObject({ provider: { command, model: "gpt-6.1-sol" } }); } finally { resolver.mockRestore(); await transport.close(); diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 4b270e81fd4..c127a1955db 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -5501,6 +5501,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { }); } this.#runAttachTemplate = structuredClone(runAttachTemplate); + core.persistRunAttachTemplate(runAttachTemplate); core.queueCommand("run.attach", runAttachTemplate); } const committedEvents = core.store.state.committedEvents; diff --git a/server/src/services/native-runtime/remote-provider-pack-cache.test.ts b/server/src/services/native-runtime/remote-provider-pack-cache.test.ts index 975d14ffd65..9fad5275291 100644 --- a/server/src/services/native-runtime/remote-provider-pack-cache.test.ts +++ b/server/src/services/native-runtime/remote-provider-pack-cache.test.ts @@ -56,6 +56,25 @@ describe("controller-pinned computer provider pack", () => { expect(selected.digest).toBe(digest); expect(selected.root).not.toBe(computerProviderPackCachePath("/agents/a", `sha256:${"b".repeat(64)}`)); }); + it.each(["pending", "completed", "failed"])("pins the controller-owned %s attachment after a cold epoch rotation", (status) => { + const input = pinned(); + const provider = { ...input.control.runAttachTemplate.provider, runId: input.identity.runId }; + const control = { ...input.control, runAttachTemplate: null, + commands: [{ type: "run.attach", controllerSeq: 1, status, payload: { provider } }] }; + expect(pinnedComputerProviderPack({ ...input, control }).digest).toBe(digest); + }); + it.each(["foreign-run", "foreign-session", "ambiguous", "malformed-template"])("rejects %s command provenance", (kind) => { + const input = pinned(); + const provider = { ...input.control.runAttachTemplate.provider, runId: input.identity.runId }; + const commands = [{ type: "run.attach", controllerSeq: 1, status: "completed", payload: { provider } }]; + if (kind === "foreign-run") provider.runId = "foreign"; + if (kind === "foreign-session") provider.normalizedSessionId = "foreign"; + if (kind === "ambiguous") commands.push({ ...commands[0]!, controllerSeq: 2, + payload: { provider: { ...provider, sidecarCommand: provider.sidecarCommand.replace("a".repeat(64), "b".repeat(64)), + sidecarArgs: provider.sidecarArgs.map(path => path.replace("a".repeat(64), "b".repeat(64))) } } }); + const control = { ...input.control, runAttachTemplate: kind === "malformed-template" ? {} : null, commands }; + expect(() => pinnedComputerProviderPack({ ...input, control })).toThrow("provenance_unavailable"); + }); it.each(["missing", "identity", "foreign-home", "sidecar", "arguments", "provider"])("rejects %s provenance", (kind) => { const input = pinned(); if (kind === "missing") delete (input.control as { runAttachTemplate?: unknown }).runAttachTemplate; diff --git a/server/src/services/native-runtime/remote-provider-pack-cache.ts b/server/src/services/native-runtime/remote-provider-pack-cache.ts index 1502d74c8f0..2fd696a305b 100644 --- a/server/src/services/native-runtime/remote-provider-pack-cache.ts +++ b/server/src/services/native-runtime/remote-provider-pack-cache.ts @@ -12,12 +12,30 @@ export function pinnedComputerProviderPack(input: { value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; const control = object(input.control); const identity = object(control.identity); - const provider = object(object(control.runAttachTemplate).provider); const fail = (): never => { throw new Error("runner_remote_provider_pack_provenance_unavailable"); }; if (control.schema !== "paperclip.runner.durable.control-plane-state.v1" || ["runId", "normalizedSessionId", "runnerInstanceId", "environmentLeaseId"].some(key => - typeof input.identity[key] !== "string" || !input.identity[key] || identity[key] !== input.identity[key]) || - provider.kind !== "acpx" || provider.provider !== "acpx" || provider.driver !== "acpx_runtime" || + typeof input.identity[key] !== "string" || !input.identity[key] || identity[key] !== input.identity[key])) return fail(); + let provider = object(object(control.runAttachTemplate).provider); + if (control.runAttachTemplate === undefined || control.runAttachTemplate === null) { + // Cold epoch rotation can retain the controller-authored run.attach command + // without also persisting its attachment template. A failed + // command still records authorized launch paths; it is not settlement proof. + const candidates = (Array.isArray(control.commands) ? control.commands : []).map(object) + .filter(command => ["run.prepare", "run.attach"].includes(String(command.type)) && + Number.isSafeInteger(command.controllerSeq) && Number(command.controllerSeq) > 0 && + ["pending", "completed", "failed"].includes(String(command.status))) + .map(command => object(object(command.payload).provider)) + .filter(candidate => candidate.runId === identity.runId && candidate.normalizedSessionId === identity.normalizedSessionId); + if (!candidates.length) return fail(); + const authority = (candidate: Record) => JSON.stringify([ + candidate.kind, candidate.provider, candidate.driver, candidate.agent, candidate.model, + candidate.normalizedSessionId, candidate.sidecarCommand, candidate.sidecarArgs, + ]); + if (candidates.some(candidate => authority(candidate) !== authority(candidates[0]!))) return fail(); + provider = candidates[0]!; + } + if (provider.kind !== "acpx" || provider.provider !== "acpx" || provider.driver !== "acpx_runtime" || provider.agent !== input.provider.agent || provider.model !== input.provider.model || provider.normalizedSessionId !== identity.normalizedSessionId || typeof provider.sidecarCommand !== "string" || !Array.isArray(provider.sidecarArgs) || provider.sidecarArgs.length !== 1) return fail();