The four glyphs
A glyph is one bottom-level OS resource. Emet has four glyph primitives
and golemd enacts those four. Every higher-level shape you write (a
container workload, a service, an ingress) is an Emet function that returns
a List Glyph and lowers to these.
Each glyph is a reserved record constructor in Emet. emetc evaluates every
field to a finished value on your own machine: string interpolation and
String.join have already run by the time a glyph is written to the
manifest, so a host receives the resulting bytes.
Identifier fields — path, name, unit, mode, target — are plain
strings. A value-bearing field — a file’s contents, a lineInFile’s line
— is a text value holding either plain text or the literal chunks around a
sealed hole, which is what an interpolated Secretspec.get value compiles to;
golemd opens the hole at enact and joins the result. Of the two, contents
is the one that may hold a secret today. See
Trust model for what sealing protects.
aptPackage
Ensure an apt package is installed.
aptPackage { name = "nginx" }| Field | Type | Meaning |
|---|---|---|
name | String | the package to install |
Glyph key: apt:<name>.
Reversibility. On apply, golemd queries whether the package is
already installed (dpkg-query). If not, it runs apt-get update and then
apt-get install -y, recording “installed by us”; on reverse it removes it.
The refresh is per-glyph: a fresh Debian cloud image ships with an empty
package list, so an install would fail to resolve without one. If the
package was already present, apply is a no-op and reverse leaves it alone.
Golem never removes a package it did not install.
systemdService
Ensure a systemd unit is enabled and started.
systemdService { unit = "nginx.service" }| Field | Type | Meaning |
|---|---|---|
unit | String | the unit to enable --now |
Glyph key: systemd:<unit>.
Reversibility. On apply, golemd records the unit’s prior
enabled/active state, runs systemctl daemon-reload so a just-written unit
file is visible, then systemctl enable --now. On reverse it restores the
prior state — if golem enabled it, disable --now; if it was already
enabled, leave it.
A generated unit refuses enable: a Podman quadlet is already enabled
by its generator through an [Install] section, so enable --now rejects
it as transient or generated. On that specific failure golemd falls back
to systemctl start and records that it only started the unit — reverse
then stops it and never disables it. Any other enable failure fails the
glyph.
A unit latched in the failed state is cleared first. A unit that
restarts more times than its StartLimitBurst allows stops in
Active: failed and refuses every later start job — enable --now,
start, restart, reload-or-restart — with “start request repeated too
quickly”, until someone runs systemctl reset-failed. Before enabling,
golemd probes
systemctl is-failed and, when the unit is latched, runs reset-failed
so the start that follows can be accepted. A reset that fails is logged
and the start is attempted anyway: the start’s own error names the real
symptom, and a reset can fail for reasons that would not have blocked it.
Nothing is recorded to undo the reset. golem reverses state it changed,
and the latch is systemd’s own count of past failed starts, not
configuration anyone authored — there is no set-failed to re-arm it
with, and re-arming it on a unit reverse has just stopped would leave the
host worse than golem found it.
The same probe guards the pokes a changed file triggers. Ordinarily a
config change earns systemctl try-restart, which restarts a running unit
and leaves a stopped one stopped, because starting an inactive unit is
this glyph’s decision and not a config file’s. A latched unit is the
exception: try-restart on a unit that is not running exits 0 and does
nothing at all, so golem clears the latch and issues the forcing restart
instead. A notifies reload does the same one rung lighter,
try-reload-or-restart becoming reload-or-restart. Without it, a
reconcile whose only change was a drop-in file reported success over a
service that never came back.
What decides this is the unit’s state on the host, not what the scroll
declares. A poke carries a unit name and nothing else, so the forcing verb
reaches any latched unit it names — including one no systemdService
glyph declares, such as a scroll that writes only a drop-in, or a
notifies naming a host-managed unit.
file
Ensure a file exists with fixed contents and mode.
file { path = "/etc/app/site.conf" , contents = "[server]\nlisten = 8080\n" , mode = "0644" }| Field | Type | Meaning |
|---|---|---|
path | String | absolute path on the host |
contents | String | the exact file body |
mode | String | octal mode, e.g. "0644" |
Glyph key: file:<path>.
mode is written as a string, with or without a 0o prefix — "0644" and
"0o644" are the same mode. emetc parses it as octal into the 12
permission bits and the wire carries a u16, so a non-octal mode or one
above 0o7777 is a compile-time error, not a reconcile-time failure.
The wire Perms also carries owner and group — names, resolved to
uid/gid at reconcile time. The surface constructors do not expose them, so
every authored entry leaves them None and ownership unmanaged.
file is the glyph that may carry a secret, and it must own the mode to
match: emetc accepts a sealed value in contents at mode = "0600" and
refuses one at any mode granting group or other read. Because the surface
constructors leave group unset, "0600" is the mode to write today. See
Trust model.
Reversibility. On apply, golemd reads the prior contents and mode
(or notes the file was absent), then writes the desired contents atomically
(temp file + rename). On reverse it restores the prior bytes and mode, or
deletes the file if golem created it.
directory and symlink — the same glyph, two more spellings
file is one of three surface spellings of the filesystem glyph. The other
two build a directory or a symlink at a path:
directory { path = "/var/lib/registry", mode = "0755" }
symlink { path = "/etc/nginx/sites-enabled/site.conf", target = "/etc/nginx/sites-available/site.conf" }Each arm carries only its own fields — directory takes no contents,
symlink takes neither contents nor mode — so a symlink with a mode or a
directory with contents cannot be written down. All three share the
file:<path> key, reconcile through the one filesystem reconciler, and
reverse the same way (restore the prior entry, or remove what golem created).
A Quadlet FromHost mount emits a directory for
its bind-mount source, which is where most of them come from.
lineInFile
Ensure a single line is present in a file golem does not own — a file the distro, another tool, or a human wrote, which golem needs to amend rather than author.
lineInFile { path = "/etc/hosts" , line = "10.0.0.7 registry.internal" }| Field | Type | Meaning |
|---|---|---|
path | String | the file to edit |
line | String | the exact line to ensure present |
Glyph key: fileline:<path>:<line>.
Reversibility. On apply, if the line is already present, apply is a
no-op and reverse does nothing. Otherwise golemd appends it and records
that it added the line; on reverse it removes exactly that line. Golem
never removes a line it did not add.
line holds plain text only. This glyph owns one line and not the file
around it, so it can promise nothing about who may read the result, and
emetc refuses a secret here — write that with a file glyph at
mode = "0600", which owns the whole file and enforces its mode.
Conflict rules within a leaf
The conflict scope is the leaf unit, not the whole scroll. Two glyphs
with the same key inside one leaf must be identical, or the compiler’s
pre-apply analysis rejects them at the conflicting glyph’s source span.
Sibling leaves inside one scroll may share a key, and two different
scrolls may share one freely — two hosts installing nginx is fine.
Uniqueness is never fleet-wide.
Building abstractions
Raw glyphs are rare beyond the smallest hosts. They compose either through a
function you write returning a List Glyph, or through a shipped library —
Quadlet for containers,
Traefik for the TLS front door that routes hostnames to
them. A Quadlet Workload lowers a typed container spec down to exactly these
glyphs:
import Quadlet exposing ( image, tcp, Restart(..), Expose(..), Workload(..), workloadGlyphs )
registry : List Glyphregistry = workloadGlyphs (Workload { name = "registry" , image = image "docker.io/library" "registry" "2" , env = [], envFiles = [], labels = [] , ports = [ tcp 5000 5000 ], networks = [], volumes = [] , restart = Always, expose = Internal })That lowers to the podman aptPackage, the .container quadlet file, its
systemdService, one nftables drop-in file for the internal port opening, and
the nftables base that loads it — every one of them a kind the agent already
enacts. See the
Quadlet reference, the
service abstraction guide, and the
lichess tour for the full pattern.