Identities for entities did.bot
atproto agent llm did

README.md

didbot #

identities for entities

did.bot is an identity provider for AI agents and other machine entities.

Unlike classic IdPs, each identity is a fully featured ATproto account, backed by W3C Decentralized Identifiers (DIDs). 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

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 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.

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/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 at it (--dry-run plans without publishing — see 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 <verb>, and didbot runs the binary that answers that verb. docs/cli.md, also at 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 #

Runs `didbot-<verb>` from PATH with the rest of the command line, and nothing else: no credential
from this shell reaches the verb.

Usage: didbot [OPTIONS]
       didbot <COMMAND>

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]
  <other>   didbot-<other> from PATH, with the rest of the command line

`didbot help <verb>` is `didbot <verb> --help`.
didbot-operator

didbot-operator #

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] <COMMAND>

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 <HOSTNAME>
          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 #

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 <NAME>.

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 <HOSTNAME>
          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 <N>
          How many rows to print, from 1 to 500; the server's default is 50

      --cursor <CURSOR>
          Print the page after the one that printed this cursor

      --revoke
          Delete your bot.did.operator record for <NAME>, which pauses it and everything beneath it
          after the deployment's grace window (six hours by default). `didbot account lock <NAME>`
          stops an account now, and `didbot estop --pause` a server

      --kind <KIND>
          What an account is: service, host, pipeline or agent. A parked key's kind wins

      --oidc <ISSUER> <CLAIM=VALUE>...
          The OpenID Connect identity the account is: an issuer, then claim=value pairs

      --fingerprint <SHA256:...>
          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 [<KIND>...]
          Let the account create these kinds beneath itself, or any kind with none named

          [possible values: service, host, pipeline, agent]

      --creates-beneath [<KIND>...]
          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 #

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 <HOSTNAME>
          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 #

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 <HOSTNAME>
          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 #

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. <NAME> is the account's handle or its did:. verify-export reads
files and needs no session.

Usage: didbot account [OPTIONS] <COMMAND>

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 <HOSTNAME>
          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 #

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 <TEXT>
          Only the account with this did:, or whose handle or DID holds this text

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --json
          Print the answer as JSON

      --kind <KIND>
          Only accounts of this kind

          [possible values: service, host, pipeline, agent]

      --under <NAME>
          Only accounts beneath this one, at any depth: its handle, or its did:

      --idle-for <DURATION>
          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 <N>
          How many rows to print, from 1 to 500; the server's default is 50

      --cursor <CURSOR>
          Print the page after the one that printed this cursor

  -h, --help
          Print help (see a summary with '-h')

didbot account show #

Print one account: its records, its ledger and what logs in as it

Usage: didbot account show [OPTIONS] <NAME>

Arguments:
  <NAME>
          The account's handle, or its did:

Options:
      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --json
          Print the answer as JSON

  -h, --help
          Print help

didbot account lock #

Locks <NAME> 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] <NAME>

Arguments:
  <NAME>
          The account's handle, or its did:

Options:
      --server <HOSTNAME>
          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 #

Lift the lock `account lock` put on the account

Usage: didbot account unlock [OPTIONS] <NAME>

Arguments:
  <NAME>
          The account's handle, or its did:

Options:
      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --json
          Print the answer as JSON

  -h, --help
          Print help

didbot account lift-quarantine #

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] <NAME>

Arguments:
  <NAME>
          The account's handle, or its did:

Options:
      --server <HOSTNAME>
          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 #

Deletes <NAME>. 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 <NAME>, 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 <DIGEST> 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 <HOSTNAME>
          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 <NAME>, every such account the filters name

      --kind <KIND>
          Only accounts of this kind

          [possible values: service, host, pipeline, agent]

      --under <NAME>
          Only accounts beneath this one, at any depth: its handle, or its did:

      --idle-for <DURATION>
          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 <NAME>

      --confirm <DIGEST>
          Delete what --never-wrote with no <NAME> listed, given the digest it printed

  -h, --help
          Print help (see a summary with '-h')

didbot account export #

Writes <NAME>'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 <DIR> [NAME]

Arguments:
  [NAME]
          The account's handle, or its did:

Options:
      --all
          Every account this deployment holds

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --json
          Print the answer as JSON

      --out <DIR>
          The directory to write into

  -h, --help
          Print help (see a summary with '-h')

didbot account verify-export #

Checks the export in <DIR>, 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] <DIR>

Arguments:
  <DIR>
          An export, or a directory of them

Options:
      --server <HOSTNAME>
          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 #

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] <NAME>

Arguments:
  <NAME>
          The account's handle, or its did:

Options:
      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --json
          Print the answer as JSON

  -h, --help
          Print help

didbot account credentials #

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] <NAME>

Arguments:
  <NAME>
          The account's handle, or its did:

