id: dev-setup
title: One command takes a new machine to a working stack
status: open
crates: [didbot-setup, didbot-stack, didbot-hookd, didbot-hook, didbot-mcp, 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. #
local-dev 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.
Three tables in ~/.config/didbot/stack.toml, read by
didbot-stack: 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
scrobble 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.
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 scrobble 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 scrobble 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.
`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.