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, anyScroll/Glyphfield or variant, the postcard format, the BLAKE3 hash. It is checked on read:from_bytesrejects an unknownformat_versionwith a typed error rather than a misparse. It is currently4:2added the filesystem glyph,3madeScrollrecursive,4addednotifiesbetweenpolicyandcontents. Adding that one field was a layout change because v3 bytes would misread the oldcontentstag as the newnotifieslength.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 PATHor stdout. This is whatgolemctl applyships. --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 artifactgolemdconsumes.
--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 revisionA 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
| Concern | Path |
|---|---|
| Manifest / AddressedScroll / ContentId | libs/scroll-format/src/manifest.rs, content_id.rs |
| Glyph / Scroll model | libs/scroll-format/src/scroll.rs |
| Compiler (writer) | apps/emet/ (re-exports the model via emet::ir) |
| Agent (reader) | apps/golemd/ |