Files
stack/scripts/foundation-inspect.mjs
jason.woltje 8ebddd6f93 feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)
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.
2026-09-07 14:06:35 -05:00

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();
}