Skip to content

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" }
FieldTypeMeaning
nameStringthe 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" }
FieldTypeMeaning
unitStringthe 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"
}
FieldTypeMeaning
pathStringabsolute path on the host
contentsStringthe exact file body
modeStringoctal 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.

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"
}
FieldTypeMeaning
pathStringthe file to edit
lineStringthe 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 Glyph
registry =
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.