mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 08:26:46 +02:00
diagnostics: identify closed provider module rejection fences
This commit is contained in:
1 parent
8e8999d681
commit
8f62eef8f1
5 files changed
+141
-30
No files matched your search
@@ -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 [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1752,6 +1752,13 @@ describe("ACPX installation integrity", () => {
|
||||
expect(verify).toHaveBeenCalledTimes(pass);
|
||||
} else {
|
||||
expect(read).toThrow();
|
||||
const reasons: Record<string, string> = { 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<string, string> = { 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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user