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
--secret-key <PATH>the fleet key file secrets are sealed to (env GOLEM_SECRET_KEY_FILE)
--secret-provider <NAME>which secretspec provider answers Secretspec.get (env SECRETSPEC_PROVIDER)
--secret-profile <NAME>which secretspec profile to resolve under (env SECRETSPEC_PROFILE)

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.

The three --secret-* flags are read only by a program that calls Secretspec.get. --secret-key names the fleet key the resolved value is sealed to — without it, and without GOLEM_SECRET_KEY_FILE, compiling such a program fails saying so. --secret-provider and --secret-profile select which secretspec provider and profile answer the lookup, overriding what secretspec.toml configures. See Trust model.

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. The <source> argument is still required and is never opened, so the path you originally applied is the one to repeat.

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.

FlagEffect
--detailexpand every group to one glyph per line with content ids
--against-hostadd a second block: the same glyphs checked against the live host — its filesystem, systemd, and dpkg, read right now — beside the journal-only diff
--jsonemit golemd’s response verbatim

--against-host costs more than the journal-only plan — it runs the same per-glyph checks apply already runs before touching anything — which is why it is opt-in; it needs no authority golemd doesn’t already hold, and reads only files, units, and packages the manifest itself names. A Remove is asked the weaker question a host can actually answer — is the resource still there? — never whether golem once owned it, so the host block confirms or contradicts a removal the journal already proposed but never originates one of its own. A glyph golem can’t read on this host — an unreadable file, a secret sealed to a key this host lacks — reports unknown, and unknown never counts as a match. See Applying changes for the enrollment case this exists for.

A --against-host plan started while golemd is writing to that host — an apply mid-reconcile — is refused with a 409 conflict rather than answered: golemd checks its own write activity before and after reading the host, and a busy or moved read is reported rather than trusted. An ordinary golemctl plan, without the flag, is unaffected and still works during an apply. This does not make a successful --against-host plan a stable snapshot: the host can change out of band the moment the plan returns, and nothing here promises otherwise.

fleet plan --against-host fans this check out per host, and a host that answers 409 fails that host’s plan — which fails the whole fleet plan command (exit 1), unlike an ordinary fleet plan’s “always exits 0”. A 409 on one host of a fleet mid-rollout is expected, not a sign anything is wrong; retry once that host’s apply has settled.

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.

fleet plan takes --against-host the same way plan does, checked independently against each host’s own live state.

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. 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], [secrets]
--auth-token-file <FILE> Shared secret every request must present
--secrets-key-file <FILE> Fleet key the manifest's secrets were sealed to

--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.

--secrets-key-file <file>

The fleet key emetc sealed this fleet’s manifests to, which golemd opens sealed values with at enact. [secrets] key_file in golemd.toml says the same thing and the flag overrides it. The key is read once at startup: an unreadable or malformed file stops the agent, so a mis-provisioned key is a boot failure rather than a fleet-wide reconcile failure found one glyph at a time.

With neither set, the agent enacts any manifest carrying no secret and refuses, by name, every glyph that carries 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. ?against_host=true adds a second block checked against the live host. 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.