feat: macOS menubar GUI (Phase 8) + config-management UX, docs restructure, sudo auto-elevation#15
Merged
Merged
Conversation
Add a macOS-only menubar app (Dezhban.app) for at-a-glance status and click-to-control, plus the daemon-side state export it reads. Daemon (Go, cross-platform, stdlib-only): - internal/state: Snapshot type + atomic (temp+rename), world-readable 0644 writer/reader for /var/db/dezhban/state.json - internal/runner: injected Publish callback (nil = no-op); publishes a full snapshot on each poll, transition, tunnel edge, endpoint refresh, and at startup, retaining the last-known reading. Best-effort — never affects enforcement - cmd/dezhban: defaultStatePath(), wire Publish into the run path only, add `status --json` App (Swift, macos-gui/, AppKit + ServiceManagement only): - NSStatusItem icon (green allow/guard, red block, gray stopped/stale), 1s state-file poll, snapshot-driven menu - Start/Stop, Block/Unblock via osascript admin prompt; Launch-at-login via SMAppService; VPN-mode indicator + Open config (live toggle deferred) - SwiftPM + build-app.sh -> dist/Dezhban.app; `make gui-macos` Docs: docs/STATE.md (state-file contract), phase-8 plan, README/CLAUDE.md. Verified: go build/vet clean, 76 tests pass, gofmt clean, Swift builds and bundles, `status --json` emits valid JSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- state: populate Tunnel.Name in published snapshots. Add Name to netdetect.TunnelState (set from the identified up interface) and a tunnelSnapshot helper in the runner that maps it through, falling back to the configured tunnel name(s) on a down/unknown edge. The JSON `name` field the contract and docs/STATE.md advertise is now meaningful instead of always "". - runner: decouple best-effort publishing from the enforcement loop. Run now hands snapshots to a background writer over a buffered (64) FIFO channel, flushed on shutdown, so a stalled state-file write can't delay the next tunnel/geo event. FIFO (not coalescing) preserves the every-transition-published contract the runner test asserts. - macos-gui: remove dead DezhbanCLI.statusJSON()/run() (the app is entirely state-file driven and never called them; the comment claiming "used on menu open" was false). Correct the matching statusJSON comment in cmd/dezhban to describe it as a tooling contract, not an app fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
liveSample emits `"<iface> up"` (e.g. "utun4 up"), never the bare "UP" the example showed. Aligns the state-file contract doc with actual daemon output; the `name` field it advertises is now genuinely populated too (this PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… mode Repositioning (docs/positioning only — no code, config-default, or CLI/JSON contract changes): - Present the always-on VPN interface guard as the primary/recommended mode and the country-blocklist as a best-effort fallback, matching the design intent already in docs/plans. Explain the retained vpn.enabled=false default as a safety opt-in, and why no poll interval can guarantee zero leak in the fallback. - "primary"/"fallback" are prose only; the guard/fullblock/legacy identifiers and state-file JSON keys are unchanged. Doc split: - Move detail out of the two oversized top-level files into focused docs/ files: modes.md, architecture.md, usage.md, development.md, safety.md, plus a docs index (readme.md). README/CLAUDE keep only essentials + non-negotiables and link. - Remove the generic jcodemunch/jdocmunch/serena tooling policy from CLAUDE.md (inherited from the user's global config). - Rename docs to lower kebab-case: CONFIG/STATE/TROUBLESHOOTING/plans/README -> config/state/troubleshooting/plans/readme; repoint every reference and fix a pre-existing broken link in plans/testing-macos-block.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion, path discovery Make the config file-free and --config optional. - Config path discovery: `resolveConfigPath` (flag > $DEZHBAN_CONFIG > system path if present > built-in defaults), routed through loadConfig so every command gains it; `validate` now reports the resolved path. - Config writer: internal/config gains Marshal + Save (Config → fileConfig, durations round-trip as "30s", 0644, vpn block emitted only when enabled), with Save↔Load round-trip tests. - `dezhban config` — path/show/get/set/edit over a small dotted-key registry; writes to /etc print a sudo hint on EACCES. - `dezhban setup` — interactive huh wizard: reuses netdetect tunnel detection (MultiSelect), endpoint auto-discover, the lockout-routing check, and a print-rules preview before saving; guarded against non-TTY. - `dezhban completion bash|zsh|fish` — hand-written scripts (no dep). Dependencies: add charmbracelet/huh (second direct dep, setup wizard only); CLAUDE.md's dependency convention amended to record it. docs/usage.md, docs/config.md, and the README updated for the new commands and discovery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Privileged commands no longer require the user to prepend sudo by hand. - requireRoot now re-execs the whole invocation under sudo when not root (syscall.Exec, so signals/exit codes pass through), preserving $DEZHBAN_CONFIG. - setup and config set/edit elevate only their config write (or the editor) so an interactive wizard never restarts. - Gated to unix + an interactive TTY; opt out with --no-sudo or DEZHBAN_NO_SUDO=1. Windows keeps the clear "must run as root" error (UAC is a separate mechanism). - Terminal detection uses charmbracelet/x/term.IsTerminal (already in the tree), not ModeCharDevice — /dev/null is a char device but not a TTY, so the old check would have wrongly tried to prompt for a sudo password in CI/pipes. Docs and --help updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…" page) Commit the self-contained HTML reference (VPN guard primary vs country-blocklist fallback: state machines, rulesets, decision guide) as a browser-openable page, wrapped in a proper <!doctype html> document. modes.md links to it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correctness: - runner: non-blocking drop-oldest state publish + bounded shutdown flush so a hung state-file write can never stall enforcement or block Cleanup teardown. - runner/state: surface firewall-action failures via a new `enforcementErr` snapshot field (incl. the VPN probe re-cut), so a failed block no longer publishes a healthy-looking "allow"; GUI shows a warning icon + menu line. - config: `config` subcommands now honor `--config` (was silently ignored). - elevate: pathWritable probes the nearest existing ancestor dir, so a write into a missing user dir no longer needlessly escalates to sudo and creates root-owned dirs in $HOME. macOS GUI: - set menu.autoenablesItems=false so disabled-item gating actually holds. - run privileged actions off the main thread and surface failures via NSAlert. - derive staleness from the daemon's pollIntervalSeconds (max(90s, 3x interval)). Minor: - reconcile dependency docs (three third-party modules) in CLAUDE.md + architecture.md. - fish completion builds its subcommand list from the shared const. - state.Write fsyncs before rename. Adds runner tests (enforcement-err on block failure; publish stall doesn't block enforcement + Cleanup still runs) and config --config tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TestWriteIsWorldReadable asserts the state file mode is exactly 0644, which is a POSIX concept. Windows has no Unix mode bits — os.Chmod only honors the write bit and os.Stat reports 0666/0444 — so the check fails on windows-latest (got 666 want 0644) while passing on macOS/Linux. The 0644 world-readable contract only matters for the root-daemon / unprivileged-reader split on Unix; Windows governs access via ACLs. Skip the assertion there. Write() behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 1 — macOS menubar GUI + daemon state export (Phase 8)
Adds a macOS-only menubar app (
Dezhban.app) for at-a-glance status andclick-to-control of the kill switch, plus the daemon-side state export it
reads. Implements
docs/plans/phase-8-macos-gui.md.Daemon: live state file (Go, cross-platform, stdlib-only)
internal/state—Snapshottype + atomic writer (temp-file + rename,0644world-readable) and reader. Path:/var/db/dezhban/state.json(unix) /%ProgramData%\dezhban\state.json(Windows).internal/runner— injectedPublish func(state.Snapshot)callback(nil = no-op). Publishes on every poll, verdict transition, tunnel edge,
endpoint refresh, and at startup. Best-effort: a write failure never affects
enforcement.
cmd/dezhban—defaultStatePath(), wiresPublishinto the realrunpath only, adds
status --jsonmerging snapshot + service/config status.Swift menubar app (
macos-gui/, AppKit + ServiceManagement only)NSStatusItemSF-Symbol icon (🟢 allow/guard, 🔴 block/full-block, ⚪stopped/stale-after-90s), 1s state-file poll, snapshot-driven menu.
osascriptadmin prompt; Launch atlogin via
SMAppService; VPN-mode indicator + Open config.build-app.sh→dist/Dezhban.app;make gui-macos.Deviations from plan: SwiftPM + bundling script instead of an Xcode project
(build box has only Command Line Tools); VPN-mode live toggle deferred (a blind
vpn.enabledflip failsconfig.Validate()) — menu routes to Open config….Part 2 — Docs split + VPN-primary repositioning (
26c9ca2)VPN interface guard is now framed as the primary/recommended mode and
country-blocklisting as the fallback.
vpn.enabledstill defaults tofalseas a documented safety opt-in.
README.md(346 → ~80 lines) andCLAUDE.md(205 → ~75)into focused docs under
docs/(modes,architecture,usage,development,safety, plus an index). Renamed existing docs to lowerkebab-case; repointed every reference; removed the generic MCP tooling policy
that duplicated the user's global config.
Part 3 — Config-management UX (
a8c8be3)--configis now optional — resolved from the flag →$DEZHBAN_CONFIG→system path (
/etc/dezhban/dezhban.json) → built-in defaults.dezhban config pathprints the winner.internal/configgains a writer (Marshal/Save; durations round-trip as"30s"; round-trip tested).dezhban setup(interactivecharmbracelet/huhwizard —reuses tunnel detection, the endpoint lockout check, and a
print-rulespreview before saving; guarded against non-TTY);
dezhban config path/show/get/set/edit;dezhban completion bash|zsh|fish.charmbracelet/huhas a second direct dependency (setupwizard only; daemon/enforcement path stays stdlib).
CLAUDE.md's dependencyconvention updated to record it.
Part 4 — sudo auto-elevation (
1d9deaa)sudowhen not root (unix +interactive TTY), preserving
$DEZHBAN_CONFIG.setupandconfig set/editelevate just their config write. Opt out with
--no-sudoorDEZHBAN_NO_SUDO=1. Windows and non-TTY contexts keep the clear "must run asroot" error (terminal detected via
IsTerminal, notModeCharDevice).Verification
go build/go vetclean;go test ./...→ 80 pass (addsinternal/configSave↔Load round-trip + validation tests); host and Windows cross-builds;
make build-allcross-compiles all 5 targets (withhuh);gofmtclean;pre-commit hooks green.
$DEZHBAN_CONFIG+ system path),config set/get/show/path,completion, non-TTYsetupguard.unblock;--no-sudofalls back to the plain root error.state-file → icon flip, admin-prompt actions, login-item registration. Steps in
the phase doc.
To try locally:
make gui-macos && open dist/Dezhban.app, thensudo ./dezhban run --config configs/dezhban.dev.json --simulate-country IRtowatch the icon flip.
🤖 Generated with Claude Code