Options:
      --key <MULTIBASE>
          Add this public key, multibase

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --fingerprint <SHA256:...>
          Add the key `didbot register` parked under the account's name with this fingerprint

      --json
          Print the answer as JSON

      --oidc <ISSUER> <CLAIM=VALUE>...
          Add this OpenID Connect identity: an issuer, then claim=value pairs

      --remove <RKEY>
          Remove the credential record with this record key

  -h, --help
          Print help (see a summary with '-h')

didbot app #

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] <COMMAND>

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 <HOSTNAME>
          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 #

List every live login, with what it may do now and whether policy admits it

Usage: didbot app logins [OPTIONS]

Options:
      --client <CLIENT_ID>
          Only this application, by its client_id

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --account <NAME>
          Only this account, by handle or did:

      --json
          Print the answer as JSON

      --limit <N>
          How many rows to print, from 1 to 500; the server's default is 50

      --cursor <CURSOR>
          Print the page after the one that printed this cursor

  -h, --help
          Print help

didbot app refusals #

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 <CLIENT_ID>
          Only this application, by its client_id

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --account <NAME>
          Only this account, by handle or did:

      --json
          Print the answer as JSON

      --since <DURATION>
          Only rows from this long ago or later, such as 7d or 12h

      --limit <N>
          How many rows to print, from 1 to 500; the server's default is 50

      --cursor <CURSOR>
          Print the page after the one that printed this cursor

  -h, --help
          Print help (see a summary with '-h')

didbot app writes #

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 <CLIENT_ID>
          Only this application, by its client_id

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --account <NAME>
          Only this account, by handle or did:

      --json
          Print the answer as JSON

      --since <DURATION>
          Only rows from this long ago or later, such as 7d or 12h

      --limit <N>
          How many rows to print, from 1 to 500; the server's default is 50

      --cursor <CURSOR>
          Print the page after the one that printed this cursor

  -h, --help
          Print help (see a summary with '-h')

didbot app end-logins #

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 <CLIENT_ID>

Options:
      --client <CLIENT_ID>
          The application's client_id

      --server <HOSTNAME>
          The deployment to act on, as its own hostname

      --account <NAME>
          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 #

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 <HOSTNAME>
          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 #

Mints a key here and parks its public half at the server, then prints the fingerprint and the
`didbot operate <NAME>` 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 <HOSTNAME> or DIDBOT_PDS, and <NAME> must be under it.

Usage: didbot register [OPTIONS] <KIND> <NAME>

Arguments:
  <KIND>
          What the account is, for policy and reporting

          [possible values: service, host, pipeline, agent]

  <NAME>
          The account's hostname, which its did:web is minted from

Options:
      --under <PARENT>
          The parent the token authenticates as. Required with a token

      --token-file <PATH>
          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 <SECONDS>
          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 <HOSTNAME>
          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 #

