GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

AUDIT_import_guard.md



RELEASE

planning/2.8.5/audits/AUDIT_import_guard.md

sha256 7da1df0f9ee51a04 · 9803 bytes · original held in the private archive

# AUDIT — registry/game-context trust guard on `autoImportState()` (commit `6d9d723`) **Stage 3 (Opus independent audit) · Protocol 8 · branch `dev` · audited against the running code and independently-reproduced behavior, NOT the implementer's report.** Snapshot as of 2026-07-14 — audit record, not a current-truth doc. --- ## VERDICT: **PASS** The guard does exactly what it claims: it prevents the data-loss scenario, it does **not** weaken Protocol 24 on the normal path, and the AI payload has **no** path to flip the trust flag. I reproduced the fix red→green two independent ways (my own vm harness, and the project runner with the guard mechanically reverted). Test count 3213→3221 is purely additive — zero deletions, no existing assertion weakened. The "fails open on an untagged registry" decision is the correct **do-no-harm** call, with one clearly-bounded residual worth recording (not a blocker). No code change required. --- ## (1) ⭐⭐ Does it prevent the data loss? — YES, reproduced BOTH ways, independently. I wrote my **own** vm-sandbox harness (not the project's Suite 229), loading the real `state.js` + real `reg_nv.js` + the extracted `autoImportState()` body, and toggled the guard myself by stripping every `_registryTrusted && ` prefix: ``` registryGame tag in reg_nv: FNV A [WITH guard, mismatch] collectibles = ["Strength","Perception"] -> survives? true B [NO guard, mismatch] collectibles = [] -> wiped? true C [WITH guard, same-game] collectibles = ["Goodsprings"] -> hallucination rejected? true D [WITH guard, mismatch+fake] collectibles = ["Strength"] -> untouched (no FAKEITEM leaked)? true ``` - **A / B are the two directions that matter.** NV registry loaded (`FALLOUT_REGISTRY.game === 'FNV'`) but `state.gameContext === 'FO3'`; the AI echoes the campaign's own real FO3 bobbleheads `Strength`/`Perception` (verified: both are genuine `reg_fo3.js` collectibles, both absent from `reg_nv.js`). **With** the guard the arrays survive intact; **without** it they are emptied to `[]` — the exact durable-data-loss the prior audit reproduced. - **D is an extra case I added** and it matters: on a mismatch the guard **skips** the field, leaving the *existing state* — it does **not** blindly accept the incoming payload. A fabricated `FAKEITEM` in the mismatched payload does not leak into state. The skip is "leave what's there," not "trust the AI instead," which is the right semantics. I then re-ran the **project's own runner** with the guard mechanically reverted (`sed 's/_registryTrusted && //g'`) and restored it clean afterward: ``` ✗ 229.4 (all five fields gated) ✗ 229.5 (mismatch — real FO3 collectibles not wiped) ✗ 229.6 (mismatch — lincolnItems object-map protected) ✓ 229.7 (same-game hallucination still dropped) ← passes with OR without the guard ✓ 229.8 (same-game real name still kept) ← passes with OR without the guard 3 TEST(S) FAILED ``` That 229.7/229.8 pass in both states is a *feature*, not a gap: they exercise the pre-existing validation, so the guard-specific tests (229.4/5/6) are the only ones that flip — proving the guard is genuinely the thing protecting the mismatch case, not incidental coupling. Suite 229 is a well-designed red→green guard. Working tree restored to `6d9d723` clean. ## (2) ⭐⭐ Did it weaken Protocol 24? — NO. Hallucination still rejected, and the flag is un-spoofable. **Normal path is untouched.** The only edit to the five field blocks is prepending `_registryTrusted &&` to each `if`. The validation bodies (`new Set(FALLOUT_REGISTRY.<field>.map(...))` + `.filter(... !names.has(x) ...)`) are byte-for-byte unchanged. When the registry matches the context (the common case), `_registryTrusted` is `true` and every block runs exactly as before — case **C** above confirms a genuinely hallucinated name (`TOTALLY HALLUCINATED 229`) is still dropped while the real `Goodsprings` is kept. **The trust flag cannot be spoofed by the AI.** Precise trace: ```js const _registryGame = typeof FALLOUT_REGISTRY !== 'undefined' ? FALLOUT_REGISTRY.game : undefined; const _registryTrusted = !_registryGame || _registryGame === (state.gameContext || 'FNV'); ``` `_registryTrusted` is `true` iff **(a)** `_registryGame` is falsy (fail-open — see §3), **or** **(b)** the boot-loaded registry tag equals `state.gameContext`. Both inputs are outside the AI's reach: - `FALLOUT_REGISTRY.game` is a boot-time-only const from `reg_nv.js`/`reg_fo3.js` (the `GAME_FILES` manifest loads exactly one). Nothing at runtime rewrites it. - `state.gameContext` is **user-only**. I grepped the entire `autoImportState()` body: the only references to `gameContext` are the two **reads** in the guard (line 688 + the error string 691). The function never writes it, and the AI's `parsed.gameContext` field is explicitly ignored (comment at line 662–664). So no payload can move `state.gameContext` *before* the flag is computed to force a false "match." There is therefore **no attacker/hallucination path that makes `_registryTrusted` true when it shouldn't be**, other than the deliberate fail-open branch. Protocol 24 is intact. The change trades nothing away on the common path. ## (3) ⭐ The "fails open" decision — SAFE, correct do-no-harm call; one bounded residual recorded. An untagged registry (`_registryGame` undefined — e.g. a stale pre-`6d9d723` bundle served by an old service-worker cache) yields `_registryTrusted === true` → validate-and-drop, i.e. **exactly the pre-commit behavior**. Verified in code; matches the implementer's claim. **My view: fail-open is the right default here.** Reasoning: - **It cannot introduce a regression.** For fail-open to lose data you need *both* a game/context mismatch *and* an untagged registry — which only co-occur on a pre-`6d9d723` bundle, where this exact data-loss bug already existed **unprotected**. So fail-open makes the new code behave identically to old code in that stale window: no new protection, but also no new harm. The moment a tagged registry loads (the norm), full protection is active. - **Fail-CLOSED would be worse in the common failure it's meant to survive.** Treating an unknown registry as untrusted → permanently skipping all five fields → those fields silently stop accepting *real* player acquisitions too (a functional freeze of collectibles/traits/books/mags/lincoln) for any transient untagged-registry window. That is a visible functional regression to defend against a scenario that (per above) equals old behavior anyway. **Bounded residual worth recording (not a blocker):** a mismatched **and** untagged registry is still vulnerable to the drop. This is acceptable because it is (a) transient — resolved on the next "Reboot Terminal" update that ships the tagged registry — and (b) no worse than the code shipped before this commit. Worth a one-line note for a future FO4 registry: **`reg_fo4.js` must declare `game: 'FO4'` when it lands**, or FO4 imports fall into this same fail-open (unprotected) branch. Not actionable now — no FO4 registry exists yet. --- ## Also verified - **`FALLOUT_REGISTRY.game` tags** — `reg_nv.js` → `'FNV'`, `reg_fo3.js` → `'FO3'`. Both match the exact `state.gameContext` literals (`gameContext: 'FNV'` default; `'FNV'|'FO3'`). No typo — confirmed a matched registry computes `trusted=true` (case C/D ran green against the real `reg_nv.js`). - **Protocol 38 (game-agnostic)** — the check is pure data-vs-data (`_registryGame === state.gameContext`); the `|| 'FNV'` is a sanctioned fail-safe absence default, not a game coercion. No `=== 'FO3' ? …` literal branching. ✔ - **Protocol 22 (extend, don't fork)** — `autoImportState()` was edited inline; no parallel importer. ✔ - **Suite 229 red→green** — reproduced independently (see §1); 179 insertions, purely additive. - **Test count 3213 → 3221 (+8), suites 228 → 229** — full runner green at 3221. **Zero deletions in `robco-diagnostics.js`** (`git show … | grep '^-[^-]'` empty) → no existing assertion loosened. The `229.4` `>= 5` count assertion meaningfully matches the exactly-five field gates. ✔ - **Protocol 2a sync** — drift scan for `3213`/`228 suites`/`Suites: 228` across `RULES.md`, `CLAUDE.md`, `README.md`, `ARCHITECTURE.md`, `CHANGELOG.md`, both test files returned **empty**. All locations show 3221/229. ✔ - **Protocol 1** — `CACHE_NAME` `robco-terminal-v2.8.0-r27` → `-r28`, monotonic, `APP_VERSION` unchanged; served files (`js/`) changed so the bump is required and correct. ✔ - **CHANGELOG (Protocol 21)** — the single `[Unreleased]` entry is plain-English, no code identifiers, leads with user impact, and every clause is **true**: "genuinely made-up items are still rejected exactly as before" (confirmed, case C) and "nothing you can see changed" (confirmed — the guard only activates on a mismatch that reboot already prevents). ✔ - **Protocol 41** — `git status --porcelain` clean; no scratch/junk files left. ✔ - **Protocol 40 (`test.html`)** — no new state field and no boot-chain change, so no `KNOWN_KEYS`/suite-marker update was owed; the headless `test-html-check.mjs` passes in the gate. ✔ --- ## Bottom line **PASS.** The guard prevents the durable data loss (reproduced both ways), leaves Protocol 24's hallucination-rejection fully intact on the normal path, and exposes no payload-controllable path to disable validation. Fail-open is the correct do-no-harm default; the only residual (mismatched **and** untagged registry) equals pre-commit behavior and self-heals on the next update — record the FO4-registry `game:` tag requirement and ship as-is. No return to Stage 2.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).