Status: DRAFT — codex review integrated 2026-05-09
Last updated: 2026-05-09
Owner: Brian (autonomous agent on Hetzner)
Scope: All Hetzner→Mac access flows through one centralized controller with a single visible UI on Jonah's Mac.
Codex's strongest critique: "the architecture centralizes intent, but not authority over the actual Mac-touching path. ProxyCommand only protects SSH connections that use that config path... A central controller is only real if all Mac access is forced through it at the network/account boundary."
Adopted changes:
mac_session.py and mac_run no longer invoke ssh directly. They send {action:"run", argv:[...], cwd, env, timeout} over the Unix socket; the daemon spawns the remote shell, owns PGIDs, logs, tokens, heartbeats, and kill semantics. The mac_gatekeeper ProxyCommand becomes belt-and-suspenders only — it refuses any direct shell that lacks a session, but is not the enforcement path.os.setpgrp / start_new_session=True. Stop sends os.killpg(-pgid, SIGTERM) then SIGKILL. Also closes any SSH ControlMaster sockets to prevent multiplexed-session survival.nonce + timestamp and a per-session shared secret is the app-layer auth. Reject replays (nonce LRU, ±30s window). Secret rotates per-session.mac_run callers), token is rotated on every step boundary so a leaked snapshot is stale.These changes are reflected throughout the architecture below. The original "ProxyCommand-as-enforcement + env-token push" design is rejected.
Brian on Mac is maximally autonomous. Anything a human user could do on Jonah's Mac, Brian can do — files, screen, clicks, native apps, voice, clipboard, browser, contacts/calendar/notes/shortcuts, accessibility tree, OCR, vision, recording, IPC. And Jonah is never confused about what is happening on his machine. Every Brian-driven action paints the same single breathing-cyan UI, with one clickable pause/stop pill, one translucent status box, and the start/stop chimes (Glass / Submarine).
v1 (mac_access_gateway.py + brian_mac_gateway.lua, commit c81e8b8) shipped a context-manager + Lua module pair. Three bugs proved the architecture is wrong:
init.lua canvas code (_brian_active_canvas, _brian_status_canvas, _brian_active_label_canvas) still runs alongside the new module. Neither side knows about the other.%20 literals. Hammerspoon's urlevent.bind does not auto-decode URL params despite documentation implying otherwise.mac-bin/ssh PATH wrapper intercepts every shell ssh call and pushes its own status URL.Root cause: 5+ uncoordinated Mac-touching systems, no central controller. Anyone can paint the screen; no one has a global view of session state.
+--------------------------+
| Caller (skill/script) |
| with MacSession() as s: |
| s.step("...") |
| s.run("...") |
+-----------+--------------+
|
Unix socket /run/brian-mac.sock
(request: run/click/screencap/...)
|
+-----------v--------------+
| brian-mac-controller | <-- systemd service
| (Python daemon, SSOT) |
| |
| - active session state |
| - pause/stop signals |
| - PGID set (kill -PGID) |
| - SSH ControlMaster mgr |
| - HMAC signer/verifier |
| - heartbeat to Mac |
+-----+-----------------+--+
| |
| spawns shell | HTTPS POST (JSON + HMAC)
| in own pgrp | over Tailscale
v v
+---------------+ +------------------+
| shell (PGID) | | Mac httpserver |
| → Mac:22 | | (Hammerspoon) |
+---------------+ +------------------+
Belt-and-suspenders (NOT enforcement path):
~/.ssh/config ProxyCommand → mac_gatekeeper → refuses if no session
Components:
brian-mac-controller.service — systemd-managed Python daemon. The only process that opens a remote shell to the Mac. Single source of truth for:
- active session: id, title, current_step, state (running / paused / stopping / idle), started_at, last_heartbeat
- pending pause/stop signals coming back from the Mac
- registry of running shell process groups (PGIDs), spawned with start_new_session=True so os.killpg(-pgid, SIGKILL) reaps grandchildren
- SSH ControlMaster socket lifecycle (close on stop to kill multiplexed sessions)
- HMAC signer/verifier for every Mac↔Hetzner payload
- HTTPS client to the Mac's hs.httpserver for control POSTs (Tailscale-bound)
- HTTP server endpoint for pause/stop callbacks from the Mac
- Exposes Unix socket /run/brian-mac.sock for IPC from sibling processes
mac_session.py — thin client library used inside Brian's Python code. Does not invoke ssh. Connects to /run/brian-mac.sock, registers a session, gets a session id (no token-in-env). Every s.run() / s.click() / s.screencap() is a JSON request to the daemon.
python
from brian_mac import MacSession
with MacSession(title="Re-linking LinkedIn") as s:
s.step("Bringing the LinkedIn login window to front")
s.run("osascript -e '...'") # daemon spawns, owns PGID
s.click(800, 450) # daemon sends to Mac
s.screencap("/tmp/after_click.png") # daemon orchestrates
Auth on the Unix socket: SO_PEERCRED + uid check (only root or the agent uid). No token push.
mac_gatekeeper — small executable used as ~/.ssh/config ProxyCommand. Belt-and-suspenders only — not the enforcement path. Asks the daemon over /run/brian-mac.sock "is there an active session for this caller?". Pipes nc 100.79.172.24 22 if yes, refuses with EX_CONFIG if no. This catches escape attempts (third-party tools, manual SSH from a shell, copy-pasted snippets) and forces them to fail loudly instead of silently bypassing the controller.# ~/.ssh/config
Host jonahs-mac mac.tail51157c4.ts.net 100.79.172.24
ProxyCommand /opt/agent/bin/mac_gatekeeper %h %p
mac_run — shell wrapper for non-Python callers. Also routes through the Unix socket, not env-token-shell:
mac_run --title "X" --step "Y" -- <cmd>
Spawns/attaches a session via the daemon, sends the command as a {action:"run", argv:[...]} request, streams stdout/stderr back, exits with the remote exit code. No env token.
PreToolUse hook (existing, brian-mac-access-gateway-guard.py) stays as a third belt for the Bash tool path.
~/.hammerspoon/brian_controller_ui.lua — single encapsulated module:
- hs.httpserver.new() on Tailscale-bound port 8765 listening for JSON POSTs from the Hetzner daemon
- Schema: {action, session_id, title, step, state} where action ∈ start | step | pause | resume | stop | heartbeat
- Owns the ONE border canvas (4-layer breathing gradient cyan #00e5d4)
- Owns the ONE clickable pill (chooser → pause/stop)
- Owns the ONE translucent status box (alpha 0.60, see-through, text legible)
- Plays Glass on start, Submarine on stop
- Heartbeat watchdog: if no POST in N seconds → tear down UI + send "kill" back to Hetzner controller
- Click pause/stop → POSTs back to Hetzner controller daemon's HTTP endpoint
init.lua cleanup — DELETE every legacy symbol:
- _brian_active_canvas
- _brian_status_canvas
- _brian_active_label_canvas
- _showBrianActive()
- _setBrianStatus()
- the brian-active and brian-status URL handlers
Keep one line: require("brian_controller_ui"). Backup init.lua.bak.20260509_pre_v2 before editing.
| Decision | Value | Rationale |
|---|---|---|
| Brand color | cyan #00e5d4 |
Matches existing Webspot/Brian palette |
| Sounds | Glass (start) / Submarine (stop) | macOS system sounds, no asset shipping |
| Status box opacity | alpha 0.60 |
Translucent, see-through, text legible |
| Migration | hard cutover, no shims | Shims would re-create the "5 uncoordinated systems" failure mode |
| Mac transport | hs.httpserver JSON-POST |
Eliminates URL-encoding bugs forever |
| Auth enforcement | ~/.ssh/config ProxyCommand |
Catches Python subprocess.run calls the PreToolUse hook can't see |
| Lua surface | single module | Old canvas code in init.lua MUST be deleted |
| Watchdog | heartbeat tear-down | If controller dies, UI clears and live SSH PIDs are killed |
| Pause | between steps | Halts before next s.step() |
| Stop | immediate | Aborts current shell, kills child PIDs |
| Codex partnership | ongoing | Adversarial review at lock-in, after each component, before final commit |
| Documentation | publishable as-built | Drive-synced folder so this can one day open-source |
Each capability gets:
- A method on MacSession
- A docs page under capabilities/<name>.md
- An end-to-end test under /opt/agent/tests/mac_controller/test_<name>.py
Categories (full list in handoff):
- Files & shell — run, scp_to_mac, scp_from_mac, read_file, write_file
- Screen — screencap (region/cursor), start_recording, stop_recording
- Mouse + keyboard — click, move, drag, scroll, type_text, keystroke, keychord
- Window mgmt — list_windows, front_app, activate, raise_window, close_window, move_window, resize_window
- Browser — Chrome tabs/activate/url/run_js across all profiles (Default, Brian, chrome-mac-cdp)
- Pasteboard — clipboard_get/set/history
- Notifications + voice — notify, say, alert
- Mac data — Contacts, Calendar, Notes, Reminders, Shortcuts, iMessage
- Native app IPC — AppleScript + JXA escape hatches
- Accessibility — find/click/value by role+label (not coords)
- System state — battery, network, audio, brightness, lock state, idle
- Permissions — TCC check/request (live grants on sshd-keygen-wrapper)
- Process mgmt — list/kill/launch/quit
- OS shortcuts — open URL/file, reveal in Finder, Spotlight, mdfind
- Photos / Music / Continuity / Bluetooth / USB / Wi-Fi
- Camera / Mic — snapshot, record (for "what's around me right now")
- OCR + Vision — find text on screen, click text-by-content not coords
- Hammerspoon escape hatch — hs.run(lua_code) for anything not yet wrapped
Build v2 in parallel with v1 still running. Verify v2 in isolation against the Mac. Then:
init.lua require for the old moduleinit.lua require("brian_controller_ui")relink_brian_linkedin.sh (and add the click-retry-on-URL-mismatch fix)chrome-mac skillclaude-in-chrome skill (via MCP bridge)use-my-browser skillbrian_alert.py (Mac notification path)brian_shared_watcher.py (Drive/dropbox watcher)mac_helper_client.py (queue-based IPC) → replace with daemon API callsmac_legacy_v1.tar.gz in /opt/agent/data/archives/, then git rm:mac-bin/sshmac_ssh_guard.shbrian_mac_active.shbrian_mac_status.sh03_security_model.md)/opt/agent/data/mac_control.json, separate from controller, KEEP) overrides everythings.idle_seconds() < threshold, controller refuses to issue actions (Jonah is using the Mac); configurable per session/usr/libexec/sshd-keygen-wrapper (FDA / Screen Recording / Accessibility), per reference_mac_tcc_grants.mdOpen: HMAC-signed JSON over the Tailscale-bound HTTP? Codex review pending.
All resolved by the codex review delta at the top of this document. Outstanding for next codex pass (after Lua module + daemon land):
brian_controller_ui.lua) FIRST — it's the SSOT for UI state. Test in isolation: POST JSON, verify border/pill/box/chime fire correctly, verify pause/stop POSTs back.brian-mac-controller) — Python, systemd unit, Unix socket, HTTP client to Mac, manages shell child PIDs.mac_gatekeeper) and wire ~/.ssh/config ProxyCommand. End-to-end smoke: with MacSession(): s.run("uname -a") → confirms gatekeeper ran, daemon authorized, UI showed, command ran, UI cleared.v1_to_v2_cutover.sh) — stops v1 service, edits init.lua to swap modules, restarts v2 service, runs smoke test.relink_brian_linkedin.sh as first caller. Add the click-retry-on-URL-mismatch fix.capabilities/ for every capability landed.00_design.md01_protocol.md02_capabilities/<name>.md03_security_model.md04_migration_v1_to_v2.md05_publishing_notes.md/opt/agent/data/session_handoffs/260509_0856_mac-controller-rearch_handoff.mdMEMORY.md → hard_rule_documentation_folder.md, hard_rule_mac_status_box_specific.md, reference_mac_tcc_grants.md, hard_rule_mac_chrome_default_profile.md