Share bounded artifact verification for ACP command and runtime

This commit is contained in:
Dotta committed 2026-10-11 01:41:17 -05:00
1 parent 92be5d8596
commit a93abeb160
2 files changed
+56 -90

No files matched your search

@@ -29,6 +29,7 @@ import { resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
import {
verifyProvisionedGrokExecutable,
openVerifiedRuntimeExecutable,
inspectCommand,
builtinGrokLauncherPath,
awaitVerifiedAcpxProviderExit,
awaitVerifiedAcpxProviderOwnership,
@@ -58,6 +59,8 @@ afterEach(async () => {
});
describe("ACPX installation integrity", () => {
describe.each(["runtime", "command"] as const)("%s executable hydration", (kind) => {
const verify = kind === "runtime" ? openVerifiedRuntimeExecutable : inspectCommand;
it.each(["once", "every_read", "wrong_digest", "mode", "mtime", "replacement", "symlink", "second_content"] as const)("requires stable full re-verification after a ctime-only transition (%s)", async (transition) => {
const directory = await mkdtemp(join(tmpdir(), "paperclip-runtime-hydration-"));
temporaryDirectories.push(directory);
@@ -100,19 +103,22 @@ describe("ACPX installation integrity", () => {
});
try {
if (transition === "once") {
const verified = await openVerifiedRuntimeExecutable(executable, digest, "fixture");
const verified = await verify(executable, digest, "fixture");
try {
const stable = await verified.handle.stat({ bigint: true });
const stable = await stat(executable, { bigint: true });
if ("bytes" in verified) expect(verified.bytes).toEqual(bytes);
expect(verified.identity.changedNanoseconds).toBe(stable.ctimeNs.toString());
expect(verified.identity.modifiedNanoseconds).toBe(stable.mtimeNs.toString());
} finally { await verified.handle.close(); }
} finally { if ("handle" in verified) await verified.handle.close(); }
} else {
await expect(openVerifiedRuntimeExecutable(executable, digest, "fixture")).rejects.toThrow(transition === "wrong_digest" ? "digest mismatch" : "changed while it was verified");
await expect(verify(executable, digest, "fixture")).rejects.toThrow(transition === "wrong_digest" ? "digest mismatch" : "changed while it was verified");
}
expect(reads).toBe(["once", "every_read", "second_content"].includes(transition) ? 2 : 1);
} finally { readSpy.mockRestore(); }
});
});
it.each([false, true])("admits only the declared qualified Codex platform in its npm-hoisted slot (published platform declaration: %s)", async (publishedDeclaration) => {
const parent = await realpath(await mkdtemp(join(tmpdir(), "paperclip-codex-npm-hoist-")));
temporaryDirectories.push(parent);
@@ -1093,7 +1093,7 @@ async function readBoundedRegularFile(
return bytes;
}
async function inspectCommand(
export async function inspectCommand(
commandPath: string,
expectedDigest: string,
agent: string,
@@ -1102,67 +1102,20 @@ async function inspectCommand(
digest: string;
identity: VerifiedAcpxCommandIdentity;
}> {
const lexicalBefore = await lstat(commandPath, { bigint: true }).catch(
() => null,
const verified = await openVerifiedExecutable(
commandPath,
expectedDigest,
`ACPX ${agent} executable`,
{ maxBytes: MAX_AGENT_COMMAND_BYTES, requireExecutable: false, captureBytes: true },
);
if (
lexicalBefore === null ||
lexicalBefore.isSymbolicLink() ||
!lexicalBefore.isFile()
) {
throw new Error(`ACPX ${agent} executable must be a real regular file`);
}
let handle: Awaited<ReturnType<typeof open>>;
try {
handle = await open(
commandPath,
verifiedExecutableOpenFlags(process.platform, constants.O_NOFOLLOW),
);
} catch {
throw new Error(
`ACPX ${agent} executable could not be opened as a no-follow regular file`,
);
}
try {
const before = await handle.stat({ bigint: true });
if (
!before.isFile() ||
before.size < 1n ||
before.size > BigInt(MAX_AGENT_COMMAND_BYTES)
) {
throw new Error(
`ACPX ${agent} executable must be a bounded regular file`,
);
}
const bytes = await readHandleAtStart(handle, Number(before.size));
const after = await handle.stat({ bigint: true });
const lexicalAfter = await lstat(commandPath, { bigint: true }).catch(
() => null,
);
const beforeIdentity = fileIdentity(before);
const afterIdentity = fileIdentity(after);
if (
bytes.length < 1 ||
bytes.length > MAX_AGENT_COMMAND_BYTES ||
lexicalAfter === null ||
lexicalAfter.isSymbolicLink() ||
!lexicalAfter.isFile() ||
!sameIdentity(fileIdentity(lexicalBefore), fileIdentity(lexicalAfter)) ||
!sameIdentity(fileIdentity(lexicalAfter), afterIdentity) ||
!sameIdentity(beforeIdentity, afterIdentity) ||
after.size !== BigInt(bytes.length)
) {
throw new Error(`ACPX ${agent} executable changed while it was verified`);
}
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
if (digest !== expectedDigest) {
throw new Error(`ACPX ${agent} executable digest mismatch`);
}
return { bytes, digest, identity: afterIdentity };
} catch (error) {
throw error;
return {
bytes: verified.bytes,
digest: verified.digest,
identity: verified.identity,
};
} finally {
await handle.close();
await verified.handle.close();
}
}
@@ -1171,6 +1124,28 @@ export async function openVerifiedRuntimeExecutable(
expectedDigest: string,
agent: string,
): Promise<{ handle: FileHandle; identity: VerifiedAcpxCommandIdentity }> {
const verified = await openVerifiedExecutable(
executablePath,
expectedDigest,
`ACPX ${agent} runtime executable`,
{ maxBytes: MAX_ACPX_RUNTIME_EXECUTABLE_BYTES, requireExecutable: true, captureBytes: false },
);
return { handle: verified.handle, identity: verified.identity };
}
/** Verify command snapshots and native executables under the same identity fence. */
async function openVerifiedExecutable(
executablePath: string,
expectedDigest: string,
label: string,
options: { maxBytes: number; requireExecutable: boolean; captureBytes: boolean },
): Promise<{
handle: FileHandle;
identity: VerifiedAcpxCommandIdentity;
digest: string;
bytes: Buffer;
}> {
const { maxBytes, requireExecutable, captureBytes } = options;
const lexicalBefore = await lstat(executablePath, { bigint: true }).catch(
() => null,
);
@@ -1180,7 +1155,7 @@ export async function openVerifiedRuntimeExecutable(
!lexicalBefore.isFile()
) {
throw new Error(
`ACPX ${agent} runtime executable must be a real regular file`,
`${label} must be a real regular file`,
);
}
@@ -1192,7 +1167,7 @@ export async function openVerifiedRuntimeExecutable(
);
} catch {
throw new Error(
`ACPX ${agent} runtime executable could not be opened as a no-follow regular file`,
`${label} could not be opened as a no-follow regular file`,
);
}
@@ -1203,11 +1178,11 @@ export async function openVerifiedRuntimeExecutable(
if (
!before.isFile() ||
before.size < 1n ||
before.size > BigInt(MAX_ACPX_RUNTIME_EXECUTABLE_BYTES) ||
(before.mode & 0o111n) === 0n
before.size > BigInt(maxBytes) ||
(requireExecutable && (before.mode & 0o111n) === 0n)
) {
throw new Error(
`ACPX ${agent} runtime executable must be a bounded executable file`,
`${label} must be a bounded ${requireExecutable ? "executable file" : "regular file"}`,
);
}
const beforeIdentity = fileIdentity(before);
@@ -1216,11 +1191,12 @@ export async function openVerifiedRuntimeExecutable(
lexicalBaseline.mode !== before.mode
) {
throw new Error(
`ACPX ${agent} runtime executable changed while it was verified`,
`${label} changed while it was verified`,
);
}
const hash = createHash("sha256");
const buffer = Buffer.alloc(1024 * 1024);
const chunks: Buffer[] = [];
let position = 0;
try {
while (position < Number(before.size)) {
@@ -1232,6 +1208,7 @@ export async function openVerifiedRuntimeExecutable(
);
if (bytesRead === 0) break;
hash.update(buffer.subarray(0, bytesRead));
if (captureBytes) chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
position += bytesRead;
}
} finally {
@@ -1256,15 +1233,15 @@ export async function openVerifiedRuntimeExecutable(
)
) {
throw new Error(
`ACPX ${agent} runtime executable changed while it was verified`,
`${label} changed while it was verified`,
);
}
const digest = `sha256:${hash.digest("hex")}`;
if (digest !== expectedDigest) {
throw new Error(`ACPX ${agent} runtime executable digest mismatch`);
throw new Error(`${label} digest mismatch`);
}
if (sameIdentity(beforeIdentity, afterIdentity)) {
return { handle, identity: afterIdentity };
return { handle, identity: afterIdentity, digest, bytes: Buffer.concat(chunks, captureBytes ? position : 0) };
}
// A lazy filesystem restore can change ctime on the first read. Only
// already-qualified bytes with otherwise identical metadata may receive
@@ -1273,7 +1250,7 @@ export async function openVerifiedRuntimeExecutable(
lexicalBaseline = lexicalAfter;
}
throw new Error(
`ACPX ${agent} runtime executable changed while it was verified`,
`${label} changed while it was verified`,
);
} catch (error) {
await handle.close();
@@ -1436,23 +1413,6 @@ function verifiedDirectoryOpenFlags(
return constants.O_RDONLY | noFollowFlag | directoryFlag;
}
async function readHandleAtStart(
handle: FileHandle,
size: number,
): Promise<Buffer> {
const bytes = Buffer.alloc(size);
let offset = 0;
while (offset < size) {
const read = await handle.read(bytes, offset, size - offset, offset);
if (read.bytesRead === 0) break;
offset += read.bytesRead;
}
if (offset !== size) {
throw new Error("ACPX provider executable ended during verification");
}
return bytes;
}
function commandLease(
commandDirectoryPath: string,
commandName: string,