@khalidadil/dbbot-ops-cli (0.1.0)
Installation
@khalidadil:registry=npm install @khalidadil/dbbot-ops-cli@0.1.0"@khalidadil/dbbot-ops-cli": "0.1.0"About this package
@khalidadil/dbbot-ops-cli
The unified dbbot CLI (Deployment & CLI): one binary over the catalog
(read-only), the manager's operator API and local laptop key material. From a
checkout: pnpm --filter ./packages/ops-cli build, then node packages/ops-cli/dist/cli.js help.
Install (laptop)
The published package is one self-contained file (dist/dbbot.mjs, no runtime
dependencies) on the Gitea npm registry. Put the scope line in ~/.npmrc, then:
@khalidadil:registry=https://git.kadil.dev/api/packages/khalidadil/npm/
npm i -g @khalidadil/dbbot-ops-cli
It needs Node >= 24 (refused at startup otherwise), age >= 1.1 on PATH for the
escrow verbs and a Docker socket for discover; dbbot --version prints the
version. Each verb is box, laptop or host (Where each verb runs):
the same file runs in the manager image as the box CLI (bin/dbbot admin, box
verbs only).
Release
Bump version in package.json and CLI_VERSION in src/version.ts
together (a published version is never republished), then publish only through
node packages/ops-cli/scripts/publish.mjs from a pushed integration or
main (a dry run; --apply --yes publishes and verifies).
Command groups
- Discovery / status
dbbot discover [--socket <path> | --tcp host:port] [--all] [--probe] [--sqlite <project>/<service>=<path>[,...]]: list the database containers a host runs, over the dockerode-free socket-proxy client (the same classifier the agent uses). The DUMP column shows what the agent would dump (the database the image created, by the agent's own resolver) orrefused:<reason>for a site it will not back up (e.g.refused:mariadb-no-database-env); STATUS issupported,unsupported(MySQL / Percona, D2, with the reason in DETAIL) orsqlite-opt-in: each path the agent-side SQLite opt-in (--sqlite, elseDBBOT_SQLITE) names on a listed container, with the mount it maps through or why it is unmapped (no realpath: the CLI may run off-host). An invalid opt-in is a usage error. Read-only; never touches a DB.dbbot dbs/dbbot status/dbbot checkpoints <db>: read the catalog (--catalog-url <conn>/DBBOT_CATALOG_URL/DATABASE_URL). Read-only.
- Session (the manager's operator API; the manager is
--manager <url>, elseDBBOT_PUBLIC_URL)dbbot login [--manager <url>] [--email <addr>]: the manager's email-code login. The code is typed at the prompt (or read from the first line of stdin); the session is saved 0600 under$DBBOT_HOME, bound to that manager's origin, and its token is never printed.dbbot logout [--local]: revoke it on the manager and delete the file (--local: delete only).dbbot auth break-glass --email <addr> [--manager <url>] [--colocated] [--deploy-dir <dir>]: when login mail is down, on the manager's box. It runsdeploy/break-glass.sh, which mints one session inside the manager container (the CLI never holds a catalog write credential; the email must be on the operator allowlist, the session is capped at 12 h and recorded asbreak_glass). The script's stdout must be exactly one 64-hex line; anything else is "unexpected output" and is never echoed. The bearer is saved (0600, bound to--manager, on the boxhttp://127.0.0.1:8080) only afterGET /api/auth/sessionconfirms it. From a laptop:ssh <box> /path/to/deploy/break-glass.sh <addr> | dbbot auth break-glass --stdin --manager "$DBBOT_PUBLIC_URL". There is no dry run, and it is a console session only: live-data changes still need a signed intent.dbbot kek status [--candidate <copy>] | new [--from <copy>] [--replace-unusable] | recover --lost-kek <ref>[,<ref>...] [--by <who>] [--confirm <phrase>] | reissue-ca [--by <who>] [--confirm <phrase>], each with[--colocated] [--deploy-dir <dir>] [--env-file <file>]: on the manager's box, for a manager KEK lost with no escrowed copy or an enrolment CA that is missing or unusable. It runsdeploy/kek-recover.shwith the terminal attached, so the work runs in the manager image with the manager's own catalog credential (the CLI holds none);--candidate/--fromfiles reach the container on stdin only.statusis the dry run and prints the exact next command and phrase;recoverandreissue-caneed the manager stopped and that phrase (typed, or--confirm), and--bydefaults toDBBOT_OPERATOR_EMAIL. Exit 3 is status FOREIGN, 4 status CA_REISSUE (deploy/docs/scripts.md).
- Run (through the operator session; "queued" is printed only after the
manager answered 2xx)
dbbot backup <db> [--wait [--wait-timeout 90s|30m|2h]] [--idempotency-key <k>]: queue a backup now. The same key never queues a second one (a retry after a lost connection is safe); a failure saysbackup NOT queued, why, and the key.--waitpolls until the checkpoint is stored (default timeout 2h).dbbot restore <checkpoint> --db <db> [--wait ...]: restore-to-NEW. The agent names the copy when it accepts the job (the CLI never sends or invents a name);--waitends when the preview is ready.dbbot restore status <job>shows the state, the copy's name once chosen, and any error.- In-place restore is not offered: restore to a new copy, then promote it
with
dbbot intent sign --kind live-name-promoteanddbbot intent submit. dbbot intent submit <file>: hand a signedbucket-rebindorlive-name-promoteintent to the manager. Other agent kinds are refused by the manager (not executed in v1); erasure-purge intents and no-personal-data confirmations are uploaded in the console.
- Operator admin (hosts, agents and enrolment tokens; the same operator
session)
dbbot host register <label>: the manager mints the host's UUID (the identity every dbId is namespaced by) and the label is renameable; registration is always explicit.dbbot host rename <host> <new-label>keeps the UUID.dbbot host list [--record | --local]shows the manager's hosts and whether this laptop has each recorded;--recordrecords them,--locallists only this laptop's record without contacting the manager.dbbot agent list: the enrolled agents, their certificate state and health.dbbot agent token mint --host <label|uuid> [--ttl <minutes>] [--replace-existing] [--out <file>]: a single-use enrolment token bound to one registered host (30 minutes by default). It is shown once;--outwrites the agent env fragment (DBBOT_HOST_ID,DBBOT_ENROLMENT_TOKEN, the manager CA on one line, the enrol and channel endpoints) at 0600, never over an existing file, and keeps the token off the terminal. Every value of the answer is shape-checked before it reaches a file, and a token that cannot be delivered is revoked.--replace-existingre-enrols a rebuilt host onto the same UUID.dbbot agent token list [--host <h>](never the secret) anddbbot agent token revoke <token-id>(idempotent).dbbot agent retire <host> [--force] [--yes]: revokes the agent's certificate and orphans its databases (their backups are untouched). It needs--yes, or the host label typed at a terminal; unattended without--yesit exits 2 before any request. An agent that still has work in flight is refused with the database ids unless--force. A retired host re-enrols with a plainagent token mint;--replace-existingis only for a rebuilt host that is still enrolled.
- Native-deploy wrappers:
dbbot setup | up | down | update. Each runs itsdeploy/script (setup.sh,up.sh,stop.sh,update.sh) and forwards exactly the flags that script accepts:--dry-run,--colocated,--ref,--timeout,--recovery-envand--seal-catalog-cred(setup),--drain-timeoutand--volumes(down; it drops the catalog, so it needs--yes, or the project name typed at a terminal). Any other option is a usage error.--env-filebecomes the script'sDBBOT_ENV_FILE. The scripts are found beside the dbbot checkout, or at--deploy-dir/DBBOT_DEPLOY_DIR(two differentdeploy/directories in reach is refused). The exit status is the script's (updateexit 5: updated, catalog-backup unhealthy, no rollback), a signal kill is 128 + the signal, and under--jsonstdout carries only the result. These manage the manager stack on its box only: the agent composes (agent.compose.yaml, andagent-a.compose.yamlon host A) and theproxy-*.compose.yamlfiles run on each app host, not throughsetup/up. - Laptop security (only where key material lives: the laptop):
dbbot enrol [--force]: generate and persist the 0600 ed25519 signing key; prints the public key to pin at each agent.dbbot host pin --whoami <json|file> --label <name>: record a host from the--whoamiline its agent printed on the host itself. The host record ($DBBOT_HOME/hosts.json, 0600) is append-only: a UUID is never recorded under two labels, a label never names two UUIDs, and a--whoamithat disagrees with the record (another agent, an older identity generation, another key at the same generation) is refused.host registerandhost list --recordwrite it too.dbbot intent sign --kind <k> --target-host <host-uuid> [kind flags…] [--out <file>] [--yes]mints an ed25519 T2 intent (exp ≤ 15 min) the agent-transport gate accepts.--target-hostis the host's UUID (a label is refused) and must be in this laptop's host record. At a terminal the summary (kind, host and the label this laptop recorded, database, names, expiry) is shown and you confirm, or pass--yes, before anything is written; without a terminal it proceeds and the summary goes to stderr.--outis created 0600 and never overwrites a file.in-place,cross-hostandsqlite-forcetake--file-id --bucket --sha256, so the signature pins the exact blob (D15 v2).--kind erasure-purge --plan <plan.json>signs the manager-only intent for a GDPR erasure purge: erasureRequestId, dbId, targetHost and itemCount come from the downloaded plan andplanSha256 = sha256(canonicalBytes(plan)); it prints the plan summary to stderr first. Agents refuse this vocabulary.--kind pii-none-confirm --target-host <recorded host-uuid> --db-id --project --service --dbname --engine --acknowledge-irreversiblesigns the laptop's no-personal-data confirmation (a T2 attestation the manager's console ingress and the verifier check against their own pinned keys; never relayed to an agent). It refuses without the bare acknowledgement (a value after it is a UsageError), a--db-idthat is not computeDbId over the named fields, or a TTL over 15 min, and prints the statement plus "delete the file if you do not submit it": once accepted, weekly+ snapshots become compliance-locked (undeletable until expiry).
- GDPR erasure (the B2 erasure break-glass key comes only from env
DBBOT_B2_KEY_ID/DBBOT_B2_APP_KEY, never a flag, and never reaches the manager):dbbot erasure purge --plan <plan.json> --endpoint https://s3.<region>.backblazeb2.com --bucket <b> [--prefix p/] [--region r] --confirm <databaseLabel> --receipt <file> [--dry-run]: for each planned blob it lists every version and delete marker ofo/<oid>,r/<oid>,_t/<oid>and_t/<oid>.r, deletes GOVERNANCE versions with the bypass header (others without), never deletes a COMPLIANCE or legal-hold version, deletes markers last (only on keys with nothing kept), re-lists and writes a 0600 receipt to upload on the console's purge panel. Plain http is accepted only for localhost endpoints (tests).dbbot erasure sweep-hidden --endpoint <url> --bucket <b> [--prefix p/] [--min-hidden-age-hours 24] --confirm <b> [--dry-run]: for keys undero/and_t/hidden by a marker older than the threshold, deletes noncurrent versions whose retention is absent or lapsed (never with bypass), then the marker: the noncurrent-version expiry Filebot does not provision; it covers flagged rows GFS already pruned.dbbot erasure check-key: callsb2_authorize_accountand fails unless the key has listFiles, readFileRetentions, readFileLegalHolds, deleteFiles and bypassGovernance.dbbot escrow init | verify | rotate | show: the recovery escrow (Key Escrow & Recovery). No command ever prints an identity, and escrow never writes or prints the secret sets: it only proves copies of them.initgenerates each slot as a new x25519 identity file (--recoveryN-out <new file>: written exclusively at 0600 and passphrase-armored, soage -d -iprompts; the passphrase, at least 12 characters, comes from a 0600--recoveryN-passphrase-fileor is typed twice at a hidden prompt;--no-passphraseopts out) or accepts an existing one (--recoveryN-recipient <age1… | age1tag1…> --recoveryN-identity <file or device stub>). A recipient is x25519 or a hardware tag recipient (age1tag1…: age-plugin-tpm >= 1.0.0 and age-plugin-se >= 0.2.0 print one; age-plugin-yubikey will from its first release after 0.5.1), accepted with the device plugin's identity stub, recorded hardware-bound, and proven by decrypting through the device (the plugin's touch or PIN prompts are relayed to stderr). At least one slot stays x25519 with an offline copy. Post-quantum, plugin and SSH recipients are refused before anything is written, and--allow-nonx25519-recipientis retired. It re-reads every file from disk and proves each slot (a canary the Go age binary seals to the slot opens in-process; one sealed only to the other slot does not) and an agent-shaped[DEK, R1, R2]blob, then links the public record ($DBBOT_HOME/escrow-record.json, or--record) into place: the single commit point, which never replaces a record that appeared meanwhile. It refuses to run without a plan for both slots, to re-mint over an existing record, and to overwrite any file. RECOVERY2 is--recovery2-custody hardware-held(its armored file on offline media plus paper):initandrotatewarn, without refusing, when RECOVERY2 is recorded as software custody, when an identity file is not passphrase-armored, and when an accepted file's passphrase is under 12 characters, each naming a fix the CLI accepts; they also warn for each hardware-bound slot (no paper copy).verifyre-proves the record at any time, one custodian at a time:--agent-env <dotenv>(only the two recipient keys are read),--recovery1-identityand/or--recovery2-identity <file>, and--blob <file.age>to drill a real sealed checkpoint (streamed, nothing written). It also proves the custody copies, each against a public value only:--manager-kek <copy>with--kek-recipient <age1…>(the "manager KEK recipient" setup.sh prints) or, at cold start,--kek-seal-ref <16 hex>(theseal_refin the catalog dump); the copy must be the bare identity line the manager loads, and an armored copy prints itsage -d -o manager_kekrestore step.--verifier-identity <copy> --verifier-recipient <VERIFIER_PUBLIC_IDENTITY>proves the verifier copy (warning when it is not armored). Either is refused if it is a recovery, retired or (given the other recipient) the other key.--laptop-key <copy>checks the escrowed laptop signing key against the live keystore's public key, or--laptop-public-key <key>(the DBBOT_TRUSTED_LAPTOP_KEYS value); it needs no record, and naming the live keystore itself is refused. One set per run: a WRITE/COMMAND copy (--manager-kek,--laptop-key) is never loaded beside a READ copy (--recoveryN-identity,--verifier-identity,--blob). Any failed check exits 1.rotate --slot Nreplaces one slot (new-slot flags as forinit) after proving the surviving one (--recoveryM-identity, required), committing by renaming over the record it started from; the old entry is retired in the record and its file is left untouched.showprints the public record and the twoDBBOT_RECOVERY{1,2}_RECIPIENTlines every agent seals to.init,rotateandshowtake--out-env <new file>to write exactly those two lines, and print the custody checklist rendered from key-escrow's escrow sets: the READ set, the separate WRITE/COMMAND set (with the manager KEK copy), what is not escrowed and what is kept with both, each item with its proof.
dbbot names check --file <f>: validates a local dbId↔name JSON file and reports every problem (nothing is synced: the manager stores no friendly names; rename a host withhost rename).
- Fleet rollout plan (print-only):
dbbot rollout --dry-run --manager-image <img@sha256:…> --host-b-agent-image … --host-b-socket-proxy-image … --host-a-agent-image … --host-a-socket-proxy-image …prints the ordered compose updates (manager, host B canary, host A). Every image must be digest-pinned (a moving tag exits 2 and prints no plan); nothing is deployed. - Project mapping: which Filebot project backs up each Dokploy compose
project, over the manager's operator API (
--manager <url>, elseDBBOT_PUBLIC_URL;https://, orhttp://only on loopback). A Dokploy project is owned by its active link, else by the one live project whose Filebot name equals it exactly.dbbot project list: LINKS, PROJECTS (how each is matched, and why one backs up nothing) and UNMATCHED DOKPLOY PROJECTS, each reason with a one-line hint. Faults (!) come first; the rows under not backed up (no enabled Filebot project or link) are informational: agents watch every Dokploy project, and an app whose Filebot project has database backups off is expected there.dbbot project link <filebot-project> <dokploy-project>: the Filebot project by Filebot id, DBBot id or exact name; the Dokploy project is the nameproject listshows unmatched (thecom.docker.compose.projectlabel its databases report). A link adopts only databases not yet backed up under another project and prints every adopted dbId; one already backed up elsewhere stays there (no re-homing in v1, so there is no--move).dbbot project unlink <dokploy-project>: retires the link and never stops a backup: its databases stay pinned to, and backed up under, the project; new ones fall back to exact-name matching. To correct a wrong link, unlink it and link the right project.- Login: the first run at a terminal asks for the operator email (or
--email, orDBBOT_OPERATOR_EMAIL) and the emailed code, then saves the session at$DBBOT_HOME/session.json(0600 in a 0700 directory) bound to that manager origin; the token is never printed. Off a terminal it fails with no request; a 401 deletes the session.--jsonprints the API body. Exit codes: 0 ok, 1 refused or failed (with the reason and request id), 2 usage.
Design notes
- Reads, never writes, the catalog.
dbs/status/checkpointsopen a read-only view; catalog owns the schema, the manager owns writes. Project links change only through the manager's operator API, which holds the locks, adopts and records who linked. - The agent never live-swaps on manager authority alone. Destructive /
trust-widening verbs (
in-place,live-name-promote,cross-host,bucket-rebind,re-wrap,un-enrol,sqlite-force) are ed25519-signed T2 intents minted here on the laptop; the private key never leaves it.intent sign --kind re-wraptakes an x25519--new-verifier-recipient(the verifier unwrapsdek.ageunattended, in process). - No identity in any output. Every byte the binary prints passes
src/render.ts: a result whose data or lines carry an age identity (native, post-quantum or plugin) is refused in--jsonand text mode alike, and identities in error messages are masked before stderr. - The library surface (
@khalidadil/dbbot-ops-cli) exports every command's logic so it is testable without a subprocess (seetest/);test/cli-binary.test.tsalso runs the builtdist/cli.jsitself.
Depends on protocol, key-escrow (escrow record, identity files, proofs, the
output guard), filebot-client, catalog (also the reason hints),
discovery-core and agent-lifecycle (rollout planning, retire). Tests cross-check
agent-transport's real authorization gate, envelope-crypto's real agent
sealing (Go age, [DEK, R1, R2]) and a real manager (test/support/manager.ts:
session, run, admin, break-glass and project verbs; an agent enrols with a minted
token); test/cli.subprocess.test.ts runs the shipped binary.