From 66ac692e446bd2ce0175613787f82eeb048f2749 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 20:44:17 -0500 Subject: [PATCH] fix(boat): retain verified ACPX provider packs across deployments --- .../native-session-executor.test.ts | 8 + .../native-runtime/native-session-executor.ts | 33 ++- .../remote-provider-pack-cache.test.ts | 204 ++++++++++++++++++ .../remote-provider-pack-cache.ts | 88 ++++++++ 4 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 server/src/services/native-runtime/remote-provider-pack-cache.test.ts diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 6c00839442..fd97540e22 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -355,6 +355,7 @@ import { REMOTE_RUNNER_CHILD_LAUNCH_SCRIPT, verifyRemoteRunnerReattachment, readRemoteProviderPackManifest, + validateRemoteProviderPackIdentity, readBundledRemoteProviderPackManifest, providerSessionIdentityFromDurableProviderState, durableProviderCheckpointFailureReason, @@ -1468,6 +1469,13 @@ describe("remote provider pack manifest", () => { }), ); await writeManifest(); + const retainedManifest = readRemoteProviderPackManifest(root); + expect(validateRemoteProviderPackIdentity(retainedManifest).digest).toBe(retainedManifest.digest); + expect(() => validateRemoteProviderPackIdentity({ ...retainedManifest, digest: `sha256:${"0".repeat(64)}` })) + .toThrow("manifest digest mismatch"); + const changedPins = structuredClone(retainedManifest); + changedPins.payload.pins = { ...changedPins.payload.pins, nodeMinimum: "0.0.0" as typeof changedPins.payload.pins.nodeMinimum }; + expect(() => validateRemoteProviderPackIdentity(changedPins)).toThrow("pins or source revision"); expect(readRemoteProviderPackManifest(root).payload.pins.opencode).toBe( "1.18.34", ); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index fe5eb2aac7..07677cb8ee 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -42,7 +42,7 @@ import { nativeCompletionFeedback } from "./native-completion-feedback.js"; import { hasAcknowledgedNativeReassignmentStopIntent, hasAcknowledgedNativeStopIntent } from "../acknowledged-native-stop.js"; import { stoppedCodexTurnIsTextOnly } from "./stopped-codex-turn.js"; import { prepareVerifiedRemoteProviderPack } from "./remote-provider-pack.js"; -import { computerProviderPackCachePath, prepareComputerProviderPackCache } from "./remote-provider-pack-cache.js"; +import { computerProviderPackCachePath, prepareComputerProviderPackCache, pinnedComputerProviderPack, computerAcpxLaunchProfileDigest, readPinnedComputerProviderMetadata, assertPinnedComputerProviderState } from "./remote-provider-pack-cache.js"; import { selectRemotePiCompanion } from "./remote-pi-companion.js"; import { readNativeLocalProcessStop, PROCESS_START_REQUESTED } from "../native-local-process-stop.js"; import { remoteLeaseCleanupScope } from "../remote-execution-termination.js"; @@ -10230,6 +10230,10 @@ function readRemoteProviderPackIdentity(packRoot: string, verifyControllerFiles: { cause: error }, ); } + return validateRemoteProviderPackIdentity(manifest, packRoot, verifyControllerFiles); +} + +export function validateRemoteProviderPackIdentity(manifest: RemoteProviderPackManifest, packRoot = "", verifyControllerFiles = false): RemoteProviderPackManifest { const payload = manifest?.payload; if ( manifest?.schema !== REMOTE_PROVIDER_PACK_SCHEMA || @@ -12032,7 +12036,31 @@ async function createRunnerdBackendWithinSessionClaim( const useBundledRemoteImageAssets = requiresRemoteProviderPack && !configuredProviderPackRoot && (input.execution.provider.kind === "opencode" || (input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor")); - if (useBundledRemoteImageAssets) { + const retainedComputerPack = remoteTarget?.transport === "computer" && + input.execution.provider.kind === "acpx" && durableBinding && durableIdentity && remoteCommandRunner && remoteStorage + ? pinnedComputerProviderPack({ + agentHome: remoteTarget.fileAuthority.agentHome, identity: durableIdentity, + provider: input.execution.provider, + control: JSON.parse(readBoundedNativeFile(resolve(root, "control-plane", "control-plane-state.json"), + NATIVE_CONTROL_PLANE_STATE_MAX_BYTES, "runner_durable_identity_too_large").toString("utf8")), + }) : null; + if (retainedComputerPack && remoteCommandRunner && remoteStorage) { + // The local attachment template chooses the digest. Remote content cannot + // authorize a newer pack or rewrite the existing provider's launch identity. + const [manifest, state] = await readPinnedComputerProviderMetadata({ runner: remoteCommandRunner, + packRoot: retainedComputerPack.root, stateDirectory: posix.join(remoteStorage.sessionRoot, "runner") }); + expectedProviderPackManifest = validateRemoteProviderPackIdentity(manifest as RemoteProviderPackManifest); + if (expectedProviderPackManifest.digest !== retainedComputerPack.digest || + expectedProviderPackManifest.payload.target.platform !== "linux" || + expectedProviderPackManifest.payload.target.architecture !== "x64") + throw new Error("runner_remote_provider_pack_provenance_mismatch"); + const launchDigest = computerAcpxLaunchProfileDigest({ authorityDigest: retainedComputerPack.digest, + command: retainedComputerPack.command, sidecar: retainedComputerPack.sidecar, + commandSha256: expectedProviderPackManifest.payload.artifacts.nodeCommand.sha256, + sidecarSha256: expectedProviderPackManifest.payload.artifacts.acpxSidecar.sha256 }); + assertPinnedComputerProviderState({ state, launchProfileDigest: launchDigest, + normalizedSessionId: nativeSessionKey(input.execution), command: retainedComputerPack.command, sidecar: retainedComputerPack.sidecar }); + } else if (useBundledRemoteImageAssets) { expectedProviderPackManifest = readBundledRemoteProviderPackManifest(); } else if (requiresRemoteProviderPack) { if ( @@ -12608,6 +12636,7 @@ async function createRunnerdBackendWithinSessionClaim( runner: remoteCommandRunner, verify: (root) => measureNativeRunnerSpan(input.trace, "provider_pack.verify", () => verifyRemoteProviderPack(root)), stage: async (root, runner) => { + if (retainedComputerPack) throw new Error("runner_remote_provider_pack_retained_artifacts_unavailable"); if (!configuredProviderPackRoot) { throw new Error("runner_remote_provider_artifact_incompatible: configure the build-owned provider pack for first use on this computer"); } 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 new file mode 100644 index 0000000000..975d14ffd6 --- /dev/null +++ b/server/src/services/native-runtime/remote-provider-pack-cache.test.ts @@ -0,0 +1,204 @@ +import { spawn } from "node:child_process"; +import { promises as fs } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime"; +import { computerProviderPackCachePath, prepareComputerProviderPackCache, pinnedComputerProviderPack, computerAcpxLaunchProfileDigest, readPinnedComputerProviderMetadata, assertPinnedComputerProviderState } from "./remote-provider-pack-cache.js"; + +const roots: string[] = []; +const digest = `sha256:${"a".repeat(64)}`; +describe("controller-pinned computer provider pack", () => { + it("checks the retained provider's session and exact launch profile without rewriting it", () => { + const state = { schema: "paperclip.runner.acpx-provider-state.v3", launchProfileDigest: digest, + descriptor: { normalizedSessionId: "session", sidecarCommand: "/pack/node", sidecarArgs: ["/pack/sidecar"] } }; + const input = { state, normalizedSessionId: "session", launchProfileDigest: digest, command: "/pack/node", sidecar: "/pack/sidecar" }; + expect(() => assertPinnedComputerProviderState(input)).not.toThrow(); + const before = JSON.stringify(state); + expect(() => assertPinnedComputerProviderState({ ...input, normalizedSessionId: "foreign" })).toThrow("provenance_mismatch"); + expect(() => assertPinnedComputerProviderState({ ...input, launchProfileDigest: `sha256:${"b".repeat(64)}` })).toThrow("provenance_mismatch"); + expect(JSON.stringify(state)).toBe(before); + }); + it.each(["valid", "pack-parent-symlink", "state-parent-symlink", "hardlink"])("reads bounded metadata with %s", async (kind) => { + const home = await fs.realpath(await fs.mkdtemp(join(tmpdir(), "provider-provenance-"))); + roots.push(home); + let packRoot = join(home, "pack"); let stateDirectory = join(home, "runner"); + await fs.mkdir(packRoot); await fs.mkdir(stateDirectory); + await fs.writeFile(join(packRoot, "provider-pack.json"), JSON.stringify({ digest })); + await fs.writeFile(join(stateDirectory, "acpx-provider-state.json"), JSON.stringify({ launchProfileDigest: digest })); + if (kind === "pack-parent-symlink") { await fs.symlink(packRoot, join(home, "pack-link")); packRoot = join(home, "pack-link"); } + if (kind === "state-parent-symlink") { await fs.symlink(stateDirectory, join(home, "runner-link")); stateDirectory = join(home, "runner-link"); } + if (kind === "hardlink") await fs.link(join(packRoot, "provider-pack.json"), join(home, "shared.json")); + const result = readPinnedComputerProviderMetadata({ runner, packRoot, stateDirectory }); + if (kind === "valid") await expect(result).resolves.toEqual([{ digest }, { launchProfileDigest: digest }]); + else await expect(result).rejects.toThrow("provenance_unavailable"); + }); + it("matches the runner's length-prefixed launch profile contract", () => { + const profile = { authorityDigest: digest, command: "/pack/node", commandSha256: `sha256:${"b".repeat(64)}`, + sidecar: "/pack/sidecar", sidecarSha256: `sha256:${"c".repeat(64)}` }; + const expected = "sha256:5d91648a1247fac8633317f80d39ad05e47961894bf8b631c47e10d031753233"; + expect(computerAcpxLaunchProfileDigest(profile)).toBe(expected); + for (const key of Object.keys(profile) as Array) { + expect(computerAcpxLaunchProfileDigest({ ...profile, [key]: `${profile[key]}-changed` })).not.toBe(expected); + } + }); + function pinned() { + const identity = { runId: "prior", normalizedSessionId: "session", runnerInstanceId: "runner", environmentLeaseId: "lease" }; + const root = computerProviderPackCachePath("/agents/a", digest); + return { agentHome: "/agents/a", identity, provider: { agent: "claude", model: "model" }, + control: { schema: "paperclip.runner.durable.control-plane-state.v1", identity: { ...identity }, runAttachTemplate: { provider: { + kind: "acpx", provider: "acpx", driver: "acpx_runtime", agent: "claude", model: "model", normalizedSessionId: "session", + sidecarCommand: `${root}/node_modules/node/bin/node`, sidecarArgs: [`${root}/dist/cli/acpx-runtime-sidecar.cjs`], + } } } }; + } + it("retains pack A when the next controller deploys pack B", () => { + const selected = pinnedComputerProviderPack(pinned()); + expect(selected.digest).toBe(digest); + expect(selected.root).not.toBe(computerProviderPackCachePath("/agents/a", `sha256:${"b".repeat(64)}`)); + }); + 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; + if (kind === "identity") input.control.identity.environmentLeaseId = "foreign"; + if (kind === "foreign-home") input.agentHome = "/agents/b"; + if (kind === "sidecar") input.control.runAttachTemplate.provider.sidecarArgs[0] += "/../evil"; + if (kind === "arguments") input.control.runAttachTemplate.provider.sidecarArgs.push("--evil"); + if (kind === "provider") input.provider.agent = "codex"; + expect(() => pinnedComputerProviderPack(input)).toThrow("runner_remote_provider_pack_provenance_unavailable"); + }); +}); +const runner: CommandManagedRuntimeRunner = { + execute: (input) => new Promise((resolve, reject) => { + const child = spawn(input.command, input.args, { cwd: input.cwd, env: { ...process.env, ...input.env } }); + let stdout = ""; let stderr = ""; + child.stdout.on("data", (chunk) => { stdout += chunk; void input.onLog?.("stdout", String(chunk)); }); + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("error", reject); + child.on("close", (exitCode, signal) => resolve({ exitCode, signal, stdout, stderr, timedOut: false, pid: child.pid ?? null, startedAt: null })); + child.stdin.end(input.stdin); + }), +}; +async function fixture() { + const home = await fs.realpath(await fs.mkdtemp(join(tmpdir(), "provider-cache-"))); + roots.push(home); + return { + cacheRoot: computerProviderPackCachePath(home, digest), sessionKey: "task-a", + owner: { ownerId: "owner-a", generation: 1 }, runner, + verify: async (root: string) => { expect(await fs.readFile(join(root, "payload"), "utf8")).toBe("verified bytes"); }, + stage: async (root: string, guarded: CommandManagedRuntimeRunner) => { + const result = await guarded.execute({ command: "python3", args: ["-c", "import os,sys;os.mkdir(sys.argv[1],0o700);open(os.path.join(sys.argv[1],'payload'),'w').write(sys.stdin.read())", root], stdin: "verified bytes" }); + if (result.exitCode !== 0) throw new Error(result.stderr); + }, + }; +} +function deferred() { let resolve!: () => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } +afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); }); + +describe("persistent computer provider pack cache", () => { + it("shares immutable identity across tasks, with separate agents and digests", () => { + expect(computerProviderPackCachePath("/agents/a", digest)).not.toBe(computerProviderPackCachePath("/agents/b", digest)); + expect(computerProviderPackCachePath("/agents/a", digest)).not.toBe(computerProviderPackCachePath("/agents/a", `sha256:${"b".repeat(64)}`)); + expect(() => computerProviderPackCachePath("/agents/../a", digest)).toThrow("identity_invalid"); + }); + + it("verifies a shared hit before the compression/upload callback and excludes its ownership marker", async () => { + const input = await fixture(); + const stage = vi.fn(input.stage); const verify = vi.fn(input.verify); + await expect(prepareComputerProviderPackCache({ ...input, stage, verify })).resolves.toBe("published"); + await expect(prepareComputerProviderPackCache({ ...input, stage, verify, sessionKey: "task-b" })).resolves.toBe("reused"); + expect(stage).toHaveBeenCalledTimes(1); + expect(verify).toHaveBeenCalledTimes(3); + expect(await fs.readdir(input.cacheRoot)).toEqual(["payload"]); + }); + + it("fails closed on a corrupt published pack, preserving its exact contents", async () => { + const input = await fixture(); + await fs.mkdir(input.cacheRoot, { recursive: true }); + await fs.writeFile(join(input.cacheRoot, "payload"), "corrupt"); + const stage = vi.fn(input.stage); + await expect(prepareComputerProviderPackCache({ ...input, stage })).rejects.toThrow("operator remove or quarantine this exact cache"); + expect(stage).not.toHaveBeenCalled(); + expect(await fs.readFile(join(input.cacheRoot, "payload"), "utf8")).toBe("corrupt"); + }); + + it("publishes without replacement when two tasks race and verifies the winner", async () => { + const input = await fixture(); const both = deferred(); let staged = 0; + const stage = async (root: string, guarded: CommandManagedRuntimeRunner) => { + await input.stage(root, guarded); if (++staged === 2) both.resolve(); await both.promise; + }; + await Promise.all(["task-a", "task-b"].map((sessionKey) => prepareComputerProviderPackCache({ ...input, sessionKey, stage }))); + expect(staged).toBe(2); + await input.verify(input.cacheRoot); + const directories = await fs.readdir(join(dirname(input.cacheRoot), ".uploads")); + for (const session of directories) expect(await fs.readdir(join(dirname(input.cacheRoot), ".uploads", session, "a".repeat(64)))).toEqual(["lock"]); + }); + + it("cleans only its marked private upload after failure", async () => { + const input = await fixture(); const unrelated = join(dirname(input.cacheRoot), ".uploads", "user-data"); + await fs.mkdir(unrelated, { recursive: true }); await fs.writeFile(join(unrelated, "keep"), "mine"); + let attempted = ""; + await expect(prepareComputerProviderPackCache({ ...input, stage: async (root, guarded) => { + attempted = dirname(root); await input.stage(root, guarded); throw new Error("upload disconnected"); + } })).rejects.toThrow("upload disconnected"); + await expect(fs.stat(attempted)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await fs.readFile(join(unrelated, "keep"), "utf8")).toBe("mine"); + }); + + it("rejects delayed old writes and keeps the replacement when a session retries", async () => { + const input = await fixture(); const oldReady = deferred(); const releaseOld = deferred(); + let oldRoot = ""; let oldRunner!: CommandManagedRuntimeRunner; + const old = prepareComputerProviderPackCache({ ...input, stage: async (root, guarded) => { + oldRoot = root; oldRunner = guarded; await input.stage(root, guarded); oldReady.resolve(); await releaseOld.promise; + throw new Error("old attempt retired"); + } }); + const rejectedOld = expect(old).rejects.toThrow("old attempt retired"); + await oldReady.promise; + await prepareComputerProviderPackCache({ ...input, owner: { ...input.owner, generation: 2 } }); + const staleWrite = await oldRunner.execute({ command: "python3", args: ["-c", "import os,sys;os.makedirs(sys.argv[1],exist_ok=True)", oldRoot] }); + expect(staleWrite.exitCode).not.toBe(0); + expect(staleWrite.stderr).toContain("stale upload generation"); + releaseOld.resolve(); await rejectedOld; + await input.verify(input.cacheRoot); + await expect(fs.stat(dirname(oldRoot))).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("waits for an in-flight guarded writer before replacing its receipt", async () => { + const input = await fixture(); const entered = deferred(); const releaseStage = deferred(); + const releaseFile = join(roots.at(-1)!, "release-writer"); + let write!: Promise>>; + const old = prepareComputerProviderPackCache({ ...input, stage: async (root, guarded) => { + await input.stage(root, guarded); + write = guarded.execute({ command: "python3", args: ["-c", "import os,sys,time;print('entered',flush=True)\nwhile not os.path.exists(sys.argv[1]):time.sleep(0.01)", releaseFile], onLog: async (_stream, text) => { if (text.includes("entered")) entered.resolve(); } }); + await releaseStage.promise; await write; throw new Error("old retired"); + } }); + const rejectedOld = expect(old).rejects.toThrow("old retired"); + await entered.promise; + let replacementStaged = false; + const replacement = prepareComputerProviderPackCache({ ...input, owner: { ...input.owner, generation: 2 }, stage: async (root, guarded) => { + replacementStaged = true; await input.stage(root, guarded); + } }); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(replacementStaged).toBe(false); + await fs.writeFile(releaseFile, "release"); + expect((await write).exitCode).toBe(0); + await replacement; releaseStage.resolve(); await rejectedOld; + await input.verify(input.cacheRoot); + }); + + it("reclaims an interrupted marked upload on the next attempt", async () => { + const input = await fixture(); let attempted = ""; + const disconnected: CommandManagedRuntimeRunner = { execute: (command) => { + if (command.args?.[2] === "cleanup") return Promise.reject(new Error("controller disconnected")); + return runner.execute(command); + } }; + await expect(prepareComputerProviderPackCache({ ...input, runner: disconnected, stage: async (root, guarded) => { + attempted = dirname(root); await input.stage(root, guarded); throw new Error("interrupted"); + } })).rejects.toThrow("interrupted"); + expect((await fs.stat(attempted)).isDirectory()).toBe(true); + // Another task can publish while this session is disconnected. Retrying + // this session must reclaim its exact old upload even on the shared hit. + await prepareComputerProviderPackCache({ ...input, sessionKey: "another-task" }); + await expect(prepareComputerProviderPackCache({ ...input, owner: { ...input.owner, generation: 2 } })).resolves.toBe("reused"); + await expect(fs.stat(attempted)).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); 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 c086a927fb..1502d74c8f 100644 --- a/server/src/services/native-runtime/remote-provider-pack-cache.ts +++ b/server/src/services/native-runtime/remote-provider-pack-cache.ts @@ -2,6 +2,94 @@ import { createHash, randomUUID } from "node:crypto"; import { posix } from "node:path"; import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime"; +/** The controller's retained attachment template pins content-addressed paths. + * Remote manifests may supply bytes, but cannot choose a new code authority. */ +export function pinnedComputerProviderPack(input: { + agentHome: string; identity: Record; control: unknown; + provider: { agent: string; model: string }; +}): { digest: string; root: string; command: string; sidecar: string } { + const object = (value: unknown): Record => + 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" || + 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(); + const match = provider.sidecarCommand.match(/\/provider-packs\/([a-f0-9]{64})\/node_modules\/node\/bin\/node$/); + if (!match) return fail(); + const digest = `sha256:${match[1]}`; + const root = computerProviderPackCachePath(input.agentHome, digest); + const command = `${root}/node_modules/node/bin/node`; + const sidecar = `${root}/dist/cli/acpx-runtime-sidecar.cjs`; + if (provider.sidecarCommand !== command || provider.sidecarArgs[0] !== sidecar) return fail(); + return { digest, root, command, sidecar }; +} + +/** Mirrors the runner's length-prefixed ACPX launch profile v1 digest. */ +export function computerAcpxLaunchProfileDigest(input: { + authorityDigest: string; command: string; commandSha256: string; sidecar: string; sidecarSha256: string; +}): string { + const digest = createHash("sha256"); + const integer = (n: number) => { const bytes = Buffer.alloc(8); bytes.writeBigUInt64BE(BigInt(n)); return bytes; }; + const update = (value: string | Buffer) => { const bytes = typeof value === "string" ? Buffer.from(value) : value; digest.update(integer(bytes.length)); digest.update(bytes); }; + update("paperclip.runner.acpx-launch-profile.v1"); + update(input.authorityDigest); update(input.command); update(integer(1)); update(input.sidecar); update(integer(2)); + for (const [path, sha256] of [[input.command, input.commandSha256], [input.sidecar, input.sidecarSha256]].sort(([a], [b]) => a! < b! ? -1 : a! > b! ? 1 : 0)) { + update(path!); update(sha256!); + } + return `sha256:${digest.digest("hex")}`; +} + +export async function readPinnedComputerProviderMetadata(input: { + runner: CommandManagedRuntimeRunner; packRoot: string; stateDirectory: string; +}): Promise<[unknown, unknown]> { + for (const path of [input.packRoot, input.stateDirectory]) { + if (!posix.isAbsolute(path) || posix.normalize(path) !== path) + throw new Error("runner_remote_provider_pack_provenance_unavailable"); + } + const result = await input.runner.execute({ command: "python3", args: ["-c", String.raw` +import os,sys,json,stat +values=[] +for path in sys.argv[1:]: + parts=path.split('/')[1:];parent=os.open('/',os.O_RDONLY|os.O_DIRECTORY) + try: + for part in parts[:-1]: + child=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent) + os.close(parent);parent=child + source=os.open(parts[-1],os.O_RDONLY|os.O_NOFOLLOW,dir_fd=parent) + with os.fdopen(source,'rb') as f: + info=os.fstat(f.fileno()) + if not stat.S_ISREG(info.st_mode) or info.st_nlink!=1 or info.st_size>16777216:raise RuntimeError('invalid retained provider metadata') + data=f.read(16777217) + if len(data)>16777216:raise RuntimeError('invalid retained provider metadata') + values.append(json.loads(data)) + finally:os.close(parent) +print(json.dumps(values))`, posix.join(input.packRoot, "provider-pack.json"), + posix.join(input.stateDirectory, "acpx-provider-state.json")], bypassSession: true, timeoutMs: 10_000 }); + if (result.exitCode !== 0 || result.timedOut) throw new Error("runner_remote_provider_pack_provenance_unavailable"); + const value: unknown = JSON.parse(result.stdout); + if (!Array.isArray(value) || value.length !== 2) throw new Error("runner_remote_provider_pack_provenance_unavailable"); + return [value[0], value[1]]; +} + +export function assertPinnedComputerProviderState(input: { + state: unknown; normalizedSessionId: string; launchProfileDigest: string; command: string; sidecar: string; +}): void { + const object = (value: unknown): Record => + value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; + const state = object(input.state); const descriptor = object(state.descriptor); + if (state.schema !== "paperclip.runner.acpx-provider-state.v3" || state.launchProfileDigest !== input.launchProfileDigest || + descriptor.normalizedSessionId !== input.normalizedSessionId || descriptor.sidecarCommand !== input.command || + !Array.isArray(descriptor.sidecarArgs) || descriptor.sidecarArgs.length !== 1 || descriptor.sidecarArgs[0] !== input.sidecar) + throw new Error("runner_remote_provider_pack_provenance_mismatch"); +} + export function computerProviderPackCachePath(agentHome: string, digest: string): string { if (!posix.isAbsolute(agentHome) || posix.normalize(agentHome) !== agentHome || !/^sha256:[a-f0-9]{64}$/.test(digest)) { throw new Error("runner_remote_provider_cache_identity_invalid");