--- id: cli title: One `didbot` command, and a binary for each place the code runs status: shipped crates: [didbot-cli, didbot-dispatch, didbot-operator, didbot-agentd] dependsOn: [] exitCriterion: > `didbot ` runs the verb's binary from `PATH` with argv and no credential inherited from the shell; a first-party verb whose binary is absent names what to install; `didbot --list` shows every installed `didbot-*` with the version it reports; and the verb binaries parse, exit and render errors by `didbot-cli`'s conventions. --- # cli A person types `didbot`. What runs is one binary per place the code runs, each versioned and installed on its own: | Ships | Runs where | Verbs | |---|---|---| | `didbot-pds` | the server, in the image | its own flags; no verb reaches it | | `didbot` | operator laptop, agent host | dispatcher only: `--list`, `--version`, `help ` | | `didbot-operator` | operator laptop | `didbot operate [] [--server ] [--kind K] [--label L] [--oidc =...] [--fingerprint SHA256:...]`, `didbot operate --check `, `didbot login`, `didbot estop`, `didbot announce` | | `didbot-oauth` | agent host | `didbot oauth pending`, `didbot oauth show`, `didbot oauth approve`, `didbot oauth decline`, each with `--token-file ` or `--token-stdin` | | `didbot-register` | the machine that will be the account | `didbot register [--under ] [--token-file \| --token-stdin] [--server ] [--timeout ]`, the server from `--server` or `DIDBOT_PDS` | | `didbot-agentd` | agent host, a service | none; a daemon the unit starts | Three install sets follow from the table. An operator installs `didbot` and `didbot-operator`. An agent host installs `didbot` and `didbot-agentd`, which ships `didbot-oauth` and `didbot-register` beside the daemon. A developer builds the workspace and reaches everything through `scripts/`. This epic is about the entry point, not what the verbs do. Each verb has its own epic; this one says how a person reaches it. ## Four rules **The dispatcher passes argv and nothing else.** `didbot …` execs the verb's binary with the remaining arguments. It adds nothing to the environment and, before the exec, removes every variable in `didbot_cli::env::CREDENTIALS`. Each verb authenticates for itself: `didbot-operator` with its own session files, `didbot-oauth` over the daemon's socket. A program on `PATH` that a typo on `didbot-` can reach therefore inherits no credential from the shell, and neither does a first-party verb — which is why `didbot-oauth`'s daemonless mode, the one the environment configures, is reached by running that binary by name. **Each binary is independently versioned and self-describing.** Every `didbot-*` answers `--version` with ` ` and `--help` on stdout. `didbot --list` scans `PATH` for `didbot-*`, asks each, and prints name, version and path; a binary that does not answer is listed with `?`. Skew is visible and never fatal. `didbot help ` runs the verb's own `--help`. **Shared conventions live in one library crate, `didbot-cli`.** `clap` parsing through `didbot_cli::parse`, the `--server ` and `--json` flags, the exit statuses — 0 did it, 1 refused or failed, 2 did not understand — and `didbot_cli::finish`, the one line a refusal is written as. First-party verbs use it so they look like one tool. **First-party verb names are known to the dispatcher even when their code is absent.** `didbot` carries a table, `didbot_dispatch::verbs::FIRST_PARTY`: `operate`, `login`, `estop` and `announce` run `didbot-operator `; `oauth` runs `didbot-oauth` and `register` runs `didbot-register`, each with what followed. Every row names the crate that ships its binary, so a first-party verb whose binary is not on `PATH` answers "install `didbot-operator`" or "install `didbot-agentd`" and exits 1. Any other word is external dispatch: `didbot foo` runs `didbot-foo` from `PATH` with argv passed through, and when there is none answers "unknown command `foo`; `didbot --list` shows what is installed" and exits 2. The table is the mechanism; there are no links between binaries. ## Two credentials in one binary, kept apart `didbot-operator` holds two sign-ins that never meet. `operate` is the claim: it authenticates to the operator's *own* atproto account through `jacquard-oauth` and writes `bot.did.operator` into their own repository, keeping that session in `didbot-authstore`'s file. For a name beneath a server the same session has the operator's PDS mint a service-auth token, and that token — never the session — is what the server sees. `login` starts the deployment's own sign-in and keeps the session the deployment minted, which `estop` and `announce` present. One is checked by the operator's PDS, the other by the deployment; neither verb can use the other's, and only the first admits anything. `didbot register` and the daemon hold a third thing that is not a credential at all: a key that never leaves the machine. The server sees its public half, and tokens it signs. - [ ] **The `didbot-claude` plugin spells the approval `didbot oauth approve `.** Another repository; its sessions are told the command to run. ## Done - [x] **`didbot-cli`.** `crates/didbot-cli`: `parse`, `Server`, `Json`, `Exit`, `Refusal`, `finish`, the `version!` macro, `env::CREDENTIALS` and `FirstPartyVerb`. - [x] **The dispatcher.** `crates/didbot-dispatch`, binary `didbot`, linking `didbot-cli` and nothing else of the workspace. `--list`, `help `, the verb table, external dispatch, and the environment scrub — each held by `crates/didbot-dispatch/tests/dispatch.rs` against stand-in binaries on a `PATH` of the test's making. - [x] **`didbot operate`.** `didbot-operator`'s `operate` verb, with `--check` for a server and for an account, and the account form beneath a server: `didbot_operator::operate::under` reads which server from the operator's own records, `::account` mints the service-auth token, writes the record and creates, taking the record back if the create is refused; `::walk` is the check. The session it keeps is `didbot_authstore::FileAuthStore`'s file. - [x] **`didbot register`.** `didbot-agentd`'s `didbot-register` binary over `didbot_agentd::register`: a parked key admitted from the operator's side, or an OIDC token as the parent, ending in a session under `didbot_cli::tokens`. - [x] **`didbot oauth --token-file` and `--token-stdin`.** `didbot_agentd::direct::Direct::from_token_flag`: a session from `bot.did.createSession`, then the command as that account. A token is never an argument. - [x] **The operator binary links no server code.** `didbot-operator` depends on `didbot-scope` — the scope grammar, which `didbot-serve` re-exports as `oauth::scope` — and on the crates the policy page shares (`didbot-claim-check`, `didbot-onboarding`, `didbot-identity`, `didbot-key`), plus `didbot-authstore`, `didbot-http` and `didbot-cli`. `cargo tree -p didbot-operator -e normal` names neither `didbot-serve` nor `didbot-pds`. - [x] **`--version` everywhere.** `didbot-pds`, `didbot-agentd`, `didbot-oauth` and `didbot-operator` each name themselves, and `didbot-pds` answers without starting. - [x] **`didbot-oauth` exits and speaks by `didbot-cli`'s conventions.** It answers `--version` through `didbot_cli::version!`, spells itself `didbot oauth` on every line, and exits by `didbot_cli::Exit`; its flags are read by its own parser.