#!/usr/bin/env node /** * foundation-inspect — offline synthetic permission inspector (charter candidate 3). * * node scripts/foundation-inspect.mjs [--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(); }