mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-18 05:23:39 +02:00
Preserve controller-owned provider pack authority across cold resume
This commit is contained in:
1 parent
5575b36a8d
commit
2e8859ab76
4 files changed
+51
-3
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,12 +12,30 @@ export function pinnedComputerProviderPack(input: {
|
||||
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
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<string, unknown>) => 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();
|
||||
|
||||
Reference in new issue
Block a user