harnessd 0.1.0

The harness daemon: API server (axum WS + REST), agent runtime host, and CLI (init/pair/doctor).
# 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.

| Method | Route | Purpose |
|---|---|---|
| `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
```