Skip to content

CLI reference

Three binaries. emetc compiles Emet to a manifest. golemctl ships a manifest to a node and reads its state. golemd runs on each node and reconciles it.

The fleet VM harness wraps these same primitives to compile, ship, and inspect against throwaway Debian VMs — see Bring up the fleet and The fleet harness.

emetc

The Emet compiler.

emetc build [FILE] [OPTIONS]

With no FILE, emetc build compiles a built-in demo program (useful for a smoke check). With a FILE, it compiles that .emet source.

FlagEffect
-o, --out <PATH>write output to a file instead of stdout
--format <binary|text|json>which view to emit; default binary
--text (--human)emit the readable plan instead of the binary manifest
--jsonemit the JSON debug view instead of the binary manifest

Default output is the binary, content-addressed manifest — raw postcard bytes to stdout (or -o PATH), with diagnostics on stderr so it composes in a pipe. --text, --human, and --json are mutually exclusive, each opt out of the binary artifact, and each override --format.

Bare emetc with no subcommand compiles the built-in demo as text; emetc build with no FILE compiles the same demo as the binary manifest.

Terminal window
# Compile the lichess fleet to a manifest file.
emetc build examples/lichess/fleet.emet -o fleet.manifest
# Eyeball the plan.
emetc build examples/lichess/fleet.emet --text
# JSON view for ad-hoc tooling (not content-addressed).
emetc build examples/lichess/fleet.emet --json | jq '.scrolls | length'

golemctl

The operator CLI. Every single-host command takes the agent’s address as <addr>; the fleet verbs take an inventory instead and name no address.

golemctl <COMMAND>
Commands:
apply <source> <addr> Compile-and-ship, or ship a prebuilt manifest
plan <source> <addr> Ask what an apply would do, changing nothing
fleet <COMMAND> Fan a verb out over an inventory, concurrently
state <addr> Print the host's applied scroll + content id
history <addr> Print the revision journal
show <addr> <id> Print one revision by id

golemctl apply <source> <addr>

If <source> ends in .emet, golemctl shells out to emetc build <source> and ships the resulting binary manifest. Otherwise it reads <source> as prebuilt manifest bytes and ships those. Either way it POSTs to <addr>/manifest.

The POST returns immediately with 202 { reconcile_id }; the reconcile runs detached on the agent. golemctl then polls GET /reconciles/:id?after=… until the attempt reaches a terminal phase, rendering a live unit tree on a TTY and plain per-event lines otherwise.

Terminal window
golemctl apply examples/lichess/fleet.emet http://127.0.0.1:7474
golemctl apply fleet.manifest http://127.0.0.1:7474
FlagEffect
--jsonemit the final report object on stdout, no TUI; per-event lines and the logs: <dir> line go to stderr
--reattachskip the POST and follow the newest attempt via GET /reconciles/latest

--reattach is what a dropped connection needs: the reconcile it started kept running on the agent, and reattaching resumes reading its progress.

Exit code 0 means the reconcile settled. Any other terminal outcome (partial, rolled_back) exits nonzero and still prints the report — a partial reconcile is a result, not a transport error. A stdout that is not a TTY takes the plain path even without --json.

golemctl plan <source> <addr>

The same compile, and no POST. golemd diffs the manifest against what it has applied and answers with what an apply would do; nothing is written, so a plan is safe to run while an apply is in flight. --detail expands every group to one glyph per line with content ids; --json emits golemd’s response verbatim.

golemctl fleet <apply|plan|status>

The same three readings, fanned out over every host in a TOML inventory, concurrently. One host’s failure never stops the others, and a host the manifest names no scroll for is skipped untouched — never POSTed to and not counted against the exit code. fleet apply exits 0 only if every host settled or was skipped.

Terminal window
golemctl fleet status
golemctl fleet plan examples/lichess/fleet.emet --hosts scaly,talos
golemctl fleet apply examples/lichess/fleet.emet

--hosts a,b narrows the run to a subset, resolved before anything is compiled or contacted, so a typo’d name fails while no daemon has been touched. The inventory itself comes from the first of --inventory, $GOLEMCTL_INVENTORY, ./fleet.toml, ./.fleet/inventory.toml.

Each host is a key under [hosts] — a bare URL string, or a table:

[hosts.scaly]
ssh = "golem@127.0.0.1"
ssh_port = 2259
remote_port = 7474
ssh_args = ["-i", ".fleet/id_ed25519", "-o", "StrictHostKeyChecking=no"]
token_file = ".fleet/golem-token"
KeyMeaning
urldial this base URL directly
sshthe ssh destination ([user@]host); golemctl opens its own forward
ssh_portssh’s own port on that host; default is ssh’s
remote_portgolemd’s loopback port on that host; default 7474
ssh_argsextra flags passed to the ssh command
token_filethis host’s bearer secret

