--- id: cli title: Six binaries with six naming conventions, and no way for anyone else to add a seventh status: open crates: [didbot-agentd, didbot-claim, didbot-serve, didbot-setup, didbot-swarm, didbot-verify, didbot-brand] dependsOn: [] exitCriterion: > A new binary is accepted by the rules written here rather than by a reviewer's taste: it is named for the seat it runs in, its verbs are bare imperatives, it spells the server, the DID, the state directory and its errors the way the conventions table says, and no credential reaches it in `argv` or forwarded by a dispatcher. --- # cli There are eight binaries: `didbot-pds`, `didbot-setup`, `didbot-swarm`, `didbot-claim`, `didbot-verify`, `didbot-brand`, `didbot-agentd` and `didbot-oauth`, each under a `crates/*/src/bin/` of its own, each with a hand-rolled parser and a `USAGE` string, each named by whoever added it. No two spell the server's address the same way, three exit statuses mean "bad arguments", and `--help` exits `0`, `2`, or starts a daemon, depending on which one you asked. The occasion for this audit is a ruling: the verb an agent named `become-host` — the host half of [handshake](handshake.md)'s reservation, on the `claude/host-reservation` branch — will not be called that, and nothing else gets a name until the whole surface has been looked at once. This epic is that look, and a proposal. Each item below is for the owner to accept, amend or refuse; the rename table is a proposal, and every name in it marked *proposed* is one nothing on `main` answers to yet. This epic is about how a person reaches a surface, not what the surface does. Each surface has its own epic. ## What is on `PATH` today Every claim in this table is read from the parser, not from a `--help` that was remembered. | binary | crate, source | who runs it | verbs, and their form | the server | the DID | the credential | |---|---|---|---|---|---|---| | `didbot-pds` | `didbot-serve`, `src/bin/didbot-pds.rs` (`parse_args`) | the server host (`Dockerfile:106`, `infra/pds/templates/user_data.sh.tftpl`); a developer through `scripts/dev-pds.sh` | none. Thirty-odd `--flags`; `--list-names` and `--list-avatars` print and exit | is the server: `--zone ` names its own hostname, `--port ` | `--owner `, the operator answerable for every account (`DEFAULT_OWNER` is a placeholder) | none on the command line. Keys live under `--data`; Route53 credentials come over IMDSv2 (`USAGE`, `--route53-zone-id`) | | `didbot-setup` | `didbot-setup`, `src/bin/didbot-setup.rs` (`Options::parse`) | a developer, on the machine that runs the stack (`scripts/dev-profile.sh` evaluates `didbot-setup env`) | bare verbs (`check`, `env`, `show`, `bind`, `unbind`, `disk`), a verb with a word (`debug on\|off`), and noun-verb groups (`service add`, `profile new`) | `--pds `, a *name in `stack.toml`*, not an address; `env` prints `DIDBOT_PDS_URL` | none | none; `check` reads `/health` over HTTP (`check.rs:143`) | | `didbot-swarm` | `didbot-swarm`, `src/bin/didbot-swarm.rs` (`parse_args`) | a developer, through `scripts/dev-swarm.sh` | none; flags only | `--pds ` (default `http://localhost:3000`) | none; it mints its own | none; provisions through the front door | | `didbot-claim` | `didbot-claim` (`[[bin]]` in its `Cargo.toml`), `src/bin/didbot-claim.rs` (`parse_args`) | the operator, on their own machine, never the server (crate description) | none; two positionals, and `--check ` as a mode | ``, positional | ``, positional, the operator's own | a browser OAuth grant against the operator's own PDS, scoped to `scope::operator_write_scope`, held in `MemoryAuthStore` | | `didbot-verify` | `didbot-verify` (`[[bin]]`), `src/bin/didbot-verify.rs` (`parse`) | anyone, on a stranger's machine (crate description) | none; the mode is chosen by which flags are present — `` alone, or `--owner` with `--server` | `--server `, a DID, not an address | `` positional; `--owner ` | none, by design | | `didbot-brand` | `didbot-brand`, `src/bin/didbot-brand.rs` (`main`) | a developer and CI, through `scripts/build-brand.sh` (`scripts/ci.sh:61` passes `--check`) | none; `` positional, `--check` as a mode | not applicable | not applicable | none | | `didbot-agentd` | `didbot-agentd`, `src/bin/didbot-agentd.rs` (`main`) | the agent host, through `scripts/dev-agentd.sh` or a unit | none; it reads no argument at all | `DIDBOT_PDS`, an environment variable, required | none; accounts are minted per context through `registrar::Pds` | none at start; what it mints it holds in memory behind the socket | | `didbot-oauth` | `didbot-agentd`, `src/bin/didbot-oauth.rs` (`main`) | the agent, on the agent host; a CI job or a token-holding host with `--direct` | bare verbs: `pending`, `show `, `approve \|--url `, `decline \|--url [--reason]` | none over the socket; `DIDBOT_PDS` in `--direct` mode (`direct.rs::SERVER`) | never — `every_command_this_binary_takes_is_in_its_usage` asserts no `--as` and no `did:` | none over the socket; `DIDBOT_AGENT_TOKEN` or `DIDBOT_AGENT_TOKEN_FILE` in `--direct` mode | The wrappers under `scripts/` front four of these and add spellings of their own. `dev-pds.sh` reads `DIDBOT_PDS_PORT`, `DIDBOT_ZONE`, `NAMES` and `DIDBOT_PDS_DATA` and passes them as flags; `dev-agentd.sh` reads `DIDBOT_PDS` and `DIDBOT_SOCK` and passes them as environment; `dev-swarm.sh` reads `DIDBOT_PDS_URL`, `DIDBOT_SWARM_AGENTS` and `DIDBOT_SWARM_RATE` and passes them as flags; `build-brand.sh` fixes the positional to `site/public`. `dev-profile.sh` is the seam between them: it evaluates `didbot-setup env`, so `stack.toml` reaches the others as exports. Two verb surfaces have no binary. The e-stop admin socket (`crates/didbot-serve/src/estop_admin.rs`) takes `STATUS`, `PAUSE`, `REVOKE`, `RELEASE`, `ANNOUNCE` and `NOTIFY` as upper-case lines over `socat` (`docs/deployment.md:371`). The agent daemon's socket (`crates/didbot-agentd/src/protocol.rs::Message`) takes `report`, `approve`, `decline`, `pending` and `show` as an `asks` field, and the branch adds `become` and `host` to it — the two verbs the ruling left unnamed. ## The conventions in use | convention | who does what | |---|---| | binary name | `didbot-` everywhere. The word is a role (`pds`, `agentd`), a verb (`claim`, `verify`, `setup`), a thing (`swarm`, `brand`), or a protocol (`oauth`). | | verb form | `didbot-setup` mixes bare verbs with noun-verb groups; `didbot-oauth` is bare verbs; `didbot-claim` and `didbot-brand` choose a mode with `--check`; `didbot-verify` chooses one by which flags are present; `didbot-pds`, `didbot-swarm` and `didbot-agentd` have none. | | flag form | `--kebab-case` with the value as the next word, in every binary. `--name=value` is read only by `didbot_agentd::cli::flag`. `-h` is read by six; `didbot-claim` reads no help flag and `didbot-agentd` reads nothing. `-y` (`didbot-setup`) is the only other short flag. `--version` exists in `didbot-setup` alone. | | the object | positional in `didbot-claim`, `didbot-verify`, `didbot-brand` and `didbot-oauth`'s token; a flag in `didbot-swarm` (`--pds`) and `didbot-pds` (`--zone`); `didbot-oauth show` takes its URL either way. | | the server's address | `--pds ` (`didbot-swarm`), `DIDBOT_PDS` (`didbot-agentd`, `didbot-oauth`), `DIDBOT_PDS_URL` (`didbot-setup env`, `dev-swarm.sh`), `` (`didbot-claim`), `--server ` (`didbot-verify`), `--zone ` (`didbot-pds`, its own). Six spellings for one thing. | | the DID | `--owner ` in `didbot-pds` and `didbot-verify`; positional in `didbot-verify` (agent) and `didbot-claim` (operator, or a handle); forbidden in `didbot-oauth`. | | state directory | `didbot-pds`: `--data `, memory without it. `didbot-setup`: `$XDG_CONFIG_HOME/didbot/stack.toml` and `$XDG_STATE_HOME/didbot/` (`didbot_stack::default_config_path`, `default_data_dir`). `dev-pds.sh`: `$XDG_STATE_HOME/didbot/pds`, spelled in shell. Sockets: `$XDG_RUNTIME_DIR/didbot-agent/agent.sock` (`socket::default_socket_path`), `$XDG_RUNTIME_DIR/didbot-estop/estop.sock` (`estop_admin::default_socket_path`), `$XDG_RUNTIME_DIR/didbot/-.pid` (`dev-pidfile.sh`). The other four keep nothing. | | errors | prefixed `didbot-setup: ` (`didbot-setup.rs:63`) and `didbot-oauth : ` (`fail`); bare `eprintln!` in `didbot-claim`, `didbot-verify`, `didbot-brand`; a `tracing` `error!` line in `didbot-pds`, `didbot-swarm`, `didbot-agentd`. | | bad arguments | stdout and exit `2` in `didbot-pds` and `didbot-swarm` (`println!("{message}"); ExitCode::from(2)`); stderr and exit `2` in `didbot-verify`; stderr and exit `1` in `didbot-setup`, `didbot-oauth`, `didbot-claim`, `didbot-brand`. | | `--help` | exit `0` on stdout in `didbot-setup`, `didbot-oauth`, `didbot-brand`; exit `2` on stdout in `didbot-pds` and `didbot-swarm`, because `-h` is an `Err(USAGE)`; exit `2` on stderr in `didbot-verify`, whose `-h` is `Err("what to check:")`; in `didbot-claim` it is a missing operator, exit `1`; in `didbot-agentd` it starts the daemon. | | logging | `RUST_LOG`, else a default: `didbot_serve::DEFAULT_LOG_FILTER` for `didbot-pds` and `didbot-claim` (`init_tracing`), `info` for `didbot-swarm`, `didbot_agentd=info` for `didbot-agentd`. `didbot-stack` carries its own copy of the filter (`lib.rs:43`). | ## One rule for binaries, one for verbs - [ ] **A binary is named for the seat it runs in: `didbot-`.** A seat is a machine role with its own credentials and its own threat model, and the name says whose machine the binary is on before it says anything else. Four seats exist: the server, the agent host, the agent, and the operator; plus the developer's own machine, which is a seat with nothing to protect. A name is a noun, never a verb: the verb is what comes after it. The rule is what makes `become-host` wrong twice — it is a verb-noun, and the noun is the seat the binary already is. - [ ] **A verb is one lower-case word, an imperative, naming what the binary does to the object that follows it.** A binary whose command line names an object has a verb; a binary that only takes options has none. Two kinds of object are two verbs, never one verb with a mode flag — so `--check ` is a verb, and so is a survey. A noun group (`service add`) is for a binary with several kinds of object that share verbs, and a binary that groups one groups all of them. A flag that prints and exits (`--help`, `--version`, `--list-names`) is output, not a verb, and stays a flag. - [ ] **The rename table.** Old on the left, what the two rules make of it on the right. *proposed* marks a name nothing on `main` answers to. | today | under the rules | why | |---|---|---| | `didbot-pds [flags]` | `didbot-pds [flags]` | the server's seat; one thing to do; no object | | `didbot-agentd` | `didbot-agentd serve` | the host's seat. It gains verbs the moment host identity lands, and a binary with verbs has one for its main job too | | *(socket `become`, branch)* | `didbot-agentd reserve ` — *proposed* | the server-side transition is a reservation (`ReservationPolicy`, `--pending-reservations`, the `list-reservations` route on the branch), and the verb says what the caller asks for | | *(socket `host`, branch)* | `didbot-agentd show` — *proposed* | what `show` means in `didbot-setup` and `didbot-oauth`: print what this seat is. The key, and the identity once a server has reserved one | | `didbot-oauth pending\|show\|approve\|decline` | `didbot-agent pending\|show\|approve\|decline` — *proposed* | the agent's seat. `oauth` is the protocol the answers travel over, not who is answering | | `didbot-claim ` | `didbot-operator claim ` — *proposed* | the operator's seat, and the name of the record it writes | | `didbot-claim --check ` | `didbot-operator check ` — *proposed* | a second kind of run is a second verb, as `didbot-setup check` already is | | `didbot-verify [--owner]` | `didbot-operator verify [--owner ]` — *proposed* | reads `bot.did.operator`; the question is an operator question, and it holds nothing, so sharing a binary with `claim` leaks nothing | | `didbot-verify --owner --server` | `didbot-operator survey --owner --server ` — *proposed* | the parser already refuses both at once with "check one agent, or survey a server"; the verb is the one the binary uses for itself | | `didbot-setup service add` / `profile new` | `didbot-setup service add` / `profile add` | one action, one verb | | `didbot-setup bind\|unbind\|check\|env\|show\|debug\|disk` | unchanged | the developer's seat, named for what it describes | | `didbot-swarm [flags]` | `didbot-swarm [flags]` | the developer's seat; no object | | `didbot-brand ` / `--check ` | `didbot-brand write ` / `check ` | the same object, two things done to it | The cost is in the docs: `didbot-claim` is named 27 times across `plan/`, `docs/` and the README, `didbot-oauth` 16, `didbot-verify` 8, and `docs/deployment.md` is a runbook. The harness adapter speaks the socket, not the binary, so `didbot-agent` costs nothing in the other repository. - [ ] **What is spelled once.** The server is `--server ` on every binary that takes one from the command line, and `DIDBOT_SERVER` in the environment for the two that take it there, so that `didbot-setup env` exports the same name `didbot-agentd` reads. A DID is `--owner ` when it is the operator's and a positional when it is the subject. A state directory is `--data `, and its default is `didbot_stack::default_data_dir()` — one function, which the shell wrappers call through `didbot-setup env` rather than re-spell. A socket sits in `$XDG_RUNTIME_DIR/didbot-/`, a directory the binary creates and nothing else shares, which is the invariant `socket::Listener::bind` already enforces. `--pds` names nothing but a service in `stack.toml`. - [ ] **How every binary says no.** A bad argument is one line on stderr, prefixed ` : ` when there is a verb and `: ` when there is not, exit `2`; a failure of the thing asked for is the same line, exit `1`; `--help` and `--version` go to stdout, exit `0`, and every binary has both. The `didbot-oauth` shape — `fail` in `didbot-oauth.rs` — is the one the rest adopt. ## Which crate, by seat - [ ] **A crate holds binaries for one seat, and the binary lives in the crate that owns that seat's credentials.** The server's binary stays in `didbot-serve`: that is what the `Dockerfile` builds, and the crate that holds the key handling. The host's daemon stays in `didbot-agentd`. The agent's commands move out of it: `didbot-oauth` shares a crate with the daemon because the socket exchange is the same (`cli.rs`'s own doc), but it is a second seat in a crate named for the first, and the rule puts it in `didbot-agent` with `protocol` and `cli` as its dependency. The operator's binary is a new crate, `didbot-operator`, holding one `main` over the `didbot-claim` and `didbot-verify` libraries, which stay separate: the verifier's crate description is a promise that it borrows nothing from the server's reading of itself, and that promise is a library boundary, not a binary one. `didbot-setup`, `didbot-swarm` and `didbot-brand` are the developer's, and stay where they are. - [ ] **Where the boundary is when one binary serves two.** `didbot-oauth` is run by an agent through the daemon, holding nothing, and by a token-holding host or CI job through `--direct`, holding `DIDBOT_AGENT_TOKEN`. Both act as one account, so both are the agent's seat; the boundary is which *process* holds the token, and `--direct` is the host saying there is no daemon to hold it. `didbot-setup` is a developer's tool today and [node](node.md) wants a `didbot-setup apply` on a server host: it writes the machine's own `stack.toml` and never holds a credential, so it stays one binary and the seat is "this machine". `didbot-pds` is one binary on a laptop and on the server, by the `Dockerfile`'s own account; a seat is a role, not a hostname. ## What a seventh binary must do - [ ] **Fit the rules, by a check.** A new binary is `didbot-` for a seat named above or a new one with its own threat model, written into this epic first. Its verbs are bare imperatives with the object after. It takes `--server`, `--owner`, `--data`, `--help` and `--version` as the previous item says, and prints its errors the way the item before that says. It takes no credential in `argv` and none forwarded by a dispatcher — a credential it needs it obtains for itself, the way `didbot-claim` opens a browser and `didbot-agentd` mints per context, or reads from its own seat's environment, the way `didbot-oauth --direct` reads `DIDBOT_AGENT_TOKEN`. A wrapper under `scripts/` re-spells no default the binary already has. The test that holds this is one over every `USAGE` string in the workspace, in the `didbot` crate, which already holds the conformance suites for the same reason. - [ ] **A dispatcher is a convenience over the rule, not the rule.** An earlier draft of this epic baked every surface into one `didbot` and dispatched unknown subcommands to `didbot-` on `PATH`. The seat rule argues the other way for first-party binaries: the operator's browser flow does not belong in the server's image, and the server's key handling does not belong on a laptop. What survives of that draft is the external convention — a `didbot ` that execs `didbot-` with no credential inherited, and a `didbot --list` that scans `PATH` — as a later item, once the names it would list are settled. ## Defects, as distinct from conventions Each of these is a disagreement between two places in the code, not a choice nobody made. - [ ] **`didbot-oauth approve --direct ` loses the token.** `didbot_agentd::cli::positional` treats every `--flag` without `=` as one that takes a value and skips the word after it; `--direct` takes none, and `didbot-oauth.rs:53` finds it anywhere in `argv`. Written after the token it works. No test pins the order. - [ ] **`didbot-setup bind []` is not optional.** `USAGE` brackets the profile (`didbot-setup.rs:32`); the parser refuses without one (`:656`). `do_bind(None)` is reached only through `unbind`. - [ ] **`--pds` is an address in `didbot-swarm` and a service name in `didbot-setup`.** Same flag, two types. - [ ] **`NAMES` is the one export without the prefix.** Every line `didbot-setup env` writes is `DIDBOT_*` or `RUST_LOG`, except `NAMES` (`didbot-setup.rs:488`), and `dev-pds.sh` reads it so. - [ ] **Two defaults spelled twice.** `dev-pds.sh` writes `${XDG_STATE_HOME:-$HOME/.local/state}/didbot/pds`, which is `didbot_stack::default_data_dir("pds")` in shell; `dev-agentd.sh` writes `${XDG_RUNTIME_DIR:-/tmp}/didbot-agent/agent.sock`, which is `socket::default_socket_path` in shell (`DIR_NAME`, `SOCKET_NAME`). A change to either Rust function leaves the script behind. - [ ] **`didbot-pds` is a library crate and a binary in a different crate.** `crates/didbot-pds` is the store; the binary is built with `-p didbot-serve --bin didbot-pds` (`Dockerfile:106`, `dev-pds.sh`). `cargo run -p didbot-pds` names the wrong package. - [ ] **Three runtime directories.** `didbot/` for pidfiles (`dev-pidfile.sh`), `didbot-agent/` for the agent socket, `didbot-estop/` for the e-stop socket, all under `$XDG_RUNTIME_DIR`. - [ ] **`didbot-verify --owner` plays two roles.** The owner expected, in agent mode; the owner whose agents are counted, in survey mode (`parse`, `didbot-verify.rs:139-153`). The verb split above ends it. - [ ] **`didbot-oauth show` takes its URL two ways.** Positional or `--url` (`didbot-oauth.rs:102-105`); `approve` and `decline` take a URL only as `--url`, because a bare word is a token there. - [ ] **`didbot-agentd` reads no arguments.** `main` in `didbot-agentd.rs` never looks at `argv`, so `--help` binds the socket. - [ ] **`didbot-verify -h` is an error.** `parse` returns `Err("what to check:")`, which `main` prints to stderr with exit `2`. - [ ] **The branch reaches for `didbot-stack` to spell a directory.** `claude/host-reservation` reads `DIDBOT_STATE` and falls back to `didbot_stack::default_data_dir("agentd")`, adding the developer's stack crate to the host daemon's graph for one path. The spelled-once item above is where that function should be reached from — by the wrapper, through `didbot-setup env`, not by the daemon. ## Done Nothing closed yet.