# Idunn **Deployment admission and daemon-survival authority for the GameCult swarm.** Idunn decides two things and nothing else: whether an artifact may be installed, and whether an already-installed body may be restarted. It keeps daemons alive across a fleet, admits releases against operator-signed authority, and reports typed health — while depending on none of the services it manages. Extracted from [Odin](https://github.com/GameCult/Odin) on 2026-09-05 with its full 136-commit history. Idunn had lived as a crate inside the repository of a service it manages, which inverted the authority it exists to hold. --- ## Quick start Requires Rust 1.95+. Idunn is a **Linux daemon**; see [Platform](#platform). ```bash git clone https://github.com/GameCult/Idunn.git cd Idunn cargo build --release ``` Two binaries land in `target/release/`: | Binary | Purpose | |---|---| | `idunn` | the daemon — keepalive supervision, deployment actuation, health | | `idunn-provision` | operator tooling — brake status, identity enrolment, provisioning | Idunn's CLI is **declarative**. There are five commands and no imperative escape hatch — you cannot hand it a shell string to run: ```bash idunn serve [runtime options] # the control plane idunn up [--no-wait] # request a deployment idunn status [--command ID] # read what happened idunn cancel # cancel a pending command idunn validate --recipe PATH [--binding PATH] # offline admission check ``` A test, `cli_exposes_only_declarative_commands`, asserts that `--deploy-command`, `--restart-command` and `--swarm-profile` are **rejected**. What a target is, how it is built, and what it is allowed to touch live in two documents, never in a command line — see [Recipes and bindings](#recipes-and-bindings). Request a deployment and watch it: ```bash idunn up ghostlight --requested-by "$USER" idunn status --command ``` `idunn up` accepts a service name or `profile:`; profiles are declared by the bindings, so `idunn up profile:full-gamecult` brings up everything bound into that profile. ### Steward orders A second root unit from the same binary, `idunn-orders` (`idunn orders serve`), takes typed orders from one steward Mind over `/run/idunn-orders/steward.sock`. It has its own store, `orders.cc`, and its own binding, `steward.toml`; the survival daemon takes no order. There is still no command string: ```bash idunn order package-install [=version]... --reason TEXT # apt-get update, install named packages, check them idunn order unit --reason TEXT idunn order deploy --reason TEXT # the same commit as `idunn up` idunn order show # status and output tails idunn order list ``` A unit order submits exactly one job, for the unit it names: every verb runs `systemctl --job-mode=ignore-requirements -- `, so systemd builds a transaction of one job. It does not start what the unit requires or restart what is part of it: order the dependency first, and order `postgresql@16-main.service` rather than the `postgresql.service` wrapper. Targets are not orderable. The job is one; what the unit does afterwards is not: systemd still runs full transactions of its own for `Restart=` after a failure, for a `StopWhenUnneeded=` dependency left without a user, and for a `Conflicts=` partner of a unit that fails, so an ordered stop of a unit that something restarts does not last, and a start of a `Restart=always` unit can report failure and be active seconds later. A unit order may act on any unit except Idunn's own (`idunn-*`) and the minds' own (`mind-self@*`, `mind-persona@*`), and the units that end or take the host (`systemd-suspend`, `systemd-hibernate`, `systemd-hybrid-sleep`, `systemd-suspend-then-hibernate`, `rescue`, `emergency`). That set is compiled into the binary and read from no configuration. An order is refused, recorded and posted when the ordered unit, or an alias it resolves from, is one of them; when it has an emergency action (`SuccessAction=`, `FailureAction=`, `StartLimitAction=`, `JobTimeoutAction=`) other than `none` or an `OnFailure=` or `OnSuccess=` unit (not for a reload); or, for a stop or restart, when another unit is `BoundBy=` it, because systemd stops that unit with it whatever the job mode; for a start or restart, when it has an `Upholds=` unit, which systemd starts in a full transaction whatever the job mode; or, for a stop, restart or reload, when it has a `ConsistsOf=` member, a wrapper such as `postgresql.service` whose verbs would exit 0 and touch nothing. A unit `systemctl show` cannot resolve fails the order. Slices, scopes, targets and globs are not units an order can name. A package order installs the packages it names and nothing else: apt runs with `--no-remove`, each name is passed as an exact-name pattern so `.` and `+` match themselves, and a last step fails the order unless every named package is installed. An order runs once, after the steward's uid is checked and the instance's brake check (`mind-control --instance check`) exits 0. The check that the order's principal and uid are the bound steward's runs again at every execution, so an order accepted before `steward.toml` changed is refused and recorded, not run. The socket belongs to the steward user alone (mode 0600); root may `show` and `list` orders but never issue one. An order that had started when the service died is recorded as lost and never run again. No order releases a brake. Acceptance and result are posted to `#operator` as a retried projection of `orders.cc`: a chat outage delays the posts and not the orders. A refusal names the field and a code, never the value it refused. ## Authority **Owner.** Deployment admission, and the continuity of installed bodies. **Inputs.** Its own typed state stores, its own credentials, and operator-authored trust anchors and bindings. It does not read a managed target's binaries, health, or availability to decide anything about itself. The one deliberate exception, stated plainly rather than hidden: Idunn reads Odin's published topology correlation to gate **deployment** promotion, so that a promotion waits for semantic discovery. It is read-only, an absent store yields `None` rather than an error, and it is structurally unreachable from the continuity path — `validate_live_providers_for_deploy` never invokes the provider check for `CommandKind::Continuity`. Odin being down blocks promotions. It cannot stop daemon survival. **The invariant.** > Idunn must be able to start, recover, observe and report while any managed > target is absent, broken, or deliberately braked. A target's brake gates mutation *of that target only*. It can neither authorize nor block Idunn's own startup, nor any other target's lifecycle. **Two brakes, not one.** Deployment and continuity are separate authorities with separate stores, declared per target in that target's binding. A binding naming one store for both is rejected outright — *"deployment and lifecycle brakes are identical"*. - *Continuity* is restarting an already-admitted installed release. Physiology. - *Deployment* is changing an artifact, revision, configuration, schema, unit, or authority binding. A deployment brake does not own the former. Suspending restart actuation requires the separately named lifecycle brake. **Forbidden.** Idunn does not depend on a managed target, and does not repair one merely to satisfy a deploy. If recovery would require that, the target dependency is the thing to cut. ## Recipes and bindings Two documents describe a target, and the split is the security model. The **recipe** lives in the target's own repository at `deployment/idunn/recipe.toml` (`gamecult.idunn.target_declaration.v1`). It declares *capability*: build and test steps, the artifacts they produce, the service's argument shape, its health contract, its state slots, and what it provides. It names no host paths, no images, no privileges. The **operator binding** lives on the host under `--bindings-dir` (`gamecult.idunn.operator_binding.v2`). It supplies everything privileged: which origin and ref are admitted, the pinned runner image, the workload's roots and hardening, the runtime trust anchor, the route, the two brake stores, rollout strategy and placement. It also declares which profiles the target belongs to. ```toml # recipe — in the target's repo, capability only [[steps]] id = "build" phase = "build" runner = "rust-build" argv = ["cargo", "build", "--locked", "--release", "-p", "thing"] [service] executable_artifact = "thing" arguments = [ { kind = "literal", value = "--store" }, { kind = "binding", name = "state_root" }, ] ``` ```toml # binding — on the host, privilege only [repository] origin = "https://github.com/GameCult/Thing.git" admitted_ref = "refs/heads/main" selection = "ref-head" recipe_path = "deployment/idunn/recipe.toml" [brakes] deployment_store = "/var/lib/gamecult/idunn-authority/thing-deployment-brake.cc" lifecycle_store = "/var/lib/gamecult/idunn-authority/thing-lifecycle-brake.cc" ``` A recipe cannot grant itself an affordance: the binding must name every affordance the recipe's runners derive, and `admit()` rejects a binding that omits one. Unknown fields are rejected on both sides. A repository that gets compromised can therefore change what is built, but not what it is allowed to touch on the host. An upstream daemon that does not speak Idunn's runtime contract is deployed with `idunn-launch` as its executable artifact and the upstream tree as a required adjacent artifact; the launcher finds the child inside the release that holds it. The launcher is the unit's process: it binds the candidate endpoint and answers Idunn's challenge there, renders the child's config, starts a stateful child only once it holds the write lease, and signs presence. Members never pass through it: the binding's `[route.member]` makes Idunn route a second listener straight to the child, which binds the address Idunn gives it in `GAMECULT_IDUNN_MEMBER_BIND`. ```toml # recipe fragment — the upstream tree is built or fetched as an artifact [service] executable_artifact = "idunn-launch" arguments = [ { kind = "literal", value = "--child" }, { kind = "literal", value = "upstream/bin/daemon" }, { kind = "literal", value = "--template" }, { kind = "literal", value = "config.tmpl" }, { kind = "literal", value = "--health-path" }, { kind = "literal", value = "/health" }, { kind = "literal", value = "--health-contract" }, { kind = "literal", value = "example.presence-launcher.v1" }, ] required_environment = [ "GAMECULT_IDUNN_RUNTIME_BUNDLE", "GAMECULT_IDUNN_CANDIDATE_BIND", "GAMECULT_IDUNN_MEMBER_BIND", ] ``` A directory artifact is sealed read-only: its files are 0444 and its directories 0555, whatever mode the build or git gave them. A file the tree must run, such as the launcher's `upstream/bin/daemon` child, is declared in `executable_members` and sealed 0555. The members are part of the sealed release address, so changing the list makes a new release. Idunn refuses the release if a member is absolute, climbs with `..`, is missing, is a symlink or sits behind one, or is not a regular file; `executable = true` on a directory output and `executable_members` on a file output are refused too. A tree may hold relative symlinks whose `..` steps all come first and climb no higher than the artifact's top (`share/terminfo/4/4025ex -> ../t/tek4025ex` is kept). An absolute link, a `..` after a name, or a climb out of the artifact refuses the release when it is sealed. Host targets (Windows) record `executable` and `executable_members` in the receipt and do not apply them. ```toml # recipe fragment — the upstream artifact declares the child it launches [[artifacts]] id = "upstream" source_kind = "runner-output" runner = "build" source = "dist/upstream" destination = "upstream" executable_members = ["bin/daemon"] ``` ```toml # binding fragment — members connect to the member endpoint, which Idunn # forwards to the private port derived from the candidate: member # private_port_start + (candidate port - route private_port_start) [route] stable_endpoint = "tcp://10.77.0.1:8008" private_port_start = 18831 private_port_end = 18839 # ... [route.member] stable_endpoint = "tcp://10.77.0.1:8448" private_port_start = 18841 # max_connections = 128 # nginx limit_conn on the member listener (1-65535) ``` ## Contracts Idunn's typed contracts are published by [CultLib](https://github.com/GameCult/CultLib) in `cultnet-rs` — deployment and lifecycle brakes, service identity, runtime activation, expected incarnation, and the provider-health family. Idunn consumes them and defines no private copies. State is CultCache (`.cc` typed documents) throughout. Health is published over CultNet RUDP. Odin keeps its own separate projection of Idunn state for aggregation. That is the all-seer's view, not Idunn's contract, and the two sets do not overlap. ## Running as a service [`deploy/`](deploy/) carries a worked systemd installation: | File | | |---|---| | `idunn-yggdrasil.service` | the unit | | `idunn-yggdrasil` | operator wrapper | | `idunn-yggdrasil.sudoers` | the narrow sudo grant the wrapper needs | The unit runs as root under `ProtectSystem=full`, `ProtectHome=yes`, `PrivateTmp=yes`. `ReadWritePaths` contains Idunn's own state, the build root, and only the runtime, state, and release roots explicitly granted to each managed target. The operator anchor, bindings, and Odin's correlation store remain read-only. Idunn admits against operator authority — it does not author it. Note `Wants=docker.service`, not `Requires=`. Only the build step is containerized; warm, fence, promote, drain and restart run on systemd, and worktree-tree recipes skip Docker entirely. A hard requirement would let a Docker outage stop Idunn itself and take daemon survival down with it. Adapt the store paths for your host. Every path in `ExecStart` is explicit on purpose; Idunn discovers no authority implicitly. ## Documentation | | | |---|---| | [`docs/migration.md`](docs/migration.md) | **start here if you run a swarm today** — moving a target from the previous generation, in order, and cutting over the authority itself | | [`docs/authority-map.md`](docs/authority-map.md) | authority map, repository declaration, operator binding, release foundation, promotion and continuity | | [`docs/deployment-authority.md`](docs/deployment-authority.md) | the deployment transaction in depth | | [`docs/signed-daemon-health-authority.md`](docs/signed-daemon-health-authority.md) | signed health admission and trust bindings | | [`docs/guide.md`](docs/guide.md) | the **previous** generation's keepalive daemon — no longer runs anywhere, kept for migration history; its commands no longer work | | [`deploy/legacy/`](deploy/legacy/) | the shell actuator that generation used, no longer running, and what replaced each part of it | ## Platform Idunn targets Linux and does not compile on Windows (`drivers.rs` uses `std::os::unix` unconditionally). A local `cargo check` on a Windows workstation proves nothing. `scripts/verify.sh` is the verification command: the library tests plus a Windows-target check of `idunn-host`. Run it on Linux with the `x86_64-pc-windows-gnu` std installed. Test and build where it runs, in the same image Odin uses, with the working tree shipped to the host's build root: ```bash git ls-files -z | tar --null -T - -cf - | ssh ygg 'sudo rm -rf /srv/build/idunn && sudo mkdir -p /srv/build/idunn && sudo tar -x -C /srv/build/idunn && sudo chown -R gamecultadmin:gamecultadmin /srv/build/idunn' ssh ygg 'sudo docker run --rm -v /srv/build/idunn:/w -v /etc/machine-id:/etc/machine-id:ro -v /srv/build/cargo-registry:/usr/local/cargo/registry -v /srv/build/cargo-git:/usr/local/cargo/git -w /w rust:1.95-bookworm sh -c "cargo test --lib && cargo build --release"' ``` `/etc/machine-id` must be mounted or every identity-enrolling test fails. `--lib` because the tests under `tests/` assume the host layout. The release binary lands in `/srv/build/idunn/target/release/idunn`; install it over `/usr/local/bin/idunn` and restart `idunn-yggdrasil.service`. ## Licence MIT.