No trusted interior: routing is one lookup, dispatch receives a row

Fourth round from sdk-qa, and PM named why the previous three failed
identically: each fix defined a trusted REGION — a spelling list, a fenced file
region, then the dispatcher body — and the next raw arm simply moved inside the
newest region. A trusted region is always one special-case away from wrong,
exactly as a denylist is always one spelling away. All three findings are that
shape; all three are closed by enforcing the property instead.

1) MUST-FIX: a raw arm inside dispatchApi was fence-exempt — sdk-qa planted one
after the namespace check and got 200/unreported/unnamed with 348 green,
recreating the shipped public-404/internal-200 split (reproduced here before
fixing). Per PM's ruling, ROUTING and DISPATCH are now separate: the single
API_ROUTES.find() in handleRequest is the only operation in the runtime that
matches a pathname to a handler, and dispatch receives the resolved ROW.
dispatchApi is deleted — there is no dispatcher body left to trust. The fence
now contains DATA only (API_PREFIX, CONTRACT_VERB, the table); nothing
executable with unbounded reach lives inside it, so "a route cannot serve
without a row" is structural: writing a special case anywhere in the dispatch
path requires typing an /api literal outside the fence, which fails the suite.
sdk-qa's plant is kept as the STANDING fifth form of the bypass control, at the
ordinary place — inside the routing block — booted live and required to be
named.

2) MUST-FIX: a dangling symlink was accepted at declaration and became an
out-of-root write target once its outside target appeared — realpath fails the
same way for nothing-there and for a broken link, so the ancestor walk stepped
OVER an existing unresolvable component and settled on the root. The walk now
distinguishes absent from broken: it may step over what does not exist, never
over what exists (lstat) and cannot be resolved (realpath) — dangling links and
link loops are unverifiable at declaration, and the contract assigns
containment to declaration. Tests cover the link-as-target, the child-below-it,
QA's full two-phase timeline (refused before AND after the outside target
exists), the live in-root link positive control, and multi-binding composition.

3) HIGH: an IIFE initializer crashed boot while the guard passed — the
exemption keyed on how the initializer's text BEGAN (a spelling), and the body
stripper removed bodies without asking whether they were invoked. The property
is "does this evaluate at initialization": only provably-deferred bare function
expressions are exempt (a bare arrow cannot be immediately invoked; a bare
function expression defers only when the statement ends at its closing brace),
and stripFunctionBodies keeps any body followed by an invocation paren.
Controls plant both IIFE spellings and both bare negatives; sdk-qa's exact
plant against the real source is named (statement -> handleRequest), and the
three legitimate IIFE initializers already in the file (OVERLAY_DIR, roots,
SDK_VERSION) scan clean because their references are all earlier.

348 -> 350 tests, 350/350 green.
This commit is contained in:
mvschwarz committed 2026-08-08 00:20:41 -07:00
1 parent 805b8b348e
commit ab936f810c
4 files changed
+171 -40

No files matched your search

