--- id: dev-setup title: One command takes a new machine to a working stack status: open crates: [didbot-serve, didbot-agentd] dependsOn: [] exitCriterion: > On a machine that has never run this, one command ends with an agent provisioned, a record written and read back, and the canvas showing it — having asked for consent before each thing it could not do itself. --- # dev-setup [local-dev](local-dev.md) is the loop once you are in it: five terminals and a swarm. This is getting there. The gap is not the scripts, it is everything around them — a certificate, a name, an edit to the user's harness configuration, and a restart before any of it takes effect. The shape is a command that **asks rather than assumes**. It touches a person's settings file and possibly their trust store, so it says what it will do, does the parts it may, and stops with a specific instruction for the parts it may not. ## What the shape is One set of defaults per machine, in `scripts/dev-profile.sh`: the port, the zone and the state directory, each overridable from the environment. A second stack on one machine is a second set of those variables, or a second checkout. Each daemon knows its own default state directory under `$XDG_STATE_HOME` (`didbot-agentd`'s `default_state_dir`, `dev-pds.sh`'s `--data`), so nothing has to be told where anything lives before it can start. Installing an agent host — the daemon's unit file, the plugin, the hooks — is the agent host's installer's job, which is shell owned by the `didbot-claude` plugin: it writes the unit files and reports whether the plugin is present. ## Where accounts come from, and the bootstrap Hooks are captured at session start, so a session that installs them is never itself hooked — and neither are its subagents, which inherit the session's captured configuration rather than re-reading disk. A directory is therefore set up from outside it, before a session starts there. Recovery is a different problem from bootstrap, and a more frequent one: a server that was down when the session opened, or one restarted without `--data`. `SessionStart` cannot be replayed, so `PreToolUse` provisions as well, and the first record of a session with no usable account mints one. The identity still comes from the harness payload and never from the model, which is why this is a hook rather than something the record host offers: the Model Context Protocol carries no verified caller identity, so a tool that let a session register itself would let a model claim to be any session. ## Setup - [ ] **Name the steps a machine cannot take for you** and stop cleanly on each: installing a local certificate authority, binding port 443, choosing a zone, and restarting the harness so new hooks load. The restart is done. The other three arrive with TLS. - [ ] **Pick the TLS story and implement one.** Deferred, with two of the three candidates now answered. `mkcert` is declined. It installs a certificate authority into a person's trust store, which is the most invasive thing this command could do, and it destroys the value of the test it exists for: a third-party client either holds that authority, in which case its trust path is not the one it uses in production, or it does not, in which case it fails for a reason that teaches nothing. No public authority would issue `*.agents.localhost` either, so the certificate's shape differs from production regardless. A public wildcard-to-loopback certificate with a published key needs no trust store change and works in any client, but it forces somebody else's hostname suffix — so the zone differs from production, so the `did:web` values differ, and the DID is the field everything downstream keys on. That leaves the delegated development subzone, the only one where local is identical to production, and it needs a DNS credential on a developer laptop that [deployment](../docs/deployment.md) says should not sit beside the signing keys. It waits for a zone. Nothing is blocked on this. TLS is load-bearing for [pds-xrpc](pds-xrpc.md), which is `blocked`, and for [oauth](oauth.md). Plain HTTP over `*.localhost` stays the default, because it works offline, in seconds, and with no secret. ## The parts that are not the server ## Working in it - [ ] **Keep [docs/running-locally.md](../docs/running-locally.md) true**, which is [local-dev](local-dev.md)'s item and fails first here. Its closing section currently interleaves several revisions of the same paragraph and contradicts itself about whether records are signed into commits. ## Done - [x] **An account the configured server already holds is adopted rather than minted again.** `localhost` and `127.0.0.1` are two strings and one machine, and the hook does not guess which spellings of a host mean the same thing — so it asks. Without that, a session whose server is named the other way dead-ends: provisioning refused because the account exists, and every record after it unstamped. - [x] **One switch for debugging, and an identifier that survives three processes.** The hook stamps the payload's `tool_use_id`, the record host logs it and forwards it in a `quernstone-turn` header, and the server puts it on the span every line of that request is emitted under. The harness's own identifier rather than a fresh one, so the same string finds the turn in the session transcript too; never written to a record, because a repository is public and permanent and an operational identifier does not belong in one. `RUST_LOG` turns the detail up, and every component reads it. - [x] **The development scripts share one set of defaults.** Each `dev-*.sh` sources `scripts/dev-profile.sh`, which exports the port, the zone and the state directory. A variable already set wins, so a second stack is a second environment and nothing in the checkout is edited. - [x] **Wiring in two scopes is a conflict.** Settings files merge rather than override, so it means two accounts provisioned per session and one never torn down, and it has no other symptom anyone would recognise. - [x] **`PreToolUse` provisions when a session has no usable account**, and the server that minted an account is recorded beside its DID, so a DID is never stamped into a call bound somewhere else. The server goes in a second map rather than in each entry, so a hook built before it existed still parses the file instead of forgetting every account in it. - [x] **The `PreToolUse` matcher is a pattern, not a literal tool name**, so a machine can run a second record host and the settings block is written once and never edited again. - [x] **The hook binary says what it was built from.** One binary serves every checkout on a machine, so the last one to build it wins for every session afterwards, and nothing else could say so.