1106 lines
42 KiB
TypeScript
1106 lines
42 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { closeSync, constants, fchmodSync, openSync, watch } from "node:fs";
|
|
import {
|
|
lstat,
|
|
mkdir,
|
|
mkdtemp,
|
|
open,
|
|
readFile,
|
|
readdir,
|
|
readlink,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
const roots: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe("provider guardian transaction protocol", () => {
|
|
it("encodes a v2 guard without accepting filesystem paths or identities", async () => {
|
|
const protocol = await import("../../scripts/lib/provider-guardian-protocol.ts") as
|
|
Record<string, unknown>;
|
|
expect(protocol.encodeProviderGuardianGuard).toBeTypeOf("function");
|
|
const encode = protocol.encodeProviderGuardianGuard as (input: Readonly<{
|
|
kind: "vulnerability";
|
|
nonce: Buffer;
|
|
deadlineEpochMs: number;
|
|
}>) => Buffer;
|
|
const nonce = Buffer.alloc(32, 0x5a);
|
|
const encoded = encode({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: 1_800_000_000_000,
|
|
});
|
|
|
|
expect(encoded.readUInt32BE(0)).toBe(encoded.byteLength - 4);
|
|
expect(JSON.parse(encoded.subarray(4).toString("utf8"))).toEqual({
|
|
type: "guard",
|
|
version: 2,
|
|
kind: "vulnerability",
|
|
nonce: "5a".repeat(32),
|
|
deadlineEpochMs: 1_800_000_000_000,
|
|
});
|
|
expect(
|
|
(protocol.providerGuardianSealedTempLeaf as (kind: "vulnerability", value: Buffer) => string)(
|
|
"vulnerability",
|
|
nonce,
|
|
),
|
|
).toBe(`.vulnerability-report.json.guardian-${"5a".repeat(16)}.tmp`);
|
|
expect(
|
|
(protocol.providerGuardianRawStagingLeaf as (
|
|
kind: "vulnerability",
|
|
value: Buffer,
|
|
) => string)("vulnerability", nonce),
|
|
).toBe(`.vulnerability-report.json.guardian-${"5a".repeat(16)}.raw.tmp`);
|
|
});
|
|
|
|
it("decodes only the exact canonical v2 guard within the lease window", async () => {
|
|
const protocol = await import("../../scripts/lib/provider-guardian-protocol.ts") as
|
|
Record<string, unknown>;
|
|
expect(protocol.decodeProviderGuardianGuard).toBeTypeOf("function");
|
|
const decode = protocol.decodeProviderGuardianGuard as (
|
|
payload: Buffer,
|
|
options: Readonly<{ nowEpochMs: number; maxLeaseMs: number }>,
|
|
) => Readonly<{ kind: string; nonce: Buffer; deadlineEpochMs: number }>;
|
|
const canonical = JSON.stringify({
|
|
type: "guard",
|
|
version: 2,
|
|
kind: "provenance",
|
|
nonce: "3c".repeat(32),
|
|
deadlineEpochMs: 5_000,
|
|
});
|
|
|
|
expect(decode(Buffer.from(canonical), { nowEpochMs: 1_000, maxLeaseMs: 5_000 }))
|
|
.toEqual({
|
|
kind: "provenance",
|
|
nonce: Buffer.alloc(32, 0x3c),
|
|
deadlineEpochMs: 5_000,
|
|
});
|
|
for (const invalid of [
|
|
canonical.replace('"nonce":', '"nonce":"3c'.concat('"'.repeat(0), ',"nonce":')),
|
|
canonical.replace(/\}$/u, ',"rawIno":9}'),
|
|
`${canonical} `,
|
|
canonical.replace("5000", "1000"),
|
|
canonical.replace("5000", "7000"),
|
|
canonical.replace("3c".repeat(32), "3c".repeat(31)),
|
|
]) {
|
|
expect(() => decode(Buffer.from(invalid), { nowEpochMs: 1_000, maxLeaseMs: 5_000 }))
|
|
.toThrow(/provider guardian/u);
|
|
}
|
|
expect(() => decode(Buffer.from([0xff]), { nowEpochMs: 1_000, maxLeaseMs: 5_000 }))
|
|
.toThrow(/provider guardian/u);
|
|
});
|
|
|
|
it("authenticates READY, publish, PUBLISHED, and v2 commit with exact fields", async () => {
|
|
const protocol = await import("../../scripts/lib/provider-guardian-protocol.ts") as
|
|
Record<string, unknown>;
|
|
for (const name of [
|
|
"decodeProviderGuardianReady",
|
|
"encodeProviderGuardianPublish",
|
|
"decodeProviderGuardianPublish",
|
|
"encodeProviderGuardianPublished",
|
|
"decodeProviderGuardianPublished",
|
|
]) {
|
|
expect(protocol[name], name).toBeTypeOf("function");
|
|
}
|
|
const nonce = Buffer.alloc(32, 0x7a);
|
|
const readyPayload = Buffer.from(JSON.stringify({
|
|
type: "ready",
|
|
version: 2,
|
|
nonce: "7a".repeat(32),
|
|
rawDev: 12,
|
|
rawIno: 34,
|
|
sealedTempLeaf: ".vulnerability-report.json.guardian-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tmp",
|
|
sealedDev: 56,
|
|
sealedIno: 78,
|
|
}));
|
|
const decodeReady = protocol.decodeProviderGuardianReady as (
|
|
payload: Buffer,
|
|
expectedNonce: Buffer,
|
|
) => Record<string, unknown>;
|
|
expect(decodeReady(readyPayload, nonce)).toEqual({
|
|
nonce,
|
|
rawDev: 12,
|
|
rawIno: 34,
|
|
sealedTempLeaf: ".vulnerability-report.json.guardian-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tmp",
|
|
sealedDev: 56,
|
|
sealedIno: 78,
|
|
});
|
|
expect(() => decodeReady(readyPayload, Buffer.alloc(32, 0x7b)))
|
|
.toThrow(/authentication/u);
|
|
|
|
const encodePublish = protocol.encodeProviderGuardianPublish as (input: Record<string, unknown>) => Buffer;
|
|
const publishFrame = encodePublish({
|
|
nonce,
|
|
sealedDev: 56,
|
|
sealedIno: 78,
|
|
size: 123,
|
|
sha256: "ab".repeat(32),
|
|
});
|
|
expect(JSON.parse(publishFrame.subarray(4).toString("utf8"))).toEqual({
|
|
type: "publish",
|
|
version: 2,
|
|
nonce: "7a".repeat(32),
|
|
sealedDev: 56,
|
|
sealedIno: 78,
|
|
size: 123,
|
|
sha256: "ab".repeat(32),
|
|
});
|
|
const decodePublish = protocol.decodeProviderGuardianPublish as (
|
|
payload: Buffer,
|
|
expectedNonce: Buffer,
|
|
) => Record<string, unknown>;
|
|
expect(decodePublish(publishFrame.subarray(4), nonce)).toEqual({
|
|
nonce,
|
|
sealedDev: 56,
|
|
sealedIno: 78,
|
|
size: 123,
|
|
sha256: "ab".repeat(32),
|
|
});
|
|
|
|
const encodePublished = protocol.encodeProviderGuardianPublished as (
|
|
input: Readonly<{ nonce: Buffer; sealedDev: number; sealedIno: number }>,
|
|
) => Buffer;
|
|
const published = encodePublished({ nonce, sealedDev: 56, sealedIno: 78 });
|
|
const decodePublished = protocol.decodeProviderGuardianPublished as (
|
|
payload: Buffer,
|
|
expectedNonce: Buffer,
|
|
expectedIdentity: Readonly<{ dev: number; ino: number }>,
|
|
) => void;
|
|
expect(() => decodePublished(published.subarray(4), nonce, { dev: 56, ino: 78 }))
|
|
.not.toThrow();
|
|
|
|
const encodeCommit = protocol.encodeProviderGuardianCommit as (value: Buffer) => Buffer;
|
|
expect(JSON.parse(encodeCommit(nonce).subarray(4).toString("utf8"))).toEqual({
|
|
type: "commit",
|
|
version: 2,
|
|
nonce: "7a".repeat(32),
|
|
});
|
|
});
|
|
|
|
it("creates and returns owned raw and sealed-temp identities before READY", async () => {
|
|
const {
|
|
decodeProviderGuardianReady,
|
|
encodeProviderGuardianGuard,
|
|
} = await import("../../scripts/lib/provider-guardian-protocol.ts");
|
|
const workspace = await createWorkspace("provider-guardian-ready-");
|
|
const rawPath = path.join(
|
|
workspace,
|
|
"provider-evidence/untrusted/vulnerability-report.json",
|
|
);
|
|
const nonce = Buffer.alloc(32, 0x4d);
|
|
const child = spawnGuardian(workspace, nonce);
|
|
const completion = waitForChild(child);
|
|
const readyPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianGuard({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: Date.now() + 2_000,
|
|
}));
|
|
|
|
const ready = decodeProviderGuardianReady(
|
|
await within(readyPayload, 1_000, "guardian READY"),
|
|
nonce,
|
|
);
|
|
const raw = await lstat(rawPath);
|
|
const sealedTempPath = path.join(workspace, "provider-evidence", ready.sealedTempLeaf);
|
|
const sealedTemp = await lstat(sealedTempPath);
|
|
expect({ dev: raw.dev, ino: raw.ino, mode: raw.mode & 0o777 }).toEqual({
|
|
dev: ready.rawDev,
|
|
ino: ready.rawIno,
|
|
mode: 0o600,
|
|
});
|
|
expect({ dev: sealedTemp.dev, ino: sealedTemp.ino, mode: sealedTemp.mode & 0o777 })
|
|
.toEqual({ dev: ready.sealedDev, ino: ready.sealedIno, mode: 0o600 });
|
|
|
|
child.stdin!.end();
|
|
await expect(within(completion, 1_000, "guardian abort")).resolves.toEqual({
|
|
code: 125,
|
|
signal: null,
|
|
});
|
|
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted"))).resolves.toEqual([]);
|
|
});
|
|
|
|
it("creates no evidence for empty or truncated guard input", async () => {
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
for (const [label, input, expectedCode] of [
|
|
["empty", undefined, 125],
|
|
["truncated-prefix", Buffer.from([0, 0]), 126],
|
|
] as const) {
|
|
const workspace = await createWorkspace(`provider-guardian-${label}-`);
|
|
const child = spawnGuardian(workspace);
|
|
const completion = waitForChild(child);
|
|
if (input) child.stdin!.end(input);
|
|
else child.stdin!.end();
|
|
|
|
await expect(within(completion, 1_000, `${label} guardian EOF`)).resolves.toEqual({
|
|
code: expectedCode,
|
|
signal: null,
|
|
});
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted"))).resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence"))).resolves.toEqual([
|
|
"untrusted",
|
|
]);
|
|
|
|
const retry = await startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
});
|
|
await retry.abort();
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted")))
|
|
.resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence")))
|
|
.resolves.toEqual(["untrusted"]);
|
|
}
|
|
});
|
|
|
|
it.each(["raw", "sealed"] as const)(
|
|
"cleans the independently bound %s private alias when peer bootstrap validation fails",
|
|
async (boundKind) => {
|
|
const workspace = await createWorkspace(`provider-guardian-partial-bootstrap-${boundKind}-`);
|
|
const nonce = Buffer.alloc(32, boundKind === "raw" ? 0x71 : 0x72);
|
|
const fixture = spawnGuardianWithPrivateModes(workspace, nonce, {
|
|
rawMode: boundKind === "raw" ? 0o600 : 0o400,
|
|
sealedMode: boundKind === "sealed" ? 0o600 : 0o400,
|
|
});
|
|
|
|
await expect(within(
|
|
waitForChild(fixture.child),
|
|
1_000,
|
|
`${boundKind} partial bootstrap exit`,
|
|
)).resolves.toEqual({ code: 126, signal: null });
|
|
const boundPath = boundKind === "raw" ? fixture.rawStagingPath : fixture.sealedTempPath;
|
|
const invalidPath = boundKind === "raw" ? fixture.sealedTempPath : fixture.rawStagingPath;
|
|
await expect(lstat(boundPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(invalidPath)).resolves.toEqual(expect.objectContaining({
|
|
mode: expect.any(Number),
|
|
}));
|
|
await rm(invalidPath);
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted")))
|
|
.resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence")))
|
|
.resolves.toEqual(["untrusted"]);
|
|
},
|
|
);
|
|
|
|
it("cleans guardian-created objects when the parent closes immediately after guard", async () => {
|
|
const { encodeProviderGuardianGuard } = await import(
|
|
"../../scripts/lib/provider-guardian-protocol.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-parent-startup-death-");
|
|
const nonce = Buffer.alloc(32, 0x2b);
|
|
const child = spawnGuardian(workspace, nonce);
|
|
const completion = waitForChild(child);
|
|
child.stdin!.end(encodeProviderGuardianGuard({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: Date.now() + 2_000,
|
|
}));
|
|
|
|
await expect(within(completion, 1_000, "guardian parent startup death"))
|
|
.resolves.toEqual({ code: 125, signal: null });
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted"))).resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence"))).resolves.toEqual(["untrusted"]);
|
|
});
|
|
|
|
it("publishes the pinned sealed inode and preserves it only after commit EOF", async () => {
|
|
const {
|
|
decodeProviderGuardianPublished,
|
|
decodeProviderGuardianReady,
|
|
encodeProviderGuardianCommit,
|
|
encodeProviderGuardianGuard,
|
|
encodeProviderGuardianPublish,
|
|
} = await import("../../scripts/lib/provider-guardian-protocol.ts");
|
|
const workspace = await createWorkspace("provider-guardian-publish-");
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawPath = path.join(evidenceRoot, "untrusted/vulnerability-report.json");
|
|
const sealedPath = path.join(evidenceRoot, "vulnerability-report.json");
|
|
const nonce = Buffer.alloc(32, 0x62);
|
|
const child = spawnGuardian(workspace, nonce);
|
|
const completion = waitForChild(child);
|
|
const readyPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianGuard({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: Date.now() + 3_000,
|
|
}));
|
|
const ready = decodeProviderGuardianReady(
|
|
await within(readyPayload, 1_000, "guardian READY"),
|
|
nonce,
|
|
);
|
|
const sealedTempPath = path.join(evidenceRoot, ready.sealedTempLeaf);
|
|
const bytes = Buffer.from('{"validated":true}\n');
|
|
const handle = await open(
|
|
sealedTempPath,
|
|
constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
);
|
|
try {
|
|
const before = await handle.stat();
|
|
expect({ dev: before.dev, ino: before.ino }).toEqual({
|
|
dev: ready.sealedDev,
|
|
ino: ready.sealedIno,
|
|
});
|
|
await handle.writeFile(bytes);
|
|
await handle.chmod(0o400);
|
|
await handle.sync();
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
|
|
const publishedPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianPublish({
|
|
nonce,
|
|
sealedDev: ready.sealedDev,
|
|
sealedIno: ready.sealedIno,
|
|
size: bytes.byteLength,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
}));
|
|
decodeProviderGuardianPublished(
|
|
await within(publishedPayload, 1_000, "guardian PUBLISHED"),
|
|
nonce,
|
|
{ dev: ready.sealedDev, ino: ready.sealedIno },
|
|
);
|
|
await expect(lstat(sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
const published = await lstat(sealedPath);
|
|
expect({ dev: published.dev, ino: published.ino, mode: published.mode & 0o777 })
|
|
.toEqual({ dev: ready.sealedDev, ino: ready.sealedIno, mode: 0o400 });
|
|
await expect(readFile(sealedPath)).resolves.toEqual(bytes);
|
|
|
|
child.stdin!.write(encodeProviderGuardianCommit(nonce));
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
|
expect(child.exitCode).toBeNull();
|
|
child.stdin!.end();
|
|
await expect(within(completion, 1_000, "guardian commit EOF")).resolves.toEqual({
|
|
code: 0,
|
|
signal: null,
|
|
});
|
|
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readFile(sealedPath)).resolves.toEqual(bytes);
|
|
});
|
|
|
|
it("owns raw and sealed publication through the authenticated client lease", async () => {
|
|
const client = await import("../../scripts/lib/provider-guardian-client.ts") as
|
|
Record<string, unknown>;
|
|
expect(client.startProviderGuardian).toBeTypeOf("function");
|
|
const start = client.startProviderGuardian as (input: Readonly<{
|
|
kind: "vulnerability";
|
|
workspaceRoot: string;
|
|
leaseMs: number;
|
|
guardianScript: string;
|
|
}>) => Promise<Readonly<{
|
|
pid: number;
|
|
rawPath: string;
|
|
rawIdentity: Readonly<{ dev: number; ino: number }>;
|
|
sealedPath: string;
|
|
sealedTempPath: string;
|
|
sealedIdentity: Readonly<{ dev: number; ino: number }>;
|
|
publish(bytes: Buffer): Promise<void>;
|
|
commit(): Promise<void>;
|
|
}>>;
|
|
const workspace = await createWorkspace("provider-guardian-client-v2-");
|
|
const guardianScript = path.resolve("scripts/lib/provider-raw-guardian.ts");
|
|
const lease = await start({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript,
|
|
});
|
|
expect((await readFile(`/proc/${lease.pid}/cmdline`)).toString("utf8")
|
|
.split("\0").filter(Boolean)).toEqual([process.execPath, guardianScript]);
|
|
await expect(readlink(`/proc/${lease.pid}/fd/3`)).resolves.toBe(
|
|
path.join(workspace, "provider-evidence/untrusted"),
|
|
);
|
|
await expect(readlink(`/proc/${lease.pid}/fd/4`)).resolves.toBe(
|
|
path.join(workspace, "provider-evidence"),
|
|
);
|
|
const rawMetadata = await lstat(lease.rawPath);
|
|
expect({ dev: rawMetadata.dev, ino: rawMetadata.ino }).toEqual(lease.rawIdentity);
|
|
const sealedTempMetadata = await lstat(lease.sealedTempPath);
|
|
expect({ dev: sealedTempMetadata.dev, ino: sealedTempMetadata.ino })
|
|
.toEqual(lease.sealedIdentity);
|
|
|
|
const bytes = Buffer.from('{"validated":"client"}\n');
|
|
await lease.publish(bytes);
|
|
await expect(lstat(lease.sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readFile(lease.sealedPath)).resolves.toEqual(bytes);
|
|
await lease.commit();
|
|
await expect(lstat(lease.rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readFile(lease.sealedPath)).resolves.toEqual(bytes);
|
|
});
|
|
|
|
it("cleans published evidence on EOF before commit and permits a same-workspace retry", async () => {
|
|
const workspace = await createWorkspace("provider-guardian-published-eof-");
|
|
const transaction = await establishPublishedGuardian(workspace, Buffer.alloc(32, 0x31));
|
|
|
|
transaction.child.stdin!.end();
|
|
await expect(within(transaction.completion, 1_000, "published guardian EOF"))
|
|
.resolves.toEqual({ code: 125, signal: null });
|
|
await assertTransactionAbsent(transaction);
|
|
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
const retry = await startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 2_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
});
|
|
await retry.abort();
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted"))).resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence"))).resolves.toEqual(["untrusted"]);
|
|
});
|
|
|
|
it("fails closed when a separate trailing frame arrives after commit", async () => {
|
|
const { encodeProviderGuardianCommit } = await import(
|
|
"../../scripts/lib/provider-guardian-protocol.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-commit-trailing-");
|
|
const nonce = Buffer.alloc(32, 0x47);
|
|
const transaction = await establishPublishedGuardian(workspace, nonce);
|
|
|
|
transaction.child.stdin!.write(encodeProviderGuardianCommit(nonce));
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
transaction.child.stdin!.write(Buffer.from([0, 0, 0, 1, 0x7b]));
|
|
transaction.child.stdin!.end();
|
|
|
|
await expect(within(transaction.completion, 1_000, "guardian trailing frame"))
|
|
.resolves.toEqual({ code: 126, signal: null });
|
|
await assertTransactionAbsent(transaction);
|
|
});
|
|
|
|
it("uses pinned fallback identities after a guardian hard death", async () => {
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-hard-death-");
|
|
const lease = await startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
});
|
|
|
|
process.kill(lease.pid, "SIGKILL");
|
|
await expect(within(lease.prematureExit, 1_000, "guardian hard death"))
|
|
.resolves.toEqual(expect.objectContaining({ message: expect.stringMatching(/SIGKILL/u) }));
|
|
await lease.abort();
|
|
await expect(lstat(lease.rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(lease.sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(lease.sealedPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("cleans a linked raw inode when killed before READY and retries immediately", async () => {
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-pre-ready-death-");
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawDirectory = path.join(evidenceRoot, "untrusted");
|
|
const rawPath = path.join(rawDirectory, "vulnerability-report.json");
|
|
const guardianScript = await createPausedGuardianFixture(workspace);
|
|
const existingChildren = await directChildPids(process.pid);
|
|
const starting = startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript,
|
|
});
|
|
const guardianPid = await waitForNewDirectChild(existingChildren);
|
|
await waitForStoppedProcess(guardianPid);
|
|
const rawLink = waitForDirectoryEntry(rawDirectory, path.basename(rawPath), () => {
|
|
process.kill(guardianPid, "SIGKILL");
|
|
});
|
|
process.kill(guardianPid, "SIGCONT");
|
|
|
|
await within(rawLink, 1_000, "pre-READY canonical raw link");
|
|
await expect(within(starting, 2_000, "pre-READY guardian rejection"))
|
|
.rejects.toThrow(/provider guardian/u);
|
|
const possibleTempLeaves = (await readdir(evidenceRoot)).filter((leaf) =>
|
|
leaf.startsWith(".vulnerability-report.json.guardian-")
|
|
);
|
|
expect(possibleTempLeaves).toEqual([]);
|
|
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
|
|
const retry = await startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
});
|
|
expect(path.basename(retry.sealedTempPath)).toMatch(
|
|
/^\.vulnerability-report\.json\.guardian-[0-9a-f]{32}\.tmp$/u,
|
|
);
|
|
await retry.abort();
|
|
await expect(readdir(rawDirectory)).resolves.toEqual([]);
|
|
await expect(readdir(evidenceRoot)).resolves.toEqual(["untrusted"]);
|
|
});
|
|
|
|
it("preserves an external raw canary created after startup checks", async () => {
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-external-canary-");
|
|
const rawPath = path.join(
|
|
workspace,
|
|
"provider-evidence/untrusted/vulnerability-report.json",
|
|
);
|
|
const { guardianScript, markerPath } = await createStalledGuardianFixture(workspace);
|
|
const starting = startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript,
|
|
});
|
|
const guardianPid = Number(await waitForFile(markerPath));
|
|
const canaryBytes = Buffer.from("external-canary\n");
|
|
const canaryHandle = await open(
|
|
rawPath,
|
|
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
0o600,
|
|
);
|
|
await canaryHandle.writeFile(canaryBytes);
|
|
const canaryIdentity = await canaryHandle.stat();
|
|
await canaryHandle.close();
|
|
|
|
process.kill(guardianPid, "SIGKILL");
|
|
await expect(within(starting, 2_000, "external-canary guardian rejection"))
|
|
.rejects.toThrow(/provider guardian/u);
|
|
expect(await readFile(rawPath)).toEqual(canaryBytes);
|
|
expect(await lstat(rawPath)).toMatchObject({
|
|
dev: canaryIdentity.dev,
|
|
ino: canaryIdentity.ino,
|
|
});
|
|
await rm(rawPath);
|
|
|
|
const retry = await startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 3_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
});
|
|
await retry.abort();
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted")))
|
|
.resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence")))
|
|
.resolves.toEqual(["untrusted"]);
|
|
});
|
|
|
|
it("allows exactly one same-kind guardian without deleting the winner", async () => {
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-concurrent-kind-");
|
|
const input = {
|
|
kind: "vulnerability" as const,
|
|
workspaceRoot: workspace,
|
|
leaseMs: 10_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
};
|
|
|
|
const results = await Promise.allSettled([
|
|
startProviderGuardian(input),
|
|
startProviderGuardian(input),
|
|
]);
|
|
const winners = results.filter((result) => result.status === "fulfilled");
|
|
const losers = results.filter((result) => result.status === "rejected");
|
|
expect(winners).toHaveLength(1);
|
|
expect(losers).toHaveLength(1);
|
|
const winner = winners[0]!.value;
|
|
const rawMetadata = await lstat(winner.rawPath);
|
|
expect({ dev: rawMetadata.dev, ino: rawMetadata.ino }).toEqual(winner.rawIdentity);
|
|
await winner.abort();
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted")))
|
|
.resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence")))
|
|
.resolves.toEqual(["untrusted"]);
|
|
|
|
const retry = await startProviderGuardian(input);
|
|
await retry.abort();
|
|
await expect(readdir(path.join(workspace, "provider-evidence/untrusted")))
|
|
.resolves.toEqual([]);
|
|
await expect(readdir(path.join(workspace, "provider-evidence")))
|
|
.resolves.toEqual(["untrusted"]);
|
|
});
|
|
|
|
it("exits nonzero after cleanup when the diagnostic stderr pipe is closed", async () => {
|
|
const {
|
|
decodeProviderGuardianReady,
|
|
encodeProviderGuardianGuard,
|
|
} = await import("../../scripts/lib/provider-guardian-protocol.ts");
|
|
const workspace = await createWorkspace("provider-guardian-closed-stderr-");
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawDirectory = path.join(evidenceRoot, "untrusted");
|
|
const nonce = Buffer.alloc(32, 0x63);
|
|
const child = spawnGuardian(workspace, nonce);
|
|
const completion = waitForChild(child);
|
|
const readyPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianGuard({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: Date.now() + 3_000,
|
|
}));
|
|
const ready = decodeProviderGuardianReady(
|
|
await within(readyPayload, 1_000, "closed-stderr guardian READY"),
|
|
nonce,
|
|
);
|
|
child.stderr!.destroy();
|
|
await new Promise<void>((resolve) => child.stderr!.once("close", resolve));
|
|
child.stdin!.end();
|
|
|
|
await expect(within(completion, 1_000, "closed-stderr guardian exit"))
|
|
.resolves.toEqual({ code: 125, signal: null });
|
|
await expect(lstat(path.join(rawDirectory, "vulnerability-report.json")))
|
|
.rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(path.join(evidenceRoot, ready.sealedTempLeaf)))
|
|
.rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("expires an uncommitted lease only after cleaning every owned object", async () => {
|
|
const {
|
|
decodeProviderGuardianReady,
|
|
encodeProviderGuardianGuard,
|
|
} = await import("../../scripts/lib/provider-guardian-protocol.ts");
|
|
const workspace = await createWorkspace("provider-guardian-deadline-v2-");
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawPath = path.join(evidenceRoot, "untrusted/vulnerability-report.json");
|
|
const nonce = Buffer.alloc(32, 0x58);
|
|
const child = spawnGuardian(workspace, nonce);
|
|
const completion = waitForChild(child);
|
|
const readyPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianGuard({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: Date.now() + 350,
|
|
}));
|
|
const ready = decodeProviderGuardianReady(
|
|
await within(readyPayload, 1_000, "deadline guardian READY"),
|
|
nonce,
|
|
);
|
|
const sealedTempPath = path.join(evidenceRoot, ready.sealedTempLeaf);
|
|
|
|
await expect(within(completion, 1_000, "guardian deadline"))
|
|
.resolves.toEqual({ code: null, signal: "SIGKILL" });
|
|
await expect(lstat(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readdir(evidenceRoot)).resolves.toEqual(["untrusted"]);
|
|
});
|
|
|
|
it("still publishes near the lease deadline when post-processing completes in time", async () => {
|
|
const { startProviderGuardian } = await import(
|
|
"../../scripts/lib/provider-guardian-client.ts"
|
|
);
|
|
const workspace = await createWorkspace("provider-guardian-near-timeout-");
|
|
const lease = await startProviderGuardian({
|
|
kind: "vulnerability",
|
|
workspaceRoot: workspace,
|
|
leaseMs: 2_000,
|
|
guardianScript: path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
});
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 1_000));
|
|
const bytes = Buffer.from('{"validated":"near-timeout"}\n');
|
|
|
|
await lease.publish(bytes);
|
|
await lease.commit();
|
|
await expect(lstat(lease.rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(readFile(lease.sealedPath)).resolves.toEqual(bytes);
|
|
});
|
|
|
|
it("signals guardian exit only while the provider scope latch is active", async () => {
|
|
const lifecycle = await import("../../scripts/lib/provider-guardian-client.ts") as
|
|
Record<string, unknown>;
|
|
expect(lifecycle.createProviderScopeGuardianLatch).toBeTypeOf("function");
|
|
const createLatch = lifecycle.createProviderScopeGuardianLatch as (
|
|
guardianExit: Promise<Error>,
|
|
) => Readonly<{
|
|
activeFailure: Promise<Error>;
|
|
close(): Promise<void>;
|
|
failure(): Error | undefined;
|
|
}>;
|
|
|
|
let failAtBoundary!: (error: Error) => void;
|
|
const boundaryExit = new Promise<Error>((resolve) => { failAtBoundary = resolve; });
|
|
const boundary = createLatch(boundaryExit);
|
|
const boundaryError = new Error("guardian died at the collection boundary");
|
|
failAtBoundary(boundaryError);
|
|
await boundary.close();
|
|
await expect(boundary.activeFailure).resolves.toBe(boundaryError);
|
|
expect(boundary.failure()).toBe(boundaryError);
|
|
|
|
let failActive!: (error: Error) => void;
|
|
const activeExit = new Promise<Error>((resolve) => { failActive = resolve; });
|
|
const active = createLatch(activeExit);
|
|
const activeError = new Error("guardian died while scope active");
|
|
failActive(activeError);
|
|
await expect(active.activeFailure).resolves.toBe(activeError);
|
|
expect(active.failure()).toBe(activeError);
|
|
|
|
let failLate!: (error: Error) => void;
|
|
const lateExit = new Promise<Error>((resolve) => { failLate = resolve; });
|
|
const late = createLatch(lateExit);
|
|
await late.close();
|
|
const lateError = new Error("guardian died after scope collection");
|
|
failLate(lateError);
|
|
await Promise.resolve();
|
|
expect(late.failure()).toBe(lateError);
|
|
await expect(Promise.race([
|
|
late.activeFailure.then(() => "active"),
|
|
new Promise<string>((resolve) => setTimeout(() => resolve("inactive"), 25)),
|
|
])).resolves.toBe("inactive");
|
|
});
|
|
|
|
it("rejects a guardian lease whose derived sealed path differs from the configured target", async () => {
|
|
const lifecycle = await import("../../scripts/lib/provider-guardian-client.ts") as
|
|
Record<string, unknown>;
|
|
expect(lifecycle.assertProviderGuardianLeasePaths).toBeTypeOf("function");
|
|
const assertPaths = lifecycle.assertProviderGuardianLeasePaths as (
|
|
lease: Readonly<{ rawPath: string; sealedPath: string }>,
|
|
expected: Readonly<{ rawPath: string; sealedPath: string }>,
|
|
) => void;
|
|
const canonical = {
|
|
rawPath: "/workspace/provider-evidence/untrusted/vulnerability-report.json",
|
|
sealedPath: "/workspace/provider-evidence/vulnerability-report.json",
|
|
};
|
|
|
|
expect(() => assertPaths(canonical, canonical)).not.toThrow();
|
|
expect(() => assertPaths(canonical, {
|
|
...canonical,
|
|
sealedPath: "/workspace/provider-evidence/configured-alias.json",
|
|
})).toThrow(/noncanonical sealed path/u);
|
|
});
|
|
});
|
|
|
|
async function createWorkspace(prefix: string): Promise<string> {
|
|
const workspace = await mkdtemp(path.join(tmpdir(), prefix));
|
|
roots.push(workspace);
|
|
await mkdir(path.join(workspace, "provider-evidence/untrusted"), { recursive: true });
|
|
return workspace;
|
|
}
|
|
|
|
async function createStalledGuardianFixture(
|
|
workspace: string,
|
|
): Promise<Readonly<{ guardianScript: string; markerPath: string }>> {
|
|
const guardianScript = path.join(workspace, "stalled-guardian.mjs");
|
|
const markerPath = path.join(workspace, "guardian-spawned");
|
|
await writeFile(guardianScript, [
|
|
'import { writeFileSync } from "node:fs";',
|
|
'import path from "node:path";',
|
|
'writeFileSync(path.join(process.cwd(), "guardian-spawned"), String(process.pid));',
|
|
"setInterval(() => undefined, 1_000);",
|
|
"",
|
|
].join("\n"));
|
|
return { guardianScript, markerPath };
|
|
}
|
|
|
|
async function createPausedGuardianFixture(workspace: string): Promise<string> {
|
|
const guardianScript = path.join(workspace, "paused-guardian.mjs");
|
|
const realGuardian = pathToFileURL(
|
|
path.resolve("scripts/lib/provider-raw-guardian.ts"),
|
|
).href;
|
|
await writeFile(guardianScript, [
|
|
'process.kill(process.pid, "SIGSTOP");',
|
|
`await import(${JSON.stringify(realGuardian)});`,
|
|
"",
|
|
].join("\n"));
|
|
return guardianScript;
|
|
}
|
|
|
|
async function waitForFile(target: string): Promise<Buffer> {
|
|
const deadline = Date.now() + 1_000;
|
|
while (Date.now() <= deadline) {
|
|
try {
|
|
return await readFile(target);
|
|
} catch (error) {
|
|
if (!hasErrorCode(error, "ENOENT")) throw error;
|
|
}
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
}
|
|
throw new Error(`file did not appear: ${target}`);
|
|
}
|
|
|
|
type PublishedGuardian = Readonly<{
|
|
child: ReturnType<typeof spawn>;
|
|
completion: Promise<Readonly<{
|
|
code: number | null;
|
|
signal: NodeJS.Signals | null;
|
|
}>>;
|
|
rawPath: string;
|
|
sealedPath: string;
|
|
sealedTempPath: string;
|
|
}>;
|
|
|
|
async function establishPublishedGuardian(
|
|
workspace: string,
|
|
nonce: Buffer,
|
|
): Promise<PublishedGuardian> {
|
|
const {
|
|
decodeProviderGuardianPublished,
|
|
decodeProviderGuardianReady,
|
|
encodeProviderGuardianGuard,
|
|
encodeProviderGuardianPublish,
|
|
} = await import("../../scripts/lib/provider-guardian-protocol.ts");
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawPath = path.join(evidenceRoot, "untrusted/vulnerability-report.json");
|
|
const sealedPath = path.join(evidenceRoot, "vulnerability-report.json");
|
|
const child = spawnGuardian(workspace, nonce);
|
|
const completion = waitForChild(child);
|
|
const readyPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianGuard({
|
|
kind: "vulnerability",
|
|
nonce,
|
|
deadlineEpochMs: Date.now() + 3_000,
|
|
}));
|
|
const ready = decodeProviderGuardianReady(
|
|
await within(readyPayload, 1_000, "guardian READY"),
|
|
nonce,
|
|
);
|
|
const sealedTempPath = path.join(evidenceRoot, ready.sealedTempLeaf);
|
|
const bytes = Buffer.from('{"validated":"pending-commit"}\n');
|
|
const handle = await open(sealedTempPath, constants.O_WRONLY | constants.O_NOFOLLOW);
|
|
try {
|
|
await handle.writeFile(bytes);
|
|
await handle.chmod(0o400);
|
|
await handle.sync();
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
const publishedPayload = readFrame(child.stdout!);
|
|
child.stdin!.write(encodeProviderGuardianPublish({
|
|
nonce,
|
|
sealedDev: ready.sealedDev,
|
|
sealedIno: ready.sealedIno,
|
|
size: bytes.byteLength,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
}));
|
|
decodeProviderGuardianPublished(
|
|
await within(publishedPayload, 1_000, "guardian PUBLISHED"),
|
|
nonce,
|
|
{ dev: ready.sealedDev, ino: ready.sealedIno },
|
|
);
|
|
return { child, completion, rawPath, sealedPath, sealedTempPath };
|
|
}
|
|
|
|
async function assertTransactionAbsent(transaction: PublishedGuardian): Promise<void> {
|
|
await expect(lstat(transaction.rawPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(transaction.sealedPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(lstat(transaction.sealedTempPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
}
|
|
|
|
async function readFrame(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
|
return await new Promise((resolve, reject) => {
|
|
let pending = Buffer.alloc(0);
|
|
const onData = (chunk: Buffer | string): void => {
|
|
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
if (pending.byteLength < 4) return;
|
|
const payloadBytes = pending.readUInt32BE(0);
|
|
if (pending.byteLength < payloadBytes + 4) return;
|
|
cleanup();
|
|
if (pending.byteLength !== payloadBytes + 4) {
|
|
reject(new Error("guardian acknowledgement contained trailing bytes"));
|
|
return;
|
|
}
|
|
resolve(pending.subarray(4));
|
|
};
|
|
const onEnd = (): void => {
|
|
cleanup();
|
|
reject(new Error("guardian closed before acknowledgement"));
|
|
};
|
|
const cleanup = (): void => {
|
|
stream.removeListener("data", onData);
|
|
stream.removeListener("end", onEnd);
|
|
};
|
|
stream.on("data", onData);
|
|
stream.once("end", onEnd);
|
|
});
|
|
}
|
|
|
|
async function waitForChild(
|
|
child: ReturnType<typeof spawn>,
|
|
): Promise<Readonly<{ code: number | null; signal: NodeJS.Signals | null }>> {
|
|
return await new Promise((resolve, reject) => {
|
|
child.once("error", reject);
|
|
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
});
|
|
}
|
|
|
|
function spawnGuardian(
|
|
workspace: string,
|
|
nonce: Buffer = Buffer.alloc(32, 0x7d),
|
|
): ReturnType<typeof spawn> {
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawDirectory = path.join(evidenceRoot, "untrusted");
|
|
const noncePrefix = nonce.subarray(0, 16).toString("hex");
|
|
const openedFds: number[] = [];
|
|
try {
|
|
openedFds.push(openSync(
|
|
rawDirectory,
|
|
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
|
));
|
|
openedFds.push(openSync(
|
|
evidenceRoot,
|
|
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
|
));
|
|
openedFds.push(openSync(
|
|
path.join(
|
|
rawDirectory,
|
|
`.vulnerability-report.json.guardian-${noncePrefix}.raw.tmp`,
|
|
),
|
|
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
0o600,
|
|
));
|
|
openedFds.push(openSync(
|
|
path.join(
|
|
evidenceRoot,
|
|
`.vulnerability-report.json.guardian-${noncePrefix}.tmp`,
|
|
),
|
|
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
0o600,
|
|
));
|
|
return spawn(process.execPath, [path.resolve("scripts/lib/provider-raw-guardian.ts")], {
|
|
cwd: workspace,
|
|
env: {},
|
|
stdio: ["pipe", "pipe", "pipe", ...openedFds],
|
|
});
|
|
} finally {
|
|
for (const fd of openedFds) closeSync(fd);
|
|
}
|
|
}
|
|
|
|
function spawnGuardianWithPrivateModes(
|
|
workspace: string,
|
|
nonce: Buffer,
|
|
modes: Readonly<{ rawMode: number; sealedMode: number }>,
|
|
): Readonly<{
|
|
child: ReturnType<typeof spawn>;
|
|
rawStagingPath: string;
|
|
sealedTempPath: string;
|
|
}> {
|
|
const evidenceRoot = path.join(workspace, "provider-evidence");
|
|
const rawDirectory = path.join(evidenceRoot, "untrusted");
|
|
const noncePrefix = nonce.subarray(0, 16).toString("hex");
|
|
const rawStagingPath = path.join(
|
|
rawDirectory,
|
|
`.vulnerability-report.json.guardian-${noncePrefix}.raw.tmp`,
|
|
);
|
|
const sealedTempPath = path.join(
|
|
evidenceRoot,
|
|
`.vulnerability-report.json.guardian-${noncePrefix}.tmp`,
|
|
);
|
|
const openedFds: number[] = [];
|
|
try {
|
|
openedFds.push(openSync(
|
|
rawDirectory,
|
|
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
|
));
|
|
openedFds.push(openSync(
|
|
evidenceRoot,
|
|
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
|
));
|
|
const rawStagingFd = openSync(
|
|
rawStagingPath,
|
|
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
0o600,
|
|
);
|
|
openedFds.push(rawStagingFd);
|
|
fchmodSync(rawStagingFd, modes.rawMode);
|
|
const sealedTempFd = openSync(
|
|
sealedTempPath,
|
|
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
0o600,
|
|
);
|
|
openedFds.push(sealedTempFd);
|
|
fchmodSync(sealedTempFd, modes.sealedMode);
|
|
const child = spawn(process.execPath, [path.resolve("scripts/lib/provider-raw-guardian.ts")], {
|
|
cwd: workspace,
|
|
env: {},
|
|
stdio: ["pipe", "pipe", "pipe", ...openedFds],
|
|
});
|
|
return { child, rawStagingPath, sealedTempPath };
|
|
} finally {
|
|
for (const fd of openedFds) closeSync(fd);
|
|
}
|
|
}
|
|
|
|
async function waitForDirectoryEntry(
|
|
directory: string,
|
|
expectedLeaf: string,
|
|
onEntry: () => void,
|
|
): Promise<void> {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const watcher = watch(directory, (_event, filename) => {
|
|
if (filename?.toString() !== expectedLeaf) return;
|
|
try {
|
|
onEntry();
|
|
resolve();
|
|
} catch (error) {
|
|
reject(error);
|
|
} finally {
|
|
watcher.close();
|
|
}
|
|
});
|
|
watcher.once("error", (error) => {
|
|
watcher.close();
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function within<T>(operation: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
let timer: NodeJS.Timeout | undefined;
|
|
try {
|
|
return await Promise.race([
|
|
operation,
|
|
new Promise<never>((_resolve, reject) => {
|
|
timer = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
|
|
}),
|
|
]);
|
|
} finally {
|
|
if (timer) clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function directChildPids(parentPid: number): Promise<Set<number>> {
|
|
const value = await readFile(`/proc/${parentPid}/task/${parentPid}/children`, "utf8");
|
|
return new Set(value.trim().split(/\s+/u).filter(Boolean).map(Number));
|
|
}
|
|
|
|
async function waitForNewDirectChild(previous: ReadonlySet<number>): Promise<number> {
|
|
const deadline = Date.now() + 1_000;
|
|
while (Date.now() <= deadline) {
|
|
for (const pid of await directChildPids(process.pid)) {
|
|
if (!previous.has(pid)) return pid;
|
|
}
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
}
|
|
throw new Error("provider guardian child did not appear");
|
|
}
|
|
|
|
async function waitForStoppedProcess(pid: number): Promise<void> {
|
|
const deadline = Date.now() + 1_000;
|
|
while (Date.now() <= deadline) {
|
|
const status = await readFile(`/proc/${pid}/status`, "utf8");
|
|
if (/^State:\s+T/mu.test(status)) return;
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
}
|
|
throw new Error("provider guardian child did not stop before bootstrap");
|
|
}
|
|
|
|
function hasErrorCode(error: unknown, code: string): boolean {
|
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
}
|