Skip to main content

Crate choir_node

Crate choir_node 

Source
Expand description

L3 node daemon: a minimal git smart-HTTP server (DECISIONS.md D12).

v1 wraps git http-backend (git’s own CGI) over bare repositories, so the daemon is a thin, self-hostable shell over git plumbing, and platform behavior (sequencer, queue, identity) layers on top. ForgeMark benchmarks this surface directly.

Authentication is per-actor basic auth (AuthTable, --auth-file), plus the credentials self-service has issued (accounts, --accounts-file); the platform API (platform) additionally verifies ed25519 op signatures. The bind stays loopback-only: beyond localhost you still need TLS or an SSH tunnel so tokens aren’t sent in the clear.

§Where this sits

docs/architecture.md is the map of the whole workspace. This crate is L3, the daemon that serves both git smart-HTTP and the platform API.

It builds on choir_fs, choir_hash, choir_identity, choir_oplog, choir_sequencer and choir_view.

The operator’s guide to this daemon:

§Running a node

One binary serves two protocols on one port: git smart-HTTP for clone, fetch and push, and the platform API for signed operations. This page is how to start it and what each policy file means.

Using a node: docs/using/cli.md. The policy behind each flag: docs/operating/authorization.md, docs/operating/limits.md, docs/operating/webhooks.md, docs/operating/observability.md, docs/operating/transports.md.

Default port 8417. A repository is sequenced only when it carries a pre-receive hook: create with choir repo create or --create; a bare repository that arrives any other way is adopted and hooked at the next start. With no arguments the binary uses ./repos and port 8417; configured invocations must supply both <repo-root> and <port> before any flags.

§Getting the binaries

One command, no Rust toolchain and no OpenSSL headers. macOS and Linux, x86-64 and arm64:

curl -fsSL https://github.com/deadcaf3/choir/releases/latest/download/choir-node-installer.sh | sh
curl -fsSL https://github.com/deadcaf3/choir/releases/latest/download/choir-cli-installer.sh | sh

Take both: every command on this page is choir, and choir node serve execs the daemon. The four binaries land in $CARGO_HOME/bin (~/.cargo/bin by default) as choir-node, choir-ssh, choir and choir-mcp; each release publishes a SHA256 per archive and a sha256.sum.

cargo binstall choir-node choir-cli fetches the same archives. From a git checkout, needing only a Rust toolchain:

cargo install --git https://github.com/deadcaf3/choir choir-cli choir-node

Neither package pulls in choir-actor, the one crate that needs this workspace’s LIBSQLITE3_FLAGS.

From source: the toolchain and headers the README lists, then:

cargo build --release -p choir-node -p choir-cli
export PATH="$PWD/target/release:$PATH"

choir node status and choir --version report the built commit from a stamp the release workflow sets.

§One command

choir host
you haverunyou get
a laptop, or a box nobody else reacheschoir hosthttp://127.0.0.1:8417, in seconds, no certificate
a name pointing at this boxchoir host --domain node.examplehttps://node.example:8417
a VPS and no namechoir host --publichttps://<this-ip>.sslip.io:8417

It prints the URL people use and, with --invite, one invite link:

choir host --repo me/thing.git --invite Ada

The two public modes take two commands. Obtaining a certificate is privileged and choir host does not run sudo: it prints the line to paste and exits 3. Paste it, run the same choir host again, and it continues.

$ choir host --domain node.example

  ok  state           /home/you/.choir — credential, key, trusted keys

  next: a certificate for node.example has to be issued as root.
  certbot writes /etc/letsencrypt, and the renewal hook that keeps
  this working for the next two years lives there too. Paste this:

    sudo choir node tls node.example --user you --port 8417
    sudo ufw allow 8417/tcp  &&  sudo ufw allow 80/tcp

  then: choir host --domain node.example

Dry-run the certificate step without spending a Let’s Encrypt rate-limit slot:

sudo choir node tls node.example --user "$(id -un)" --dry-run

§Why a public bind needs a certificate at all

choir-node refuses a non-loopback bind without TLS (invariant 9). There is no flag to soften it.

§--public, and why the address is ugly

choir host --public uses an sslip.io name built from the box’s address, 203-0-113-7.sslip.io for 203.0.113.7, so a certificate can be issued with no DNS. Replace it any time:

choir host --domain the-name-you-bought.example