+60 -24
View File
@@ -894,13 +894,33 @@ function resolveInRoot(kind, rel) {
// slice's feedback/ before anything was captured) and realpath throws on a
// missing path. Walking up validates a not-yet-created directory without
// accepting one that escapes.
let probe = target, real = null;
//
// ABSENT AND BROKEN ARE DIFFERENT, and realpath alone cannot tell them
// apart: it throws the same way for nothing-there and for a DANGLING
// SYMLINK. The walk once stepped over a dangling link, settled on the root,
// and called the path performable — then the link's outside target came
// into existence and the stored path wrote OUTSIDE the bound root, because
// containment had been decided while there was nothing to check. So the
// walk may step over what does not exist, NEVER over what exists and
// cannot be resolved: an lstat-present, realpath-failing component
// (dangling link, link loop) is unverifiable at declaration, and the
// contract assigns containment to declaration — refuse what cannot be
// verified there.
let probe = target, real = null, unverifiable = null;
for (;;) {
try { real = fs.realpathSync(probe); break; } catch {}
let present = false;
try { fs.lstatSync(probe); present = true; } catch {}
if (present) { unverifiable = probe; break; }
const up = path.dirname(probe);
if (up === probe) break;
probe = up;
}
if (unverifiable) {
candidates.push({ base: baseReal, path: target, exists: false, isDir: false,
performable: false, blockedBy: unverifiable });
continue;
}
if (!real) continue;
const rest = path.relative(probe, target);
const resolved = rest ? path.join(real, rest) : real;
@@ -934,7 +954,8 @@ function resolveInRoot(kind, rel) {
const blocked = candidates.filter((c) => !c.performable);
if (blocked.length && !candidates.some((c) => c.performable)) {
return { ok: false, error: `'${rel}' cannot be a capture target under '${kind}': ` +
`'${blocked[0].blockedBy}' is not a directory — a capture target is a directory to write into` };
`'${blocked[0].blockedBy}' is not a directory or cannot be resolved — a capture target ` +
`is a directory a consumer can verifiably write into, decided at declaration` };
}
// DEDUPE BY RESOLVED PATH before counting ambiguity. Two bindings that resolve to
// the SAME real location are one location: `[first, first]`, or two paths through
@@ -1012,15 +1033,32 @@ function filesSearch(q) {
// two markers, enforced by routingSurfaceViolations() in app/verbs.mjs and the
// reserved-verbs tests. A mention outside the fence fails the suite.
//
// ONE STRUCTURE, TWO READERS — PM's ruling, third round on this class. This
// table is what dispatch consults at request time AND what `runtime.routes` and
// classification enumerate, so a route cannot serve without a row and
// enumeration cannot be opt-in. The registration wrapper it replaces
// (`serves()`) was a convention: a raw handler that never called it served live
// while the classifier stayed blind — sdk-qa proved it in three spellings.
// THE FENCE CONTAINS DATA, NOT DISPATCH — PM's ruling, fourth round on this
// class, and the reason it is the last: every earlier fix defined a trusted
// region (a spelling list, a fenced file region, a dispatcher body) and the
// next raw arm simply moved inside the newest region. A trusted region is
// always one special-case away from wrong. So the fence now holds three
// constants and nothing executable with unbounded reach: the prefix, the
// contract verb, and the table. Code inside a handler runs only AFTER its row
// matched — bounded by the row that registered it.
//
// ROUTING is the single table lookup in handleRequest below — the ONLY
// operation in the runtime that matches a pathname to a handler. DISPATCH
// receives the resolved row. A special case in the dispatch path has no
// pathname to branch on without typing an /api literal outside this fence,
// which fails the suite. "A route cannot serve without a row" is therefore
// structural: a served route IS a dispatched row.
//
// ONE STRUCTURE, TWO READERS: this table is what routing consults at request
// time AND what `runtime.routes` and classification enumerate, so enumeration
// cannot be opt-in. The registration wrapper it replaced (`serves()`) was a
// convention: a raw handler that never called it served live while the
// classifier stayed blind — sdk-qa proved it in three spellings, then proved
// the dispatcher-body variant in a fourth.
//
// ADD A ROUTE BY ADDING A ROW, then classify the verb in app/verbs.mjs.
// A verb ending in `/` is a PREFIX, per the shared grammar (verbMatches).
const API_PREFIX = "/api/";
const CONTRACT_VERB = "/api/contract";
const API_ROUTES = [
{ verb: "/api/focus", handler: async (u, req, res) => {
@@ -1178,26 +1216,24 @@ const API_ROUTES = [
return sendJson(res, 200, { ok: true, hits: q.length < 2 ? [] : filesSearch(q) });
} },
];
// The one door. Claims the WHOLE /api namespace: a row matches or the request
// 404s here, so nothing after this dispatcher ever sees an /api path and a
// handler cannot exist outside the table it is enumerated from.
const dispatchApi = async (u, req, res) => {
if (!u.pathname.startsWith("/api/")) return false;
for (const r of API_ROUTES) {
if (verbMatches(r.verb, u.pathname)) { await r.handler(u, req, res); return true; }
}
sendJson(res, 404, { ok: false,
error: `no such contract route: ${u.pathname} — see GET ${CONTRACT_VERB} for capabilities and contract/runtime-api.md for the verb set` });
return true;
};
// <<< API ROUTING SURFACE
const handleRequest = async (req, res) => {
const u = new URL(req.url, `http://127.0.0.1:${PORT}`);
try {
if (await dispatchApi(u, req, res)) return;
} catch (e) { return sendJson(res, 500, { ok: false, error: String(e.message || e) }); }
// ROUTING, then DISPATCH — deliberately not a function, so there is no
// dispatcher body to trust. The find() below is the only operation in the
// runtime that matches a pathname to a handler; dispatch receives the
// resolved ROW. Claims the whole /api namespace: a row matches or the
// request 404s, so no later code ever sees an /api path.
if (u.pathname.startsWith(API_PREFIX)) {
const row = API_ROUTES.find((r) => verbMatches(r.verb, u.pathname));
try {
if (row) await row.handler(u, req, res);
else sendJson(res, 404, { ok: false,
error: `no such contract route: ${u.pathname} — see GET ${CONTRACT_VERB} for capabilities and contract/runtime-api.md for the verb set` });
} catch (e) { return sendJson(res, 500, { ok: false, error: String(e.message || e) }); }
return;
}
// the manifest is served as its VALIDATED projection, not the raw file —
// this is where "invalid rows are excluded from the served rail" is true.
+59
View File
@@ -317,6 +317,65 @@ test("multi-location: a file-blocked binding loses to a performable one — and
assert.match(r2.body.error, /not a directory/);
});
test("a DANGLING symlink is refused at declaration — absent and broken are different", async (t) => {
// QA finding, and the only one of its round with a boundary consequence: the
// ancestor walk used realpathSync, which fails on a dangling symlink exactly
// like it fails on nothing-there — so the walk STEPPED OVER an existing,
// unresolvable component, settled on the root, and called the path
// performable. The declaration returned 200 while the link dangled; when the
// link's outside target later came into existence, the stored path resolved
// OUTSIDE the bound root and the consumer's mkdir succeeded there.
//
// The property, per PM: refuse at declaration what you cannot VERIFY at
// declaration. A component that exists but cannot be resolved is not absent —
// it is unverifiable, and containment is assigned to declaration by contract.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dangle-"));
const root = path.join(dir, "feedback");
const outside = path.join(dir, "outside-not-yet");
fs.mkdirSync(root, { recursive: true });
fs.symlinkSync(outside, path.join(root, "dangling"));
const s = await start({ roots: { feedback: root } });
t.after(() => s.stop());
// The link itself as target, and a child below it — both must refuse.
for (const p of ["dangling", "dangling/child"]) {
const r = await s.declare({ root: "feedback", path: p });
assert.equal(r.status, 400,
`'${p}' accepted while its link dangles — once the outside target exists, writes land outside the root`);
assert.ok(r.body.error, `'${p}' refused without saying why`);
}
assert.equal((await s.read()).target, null, "a refused target was stored anyway");
// QA's full scenario: even after the outside target exists, re-declaring must
// refuse — the resolved path is outside the root, which containment already
// catches. Both halves of the timeline are closed, not just the early one.
fs.mkdirSync(outside, { recursive: true });
const late = await s.declare({ root: "feedback", path: "dangling/child" });
assert.equal(late.status, 400, "a symlink out of the root was accepted once its target existed");
// positive control: a live symlink INSIDE the root still resolves.
fs.mkdirSync(path.join(root, "real-dir"), { recursive: true });
fs.symlinkSync(path.join(root, "real-dir"), path.join(root, "live-link"));
assert.equal((await s.declare({ root: "feedback", path: "live-link/sub" })).status, 200,
"the broken-link refusal also refuses resolvable in-root links");
});
test("multi-location: a broken-link binding loses to a performable one", async (t) => {
// Composes with existence/performability-decides exactly like the
// file-blocked case: unverifiable under one binding must not poison a kind
// that is genuinely performable under another, and must not win by being first.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "danglemulti-"));
const first = path.join(dir, "first"), second = path.join(dir, "second");
fs.mkdirSync(first, { recursive: true }); fs.mkdirSync(second, { recursive: true });
fs.symlinkSync(path.join(dir, "nowhere"), path.join(first, "proof"));
const s = await start({ roots: { feedback: [first, second] } });
t.after(() => s.stop());
const r = await s.declare({ root: "feedback", path: "proof/shot" });
assert.equal(r.status, 200, `the performable second binding lost to a broken-link first: ${r.body.error}`);
assert.ok(r.body.target.path.startsWith(fs.realpathSync(second) + path.sep),
"resolved under the broken-link binding");
});
test("a SYMLINKED duplicate is also one location", async (t) => {
// The same property through a different spelling: two paths that realpath to one
// place are one place. Deduping by the RESOLVED target rather than the declared
+16 -1
View File
@@ -209,8 +209,23 @@ test("BYPASS CONTROL: a raw handler that does not go through the table cannot la
["comment-cut evasion", ' { const cut = /\\/\\//; if (cut && u.pathname === "/api/qa-raw-evade") return sendJson(res, 200, { ok: true }); }\n',
"/api/qa-raw-evade", "qa-raw-evade"],
];
// sdk-qa's fourth-round plant, kept as a STANDING control per PM: a raw arm at
// the MOST ORDINARY place — inside the routing block, after the namespace
// gate, where a competent author writes a special case by accident. Under the
// previous design this spot was INSIDE the fence and therefore trusted: the
// arm served 200, runtime.routes stayed empty, and every committed control
// passed. The fence now holds only data, so this spot is outside it and the
// literal is named. If this control ever stops firing, a trusted region has
// been reintroduced.
const routingAnchor = /if \(u\.pathname\.startsWith\(API_PREFIX\)\) \{\n/;
assert.match(src, routingAnchor, "the routing block moved — re-anchor sdk-qa's standing plant before trusting it");
forms.push(["inside the routing block (sdk-qa round 4)",
null, "/api/qa-inside-fence", "qa-inside-fence"]);
for (const [form, arm, probe, needle] of forms) {
const planted = src.replace(anchor, (m) => m + arm);
const planted = arm !== null
? src.replace(anchor, (m) => m + arm)
: src.replace(routingAnchor, (m) =>
m + ' if (u.pathname === "/api/qa-inside-fence") { sendJson(res, 200, { ok: true, source: "raw-dispatch-arm" }); return; }\n');
assert.notEqual(planted, src, `${form}: the plant did not apply`);
const { statuses, routes } = await bootMutated(planted, [probe]);
+36 -15
View File
@@ -511,22 +511,29 @@ function stripStrings(text) {
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
}
// Remove `=> { … }` and `function … { … }` bodies: code inside a function does
// NOT evaluate at module load, so its references belong to call time, not to
// the initializer being scanned. Expects string-stripped input so brace
// counting cannot be derailed by a brace in a message.
// Remove `=> { … }` and `function … { … }` bodies — UNLESS the function is
// immediately invoked. The property is "does this code evaluate at module
// load", not "does this text look like a function": a deferred closure's body
// belongs to call time and is dropped, but an IIFE's body RUNS DURING
// INITIALIZER EVALUATION and must stay in for scanning. The previous version
// stripped every body without asking, which is how
// `const x = (() => later)();` crashed boot while the guard passed. The
// invocation paren may sit after a wrapping close-paren: `(() => {…})()`.
// Expects string-stripped input so brace counting cannot be derailed.
function stripFunctionBodies(text) {
let out = "", i = 0;
for (;;) {
const m = /(?:=>\s*\{|function\b[^{]*\{)/.exec(text.slice(i));
if (!m) { out += text.slice(i); break; }
out += text.slice(i, i + m.index) + "{}";
let depth = 1, j = i + m.index + m[0].length;
while (j < text.length && depth > 0) {
if (text[j] === "{") depth++;
else if (text[j] === "}") depth--;
j++;
}
const invokedNow = /^\s*\)?\s*\(/.test(text.slice(j));
if (invokedNow) out += text.slice(i, j);
else out += text.slice(i, i + m.index) + m[0] + "}";
i = j;
}
return out;
@@ -574,13 +581,23 @@ function startupOrderingViolations(src) {
const line = lines[i];
const decl = line.match(/^(?:const|let)\s+[^=]*=\s*(.*)$/);
if (decl) {
const initStart = decl[1];
// A pure function expression: nothing but the closure evaluates at load.
if (/^(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(initStart) ||
/^(?:async\s+)?function\b/.test(initStart)) continue;
const end = statementEnd(i);
const text = lines.slice(i, end + 1).join("\n");
initializers.push({ line: i + 1, text: stripFunctionBodies(stripStrings(text)) });
const raw = lines.slice(i, end + 1).join("\n");
const init = raw.slice(raw.indexOf("=") + 1).trim();
const stripped = stripStrings(init);
// Exempt only what PROVABLY defers: a BARE function expression. A bare
// arrow cannot be immediately invoked without first being parenthesized
// (which changes the leading shape), and a bare `function` expression is
// deferred only when the statement ends at its closing brace — ending in
// `)` means it was called. Anything else — including an IIFE, which
// textually BEGINS like a function expression and runs at load — is
// scanned. The previous exemption keyed on how the text STARTED, which is
// a spelling, not the property.
const bareArrow = /^(?:async\s+)?(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(init);
const bareFunction = /^(?:async\s+)?function\b/.test(init) && /\}\s*;?\s*$/.test(stripped);
if (!bareArrow && !bareFunction) {
initializers.push({ line: i + 1, text: stripFunctionBodies(stripped) });
}
i = end;
continue;
}
@@ -659,17 +676,21 @@ test("positive control: the ordering checker fires on BOTH load-time spellings o
" { url: x },",
").catch(() => {});",
"const qaInitializerProbe = handleThing;",
"const qaInitializerIife = (() => handleThing)();",
"const qaBraceIife = (function () { return handleThing; })();",
"const notALoad = () => handleThing;",
"const notALoadBrace = () => { return handleThing; };",
"const handleThing = async () => {};",
"listenSomething();",
].join("\n");
const { violations } = startupOrderingViolations(planted);
const reach = violations.filter((v) => v.kind === "reachability");
assert.deepEqual(reach.map((v) => [v.statement, v.binding]).sort((a, b) => a[0] - b[0]),
[[3, "handleThing"], [6, "handleThing"]],
`expected the STATEMENT plant and the INITIALIZER plant, exactly: ${JSON.stringify(reach)}`);
assert.ok(!reach.some((v) => v.statement === 7),
"a function-expression initializer was flagged — its body runs at call time, not load time, " +
[[3, "handleThing"], [6, "handleThing"], [7, "handleThing"], [8, "handleThing"]],
`expected the STATEMENT plant, the direct INITIALIZER, and BOTH IIFE spellings — an IIFE is not ` +
`a deferred closure, its body runs during initializer evaluation: ${JSON.stringify(reach)}`);
assert.ok(!reach.some((v) => v.statement === 9 || v.statement === 10),
"a bare function-expression initializer was flagged — its body runs at call time, not load time, " +
"and over-firing there would force every helper above the binding it eventually calls");
assert.ok(violations.some((v) => v.kind === "position"),
"the position property must also fire on statements before the last binding");