Skip to content

Latest commit

 

History

History
224 lines (192 loc) · 19.4 KB

cli.md

File metadata and controls

224 lines (192 loc) · 19.4 KB

The choir CLI and the node's HTTP API

The complete surface, rendered from one table shared with the CLI's --help, AGENTS.md, /llms.txt and /api/schema. A staleness test fails the gate when they differ.

Authentication and exit codes

Auth on the CLI is flags, not env:

choir --auth-file ~/.choir/auth --auth-user choir <command> ...

Exit codes: 0 accepted, 1 rejected (JSON body printed, see ERRORS.md), 2 usage.

The signed-operation API is the primary agent path: it carries actor identity and batches many operations behind one durability barrier. git push remains the compatibility and bulk-transfer path.

Signed-operation CLI and API (primary agent path)

HTTP endpoints

Endpoint Purpose
POST /api/submit Submit one signed operation (hex payload, hex signature)
POST /api/submit-batch Same, in array order; the primary path for agent workloads
GET /api/view?limit=N&offset=M The materialized view plus the latest ref-state attestation, key bindings, T2 review outcomes, T3 concentration, T4 newcomer harm, view growth, the build commit, and the sequencer's p99 against the 100 ms gate; on a seed, replica says whose copy it is and how far behind. Under an ACL you get your own slice; node-wide sections need a node-wide grant, and a missing repository is one you were not granted. Map-shaped sections are bounded: limit rows (200 default, 1000 max), offset, <section>_omitted, and paging.next
POST /api/appeal Record an appeal for a rejected newcomer attempt; requests operator adjudication and never changes privilege
GET /api/log?from=N Ordered log entries, the catch-up and sync primitive. Absolute from; evicted entries are served from the persisted log (source says which), and a node that cannot reach back answers 409. Each entry carries hash, parent and author signature; SYNC.md is the verification procedure
GET /api/signers This node's public key and every key it trusts, with the channel a key is bound to, versioned: what SYNC.md's authorship check needs, since the log names a key's id and never the key. Behind the same node-wide read grant as /api/log
GET /api/witness On a seed: its signed statements that it folded the home's attestations, the latest and the last 64, versioned. Compare one with the home's snapshot by ancestry in the prev_snapshot chain (SYNC.md, Seeds); neither an ancestor of the other is a fork. 404 on a home
POST /api/workspace Provision a CoW workspace; optional exact base/change binding makes retries idempotent
POST /api/workspace/archive Recoverably archive a change-bound workspace and remove it from the active view
GET /api/reviews?reviewer=X One actor's pending review queue
GET /api/schema This surface, machine-readable and versioned, plus what this node will accept; the description an agent generates a client from (D17)
GET /api/search?q=X&in=code&repo=owner/name&rev=R&limit=N Search repository contents, file names or commit messages across every repository you may read, each at HEAD; rev needs a single repo. Ungranted repositories are absent, or answered as nonexistent by name. Unindexed (git grep): limit bounds the results, matches counts everything, truncated says which
GET /api/profile?channel=X One actor's standing out of the view you may see: bound keys and their age, changes owned, reviews assigned and verdicts given, approvals slashed, checks reported, and vouches with direction. Two callers with different grants get different numbers. No score; time-locked grants (D66) live outside the log and are not counted
GET /llms.txt This surface, as text, for an agent that has never seen choir
GET /sync.md The sync contract: cursor semantics and how to verify a page's hash chain and author signatures
GET /api/repos Which repositories this credential can see, read from the filesystem; narrowed says whether an ACL was applied
POST /api/repo Create a repository on a running node with the pre-receive hook that sequences its pushes; needs a node-wide write grant; appends nothing to the log
GET /api/ref-agreement Where the op log and the bare repos disagree about a ref, read-only
POST /api/accounts/invite Mint a single-use, expiring invite and the grants it will hold; needs a node-wide write grant and can never issue one; a grant may carry until=<unix seconds> (D66)
POST /api/accounts/redeem Redeem an invite, presented as the credential, for a token, once, and register an ssh key
POST /api/accounts/request/grant Answer an access request (D72): turns it into an invite under the id and secret the asker already holds
POST /api/accounts/request/decline Drop a pending access request; their link then reads as never valid
POST /api/accounts/revoke Delete an account: its token stops authenticating on the next request, and its grants and keys go with it
GET /api/accounts Who holds an account, what they were granted, and which invites are outstanding; never a secret or its hash
POST /api/git-update Internal: the pre-receive hook callback
POST /api/git-abort Internal: retracts a refused push's already-accepted refs