A table carries url or ssh, never both. A bare string says the same thing as url. The name is the join key: it matches the scroll of the same name in the manifest. See The fleet harness for the harness that writes this file for you.

golemctl state <addr>

Reads GET /state — the current applied scroll for the host and its content id (lowercase hex).

golemctl history <addr> / golemctl show <addr> <id>

history reads GET /revisions (the append-only journal). show reads GET /revisions/:id for one entry — its kind (init / reconcile), the content id enacted, the ordered glyph ops, and the reversal receipts.

Reaching a remote agent

Only the ssh:// prefix is interpreted. ssh://[user@]host[:port] makes golemctl open its own SSH forward to the agent’s loopback port and speak HTTP through it; the port there is ssh’s, not the agent’s, which stays 7474. Any other <addr> is carried through untouched as a base URL, so http://host:port dials directly.

Terminal window
golemctl apply fleet.emet ssh://golem@scaly
golemctl apply fleet.emet ssh://golem@scaly:2222

An ssh:// target that names no host, that begins with - (ssh would read it as a flag), or whose :port does not parse is refused before anything is compiled or dialed.

Every request carries Authorization: Bearer <token>, taken from the first of:

  1. the host’s inventory token_file,
  2. $GOLEM_AUTH_TOKEN,
  3. the file named by $GOLEM_AUTH_TOKEN_FILE.

A per-host token_file therefore overrides the ambient environment for that host alone. Absent everywhere is not an error — golemctl still talks to ungated daemons. A 401 comes back as an error naming those sources. See Trust model.

golemd

The per-host agent.

golemd [OPTIONS] --host <HOST>
Options:
--host <HOST> This node's name; selects its scroll from the manifest [env: GOLEM_HOST]
--state-dir <DIR> Where to keep planroom.db [default: /var/lib/golem]
--listen <ADDR> HTTP listen address [default: 127.0.0.1:7474]
--reconciler <KIND> host | fake [default: fake] [env: GOLEM_RECONCILER]
--config <FILE> golemd.toml: [retry], [enact], [auth]
--auth-token-file <FILE> Shared secret every request must present

--host <name> (required)

The node’s identity. On a manifest, golemd selects the AddressedScroll whose scroll.name equals this value and reconciles toward it, ignoring every other host’s scroll. Also settable via GOLEM_HOST.

--reconciler host | fake

Which reconciler enacts glyphs:

  • fake (default) — an in-memory reconciler that records ops without touching the host. Exercises the full diff / enact / journal spine with zero I/O; used by tests and for dry runs.
  • host — the real reconcilers: apt, systemd, file, lineInFile against a live Debian box.

Also settable via GOLEM_RECONCILER.

--state-dir <dir> / --listen <addr>

--state-dir holds the SQLite journal (planroom.db), golem’s record of what it applied and how to reverse it. --listen is the HTTP bind address; default 127.0.0.1:7474. Keep it on loopback — operators reach a deployed agent through an SSH forward, and a routable bind publishes root-equivalent control of that host.

--auth-token-file <file>

The file holding the shared secret every request must present as Authorization: Bearer <token>; anything else is a 401. [auth] token_file in golemd.toml says the same thing and the flag overrides it. An unreadable or empty file stops the agent at startup rather than starting it open.

With neither set, the agent answers anyone who reaches the port — the local-development posture, never a deployed one. See Trust model.

HTTP endpoints

MethodPathPurpose
POST/manifestAccept a binary manifest; select this host’s scroll and start a reconcile. Answers 202 { reconcile_id } — the reconcile runs detached.
POST/planAccept a binary manifest; answer what an apply would do. Writes nothing.
GET/reconciles/latest?after=<seq>Progress for the newest attempt: its phase, and every event after after.
GET/reconciles/:id?after=<seq>The same projection for one attempt by id.
GET/stateThe current applied scroll and its content id.
GET/revisionsThe full revision journal.
GET/revisions/:idOne revision.
GET/status{ host, latest_revision }.

/manifest answers before the work is done because a reconcile is not short — a cold host runs apt update, package installs, and image pulls, and tens of minutes is normal. A held-open request would bind the report’s lifetime to one TCP connection, so a dropped connection would lose an outcome the agent had already produced. Firing and polling separates the two: the attempt outlives the client that started it, and /reconciles/latest is how the client finds its way back.

Only a reconcile that actually started yields a reconcile_id. Failures in the ingest itself — undecodable manifest bytes, an unreadable journal, a reconcile already in flight — come back as typed non-2xx on the POST.