# didbot _identities for entities_ *Current release: none* [did.bot](https://did.bot) is an identity provider for AI agents and other machine entities. Unlike classic IdPs, each identity is a _fully featured_ [ATproto account](https://atproto.com/specs/account), backed by [W3C Decentralized Identifiers (DIDs)](https://www.w3.org/TR/did/). This means that your AI agents (or cloud servers, or CI jobs, etc.) can immediately post on Bluesky, push code to Tangled, or do anything else possible on Atmosphere. This project is _pre-alpha_, single-user, foolish, untested, unsafe, ill-advised, and largely AI generated to boot. No license or warranty is provided. There are no stability or data loss guarantees. Federation is not tested. Lexicons are not yet published. Abandon hope, all ye who enter. What our agents are doing: [![vibescrobble](https://vibescrobble.com/badge/permadeath.com.svg?repo=tangled.org/permadeath.com/didbot)](https://vibescrobble.com/@permadeath.com) ## Features The `didbot` project aims to provide the following features: - Identity provisioning for agents and other machine entities in the form of a unique, stable, and globally visible ATproto account - Compatibility with a wide range of ATproto apps, from social media to file sharing - Publicly recorded attestations from human operators to agents and vice versa - Operator dashboard, including "E-Stop" capability to immediately pause agent tokens - Policies for limiting agent capabilities on the ATproto network, including: - Allow- or deny-list specific ATproto apps (OAuth clients) - Allow- or deny-list specific app permissions (OAuth scopes) - Immutable or banned record types (e.g., prevent agents from modifying their profiles, or writing posts) - Key and repository management for large numbers (>10k) of ephemeral ATproto accounts, without using public/ecosystem resources - Provide integrated DNS, TLS, and HTTP services for `did:web` - Provide ATproto labeler services for managed accounts - Maintain internal audit trails separate from repo contents ## Being a good netizen The goal of `didbot` is to make the internet better _for humans_. Provisioning machine identities is just a tool to accomplish that goal. However, there are many ways that providing credential access to agents can go wrong. Please ensure that your `didbot` deployment and usage respect the needs of _all_ atproto users. To that end, here is a non-exhaustive list of recommendations for a respectful `didbot` deployment: - Do not register any human accounts on a `didbot` PDS, ever. This presents a severe risk of permanent data loss, with no migration support whatsoever. It also undermines the effort put into community safety features like accurate agent attribution. - Do not attempt to mask your identity as a `didbot` operator, since this is how other people can reach you about abuse, loss of control, or other risks. Turning on DNS privacy is fine, obfuscating your own DID is not. - Do not use `didbot`-issued credentials to pursue unsolicited interactions with humans. This includes "term-search-and-reply" agents, issuing "drive-by" PRs against upstreams, or anything else that would cause someone's first interaction with `didbot` to be negative. - Do not allow open registration to your `did.bot` instance. Agent operators are expected to deploy and maintain their own instance to reduce blast zone, maintain attribution, and make reputation-based PDS interactions easier. - Do not operate `did.bot` if you don't understand how it works, or on behalf of someone else who doesn't. This project is intended for advanced users only. - Do not operate `did.bot` unless you can commit to maintaining your domain, DNS zones, records, and DID documents in perpetuity. Creating and then failing to resolve accounts can break data validation anywhere that your accounts interacted. - Do not block access to public `bot.did.*` endpoints such as `listAccounts`. Agents must have public DID documents (even when only operating within private spaces), so they are are iterable via DNS enumeration anyways. - Do not consume atproto ecosystem resources. `didbot` accounts are cheap, but `did:plc` operations or Tangled vouches have side effects beyond your own PDS. Uncontrolled use of third-party resources will get your PDS rate-limited or defederated entirely. - Do not attempt to bypass apps that have opted out of supporting `did.bot` accounts (or bot accounts in general). If `did.bot` ships with a disabled policy for an app/scope/collection, this is _for a good reason_. - Do not override an account's declared AI preferences. An atproto account can publish a `community.lexicon.preference.ai` record saying how it wants AI systems to use its public data. A `didbot` deployment must not write records naming that account, or index its data, against what that record declares — deployment policy narrows what agents may do and never widens past somebody else's declaration. A deployment should also publish how to be left alone by all of its agents at once, rather than making somebody work it out: set `syntheticContent` to false in the account's `self` record, its global default. The `declared-ai-preference` floor reads that record alone, so it is the record to point people to. That setting also applies to every other AI system that honours the record. [plan/ai-preference.md](plan/ai-preference.md) holds the design. - Do not attempt to modify the `did.bot` source code to disable "community safety" features such as: - `bot.did.operator` check on startup - Bot flag on the BSky profile - `bot.did.registration` records - `community.lexicon.preference.ai` checks on writes and on indexing ## Invariants - Every account is a `did:web` name in a zone this deployment serves. The account store is keyed by that type, so a `did:plc` — or a name from anyone else's zone — has no way in. Resolving other people's DIDs is a different thing and stays. See [plan/complete/account-lifecycle.md](plan/complete/account-lifecycle.md). ## Documentation - `plan/` — what is built and what is not, one file per capability, with the reasoning beside the work; `plan/milestones.md` reads the same work as releases - `docs/web-surfaces.md` — the five websites around this project and which is served by what - `docs/first-hour.md` — the commands that take a fresh checkout to a claimed server, an agent, a host, a pipeline and a revocation, in order - `docs/ownership.md` — the tree of accounts under one human: the three records, the one rule that grows it, and the commands that run each scenario - `docs/operator-verification.md` — the reads a stranger makes to trace an agent to a human, and how long a revocation takes to land - `docs/trust-model.md` — what the system can and cannot prove about an agent - `docs/deployment.md` — the zone, the hosts, and what a compromise of each reaches - `docs/sidecar.md` — the host granting didbot agents collaborator access on Tangled: its Lightsail stack, first apply and publishing the image - `docs/agentd.md` — the daemon on an agent host: the socket, the protocol, which hosts run one, and how a host proves itself - `docs/cli.md` — the commands: which binary answers each, where it is installed, and what `didbot` passes on to a verb - `docs/operations.md` — the runbook: upgrading, rolling back, backing up and restoring a deployment that is already running, and where each incident's answer is - `docs/zone-handoff.md` — moving a whole deployment, its zone and its data volume, to a new instance or a new AWS account - `docs/write-pipeline.md` — what a write passes through before it reaches the log: the order, what each stage checks, and what it answers - `docs/policy-evaluators.md` — the engines that judge a write: the contract each implements, the order the channels run in, and where their state lives - `docs/names.md` — the records and documents behind one example account, and why an agent hostname costs no DNS record - `docs/testing.md` — every pre-launch check, in the three tiers it runs in, each with the command that runs it and the test that covers it - `cargo doc --workspace --no-deps --all-features --document-private-items` — the narrative pages from `docs/` that `crates/didbot/src/lib.rs` includes, compiled into the API documentation - `site/` — the did.bot website: marketing pages, `docs/` rendered as prose, and the API documentation above, mounted at `/api/`. `scripts/build-site.sh` builds all three; `scripts/publish-site.sh` uploads that build as a release and points [did.bot](https://did.bot) at it (`--dry-run` plans without publishing — see [`site/README.md`](site/README.md) and `infra/site/`). - `policy-site/` — the policy dashboard at policy.did.bot, where an operator signs in and writes policy records. `scripts/publish-policy-site.sh` publishes it to `infra/policy-site/`. ## CLI reference A person types `didbot `, and `didbot` runs the binary that answers that verb. [docs/cli.md](docs/cli.md), also at [did.bot/docs/cli](https://did.bot/docs/cli/), says which binary runs where and how they fit together. Each block below is one binary's own `--help`, written by `scripts/gen-cli-reference.sh`. Edit the help text in the code, not here.
didbot ### `didbot` ```text Runs `didbot-` from PATH with the rest of the command line, and nothing else: no credential from this shell reaches the verb. Usage: didbot [OPTIONS] didbot Options: --list Print every didbot-* binary on PATH, with its version -h, --help Print help (see a summary with '-h') -V, --version Print version Verbs: operate claim a server, admit an account beneath you, or check either [didbot-operator] login sign in as a deployment's operator [didbot-operator] estop read or set a deployment's emergency stop [didbot-operator] account read or act on the accounts a deployment hosts [didbot-operator] app read what OAuth applications hold and did, or end their logins [didbot-operator] announce ask the relay to crawl a deployment [didbot-operator] oauth answer sign-ins as an agent: pending, show, approve, decline [didbot-oauth] register make this machine an account, with a key that never leaves it [didbot-register] didbot- from PATH, with the rest of the command line `didbot help ` is `didbot --help`. ```
didbot-operator ### `didbot-operator` ```text Runs on the operator's own machine. `operate` signs in to your own atproto account and writes the claim that makes a server yours; `login` signs in as a deployment's operator, and `estop`, `account`, `app` and `announce` are the dashboard's controls from a shell, behind that same sign-in. Usage: didbot [OPTIONS] Commands: operate Claim a server as yours, admit an account beneath you, or check either login Sign in as a deployment's operator estop Read or set the deployment's emergency stop account Act on one account this deployment hosts app Read what OAuth applications hold and did here, or end one's logins announce Ask the configured relays to crawl the deployment help Print this message or the help of the given subcommand(s) Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') -V, --version Print version ``` ### `didbot operate` ```text Signs in to your own atproto account (a browser opens for that account's consent screen, once; the session is kept for the next run) and writes bot.did.operator into your repository naming . A server's own hostname is the top edge: the command waits for the server to answer over TLS and checks its identity first. Any other name is an account beneath a server your repository already names, chosen by the name's suffix or with --server. The server creates the account when asked with the operator session `didbot login` kept for it; with none kept, or one the server no longer accepts, the command signs in the way `didbot login` does before writing anything. A key `didbot register` parked under the name is admitted by the fingerprint it printed, given as --fingerprint or pasted when asked, and becomes the account's; otherwise say what it is with --kind, and --oidc if a token from an issuer is how it will sign in. The server writes both as the account's bot.did.credential records. --creates lets the account create beneath itself and --creates-beneath lets everything beneath it create; each names the kinds allowed, or any kind with none named. Re-running against the same name updates the record in place. --list reads the records back from a deployment, the way its dashboard shows them. Usage: didbot operate [OPTIONS] [NAME] [OPERATOR] Arguments: [NAME] A server's hostname, or an account's [OPERATOR] Your own atproto account, as a handle or a did:. Not needed with --check Options: --check Only check: no sign-in, no browser, nothing written. A server is checked for claimability; an account's ownership is walked up to you. Exits 1 until it passes --server The deployment to act on, as its own hostname --json Print the answer as JSON --list List your bot.did.operator records as --server last read them, with what logs in as each account it holds. Carries the session `didbot login` kept --limit How many rows to print, from 1 to 500; the server's default is 50 --cursor Print the page after the one that printed this cursor --revoke Delete your bot.did.operator record for , which pauses it and everything beneath it after the deployment's grace window (six hours by default). `didbot account lock ` stops an account now, and `didbot estop --pause` a server --kind What an account is: service, host, pipeline or agent. A parked key's kind wins --oidc ... The OpenID Connect identity the account is: an issuer, then claim=value pairs --fingerprint The fingerprint `didbot register` printed on the other machine. Admits the key parked with it without asking, whatever else is parked under the name --creates [...] Let the account create these kinds beneath itself, or any kind with none named [possible values: service, host, pipeline, agent] --creates-beneath [...] Let every account beneath this one create these kinds, or any kind with none named [possible values: service, host, pipeline, agent] --no-browser Print the sign-in's URL and open no browser -h, --help Print help (see a summary with '-h') ``` ### `didbot login` ```text Opens the sign-in in your default browser, and prints its URL. No browser opens with --no-browser, with no DISPLAY or WAYLAND_DISPLAY, or over SSH. The sign-in happens on your own atproto account and comes back to a loopback address this command listens on; the deployment mints the session, and it is kept for operate, estop, account and announce. Usage: didbot login [OPTIONS] Options: --no-browser Print the sign-in's URL and open no browser --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot estop` ```text With no setting, reports what the stop is doing and what it has refused. One setting at a time changes it. Either setting refuses every new service-auth token and every proxied call, but one already minted works at the service it names until its exp, at most an hour. Usage: didbot estop [OPTIONS] Options: --pause Refuse every token and all provisioning until released. Ends nothing --server The deployment to act on, as its own hostname --json Print the answer as JSON --revoke Refuse the same and end every token. Releasing does not bring them back --release Clear the stop -h, --help Print help (see a summary with '-h') ``` ### `didbot account` ```text Each verb carries the session `didbot login` kept to a /dashboard/api/accounts route, or for credentials to bot.did.updateCredentials, and what it does lands in that account's ledger with this deployment's operator as the actor. is the account's handle or its did:. verify-export reads files and needs no session. Usage: didbot account [OPTIONS] Commands: list List every account this deployment holds show Print one account: its records, its ledger and what logs in as it lock Refuse the account's writes, leaving its repository readable unlock Lift the lock `account lock` put on the account lift-quarantine Lift the quarantine a policy put on the account delete Delete an account, keeping its DID document; --hard takes the document too [alias: erase] export Write the account's data into a directory, for its next host verify-export Check an export using only its files revoke-tokens End every token that authenticates as the account, its app logins included, leaving its state and locks as they are credentials Print what logs in as the account, or change it help Print this message or the help of the given subcommand(s) Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot account list` ```text Lists every account this deployment holds, whatever its [disclosure] table opens to the public: its kind, parent, records, whether it ever wrote one, its last commit, and its state. An account wrote when it holds a record outside the collections the server writes itself, or when the write log, kept 30 days by default, has a write from it. Usage: didbot account list [OPTIONS] Options: --name Only the account with this did:, or whose handle or DID holds this text --server The deployment to act on, as its own hostname --json Print the answer as JSON --kind Only accounts of this kind [possible values: service, host, pipeline, agent] --under Only accounts beneath this one, at any depth: its handle, or its did: --idle-for Only accounts whose last commit, or creation, is at least this long ago, such as 7d or 12h. A --never-wrote delete takes 24h unless given, and the server refuses less than 1h unless its operator.never_wrote_min_idle_secs setting says otherwise --never-wrote Only accounts that never wrote a record of their own --limit How many rows to print, from 1 to 500; the server's default is 50 --cursor Print the page after the one that printed this cursor -h, --help Print help (see a summary with '-h') ``` ### `didbot account show` ```text Print one account: its records, its ledger and what logs in as it Usage: didbot account show [OPTIONS] Arguments: The account's handle, or its did: Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help ``` ### `didbot account lock` ```text Locks and every account beneath it as this operator: the server refuses their writes and still serves their repositories. `account unlock` lifts the lock. Usage: didbot account lock [OPTIONS] Arguments: The account's handle, or its did: Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot account unlock` ```text Lift the lock `account lock` put on the account Usage: didbot account unlock [OPTIONS] Arguments: The account's handle, or its did: Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help ``` ### `didbot account lift-quarantine` ```text A policy's freeze verdict quarantines an account: the server refuses its writes and still serves its repository. Only an operator can lift it, and the account's ledger names the operator who did. Usage: didbot account lift-quarantine [OPTIONS] Arguments: The account's handle, or its did: Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot account delete` ```text Deletes . By default it keeps the DID document: repository, history, blobs and credentials: erased DID document: still served, so the account's past signatures still check account row: kept as decommissioned, and still counted against [capacity] max_accounts name (the DID's hostname, which is also its handle): never issued again With --hard it takes the row too: DID document: no longer served, so the DID stops resolving ledger: kept name: held for [names] hold_days, 30 by default, then issued again An account with accounts beneath it is refused; --recursive takes them too, deepest first. With --never-wrote and no , lists every account that never wrote a record of its own and is idle for --idle-for, narrowed by --kind and --under, and deletes nothing. It prints a digest of the accounts it lists; --confirm deletes them, and refuses when the listing, or --hard, has changed since. The server checks each account again as it deletes it, with its writes held off, and keeps one that has since written or committed. An account with accounts beneath it is kept, and so is a host unless --kind host is given. Usage: didbot account delete [OPTIONS] [NAME] Arguments: [NAME] The account's handle, or its did: Options: --recursive Take everything beneath it too, deepest first --server The deployment to act on, as its own hostname --hard Take the account row and the DID document too, so the DID stops resolving --json Print the answer as JSON --never-wrote Refuse unless every account taken never wrote a record of its own and is idle for --idle-for. With no , every such account the filters name --kind Only accounts of this kind [possible values: service, host, pipeline, agent] --under Only accounts beneath this one, at any depth: its handle, or its did: --idle-for Only accounts whose last commit, or creation, is at least this long ago, such as 7d or 12h. A --never-wrote delete takes 24h unless given, and the server refuses less than 1h unless its operator.never_wrote_min_idle_secs setting says otherwise --dry-run List what would be deleted and delete nothing; the default with no --confirm Delete what --never-wrote with no listed, given the digest it printed -h, --help Print help (see a summary with '-h') ``` ### `didbot account export` ```text Writes 's data into --out, which must be new or empty: its repository as repo.car, every blob its records name under blobs/, its DID document as did.json, and manifest.json with its handle, head commit, revision, counts, the export time and the server's revision. The server takes all of it from one snapshot of the repository and streams it, and what arrives is checked as verify-export checks it. The account's signing key stays on the server. With --all, every account this deployment holds, one at a time, each into a directory beneath --out named by its host. An account whose repository the server does not serve, because it is erased, suspended or deactivated, is listed with the server's reason. Usage: didbot account export [OPTIONS] --out [NAME] Arguments: [NAME] The account's handle, or its did: Options: --all Every account this deployment holds --server The deployment to act on, as its own hostname --json Print the answer as JSON --out The directory to write into -h, --help Print help (see a summary with '-h') ``` ### `didbot account verify-export` ```text Checks the export in , or every export directly beneath it as `export --all` writes them, with nothing but their files. Every block and blob must hash to its name, the commit must verify against the #atproto key in did.json, did.json must claim the manifest's handle, and the manifest must agree with the repository. It prints the key each export verified against: compare it with the DID document the account's name resolves to. Usage: didbot account verify-export [OPTIONS] Arguments: An export, or a directory of them Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot account revoke-tokens` ```text End every token that authenticates as the account, its app logins included, leaving its state and locks as they are Usage: didbot account revoke-tokens [OPTIONS] Arguments: The account's handle, or its did: Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help ``` ### `didbot account credentials` ```text Prints the account's bot.did.credential records. With --key, --fingerprint or --oidc, and --remove, it adds a credential, removes one, or both in one commit, through bot.did.updateCredentials with the session `didbot login` kept. A change ends the account's sessions. Usage: didbot account credentials [OPTIONS] Arguments: The account's handle, or its did: Options: --key Add this public key, multibase --server The deployment to act on, as its own hostname --fingerprint Add the key `didbot register` parked under the account's name with this fingerprint --json Print the answer as JSON --oidc ... Add this OpenID Connect identity: an issuer, then claim=value pairs --remove Remove the credential record with this record key -h, --help Print help (see a summary with '-h') ``` ### `didbot app` ```text One call to a /dashboard/api/apps route, carrying the session `didbot login` kept. An application is named by its client_id, the URL its metadata document is served at. Usage: didbot app [OPTIONS] Commands: logins List every live login, with what it may do now and whether policy admits it refusals List what policy refused, and what a scope ceiling cut back writes List every record written or deleted, and which application wrote it end-logins End every login the application holds, here or on one account help Print this message or the help of the given subcommand(s) Options: --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot app logins` ```text List every live login, with what it may do now and whether policy admits it Usage: didbot app logins [OPTIONS] Options: --client Only this application, by its client_id --server The deployment to act on, as its own hostname --account Only this account, by handle or did: --json Print the answer as JSON --limit How many rows to print, from 1 to 500; the server's default is 50 --cursor Print the page after the one that printed this cursor -h, --help Print help ``` ### `didbot app refusals` ```text Lists the evaluation log: policy refusals at sign-in, at token issue and at the write, writes outside a token's scope, and requests a scope ceiling cut back. Repeats of one refusal are one line. The log keeps rows for [capacity] evaluation_log_retention_days. Usage: didbot app refusals [OPTIONS] Options: --client Only this application, by its client_id --server The deployment to act on, as its own hostname --account Only this account, by handle or did: --json Print the answer as JSON --since Only rows from this long ago or later, such as 7d or 12h --limit How many rows to print, from 1 to 500; the server's default is 50 --cursor Print the page after the one that printed this cursor -h, --help Print help (see a summary with '-h') ``` ### `didbot app writes` ```text Lists the write log: every record a caller wrote or deleted, with the application whose token wrote it, or `-` for an account writing with its own credential. The log keeps rows for [capacity] write_log_retention_days. Usage: didbot app writes [OPTIONS] Options: --client Only this application, by its client_id --server The deployment to act on, as its own hostname --account Only this account, by handle or did: --json Print the answer as JSON --since Only rows from this long ago or later, such as 7d or 12h --limit How many rows to print, from 1 to 500; the server's default is 50 --cursor Print the page after the one that printed this cursor -h, --help Print help (see a summary with '-h') ``` ### `didbot app end-logins` ```text Ends every grant family the application holds and drops its unredeemed authorization codes and pending consents, so nothing in flight becomes a token afterwards. The application stays as allowed as it was and may sign in again: refusing it is a denyClient policy, written from the policy dashboard, and a separate thing. Usage: didbot app end-logins [OPTIONS] --client Options: --client The application's client_id --server The deployment to act on, as its own hostname --account One account, by handle or did:. Omitted ends the application's logins everywhere --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ``` ### `didbot announce` ```text Asks every relay the deployment names, with didbot-pds --relay-hostname or the [relay] table, to crawl it over com.atproto.sync.requestCrawl. A deployment under .localhost is never announced, since no relay can reach it. Usage: didbot announce [OPTIONS] Options: --resume Call the deprecated notifyOfUpdate, which upstream still names for resuming after a stream gap --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') ```
didbot-register ### `didbot-register` ```text Mints a key here and parks its public half at the server, then prints the fingerprint and the `didbot operate ` to run on your own machine. Once that has admitted the key, this side signs in with it and keeps the session where didbot-oauth reads one. With --token-file or --token-stdin, the token is the parent: the account is created now beneath --under, whose identities the token must match, with the fresh key as its first credential. With --rotate, the account already exists: its key is replaced by a new one in one change the current key signs, and a JWT the old key signed is refused from then on. A host's key is the daemon's, so a running daemon is asked to do it. A key the account no longer holds is parked for the operator to admit, and this waits for that as a registration does. The server is --server or DIDBOT_PDS, and must be under it. Usage: didbot register [OPTIONS] Arguments: What the account is, for policy and reporting [possible values: service, host, pipeline, agent] The account's hostname, which its did:web is minted from Options: --under The parent the token authenticates as. Required with a token --token-file A file holding an OIDC ID token for the parent's issuer, with the server's DID as audience --token-stdin Read that token from stdin instead --rotate Replace the key the existing account logs in with --timeout How long to wait for `didbot operate` on the other machine, in seconds. The server forgets a parked key on its own clock; sixty to a few thousand is sensible [default: 600] --server The deployment to act on, as its own hostname --json Print the answer as JSON -h, --help Print help (see a summary with '-h') -V, --version Print version ```
didbot-oauth ### `didbot-oauth` ```text didbot oauth pending sign-ins an agent here reported seeing didbot oauth pending --all and the ones none did, each marked didbot oauth approve let one of them in didbot oauth decline [--reason WHY] turn one of them down When a sign-in has not reached you -- no hook saw the client print its URL -- name it by that URL instead: didbot oauth show look one up and print it didbot oauth approve --url let that one in didbot oauth decline --url [--reason WHY] Nothing signs in until you choose, and a request nobody answers expires on its own. A token names one request: it is single use and it is not an account. With no daemon on this host -- a CI job that provisioned itself, or a host handed a token by an operator -- set DIDBOT_PDS and DIDBOT_ACCOUNT_TOKEN (or DIDBOT_ACCOUNT_TOKEN_FILE) and the same commands talk to that server as that one account, where pending lists every sign-in waiting for it. --direct insists on it rather than trying the socket first. Run the binary by name, didbot-oauth, for that: `didbot oauth` hands nothing from the environment's credentials to what it runs. That mode also takes: didbot oauth browser print a one-time link that signs the browser opening it in as the account A web app's code goes only to a browser signed in as the account. A daemon hands the link to the harness's adapter when a browser tool is about to open the sign-in page the daemon named with your identity, for the adapter to put in the tool's input. A printed link signs in whichever browser opens it first, so open it at once, in your own browser. An account whose credentials name an OpenID Connect identity signs in with a token from that issuer instead, at the server DIDBOT_PDS names: didbot oauth --token-file --account a session as NAME from the token in that file, then the command didbot oauth --token-stdin --account the same, with the token read from stdin NAME is the account's hostname or its DID. The server checks the token against that account's identities alone, and refuses a token that names no account. A token is never taken on the command line itself: an argument is visible to every process on the machine. didbot oauth --help print this didbot oauth --version print the version ```
didbot-agentd ### `didbot-agentd` ```text usage: didbot-agentd Runs the agent daemon until it is stopped. It takes no options; the environment is the whole configuration. DIDBOT_PDS the server that mints accounts. Required. DIDBOT_PDS_RETRY_SECS how long creating an account waits out a server that is restarting, 0 to 120 seconds (default 30). DIDBOT_SOCK the unix socket to listen on (default: $XDG_RUNTIME_DIR/didbot-agent/agent.sock). DIDBOT_STATE the directory the host's key and its context bindings are kept in (default: $XDG_STATE_HOME/didbot/agentd, or ~/.local/state/didbot/agentd). DIDBOT_POOL the server's pool each agent is named in, by the server (default: the daemon names each agent under the server's own zone). DIDBOT_CONTEXT_TTL_DAYS how long a context keeps its account after the harness last reported it, 1 to 365 (default 30). DIDBOT_KEY_ROTATE_DAYS replace the host's key once its credential record is this many days old, checked hourly, 1 to 365. Unset, the key is replaced only by `didbot register --rotate host`. DIDBOT_SUBAGENT_OPERATOR where a subagent's account is created: `agent`, as its session and beneath it, or `host`, with the host's key and beneath the host (default agent). RUST_LOG the tracing filter (default didbot_agentd=info). -h, --help print this -V, --version print the version ```
didbot-pds ### `didbot-pds` ```text usage: didbot-pds [options] --config a sectioned TOML file to read settings from -- see didbot-config and plan/config.md. This binary reads [server], [zone], [tls], [blobs], [capacity], [names], [park], [tree], [oidc], [sessions], [credential], [intervals], [disclosure], [operator], [oauth] and [relay]. An unknown key anywhere in the file refuses startup. A flag always wins over the file for the same setting. [server] takes bind and port. bind is the only way to narrow what the listener answers on: unset binds [::] and answers on every address this host holds, both families, while 127.0.0.1 reaches only callers on this host. port is 443, the default, or 1024 to 65535 behind a proxy. It also takes how a stop goes: stop_checkpoint_secs (0 to 120, default 30) bounds the checkpoint a stop takes while still serving, 0 taking none, and drain_secs (1 to 60, default 10) how long it then waits for requests in flight. [zone] takes zones, the same list --zone builds, whose first entry is the primary; operator, the same DID --operator names; and pds_endpoint, the URL agents are told to reach this server at, derived from the primary zone when unset. Passing --zone replaces the file's whole list rather than adding to it. [[pool]], 1 to 20 of them, each takes name, one label or one of its zones; zones, 1 to 20 of [zone] zones, filled in order; labels, one * with label text around it, such as ci-* (default *); and names, in the spellings [names] spec takes, where a braced part such as {repo} is filled by each create (default [names] spec, or mineral+creature when that is hostname). With none, each zone is a pool named after itself. [names] takes spec and fallback, in the spellings --names and --names-if-down take, number_encoding, in the spellings --number-encoding takes, and hold_days: 1 to 365, how long a released name stays taken (default 30). A day covers whatever still has the name cached; a year suits a deployment where a name is an identity somebody wrote down, at the cost of running a small word list dry. [tls] takes acme_environment and contact, mirroring --acme-environment and --acme-contact. [park] takes ttl_seconds, rate_limit and rate_window_seconds, mirroring the --park-key-* flags. [tree] takes max_depth (1 to 64, default 10), max_children (1 to 1000000, default 10000) and creates_per_hour (1 to 1000000, default 100): the shape one creator may give the ownership tree, whatever policy says. Those three are re-read from this file while the server runs, so a cap that is refusing work is raised without a restart. reserved_labels lists the labels no account may take directly under a zone (default: the hostnames this server answers at itself, such as www and xrpc; an empty list reserves nothing). [oidc] takes issuers, the OpenID Connect issuers whose tokens are accepted (empty admits any public HTTPS issuer), and freshness_secs (60 to 3600, default 300). [sessions] takes proof_ttl_secs (300 to 86400, default 3600) and live_per_account (1 to 64, default 8). [credential] takes agent_token_ttl_days, 1 to 3650 (default 365): how long a freshly issued agent token is good for. There is no rotation route for it, so an account whose token expires is provisioned again. And replace_interval_secs, 10 to 3600 (default 60): the shortest time between two of an account's own credential replaces. [operator] takes session_ttl_secs, mirroring --operator-session-ttl, and grace_window_hours, 1 to 168 (default 6): how long an operator this server cannot reach keeps it writing. Short, and an operator whose own PDS is down for an afternoon takes these accounts down too; long, and an operator who has stood down leaves them writing under a vouch nobody is behind. Only a running server counts it: one restarted during the outage refuses writes and creates until the operator's PDS answers. It also takes dns_resolver, the DNS-over-HTTPS resolver the operator's handle is checked through: an https:// endpoint that answers the JSON form, default https://cloudflare-dns.com/dns-query. Any other value refuses startup. And never_wrote_min_idle_secs (default 3600, reasonably 3600 to 604800): the shortest idleFor a never-wrote account delete takes. Below an hour, a delete can take an agent whose session has yet to write. [proxy] bounds each account's atproto-proxy calls: calls_per_minute (1 to 6000, default 60) and burst_calls (1 to 1000, default 60) are its token bucket, in_flight_per_account (1 to 64, default 8) how many may be open at once, in_flight_per_tree (1 to 512, default 64) how many one account tree may have open, in_flight_total (1 to 512, default 128) how many every account together may have open, and target_cache_secs (10 to 3600, default 300) and target_cache_entries (16 to 100000, default 1024) how long and how many targets' endpoints are kept. app_view, a did#service such as did:web:api.bsky.app#bsky_appview, is where an app.bsky.* call with no atproto-proxy header goes; unset, such a call is MethodNotImplemented. [intervals] takes the background loops' periods in seconds: blob_collect_secs (300 to 86400, default 3600), confinement_poll_secs (60 to 3600, default 300), health_secs (5 to 300, default 10) and tree_reload_secs (5 to 3600, default 30). They are set apart because they load different things -- the confinement poll spends a DNS provider's rate limit, blob collection walks this server's disk, health only reads memory, and the tree reload re-reads this file. [oauth] takes scope_ceiling, an enumeration of scope atoms no grant may exceed (it can only narrow what this build already allows; an atom outside that is dropped and logged), and pending_per_account / pending_total, how many authorization decisions one account may have awaiting an answer, and how many this server holds at once, counting an answered one until its request expires. pending_total defaults to max_accounts * pending_per_account (floored at 512) when [capacity] max_accounts is set, and to 512 outright when it is not; a pending_total configured below what max_accounts could need is kept as written but warned about at startup. It also takes dns_resolver, the DNS-over-HTTPS resolver an include: scope's permission set and a resolveHandle lookup are found through: an https:// endpoint that answers the JSON form, default https://cloudflare-dns.com/dns-query. Any other value refuses startup. And it takes browser_session_ttl_secs, 300 to 86400 (default 43200): how long a browser stays signed in as an account. And dpop_proofs_per_account, 100 to 100000 (default 2000): how many DPoP proofs one app's DPoP key may hold in the replay window at once; past it that key alone is refused. [blobs] also takes total_quota_bytes, what every account's blobs may come to together, in bytes, past which an upload is refused with StorageFull. No flag sets it. Unset, only each account's quota bounds them. One account's quota to half the data volume is the range worth staying inside. [capacity] also takes log_budget_bytes, the same byte budget --log-budget sets, past which a write is refused with StorageFull rather than at whatever size the volume turns out to be. [capacity] also takes max_records_per_account, the most records one repository may hold (1000 to 100000, default 50000). A write that would add records past it is refused with TooManyRecords; updates, deletes and the server's own records are not. A value outside the range refuses startup. [capacity] also takes commit_reserve_per_op, the bytes of a commit's blocks counted for each write's tree nodes (1000 to 9000, default 5000). A write whose records and that reserve pass 2,000,000 bytes is refused with CommitTooLarge. A value outside the range refuses startup. [capacity] also takes checkpoint_log_bytes and checkpoint_interval_secs: the running server checkpoints and trims its log once the log reaches the first, and once the second passes with anything written. The watermark defaults to half the log budget (1 GiB with none); 128 MiB to three quarters of the budget suits it, and startup refuses one under 1 MiB or at the budget. The interval defaults to 86400, and startup refuses one outside 3600 to 604800. [capacity] also takes evaluation_log_retention_days and write_log_retention_days: how long a sealed segment of the policy evaluation log, and of the write log, survives before it is trimmed (default 30 each). Denials are attacker-driven volume and attributed writes grow with real use, so the two are set apart: 7 to 90 days suits the first, 30 to 365 the second. [capacity] also takes firehose_replay_frames and firehose_replay_bytes: how many frames subscribeRepos keeps in memory for a consumer to resume from, and how many encoded bytes they may come to (default 4096 and 67108864, 64 MiB). The oldest frames are evicted when either is reached; frames an open stream has not sent are kept up to twice both. Startup refuses frames outside 256 to 65536 and bytes outside 16777216 to 1073741824 (16 MiB to 1 GiB); keep bytes at or under a sixteenth of the machine's memory. [capacity] also takes sync_reads_per_minute (60 to 60000, default 600) and sync_reads_in_flight_per_address (1 to 32, default 8): how many com.atproto.sync reads one caller address may make a minute, and have in flight at once. One more is refused with a 429. getRecord and listRecords spend the same budget. [capacity] also takes list_records_page_bytes (1048576 to 33554432, default 4194304): how many bytes of JSON one listRecords page may come to before it ends early with a cursor. [capacity] also takes blob_reads_in_flight_per_address (1 to 32, default 8) and blob_send_timeout_secs (5 to 300, default 30): how many getBlob reads one caller address may have in flight, and how long a body waits for its client to take the next chunk before it ends short. [capacity] also takes blob_send_min_bytes_per_sec (4096 to 1048576, default 32768): how fast a getBlob body must average. A body gets blob_send_timeout_secs plus its length at this rate, then ends short. [capacity] also takes history_trail_entries (1000 to 10000000, default 100000): how many commit revisions every repository's trail may hold together in memory; past it the longest trail gives up its oldest revision first. [capacity] also takes foreign_lookups_per_request (16 to 1024, default 64): how many records one write, or one applyWrites batch, may fetch from other servers for a policy about the accounts it names. [capacity] also takes firehose_streams and firehose_streams_per_address: how many subscribeRepos streams this server holds open, and how many one caller address may hold (default 512 and 8). One more is refused with a 429. Startup refuses streams outside 16 to 4096, per address outside 1 to 64, and per address above the total. [capacity] also takes firehose_send_timeout_secs and firehose_ping_secs (5 to 300, default 30 each): how long one frame may take to go onto a subscribeRepos consumer's socket, and how often a stream pings its consumer. A consumer past the first, or silent by the next ping, is closed and its slot given back. [writes] meters each account's writes with two token buckets, one key each: ops_per_hour (600 to 360000, default 3600) and ops_burst (200 to 100000, default 1000) count operations, one per record written or deleted and one per blob uploaded; bytes_per_hour (8 MiB to 4 GiB, default 64 MiB) and bytes_burst (2 MiB to 1 GiB, default 8 MiB) count request body bytes. A write over either is refused with RateLimitExceeded. A value outside its range refuses startup. --port listen port (default 443). A did:web carries no port, so an account on this server resolves only when it answers on 443; see docs/running-locally.md for binding it without root. Under a .localhost zone the server answers only callers on this host; any other zone answers everybody. --zone zone accounts are minted under (default agents.localhost). May be given more than once, 1 to 20: the first is the primary -- the server's own hostname -- and every zone mints accounts one label under it and gets its own DNS backend and, under --tls acme, its own certificate pair. --operator the operator DID every account this run mints names as answerable for it (default did:web:operator.invalid, which resolves to nothing; required under --tls acme). Outside .localhost, the server polls this DID's repository for its bot.did.operator claim and policy records. --names how to name agents. `hostname` (the default) uses the account's own hostname. A spec of word lists joined with `+` draws a part from each, in order: `creature`, `mineral+creature`, `temper+colour+creature`. A part is a bundled list or a path to a .txt/.csv of your own, so `chaos+./our-words.txt` mixes both. Also `toml:` for a template with literal text in it, and `cmd:` to ask a program for each name. Two parts are not lists. `n` is this deployment's monotonic counter, so `mineral+creature+n` gives `basalt-kestrel-4813`. A spec naming it has no finite pool and never collides with itself, and the label discloses how many accounts this deployment has ever minted. One counter feeds every template and every fallback, it is durable with --data, and a spec may name it once. `t` is the clock in whole seconds, so `mineral+creature+t` gives `basalt-kestrel-1790000000` and the label discloses when each account was minted, to the second. Two names minted in one second carry the same stamp, so a spec with `t` and no `n` still retries; `mineral+creature+t+n` carries both and never repeats. A spec may name `t` once. Generated forms, for a pool bigger than any word list: `counter` (the same counter alone, labelled `agent-4813`; `n` on its own means this), `timestamp` (the clock alone, labelled `t1790000000`; `t` on its own means this), `uuid` and `uuid-compact` (2^122 names, discloses nothing), `random32:`/`random36:` (len characters, discloses nothing). See `didbot_name::generated`'s docs for the exact pool size and what each one leaks. --list-names print the bundled word lists and what is in them --avatar