Rocko-authored, Filbert-reviewed inspector (r6 manifest a4a44930...) with full review/build/verdict evidence under docs/plans/reviews. 43/0 selftests, oracle zero-disagreement, foundation checker PASS. Owner A9 acceptance recorded separately.
245 lines
8.4 KiB
JavaScript
245 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* foundation-inspect — offline synthetic permission inspector (charter candidate 3).
|
|
*
|
|
* node scripts/foundation-inspect.mjs [--json] <bundle.json>
|
|
*
|
|
* SYNTHETIC PREVIEW — NO LIVE EFFECTS. Reads exactly one bounded regular file,
|
|
* evaluates it with the pure core in scripts/foundation/ and prints a closed result.
|
|
* It never spawns processes, reads environment/config, touches the network, or
|
|
* consults any state outside the named file. Exit: 0 permitted preview, 2 malformed
|
|
* or unsupported input, 3 simulated refusal or unresolved admission, 4 I/O failure.
|
|
*/
|
|
|
|
import { openSync, fstatSync, readSync, closeSync, constants as fsConstants } from "node:fs";
|
|
import { pathToFileURL } from "node:url";
|
|
import { resolve as resolvePath } from "node:path";
|
|
import { parseStrict, StrictJsonError, MAX_INPUT_BYTES } from "./foundation/strict-json.mjs";
|
|
import { evaluate, buildResult, exitFor } from "./foundation/resolve.mjs";
|
|
|
|
const REQUIRED_FLAGS = ["O_RDONLY", "O_NOFOLLOW", "O_NONBLOCK"];
|
|
|
|
class IoFailure extends Error {
|
|
constructor(exit, reason, rule, diagnostic) {
|
|
super(rule);
|
|
this.exit = exit;
|
|
this.reason = reason;
|
|
this.rule = rule;
|
|
this.diagnostic = diagnostic;
|
|
}
|
|
}
|
|
|
|
const RISKY_RE = /^[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]$/u;
|
|
|
|
/** \uXXXX (or a surrogate-pair escape for astral code points) for one code point. */
|
|
function escapeCodePoint(cp) {
|
|
if (cp > 0xffff) {
|
|
const v = cp - 0x10000;
|
|
return `\\u${(0xd800 + (v >> 10)).toString(16)}\\u${(0xdc00 + (v & 0x3ff)).toString(16)}`;
|
|
}
|
|
return `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
}
|
|
|
|
/**
|
|
* Escape C0/C1 controls, format, lone surrogate and line/paragraph separators
|
|
* (and backslash) as \uXXXX, iterating by code point so well-formed astral
|
|
* characters pass through unchanged.
|
|
*/
|
|
export function escapeText(value) {
|
|
let out = "";
|
|
for (const ch of value) {
|
|
const cp = ch.codePointAt(0);
|
|
out += (cp === 0x5c || RISKY_RE.test(ch)) ? escapeCodePoint(cp) : ch;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function refText(ref) {
|
|
if (ref === null) return "null";
|
|
const scope = ref.scope.kind === "system" ? "system"
|
|
: ref.scope.kind === "project" ? `project:${escapeText(ref.scope.projectId)}`
|
|
: `workspace:${escapeText(ref.scope.projectId)}/${escapeText(ref.scope.workspaceId)}`;
|
|
return `${escapeText(ref.kind)} ${escapeText(ref.id)} @${scope} rev ${ref.revision}`;
|
|
}
|
|
|
|
/** Text rendering derived from the same closed result object as the JSON form. */
|
|
export function renderText(r) {
|
|
const lines = [];
|
|
lines.push(r.disclaimer);
|
|
lines.push(r.preview);
|
|
lines.push(`authentication: ${r.authentication}; declarations: ${r.declarations}`);
|
|
lines.push(`result: ${r.result}`);
|
|
lines.push(`reason: ${r.reason}`);
|
|
lines.push(`rule: ${r.rule === null ? "null" : r.rule}`);
|
|
if (r.selection === null) {
|
|
lines.push("selection: null");
|
|
} else {
|
|
const s = r.selection;
|
|
lines.push(`selection: agent ${escapeText(s.agentId)}, project ${escapeText(s.projectId)}, workspace ${escapeText(s.workspaceId)}, assignment ${refText(s.assignmentRef)}`);
|
|
}
|
|
if (r.operation === null) {
|
|
lines.push("operation: null");
|
|
} else {
|
|
const target = r.operation.target === null ? "null" : `workspace:${escapeText(r.operation.target.path)}`;
|
|
lines.push(`operation: ${escapeText(r.operation.name)} target ${target}`);
|
|
}
|
|
if (r.proposal === null) {
|
|
lines.push("proposal: null");
|
|
} else {
|
|
const p = r.proposal;
|
|
lines.push(`proposal: ${p.result} (${p.reason}; ${p.rule}) selected assignment ${refText(p.selectedAssignmentRef)}`);
|
|
}
|
|
if (r.diagnostic === null) {
|
|
lines.push("diagnostic: null");
|
|
} else {
|
|
const d = r.diagnostic;
|
|
lines.push(`diagnostic: byteOffset ${d.byteOffset === null ? "null" : d.byteOffset}; inputPath ${d.inputPath === null ? "null" : escapeText(d.inputPath)}`);
|
|
}
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
/**
|
|
* Valid JSON whose decoded value equals the result. Inside string tokens, risky
|
|
* code points (controls, format, lone surrogates, line/paragraph separators) become
|
|
* \\uXXXX escapes; structural whitespace outside strings is left alone.
|
|
*/
|
|
export function renderJson(r) {
|
|
const raw = JSON.stringify(r, null, 2);
|
|
let out = "";
|
|
let inString = false;
|
|
for (let i = 0; i < raw.length; i += 1) {
|
|
const ch = raw[i];
|
|
if (inString) {
|
|
if (ch === "\\") {
|
|
out += ch + raw[i + 1];
|
|
i += 1;
|
|
} else if (ch === '"') {
|
|
inString = false;
|
|
out += ch;
|
|
} else {
|
|
const cp = raw.codePointAt(i);
|
|
const whole = String.fromCodePoint(cp);
|
|
out += RISKY_RE.test(whole) ? escapeCodePoint(cp) : whole;
|
|
i += whole.length - 1;
|
|
}
|
|
} else {
|
|
if (ch === '"') inString = true;
|
|
out += ch;
|
|
}
|
|
}
|
|
return `${out}\n`;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
let json = false;
|
|
let input = null;
|
|
for (const a of argv) {
|
|
if (a === "--json") {
|
|
if (json) return { error: true, json };
|
|
json = true;
|
|
} else if (a.startsWith("-") && a !== "-") {
|
|
return { error: true, json };
|
|
} else if (input === null) {
|
|
input = a;
|
|
} else {
|
|
return { error: true, json };
|
|
}
|
|
}
|
|
if (input === null) return { error: true, json };
|
|
return { error: false, json, input };
|
|
}
|
|
|
|
/** Bounded, non-following, regular-file read. Returns a Uint8Array or throws IoFailure. */
|
|
export function readBundleBytes(inputPath) {
|
|
for (const name of REQUIRED_FLAGS) {
|
|
if (typeof fsConstants[name] !== "number") {
|
|
throw new IoFailure(4, "io-failure", "open-flags-unavailable", { byteOffset: null, inputPath });
|
|
}
|
|
}
|
|
const flags = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
|
|
let fd = -1;
|
|
try {
|
|
try {
|
|
fd = openSync(inputPath, flags);
|
|
} catch (e) {
|
|
throw new IoFailure(4, "io-failure", "input-open-failed", { byteOffset: null, inputPath });
|
|
}
|
|
let st;
|
|
try {
|
|
st = fstatSync(fd);
|
|
} catch (e) {
|
|
throw new IoFailure(4, "io-failure", "input-open-failed", { byteOffset: null, inputPath });
|
|
}
|
|
if (!st.isFile()) throw new IoFailure(4, "io-failure", "input-not-regular", { byteOffset: null, inputPath });
|
|
// Size gate is a bound refusal (exit 2), not an I/O failure: no path echo, no offset.
|
|
if (st.size > MAX_INPUT_BYTES) throw new IoFailure(2, "invalid-request", "input-too-large", null);
|
|
const size = Number(st.size);
|
|
const buf = new Uint8Array(size + 1);
|
|
let total = 0;
|
|
for (;;) {
|
|
let n;
|
|
try {
|
|
n = readSync(fd, buf, total, buf.length - total, null);
|
|
} catch (e) {
|
|
throw new IoFailure(4, "io-failure", "input-open-failed", { byteOffset: null, inputPath });
|
|
}
|
|
if (n === 0) break;
|
|
total += n;
|
|
if (total >= buf.length) break;
|
|
}
|
|
if (total !== size) throw new IoFailure(4, "io-failure", "input-size-changed", { byteOffset: null, inputPath });
|
|
return buf.subarray(0, size);
|
|
} finally {
|
|
if (fd >= 0) {
|
|
try { closeSync(fd); } catch (e) { /* nothing further to release */ }
|
|
}
|
|
}
|
|
}
|
|
|
|
function closed(fields) {
|
|
return buildResult({ selection: null, operation: null, proposal: null, diagnostic: null, ...fields });
|
|
}
|
|
|
|
function withExit(json, result) {
|
|
return { json, result, exit: exitFor(result) };
|
|
}
|
|
|
|
/** Returns {json, result, exit}: exit is process metadata derived from the closed result. */
|
|
export function run(argv) {
|
|
const args = parseArgs(argv);
|
|
if (args.error) {
|
|
return withExit(args.json, closed({ exit: 2, reason: "invalid-request", rule: "usage-invalid" }));
|
|
}
|
|
let bytes;
|
|
try {
|
|
bytes = readBundleBytes(args.input);
|
|
} catch (e) {
|
|
if (e instanceof IoFailure) {
|
|
return withExit(args.json, closed({ exit: e.exit, reason: e.reason, rule: e.rule, diagnostic: e.diagnostic }));
|
|
}
|
|
throw e;
|
|
}
|
|
let value;
|
|
try {
|
|
value = parseStrict(bytes);
|
|
} catch (e) {
|
|
if (e instanceof StrictJsonError) {
|
|
const rule = e.code === "input-too-large" ? "input-too-large" : "input-parse-failed";
|
|
return withExit(args.json, closed({ exit: 2, reason: "invalid-request", rule, diagnostic: { byteOffset: e.byteOffset, inputPath: null } }));
|
|
}
|
|
throw e;
|
|
}
|
|
const evaluated = evaluate(value);
|
|
return { json: args.json, result: evaluated.result, exit: evaluated.exit };
|
|
}
|
|
|
|
function main() {
|
|
const { json, result, exit } = run(process.argv.slice(2));
|
|
process.stdout.write(json ? renderJson(result) : renderText(result));
|
|
process.exitCode = exit;
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(resolvePath(process.argv[1])).href) {
|
|
main();
|
|
}
|