From 8f62eef8f13e199f7a98d8a9ec27d617385e6fde Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 11 Oct 2026 06:00:00 -0500 Subject: [PATCH] diagnostics: identify closed provider module rejection fences --- .../runner-core/src/acpx_sidecar_transport.rs | 81 ++++++++++++++++++- .../src/cli/acpx-sidecar-input.test.ts | 15 ++++ .../src/cli/acpx-sidecar-input.ts | 12 ++- .../acpx/installation-integrity.test.ts | 18 ++++- .../drivers/acpx/installation-integrity.ts | 45 ++++++----- 5 files changed, 141 insertions(+), 30 deletions(-) diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index ade615f7ff..8e5eb1e699 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -458,10 +458,11 @@ impl AcpxSidecarTransport { let error = response.error.expect("failed response has validated error"); return Ok(CommandOutcome::Rejected(LocalRunnerError::invalid( format!( - "ACPX sidecar command {} was rejected (retryable={}, classification={}){}", + "ACPX sidecar command {} was rejected (retryable={}, classification={}){}{}", command.as_str(), error.retryable, response_error_classification(&error), + response_error_module_fence(&error).map(|fence| format!(" moduleFence={fence}")).unwrap_or_default(), self.diagnostic_suffix(), ), ))); @@ -923,7 +924,34 @@ fn stderr_diagnostic_categories(value: &str) -> BTreeSet<&'static str> { categories } +// Only fixed loader fence codes may cross the private provider-stderr boundary. +fn response_error_module_fence(error: &ResponseError) -> Option<&'static str> { + match error.code.as_str() { + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.RESOLUTION" => Some("resolution"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.OPEN" => Some("open"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.BOUNDS" => Some("bounds"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.ANCESTRY" => Some("ancestry"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.FORMAT" => Some("format"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.PATH_IDENTITY" => Some("path_identity"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.READ_RESULT" => Some("read_result"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.SIZE" => Some("size"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.DEVICE" => Some("device"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.INODE" => Some("inode"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.MODE" => Some("mode"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.UID" => Some("uid"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.GID" => Some("gid"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT" => Some("link_count"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.MTIME" => Some("mtime"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.BYTE_CONTINUITY" => Some("byte_continuity"), + "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.REPEATED_CTIME" => Some("repeated_ctime"), + _ => None, + } +} + fn response_error_classification(error: &ResponseError) -> &'static str { + if response_error_module_fence(error).is_some() { + return "agent_startup_unverified_module"; + } match error.code.as_str() { "ACP_MODEL_UNSUPPORTED" => return "requested_model_unsupported", "AGENT_STARTUP_FAILED" => return "agent_startup_failed", @@ -1179,7 +1207,7 @@ mod tests { }) .collect(); let response = json!({ "protocolVersion": GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION, - "id": 1, "ok": false, "error": { "code": "UNKNOWN_PRIVATE_CODE", + "id": 1, "ok": false, "error": { "code": "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.REPEATED_CTIME", "message": "/private/token-canary", "retryable": false } }); let lines = frames .iter() @@ -1203,9 +1231,12 @@ mod tests { .unwrap_err() .to_string(); assert!( - error.contains("was rejected (retryable=false, classification=unclassified)"), + error.contains( + "was rejected (retryable=false, classification=agent_startup_unverified_module)" + ), "{error}" ); + assert!(error.contains("moduleFence=repeated_ctime"), "{error}"); assert!( error.contains("admissionStage=handshake admissionElapsedMs=24"), "{error}" @@ -1213,7 +1244,7 @@ mod tests { assert!( !error.contains("private") && !error.contains("canary") - && !error.contains("UNKNOWN_PRIVATE_CODE"), + && !error.contains("AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.REPEATED_CTIME"), "{error}" ); assert!(!transport.poisoned); @@ -1361,6 +1392,48 @@ mod tests { } } + #[test] + fn module_fence_diagnostics_are_closed_and_keep_the_top_level_category() { + for (suffix, expected) in [ + ("RESOLUTION", Some("resolution")), + ("OPEN", Some("open")), + ("BOUNDS", Some("bounds")), + ("ANCESTRY", Some("ancestry")), + ("FORMAT", Some("format")), + ("PATH_IDENTITY", Some("path_identity")), + ("READ_RESULT", Some("read_result")), + ("SIZE", Some("size")), + ("DEVICE", Some("device")), + ("INODE", Some("inode")), + ("MODE", Some("mode")), + ("UID", Some("uid")), + ("GID", Some("gid")), + ("LINK_COUNT", Some("link_count")), + ("MTIME", Some("mtime")), + ("BYTE_CONTINUITY", Some("byte_continuity")), + ("REPEATED_CTIME", Some("repeated_ctime")), + ("MODE private-canary", None), + ("MODE.PRIVATE_CANARY", None), + ("PRIVATE_CANARY", None), + ("", None), + ] { + let error = ResponseError { + code: format!("AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.{suffix}"), + message: "/private/token-canary MODE".to_owned(), + retryable: false, + }; + assert_eq!(response_error_module_fence(&error), expected); + assert_eq!( + response_error_classification(&error), + if expected.is_some() { + "agent_startup_unverified_module" + } else { + "unclassified" + } + ); + } + } + #[test] fn candidate_auth_diagnostics_use_only_closed_codes_and_never_provider_text() { for (code, expected) in [ diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts index 4aedd31f72..527ae39a35 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts @@ -126,6 +126,21 @@ describe("ACPX sidecar input sequencing", () => { expect(acpxSidecarErrorCode(new Error("wrapped", { cause: error }))).toBe(code); }); + it.each(["RESOLUTION", "OPEN", "BOUNDS", "ANCESTRY", "FORMAT", "PATH_IDENTITY", "READ_RESULT", + "SIZE", "DEVICE", "INODE", "MODE", "UID", "GID", "LINK_COUNT", "MTIME", "BYTE_CONTINUITY", "REPEATED_CTIME"])( + "preserves only closed module fence %s", fence => { + const classify = (marker: string) => acpxSidecarErrorCode(Object.assign(new Error("private-canary"), { + detailCode: "AGENT_STARTUP_FAILED", stderrSummary: `${marker}\nprivate-canary /secret`, exitCode: 1, + })); + expect(classify(`PAPERCLIP_ACPX_IMPORT_FAILURE:UNVERIFIED_MODULE:${fence}`)) + .toBe(`AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.${fence}`); + for (const suffix of ["PRIVATE_CANARY", `${fence} private-canary`, `${fence}.PRIVATE_CANARY`]) { + expect(classify(`PAPERCLIP_ACPX_IMPORT_FAILURE:UNVERIFIED_MODULE:${suffix}`)) + .toBe("AGENT_STARTUP_FAILED.EXIT_NONZERO"); + } + }, + ); + it.each(["OTHER", "UNVERIFIED_MODULE"])("classifies closed import marker %s without private stderr", category => { const classify = (stderrSummary: string) => acpxSidecarErrorCode(Object.assign(new Error("private-canary"), { detailCode: "AGENT_STARTUP_FAILED", stderrSummary, exitCode: 1, diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts index 8a07bf7354..ea60e425cc 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts @@ -90,8 +90,11 @@ function directAcpxSidecarErrorCode(error: Error): string | null { typeof details.stderrSummary === "string" ? details.stderrSummary : ""; // Bootstrap writes this fixed marker before printing private error details and // terminating. Never copy those details into the public classification. - const importFailure = /(?:^|\n)PAPERCLIP_ACPX_IMPORT_FAILURE:(UNVERIFIED_MODULE|OTHER)(?:\r?\n|$)/.exec(stderr); - if (importFailure?.[1] === "UNVERIFIED_MODULE") return "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE"; + const importFailure = /(?:^|\n)PAPERCLIP_ACPX_IMPORT_FAILURE:(UNVERIFIED_MODULE|OTHER)(?::([A-Z_]+))?(?:\r?\n|$)/.exec(stderr); + if (importFailure?.[1] === "UNVERIFIED_MODULE" && importFailure[2] === undefined) return "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE"; + if (importFailure?.[1] === "UNVERIFIED_MODULE" && MODULE_FENCES.has(importFailure[2])) { + return `AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.${importFailure[2]}`; + } if (/ERR_ACPX_UNVERIFIED_MODULE/.test(stderr)) { return "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE"; } @@ -110,7 +113,7 @@ function directAcpxSidecarErrorCode(error: Error): string | null { if (/ERR_INVALID_ARG|invalid argument/i.test(stderr)) { return "AGENT_STARTUP_FAILED.INVALID_ARGUMENT"; } - if (importFailure) return "AGENT_STARTUP_FAILED.OTHER"; + if (importFailure?.[1] === "OTHER" && importFailure[2] === undefined) return "AGENT_STARTUP_FAILED.OTHER"; if (!stderr.trim()) return "AGENT_STARTUP_FAILED.NO_STDERR"; if (typeof details.signal === "string" && details.signal) { return "AGENT_STARTUP_FAILED.SIGNAL"; @@ -134,6 +137,8 @@ const GENERIC_ACPX_OUTPUT_CODES = new Set([ "USAGE", ]); +const MODULE_FENCES = new Set(["RESOLUTION", "OPEN", "BOUNDS", "ANCESTRY", "FORMAT", "PATH_IDENTITY", "READ_RESULT", "SIZE", "DEVICE", "INODE", "MODE", "UID", "GID", "LINK_COUNT", "MTIME", "BYTE_CONTINUITY", "REPEATED_CTIME"]); + const STABLE_ACPX_SIDECAR_CODES = new Set([ "COPILOT_POLICY_VIOLATION", "COPILOT_DETACHED_WORK_UNSUPPORTED", @@ -151,6 +156,7 @@ const STABLE_ACPX_SIDECAR_CODES = new Set([ "AGENT_STARTUP_FAILED.SIGNAL", "AGENT_STARTUP_FAILED.SYNTAX_ERROR", "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE", + ...Array.from(MODULE_FENCES, fence => `AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.${fence}`), "AUTH_REQUIRED", "CLAUDE_ACP_SESSION_CREATE_TIMEOUT", "SESSION_CONFIG_OPTION_REPLAY_FAILED", diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts index 6b94ce1315..a9fc6fa678 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts @@ -1752,6 +1752,13 @@ describe("ACPX installation integrity", () => { expect(verify).toHaveBeenCalledTimes(pass); } else { expect(read).toThrow(); + const reasons: Record = { recurring: "REPEATED_CTIME", changed_bytes: "BYTE_CONTINUITY", + dev: "DEVICE", ino: "INODE", mode: "MODE", uid: "UID", gid: "GID", nlink: "LINK_COUNT", + size: "SIZE", mtimeNs: "MTIME", not_regular: "BOUNDS", oversized: "BOUNDS" }; + if (reasons[transition]) { + pass = 0; + expect(read).toThrow(expect.objectContaining({ code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence: reasons[transition] })); + } expect(pass).toBeLessThanOrEqual(2); if (transition === "snapshot_second") expect(verify).toHaveBeenCalledTimes(2); } @@ -1799,13 +1806,18 @@ describe("ACPX installation integrity", () => { const closed = once(child, "close"); let stdout = ""; child.stdout?.on("data", bytes => { stdout += String(bytes); }); - // Drain private stderr while keeping it out of test/report output. - child.stderr?.resume(); + let stderr = ""; + child.stderr?.on("data", bytes => { stderr += String(bytes); }); const timeout = setTimeout(() => child.kill("SIGKILL"), 4_000); try { const [code] = await closed; expect(code).toBe(transition === "once" ? 0 : 1); expect(stdout).toBe(transition === "once" ? "dependency-success" : ""); + const fences: Record = { recurring: "REPEATED_CTIME", bytes: "BYTE_CONTINUITY", mode: "MODE" }; + const fence = fences[transition]; + if (fence && !(transition === "bytes" && process.platform === "darwin")) { + expect(stderr).toContain(`PAPERCLIP_ACPX_IMPORT_FAILURE:UNVERIFIED_MODULE:${fence}\n`); + } } finally { clearTimeout(timeout); if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); @@ -1867,7 +1879,7 @@ describe("ACPX installation integrity", () => { detailCode: "AGENT_STARTUP_FAILED", stderrSummary: stderr, exitCode: child.exitCode, signal: child.signalCode, })); - expect(code).toBe(`AGENT_STARTUP_FAILED.${category}`); + expect(code).toBe(category === "UNVERIFIED_MODULE" ? "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.ANCESTRY" : "AGENT_STARTUP_FAILED.OTHER"); expect(code).not.toMatch(/canary|\//); expect(stderr).not.toContain("must-not-execute"); } finally { diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index 136c37ee60..10b4be4214 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -1861,40 +1861,45 @@ export function readStableSnapshotModuleSource(input: { verify: (bytes: Buffer) => void; maxBytes: number; }): Buffer { - const reject = (): never => { + const reject = (moduleFence: string): never => { throw Object.assign(new Error("ACPX provider module changed while it was read"), { - code: "ERR_ACPX_UNVERIFIED_MODULE", + code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence, }); }; let before = input.before; let first: Buffer | null = null; for (let attempt = 0; attempt < 2; attempt += 1) { - if (!before.isFile() || before.size < 0n || before.size > BigInt(input.maxBytes)) reject(); + if (!before.isFile() || before.size < 0n || before.size > BigInt(input.maxBytes)) reject("BOUNDS"); const size = Number(before.size); const buffer = Buffer.alloc(size + 1); let offset = 0; while (offset < buffer.length) { const count = input.read(buffer, offset, buffer.length - offset, offset); - if (!Number.isSafeInteger(count) || count < 0 || count > buffer.length - offset) reject(); + if (!Number.isSafeInteger(count) || count < 0 || count > buffer.length - offset) reject("READ_RESULT"); if (count === 0) break; offset += count; } const bytes = buffer.subarray(0, offset); const after = input.stat(); input.verify(bytes); - if (!after.isFile() || bytes.length !== size || BigInt(bytes.length) !== after.size - || before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode - || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink - || before.size !== after.size || before.mtimeNs !== after.mtimeNs) reject(); - if (first !== null && !first.equals(bytes)) reject(); + if (!after.isFile()) reject("BOUNDS"); + if (bytes.length !== size || BigInt(bytes.length) !== after.size || before.size !== after.size) reject("SIZE"); + if (before.dev !== after.dev) reject("DEVICE"); + if (before.ino !== after.ino) reject("INODE"); + if (before.mode !== after.mode) reject("MODE"); + if (before.uid !== after.uid) reject("UID"); + if (before.gid !== after.gid) reject("GID"); + if (before.nlink !== after.nlink) reject("LINK_COUNT"); + if (before.mtimeNs !== after.mtimeNs) reject("MTIME"); + if (first !== null && !first.equals(bytes)) reject("BYTE_CONTINUITY"); if (before.ctimeNs === after.ctimeNs) return bytes; - if (attempt !== 0) reject(); + if (attempt !== 0) reject("REPEATED_CTIME"); first = bytes; // Continue from the full final metadata, including ctime. A further drift // between passes is therefore rejected, not granted another transition. before = after; } - return reject(); + return reject("REPEATED_CTIME"); } function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { @@ -1960,7 +1965,7 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { `const snapshotDescriptorResolution = ${snapshotDescriptorResolution.toString()};`, "const dependencyAncestorIndex = (url) => { const recorded = dependencyAncestorByUrl.get(url); return recorded === undefined ? snapshotDescriptorAncestorIndex(url, directoryUrl, dependencyDirectoryUrls) : recorded; };", `const guardSnapshotModuleResolution = ${guardSnapshotModuleResolution.toString()};`, - 'const canonicalizeDescriptorResolution = (url) => { if (typeof url !== "string" || !url.startsWith("file:") || snapshotDescriptorAncestorIndex(url, directoryUrl, dependencyDirectoryUrls) < 0) return url; try { return pathToFileURL(fs.realpathSync(fileURLToPath(url))).href; } catch { const error = new Error("ACPX provider module could not be canonicalized through its retained descriptor"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; } };', + 'const canonicalizeDescriptorResolution = (url) => { if (typeof url !== "string" || !url.startsWith("file:") || snapshotDescriptorAncestorIndex(url, directoryUrl, dependencyDirectoryUrls) < 0) return url; try { return pathToFileURL(fs.realpathSync(fileURLToPath(url))).href; } catch { const error = new Error("ACPX provider module could not be canonicalized through its retained descriptor"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "RESOLUTION"; throw error; } };', 'const rememberDependencyAncestor = (specifier, resolution) => { const canonicalUrl = canonicalizeDescriptorResolution(resolution?.url); const pinned = snapshotDescriptorResolution(canonicalUrl, directoryUrl, dependencyDirectoryUrls, canonicalDirectoryUrl, canonicalDependencyDirectoryUrls); guardSnapshotModuleResolution(isBuiltin(specifier), resolution?.url, pinned !== null); if (pinned !== null && typeof resolution?.url === "string") { for (const rememberedUrl of [resolution.url, canonicalUrl, pinned.url]) { if (typeof rememberedUrl !== "string") continue; dependencyAncestorByUrl.set(rememberedUrl, pinned.ancestorIndex); if (typeof resolution.format === "string") descriptorFormatByUrl.set(rememberedUrl, resolution.format); } } return pinned === null || pinned.url === resolution?.url ? resolution : { ...resolution, url: pinned.url }; };', `const source = fs.readFileSync(${COMMAND_SOURCE_FD});`, "let resolvingDescriptorBare = false;", @@ -1987,7 +1992,7 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { 'return new URL(segments.slice(ancestorLevels).join("/") || ".", dependencyDirectoryUrls[targetAncestorIndex]);', "};", "const pinnedSpecifier = relativeImport ? pinRelativeSpecifier() : null;", - 'if (relativeImport && pinnedSpecifier === null) { const error = new Error("ACPX provider relative module escaped its verified package"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; }', + 'if (relativeImport && pinnedSpecifier === null) { const error = new Error("ACPX provider relative module escaped its verified package"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "ANCESTRY"; throw error; }', 'const lookupSpecifier = pinnedSpecifier === null ? specifier : context.conditions?.includes("require") ? fileURLToPath(pinnedSpecifier) : pinnedSpecifier.href;', "const snapshotImport = entryImport || parentDependencyAncestorIndex >= 0;", 'const bareImport = snapshotImport && !isBuiltin(specifier) && !specifier.startsWith("./") && !specifier.startsWith("../") && !specifier.startsWith("/") && !specifier.includes(":");', @@ -2017,21 +2022,21 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { "if (!descriptorLookup) return nextLoad(url, context);", "const canonicalRootUrl = url.startsWith(directoryUrl) ? canonicalDirectoryUrl : canonicalDependencyDirectoryUrls[dependencyDescriptorIndex];", "let moduleFd;", - 'try { moduleFd = fs.openSync(fileURLToPath(url), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); } catch { const error = new Error("ACPX provider module could not be opened without following its final component"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; }', + 'try { moduleFd = fs.openSync(fileURLToPath(url), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); } catch { const error = new Error("ACPX provider module could not be opened without following its final component"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "OPEN"; throw error; }', "try {", "const metadataBefore = fs.fstatSync(moduleFd, { bigint: true });", - `if (!metadataBefore.isFile() || metadataBefore.size > BigInt(${MAX_AGENT_COMMAND_BYTES})) { const error = new Error("ACPX provider module is not a bounded regular file"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; }`, + `if (!metadataBefore.isFile() || metadataBefore.size > BigInt(${MAX_AGENT_COMMAND_BYTES})) { const error = new Error("ACPX provider module is not a bounded regular file"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "BOUNDS"; throw error; }`, 'const openedUrl = pathToFileURL(fs.realpathSync(privateSnapshot ? fileURLToPath(url) : "/proc/self/fd/" + moduleFd)).href;', - 'if (typeof canonicalRootUrl !== "string" || !openedUrl.startsWith(canonicalRootUrl)) { const error = new Error("ACPX provider module escaped descriptor-pinned ancestry"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; }', + 'if (typeof canonicalRootUrl !== "string" || !openedUrl.startsWith(canonicalRootUrl)) { const error = new Error("ACPX provider module escaped descriptor-pinned ancestry"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "ANCESTRY"; throw error; }', "const packageFormat = url.startsWith(directoryUrl) ? serverPackageFormat : dependencyAncestorFormats[dependencyDescriptorIndex];", "const hintedFormat = descriptorFormatByUrl.get(url) || context.format;", "const extension = extname(fileURLToPath(url));", 'const moduleFormat = extension === ".mjs" ? "module" : extension === ".cjs" ? "commonjs" : extension === ".json" ? "json" : extension === ".node" ? "addon" : extension === ".js" ? (hintedFormat === "module" || hintedFormat === "commonjs" ? hintedFormat : packageFormat) : hintedFormat;', - 'if (moduleFormat !== "module" && moduleFormat !== "commonjs" && moduleFormat !== "json") { const error = new Error("ACPX provider module format is not supported by descriptor-pinned loading"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; }', + 'if (moduleFormat !== "module" && moduleFormat !== "commonjs" && moduleFormat !== "json") { const error = new Error("ACPX provider module format is not supported by descriptor-pinned loading"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "FORMAT"; throw error; }', // Re-check both the retained descriptor's canonical path and its named // inode after each pass; a rename is not harmless hydration just because // its already-open descriptor still returns identical bytes. - 'const statModule = () => { const currentUrl = pathToFileURL(fs.realpathSync(privateSnapshot ? fileURLToPath(url) : "/proc/self/fd/" + moduleFd)).href; const named = fs.lstatSync(fileURLToPath(url), { bigint: true }); const metadata = fs.fstatSync(moduleFd, { bigint: true }); if (currentUrl !== openedUrl || !named.isFile() || named.dev !== metadata.dev || named.ino !== metadata.ino) { const error = new Error("ACPX provider module path changed while it was read"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; throw error; } return metadata; };', + 'const statModule = () => { const currentUrl = pathToFileURL(fs.realpathSync(privateSnapshot ? fileURLToPath(url) : "/proc/self/fd/" + moduleFd)).href; const named = fs.lstatSync(fileURLToPath(url), { bigint: true }); const metadata = fs.fstatSync(moduleFd, { bigint: true }); if (currentUrl !== openedUrl || !named.isFile() || named.dev !== metadata.dev || named.ino !== metadata.ino) { const error = new Error("ACPX provider module path changed while it was read"); error.code = "ERR_ACPX_UNVERIFIED_MODULE"; error.moduleFence = "PATH_IDENTITY"; throw error; } return metadata; };', `const moduleSource = readStableSnapshotModuleSource({ before: metadataBefore, stat: statModule, read: (buffer, offset, length, position) => fs.readSync(moduleFd, buffer, offset, length, position), verify: (bytes) => verifySnapshotBytes(fileURLToPath(url), bytes), maxBytes: ${MAX_AGENT_COMMAND_BYTES} });`, "return { format: moduleFormat, source: moduleSource, shortCircuit: true };", "} finally { fs.closeSync(moduleFd); }", @@ -2046,7 +2051,7 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { guarded ? "const exitOnImportFailure = reapOnGuardianLoss;" : "const exitOnImportFailure = process.exit.bind(process);", - 'import(target).catch((error) => { let category = "OTHER"; try { if (error?.code === "ERR_ACPX_UNVERIFIED_MODULE") category = "UNVERIFIED_MODULE"; } catch {} try { writeImportFailure(2, "PAPERCLIP_ACPX_IMPORT_FAILURE:" + category + "\\n"); console.error(error); } finally { exitOnImportFailure(1); } });', + 'import(target).catch((error) => { let category = "OTHER"; try { if (error?.code === "ERR_ACPX_UNVERIFIED_MODULE") { category = "UNVERIFIED_MODULE"; const fence = error.moduleFence; if (["RESOLUTION", "OPEN", "BOUNDS", "ANCESTRY", "FORMAT", "PATH_IDENTITY", "READ_RESULT", "SIZE", "DEVICE", "INODE", "MODE", "UID", "GID", "LINK_COUNT", "MTIME", "BYTE_CONTINUITY", "REPEATED_CTIME"].includes(fence)) category += ":" + fence; } } catch {} try { writeImportFailure(2, "PAPERCLIP_ACPX_IMPORT_FAILURE:" + category + "\\n"); console.error(error); } finally { exitOnImportFailure(1); } });', ].join(""); } @@ -2079,7 +2084,7 @@ export function guardSnapshotModuleResolution( const error = new Error( "ACPX provider module escaped descriptor-pinned ancestry", ); - Object.assign(error, { code: "ERR_ACPX_UNVERIFIED_MODULE" }); + Object.assign(error, { code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence: "ANCESTRY" }); throw error; } }