didbot oauth pending                    sign-ins an agent here reported seeing
didbot oauth pending --all              and the ones none did, each marked
didbot oauth approve <token>            let one of them in
didbot oauth decline <token> [--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 <url>                 look one up and print it
didbot oauth approve --url <url>        let that one in
didbot oauth decline --url <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 <command> --token-file <path> --account <NAME>
                                            a session as NAME from the token in
                                            that file, then the command
didbot oauth <command> --token-stdin --account <NAME>
                                            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 #

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 #

usage: didbot-pds [options]

  --config <path>
                  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 <n>      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 <host>   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 <did>
                  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>   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:<path>` for
                  a template with literal text in it, and `cmd:<program
                  args...>` 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:<len>`/`random36:<len>` (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 <style>
                  draw every account's avatar in one style. By default the
                  DID picks one, which is what a deployment gets; naming one
                  is for looking at them side by side.
  --list-avatars  print the avatar styles and what they draw
  --names-if-down <how>
                  a second naming to fall back on when the first will not
                  answer, in the same forms as --names. Off unless asked
                  for: a deployment whose namer has been broken for a week
                  should not look like one whose namer works.
  --number-encoding <enc>
                  how a number in a label is written -- the counter, the
                  clock, and both generated forms: `decimal` (the default)
                  or `base32`, lowercase Crockford, which fits the whole
                  range in 13 characters instead of 20
  --name-hold <d> days a released name stays taken (default 30)
  --data <dir>    keep accounts, records and name holds in a write-ahead log
                  under <dir>, so a restart resumes instead of starting
                  over. Without it everything lives in memory and a restart
                  is a factory reset. Blob bytes go in `blobs/` beside it.
  --log-budget <n>
                  refuse writes once the log passes <n> bytes, so a full
                  volume is a deployment that says StorageFull rather than
                  one that discovers the size of its disk. Unset by default;
                  a failed append is refused cleanly with or without it.
  --park-key-ttl <seconds>
                  how long bot.did.parkKey holds a key for the operator to
                  admit (default 600; 60 to 3600 is the range worth staying
                  inside).
  --park-key-rate-limit <n>
                  how many parkKey and describeParkedKey calls one caller
                  address may make per window (default 6; 1 to 1000).
  --park-key-rate-window <seconds>
                  the window that limit is measured over (default 600; 1 to
                  86400).
  --max-blob <n>  largest single blob, in bytes (default 5242880)
  --blob-quota <n>
                  what one account's blobs may come to, in bytes
                  (default 67108864). An agent in a retry loop is otherwise
                  a full disk.
  --blob-collection-grace <hours>
                  how long an upload nothing references is kept before it
                  may be collected (default 6). Below the atproto floor of
                  1 hour is refused at startup rather than accepted and
                  quietly non-conformant -- see
                  <https://atproto.com/guides/blob-lifecycle>.
  --max-accounts <n>
                  how many accounts this deployment may hold before
                  bot.did.createAccount refuses to mint another (default
                  1000; 1 to 100000 is the range worth staying inside, and
                  the default name pool runs short near 11000 -- see
                  plan/capacity.md).
  --operator-session-ttl <seconds>
                  how long a signed-in operator stays signed in (default
                  43200, twelve hours; 300 to 86400 is the range worth
                  staying inside -- see [operator] in the config docs).
  --close-disclosure <routes>
                  close one or more disclosure routes: a comma-separated
                  list drawn from `list-accounts`, `list-account-ledgers`,
                  `get-account-ledger`, `stats`. Every
                  one defaults open —
                  this deployment's own roster of accounts and what happened
                  to them is published by default, on purpose, so a
                  stranger can audit it — and this flag only ever narrows
                  that; there is no flag that widens past public. A closed
                  route is closed to everyone: there is no credential that
                  reaches past the toggle, so reopening it is the only way
                  back.
  --trust-forwarded-headers
                  trust `X-Forwarded-For`/`X-Real-IP` as the caller's
                  address for the `createAccount`, `par` and `authorize`
                  rate limiters, instead of the real TCP peer address. The
                  rightmost `X-Forwarded-For` element is read, which is the
                  one the proxy appended. Off by default: only turn this on
                  once a real reverse proxy sits in front of this server and
                  sets those headers itself.
  --tls <mode>    where this run's certificate comes from. `local` signs
                  it with an authority kept under --tls-ca, for a --zone
                  under .localhost that no public authority can issue for;
                  the path to trust is printed at startup. `acme` obtains
                  and renews one over ACME DNS-01 against a Route53-backed
                  zone, and needs --data, a real --zone, --route53-zone-id,
                  --operator and a binary built with `--features route53`.
                  Unset takes the only mode the primary --zone may use, so
                  this is worth passing only to be explicit. This server
                  always terminates TLS.
  --tls-ca <dir>  where `--tls local` keeps its certificate authority
                  (default $XDG_STATE_HOME/didbot/local-ca, or
                  ~/.local/state/didbot/local-ca). One directory per
                  machine: a developer trusts the certificate in it once,
                  and every local server signing from it is then reachable.
  --acme-environment <staging|production>
                  which ACME directory `--tls acme` requests against
                  (default staging -- untrusted certificates, generous
                  rate limits). Production has hard weekly rate limits;
                  do not iterate against it.
  --acme-contact <uri>
                  a contact URI (`mailto:ops@example.com`) for the ACME
                  account `--tls acme` creates. May be given more than
                  once. Optional.
  --route53-zone-id <zone>=<id> | <id>
                  the Route53 hosted zone id a zone's records -- `--tls
                  acme`'s DNS-01 challenge TXT records and the apex CAA
                  record -- are written into. A bare <id> names the primary
                  zone; repeat the flag with <zone>=<id> for each further
                  --zone. Agent hostnames cost no record: the deployment's
                  wildcard record (terraform-aws-didbot-pds/route53.tf) answers for every
                  one of them. This server is handed the zones it may write
                  and never creates one. Credentials come from this
                  instance's IAM role over IMDSv2, not this flag.
  --relay-hostname <host>
                  a relay that `didbot announce` and the dashboard ask to
                  crawl this server, over com.atproto.sync.requestCrawl, or
                  notifyOfUpdate with `didbot announce --resume`. Repeat the
                  flag for each relay; every one is asked. No default:
                  nothing here vendors a relay address. Naming a relay here does not
                  announce anything by itself -- an operator triggers that
                  explicitly, and never for a --zone under .localhost, which
                  no relay can reach. When each account first reaches a
                  relay is the file's [relay] announce: on-first-write, the
                  default, at its first write, or on-create, at activation.
  -h, --help      this text
  -V, --version   the version