Skip to content

Architecture — the gothalo binary

gothalo is one Go binary: a Herdr bridge plus a CLI to run and manage it. Standard module layout; the HTTP surface is transport-agnostic so it can run over Tailscale today and a hosted relay later (see DECISIONS D10–D12).

cmd/
└── gothalo/ thin entrypoint -> internal/cli
internal/
├── cli/ cobra tree: serve · pair · devices (+ daemon client)
├── config/ ~/.gothalo config (JSON), admin token, transport mode
├── herdr/ typed herdr CLI wrapper + multi-session Manager (all sessions)
├── watcher/ event-driven agent watcher (herdr agent wait) + poll fallback
├── push/ FCM v1 sender (stdlib JWT->OAuth->messages:send, data-only)
├── store/ paired-device registry -> ~/.gothalo/devices.json
├── pairing/ one-time codes + QR (transport-agnostic connect payload)
├── server/ http.Handler routes + per-device/admin auth ← the heart
├── transport/ Transport interface
│ ├── direct/ http.Server (tailnet / LAN / localhost) [active]
│ └── relay/ outbound-WS broker client [stub, later]
├── events/ in-process pub/sub bus (fan-out; drops slow subscribers)
├── agentstate/ parse `herdr agent read` -> compact per-agent state (per-kind)
├── transcript/ tail the agent's on-disk transcript -> kind-agnostic chat
├── browse/ read-only, directories-only host listing + its containment rules
├── imagedrop/ land an uploaded image in the agent's cwd -> a path it can read
├── gitbranch/ is a branch safe to delete, and delete it (Herdr has no branches)
├── ports/ lsof + probe + process-tree walk -> the host's dev servers, per pane
├── preview/ relays a loopback-bound dev server onto the tailnet (auth + WS)
├── suggest/ pane observation (servers + git + foreground) -> one-tap actions (GET /suggestions)
├── notify/ bus consumer: dismiss stale "blocked" pushes
└── web/ embedded web-push receiver page (go:embed)
phone / browser ─▶ transport (direct: listen; relay: dial-out) ─▶ server.Handler
│
herdr CLI ◀── herdr.Client ──┐ │
FCM ◀── push.Client ────┼── handlers ─────┤
devices.json ◀ store.Store ───┤ │
codes/QR ◀── pairing.Manager ┘ │

The same handlers run under any transport. serve picks the transport from config; pair/devices are localhost clients of the running daemon’s admin API.

Method Path Auth Purpose
GET /snapshot device bearer or admin live Herdr state
POST /send device bearer or admin type into a pane
POST /approve device bearer or admin idempotent one-tap approval (D8)
GET /attach device bearer or admin (?token=) WS live terminal (PTY-streamed)
GET /agent-state device bearer or admin parsed compact state for one agent pane
GET /diff device bearer or admin an agent pane’s working-tree changes (branch + git context + per-file unified diff); ?context=1 is the git read behind the create_pr suggestion
GET /diff/expand device bearer or admin the unchanged lines around a hunk, for the diff viewer’s collapsed regions
GET /suggestions device bearer or admin context actions for a pane, from what is running in it
GET /ports device bearer or admin raw dev-server scan behind the dev_server suggestion (app reads /suggestions)
GET /agent-transcript device bearer or admin (?token=) WS normalized transcript chat + backlog
GET /events device bearer or admin (?token=) WS unified event bus (state changes, push lifecycle)
GET /browse device bearer or admin host directory picker — read-only, directories-only, root-confined
POST /herdr device bearer or admin allowlisted Herdr CLI proxy (worktree/tab/pane parity)
GET /branch-info device bearer or admin can this worktree’s branch be deleted, and is it merged
POST /branch-delete device bearer or admin delete a local git branch (after its worktree is gone)
POST /register-token device bearer or admin (re)register a push token
POST /testpush device bearer or admin fan a sample push to all devices
POST /pair one-time code issue a per-device bearer
POST /admin/pairing admin mint a code + connect URL (for the QR)
GET /admin/devices admin list paired devices
POST /admin/devices/revoke admin revoke a device
GET / … none embedded web-push receiver page
  • Admin token — auto-generated into ~/.gothalo/config.json; gates the CLI, admin endpoints, and the browser test page.
  • Per-device bearer — minted by /pair, stored per device; revocable one at a time. Push fans out to every device’s FCM token.

The watcher fires server.Notify(pane, status, title, seq) on each transition into blocked/done; Notify pushes a data-only, high-urgency FCM message to every registered device (reliable lock-screen delivery). The message carries the agent’s state_change_seq so an Approve action tapped later can echo it back to POST /approve, which no-ops if the agent has moved past that seq (D8). A pre-existing blocked/done state at watcher start is not replayed.

GET /attach upgrades to a WebSocket (github.com/coder/websocket) and runs herdr agent attach <pane> under a PTY (github.com/creack/pty). Two io.Copy loops bridge the PTY and the socket (via websocket.NetConn, binary frames): pty stdout → WS and WS → pty stdin. Whichever side ends first tears down the other and the subprocess is killed. Auth accepts ?token= since WS clients can’t always set headers.