# harnessd
The daemon. One `harnessd` = one user. Everything stateful lives in
[`harness-core`](../harness-core); this crate is a thin [axum](https://github.com/tokio-rs/axum)
transport shell (WebSocket + REST) plus the operator CLI. It binds loopback/Tailscale only,
**never** a public interface.
## Where it sits
```
TUI / iOS ──WS + REST over Tailscale──► harnessd ──► harness-core
(this crate) runtime · store · providers
```
## HTTP surface (`server.rs`)
All routes are versioned under `/v1` so clients and daemon can evolve independently.
| `GET` | `/v1/health` | Per-subsystem status (providers build-checked; live probe is M2). |
| `GET` | `/v1/version` | Daemon + protocol version. |
| `GET` | `/v1/sessions` | List sessions, most recent first. |
| `POST` | `/v1/sessions` | Create a session (provider optional; defaults to the `main` role). |
| `GET` | `/v1/sessions/:id/events?from_seq=N` | Backfill the log without a WebSocket. |
| `GET` | `/v1/providers` | Configured providers and their `enabled` state. |
| `POST` | `/v1/pair/start`, `/v1/pair/complete` | Pairing — **scaffolded**, returns `501` until M4 (§8). |
| `GET` | `/v1/ws` | The live event stream + client command channel. |
REST handlers live in `rest.rs`; `CoreError` is mapped onto stable HTTP status + `{code,
message}` bodies there.
## The WebSocket flow (`ws.rs`)
One task per connection, multiplexing inbound client frames and the runtime's broadcast bus
with `tokio::select!`:
1. **Handshake** — the first frame must be `hello`. The daemon negotiates the protocol
version (accepts current and current − 1), checks auth, and replies with `welcome`.
2. **Subscribe → replay → live tail** — on `Subscribe { session, from_seq }` the daemon
replays the durable log from `from_seq` (via `store.events_since`), then marks the
session live so the select loop tails new broadcast events for it. Replay and live
delivery use the identical `EventEnvelope`, so reconnect-and-resume is seamless.
3. **Commands** — `SendMessage` spawns the turn in the background (so the socket stays
responsive for approvals/cancel while the model streams) and acks. `Approve` / `Deny` /
`CancelTurn` ack today and wire into the runtime's tool gates in **M2**.
A client that falls behind the broadcast buffer gets a `Lagged` warning; it recovers by
re-subscribing with a `from_seq` to catch up from the durable log.
## Auth (`server.rs::Auth`)
M1 is **permissive** — it trusts the loopback/Tailscale boundary and accepts any client.
The handshake already threads an optional device token, so pairing (M4, design doc §8) can
tighten `Auth::accepts` to check a hashed token set without any protocol churn.
## CLI (`cli.rs`)
Operability is a set of commands, not a README:
- `serve` — run the daemon (the default if no subcommand is given).
- `init` — write a documented starter config (mock provider enabled, anthropic present but
disabled). M4 turns this into an interactive wizard.
- `doctor` — a checklist as a command: config loads, each enabled provider builds, bind
address. TCC/permissions and Tailscale checks arrive in M4.
- `pair` — **scaffolded**; prints that device tokens + QR land in M4.
- `version` — daemon semver + protocol version.
## Config file
Loaded once at startup from `--config <path>`, else the platform config dir
(`paths::default_config_path`). A **missing** file yields the mock-only `demo_default()` so
the daemon boots with zero setup. Secrets are read from the env var named by
`api_key_env` and never written to the config or the event log.
## Quick start (zero credentials)
```bash
cargo run --bin harnessd -- init # writes a starter config (mock provider)
cargo run --bin harnessd -- serve # listens on 127.0.0.1:8787
# then drive it with the TUI, or headlessly:
cargo run --bin harness-tui -- --script "hello there"
```
## Build & test
```bash
cargo build -p harnessd
cargo test -p harnessd
bash scripts/e2e-smoke.sh # boots the daemon + drives the TUI end-to-end
```