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.