Skip to content

Manifest format

The manifest is the contract between the compiler and the agent. emetc build emits it; golemd consumes it. It is a binary, content-addressed artifact — not JSON, not a text plan. Its schema is the shared scroll-format crate (libs/scroll-format/), which both emet and golemd depend on and neither owns, so the writer and reader cannot drift.

The model

Manifest {
format_version : u32,
emet_version : String,
scrolls : [ AddressedScroll ],
}
AddressedScroll {
content_id : ContentId, // BLAKE3 over the scroll's bytes
scroll : Scroll, // one host's desired state
}
Scroll {
name : String,
policy : Option<Policy>,
notifies : [ String ],
contents : Glyphs [ Glyph ] | Groups [ Scroll ],
}
Policy {
base_delay_ms : Option<u64>,
backoff_multiplier : Option<f64>,
max_delay_ms : Option<u64>,
jitter_fraction : Option<f64>,
max_attempts : Option<u32>,
max_elapsed_ms : Option<u64>,
on_exhaust : Option<Rollback | Keep>,
}

A manifest carries the whole fleet — every host’s scroll. Each golemd selects the one AddressedScroll whose scroll.name matches its own --host and ignores the rest.

A Scroll is a recursive tree. Every level holds either glyphs (a leaf) or named sub-scrolls (a branch), never both — contents is a sum, so a level mixing loose glyphs with sub-scrolls is unrepresentable. A leaf is the unit of enact, retry, and rollback; a branch only groups.

policy is optional at every level and resolves nearest-wins down the root-to-leaf chain; notifies unions down instead, so reload obligations accumulate rather than override. Both sit inside the hashed scroll and outside every glyph: rewiring a notification or changing a retry budget yields a new scroll content id while every glyph content id stays what it was.

Content addressing

A scroll’s identity is content_id(scroll) = blake3(postcard::to_stdvec(scroll)) — the BLAKE3 hash of the scroll’s deterministic postcard bytes. The hash is over the scroll alone, never over the manifest or its neighbours, so:

  • An identical scroll always yields an identical ContentId.
  • A scroll’s id does not depend on its position in the manifest.
  • A scroll’s id does not depend on emet_version.

That is what makes reconciliation cheap and precise. Same bytes ⇒ same id ⇒ no-op; a changed field ⇒ new id ⇒ an upgrade. The agent also hashes each glyph individually (content_id_of_glyph) so a one-line change to one file re-applies only that glyph, not the whole scroll — see Reversible reconcile.

ContentId is a 32-byte digest. Its string form is lowercase hex (64 characters); golemctl state prints it that way.

Determinism

Postcard is non-self-describing: a type’s field order and enum variant order are the encoding. There are no field names on the wire. That makes the bytes canonical by construction — the same Scroll value always serializes to the same bytes and hashes to the same id — but it also means reordering or adding a field is a wire-breaking change, not a free refactor.

The scroll-format crate pins this with golden-byte and round-trip tests: a fixed scroll serializes to a committed byte constant and hashes to a committed id; from_bytes(to_bytes(m)) == m; a scroll’s id is invariant to its position and to emet_version.

format_version vs emet_version

Two version fields, two jobs:

  • format_version (u32) versions the wire layout — the manifest shape, any Scroll/Glyph field or variant, the postcard format, the BLAKE3 hash. It is checked on read: from_bytes rejects an unknown format_version with a typed error rather than a misparse. It is currently 4: 2 added the filesystem glyph, 3 made Scroll recursive, 4 added notifies between policy and contents. Adding that one field was a layout change because v3 bytes would misread the old contents tag as the new notifies length.
  • emet_version (String) records which compiler build wrote the manifest. It is provenance only, sits deliberately outside every hash, and is never checked. Rebuilding an unchanged fleet with a newer compiler yields byte-identical per-scroll content ids.

The three emetc build views

The binary manifest is the default and the only content-addressed artifact. Two other views exist for humans and never leave the trust path:

  • default — the binary manifest (postcard bytes), to -o PATH or stdout. This is what golemctl apply ships.
  • --text (--human) — a readable plan (Scroll::describe / Glyph::describe). Eyeball view only.
  • --json — a self-describing JSON rendering of the same manifest, for ad-hoc tooling. Not content-addressed; never the artifact golemd consumes.

--format <binary|text|json> selects the same three views by name; the boolean flags override it.

See the CLI reference for the exact flags.

What the agent does on receipt

golemd accepts manifest bytes at POST /manifest:

on POST /manifest:
manifest = scroll_format::from_bytes(body) // checks format_version
scroll = foreman selects by scroll.name == this host's --host
answer 202 { reconcile_id } // the enact runs detached
ops = diff(last applied outcomes, the scroll's leaf units) // by glyph key + CID
enact each op through the Reconciler, capturing an Inverse per glyph
journal the ordered outcomes as a Reconcile revision

A manifest that names no scroll for this host selects an empty scroll — the host reconciles toward nothing rather than erroring.

The diff, the enact, and the journalled reversal records are the subject of Reversible reconcile.

Where to look in the source

ConcernPath
Manifest / AddressedScroll / ContentIdlibs/scroll-format/src/manifest.rs, content_id.rs
Glyph / Scroll modellibs/scroll-format/src/scroll.rs
Compiler (writer)apps/emet/ (re-exports the model via emet::ir)
Agent (reader)apps/golemd/