The choir CLI

getting started

  • choir host [--domain <name> | --public [--ip <addr>] | --public-name <name>] [--port <n>] [--repo <owner/name.git>] [--invite <name>] [--state <dir>] [--yes] [--dry-run] [--foreground] [-- <daemon flags>]
    take this machine from nothing to a running node and print its URL; bare binds loopback, --domain issues a Let's Encrypt certificate for a name you own, --public uses a magic-DNS name over this box's address, --foreground execs the daemon instead of installing a unit
  • choir init [<state-dir>] [--port <n>] [--force]
    set up a node's layout on this machine: repository root, credential at 0600, an actor key the node trusts, and .choir/config; refuses to overwrite what exists
  • choir key <key-file> [name]
    mint a key and print the line the operator registers; pass your channel name to print the bound form
  • choir git-credential <auth-file> [--auth-user <name>] get|store|erase
    git credential helper: hands git your token on stdin so it never lives in a remote URL; configure with git config credential.helper '!choir git-credential <auth-file>'
  • choir join <link> | <api> <invite-file> <key-file> [--user <name>] [--channel <name>] [--key-file <path>] [--ssh-key <path>] [--token-file <path>] [--no-clone]
    redeem an invite link and set this machine up: actor key at ~/.choir/agent.key, token at ~/.choir/auth (0600), a git credential helper for that node, the node URL in ~/.choir/config, and a clone of each repository the invite names in the current directory (--no-clone skips it); the same link run again on this machine clones only what is missing; --user names the account when the invite left it open, asked on the terminal otherwise; the three-argument form takes the invite from a file, answers JSON and touches neither git nor your home directory
  • choir docs [--open]
    build the book from docs/ with the API documentation inside it at book/api/; needs a checkout and mdbook, and names the install command if it is missing
  • choir skill install [--into <dir>]
    install the choir agent skill (default .claude/skills), rendered from this binary's own surface table; re-run after upgrading

changing code

  • choir workspace <api> <owner/repo> <name> [--base <git-oid> --owner <channel> --key-file <path> --change <id> --idempotency-key <key>] [--path <prefix>]...
    provision a CoW workspace; advanced flags owner-sign an exact base and stable change, and each --path owner-signs a subtree
  • choir checkpoint <api> <key-file> <channel> <change-id> <workspace-id> <git-oid>
    publish an immutable change revision after committing and pushing its git object
  • choir propose [reviewer]... [--key-file <path>] [--channel <name>] [--api <url>] [--repo <owner/repo>] [--remote <name>] [--onto <branch>] [--change <id>] [--path <prefix>]...
    create a change, push its commits and request review, with no arguments; run from a git checkout, with the key and channel from ~/.choir, every value overridable by flag; re-running after an amend updates the same proposal; a leading <key-file> <channel> pair is still accepted
  • choir workspace-archive <api> <key-file> <channel> <owner/repo> <name> <change-id> <idempotency-key>
    owner-sign and recoverably archive a bound workspace; exact retries are idempotent
  • choir submit <api> <key-file> <channel> '<op-json>'
    sign and submit one raw operation
  • choir batch <api> <key-file> <channel> <ops-file>
    sign and submit many operations as one batch, the primary path for agent workloads; one op per line, - reads stdin, one result line per op
  • choir intent <api> <key-file> <channel> <subject> <kind> '<body>'
    publish a task spec or plan so other agents can see intent
  • choir state <api> <channel>
    list what you owe and what you are waiting on; every row carries the command that answers it and its risk

review

  • choir review <api> <key-file> <channel> <id> <git-oid> [--ref <repo:ref>] [reviewer]...
    request review on a commit; name no reviewers and the node draws them
  • choir verdict <api> <key-file> <reviewer> <id> approve|request-changes [note]
    answer a review you were assigned
  • choir comment <api> <key-file> <channel> <review-id> <comment-id> '<body>'
    say something on a review; append-only, and the comment id is your retry identity
  • choir viewed <api> <key-file> <viewer> <review-id>
    record that you read a review; first read only, resubmitting is refused
  • choir slash <api> <node-key-file> <id> <reviewer> '<reason>'
    invalidate one reviewer's approval; operator-only and never moves a ref
  • choir abandon <api> <node-key-file> <id>
    archive a stale incomplete review as lapsed, settling it unapproved; operator-only and never moves a ref
  • choir reviews <api> <reviewer>
    your pending review queue

