feat(security): incognito coherence (Wave 2) #2
Reference in New Issue
Block a user
Delete Branch "feat/security-wave2-incognito"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes the cluster of audit findings where gnoma's incognito promise — "no persistence, no learning, local-only routing" — silently broke because state was duplicated across the CLI flag, the firewall's IncognitoMode, the router's localOnly flag, and the TUI's local m.incognito field. After this PR, security.IncognitoMode is the canonical source of truth and the three downstream flags follow.
Plan:
docs/superpowers/plans/2026-05-19-security-wave2-incognito.mdBuilds on Wave 1 (#1, merged) which closed the firewall-bypass call sites at the provider boundary.
What's fixed
Also wires `rtr.SetLocalOnly(true)` on `--incognito` launch — `main` previously activated the firewall flag but never told the router to filter, so even without the forced-arm bug, launching with `--incognito` alone gave you "incognito badge but full arm pool".
Notable design decisions
Test plan
Out of scope
Closes the cluster of audit findings where gnoma's incognito promise ('no persistence, no learning, local-only routing') silently broke because state was duplicated across the CLI flag, the firewall's IncognitoMode, the router's localOnly flag, and the TUI's local m.incognito field. Wave 2 makes security.IncognitoMode the canonical source of truth. W2-1 Router.Select rejects forced non-local arms when localOnly is on rather than short-circuiting and silently routing to cloud. Main fails fast when --incognito + --provider <cloud> are combined; the TUI toggle (Ctrl+X, /incognito, config panel) refuses with an actionable message when a non-local arm is pinned. Factored the three duplicated toggle sites into Model.attemptIncognitoToggle. W2-2 persist.Store.Save consults an IncognitoGate (local interface, *security.IncognitoMode satisfies it). nil gate = always persist (legacy behaviour for tests); non-nil gate is consulted on every Save so TUI runtime toggles take effect without reconstructing the store. File mode 0o600, dir mode 0o700. W2-3 tui.New seeds m.incognito from cfg.Firewall.Incognito().Active(). Fixes the Ctrl+X-on-launch-with-incognito case where the first toggle silently turned the firewall OFF because the local flag started false out of sync with the firewall. W2-4 saveQuality gates on both *incognito (defensive, covers the window before fwRef.Set fires) and fw.Incognito().ShouldLearn() (so TUI Ctrl+X suppresses the snapshot on exit). Quality restore skipped under --incognito. Quality file written 0o600 in dir 0o700. engine.reportOutcome and elf.Manager.ReportResult both gate on fw.Incognito().ShouldLearn() — bandit signal no longer leaks out of incognito sessions. W2-5 session files written 0o600 in dirs 0o700 (was 0o644 / 0o755). W2-6 IncognitoMode.LocalOnly dropped — dead field with no readers; routing local-only state lives on the router, not the firewall. Also wires rtr.SetLocalOnly(true) when --incognito at launch — main previously activated the firewall's flag but never told the router to filter, so even without the forced-arm bug, launching with --incognito alone gave you 'incognito badge but full arm pool'.