34f6f1c786
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'.
61 lines
1.4 KiB
Go
61 lines
1.4 KiB
Go
package security
|
|
|
|
import "sync"
|
|
|
|
// IncognitoMode controls privacy-sensitive behavior.
|
|
// When active: no persistence, no learning, no content logging.
|
|
//
|
|
// Routing constraint (local-only) is enforced by the router, not here —
|
|
// see router.SetLocalOnly. The two states must agree, but they live on
|
|
// different types because the router owns enforcement (mutex around arm
|
|
// selection) and the firewall owns intent. TUI/CLI bootstrap is
|
|
// responsible for keeping them in sync.
|
|
type IncognitoMode struct {
|
|
mu sync.RWMutex
|
|
active bool
|
|
}
|
|
|
|
func NewIncognitoMode() *IncognitoMode {
|
|
return &IncognitoMode{}
|
|
}
|
|
|
|
func (m *IncognitoMode) Activate() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.active = true
|
|
}
|
|
|
|
func (m *IncognitoMode) Deactivate() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.active = false
|
|
}
|
|
|
|
func (m *IncognitoMode) Toggle() bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.active = !m.active
|
|
return m.active
|
|
}
|
|
|
|
func (m *IncognitoMode) Active() bool {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.active
|
|
}
|
|
|
|
// ShouldPersist returns false when incognito is active.
|
|
func (m *IncognitoMode) ShouldPersist() bool {
|
|
return !m.Active()
|
|
}
|
|
|
|
// ShouldLearn returns false when incognito is active (no router feedback).
|
|
func (m *IncognitoMode) ShouldLearn() bool {
|
|
return !m.Active()
|
|
}
|
|
|
|
// ShouldLogContent returns false when incognito is active.
|
|
func (m *IncognitoMode) ShouldLogContent() bool {
|
|
return !m.Active()
|
|
}
|