--- id: dev-setup title: One command takes a new machine to a working stack status: open crates: [didbot-setup, didbot-stack, didbot-serve] 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 turned out to be Three tables in `~/.config/didbot/stack.toml`, read by [didbot-stack](../crates/didbot-stack/README.md): a *service* is one process, a *profile* names one service per role, and a *binding* maps a directory to a profile. A whole stack is the wrong unit. The composition that comes up first is two record hosts over one personal data server — so that rebuilding one does not interrupt the other — and five-processes-or-nothing cannot say that. Naming services per role makes sharing what happens when two profiles name the same one. The directory is the key because it is the only identifier every consumer already has: a hook payload carries it, a script has one, and the harness keys its own per-directory configuration on the same absolute path. Resolution is longest-prefix, and a directory nobody bound falls back to `default`, which is what lets a worktree created five minutes ago work with nobody registering it. A machine with no configuration file behaves exactly as one whose file names today's ports, and a test reads `scripts/dev-*.sh` and fails if the two ever disagree. ## Where accounts come from, and the bootstrap Hooks are captured at session start, so a session that runs the setup command 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, which is what `--dir` is for. 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] **The three tables are written a flag at a time.** `service add`, `profile new`, `bind`, `unbind` and `show`, and nothing writes a configuration that `check` would then report as broken — a file naming a service that does not exist is easy to produce a flag at a time and fails when somebody is starting a stack rather than when they wrote it. A service given no port lands just above the upstream it names, so a second stack reads as a block. - [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. `didbot-setup debug on` turns the detail up for every component at once, through one key in `stack.toml` that `env` renders as a `RUST_LOG`. - [x] **The development scripts read the profile.** Each `dev-*.sh` sources `scripts/dev-profile.sh`, which evaluates `didbot-setup env`. A variable already set wins, a machine with no configuration is unaffected, and a checkout with no setup binary built yet is unaffected too. - [x] **`apply` offers to put the hook binary on `PATH`**, separately from the file edits and not covered by `--yes`: the harness invokes the hook by name, so settings with no binary point at nothing, but one copy in `~/.cargo/bin` is then run by every checkout on the machine. - [x] **Three tables, and a validator.** A profile naming a service that is not there or is the wrong kind, a service naming an upstream that is not there, two services on one address, a binding to a profile that does not exist — and the silent one, a profile whose record host writes to a different server than the profile mints accounts on. That last one otherwise fails as a record refused for an account the target has never heard of. - [x] **`check`, `apply`, `undo`, `verify`, `env` and `forget`**, over one list of changes, so a doctor and an installer that state the same facts cannot drift apart. - [x] **Say what it will touch, before touching it.** `apply` prints every change as a file, a path and a value, and asks once; a run with no terminal and no `--yes` refuses rather than assuming consent. - [x] **Idempotent and resumable**, as a consequence rather than a feature: every change is probed before it is made, so there is no checkpoint file to go stale — the machine's own state is the checkpoint. - [x] **A teardown that removes what setup added**, from a ledger holding the value that was at each location before. JSON has no comments, so a change cannot mark itself; a location whose value has changed since is reported and left alone, because somebody edited it and it is theirs now. - [x] **A doctor command**, read-only and the default, because "why is nothing scrobbling" is asked far more often than "please set this up" and must never be something a person hesitates to run. - [x] **Hooks take effect on the next session, not on save.** `apply` ends by saying so and stopping; `verify` is what proves a new session took them. - [x] **Finish with a real check.** `verify` provisions through the same client the hook uses, writes a record, reads the record back, asks the query service whether it arrived, and deletes the account whatever happened. Its exit status is the answer, and a port that accepts a connection is not consulted. - [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**, and `check` compares it against the worktree it is run in. One binary serves every checkout on a machine, so the last one to run `dev-pds.sh` wins for every session afterwards, and nothing else could say so.