mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-13 04:02:28 +02:00
fix(boat): retain verified ACPX provider packs across deployments
This commit is contained in:
1 parent
a946f0a084
commit
66ac692e44
4 files changed
+331
-2
No files matched your search
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<keyof typeof profile>) {
|
||||
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<void>((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<Awaited<ReturnType<CommandManagedRuntimeRunner["execute"]>>>;
|
||||
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" });
|
||||
});
|
||||
});
|
||||
@@ -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<string, unknown>; control: unknown;
|
||||
provider: { agent: string; model: string };
|
||||
}): { digest: string; root: string; command: string; sidecar: string } {
|
||||
const object = (value: unknown): Record<string, unknown> =>
|
||||
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" ||
|
||||
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<string, unknown> =>
|
||||
value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
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");
|
||||
|
||||
Reference in new issue
Block a user