Files

764 lines
24 KiB
TypeScript

import { spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { constants } from "node:fs";
import { lstat, open, type FileHandle } from "node:fs/promises";
import path from "node:path";
import {
decodeProviderGuardianPublished,
decodeProviderGuardianReady,
encodeProviderGuardianCommit,
encodeProviderGuardianGuard,
encodeProviderGuardianPublish,
MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES,
MAX_PROVIDER_GUARDIAN_LEASE_MS,
MAX_PROVIDER_SEALED_BYTES,
providerGuardianRawStagingLeaf,
providerGuardianSealedTempLeaf,
type ProviderGuardianKind,
} from "./provider-guardian-protocol.ts";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
const RESPONSE_TIMEOUT_MS = 5_000;
const CLOSE_TIMEOUT_MS = 5_000;
const MAX_CONTROL_OUTPUT_BYTES = 4_096;
type OwnedIdentity = Readonly<{ dev: number; ino: number }>;
type RecoveryAuthority = Readonly<{
rawDirectoryHandle: FileHandle;
evidenceDirectoryHandle: FileHandle;
rawStagingHandle: FileHandle;
sealedTempHandle: FileHandle;
rawIdentity: OwnedIdentity;
sealedIdentity: OwnedIdentity;
rawStagingPinnedPath: string;
rawPinnedPath: string;
sealedTempPinnedPath: string;
sealedPinnedPath: string;
}>;
export type ProviderGuardianLease = Readonly<{
pid: number;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedPath: string;
sealedTempPath: string;
sealedIdentity: OwnedIdentity;
prematureExit: Promise<Error>;
publish(bytes: Buffer): Promise<void>;
commit(): Promise<void>;
abort(): Promise<void>;
}>;
export type StartProviderGuardianInput = Readonly<{
kind: ProviderGuardianKind;
workspaceRoot: string;
leaseMs: number;
guardianScript: string;
}>;
export type ProviderScopeGuardianLatch = Readonly<{
activeFailure: Promise<Error>;
close(): Promise<void>;
failure(): Error | undefined;
}>;
export function createProviderScopeGuardianLatch(
guardianExit: Promise<Error>,
): ProviderScopeGuardianLatch {
let active = true;
let closing: Promise<void> | undefined;
let observedFailure: Error | undefined;
let signalActiveFailure!: (error: Error) => void;
const activeFailure = new Promise<Error>((resolve) => { signalActiveFailure = resolve; });
void guardianExit.then((error) => {
observedFailure = error;
if (active) signalActiveFailure(error);
});
return Object.freeze({
activeFailure,
close: () => {
closing ??= Promise.resolve().then(() => { active = false; });
return closing;
},
failure: () => observedFailure,
});
}
export function assertProviderGuardianLeasePaths(
lease: Readonly<{ rawPath: string; sealedPath: string }>,
expected: Readonly<{ rawPath: string; sealedPath: string }>,
): void {
if (lease.rawPath !== expected.rawPath) {
throw new Error("provider guardian returned a noncanonical raw path");
}
if (lease.sealedPath !== expected.sealedPath) {
throw new Error("provider guardian returned a noncanonical sealed path");
}
}
type GuardianResult = Readonly<{
code: number | null;
error?: Error;
signal: NodeJS.Signals | null;
}>;
export async function startProviderGuardian(
input: StartProviderGuardianInput,
): Promise<ProviderGuardianLease> {
if (
(input.kind !== "vulnerability" && input.kind !== "provenance") ||
!path.isAbsolute(input.workspaceRoot) || !path.isAbsolute(input.guardianScript) ||
!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0 ||
input.leaseMs > MAX_PROVIDER_GUARDIAN_LEASE_MS
) {
throw new TypeError("provider guardian client input is invalid");
}
const rawLeaf = input.kind === "vulnerability"
? "vulnerability-report.json"
: "provenance-attestation.json";
const evidenceRoot = path.resolve(input.workspaceRoot, "provider-evidence");
const rawDirectory = path.join(evidenceRoot, "untrusted");
const rawPath = path.join(evidenceRoot, "untrusted", rawLeaf);
const sealedPath = path.join(evidenceRoot, rawLeaf);
const nonce = randomBytes(32);
const rawStagingLeaf = providerGuardianRawStagingLeaf(input.kind, nonce);
const sealedTempLeaf = providerGuardianSealedTempLeaf(input.kind, nonce);
const sealedTempPath = path.join(evidenceRoot, sealedTempLeaf);
const recovery = await openRecoveryAuthority({
rawDirectory,
evidenceRoot,
rawLeaf,
rawStagingLeaf,
sealedLeaf: rawLeaf,
sealedTempLeaf,
});
let child: ReturnType<typeof spawn>;
try {
await assertRecoveryLeavesMissing(recovery);
child = spawn(process.execPath, [input.guardianScript], {
cwd: input.workspaceRoot,
env: {},
stdio: [
"pipe",
"pipe",
"pipe",
recovery.rawDirectoryHandle.fd,
recovery.evidenceDirectoryHandle.fd,
recovery.rawStagingHandle.fd,
recovery.sealedTempHandle.fd,
],
});
} catch (error) {
return await closeRecoveryAndThrow(recovery, error);
}
if (!child.pid || !child.stdin || !child.stdout || !child.stderr) {
child.kill("SIGKILL");
return await closeRecoveryAndThrow(
recovery,
new Error("provider guardian process pipes are unavailable"),
);
}
let state: "starting" | "guarding" | "publishing" | "published" |
"committing" | "aborting" | "terminated" = "starting";
let stderr = Buffer.alloc(0);
let inputError: Error | undefined;
child.stdin.once("error", (error) => { inputError = error; });
child.stderr.on("data", (chunk: Buffer | string) => {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
if (stderr.byteLength < MAX_CONTROL_OUTPUT_BYTES) {
stderr = Buffer.concat([stderr, bytes.subarray(0, MAX_CONTROL_OUTPUT_BYTES - stderr.byteLength)]);
}
});
const completion = guardianCompletion(child);
let signalPrematureExit!: (error: Error) => void;
const prematureExit = new Promise<Error>((resolve) => { signalPrematureExit = resolve; });
void completion.then((result) => {
if (state === "guarding" || state === "publishing" || state === "published") {
signalPrematureExit(guardianCloseError(result, stderr));
}
});
let ready: ReturnType<typeof decodeProviderGuardianReady>;
try {
const readyResponse = waitForFrame(child.stdout, completion, "READY");
child.stdin.write(encodeProviderGuardianGuard({
kind: input.kind,
nonce,
deadlineEpochMs: Date.now() + input.leaseMs,
}));
ready = decodeProviderGuardianReady(await readyResponse, nonce);
if (
ready.sealedTempLeaf !== sealedTempLeaf ||
ready.rawDev !== recovery.rawIdentity.dev ||
ready.rawIno !== recovery.rawIdentity.ino ||
ready.sealedDev !== recovery.sealedIdentity.dev ||
ready.sealedIno !== recovery.sealedIdentity.ino
) {
throw new TypeError("provider guardian READY identity is invalid for its allocation");
}
await assertPinnedLeafIdentity(recovery.rawPinnedPath, {
dev: ready.rawDev,
ino: ready.rawIno,
}, 0o600);
await assertPinnedLeafIdentity(recovery.sealedTempPinnedPath, {
dev: ready.sealedDev,
ino: ready.sealedIno,
}, 0o600);
await assertPinnedLeafMissing(recovery.rawStagingPinnedPath);
if (child.exitCode !== null || child.signalCode !== null) {
throw guardianCloseError(await completion, stderr);
}
state = "guarding";
} catch (error) {
state = "aborting";
child.stdin.end();
const failures = [toError(error)];
try {
await waitForClose(completion, child);
} catch (closeError) {
failures.push(toError(closeError));
}
try {
await cleanupStartupRecovery(recovery);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
state = "terminated";
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian startup failed", { cause: error });
}
throw failures[0]!;
}
const rawIdentity = Object.freeze({ dev: ready.rawDev, ino: ready.rawIno });
const sealedIdentity = Object.freeze({ dev: ready.sealedDev, ino: ready.sealedIno });
const fallback = Object.freeze({
rawStagingPath: recovery.rawStagingPinnedPath,
rawPath: recovery.rawPinnedPath,
rawIdentity,
sealedPath: recovery.sealedPinnedPath,
sealedTempPath: recovery.sealedTempPinnedPath,
sealedIdentity,
});
const publish = async (bytes: Buffer): Promise<void> => {
if (state !== "guarding") throw new Error("provider guardian lease is not ready to publish");
if (!Buffer.isBuffer(bytes) || bytes.byteLength <= 0 || bytes.byteLength > MAX_PROVIDER_SEALED_BYTES) {
throw new TypeError("provider guardian sealed bytes are invalid");
}
state = "publishing";
try {
await writePinnedSealedBytes(recovery.sealedTempHandle, sealedIdentity, bytes);
const publishedResponse = waitForFrame(child.stdout!, completion, "PUBLISHED");
child.stdin!.write(encodeProviderGuardianPublish({
nonce,
sealedDev: sealedIdentity.dev,
sealedIno: sealedIdentity.ino,
size: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
}));
decodeProviderGuardianPublished(
await publishedResponse,
nonce,
sealedIdentity,
);
if (inputError) throw inputError;
state = "published";
} catch (error) {
state = "guarding";
throw error;
}
};
const commit = async (): Promise<void> => {
if (state !== "published") throw new Error("provider guardian lease is not ready to commit");
state = "committing";
child.stdin!.write(encodeProviderGuardianCommit(nonce));
child.stdin!.end();
let result: GuardianResult;
try {
result = await waitForClose(completion, child);
} catch (error) {
state = "terminated";
return await cleanupFallbackCloseAndThrow(fallback, recovery, error);
}
state = "terminated";
if (inputError) return await cleanupFallbackCloseAndThrow(fallback, recovery, inputError);
if (result.error || result.code !== 0 || result.signal !== null) {
return await cleanupFallbackCloseAndThrow(
fallback,
recovery,
guardianCloseError(result, stderr),
);
}
await closeRecoveryAuthority(recovery);
};
const abort = async (): Promise<void> => {
if (state !== "guarding" && state !== "published") {
throw new Error("provider guardian lease already terminated");
}
state = "aborting";
child.stdin!.end();
let closeError: unknown;
try {
await waitForClose(completion, child);
} catch (error) {
closeError = error;
}
state = "terminated";
if (closeError) return await cleanupFallbackCloseAndThrow(fallback, recovery, closeError);
await cleanupFallbackAndClose(fallback, recovery);
};
return Object.freeze({
pid: child.pid,
rawPath,
rawIdentity,
sealedPath,
sealedTempPath,
sealedIdentity,
prematureExit,
publish,
commit,
abort,
});
}
async function writePinnedSealedBytes(
handle: FileHandle,
identity: OwnedIdentity,
bytes: Buffer,
): Promise<void> {
assertPinnedMetadata(await handle.stat(), identity, 0o600, 0);
await handle.truncate(0);
await handle.writeFile(bytes);
await handle.chmod(0o400);
await handle.sync();
assertPinnedMetadata(await handle.stat(), identity, 0o400, bytes.byteLength);
}
function assertPinnedMetadata(
metadata: Awaited<ReturnType<Awaited<ReturnType<typeof open>>["stat"]>>,
identity: OwnedIdentity,
mode: number,
size: number,
): void {
if (
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
(Number(metadata.mode) & 0o777) !== mode || Number(metadata.size) !== size
) {
throw new TypeError("provider guardian sealed temp identity changed");
}
}
function guardianCompletion(child: ReturnType<typeof spawn>): Promise<GuardianResult> {
return new Promise((resolve) => {
child.once("error", (error) => resolve({ code: null, error, signal: null }));
child.once("close", (code, signal) => resolve({ code, signal }));
});
}
async function waitForFrame(
stdout: NodeJS.ReadableStream,
completion: Promise<GuardianResult>,
label: string,
): Promise<Buffer> {
let timer: NodeJS.Timeout | undefined;
let pending = Buffer.alloc(0);
const response = new Promise<Buffer>((resolve, reject) => {
const onData = (chunk: Buffer | string): void => {
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (pending.byteLength > MAX_CONTROL_OUTPUT_BYTES) {
reject(new Error(`provider guardian ${label} output exceeded its bound`));
return;
}
if (pending.byteLength < 4) return;
const payloadBytes = pending.readUInt32BE(0);
if (payloadBytes <= 0 || payloadBytes > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
reject(new Error(`provider guardian ${label} frame length is invalid`));
return;
}
if (pending.byteLength < payloadBytes + 4) return;
if (pending.byteLength !== payloadBytes + 4) {
reject(new Error(`provider guardian ${label} output has trailing bytes`));
return;
}
resolve(pending.subarray(4));
};
stdout.on("data", onData);
});
try {
return await Promise.race([
response,
completion.then((result) => { throw guardianCloseError(result, Buffer.alloc(0)); }),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new Error(`provider guardian ${label} timed out`)),
RESPONSE_TIMEOUT_MS,
);
}),
]);
} finally {
if (timer) clearTimeout(timer);
stdout.removeAllListeners("data");
}
}
async function waitForClose(
completion: Promise<GuardianResult>,
child: ReturnType<typeof spawn>,
): Promise<GuardianResult> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
completion,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error("provider guardian did not close within its bound"));
}, CLOSE_TIMEOUT_MS);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
type FallbackIdentity = Readonly<{
rawStagingPath: string;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedPath: string;
sealedTempPath: string;
sealedIdentity: OwnedIdentity;
}>;
async function cleanupFallback(input: FallbackIdentity): Promise<void> {
const failures: Error[] = [];
for (const target of [
{ path: input.rawStagingPath, identity: input.rawIdentity },
{ path: input.rawPath, identity: input.rawIdentity },
{ path: input.sealedTempPath, identity: input.sealedIdentity },
{ path: input.sealedPath, identity: input.sealedIdentity },
]) {
try {
await cleanupOwnedProviderReport({
reportPath: target.path,
reportDev: target.identity.dev,
reportIno: target.identity.ino,
});
} catch (error) {
failures.push(toError(error));
}
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian fallback cleanup failed", {
cause: failures[0],
});
}
}
async function cleanupFallbackCloseAndThrow(
fallback: FallbackIdentity,
recovery: RecoveryAuthority,
primaryError: unknown,
): Promise<never> {
const failures = [toError(primaryError)];
try {
await cleanupFallback(fallback);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian failure and recovery failed", {
cause: failures[0],
});
}
throw failures[0]!;
}
async function cleanupFallbackAndClose(
fallback: FallbackIdentity,
recovery: RecoveryAuthority,
): Promise<void> {
const failures: Error[] = [];
try {
await cleanupFallback(fallback);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian abort recovery failed", {
cause: failures[0],
});
}
}
async function openRecoveryAuthority(input: Readonly<{
rawDirectory: string;
evidenceRoot: string;
rawLeaf: string;
rawStagingLeaf: string;
sealedLeaf: string;
sealedTempLeaf: string;
}>): Promise<RecoveryAuthority> {
let rawDirectoryHandle: FileHandle | undefined;
let evidenceDirectoryHandle: FileHandle | undefined;
let rawStagingHandle: FileHandle | undefined;
let sealedTempHandle: FileHandle | undefined;
let rawIdentity: OwnedIdentity | undefined;
let sealedIdentity: OwnedIdentity | undefined;
let rawStagingPinnedPath: string | undefined;
let rawPinnedPath: string | undefined;
let sealedTempPinnedPath: string | undefined;
let sealedPinnedPath: string | undefined;
try {
rawDirectoryHandle = await open(
input.rawDirectory,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
await assertPinnedDirectory(rawDirectoryHandle, input.rawDirectory, "raw");
evidenceDirectoryHandle = await open(
input.evidenceRoot,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
await assertPinnedDirectory(evidenceDirectoryHandle, input.evidenceRoot, "evidence");
rawStagingPinnedPath =
`/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawStagingLeaf}`;
rawPinnedPath = `/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawLeaf}`;
sealedTempPinnedPath =
`/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedTempLeaf}`;
sealedPinnedPath = `/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedLeaf}`;
rawStagingHandle = await open(
rawStagingPinnedPath,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
const rawMetadata = await rawStagingHandle.stat();
rawIdentity = Object.freeze({ dev: rawMetadata.dev, ino: rawMetadata.ino });
assertAllocatedPrivateMetadata(rawMetadata, rawIdentity, "raw staging");
await assertPinnedLeafIdentity(rawStagingPinnedPath, rawIdentity, 0o600);
sealedTempHandle = await open(
sealedTempPinnedPath,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
const sealedMetadata = await sealedTempHandle.stat();
sealedIdentity = Object.freeze({ dev: sealedMetadata.dev, ino: sealedMetadata.ino });
assertAllocatedPrivateMetadata(sealedMetadata, sealedIdentity, "sealed temp");
await assertPinnedLeafIdentity(sealedTempPinnedPath, sealedIdentity, 0o600);
return Object.freeze({
rawDirectoryHandle,
evidenceDirectoryHandle,
rawStagingHandle,
sealedTempHandle,
rawIdentity,
sealedIdentity,
rawStagingPinnedPath,
rawPinnedPath,
sealedTempPinnedPath,
sealedPinnedPath,
});
} catch (error) {
const failures = [toError(error)];
for (const target of [
{ path: rawStagingPinnedPath, identity: rawIdentity },
{ path: rawPinnedPath, identity: rawIdentity },
{ path: sealedTempPinnedPath, identity: sealedIdentity },
{ path: sealedPinnedPath, identity: sealedIdentity },
]) {
if (!target.path || !target.identity) continue;
try {
await cleanupOwnedProviderReport({
reportPath: target.path,
reportDev: target.identity.dev,
reportIno: target.identity.ino,
});
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
}
for (const handle of [
sealedTempHandle,
rawStagingHandle,
evidenceDirectoryHandle,
rawDirectoryHandle,
]) {
if (!handle) continue;
try { await handle.close(); } catch (closeError) { failures.push(toError(closeError)); }
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian recovery setup failed", {
cause: error,
});
}
throw failures[0]!;
}
}
function assertAllocatedPrivateMetadata(
metadata: Awaited<ReturnType<FileHandle["stat"]>>,
identity: OwnedIdentity,
label: string,
): void {
if (
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
(Number(metadata.mode) & 0o777) !== 0o600 || Number(metadata.size) !== 0
) {
throw new TypeError(`provider guardian ${label} allocation is invalid`);
}
}
async function assertPinnedDirectory(
handle: FileHandle,
canonicalPath: string,
label: string,
): Promise<void> {
const [descriptorMetadata, pathMetadata] = await Promise.all([
handle.stat(),
lstat(canonicalPath),
]);
if (
!descriptorMetadata.isDirectory() || !pathMetadata.isDirectory() ||
pathMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathMetadata.dev ||
descriptorMetadata.ino !== pathMetadata.ino
) {
throw new TypeError(`provider guardian ${label} recovery directory identity changed`);
}
}
async function assertRecoveryLeavesMissing(recovery: RecoveryAuthority): Promise<void> {
await assertPinnedLeafMissing(recovery.rawPinnedPath);
await assertPinnedLeafMissing(recovery.sealedPinnedPath);
assertAllocatedPrivateMetadata(
await recovery.rawStagingHandle.stat(),
recovery.rawIdentity,
"raw staging",
);
assertAllocatedPrivateMetadata(
await recovery.sealedTempHandle.stat(),
recovery.sealedIdentity,
"sealed temp",
);
await assertPinnedLeafIdentity(
recovery.rawStagingPinnedPath,
recovery.rawIdentity,
0o600,
);
await assertPinnedLeafIdentity(
recovery.sealedTempPinnedPath,
recovery.sealedIdentity,
0o600,
);
}
async function assertPinnedLeafMissing(target: string): Promise<void> {
try {
await lstat(target);
throw new Error("provider guardian transaction leaf already exists");
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
}
async function assertPinnedLeafIdentity(
target: string,
identity: OwnedIdentity,
mode: number,
): Promise<void> {
const metadata = await lstat(target);
if (
!metadata.isFile() || metadata.isSymbolicLink() || metadata.dev !== identity.dev ||
metadata.ino !== identity.ino || metadata.nlink !== 1 ||
(metadata.mode & 0o777) !== mode || metadata.size !== 0
) {
throw new TypeError("provider guardian READY identity changed");
}
}
async function cleanupStartupRecovery(recovery: RecoveryAuthority): Promise<void> {
await cleanupFallback({
rawStagingPath: recovery.rawStagingPinnedPath,
rawPath: recovery.rawPinnedPath,
rawIdentity: recovery.rawIdentity,
sealedTempPath: recovery.sealedTempPinnedPath,
sealedPath: recovery.sealedPinnedPath,
sealedIdentity: recovery.sealedIdentity,
});
}
async function closeRecoveryAuthority(recovery: RecoveryAuthority): Promise<void> {
const failures: Error[] = [];
for (const handle of [
recovery.rawStagingHandle,
recovery.sealedTempHandle,
recovery.rawDirectoryHandle,
recovery.evidenceDirectoryHandle,
]) {
try { await handle.close(); } catch (error) { failures.push(toError(error)); }
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian recovery directory close failed", {
cause: failures[0],
});
}
}
async function closeRecoveryAndThrow(
recovery: RecoveryAuthority,
primaryError: unknown,
): Promise<never> {
const failures = [toError(primaryError)];
try {
await cleanupStartupRecovery(recovery);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 1) {
throw new AggregateError(failures,
"provider guardian failure and recovery close failed", { cause: failures[0] });
}
throw failures[0]!;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
function guardianCloseError(result: GuardianResult, stderr: Buffer): Error {
if (result.error) return result.error;
const detail = stderr.toString("utf8").trim();
return new Error(
`provider guardian failed: exit=${result.code ?? "none"}, signal=${result.signal ?? "none"}${detail ? `, output=${detail}` : ""}`,
);
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}