Idunn/docs/host-actuator.md
Meta Vens 6e2a31cf31 Body map after reincarnation cut 2: two slots, take_target, forward retry, operator-required
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 02:40:55 +02:00

9.2 KiB

Host actuator: Idunn on a Windows workstation

Idunn on yggdrasil is a Linux control plane whose every driver is a local actuator: git, docker, systemd-run, nginx, a file lock. It has no remote execution. Muninn runs on Raven, a Windows workstation, inside the logged-on user's desktop session because it captures that desktop and loops its audio. Windows sshd spawns in session 0, so every remote path into the desktop session is a hop through a scheduled task, and that hop is where deaths hide.

This document is the authority map for the cut that lets Idunn manage a target on a host it cannot actuate locally. It is written before the code and maintained with it.

Objective

A Muninn deployment to Raven is one Idunn transaction: the same brake, the same sealed release, the same Expected/activation/presence chain, the same continuity, as odin on yggdrasil. Nothing on Raven is started by an operator running a script, and a dead muninn serve is seen the tick it dies.

Authority map

  • Owner: the yggdrasil Idunn daemon stays the single deployment and continuity authority. It seals, brakes, publishes Expected, issues activations, admits Odin's correlation, fences, leases and retires. The host actuator decides nothing.
  • Host actuator (idunn-host): one process per managed Windows host, running in the user session, started at logon by one scheduled task. It dials Idunn over CultNet RUDP on the WireGuard mesh and executes the consequences of two ports, runner and workload, for that host. It holds the host's service identity and signs every report with it. It keeps no transaction state; a restarted actuator re-adopts a running workload by pid, creation time and executable digest.
  • Inputs to the actuator: signed requests from Idunn carrying the compiled plan, sealed release, Expected, activation and its credential. The exact source is fetched by the actuator from the origin at the exact revision Idunn froze; the plan's recipe bytes must match the tree.
  • Outputs from the actuator: SealedRelease, installed root, and a HostWorkloadObservation (pid, creation time, executable path and sha256, command line sha256, environment names, runtime bundle, session id, user sid, exit code once exited), each signed by the host identity.
  • Transport: the actuator dials; the host opens no inbound port and holds no credential for yggdrasil. Idunn binds its actuator hub on the mesh address only. Confidentiality is WireGuard's; authenticity is the two service identities (Idunn's, already enrolled; one new profile for host actuators). A request Idunn did not sign is dropped; a report the bound host anchor did not sign is dropped.
  • Derived state: the actuator's own logs, the scheduled task's state, and Task Manager are evidence, never admission.
  • Forbidden writers: restart-muninn.ps1 and its VBS/schtasks launcher no longer start serve on a host with an actuator. The actuator install unregisters the Muninn serve task. Operator scripts on other Muninn hosts (starfire, nightwing) are untouched until those hosts get an actuator.
  • Shared paths: deploy, continuity restart, and cancel all reach Raven through the same two ports the systemd and docker drivers implement. There is no second launch path.
  • Deletion line: WorkloadDriver and RunnerDriver as single-variant enums are gone; the driver is the tag of the binding. The observation is a variant per workload kind, not a systemd struct that a Windows process would have to counterfeit. The Muninn serve scheduled task, its VBS and cmd launchers, and Register-HiddenVbsTask for serve leave restart-muninn.ps1 once Raven is admitted.

Current mechanism versus intended

Concern Today After
Runner DockerRunnerDriver, concrete field on Engine RunnerPort chosen per plan from the binding: docker on yggdrasil, host-native through the actuator
Workload SystemdTransientWorkloadDriver, concrete field WorkloadPort chosen per plan: systemd-transient or host-actuator
Observation 47-field systemd//proc struct WorkloadObservation::{Systemd, Host}, untagged on the wire so persisted records decode unchanged
Isolation proof uid, pid ns, mount ns per variant: Linux as before; host by pid and creation time, which a workstation cannot fake for two live processes
Liveness systemctl show per tick Observe request per tick over the session; a closed session is an error, and the error is "dead"
Route nginx stream fragment none for Muninn: consumers discover its endpoint through Odin's advertisement
Presence Muninn publishes none Muninn publishes a dual-signed gamecult.runtime_presence_health.v2 to Odin from the runtime bundle and activation credential the actuator hands it

