rendered from docs/first-hour.md 0c82598a3761

The first hour

The commands that take a fresh checkout to a server with a human behind it, an agent made by hand, a laptop host whose daemon makes agents, a pipeline whose runs sign in, and a revocation, in the order they work. Who operates an account is what each step means and the command line is every flag; this page is the sequence.

Every server here serves TLS, so before anything else do the two one-time steps in running the stack locally: trust the local certificate authority, and let didbot-pds bind 443. This page puts its server on 3413 instead, which is what DIDBOT_RESOLVE_PORTS below is for — a did:web carries no port, so every tool has to be told where the zone answers.

export DIDBOT_RESOLVE_PORTS=nc.localhost=3413

Two stand-ins

didbot operate signs in to your own atproto account in a browser, and a .localhost PDS cannot be that account: an atproto handle may not end in .localhost. Locally the human’s PDS and the OpenID Connect issuer are the harness’s, left running on fixed ports with the session didbot operate resumes already written:

export NEWCOMER_CONFIG=$HOME/first-hour/human/config
export DIDBOT_LOCAL_CA=$HOME/.local/state/didbot/local-ca
cargo test -p didbot --test scenarios the_stand_ins_by_hand -- --ignored --nocapture

It prints the human’s DID, did:web:localhost%3A3415 — a bare loopback hostname is the one place a did:web may carry a port — the issuer at http://127.0.0.1:3416, a mint at http://127.0.0.1:3417/ that answers an ID token for ?aud=<server did>&<claim>=<value>, and the authority it signed the human’s certificate with. Every didbot operate below runs with XDG_CONFIG_HOME=$NEWCOMER_CONFIG. On a real deployment the two are your own PDS and your platform’s issuer, and the browser opens once.

The server

printf '[operator]\ngrace_window_hours = 0\n' > nc.toml
didbot-pds --port 3413 --zone nc.localhost \
  --operator did:web:localhost%3A3415 --config nc.toml --data ./nc-data

The grace window is how long a deleted operator record keeps writing; zero makes the revocation at the end land at the next poll. Then, as the human:

didbot operate nc.localhost did:web:localhost%3A3415
curl -X POST https://nc.localhost:3413/xrpc/bot.did.pollOperatorClaim  # look now

The server’s log says found the operator's record; the claim stands. Until it does, every write is refused as ServerNotReady.

An agent by hand

didbot operate kestrel.nc.localhost did:web:localhost%3A3415 --kind agent
didbot operate --check kestrel.nc.localhost

The first run has no operator session for the server yet, so it prints a link to open, the way didbot login --server nc.localhost:3413 does. The browser lands on a page saying whether the sign-in worked, and the command keeps the session for every later run. It then prints the export DIDBOT_ACCOUNT_TOKEN_FILE= line for didbot-oauth. The second command walks one edge and exits 0. --check on the server’s own name needs DNS delegation a .localhost zone has none of; --check nc.localhost --server nc.localhost walks the server’s own edge instead.

What the server holds

The operator session is kept under the address it signed in at, nc.localhost:3413. The commands that carry it name the server the same way:

didbot account list --server nc.localhost:3413
didbot account show kestrel.nc.localhost --server nc.localhost:3413

list prints one line per account. show prints one account’s records, recent ledger entries and credentials. https://nc.localhost:3413/dashboard shows the same accounts once you sign in there, and an account’s detail adds and removes its credentials.

A laptop host and its daemon

On the laptop, with DIDBOT_STATE chosen and DIDBOT_SOCK left alone, so the daemon binds the path the hook connects to:

export DIDBOT_STATE=$HOME/first-hour/laptop
didbot register host laptop.nc.localhost --server nc.localhost

It prints a fingerprint and waits. On the human’s machine, with the fingerprint it printed:

didbot operate laptop.nc.localhost did:web:localhost%3A3415 --fingerprint SHA256:... \
  --creates agent --creates-beneath agent

register then signs in and exits 0. --creates agent lets the daemon create each session’s account beneath the laptop, and --creates-beneath agent lets each session create its subagents’ beneath itself; without them those creates are refused. Only then start the daemon: it holds the same key, and register refuses while a daemon holds it.

DIDBOT_PDS=nc.localhost didbot-agentd

In a second terminal, report a session’s first tool call the way the didbot-claude plugin’s hook does. Claude Code installs the hook under ~/.claude/plugins/cache/didbot/didbot/<version>/bin/, one directory per version, so pick the newest:

hook=$(ls -d ~/.claude/plugins/cache/didbot/didbot/*/bin/didbot-hook | sort -V | tail -n 1)
echo '{"hook_event_name":"PreToolUse","session_id":"sess-1","tool_name":"Bash","cwd":"/tmp"}' \
    | "$hook"

A session’s account is created at its first tool call. The daemon waits up to 1.5 seconds for that create, and the hook prints hook JSON whose additionalContext names the new agent as did:web:<name>. If the hook prints nothing, the create took longer; send the same event again. A context’s name is built from its session and its subagent together, so read it off that answer. The walk climbs two edges:

didbot operate --check <name>

A pipeline and its runs

didbot operate deploy.nc.localhost did:web:localhost%3A3415 \
  --kind pipeline --oidc http://127.0.0.1:3416 repository_id=456789
curl -s 'http://127.0.0.1:3417/?aud=did%3Aweb%3Anc.localhost&repository_id=456789' > id-token
DIDBOT_PDS=nc.localhost didbot oauth pending --token-file id-token \
  --account deploy.nc.localhost

The token’s aud is the server’s DID, encoded once for the URL. Each token is a session once: presenting it again is refused as already presented.

Revoking the host

The human deletes bot.did.operator/laptop.nc.localhost from their own repository, and nudges the poll:

curl -X POST https://nc.localhost:3413/xrpc/bot.did.pollOperatorClaim
didbot operate --check <name>                        # exit 1

The host and every agent beneath it are quarantined. didbot account list --server nc.localhost:3413 shows each as active, paused: the edge above it no longer stands. Their writes are refused, the daemon’s next create is refused as OperatorRecordMissing, and the walk fails at the missing record.