Three review rounds found the same defect in this guard: the value validated
and the value used came from two different reads. Each repair narrowed the
window and the next round reopened it -- a Proxy, then a getter replacing
Object.keys mid-enumeration. The single-entries-array form closes both, and a
fourth round showed it still is not a proof: the helper calls Object.fromEntries,
map and filter after the enumeration, and a getter can replace any of them.
That is not closable here. These cases share a closure with the guard, so an
author who can patch a global can patch the guard. The escalation is what was
wrong -- the previous round added a caller-supplied callback to the one spawn
site purely to make a mutation proof read better, and it had to be reverted.
So the claim is bounded rather than narrowed again:
- Freeze ALLOWED_CHILD_ENV, DECLARABLE_CASE_ENV_NAMES and SHELL_GENERATED_ENV.
The name list was mutable; a case could push a forbidden name before
declaring it. Verified: the push now throws and the spawn is still rejected.
- Say what the guard does. It rejects an ambient copy or a stray name passed
as a declaration, which is the mistake a maintainer makes by accident. The
comments claimed it closed the accessor gap; they no longer do.
- Correct the bootstrap() comment: main.ts constructs a logger before the
secret check. The load-bearing claim -- the throw precedes any config read --
is unchanged.
Document corrections, all found by review of claims written in earlier rounds:
- Withdraw "no second call to any replaceable global stands between the check
and the use". False, and stated while correcting a different overclaim.
- Both exploits are rejected, but by different assertions. The ambient copy is
named by the offending-keys message; the Proxy is stopped one assertion
earlier by isProxy, whose message names no keys.
- The sandbox is created in spawnHermetic before the spawn call, not inside
the spawn arguments. main.spec.ts:58 cleans up in a finally rather than by
registration, outside the kbn-02a- prefix the residue count measures.
- The package.json RED does not execute the route yet: the script is absent, so
the case stops on the missing manifest entry before any child is created.
- Three route cases, not two. Four XDG_*_HOME roots plus XDG_RUNTIME_DIR, not
"five XDG home roots". Not every failure is behavioral -- the structural
source guards are counted in the same 83.
RED unchanged: 83 failing / 17 passing (100 cases, 6 files), no Errors row,
zero /tmp/kbn-02a-* residue, fixture control green. Typecheck, lint, Prettier
clean.
The dev:local route case built its child environment as a denylist — copy the
ambient process.env, delete three keys — leaving the real HOME, every XDG root,
MOSAIC_GATEWAY_HOME, service endpoints, auth/credential families, the Node
loader controls and an ambient PATH in place. A faulty GREEN could read real
host state before an assertion caught it, and a refusal could be produced by
the host's own configuration rather than by the route, so the case would pass
while proving nothing about the code under test. The hard timeout bounds how
long a leak runs; it does not prevent one.
The child now receives exactly eight variables built from scratch — a private
HOME under a per-run temporary sandbox, all five XDG roots and TMPDIR beneath
it, and a PATH built from the two workspace .bin directories and the directory
of the running Node binary — plus the per-case declarations. Nothing is
inherited.
Two cases hold that rather than assume it: a fixture control asserting the
constructed key set, that HOME is neither the real home nor an ancestor of it,
and that PATH is the three intended directories (green by construction, a
harness negative control, not coverage); and an executed case that plants a
complete valid local declaration and a forbidden DSN in the parent environment
and requires the undeclared refusal — a leaky fixture would fire the DSN
refusal instead.
RED only. 83 failing / 17 passing (100 cases, 6 files), Errors 0, zero
/tmp/kbn-02a-* residue. No GREEN, no push, no 17th fenced path.
Second AMEND from the verifier. Five blockers, all inside the 16-path fence; no
new path taken and no GREEN implemented.
1. The run carried an out-of-band `process.exit unexpectedly called with "1"`,
raised by main.ts:71 self-invoking on import and rejecting after the importing
case had returned. Vitest attributes that to no case and warns it can produce
false positives; it also falsified the document's "zero non-assertion
failures". The import is now wrapped so `process.exit` is stubbed
non-throwing and the case awaits a settle turn — the behavioral red is
unchanged, the error is owned, and the run reports `Errors 0`.
2. Two inventory cases demanded the raw values 'standalone' and 'postgres',
which the value-free classification unions cannot represent, so no compliant
implementation could have turned them green. They assert 'non-local' and
'non-pglite'.
3. SourceReadability gained `absent`. Without it an absent source has to be
reported `readable` — the one value meaning "read, and clean" — collapsing
the distinction the refusal path is built on. The invariant
`present === false` iff `readability === 'absent'` is now asserted per state
and across all seven rows at once.
4. Acceptance #6 had no positive anchor: an empty `bootstrap()` and an
always-refusing `onModuleInit()` satisfied every case, so "safe" could have
meant "does not work". Two cases now fail against that — bootstrap must reach
NestFactory.create and listen under the frozen policy with zero independent
read, and the sterile local lifecycle must actually run both migrations with
no Postgres handle. Both in-process and substituted at their seams; no port
is bound and nothing connects.
5. Every mkdtemp site in the six files now registers teardown at the point of
creation, including the child route's root created inside the spawn
arguments. The matrix leaves zero /tmp/kbn-02a-* residue, measured.
Focused matrix: 82 failing / 16 passing (98 cases, 6 files), Errors 0.
Typecheck, lint, Prettier, diff --check clean.
Author phase only; still no implementation. Kept as a distinct fifth commit so
the RED baseline, the contract restoration, the span audit and this supplement
remain separately reviewable.
The finding that mattered: every frozen-policy case mocked ./main.js wholesale,
so nothing executed main.ts, AppModule or the ConfigModule provider, and the
three independent reads at main.ts:28, app.module.ts:39 and config.module.ts:12
could all have survived a fully green matrix. Fence revision 15 -> 16 (granted
solely for apps/gateway/src/main.spec.ts) is used to import the real modules
with no ./main.js mock and assert no independent loadConfig, no tier probe and
no NestFactory.create. The existing anchoring case is left red-in-place rather
than deleted: its import-time-exit premise is the opposite of the new contract,
so it must be migrated by whoever implements GREEN.
Also closes: the executed dev:local child-process route, which requires a
LOCAL_START_* code AND the absence of the BETTER_AUTH_SECRET throw, since a
nonzero exit alone is satisfied by a crash; the real database onModuleInit
lifecycle, asserting migrations never run; the full disposable state-root
contract (absent, not-a-directory, symlink, non-private, foreign-owner), with
ownership substituted at lstat because a filesystem-built case passes trivially
wherever CI runs as root; unreadable and malformed sources, which refuse rather
than being skipped, built as EISDIR so they cannot be vacuous under any uid;
per-source non-local tier detection for all seven labels; canary retention and
console cases; post-freeze source-mutation cases; and a three-form static-import
guard.
Two defects of my own are fixed rather than papered over. The non-canonical
state-root case normalized to the valid path before the policy saw it, so it
demanded a refusal on input identical to the accepted one — a red that could
never go green for its stated reason. And the first substitution of @nestjs/core
was wholesale, which dropped APP_GUARD and killed app.module.ts at import, so
two cases failed for a reason they did not state.
Classification types are unions, not strings: a declaredTier?: string is
value-free only by convention, and a two-member union cannot carry a canary.
77 failing / 15 passing (92 cases, 6 files). Typecheck, lint and Prettier clean.
Zero non-assertion failures. Seven of the fifteen greens are vacuous against the
stub, up from three; that rise is recorded in the document so it cannot later be
read as coverage. The first RED report's implication that the GREEN targets were
covered behaviorally is corrected there too.
Refs: KBN-101-02a (Stack #1178)
Scrappy's ruling, verified independently at
docs/scratchpads/771-kbn101-db-role-split.md:98, which records
apps/gateway/Dockerfile explicitly as 'a planned KBN-101-05 artifact'.
The row names something intended to exist, not a mis-naming of
docker/gateway.Dockerfile, so it is not the inverse defect I filed it as.
The correction sharpens the proposed control: a link check failing on any
absent ownership path would have flagged this row and been wrong. The
check needs a planned marker, and an unmarked absent path is the failure.
Negative existence evidence retained; classification corrected.
Commissioned with -02a on the board ruling. Four findings: the -02
local-start hold is carried by eight files not five (AGENTS.md and
README.md among them, so -02a's completion surface is wider than its
record); a candidate defect of the same shape at -05, whose scope row
contains neither 'process-exec' nor 'LoadCredential' though three guides
condition deployment on that interface; a negative result confirming the
-00/-03/-05 activation hold spans correctly; and the structural hole that
makes all of them unverifiable, whose inverse form is the -05 rows naming
apps/gateway/Dockerfile, a path that does not exist.
Additive to section 10 only. No other item's rows altered; the Dockerfile
discrepancy is reported, not corrected — that row is -05's.
b4c468b3 wrote the -02a path-ownership manifest OVER the frozen KBN-101
contract (#774, #902) rather than appending to it, deleting 272 lines
including the KBN-101-05 exclusive-ownership row, the K101-REQ-04
traceability row and the -03 runner/manifest contract.
Sections 1-9 are restored byte-identical to 216cd722. The -02a amendment
is re-added as additive section 10 with its headings demoted. No other
item's ownership rows are altered.
Self-reported to the verifier; the defect class is the same span/scope
failure -02a exists to close.
Author phase only. No implementation: the scaffolding is deliberately
permissive so every refusal assertion fails behaviorally rather than on a
missing module. A `not implemented` throw would have satisfied `.toThrow()`
and produced a green matrix that proved nothing.
Adds the path-ownership manifest and 60 behavioral cases across the nine
mutation->guard rows: inherited env, shared dotenv, daemon JSON, app JSON,
root JSON, MOSAIC_GATEWAY_HOME redirect, exact local tier, exact PGlite plus
disposable state root, and one-shot freeze with dynamic import.
50 red / 10 green. Zero non-assertion failures. The ten greens are itemized
in the manifest, including three that are vacuous until the implementation
exists.
Enforcement sits at the apps/gateway seam. Nothing in packages/db or
packages/config is touched: -02a proves the local entrypoint never reaches
those paths rather than modifying code owned by -01, -03 and -10.
Refs: KBN-101-02a (Stack #1178)
First-pass structural reference for the Vite SPA migration (apps/web): dual-app
tree during migration, shared lib/ networking layer, origin-relative/same-origin
serving model, build scripts, the P1-P6 increment map, and the #1145 P5 blocker.
Living doc — details to be fleshed out by follow-up.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ESFAnh2t9HmLwng8oW95St
Docs-only plan PR. FRED_APPROVED_REF=0629361ca39a4dd7fb3e575d11c64bea9e545dae (review 147). Merged by fred (orchestrator) via API: pr-merge.sh policy predates the next lane (main-only hardcode) — wrapper fix tracked separately.
Co-authored-by: Velma <[email protected]>