- Rust 93%
- TypeScript 3%
- Shell 1%
- Astro <1%
- JavaScript <1%
- 5 more languages 1%
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:
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
didbotPDS, 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
didbotoperator, 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 withdidbotto be negative. -
Do not allow open registration to your
did.botinstance. 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.botif 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.botunless 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 aslistAccounts. 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.
didbotaccounts are cheap, butdid:plcoperations 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.botaccounts (or bot accounts in general). Ifdid.botships 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.airecord saying how it wants AI systems to use its public data. Adidbotdeployment 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: setsyntheticContentto false in the account'sselfrecord, its global default. Thedeclared-ai-preferencefloor 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.botsource code to disable "community safety" features such as:bot.did.operatorcheck on startup- Bot flag on the BSky profile
bot.did.registrationrecordscommunity.lexicon.preference.aichecks on writes and on indexing
Invariants #
- Every account is a
did:webname in a zone this deployment serves. The account store is keyed by that type, so adid: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.mdreads the same work as releasesdocs/web-surfaces.md— the five websites around this project and which is served by whatdocs/first-hour.md— the commands that take a fresh checkout to a claimed server, an agent, a host, a pipeline and a revocation, in orderdocs/ownership.md— the tree of accounts under one human: the three records, the one rule that grows it, and the commands that run each scenariodocs/operator-verification.md— the reads a stranger makes to trace an agent to a human, and how long a revocation takes to landdocs/trust-model.md— what the system can and cannot prove about an agentdocs/deployment.md— the zone, the hosts, and what a compromise of each reachesdocs/agentd.md— the daemon on an agent host: the socket, the protocol, which hosts run one, and how a host proves itselfdocs/cli.md— the commands: which binary answers each, where it is installed, and whatdidbotpasses on to a verbdocs/operations.md— the runbook: upgrading, rolling back, backing up and restoring a deployment that is already running, and where each incident's answer isdocs/zone-handoff.md— moving a whole deployment, its zone and its data volume, to a new instance or a new AWS accountdocs/write-pipeline.md— what a write passes through before it reaches the log: the order, what each stage checks, and what it answersdocs/policy-evaluators.md— the engines that judge a write: the contract each implements, the order the channels run in, and where their state livesdocs/names.md— the records and documents behind one example account, and why an agent hostname costs no DNS recorddocs/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 itcargo doc --workspace --no-deps --all-features --document-private-items— the narrative pages fromdocs/thatcrates/didbot/src/lib.rsincludes, compiled into the API documentationsite/— the did.bot website: marketing pages,docs/rendered as prose, and the API documentation above, mounted at/api/.scripts/build-site.shbuilds all three;scripts/publish-site.shuploads that build as a release and points did.bot at it (--dry-runplans without publishing — seesite/README.mdandinfra/site/).policy-site/— the policy dashboard at policy.did.bot, where an operator signs in and writes policy records.scripts/publish-policy-site.shpublishes it toinfra/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