sslip.io and nip.io share one Let’s Encrypt rate-limit pool. If issuance fails for that reason, --public-name <name> takes any name pointing at this box.

§What it did

Every step is a command you could run yourself:

stepwhatundo
statechoir init: ~/.choir with the credential, actor key, trusted keys, and .choir/configdelete ~/.choir
acl + accounts~/.choir/acl granting the operator everything, and an empty ~/.choir/accounts.jsonldelete either file
certificatechoir node tls: certbot, a renewal deploy hook, the pair projected where the node can read it, and ~/.choir/tls.enabledsudo rm /etc/letsencrypt/renewal-hooks/deploy/choir-tls, sudo certbot delete
address~/.choir/public-url, and .choir/config pointed hereedit either
supervisedchoir node install: a launchd agent or a systemd --user unitchoir node uninstall
healthypolls /healthz
repositorychoir repo create, with --repo
invitedchoir invite, with --invitechoir revoke

Two things it prints but never runs:

  • the firewall. ufw or firewalld, opening the serving port, and port 80 when HTTP-01 is the challenge (renewals rebind it every ~60 days).
  • linger. Without loginctl enable-linger, systemd stops a --user unit at logout. choir host stops with the line to paste; --yes accepts a node that dies at logout.

§TLS, and what happens every 60 days

The daemon terminates TLS itself with --tls-cert and --tls-key. An nginx or Caddy front is a valid topology; it is not required.

The daemon reads its certificate once, at bind. The certbot deploy hook choir node tls installs re-projects the pair into ~/.choir/tls/ and restarts the unit. Renewal runs on certbot’s timer.

Root for certbot, an unprivileged account for the node; the node’s copy of the pair is a copy for that reason.

The ACME account is registered without an email. sudo certbot renew --dry-run is the manual check; choir doctor reports the expiry date.

§Checking it

choir doctor

On a hosting machine it adds six rows: bind address, TLS on, certificate expiry, linger, unit loaded and running, and whether the public URL answers (asked from the box, so it proves name and certificate, not the firewall).

§Uninstalling

choir node uninstall

Removes the unit and stops the node. Keeps ~/.choir: keys, repositories and the op log. If a renewal hook is installed it prints the two sudo lines that remove it and the certificate.

§In a container

Dockerfile at the repository root builds from source and runs the daemon as an unprivileged user with /var/lib/choir as a volume. Loopback mode and bring-your-own-certificate mode; no ACME client in the daemon. No compose file: the volume and the port are one flag each.

§By hand

choir init                                  # the same layout, without a service manager
choir node serve                            # runs here, in this terminal
choir node serve -- --reviewers-file ~/.choir/reviewers   # any daemon flag, after `--`

choir node serve derives root, port, credential and trusted-key file from what choir init wrote, then execs the daemon. Three files switch behaviour by existing:

fileeffect
~/.choir/tls.enabledtwo lines, cert path then key path: binds 0.0.0.0 with that pair
~/.choir/acl--acl-file
~/.choir/accounts.jsonl--accounts-file (refused without an ACL)

Supervised, step by step:

choir init                      # mint ~/.choir: credential, key, trusted keys, config
choir node install              # hand it to launchd (macOS) or systemd (Linux)
choir node status               # health, the commit serving, sequencer position
choir repo create me/thing.git  # a repository on the running node
choir repo url me/thing.git     # the clone URL, and the git config to go with it
choir node logs                 # the tail of the daemon log
# choir node restart | stop | uninstall   -- uninstall keeps ~/.choir

choir node install writes a unit that runs choir node serve. Daemon flags go after -- and are recorded in the unit:

choir node install -- --acl-file ~/.choir/acl --rate-limit-api 60

The daemon can be run directly:

choir-node /tmp/choir-repos 8417 \
  --create owner/demo.git \
  --auth-file ~/.choir/auth \
  --keys-file ~/.choir/keys

Useful flags: --bind, --tls-cert / --tls-key, --acl-file <file> (required before a second credential), --request-log <file> and --rate-limit-api / --rate-limit-git (also required before a second credential), --quota-push-bytes / --quota-workspaces, --api-body-limit, --batch-limit, --ready-min-free-bytes, --read-only-browser, --journal <file>, --require-assignment, --protected-refs <file>, --require-review, --reviewer-conflict-graph <file> with --reviewer-conflict-distance <hops>, --review-retention <count>, and --review-lapse-after-secs <seconds>. Authenticated operations endpoints: /healthz, /readyz, /metrics. Flag reference: crates/choir-node/src/main.rs module docs, or AGENTS.md.