Invariants that must hold across the cut

  1. A persisted transaction or admitted generation written by the previous Idunn decodes unchanged. Verified by decoding a systemd observation encoded alone as the enum.
  2. No path on yggdrasil can start, stop or observe a host workload except through the actuator session bound to that host's anchor.
  3. A host with no attached actuator is a failed observation, which continuity counts as a charged attempt and retries on its capped backoff (it never gives up; six in an hour only pages), exactly like a unit that will not start.
  4. The actuator cannot be told to run a program the recipe did not declare; allowed_programs on the host-native runner is enforced on the host.

What the cut taught, same day

Each of these was found by running the pipe against Raven, not by reading.

  • Fixed host resources cannot overlap. Muninn owns the desktop, the capture device and two advertised ports; candidate-then-promote failed at bind time. rollout.strategy = "stop-then-start" exists for exactly a route-less host-actuator workload: the admitted incumbent is stopped at Starting, before the candidate starts, and supervision leaves the target alone from Starting on: the transaction takes the target there (one take_target CAS) rather than at Fencing. A systemd-transient workload may not declare it.
  • An unobservable host is silence. The hub returns a typed HostUnobservable for not-attached, detached, reconnected and unanswered. Supervision skips that tick and logs it. Before this, an Idunn restart burned all three continuity attempts inside the actuator's 15-second reattach window and demoted a healthy Muninn.
  • The hub is serviced on its own thread. A tick that blocked on odin's docker build for a minute starved every attached host into a timeout.
  • A binding path is absolute by its own shape. The actuator re-parses the Linux binding on Windows; /var/... must not fail there.
  • The actuator's git never rewrites line endings. The frozen tree is byte-exact; Windows git's autocrlf made the recipe bytes differ from the plan's.
  • The actuator must be rebuilt when the binding vocabulary grows. It parses the same TOML the daemon does, from the same crate; an older actuator refuses a plan whose binding it cannot decode, which is the right refusal and a deploy step to remember.
  • A provider's presence sequence must survive its restart. Odin admits only above the sequence it last stored for a signer; Muninn now uses the observation clock.
  • Odin (not this repo) died on a stale provider presence. A continuity restart of raven-muninn left the previous presence in Odin's store; Odin treated a stored presence that no longer authenticated as tampering and exited, on every restart, until Idunn gave up on it. Fixed in Odin aaaa8ed: stale, logged, not fatal. Recorded here because this cut is what made a provider restart under Idunn a routine event.

Measured on Raven: cold build 1 m 20 s, warm 50 s; seal to admitted about 90 s including the brake release; a killed serve is observed on the next tick and restarted by continuity in 6 s; reattach after an Idunn restart about 20 s, during which the target is logged unobservable and untouched.

Subtraction budget

Removed: WorkloadDriver, RunnerDriver enums; the Muninn serve launcher body in restart-muninn.ps1 and its repair-raven-muninn-task-actions.ps1 once Raven is admitted. Added: one binary target (idunn-host, Windows), one module of wire records shared by both binaries, one identity profile, windows-sys behind cfg(windows). Net additive, bought by the capability the operator ordered: a Windows host inside Idunn's authority.

Build budget

idunn and idunn-provision are built for yggdrasil in docker (rust:latest, CARGO_TARGET_DIR=target/linux; the test suite needs an /etc/machine-id in the container) and installed with gamecult-ops/scripts/install-idunn-yggdrasil-release.sh. idunn-host builds on a Windows host with the stable toolchain and runs on Raven; it is not built on yggdrasil. The library compiles on both; unix-only code is cfg(unix), and the Linux-shaped test fixtures pass on a Windows checkout except the one parent-only-descriptor test.

Invoke Cargo directly as the container command or through a non-login shell. A login shell can reset PATH and hide the Rust toolchain installed in the build image.