checks

  • choir check <api> <key-file> <channel> <git-oid> <name> passed|failed|running|errored [evidence] [--ref <repo:ref>]
    report one automated check's outcome on a commit; any runner or person can report by signing, and the node never runs the check
  • choir checks <api> <git-oid>
    every check reported on a commit, and one verdict; exits 0 passed, 1 failed or unreported, 3 still running, 4 could not be run

trust

  • choir witness <api> <key-file> <channel>
    cosign the node's current ref-state attestation (D67); the snapshot id is read from the view, and the node may not witness its own
  • choir vouch <api> <key-file> <channel> <subject> [note]
    vouch for another operator; both ends need a key bound in the log, and it authorizes nothing on its own
  • choir unvouch <api> <key-file> <channel> <subject> '<reason>'
    withdraw a vouch; both ops stay in the log, and vouching again starts a fresh clock

reading the node

  • choir schema <api>
    print this node's machine-readable API description and its live capabilities
  • choir log <api> [--from <n>] [--verify] [--keys <file>]
    read log entries from a cursor; --verify checks continuity, recomputes every hash and verifies the signatures whose keys you hold
  • choir appeal <api> <attempt-id>
    appeal a rejected newcomer attempt for operator adjudication; never grants privilege
  • choir profile <api> <channel>
    what the log records about one actor: keys and their age, changes owned, verdicts given, checks reported
  • choir search <api> <term> [--in files|code|commits] [--repo owner/name] [--rev R] [--limit N]
    find a literal term across every repository you may read
  • choir triage <api>
    every review and change in a bucket (landed, awaiting verdicts, changes requested, approved awaiting landing), most actionable first, capped, with truncation marked in-band
  • choir funnel <api>
    the contribution funnel from admission to first verdict and the steepest drop between stages; counts what this credential may read, and reports an unmeasured stage as null
  • choir view <api> [--limit <n>] [--offset <n>]
    read the materialized view, its ref-state attestation and the node's health counters; map-shaped sections page 200 rows at a time, with <section>_omitted and paging.next

