← index2026-05-09 14:56 (Beirut)(backfill from DOCUMENTATION/)

Mac Brian Control Center — v4 (HeyBrian-Consolidated)

Mac Brian Control Center — v4 (HeyBrian-Consolidated)

Pivot 2026-05-09 (afternoon, Jonah): the user installs one app — HeyBrian — and gets everything. No "parts and pieces." HeyBrian app on Mac is the host of every Brian-on-Mac function. mac.jonahtebaa.com is the same product viewed remotely. Hammerspoon-as-orchestrator retires. App closed → system down. App running → everything works.

This supersedes v3 (01_control_center_architecture.md) at the hosting layer. The v3 wire protocol (HMAC POSTs, JSON envelopes, capability registry) survives — only the process that hosts it changes.


Codex review delta (2026-05-09 afternoon — integrated)

Codex returned 10 critiques in 20s. Adopted in full:

  1. No all-at-once flip. HeyBrian binds 9100 with full v2 parity. Hetzner gets a feature-flag for target port. Run dual (Lua and Swift answering side-by-side), compare envelopes + audit logs over real traffic, only THEN unload Hammerspoon and rebind 8765. No "back to 8765" flip-flop — make Hetzner endpoint configurable so the migration is one-way.
  2. Native overlays not phase-1 critical. Defer them. But do not add a Hammerspoon helper protocol — it would create a "zombie dependency" where app-closed doesn't actually kill the visual system. Keep HS painter as-is for phase 1 (with the caveat: until native overlays ship, app-closed is partially leaky on the visual side).
  3. WKWebView token: NOT URL params, NOT fragment. Both leak to page JS. Use WKURLSchemeHandler to load bundled HTML + inject a short-lived in-memory token via WKUserScript / message handler. Bind to 127.0.0.1 only. Set no-cache headers. Require token even on cpanel state read.
  4. Phone latency: accept proxy hop. Don't try Tailscale-direct from PWA — DNS, identity, enrollment all get hard. Settings polling is fine via Caddy → Hetzner → Tailscale → HeyBrian. For realtime later: SSE/WebSocket via Hetzner.
  5. Crash policy: BOTH. User-quit → stays down. Crash → launchd KeepAlive restarts. Distinguish by sentinel file (~/.brian/heybrian_quit_intent). Plus Hetzner watchdog showing "Mac offline / HeyBrian down" on extended outage — no SIP, just LOGS.
  6. Secrets to Keychain, not dotfiles. ~/.brian_mac_secret and ~/.brian/cpanel_token migrate to macOS Keychain. capabilities.json moves to Application Support (canonical). Keep one-version compat reads from ~/.brian* for migration window.
  7. Contract tests for executor migration. Replay identical JSON against Lua and Swift; compare status / result / error / audit fields. Document intentional divergences. Test all TCC failure modes explicitly: Accessibility denied, Screen Recording denied, Automation denied, Full Disk Access denied — Swift fails differently than Hammerspoon.
  8. Visual model stays distinct. Orb = app presence / listening. Pill + status = action state. Border = active remote control. Codex: do NOT merge them or you get ambiguous state. (Saved me from a wrong instinct.)
  9. Under-considered list — track and address before phase-1 lands:
    - localhost CSRF from malicious web pages
    - Tailscale ACL scope (who on the tailnet can hit :8765)
    - HMAC nonce persistence across restart
    - Large file read/write limits (currently unbounded — set 10 MB cap)
    - Path sandboxing for file.read / file.write (whitelist prefixes)
    - AppleScript injection (parameterize, never interpolate user strings)
    - Audit log tamper resistance (append-only fs flag? hash chain?)
    - Permission onboarding (TCC consents must be requested gracefully)
  10. Simpler design (codex's recommended path): ship phase 1 as HeyBrian-hosted HTTP control plane + audit/security parity ONLY. Defer visual rewrite and remote PWA polish until executor parity is proven.


The product, from the user's eyes

"I installed HeyBrian on my Mac. It put a Brian icon in my menubar. I can talk to it ('Hey Brian'). I can open Settings to see and toggle every capability. I can open mac.jonahtebaa.com on my phone and see the same thing. When I quit the app, Brian stops doing anything on my Mac."

That's the whole story. Border, orb, voice, settings, audit, capabilities — one app, one menubar icon, one settings panel, one remote view.


Three surfaces, one product

Surface What lives there Talks to
HeyBrian Mac app (native Swift) wake-word, orb, voice, screen pixels, Step A/B/C, breathing border, pill, status box, HTTP server :8765 (the only chokepoint), capability registry, executors, audit log, settings webview Hetzner agent-api (out), itself (everything in)
HeyBrian Settings (inside the app) cpanel UI (WKWebView serving the HTML I built, OR native SwiftUI port) localhost loopback only
mac.jonahtebaa.com (PWA) same cpanel UI, phone-first re-skin; chat with Brian; remote action send Hetzner agent-api → HeyBrian over Tailscale

Single source of truth: HeyBrian's own state. Everything else reads/writes through its :8765 HTTP API.


Process model

This is the "app closed = system down" property Jonah asked for.


What retires

Result of v4 phase 1: Hammerspoon still runs (because of brian_panel/brian_prompt), but it no longer hosts the control center. Eventually we can pull the rest into HeyBrian and uninstall Hammerspoon entirely.


What gets ported into HeyBrian

From the existing v3 Lua module (~1300 LOC), these jobs move to Swift:

  1. HTTP routes (extend HTTPServer.swift):
    - POST /v2 — session lifecycle (HMAC verified)
    - POST /v2/action — action dispatch (HMAC verified)
    - GET /v2/caps — registry dump (no auth)
    - GET /cpanel — serves the HTML (already written) from app bundle
    - GET /cpanel/api/state — token-authed JSON
    - POST /cpanel/api/{toggle,source-allow,stop-session} — token-authed mutations
    - existing: /notify, /activate, /status, /ask_screen, /start_loop, /cancel_loop, /health — keep
    - port 9100 retires; everything moves to 8765

  2. HMAC + nonce + replay protection — port verbatim from Lua.

  3. Capability registry — JSON read/write with atomic temp+rename. Same schema.

  4. 12 executors — reimplement in Swift:
    - runProcess with /bin/sh -c
    - click → shell out to /opt/homebrew/bin/cliclick OR CGEvent directly
    - screencap → shell out to /usr/sbin/screencapture (existing ScreenCapture.swift already does this)
    - typeCGEvent keystrokes
    - keystrokeCGEvent.keyboardEvent
    - chrome.eval / chrome.urlNSAppleScript
    - file.read / file.writeFileManager + Data
    - applescriptNSAppleScript
    - notifyUNUserNotificationCenter (already used)
    - sayProcess shell out to /usr/bin/say, gated by ~/.brian/no_speech flag

  5. Audit log + size rotation + in-memory ring — port verbatim semantics.

  6. Cpanel — HTML stays as-is. Settings menu in HeyBrian opens a WKWebView pointing at http://127.0.0.1:8765/cpanel with token pre-injected. Same HTML works on the phone via mac.jonahtebaa.com.

  7. Breathing border + pill + status box:
    - Border: full-screen NSWindow with custom CALayer for the 4-layer cyan stroke + breathing animation
    - Pill: NSPanel floating in top-right, click → NSMenu chooser (Pause/Resume/Stop)
    - Status box: NSPanel at bottom-center, translucent NSVisualEffectView, two-line text
    - Glass / Submarine sounds: NSSound(named:)

  8. Heartbeat watchdog — Swift Timer runs every 2s; if last_heartbeat older than 30s, dismount + post watchdog_kill callback.


mac.jonahtebaa.com — same cpanel, remote view (must-ship in Phase 1)

Jonah's requirement (clarified 2026-05-09): the control center accessed inside the HeyBrian app must ALSO be accessible at mac.jonahtebaa.com. No separate phone UI. The cpanel is the truth; the phone is just a different window onto it.

Implementation: one HTML file, two delivery paths, same state.

                   ┌────────────────────────────────┐
                   │   cpanel HTML (single source)  │
                   │   detects origin, picks auth   │
                   └────────────────────────────────┘
                            │              │
              localhost      │              │      mac.jonahtebaa.com
              (Mac browser   │              │      (phone, other Mac, anywhere)
               OR HeyBrian   │              │
               WKWebView)    ▼              ▼
                   ┌─────────────────┐  ┌─────────────────────┐
                   │ HeyBrian app    │  │ Caddy gate_auth     │
                   │ :8765/cpanel    │  │  ↓                  │
                   │ token auth      │  │ Hetzner             │
                   │                 │  │ mac_control_routes  │
                   │                 │  │  ↓ (HMAC over TS)   │
                   │                 │  │ HeyBrian            │
                   │                 │  │ :8765/v2/action     │
                   └─────────────────┘  └─────────────────────┘
                                                 │
                                          (proxy adds <100ms)

The HTML reads window.location.hostname:
- 127.0.0.1 / localhost → call /cpanel/api/* directly with X-Brian-Cpanel-Token
- mac.jonahtebaa.com → call /api/mac/cpanel/* (Caddy-gate-protected); Hetzner-side handler signs HMAC and forwards to HeyBrian

One HTML to maintain. One state. No drift between the in-app Settings and the remote PWA.


Auth layers

Channel Auth
Hetzner → HeyBrian /v2, /v2/action HMAC over body, shared secret in ~/.brian_mac_secret
Mac local browser → HeyBrian /cpanel Token in ~/.brian/cpanel_token (mode 0600)
Phone → mac.jonahtebaa.com → Hetzner Caddy gate (existing gate_auth)
Hetzner → HeyBrian (proxying for phone) HMAC (same as Hetzner direct)

When HeyBrian app is the only running process, it's also the only place where the secrets live and the only place where actions can fire. Stop the app → no path to anything.


Codex review questions (next step)

  1. Port collision migration. HeyBrian moves to :8765. Hammerspoon's lua needs to release it first. Cleanest sequence?
  2. Native overlays vs WKWebView for border/pill/status. Hammerspoon canvas has been cheap and reliable. Native NSWindow is cleaner but more code. Worth it?
  3. WKWebView for Settings. Embedded webview pointing at localhost:8765/cpanel. Auth flow: token query param? Auto-injected from app? Concerns?
  4. mac.jonahtebaa.com proxy. Adds latency Phone → Caddy → Hetzner → Tailscale → HeyBrian. Acceptable for a settings UI? Or have the PWA talk Tailscale-direct via a magic URL?
  5. Hammerspoon partial-retirement. brian_panel.lua / brian_prompt.lua stay alive in HS for now. Risk of confusion (two systems on Mac)? Or fine because they don't paint borders?
  6. App-closed = system-down. What about the Mac launchd agent that auto-relaunches HeyBrian on crash? Want it (resilient) or out (so closed = down)?
  7. Anything I'm missing?

Phased execution

Phase 1 — Port v3 to HeyBrian (the foundation)

1.1. Extend HTTPServer.swift: add HMAC verifier + nonce LRU + /v2, /v2/action, /v2/caps routes. Port stays 9100 for this phase — Hammerspoon keeps :8765. Test against Hetzner gateway pointed at :9100.
1.2. Port the 12 executors to Swift. Each gets a unit test that fires the same JSON the Lua got and gets the same shape back.
1.3. Port capability registry + audit log + in-memory metrics + cpanel routes to Swift. Cpanel HTML served from app bundle.
1.4. Add WKWebView Settings window, opens http://127.0.0.1:9100/cpanel with ?t=<token>.
1.5. Cutover: flip Hetzner gateway to :9100, retire Hammerspoon brian_controller_ui_v2.lua, then HeyBrian binds :8765 (and :9100 for backcompat for a week).

Phase 2 — Native overlays

2.1. Replace Hammerspoon canvases for border/pill/status with native NSWindow overlays inside HeyBrian.
2.2. Border breathing animation in CABasicAnimation.
2.3. Pill chooser → NSMenu.

Phase 3 — mac.jonahtebaa.com is the remote view

3.1. Hetzner mac_control_routes.py proxies /api/mac/cpanel/* to HeyBrian.
3.2. PWA shell + cpanel HTML render the same Lua-era UI on phone.
3.3. Phone-first CSS responsive tweaks.

Phase 4 — Pull the rest of Hammerspoon into HeyBrian (optional, later)

4.1. Port brian_panel.lua / brian_prompt.lua to HeyBrian native input window.
4.2. Port file watcher.
4.3. Uninstall Hammerspoon at the end.


Smallest first ship

Phase 1.1: HeyBrian HTTPServer.swift gains HMAC + /v2/caps GET route. Hetzner gateway's _probe_reachable switches to point at :9100/v2/caps (or stays at :8765 and the Hammerspoon module continues to answer that probe — both ports answer the same JSON, soft transition). One test: python3 -c 'from mac_access_gateway import _probe_reachable; print(_probe_reachable())' returns ok against HeyBrian.

After 1.1 lands, every subsequent phase is incremental — capability migration one at a time, with both Lua and Swift answering until Lua retires.


What this means for what's already built

v3 piece v4 fate
brian_controller_ui_v2.lua (1300 LOC) retires after Swift port lands; reference implementation
~/.brian/capabilities.json unchanged — same file, HeyBrian reads/writes
~/.brian_mac_secret unchanged
~/.brian/cpanel_token unchanged
Cpanel HTML (~250 LOC vanilla JS) unchanged — served from HeyBrian app bundle
Hetzner mac_access_gateway.py thin client unchanged — endpoints same
HMAC envelope shape unchanged
Audit log path / format unchanged

In other words: Section 1 + Section 2 of v3 are the spec. v4 phase 1 is implementing them in Swift instead of Lua. No throwaway work.