File formats (all mode 0600)

FileFormat
--auth-fileuser:token per line; authentication only, pair with --acl-file
--acl-file<user> <repo|*|@node> <level> per line; levels read < propose < write < own, and auditor on @node
--keys-file<64-hex> or <channel> <64-hex> (bound key)
--reviewers-filechannel name per line; re-read on each draw
--protected-refsowner/repo.git:refs/heads/main (trailing * ok)
--reviewer-conflict-graphundirected operator operator edges; pair with an explicit maximum hop distance

Hot-reload: trusted keys, channel bindings, push-certificate signers, reviewers, and ACL grants take effect on the next request.

[!WARNING] Without --acl-file, every credential reaches every repository. A second user:token line can clone every repo, push to any unprotected ref and provision workspaces anywhere. Do not issue a second credential without an ACL. choir host writes one.

§Behind a TLS proxy

For the private beta, bind 127.0.0.1 and terminate TLS at a hardened reverse proxy. Do not use the legacy direct-TLS installer. The beta service renderer requires an ACL, protected-ref review policy, scoped operations, operator-issued auth, and the read-only browser.

docs/private-beta-runbook.md covers the network hold, renderers, backups, CI packaging, staging promotion, monitoring, rollback and go-live receipts.

scripts/flip/RUNBOOK.md is the operator’s own dogfood procedure; not the page to start from.

Re-exports§

pub use platform::Platform;

Modules§

accounts
Account and credential self-service (D36).
acl
Per-repository authorization (D29).
hooks
Outbound ref-landed webhooks (D32): “something moved, go run this”.
limits
Request accounting and admission control (D33): who did what, and how much of it they may do.
platform
The platform API: signed op submission and view queries over HTTP.
portable
Exporting a node root, importing one, and settling what an export claims (§E).
profile
Who an actor is, from what the log already says about them (D63).
provision
Instant workspace provisioning (POST /api/workspace) — the D21 “productized provisioning” wedge feature.
queue
The node’s own merge queue (D68).
queue_api
Driving the node’s merge queue from outside (D5, D68).
quota
Per-user quotas (D37): how much of the node one credential may hold.
reject
Rejections that name the repair.
ssh
SSH transport for git (D31): the host’s sshd, a forced command, and this shim.

Structs§

Node
A running node daemon serving repos under a root directory.
TrustedKey
One line of the trusted-keys file: a public key the node accepts, and optionally the channel name its holder is allowed to speak as.

Constants§

BUILD_COMMIT
The commit this binary was built from, or the literal unknown when the build had no way to find out. See build.rs.
BUILD_DIRTY
Whether the build tree had uncommitted changes. Only meaningful under BUILD_SOURCE == "git", and even then best-effort: cargo cannot rerun the build script on every source edit, so this can be stale where BUILD_COMMIT cannot.
BUILD_SOURCE
Where BUILD_COMMIT came from: env (the installer passed CHOIR_GIT_HEAD, the sound path), git (best-effort at build time), or unavailable (no commit could be determined).
DEFAULT_API_BODY_BYTES
Default maximum body size for every /api/... request: 1 MiB.
DEFAULT_READY_MIN_FREE_BYTES
Readiness refuses when the filesystem reports less than 1 GiB free.

Functions§

build_json
The build stamp as served under /api/view.build.
build_line
One line naming the running binary’s provenance, for the startup log.
git_invocations
How many times this process has shelled out to git while serving.
parse_keys_file
Parses a trusted-keys file. One key per line, # comments and blank lines skipped, in either form:
ssh_ed25519_pubkey
Renders a raw ed25519 public key in OpenSSH ssh-ed25519 <b64> form (the wire blob is two length-prefixed strings: key type, key bytes).
ui_tokens_css
The design tokens from ui.css, alone, for the documentation book.
write_allowed_signers
Writes <root>/.choir/allowed_signers — the file git’s ssh signature verification checks push certificates against. One line per actor: principal (the actor id) followed by the OpenSSH public key.

Type Aliases§

AuthTable
Per-actor credentials: username → token, checked as HTTP basic auth (the standard git-over-HTTP shape; every forge client speaks it).