The command line #
What commands didbot has, who runs which, where each is installed, and how
they fit together. cli is the epic that owns the
decisions; crates/didbot-dispatch, crates/didbot-cli,
crates/didbot-operator and crates/didbot-agentd are the code, and
the shape of the system draws the hosts these run on.
Five binaries #
A person types didbot. What runs is one binary per place the code runs,
each versioned and installed on its own.
| Binary | Runs on | Commands |
|---|---|---|
didbot-pds |
the server, as a container of the release image under one systemd unit | didbot-pds --help lists its flags. No verb reaches it |
didbot |
the operator's own machine, and every agent host | didbot --list, didbot help <verb>, and every verb below — each one run by the binary it is listed under |
└─ didbot-operator |
the operator's own machine | didbot operate, didbot login, didbot estop, didbot announce |
└─ didbot-oauth |
an agent host | didbot oauth pending, didbot oauth show, didbot oauth approve, didbot oauth decline |
didbot-agentd |
an agent host, as one long-running process | none; the environment is its whole configuration |
didbot is a router and nothing more. It carries a table of the first-party
verbs and the binary that runs each, so a verb whose binary is absent is met
with what to install rather than "unknown command".
What didbot does with a verb #
didbot <verb> … looks the verb up in its table, finds the binary on
PATH, and replaces itself with it. The verb receives the rest of the
command line as typed and this process's environment with every credential
variable removed: DIDBOT_AGENT_TOKEN and DIDBOT_AGENT_TOKEN_FILE. Nothing
is added. On unix the verb's process replaces the dispatcher's, so the verb's
exit status is the shell's.
didbot --list scans PATH for didbot-*, asks each for --version, and
prints name, version and path; a binary that does not respond inside three
seconds is listed with ?. A first-party binary that is not installed is
named after the list, with the verbs it would run:
didbot 0.1.0 /home/operator/.cargo/bin/didbot
didbot-operator 0.1.0 /home/operator/.cargo/bin/didbot-operator
didbot-oauth: not installed (oauth)
didbot help <verb> is didbot <verb> --help, resolved the same way; didbot help alone prints the dispatcher's own help.
The operator's commands #
didbot-operator holds two sign-ins that never meet. operate authenticates
to the operator's own atproto account and writes bot.did.operator into
their own repository; the session it keeps is checked by the operator's own
PDS. login starts the deployment's own sign-in and keeps the session the
deployment minted; estop and announce present that session to the same
/dashboard/api/* routes the dashboard's buttons call. Neither verb can use
the other's session.
didbot operate --check <HOST> runs the read-only half alone: it resolves
the hostname, opens HTTPS to it, reads its did:web document and
cross-checks describeServer. It signs in to nothing and writes nothing, and
exits 1 until every check passes, so a script can wait on it. A host the
server reserved is named by the name the server chose, under --server:
didbot operate --server pds.example alpha claims alpha.pds.example.
didbot operate <HOST> <OPERATOR> signs in to the operator's own account
through OAuth, with a scope no wider than writing bot.did.operator. A
browser opens for the consent screen once; the session is kept in
$XDG_CONFIG_HOME/didbot-operator/sessions.json and resumed on the next host.
The command prints the record's URI and returns. The server polls the
operator's repository on its own interval and finds the claim there.
didbot login --server <HOSTNAME> [HANDLE] prints a URL and listens on a
loopback port. The deployment runs its own sign-in against the operator's
account, identity only, and mints a session; the browser brings it back to
the loopback port and the command stores it in
$XDG_CONFIG_HOME/didbot/operator.json, mode 0600, one entry per server.
When estop or announce is refused with 401, the fix the refusal names is
to run didbot login --server <HOSTNAME> again.
didbot estop --server <HOSTNAME> with no setting reports what the stop is
doing and what it has refused. --pause stops new tokens and new
provisioning, --revoke stops outstanding work too, and --release clears
the stop; one setting at a time. didbot announce --server <HOSTNAME> asks
the configured relay to crawl the deployment, and --resume calls the
deprecated notifyOfUpdate instead.
The agent host's commands #
An agent host runs didbot-agentd once, and the hook the
didbot-claude plugin
installs reports every harness event to it over a unix socket. The daemon
provisions one account per context, holds each account's token, and follows
the sign-in requests the server holds for those accounts. didbot oauth is
the one command an agent runs, and everything in it names a request — a
token, or the URL of one — and never an account. The daemon on an agent
host draws the daemon's inside; this picture is the commands and
the socket between them.
didbot oauth pending prints one line per request waiting for this agent,
each with its token. didbot oauth approve <token> lets one in and
didbot oauth decline <token> [--reason WHY] turns it down. A token names one
request, is good once, and is not an account: the daemon acts as the account
named by the record it holds that token in. When no hook saw the client
print its URL and the poll has not come back, didbot oauth show <url> looks
the request up by that URL, and didbot oauth approve --url <url> and
didbot oauth decline --url <url> [--reason WHY] settle it.
didbot-agentd takes no options. DIDBOT_PDS names the server that mints
accounts and is required; DIDBOT_SOCK moves the socket from
$XDG_RUNTIME_DIR/didbot-agent/agent.sock; DIDBOT_STATE moves the node
key from $XDG_STATE_HOME/didbot/agentd. The hook and didbot oauth read
DIDBOT_SOCK too, so all three agree on where the socket is.
A host with no daemon runs the binary by name. With DIDBOT_PDS and either
DIDBOT_AGENT_TOKEN or DIDBOT_AGENT_TOKEN_FILE set, didbot-oauth pending,
show, approve and decline talk to that server as that one account;
--direct insists on it rather than trying the socket first. Through the
dispatcher both credential variables are removed before the hand-off, so
didbot oauth reaches the socket and nothing else.
Installing #
Every binary is a crate in one workspace, and cargo install takes the
crate's name. The operator's own machine:
cargo install --git https://tangled.org/permadeath.com/didbot didbot-dispatch
cargo install --git https://tangled.org/permadeath.com/didbot didbot-operator
An agent host, where the didbot-agentd crate carries both didbot-agentd
and didbot-oauth:
cargo install --git https://tangled.org/permadeath.com/didbot didbot-dispatch
cargo install --git https://tangled.org/permadeath.com/didbot didbot-agentd
The hook that reports to the daemon is the didbot-claude plugin, installed
from Claude Code:
claude plugin marketplace add git@tangled.org:permadeath.com/didbot-claude
claude plugin install didbot@didbot
The server runs didbot-pds as a container of the release image, built from
the repository's Dockerfile and started by one systemd unit;
deployment says how the image is built and run, and
operations how it is upgraded and rolled back. The same
binary runs on a laptop: it is the didbot-serve crate's, so cargo install --git https://tangled.org/permadeath.com/didbot didbot-serve installs it, and
scripts/dev-pds.sh runs it from a checkout.
Extending #
didbot foo runs didbot-foo from PATH. The program receives the rest of
the command line as its own arguments and the ordinary environment, less
DIDBOT_AGENT_TOKEN and DIDBOT_AGENT_TOKEN_FILE; nothing is added, and no
credential from the shell reaches it. It authenticates for itself, the way
the first-party verbs do. didbot --list shows it beside them, with whatever
its --version prints, and didbot help foo runs didbot-foo --help.
Conventions #
Every binary accepts --help and --version, prints on stdout and exits 0.
--version prints <name> <version>, which is what didbot --list shows.
Three exit statuses: 0 did it, 1 understood the request and could not or
would not do it, 2 did not understand the request. A refusal is one line on
stderr, <program>: <message>, where the program is the spelling the person
typed: didbot estop: --server <HOSTNAME> is required.
Every didbot-operator verb takes --server <HOSTNAME>, the deployment to
act on, and --json, the result as one line of JSON on stdout for a script.
Both read the same before or after the verb. didbot oauth names its server
through the daemon, or through DIDBOT_PDS when run by name.