Report bounded metadata for module link-count rejections

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-11 08:05:26 -05:00
1 parent ab1e8acc68
commit f5a393dd60
5 files changed
+192 -10

No files matched your search

@@ -458,11 +458,12 @@ 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(),
response_error_module_read(&error).unwrap_or_default(),
self.diagnostic_suffix(),
),
)));
@@ -926,6 +927,9 @@ fn stderr_diagnostic_categories(value: &str) -> BTreeSet<&'static str> {
// Only fixed loader fence codes may cross the private provider-stderr boundary.
fn response_error_module_fence(error: &ResponseError) -> Option<&'static str> {
if response_error_module_read(error).is_some() {
return Some("link_count");
}
match error.code.as_str() {
"AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.RESOLUTION" => Some("resolution"),
"AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.OPEN" => Some("open"),
@@ -948,6 +952,59 @@ fn response_error_module_fence(error: &ResponseError) -> Option<&'static str> {
}
}
// Independently validate the compact failure-only metadata from the sidecar.
fn response_error_module_read(error: &ResponseError) -> Option<String> {
let suffix = error
.code
.strip_prefix("AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT.READ.")?;
let fields: Vec<&str> = suffix.split('.').collect();
if fields.len() != 5 || !matches!(fields[0], "1" | "2") {
return None;
}
let number = |text: &str| -> Option<u64> {
if text.is_empty()
|| text.len() > 20
|| !text.bytes().all(|b| b.is_ascii_digit())
|| (text.len() > 1 && text.starts_with('0'))
{
return None;
}
text.parse().ok()
};
let before = number(fields[1])?;
let after = number(fields[2])?;
if before == after || fields[3] != if after > before { "I" } else { "D" } {
return None;
}
let flags = fields[4];
if flags.len() != 8
|| !flags.starts_with("111111")
|| !flags.bytes().all(|b| matches!(b, b'0' | b'1'))
{
return None;
}
let mut diagnostic = format!(
" moduleReadAttempt={} moduleNlinkBefore={} moduleNlinkAfter={} moduleNlinkDirection={}",
fields[0],
before,
after,
if after > before {
"increase"
} else {
"decrease"
}
);
for (field, equal) in [
"Dev", "Inode", "Mode", "Uid", "Gid", "Size", "Mtime", "Ctime",
]
.into_iter()
.zip(flags.bytes())
{
diagnostic.push_str(&format!(" module{field}Equal={}", equal == b'1'));
}
Some(diagnostic)
}
fn response_error_classification(error: &ResponseError) -> &'static str {
if response_error_module_fence(error).is_some() {
return "agent_startup_unverified_module";
@@ -1207,7 +1264,7 @@ mod tests {
})
.collect();
let response = json!({ "protocolVersion": GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION,
"id": 1, "ok": false, "error": { "code": "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.REPEATED_CTIME",
"id": 1, "ok": false, "error": { "code": "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT.READ.1.2.1.D.11111110",
"message": "/private/token-canary", "retryable": false } });
let lines = frames
.iter()
@@ -1236,7 +1293,12 @@ mod tests {
),
"{error}"
);
assert!(error.contains("moduleFence=repeated_ctime"), "{error}");
assert!(error.contains("moduleFence=link_count"), "{error}");
assert!(error.contains("moduleReadAttempt=1 moduleNlinkBefore=2 moduleNlinkAfter=1 moduleNlinkDirection=decrease"), "{error}");
assert!(
error.contains("moduleMtimeEqual=true moduleCtimeEqual=false"),
"{error}"
);
assert!(
error.contains("admissionStage=handshake admissionElapsedMs=24"),
"{error}"
@@ -1434,6 +1496,48 @@ mod tests {
}
}
#[test]
fn link_count_failure_details_are_bounded_and_keep_the_fence() {
for (suffix, valid) in [
("1.1.2.I.11111110", true),
("2.2.1.D.11111101", true),
("1.0.18446744073709551615.I.11111111", true),
("0.1.2.I.11111110", false),
("3.1.2.I.11111110", false),
("1.-1.2.I.11111110", false),
("1.01.2.I.11111110", false),
("1.1.18446744073709551616.I.11111110", false),
("1.1.2.D.11111110", false),
("1.1.1.I.11111110", false),
("1.1.2.I.01111110", false),
("1.1.2.I.111111100", false),
("1.1.2.I.11111110.PRIVATE_CANARY", false),
] {
let error = ResponseError {
code: format!("AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT.READ.{suffix}"),
message: "/private/token-canary".to_owned(),
retryable: false,
};
let detail = response_error_module_read(&error);
assert_eq!(detail.is_some(), valid, "{suffix}");
assert_eq!(
response_error_module_fence(&error),
if valid { Some("link_count") } else { None }
);
assert_eq!(
response_error_classification(&error),
if valid {
"agent_startup_unverified_module"
} else {
"unclassified"
}
);
if let Some(detail) = detail {
assert!(!detail.contains("private") && !detail.contains("CANARY"));
}
}
}
#[test]
fn candidate_auth_diagnostics_use_only_closed_codes_and_never_provider_text() {
for (code, expected) in [
@@ -1,3 +1,7 @@
import { createRequire } from "node:module";
import { readdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import {
@@ -246,3 +250,31 @@ describe("ACPX sidecar input sequencing", () => {
);
});
});
describe("closed link-count failure detail", () => {
const classify = (detail: string) => acpxSidecarErrorCode(Object.assign(new Error("private-canary"), {
detailCode: "AGENT_STARTUP_FAILED", stderrSummary: `PAPERCLIP_ACPX_IMPORT_FAILURE:UNVERIFIED_MODULE:LINK_COUNT:END PAPERCLIP_ACPX_LINK_COUNT:${detail}:END private-canary /secret`,
}));
it.each(["1:1:2:I:11111110", "2:2:1:D:11111101", "1:0:18446744073709551615:I:11111111"])("preserves bounded metadata %s", detail => {
expect(classify(detail)).toBe(`AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT.READ.${detail.replaceAll(":", ".")}`);
});
it.each(["0:1:2:I:11111110", "3:1:2:I:11111110", "1:-1:2:I:11111110", "1:01:2:I:11111110", "1:1:18446744073709551616:I:11111110", "1:1:2:D:11111110", "1:1:1:I:11111110", "1:1:2:I:01111110", "1:1:2:I:111111100", "1:1:2:I:11111110.PRIVATE", "1:1:2:I:11111110:END_PRIVATE"])("drops malformed metadata without dropping fence: %s", detail => {
expect(classify(detail)).toBe("AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT");
});
it("survives the installed ACPX summarizer without leaking or accepting truncated details", async () => {
const directory = dirname(createRequire(import.meta.url).resolve("acpx/runtime"));
const chunk = (await readdir(directory)).find(name => /^live-checkpoint-.*\.js$/.test(name));
expect(chunk).toBeDefined();
const exports = await import(pathToFileURL(join(directory, chunk!)).href);
const client = Object.values(exports).find(value => typeof value === "function" && typeof value.prototype?.summarizeStartupStderr === "function") as { prototype: { summarizeStartupStderr: (parts: string[]) => string } };
const base = "PAPERCLIP_ACPX_IMPORT_FAILURE:UNVERIFIED_MODULE:LINK_COUNT:END";
const detail = "PAPERCLIP_ACPX_LINK_COUNT:1:2:1:D:11111110:END";
for (const complete of [true, false]) {
const summary = client.prototype.summarizeStartupStderr([`${base}\n${complete ? detail : detail.slice(0, -1)}\nprivate-canary /secret`]);
const code = acpxSidecarErrorCode(Object.assign(new Error("private-canary"), { detailCode: "AGENT_STARTUP_FAILED", stderrSummary: summary }));
expect(code).toBe(`AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT${complete ? ".READ.1.2.1.D.11111110" : ""}`);
expect(code).not.toMatch(/private|secret/);
}
});
});
@@ -97,7 +97,8 @@ function directAcpxSidecarErrorCode(error: Error): string | null {
?? /(?:^|\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]}`;
const detail = importFailure[2] === "LINK_COUNT" ? moduleLinkCountDetail(stderr) : null;
return `AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.${importFailure[2]}${detail ? `.READ.${detail.replaceAll(":", ".")}` : ""}`;
}
if (/ERR_ACPX_UNVERIFIED_MODULE/.test(stderr)) {
return "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE";
@@ -181,3 +182,15 @@ const STABLE_ACPX_SIDECAR_CODES = new Set([
"ACPX_TOOL_CALL_STALE",
...GENERIC_ACPX_OUTPUT_CODES,
]);
/** Failure-only unsigned metadata; no provider strings or identifiers cross. */
function moduleLinkCountDetail(stderr: string): string | null {
const match = /(?:^|\s)PAPERCLIP_ACPX_LINK_COUNT:([12]):(0|[1-9][0-9]{0,19}):(0|[1-9][0-9]{0,19}):([ID]):(111111[01]{2}):END(?=\s|$)/.exec(stderr);
if (!match) return null;
const before = BigInt(match[2]!);
const after = BigInt(match[3]!);
if (before > 18446744073709551615n || after > 18446744073709551615n || before === after
|| match[4] !== (after > before ? "I" : "D")) return null;
return match.slice(1).join(":");
}
@@ -1766,6 +1766,10 @@ describe("ACPX installation integrity", () => {
if (reasons[transition]) {
pass = 0;
expect(read).toThrow(expect.objectContaining({ code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence: reasons[transition] }));
if (transition === "nlink") {
pass = 0;
expect(read).toThrow(expect.objectContaining({ moduleReadDiagnostic: "1:1:2:I:11111110" }));
}
}
expect(pass).toBeLessThanOrEqual(2);
if (transition === "snapshot_second") expect(verify).toHaveBeenCalledTimes(2);
@@ -1774,7 +1778,19 @@ describe("ACPX installation integrity", () => {
);
});
it.runIf(process.platform !== "win32").each(["once", "recurring", "bytes", "mode", "rename", "replacement", "symlink"] as const)("loads dependency bytes only after stable ctime-only continuity (%s)", async (transition) => {
it("reports the second read attempt without accepting link drift", () => {
const bytes = Buffer.from("stable bytes");
const before = { dev: 1n, ino: 2n, mode: 0o100444n, uid: 1n, gid: 1n, nlink: 1n,
size: BigInt(bytes.length), mtimeNs: 10n, ctimeNs: 20n, isFile: () => true };
let passes = 0;
expect(() => readStableSnapshotModuleSource({ before, maxBytes: 64, verify: () => {},
read: (buffer, offset, length, position) => { if (position === 0) passes++; return bytes.copy(buffer, offset, position, position + length); },
stat: () => ({ ...before, ctimeNs: 21n, nlink: passes === 2 ? 0n : 1n }),
})).toThrow(expect.objectContaining({ code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence: "LINK_COUNT", moduleReadDiagnostic: "2:1:0:D:11111111" }));
expect(passes).toBe(2);
});
it.runIf(process.platform !== "win32").each(["once", "recurring", "bytes", "mode", "rename", "replacement", "symlink", "link"] as const)("loads dependency bytes only after stable ctime-only continuity (%s)", async (transition) => {
const fixture = await installationFixture();
await writeFile(join(fixture.commandDirectory, "dependency.mjs"), 'export const value = "dependency-success";');
const command = `import fs from 'node:fs';
@@ -1785,6 +1801,7 @@ describe("ACPX installation integrity", () => {
if (buffer.subarray(offset, offset + count).includes(Buffer.from('dependency-success'))) {
moduleFd = fd; passes += 1;
if (${JSON.stringify(transition)} === 'bytes' && passes === 2) buffer[offset] = 0;
if (process.platform === 'linux' && passes === 1 && ${JSON.stringify(transition)} === 'link') fs.linkSync(new URL('./dependency.mjs', import.meta.url), new URL('./dependency-link.mjs', import.meta.url));
if (passes === 1 && ['rename','replacement','symlink'].includes(${JSON.stringify(transition)})) {
const named = new URL('./dependency.mjs', import.meta.url);
fs.renameSync(named, new URL('./moved.mjs', import.meta.url));
@@ -1799,6 +1816,7 @@ describe("ACPX installation integrity", () => {
if (fd === moduleFd && options?.bigint) {
metadata.ctimeNs += ${JSON.stringify(transition)} === 'recurring' ? BigInt(passes) : 1n;
if (${JSON.stringify(transition)} === 'mode') metadata.mode ^= 0o100n;
if (process.platform !== 'linux' && ${JSON.stringify(transition)} === 'link') metadata.nlink += 1n;
}
return metadata;
};
@@ -1821,7 +1839,12 @@ describe("ACPX installation integrity", () => {
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 fences: Record<string, string> = { recurring: "REPEATED_CTIME", bytes: "BYTE_CONTINUITY", mode: "MODE", link: "LINK_COUNT" };
if (transition === "link") {
expect(stderr).toContain("PAPERCLIP_ACPX_LINK_COUNT:1:1:2:I:11111110:END");
expect(acpxSidecarErrorCode(Object.assign(new Error("private-canary"), { detailCode: "AGENT_STARTUP_FAILED", stderrSummary: stderr.replace(/\s+/gu, " ") })))
.toBe("AGENT_STARTUP_FAILED.UNVERIFIED_MODULE.LINK_COUNT.READ.1.1.2.I.11111110");
}
const fence = fences[transition];
if (fence && !(transition === "bytes" && process.platform === "darwin")) {
expect(stderr).toContain(`PAPERCLIP_ACPX_IMPORT_FAILURE:UNVERIFIED_MODULE:${fence}:END\n`);
@@ -1870,9 +1870,9 @@ export function readStableSnapshotModuleSource(input: {
verify: (bytes: Buffer) => void;
maxBytes: number;
}): Buffer {
const reject = (moduleFence: string): never => {
const reject = (moduleFence: string, moduleReadDiagnostic?: string): never => {
throw Object.assign(new Error("ACPX provider module changed while it was read"), {
code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence,
code: "ERR_ACPX_UNVERIFIED_MODULE", moduleFence, ...(moduleReadDiagnostic ? { moduleReadDiagnostic } : {}),
});
};
let before = input.before;
@@ -1898,7 +1898,17 @@ export function readStableSnapshotModuleSource(input: {
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.nlink !== after.nlink) {
// Failure-only data: never admit a link topology change as hydration.
const bounded = before.nlink >= 0n && after.nlink >= 0n
&& before.nlink <= 18446744073709551615n && after.nlink <= 18446744073709551615n;
const equal = [before.dev === after.dev, before.ino === after.ino, before.mode === after.mode,
before.uid === after.uid, before.gid === after.gid, before.size === after.size,
before.mtimeNs === after.mtimeNs, before.ctimeNs === after.ctimeNs].map(value => value ? "1" : "0").join("");
reject("LINK_COUNT", bounded
? `${attempt + 1}:${before.nlink}:${after.nlink}:${after.nlink > before.nlink ? "I" : "D"}:${equal}`
: undefined);
}
if (before.mtimeNs !== after.mtimeNs) reject("MTIME");
if (first !== null && !first.equals(bytes)) reject("BYTE_CONTINUITY");
if (before.ctimeNs === after.ctimeNs) return bytes;
@@ -2060,7 +2070,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"; 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 + ":END\\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 + ":END\\n"); const detail = error?.moduleReadDiagnostic; if (category === "UNVERIFIED_MODULE:LINK_COUNT" && typeof detail === "string" && /^[12]:(0|[1-9][0-9]{0,19}):(0|[1-9][0-9]{0,19}):[ID]:111111[01]{2}$/.test(detail)) writeImportFailure(2, "PAPERCLIP_ACPX_LINK_COUNT:" + detail + ":END\\n"); console.error(error); } finally { exitOnImportFailure(1); } });',
].join("");
}