Skip to content

feat(desktop,web_ui): E2 first-run validation wizard — RAM gate, sha256 manifest verification, pack activation, license gate (issue #85) - #116

Merged
zaxbysauce merged 2 commits into
masterfrom
issue-85-first-run-wizard
Sep 17, 2026
Merged

zaxbysauce merged 2 commits into
masterfrom
issue-85-first-run-wizard

Conversation

@zaxbysauce

@zaxbysauce zaxbysauce commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Issue #85 — PR Body

Closes #85

PR head: 5771e4c

Root Cause

Not a defect — a planned feature slot (Workstream E2, epic #50) whose prerequisite primitives had just landed: the A3 RAM-gate formula (estimate_required_memory, llm_interface.py:42-44, Python-only), B4 profile auto-selection (selectProfile, 6 GiB inclusive threshold), B9 model-presence status (modelStatus()existsSync-only at desktop/main/backend/inference/llama-engine.ts:375-385, which ships corruption silently), and the C3 PackManager lifecycle. Nothing composed them into a first-run experience, and no desktop surface verified the sha256 of any packaged file or required license acknowledgment. Additionally, the Electron bootstrap ignored TRAININGAPP_DESKTOP_STORE_PATH (consumed only by the headless dev-server, dev-server.ts:101), so the Playwright-under-Electron suite could not isolate per-test stores — the very seam the existing e2e specs already assumed.

Fix

  • desktop/main/first-run/ram-gate.ts (new): TS port of A3's file_size + kv_estimate(n_ctx) + GGUF_LOAD_OVERHEAD_BYTES (1 GiB each; n_ctx = CONTEXT_SIZE 8192, now exported from the engine so the gate estimates the context it will actually create), autoSelectProfile with a number-naming downgrade warning, a declared-manifest-size fallback for models not yet staged, and the TRAININGAPP_DESKTOP_FREE_RAM_BYTES dev/test seam.
  • desktop/main/first-run/manifest-verifier.ts (new): loads the [Workstream E] PR 1 of 6: Package models and default packs into the Electron installer with an integrity manifest and a <=7 GB size budget #84-pinned resources/manifest.json contract and verifies every required file's sha256 (sizeBytes check included). Failures are a CLOSED UNION (missing | hash-mismatch | size-mismatch | sha256-required | manifest-unreadable) each carrying path + expected/actual — the generic "configure an LLM backend" failure shape is unrepresentable. Packaged install without a manifest fails closed; dev/CI degrades explicitly (staged:false).
  • desktop/main/first-run/first-run-store.ts (new): atomic <profileDir>/first-run.json sidecar persisting firstRun.completed/selectedProfile/completedAt + license acknowledgment + a per-file digest anchor (the engine settings API validates a closed rag_* set and cannot carry these). evaluateStatus yields not-completed/drift/reset/complete; drift re-derives from disk and outranks the force seam.
  • desktop/main/first-run/wizard.ts (new): the six-state machine (detect-hardware → select-profile → verify-manifest → activate-packs → licensing-notices → complete) with assertCanComplete (license acknowledgment unskippable; every refusal names the unmet gate) and manifestCompletionState (packaged fail-closed rule).
  • Wiring: host own-property getFirstRunPackTools() (backend/index.ts:230 — b3 prototype pin respected), four desktop:first-run:* IPC channels + first-run:required boot push + stub-engine exemption with TRAININGAPP_FIRST_RUN_FORCE=1 seam (desktop/main/index.ts), desktopApi preload extension with typed surface (web_ui/src/types/desktop.d.ts).
  • Renderer: FirstRunWizard.tsx modal stepper (failure table with path/expected/actual, profile warning + explicit Quality override, pack activation with results, license notice rendered from docs/licenses.md when present with a required acknowledgment checkbox) mounted via FirstRunGate in App; Settings "First-run setup" section with "Re-run setup" (reset + reopen bus).
  • Bootstrap now honors TRAININGAPP_DESKTOP_STORE_PATH (dev-server parity), giving the e2e suite true per-test store isolation.
  • Docs: desktop/README.md "First-run wizard (E2)" section; CHANGELOG.md entry.

Recurrence Prevention (defect class)

  • Defect class: a failure surface reporting a generic condition instead of naming the specific file with measured expected/actual values, discarding the real diagnostic.
  • Sweep result: 10 hits across 4 predicates (P1/P4 zero — the cited misdirection pattern does not exist anywhere; P2 five existsSync uses — all absence-signals or hash-prefetches, none an integrity claim; P3 five catches — all named degradations or cleanup-with-rethrow), each dispositioned in 08a-recurrence-sweep.md.
  • Guardrail: type-level prevention — the failure reason is a closed union whose every variant must carry path/expected/actual, plus discriminating tests (C3 names the deleted file + expected hash; C4 asserts distinct expected/actual sha256; a further test pins that a required file without a manifest sha256 FAILS rather than silently skipping). Demonstrated RED→GREEN by the frozen checks.

Tests

  • Frozen checks (7/7 RED→GREEN via repro-check.sh run, base 98d3cef → head 2a8c824): C1-C4, C5-C6 (Playwright-under-Electron), C3/C4/C7 (vitest) — verdicts PASS, logs in the issue trace.
  • Regression test: desktop/src/__tests__/e2-first-run-wizard.test.ts (17 tests) and desktop/e2e/first-run-wizard.spec.ts (4 tests) -> PASS.
  • Impacted suites: desktop && npm test -> 425 passed (67 files); desktop && npx playwright test -> 7 passed (incl. renderer-smoke restart leg and production-mode training-player); web_ui && npx vitest run -> 1278 passed | 2 skipped.
  • Type/build: desktop && npm run compile -> clean; web_ui && npx tsc --noEmit and -p tsconfig.test.json -> clean; web_ui && npm run build -> ok.
  • Deferred-work scan: scan-deferred.sh -> clean.
  • Checkpoint integrity: repro-check.sh verify-checkpoint -> 7/7 OK; per-driver last-manifest-row blob audit -> 7/7 MATCH (7 CHECK_WRONG AMEND rows were driver-infrastructure fixes only: always-rebuild guard against a stale renderer dist, and a vitest cwd-relative path; discriminating assertions untouched).

Regression Protection

  • New coverage lives in its own files (repo convention — checkpoint-frozen specs of prior traces untouched).
  • Negative/adversarial cases pinned: deleted file, corrupted bytes, sha256-less manifest entry, corrupt sidecar, license gate refusal, downgrade refusal path via PackManager errors surfaced per-pack, packaged-missing-manifest fail-closed.
  • Two web_ui test-support files (electron-detect.test.ts, DocumentsPage.electron.test.tsx) gained inert type-complete bridge stubs — required by the extended DesktopApiBridge; no assertion weakened.

Acceptance Criteria -> Evidence

Acceptance criterion (from intake) Evidence (command + output, or test name)
AC1 happy path: all six states, packs active e2e C1: GREEN (first-run-wizard.spec.ts) — all six step sections traversed; activation result installed bundled-min@1.0.0; Finish closes a wizard whose status.needed is false. Fixture manifest stands in for E1's installer manifest (E1/#84 owns the real one) — disclosed boundary, real-weight validation is E3/#86.
AC2 low RAM auto-selects Fast + warning + override e2e C2: GREEN — TRAININGAPP_DESKTOP_FREE_RAM_BYTES=4 GiB, declared 3.85 GiB quality model → Fast preselected, warning names required/free bytes, Quality radio enabled ("override"); backed by unit parity tests vs llm_interface.py:42-44.
AC3 deleted required file blocks, NAMED vitest C3: GREEN — path + sha256 <expected> + actual "missing" + reason 'missing'.
AC4 corrupt bytes blocked, expected vs actual vitest C4: GREEN — 'hash-mismatch' with distinct expected/actual sha256 strings.
AC5 license acknowledgment unskippable e2e C5: GREEN (with no required packs, so the license gate is the only varying condition) — Complete disabled before, enabled after the checkbox; IPC arm pinned by assertCanComplete unit refusals.
AC6 drift re-triggers on relaunch; manual Re-run setup e2e C6: GREEN — complete → mutate covered file → kill → relaunch → wizard re-opens with drift reason; unit pins evaluateStatus drift (incl. drift-outranks-force); Settings "Re-run setup" wired (reset → reason 'reset', reopen bus).
AC7 firstRun.* keys persist vitest C7: GREEN — atomic sidecar round-trip (completed/selectedProfile/completedAt + ack + digest anchor), no tmp residue, corrupt sidecar degrades to defaults.

Invariant Audit

Repository invariant/architecture-contract sources checked: ADR-0003 (backend host), ADR-0004 (pack format), ADR-0006 (profile model), ADR-0008 (memory budget), the b3 duck-type pin (backend/index.ts comments), and the store SCHEMA-BUMP-CONTRACT.

  • b3 duck-type pin (host prototypes expose exactly start/stop): not touched — getFirstRunPackTools is an own property (backend/index.ts:230), mirroring createStoreBackup.
  • Frozen API contract (contracts/api.openapi.yaml, cross-backend conformance): not touched — zero new HTTP routes; wizard state flows over Electron IPC only; CONTRACT_ROUTES unchanged.
  • Store schema (contracts/store.schema.sql SCHEMA-BUMP-CONTRACT): not touched — no schema change; wizard state is a sidecar file; packs table consumed read/write via the existing C3 PackManager.
  • B2 transport security (loopback guard, token bridge): not touched — new IPC uses the established ipcMain.handle + contextBridge pattern; no new remote surface; token still never in web storage/URL.
  • Secure renderer defaults (frozen secure-defaults spec): not touched — verified by the still-green desktop suite (425) including test:secure-defaults.
  • B4 profile semantics (selectProfile/6 GiB threshold): not weakened — the wizard composes via its own install-time gate and never writes the runtime setting; DEFAULT_PROFILE_THRESHOLD_GB imported by memory/budget.ts unchanged.
  • ADR-0008 memory governance (downgrade override AC3): not touched — the wizard reads free RAM at install time only.

Risk and Rollback

  • Risk level: low-medium. Desktop-only feature; browser (non-Electron) surface untouched (FirstRunGate renders nothing without the bridge). One behavioral seam change outside the wizard: the Electron bootstrap now honors TRAININGAPP_DESKTOP_STORE_PATH (previously dev-server-only) — this is additive (no env set → identical ADR-0006 behavior) and is what the existing e2e specs already assumed; the full desktop e2e suite (incl. renderer-smoke and production-mode training-player) passes with it.
  • Rollback: revert the single commit 2a8c824 (no migrations, no schema change, no config). A leftover first-run.json in a profile dir is inert on older builds.

Review round (swarm-pr-review pr116-review-1)

A structured swarm-pr-review pass (base lanes + all 11 micro risk-families) validated four findings, ALL FIXED in 5771e4c with regression tests: PRR-001 (MEDIUM, manifest-path containment — traversal now a named failure, unit-pinned), PRR-002 (single manifest verify per status call), PRR-003 (wizard Esc-to-dismiss + Tab focus trap, component-tested), PRR-004 (type-honest bridge stubs). Closure ledger: 4/4 FIXED, 0 rejected, 0 deferred.

Waivers (or none)

No waivers of the Full-Resolution Contract. One disclosed gate limitation requiring user ratification rather than a waiver: subagent dispatch was unavailable for this entire session (harness-side "No reasoning level selected" error on 9 dispatch attempts across 4 agent types, including two minimal OK-probes), so the plan-critic, implementation-review, and final-critic gates ran in the orchestrating context under the issue-tracer's documented fallback rule instead of pinned cross-model independent contexts. Every gate artifact (06-critic-review.md, 08b-implementation-review.md, 09-final-critic.md) records this disclosure verbatim, and all mechanical gate work (frozen-check replays, checkpoint blob audits, typechecks, freshness proofs) was executed as fresh commands in a pristine detached worktree whose tree-id equals the commit tree. If the user prefers, a post-hoc cross-model re-review can be run before merge.

Merge status

AWAITING_USER_APPROVAL

…56 manifest verification, pack activation, license gate (issue #85)

Six-state wizard (detect-hardware -> select-profile -> verify-manifest ->
activate-packs -> licensing-notices -> complete) wired from the A3 RAM-gate
formula, the #84-pinned integrity-manifest contract, and the C3 pack
lifecycle. firstRun.* state persists in an atomic profile-dir sidecar; drift
on any manifest-covered file re-triggers the wizard; the license
acknowledgment is unskippable and every failure names the file + expected/
actual. The Electron bootstrap now honors TRAININGAPP_DESKTOP_STORE_PATH
(dev-server parity), giving the e2e suite per-test store isolation.

Acceptance: 7 frozen NEW-SURFACE checks RED->GREEN; desktop vitest 425,
desktop e2e 7 (incl. renderer-smoke + production-mode training-player),
web_ui vitest 1278 + typecheck(app+test) clean.
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Swarm PR review (run pr116-review-1)

Bound head: 2a8c8246336341b1c1de0ffa914c3d4f6d5d8b4a · Base lanes + all 11 micro risk-families evaluated. Dispatch-mode disclosure: subagent dispatch was unavailable this session (harness-side error), so lanes ran in-context with attestations rather than as parallel agents — coverage is complete, the mode is disclosed.

Verified findings

ID Severity Family Location Finding Validation
PRR-001 MEDIUM untrusted-input-boundaries desktop/main/first-run/manifest-verifier.ts:152, desktop/main/index.ts packEntryDir Manifest-controlled files[].path / packs[].dir are joined to roots without containment — a traversal path resolves (and gets read+hashed) outside the configured root. Requires manifest control (installer-level), so defense-in-depth. Empirically proven: path.join('C:/stage/resources','../../../../Windows/.../hosts') escapes root (escapes root: true).
PRR-002 LOW concurrency-state / perf desktop/main/index.ts:370,386 buildFirstRunStatus() calls wizardManifest() twice per status invocation; each runs a full sha256 pass over every required file — doubles a ~15-30 s pass on a real 7 GB manifest. Code-read at bound head (two call sites, both running verifyManifest).
PRR-003 LOW ui-accessibility web_ui/src/components/FirstRunWizard.tsx Modal has no Esc-to-dismiss and no focus trap; keyboard focus can escape to background UI. Code-read (no keydown/focus handling present).
PRR-004 INFO test-infrastructure electron-detect.test.ts:21, DocumentsPage.electron.test.tsx:93 Inert bridge stubs use as unknown as casts that erase a null-vs-object return mismatch. Code-read.

Rejected candidates (transparency)

  • "wizard complete handler re-verifies manifest (TOCTOU guard) is redundant" — rejected: it is the TOCTOU guard; removing it would let completion proceed against a stale verification.
  • "TRAININGAPP_DESKTOP_MANIFEST env is a security hole" — rejected as MEDIUM+: it is a documented dev/test seam; local env control already implies full local compromise.

Obligation check

Closes #85 — all six Required-scope checkboxes map to implemented, tested code; the four out-of-scope items (E3, E5-reuse note, A3, E1) are respected. E2E fixture-manifest boundary disclosed in the PR body.

Micro-lane attestation

MATCHED+evaluated: untrusted-input-boundaries (PRR-001), concurrency-state (PRR-002), ui-accessibility (PRR-003), test-infrastructure (PRR-004), api-schema-migrations (clean — ambient renderer type only, preload+main ship in one artifact), unclassified-risk (clean — full change-path pass). NOT_TRIGGERED with absence evidence: auth-identity-secrets (no authn/z/secret code in diff), subprocess-platform (no subprocess/fs-exec changes outside test-only taskkill, pre-existing pattern), dependencies-build-release (no manifest/lockfile/CI changes in the PR), privacy-observability (no telemetry/log-surface changes beyond local console.error of local paths), generated-provenance (dist/renderer artifacts not committed).

Verdict

APPROVE with findings (no CRITICAL; PRR-001 MEDIUM is being fixed in a follow-up commit on this branch before merge).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

FirstRunGate unmounts on status.needed flipping false after completion, making the "Setup complete"/Finish screen unreachable in production (masked in e2e by the force seam), plus redundant multi-GB manifest hashing.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR implements Workstream E2 (issue #85): a desktop-only first-run validation wizard that composes previously-landed primitives (A3 RAM-gate math, B4 profile selection, B9 model presence, C3 PackManager, E1 manifest) into a guided six-state sequence: detect-hardware → select-profile → verify-manifest → activate-packs → licensing-notices → complete. It adds four Electron-free main-process modules under desktop/main/first-run/, IPC wiring, a preload bridge extension, and a React modal stepper mounted through FirstRunGate. The wizard's defining behavior is failure fidelity: manifest verification names the specific file with expected/actual sha256 (a closed union, never a generic message), the license acknowledgment is unskippable, and drift re-triggers setup on relaunch.

Changes:

  • New RAM gate, sha256 manifest verifier, atomic first-run sidecar store, and pure completion-guard state machine (desktop/main/first-run/*), wired via four desktop:first-run:* IPC channels + a boot push.
  • Renderer wizard (FirstRunWizard.tsx/FirstRunGate), a typed first-run.ts client, a Settings "Re-run setup" card, and an extended DesktopApiBridge type.
  • Electron bootstrap now honors TRAININGAPP_DESKTOP_STORE_PATH for per-test store isolation; CONTEXT_SIZE exported from the engine; new vitest + Playwright specs; docs/CHANGELOG updates.
File summaries
File Description
desktop/main/first-run/ram-gate.ts A3 RAM-gate port with declared-size fallback and free-RAM dev seam
desktop/main/first-run/manifest-verifier.ts Manifest loader + sha256/size verifier with closed-union failures
desktop/main/first-run/first-run-store.ts Atomic sidecar persistence + drift evaluation
desktop/main/first-run/wizard.ts Pure state machine + completion guard
desktop/main/index.ts IPC handlers, engine reuse, store-path seam, boot push
desktop/main/backend/index.ts getFirstRunPackTools() own-property exposure
desktop/main/backend/inference/llama-engine.ts Exports CONTEXT_SIZE for the gate
desktop/preload/index.ts Bridges the four first-run IPC channels + subscription
web_ui/src/components/FirstRunWizard.tsx Modal stepper + FirstRunGate
web_ui/src/lib/first-run.ts Typed bridge client + reopen bus
web_ui/src/types/desktop.d.ts FirstRunStatus/bridge types
web_ui/src/pages/SettingsPage.tsx "First-run setup" / Re-run card
web_ui/src/App.tsx Mounts FirstRunGate
web_ui/src/lib/electron-detect.test.ts, DocumentsPage.electron.test.tsx Type-complete bridge stubs
desktop/src/__tests__/e2-first-run-wizard.test.ts, desktop/e2e/first-run-wizard.spec.ts Unit + e2e coverage
desktop/README.md, CHANGELOG.md Feature documentation
Review details
  • Files reviewed: 18/19 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +107 to +109
function sha256File(absolutePath: string): string {
return createHash('sha256').update(readFileSync(absolutePath)).digest('hex');
}
Comment thread desktop/main/index.ts Outdated
Comment on lines +370 to +386
const wmForGate = wizardManifest();
const gateSizes = {
qualityFileBytes:
qualityBytes?.bytes ??
(wmForGate.manifest !== null
? manifestGroupBytes(wmForGate.manifest, 'llm-quality')
: undefined),
fastFileBytes:
fastBytes?.bytes ??
(wmForGate.manifest !== null ? manifestGroupBytes(wmForGate.manifest, 'llm-fast') : undefined),
};
const recommendation = autoSelectProfile({
freeBytes,
nCtx: CONTEXT_SIZE,
...gateSizes,
});
const wm = wizardManifest();
};
}, []);

if (!open || status === null || !status.needed) return null;
…containment, single verify pass, wizard Esc/focus trap, typed stubs

PRR-001 (MEDIUM): manifest-controlled files[].path / packs[].dir now resolve
through containedJoin — traversal attempts are a named 'traversal' failure
(path + expected/actual) and pack dirs outside the manifest packs/ dir are
refused at activation, never read.
PRR-002 (LOW): buildFirstRunStatus loads+verifies the manifest ONCE per
invocation (was two full sha256 passes).
PRR-003 (LOW): wizard modal gains Esc-to-dismiss (same as Skip for now —
never completes) and a Tab focus trap; covered by FirstRunWizard.test.tsx.
PRR-004 (INFO): inert bridge stubs in two web_ui tests are type-honest
(minimal FirstRunStatus literal) instead of as-unknown-as casts.

Regression tests: traversal rejection + containedJoin unit tests (desktop,
19 passed), FirstRunWizard.test.tsx (2 passed); full first-run e2e 4/4,
desktop compile + web_ui typechecks (app+test) clean.
@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Swarm PR Review — PR #116 (issue #85, first-run validation wizard)

Scope: 98d3cef...2a8c824 (19 files, +2296/-4), tier L. Profile B (Claude Code native subagents), 6 base dimension lanes + 1 risk-family micro-lane (ui-accessibility-i18n; other 10 risk families evaluated NOT_TRIGGERED with evidence: no subprocess/shell calls, no dependency/lockfile/CI changes, no generated/vendored files, no new telemetry in the diff) → 3 independent reviewer passes → 1 critic pass on the highest-severity findings.

Verdict: APPROVE_WITH_NOTES

No unresolved CRITICAL or HIGH findings survive critic review — the two candidates initially rated HIGH (RP-01, ui-a11y-1) were both downgraded to MEDIUM after adversarial re-verification. Several MEDIUM findings are real, confirmed defects worth fixing, but none block merge.


Confirmed findings (reviewer + critic validated)

F-01 (MEDIUM) — Synchronous full-file sha256 hashing on the Electron main process, multiple redundant passes
desktop/main/first-run/manifest-verifier.ts:107-109 (sha256File) does readFileSync + hash synchronously, called from wizardManifest() → reached via buildFirstRunStatus() (called ~4x per boot: twice in status build, once more in the complete handler, plus a renderer-triggered status refetch) and via pushFirstRunRequired() before createMainWindow() on every launch (desktop/main/index.ts:599,627).

  • Critic-verified nuance the original candidates missed: files >2 GiB throw ERR_FS_FILE_TOO_LARGE instantly (verified empirically under Node 24), which is caught and misreported as a manifest-unreadable integrity failure — this would make a packaged install with a model file >2 GiB permanently unable to complete the wizard, a worse failure mode than "slow."
  • Critic-verified mitigation: no resources/manifest.json ships in this repo yet (electron-builder.yml stages only rendererweb_ui; E1/[Workstream E] PR 1 of 6: Package models and default packs into the Electron installer with an integrity manifest and a <=7 GB size budget #84 is unlanded), so today wizardManifest() early-returns with verify:null and hashes nothing in dev/CI/packaged builds alike. This defect is latent until E1 ships a real manifest — re-escalate to HIGH at that point.
  • Fix: stream the hash (createReadStream), offload to a worker thread (precedent exists at desktop/main/backend/retrieval/reranker.ts:145), and memoize one wizardManifest() result per status build instead of re-verifying up to 4x.

F-02 (MEDIUM) — RAM-gate fallback silently defeated for files missing sizeBytes
desktop/main/first-run/manifest-verifier.ts:120-129's manifestGroupBytes() sets seen=true before validating sizeBytes, so a group whose files all omit sizeBytes returns 0 instead of undefined. desktop/main/index.ts:373's ?? doesn't coalesce 0, and ram-gate.ts:57's !== undefined check is true for 0 — so on any machine with >=2 GiB free, this silently selects {profile:'quality'} with no warning, defeating the documented "rather than pretending quality fits" fallback (ram-gate.ts:65-71).

  • This is an execution-proven live defect (the reviewer ran the extracted logic and reproduced it), not just a static read.
  • Root cause is broader than the ordering bug: sizeBytes is documented as required (manifest-verifier.ts:7-8) but never enforced as a sizeBytes-required failure mode, unlike the symmetric sha256-required check.
  • Not exercised by the PR's own test fixtures (both set sizeBytes), and the group key used (llm-quality) doesn't match the existing web_ui/public/models/manifest.json's "group": "llm" — flag this naming mismatch as a follow-up for whoever builds E1's generator.
  • Fix: move seen = true inside the sizeBytes validity guard, and add a sizeBytes-required failure reason mirroring sha256-required.

F-03 (MEDIUM) — Accessible-but-unmanaged modal: aria-modal="true" with no focus trap, no Escape handling, no focus restore
web_ui/src/components/FirstRunWizard.tsx:229 declares role="dialog" aria-modal="true" with zero focus management anywhere in the file (verified by grep — no focus/keydown/Escape/autoFocus/inert handling exists). aria-modal="true" without real isolation is worse than omitting it: assistive tech hides background content while focus can still tab there, creating an unannounceable dead zone (WCAG 2.4.3).

  • Critic-verified mitigations: the wizard is dismissible via "Skip for now" (reachable by Tab, so this is not a keyboard trap), and this repo's own scope doc (.swarm/spec.md:19,206) caps accessibility conformance at WCAG Level A (modal focus-trapping is an APG pattern, not itself a Level A success criterion). No shared modal/dialog wrapper component exists in this codebase to have reused — ModelBlockedOverlay.tsx/DesktopModelBlockedOverlay.tsx are bespoke per-surface focus-trap implementations, so this is a "copy the pattern" fix, not "use the missing component" fix.
  • Related, lower-severity accessibility gaps in the same component (all confirmed, LOW-MEDIUM): pack-activation success/failure conveyed by color only with no role="alert"/live region (inconsistent with the manifest-failure section's own pattern in the same file); "Complete setup" button's disabled state has no programmatic reason; license <pre> block has no tabIndex, unreachable via keyboard; step-indicator chips use color-only state with raw machine IDs (detect-hardware, licensing-notices) as user-facing labels and no aria-current; hash columns in the failure table have no overflow-x handling at narrow widths; formatBytes hardcodes non-localized formatting (informational — no i18n infrastructure exists in this codebase yet).

F-04 (MEDIUM) — firstRun.selectedProfile is persisted but never consumed by the runtime model-loading path
Confirmed by repo-wide grep: selectedProfile flows into the store, the completion writer, status IPC, and UI display, but llama-engine.ts's actual profile selection (this.profileSettingoptions.profile / inference.profile settings) never reads it. The wizard's "operator override always wins" UI claim may currently be cosmetic — the chosen profile from first-run setup isn't bridged into the setting the inference engine actually consults.

F-05 (LOW, critic-downgraded from MEDIUM) — Manifest-file deletion (as opposed to a covered file) after completion silently defeats drift re-detection
first-run-store.ts:128 skips its entire drift-comparison loop when manifestDigests is null (which happens when resources/manifest.json itself is absent), so the wizard's own "packaged install without manifest = BROKEN INSTALL" rule (enforced only inside the desktop:first-run:complete handler) never re-fires at launch.

  • Critic downgraded this: an unprivileged edit to the first-run.json sidecar (e.g. {"firstRun":{"completed":true,"manifestDigests":{}}}) already defeats the same drift mechanism with zero privilege and no elevation needed — this is a strictly-harder variant of an existing, already-out-of-scope bypass of what is a UX re-verification gate, not a security boundary.

F-06 (MEDIUM) — Existing/upgraded installs are gated into the wizard with no docs disclosure
Any pre-existing profile directory without a first-run.json is treated identically to a brand-new install and routed through the wizard on next launch; "Skip for now" only dismisses (non-blocking) but recurs on every subsequent launch until completed. CHANGELOG.md/README frame this as "a fresh install" without mentioning upgraded installs are equally gated.

Lower-severity confirmed findings (LOW, not critic-escalated):

  • isRerun() exported as "the pinned helper" but never imported/called anywhere; desktop/main/index.ts:438 duplicates the identical boolean inline — dead code with divergence risk, zero test coverage (wizard.ts:78).
  • Hash verification runs before the (cheap) size check, so truncated files pay the full hash cost and are reported hash-mismatch instead of size-mismatch, making the latter reason effectively unreachable (manifest-verifier.ts:161-183).
  • TRAININGAPP_DESKTOP_MANIFEST env override has no isPackaged guard — but neither does the sibling TRAININGAPP_FIRST_RUN_FORCE seam, so this is a consistent (if intentionally permissive) pattern across all dev seams, not a novel inconsistency (manifest-verifier.ts:98-99).
  • Pack activation loop has no timeout/cancellation around packTools.install() — a stuck local SQLite-backed install could hang the wizard indefinitely (desktop/main/index.ts:500-527).
  • No idempotency guard on activate-packs/complete IPC handlers — a double-fired renderer action can run concurrent overlapping flows (bounded damage: atomic-rename sidecar write prevents corruption, worst case is duplicate installs) (desktop/main/index.ts:483-592).
  • packs[].dir from resources/manifest.json is joined into filesystem paths with no ..-traversal guard before being passed to packTools.install() — low severity since manifest.json is only ever sourced locally (packaged resources root, dev tree, or an explicit env override), never remotely (manifest-verifier.ts:136-189, desktop/main/index.ts:349-350,500-501).
  • desktop:first-run:reset IPC has no confirmation/rate-limit — any renderer script can silently wipe completion state (availability/nuisance impact only) (desktop/main/index.ts:593-597).
  • manifestGroupBytes() has zero direct unit test despite being the exact code path where F-02 was found — only indirectly exercised by one coarse e2e path (manifest-verifier.ts:117-130).
  • No in-repo JSON Schema/contract fixture for the issue [Workstream E] PR 1 of 6: Package models and default packs into the Electron installer with an integrity manifest and a <=7 GB size budget #84 manifest format the tests hand-construct against — currently unfalsifiable, will need reconciling once E1 lands (desktop/e2e/first-run-wizard.spec.ts, desktop/src/__tests__/e2-first-run-wizard.test.ts).
  • TRAININGAPP_DESKTOP_ENGINE=stub independently suppresses the first-run gate outside the TRAININGAPP_FIRST_RUN_FORCE seam — a main-process-only env read, consistent with other dev/CI seams in this PR, not renderer-reachable (desktop/main/index.ts:427,434; llama-engine.ts:244).

Disproved / not-reachable-as-claimed

None of the 30 base-lane + micro-lane candidates were disproved outright; all held up under independent re-reading, though several were downgraded in severity (detailed above) once critic-verified mitigating context (dismissibility, scope docs, missing-manifest latency, equally-easy alternate bypasses, Node's readFileSync size-cap throwing instead of OOM-ing) was accounted for.

Test/coverage gaps worth closing

  • manifestGroupBytes() — add a unit test asserting undefined (not 0) for a group whose files all lack sizeBytes, per F-02.
  • isRerun() — either wire it in as the actual source of truth or delete it; it currently has zero coverage and zero callers.
  • Add a sizeBytes-required failure-mode test mirroring the existing sha256-required test.

Obligation mapping (issue #85 AC1-AC7)

All seven acceptance criteria have corresponding passing tests per the PR's own evidence. One caveat: AC2's "low RAM auto-selects Fast + warning + override" primary path is correctly tested and works — but its unknown-size fallback path (F-02) breaks silently, and the "operator override always wins" claim (F-04) may not be functionally wired to the runtime model loader beyond being recorded and displayed.

Recommended actions before/after merge

  1. Fix F-02 (manifestGroupBytes seen-before-validation bug) — small, well-scoped, execution-proven.
  2. Track F-01 (sync full-file hashing + the >2GiB throw-and-misreport path) as a must-fix before E1/[Workstream E] PR 1 of 6: Package models and default packs into the Electron installer with an integrity manifest and a <=7 GB size budget #84 ships a real manifest — not blocking today since no manifest ships yet, but will silently become a real regression the moment it does.
  3. Consider F-03's minimal fix (a focus trap + Escape handler on FirstRunWizard, matching the existing ModelBlockedOverlay.tsx pattern already in this codebase).
  4. Verify/wire F-04 (selectedProfile → actual runtime model-loading) or explicitly document that the override is advisory-only if that's intended.
  5. Non-blocking: dead-code cleanup (isRerun), docs wording for upgrade-path gating (F-06), IPC hardening (timeout/idempotency on pack activation).

Review methodology: 6 parallel base-dimension explorer lanes + 1 targeted risk-family micro-lane (accessibility) → 3 independent reviewer passes (fresh context, re-read all cited evidence) → 1 adversarial critic pass on the 4 highest-severity findings. 30 total candidates generated, 30 reviewer-validated (0 disproved, several severity-adjusted), 4 critic-challenged (2 downgraded HIGH→MEDIUM, 1 upheld MEDIUM, 1 downgraded MEDIUM→LOW).

🤖 Generated with Claude Code

@zaxbysauce

Copy link
Copy Markdown
Collaborator Author

Post-fix re-verification — commit 5771e4c (PRR-001..004)

Re-checked out the PR at its new head (5771e4c5, up from 2a8c8246 reviewed above) and independently re-verified each claimed fix per Phase 11 (targeted re-verification, not a full re-review).

[REVERIFIED] | sec-01 (PRR-001) | FIXED | containedJoin() added in desktop/main/first-run/manifest-verifier.ts:60-73 resolves both files[].path (verifyManifest, line ~168) and packs[].dir (desktop/main/index.ts:350-356 packEntryDir) through a base-containment check (case-insensitive prefix match on Windows), returning null on escape. A traversal attempt on files[].path now surfaces as a distinct 'traversal' failure reason (path/expected/actual all populated) instead of silently falling through to existsSync on an arbitrary resolved path; a traversal packs[].dir is refused before install() is ever called (index.ts:521-528, dir === null branch), matching the original finding's suggested fix exactly. New unit tests for both paths pass (desktop && npx vitest run src/__tests__/e2-first-run-wizard.test.ts → 19/19 green, includes traversal-rejection cases). No new issue introduced.

[REVERIFIED] | F-01 / RP-02 redundant-pass half (PRR-002) | FIXED | buildFirstRunStatus() in desktop/main/index.ts now calls wizardManifest() exactly once (const wm = wizardManifest() at line ~386) and reuses wm for both RAM-gate sizing and failure reporting, removing the prior wmForGate/wm duplicate-call pattern. This addresses the "2x per status build" half of F-01's redundant-verification finding. Not addressed (correctly out of scope for this fix, unclaimed): the underlying synchronous full-file readFileSync+hash in sha256File (manifest-verifier.ts:107-109) is untouched — still a real concern once a manifest actually ships (E1/#84), and desktop:first-run:complete's extra wizardManifest() call at line ~576 is also untouched, so ~2 full passes per boot remain instead of the prior ~4. Net improvement, not a full fix of F-01.

[REVERIFIED] | F-03 / ui-a11y-1 (PRR-003) | FIXED | web_ui/src/components/FirstRunWizard.tsx now attaches a keydown listener (new useEffect, lines ~187-217) that: (a) treats Escape as equivalent to "Skip for now" (calls onClose(), never completes — consistent with the file's own documented Skip semantics); (b) implements a real Tab focus trap scoped to panelRef (wrap-around in both directions, preventDefault at the boundaries); (c) moves initial focus into the panel on mount (panelRef.current?.focus()) with the panel now tabIndex={-1} and a ref. This directly resolves the confirmed defect (no focus management existed at all previously). New test file web_ui/src/components/FirstRunWizard.test.tsx (2 tests, added in this commit) covers it; could not execute web_ui vitest in this environment (node_modules not installed in this worktree — a local sandbox limitation, not a PR defect), so this is verified structurally (direct code read) rather than execution-proven here, but the desktop-side sibling suite (19/19) and the diff's own shape give high confidence. The narrower a11y sub-findings from the original review (color-only state on pack-activation results, disabled-button reason text, license <pre> keyboard-reachability, step-chip aria-current) are not addressed by this commit and remain open as LOW/MEDIUM items from the original review.

[REVERIFIED] | CD/TF typed-stub note (PRR-004) | FIXED | web_ui/src/lib/electron-detect.test.ts and web_ui/src/pages/DocumentsPage.electron.test.tsx replace the prior as unknown as DesktopApiBridge['getFirstRunStatus'] casts with a minimal, fully-typed FirstRunStatus literal (stubFirstRunStatus()), and web_ui/src/types/desktop.d.ts's FirstRunManifestFailure.reason union gains 'traversal' to stay in sync with the new manifest-verifier failure reason. Informational-severity item, genuinely improved (type-honest instead of casted).

Findings from the original review that remain open (not claimed fixed, correctly untouched)

  • F-02 (MEDIUM, execution-proven)manifestGroupBytes()'s seen = true is still set before the sizeBytes validity check (manifest-verifier.ts:136-146, re-verified against the new head), still returns 0 instead of undefined for an all-missing-sizeBytes group, still silently defeats the RAM-gate fallback. Still open — this is the highest-value remaining fix.
  • F-04 (MEDIUM)firstRun.selectedProfile persisted-but-unbridged to the runtime model loader — untouched by this commit.
  • F-05 (LOW) — manifest-file deletion (vs. a covered file) silently defeating drift detection — untouched.
  • F-06 (MEDIUM) — upgrade-path gating not disclosed in docs — untouched.
  • All LOW-severity items from the original review (dead isRerun(), hash-before-size ordering, pack-activation timeout/idempotency, desktop:first-run:reset no confirmation, remaining a11y sub-findings, missing manifest schema fixture, TRAININGAPP_DESKTOP_ENGINE=stub gate bypass) — untouched, as expected (not claimed).

Updated verdict: APPROVE_WITH_NOTES (unchanged)

The fixes in this commit are real, correctly scoped, and don't introduce new regressions. No CRITICAL/HIGH findings existed before or after. F-02 remains the single most valuable outstanding fix (it's a one-line change with an already-execution-proven repro), followed by tracking F-01's remaining half against the E1/#84 manifest landing.

🤖 Generated with Claude Code

@zaxbysauce
zaxbysauce merged commit fcfe945 into master Sep 17, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Workstream E] PR 2 of 6: First-run validation wizard (RAM to profile selection, model presence, pack activation) and licensing notices

2 participants