operating a node

  • choir invite <api> <name> <owner/repo> [read|write]
    mint an invite and print the one link to send; the same thing the /people page does
  • choir asks <api>
    who has asked for access and is waiting on an answer (D72)
  • choir grant <api> <request-id> <owner/repo> [read|write]
    let one of them in; the link they already hold becomes their invite
  • choir decline <api> <request-id>
    drop a pending request; their link then reads as never valid
  • choir runner <config-file>
    drive one workspace lifecycle step for an orchestrator; JSON request on stdin, JSON result on stdout
  • choir bind <api> <node-key-file> <operator> <key-hex> [channel]
    record in the log that a key belongs to an operator; operator-only and never moves a ref
  • choir revoke <api> <node-key-file> <key-hex> '<reason>'
    withdraw a key binding; terminal, and the attribution row survives
  • choir acl render <api> <acl-file>
    rewrite an ACL file's trailing comments to name the person behind each handle; grants are copied through unchanged
  • choir repo create <api> <owner/repo.git>
    create a repository on a running node, sequenced from its first push; needs a node-wide write grant, answers 409 when it already exists, and prints the clone URL
  • choir repo list <api>
    the repositories on a node this credential can read, one per line; an ACL narrows the list rather than refusing it
  • choir repo url <api> <owner/repo.git>
    the clone URL for a repository, and the one line of git configuration that makes pushing work; the credential is never put in the URL
  • choir seed <home-url> [--credential <file>] [--name <principal>] [--port <n> | --archival] [--state <dir>] [--foreground] [-- <daemon flags>]
    take this machine from nothing to a running seed of another node's log (D80): mints the seed's node key and prints the three lines the home registers, then, run again with the credential the home issued, starts and supervises the seed; --archival replicates and stores without serving
  • choir repo follower add <owner/repo.git> <name> <url> [--state <dir>]
    on the node host, add a remote the repository's landings are pushed to (D21): git remote add on the bare repository, plus the marker that makes node serve pass --followers; restart the node once and every landing follows
  • choir repo follower list [--state <dir>]
    every repository this node holds with the remotes it pushes to; a repository with none is named, since its objects have no second copy yet
  • choir repo follower push [<owner/repo.git>] [--state <dir>]
    push every branch and tag to each remote now, no force and no prune; what the daemon does after each landing, for when it was not running with --followers
  • choir node serve [--state <dir>] [--port <n>] [--create <owner/repo.git>] [-- <daemon flags>]
    run the node in this terminal, deriving root, credential and trusted keys from what choir init wrote; execs the daemon so signals and the exit code reach the real process
  • choir node install [--state <dir>] [--port <n>] [-- <daemon flags>]
    hand the node to launchd (macOS) or a systemd user unit (Linux) so it survives logout, crash and reboot; the unit runs choir node serve
  • choir node tls <domain> --user <account> [--port <n>] [--dry-run | --staging]
    obtain a Let's Encrypt certificate for this node and wire up renewal: certbot, a deploy hook that re-projects the pair and restarts the node, and the marker node serve reads; the only command here that expects root, and --user is required
  • choir node stop
    stop the supervised node for this boot, leaving the unit in place; node uninstall is the one that ends it
  • choir node restart
    reload the unit and start it again, which is how a rebuilt binary reaches the running node
  • choir node upgrade --from <node-url> | --source <checkout> [--into <dir>] [--state <dir>] [--dry-run]
    newer binaries into the directory this choir runs from (or --into), a restart of the supervised node, and the build stamp read back from the running daemon; --from runs a node's own installer, --source builds a checkout with cargo
  • choir node uninstall
    stop the node and remove its unit; the state directory, with the keys, repositories and op log, is kept
  • choir node logs [<lines>] [--state <dir>]
    the tail of the node's log; defaults to the last 30 lines
  • choir node status [<api>]
    health, the commit serving, the sequencer's position and its p99 against the 100 ms gate, and how much this credential can see
  • choir doctor [<api>] [--state <dir>]
    check everything the other commands assume: the binaries shelled out to, the auth file and its mode, and whether a node answers; each failure prints the fix; on a hosting machine it adds bind address, TLS, certificate expiry, linger, unit state and whether the public URL answers; with seeds = in .choir/config, a fork check per seed
  • choir backup verify <backup-dir>
    whether a backup can be restored from: the four files, the manifest checksum, the hash chain, the policy archive and every git bundle, refusing a backup that carries a key or credential; every check local
  • choir backup take <backup-dir> [--state <dir>]
    take a backup of this machine's node into a directory, in the shape backup verify reads: the log, checked to extend the copy already there and to verify as a chain, the node fingerprint, the attestation, the policy files as one archive with no secret in it, and one git bundle per repository, kept when its refs have not moved; then verifies what it wrote
  • choir backup schedule <backup-dir> [--every <seconds>] [--state <dir>]
    run backup take into that directory on a timer, launchd or systemd, every hour unless told otherwise; backup unschedule removes it
  • choir backup unschedule
    remove the backup timer; the backups already taken are kept
  • choir backup restore <backup-dir> <target-root>
    turn a backup back into a node and prove it by accepting a real push: reads and refuses before writing, unbundles git objects before the first boot, rehearses on a port it picks; exit 3 means a secret only you can supply is missing
  • choir repair <log-file> --verify | --truncate-tail
    inspect a stopped node's op log, or repair a tail that was still being written; --verify changes nothing, --truncate-tail quarantines the partial record before cutting, and damage anywhere but the tail is refused

Most commands take the node's URL first. Put node = <url> in .choir/config, in the working directory or any parent, and it is filled in when omitted. choir <command> --help prints one command's spec.

Exit codes: 0 accepted, 1 the node rejected (its JSON error body is printed), 2 usage error.

Live surface on a running node: GET /llms.txt. Sync verification: SYNC.md / GET /sync.md.

MCP adapter

A synchronous stdio adapter mapping generated tools onto the same HTTP endpoints; no second implementation, no session state.

choir-mcp http://127.0.0.1:8417 --auth-file ~/.choir/auth --auth-user choir

Serves the legacy handshakes and the stateless 2026-07-28 request path. Tool order and schemas come from crates/choir-cli/src/surface.rs.