diff --git a/README.md b/README.md index 028a955..d96a0f5 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,142 @@ # Idunn -GameCult's deployment admission and daemon-survival authority. +**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 history (136 commits). Idunn had lived as `crates/idunn-daemon` inside the -repository of a service it manages, which inverted the authority it exists to -hold. +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 | + +Run one daemon locally, with no swarm and no persistence: + +```bash +./target/release/idunn --daemon demo --restart-command "echo restart demo" +``` + +Inspect a deployment brake without touching anything: + +```bash +./target/release/idunn-provision deployment-brake-status \ + --store /var/lib/gamecult/idunn-authority/deployment-brake.cc \ + --operator-anchor /etc/gamecult/idunn/deployment-brake-operator-anchor.cc \ + --runtime-id --release-id --deployment-id +``` + +`--rudp-health-bind` is explicit and off by default. A bare single-daemon Idunn +opens no health ingress. ## Authority -**Owner.** Idunn owns deployment admission and the continuity of installed -bodies. It decides whether an artifact may be installed and whether an -already-admitted body may be restarted. +**Owner.** Deployment admission, and the continuity of installed bodies. **Inputs.** Its own typed state stores, its own credentials, and an -operator-provided trust anchor. Nothing else. +operator-provided trust anchor. Nothing else. In particular, not the state, +health, binaries or availability of anything it manages. -**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 another target's lifecycle. +**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, and the daemon refuses to start if they resolve to the -same store. Restarting an already-admitted installed release is continuity -physiology. Changing an artifact, revision, configuration, schema, unit or -authority binding is deployment. A deployment brake does not own the former. +with separate stores, and the daemon refuses to start if the two resolve to the +same store. -**Forbidden.** Idunn does not depend on any managed target. It does not repair -a target merely to satisfy a deploy; if recovery would require that, the target +- *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. ## Contracts Idunn's typed contracts are published by -[CultLib](https://github.com/GameCult/CultLib) in `cultnet-rs` — the deployment -and lifecycle brakes, service identity, runtime activation, expected -incarnation, and the provider-health family. Idunn consumes them; it does not -define its own copies. +[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. -Odin holds its own separate projection of Idunn state for aggregation, in -`odin-core`. That is Odin's view as the all-seer, not Idunn's contract, and the -two sets do not overlap. +State is CultCache (`.cc` typed documents) throughout. Health is published over +CultNet RUDP. -## Binaries +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. -- `idunn` — the daemon. -- `idunn-provision` — operator provisioning and brake inspection. +## Running as a service -## Building +[`deploy/`](deploy/) carries a worked systemd installation: -``` -cargo build -cargo test -``` +| File | | +|---|---| +| `idunn-yggdrasil.service` | the unit | +| `idunn-yggdrasil` | operator wrapper | +| `idunn-yggdrasil.sudoers` | the narrow sudo grant the wrapper needs | -**Idunn is a Linux daemon.** On Windows, 18 of 81 tests fail because they -assert against absolute deployment paths (`/srv/...`, `/var/lib/gamecult/...`) -and Rust's `Path::is_absolute()` requires a drive prefix there. That is -platform semantics, not breakage; the same 18 fail identically in the Odin tree -this was extracted from. Build and test on Linux for a true result. +The unit runs as a dedicated `idunn` user under `ProtectSystem=full`, +`ProtectHome=yes`, `PrivateTmp=yes`, with `ReadWritePaths` enumerated per +managed target and its own authority store mounted **read-only** — +`ReadOnlyPaths=/var/lib/gamecult/idunn-authority`. + +Note `Wants=docker.service`, not `Requires=`. Deployment runners use Docker; +continuity actuation does not. A hard requirement would let a Docker outage +stop Idunn itself and take daemon survival down with it. This was changed from +`Requires=` during extraction — if you are copying an older installed unit, +change it. + +Adapt the store paths, `--swarm-profile`, and `--rudp-health-bind` for your +host. Every path in `ExecStart` is explicit on purpose; Idunn discovers no +authority implicitly. + +## Documentation + +| | | +|---|---| +| [`docs/guide.md`](docs/guide.md) | what Idunn is for, who it helps, running it, what daemons should publish, typed records | +| [`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 | + +## Platform + +Idunn targets Linux. `cargo test` on Windows reports **63 passed, 18 failed**; +those 18 assert against absolute deployment paths (`/srv/...`, +`/var/lib/gamecult/...`) and Rust's `Path::is_absolute()` requires a drive +prefix there. It is platform semantics, not breakage — the same 18 fail +identically in the Odin tree this was extracted from. Build and test on Linux +for a true result. + +## Licence + +MIT. diff --git a/deploy/idunn-yggdrasil b/deploy/idunn-yggdrasil new file mode 100644 index 0000000..1d83b52 --- /dev/null +++ b/deploy/idunn-yggdrasil @@ -0,0 +1,383 @@ +#!/bin/sh +set -eu + +PATH=/usr/sbin:/usr/bin:/sbin:/bin +export PATH +LC_ALL=C +export LC_ALL + +if [ "$(id -u)" -ne 0 ]; then + echo "idunn-yggdrasil must run through its narrow sudo boundary" >&2 + exit 1 +fi + +action="${1:-}" +target="${2:-}" +compose=/srv/compose/yggdrasil-apps.yaml +brake_store=/var/lib/gamecult/idunn-authority/deployment-brake.cc +brake_lock=/var/lib/gamecult/idunn-authority/deployment-brake.cc.lock +brake_anchor=/etc/gamecult/idunn/deployment-brake-operator-anchor.cc +generation_lock=/var/lib/gamecult/idunn-authority/idunn-generation.lock +idunn_provision=/usr/local/bin/idunn-provision + +require_root_owned_nonwritable_component() { + component="$1" + kind="$2" + if [ -L "$component" ]; then + echo "privileged actuator path component must not be a symlink: $component" >&2 + exit 1 + fi + case "$kind" in + file) [ -f "$component" ] || { echo "privileged actuator path component is not a regular file: $component" >&2; exit 1; } ;; + directory) [ -d "$component" ] || { echo "privileged actuator path component is not a directory: $component" >&2; exit 1; } ;; + *) echo "internal privileged actuator path-kind error" >&2; exit 1 ;; + esac + owner="$(stat -c '%u' -- "$component")" + mode="$(stat -c '%a' -- "$component")" + [ "$owner" = 0 ] || { echo "privileged actuator path component is not root-owned: $component" >&2; exit 1; } + case "$mode" in + ''|*[!0-7]*) echo "privileged actuator path component has an unreadable mode: $component" >&2; exit 1 ;; + esac + group_mode=$(( (mode / 10) % 10 )) + other_mode=$(( mode % 10 )) + if [ $((group_mode & 2)) -ne 0 ] || [ $((other_mode & 2)) -ne 0 ]; then + echo "privileged actuator path component is group/world-writable: $component" >&2 + exit 1 + fi +} + +require_root_owned_regular_path() { + path="$1" + case "$path" in + /*) ;; + *) echo "privileged actuator path must be absolute: $path" >&2; exit 1 ;; + esac + ancestor="$(dirname -- "$path")" + while :; do + require_root_owned_nonwritable_component "$ancestor" directory + [ "$ancestor" = / ] && break + ancestor="$(dirname -- "$ancestor")" + done + require_root_owned_nonwritable_component "$path" file +} + +require_root_owned_resolved_executable() { + entrypoint="$1" + ancestor="$(dirname -- "$entrypoint")" + while :; do + require_root_owned_nonwritable_component "$ancestor" directory + [ "$ancestor" = / ] && break + ancestor="$(dirname -- "$ancestor")" + done + [ -e "$entrypoint" ] || { echo "privileged executable entrypoint is missing: $entrypoint" >&2; exit 1; } + [ "$(stat -c '%u' -- "$entrypoint")" = 0 ] || { echo "privileged executable entrypoint is not root-owned: $entrypoint" >&2; exit 1; } + resolved="$(readlink -f -- "$entrypoint")" + [ -n "$resolved" ] || { echo "privileged executable entrypoint cannot be resolved: $entrypoint" >&2; exit 1; } + require_root_owned_regular_path "$resolved" + [ -x "$resolved" ] || { echo "privileged executable is not executable: $resolved" >&2; exit 1; } +} + +acquire_idunn_generation_lock() { + # Every target consequence observes one installed Idunn generation. The + # self-bootstrap owns the exclusive side of this lock while replacing and + # accepting that generation; target manifests hold the shared side from + # admission through completion. This lock does not decide brake posture, + # target lifecycle, or release authority. + require_root_owned_regular_path "$generation_lock" + exec 8<"$generation_lock" + /usr/bin/flock --shared 8 +} + +deployment_target_policy() { + manifest_takes_action=false + minimum_source_revision_required=false + case "$target" in + odin) + daemon_id=yggdrasil-odin + repository_full_name=GameCult/Odin + upstream_ref=refs/heads/codex/ygg-idunn-independent-bootstrap + requires_bifrost_authority=false + manifest_takes_action=true + ;; + codex-connector) + daemon_id=yggdrasil-codex-connector + repository_full_name=GameCult/CodexConnector + upstream_ref=refs/heads/codex/ghostlight-release-binding + requires_bifrost_authority=false + minimum_source_revision_required=true + manifest_takes_action=true + ;; + ghostlight) + daemon_id=yggdrasil-ghostlight + repository_full_name=GameCult/Ghostlight + upstream_ref=refs/heads/codex/ghostlight-dungeon-mvp + requires_bifrost_authority=false + minimum_source_revision_required=true + manifest_takes_action=true + ;; + voidbot) + daemon_id=yggdrasil-voidbot + repository_full_name=GameCult/VoidBot + upstream_ref=refs/heads/main + requires_bifrost_authority=false + manifest_takes_action=true + ;; + heimdall) + daemon_id=yggdrasil-heimdall + repository_full_name=GameCult/Heimdall + upstream_ref=refs/heads/main + requires_bifrost_authority=false + ;; + epiphany-capstone-17) + daemon_id=yggdrasil-epiphany-capstone-17 + repository_full_name=GameCult/Epiphany + upstream_ref=refs/heads/codex/epiphany-shakedown-live + requires_bifrost_authority=false + manifest_takes_action=true + ;; + epiphany) + daemon_id=yggdrasil-epiphany + repository_full_name=GameCult/Epiphany + upstream_ref=refs/heads/codex/epiphany-shakedown-live + requires_bifrost_authority=false + manifest_takes_action=true + ;; + bifrost-persona-feedback) + daemon_id=yggdrasil-bifrost-persona-feedback + repository_full_name=GameCult/Bifrost + upstream_ref=refs/heads/main + requires_bifrost_authority=true + ;; + repixelizer) + daemon_id=yggdrasil-repixelizer + repository_full_name=GameCult/repixelizer + upstream_ref=refs/heads/main + requires_bifrost_authority=false + ;; + streampixels) + daemon_id=yggdrasil-streampixels + repository_full_name=GameCult/StreamPixels + upstream_ref=refs/heads/main + requires_bifrost_authority=false + ;; + gjallar) + daemon_id=yggdrasil-gjallar + repository_full_name=GameCult/Gjallar + upstream_ref=refs/heads/codex/yggdrasil-aggregate-daemon + requires_bifrost_authority=false + ;; + *) echo "deployment target has no root actuator policy: $target" >&2; exit 1 ;; + esac +} + +require_signed_deployment_grant() { + source_commit="$3" + deployment_request_id="$4" + release_authority_id="$5" + release_authority_sha256="$6" + + deployment_target_policy + case "$source_commit" in + *[!0-9a-f]*|'') echo "invalid or missing source commit" >&2; exit 1 ;; + esac + [ "${#source_commit}" -eq 40 ] || { echo "source commit must be 40 lowercase hex characters" >&2; exit 1; } + case "$deployment_request_id" in + *[!A-Za-z0-9._:/+-]*|'') echo "invalid or missing Idunn deployment request id" >&2; exit 1 ;; + esac + [ "${#deployment_request_id}" -le 256 ] || { echo "Idunn deployment request id is oversized" >&2; exit 1; } + case "$deployment_request_id" in + "deploy:$daemon_id:"?*|"manual:redeploy:$daemon_id:"?*) ;; + *) echo "signed deployment request is not for root target $target" >&2; exit 1 ;; + esac + + case "$requires_bifrost_authority" in + true) + [ -n "$release_authority_id" ] || { echo "missing Bifrost release authority id" >&2; exit 1; } + case "$release_authority_sha256" in + *[!0-9a-f]*|'') echo "invalid or missing Bifrost release authority digest" >&2; exit 1 ;; + esac + [ "${#release_authority_sha256}" -eq 64 ] || { echo "Bifrost release authority digest must be 64 lowercase hex characters" >&2; exit 1; } + ;; + false) + [ -z "$release_authority_id" ] || { echo "source-observed release cannot claim Bifrost authority id" >&2; exit 1; } + [ -z "$release_authority_sha256" ] || { echo "source-observed release cannot claim Bifrost authority digest" >&2; exit 1; } + ;; + esac + + require_root_owned_resolved_executable "$idunn_provision" + require_root_owned_regular_path "$brake_store" + require_root_owned_regular_path "$brake_lock" + require_root_owned_regular_path "$brake_anchor" + + # This validation is the deployment-admission point. The signed expiry is a + # start-before deadline: it denies a later transaction, but does not become a + # timer-shaped veto over one already admitted. The root actuator remains the + # sole shared-lock holder through the synchronous consequence, so an explicit + # engage/release transition cannot race or rewrite that transaction. + exec 9<"$brake_lock" + /usr/bin/flock --shared 9 + now_unix_millis="$(/usr/bin/date +%s%3N)" + case "$now_unix_millis" in + ''|*[!0-9]*) echo "root actuator clock did not produce Unix milliseconds" >&2; exit 1 ;; + esac + "$idunn_provision" deployment-brake-status \ + --store "$brake_store" \ + --operator-anchor "$brake_anchor" \ + --runtime-id yggdrasil \ + --release-id "$source_commit" \ + --deployment-id "$deployment_request_id" \ + --now-unix-millis "$now_unix_millis" + + if [ "$minimum_source_revision_required" = true ]; then + require_root_owned_resolved_executable /usr/local/bin/idunn + /usr/local/bin/idunn validate-minimum-source-revision \ + --daemon "$daemon_id" \ + --source-revision "$source_commit" + fi + + if [ "$requires_bifrost_authority" = true ]; then + require_root_owned_resolved_executable /usr/local/bin/idunn + /usr/local/bin/idunn validate-release-authority \ + --store /var/lib/gamecult/idunn/yggdrasil.keepalive.cc \ + --release-authority-store /srv/bifrost/state/repository-release-authority.cc \ + --repository-full-name "$repository_full_name" \ + --upstream-ref "$upstream_ref" \ + --source-revision "$source_commit" \ + --release-authority-id "$release_authority_id" \ + --release-authority-sha256 "$release_authority_sha256" + fi +} + +manifest_group_exists() { + kill -s 0 "-$manifest_pid" 2>/dev/null +} + +signal_manifest_group() { + kill -s "$1" "-$manifest_pid" 2>/dev/null || true +} + +forward_manifest_signal() { + manifest_wait_interrupted=true + signal_manifest_group "$1" +} + +run_manifest_while_holding_authority_locks() { + # Stay alive as the sole lock owner. The child must not inherit descriptors + # 8 or 9: flock locks attach to shared open-file descriptions, so an + # inherited descriptor could retain or unlock the parent's generation or + # deployment transaction. The owned process group also prevents ordinary + # descendants from outliving interruption. + require_root_owned_resolved_executable /usr/bin/setsid + /usr/bin/setsid "$@" 8<&- 9<&- & + manifest_pid=$! + manifest_wait_interrupted=false + trap 'forward_manifest_signal HUP' HUP + trap 'forward_manifest_signal INT' INT + trap 'forward_manifest_signal TERM' TERM + while :; do + manifest_wait_interrupted=false + if wait "$manifest_pid"; then + manifest_status=0 + else + manifest_status=$? + fi + if [ "$manifest_wait_interrupted" = true ] && manifest_group_exists; then + continue + fi + break + done + trap - HUP INT TERM + + manifest_descendants_leaked=false + if manifest_group_exists; then + manifest_descendants_leaked=true + signal_manifest_group TERM + for _ in 1 2 3 4 5; do + manifest_group_exists || break + sleep 1 + done + fi + if manifest_group_exists; then + signal_manifest_group KILL + for _ in 1 2 3 4 5; do + manifest_group_exists || break + sleep 1 + done + fi + if manifest_group_exists; then + echo "deployment manifest process group survived SIGKILL" >&2 + return 1 + fi + if [ "$manifest_descendants_leaked" = true ] && [ "$manifest_status" -eq 0 ]; then + echo "deployment manifest left a descendant process behind" >&2 + return 1 + fi + return "$manifest_status" +} + +case "$action" in + deploy) + [ "$#" -eq 6 ] || { echo "deploy requires exact target, source, request, and release-authority arguments" >&2; exit 2; } + ;; + restart) + [ "$#" -eq 2 ] || { echo "restart accepts only one exact target" >&2; exit 2; } + ;; + *) + echo "unsupported Idunn Yggdrasil actuator action: $action" >&2 + exit 2 + ;; +esac + +acquire_idunn_generation_lock + +case "$action:$target" in + restart:heimdall) + require_root_owned_regular_path "$compose" + exec docker compose -f "$compose" restart heimdall + ;; + restart:repixelizer) + require_root_owned_regular_path "$compose" + exec docker compose -f "$compose" restart repixelizer + ;; + restart:streampixels) + require_root_owned_regular_path "$compose" + docker compose -f "$compose" restart streampixels-service streampixels-web + ;; + restart:gjallar) + exec systemctl restart gjallar-yggdrasil.service + ;; + restart:voidbot|restart:epiphany|restart:odin|restart:codex-connector|restart:ghostlight) + manifest="/srv/odin/deploy-manifests/$target" + require_root_owned_regular_path "$manifest" + [ -x "$manifest" ] || { echo "root actuator manifest is not executable: $manifest" >&2; exit 1; } + export IDUNN_ACTUATOR=1 + export IDUNN_COMMAND_AUTHORITY=idunn-daemon + run_manifest_while_holding_authority_locks "$manifest" restart + exit $? + ;; + deploy:voidbot|deploy:epiphany|deploy:epiphany-capstone-17|deploy:odin|deploy:codex-connector|deploy:ghostlight|deploy:heimdall|deploy:repixelizer|deploy:streampixels|deploy:bifrost-persona-feedback|deploy:gjallar) + manifest="/srv/odin/deploy-manifests/$target" + require_root_owned_regular_path "$manifest" + [ -x "$manifest" ] || { echo "root actuator manifest is not executable: $manifest" >&2; exit 1; } + require_signed_deployment_grant "$@" + export IDUNN_ACTUATOR=1 + export IDUNN_COMMAND_AUTHORITY=idunn-daemon + export IDUNN_SOURCE_COMMIT="$source_commit" + export IDUNN_REPOSITORY_FULL_NAME="$repository_full_name" + export IDUNN_UPSTREAM_REF="$upstream_ref" + export BIFROST_RELEASE_AUTHORITY_ID="$release_authority_id" + export BIFROST_RELEASE_AUTHORITY_SHA256="$release_authority_sha256" + export IDUNN_DEPLOYMENT_REQUEST_ID="$deployment_request_id" + export IDUNN_REQUIRES_BIFROST_AUTHORITY="$requires_bifrost_authority" + if [ "$manifest_takes_action" = true ]; then + run_manifest_while_holding_authority_locks "$manifest" "$action" + exit $? + fi + run_manifest_while_holding_authority_locks "$manifest" + exit $? + ;; + *) + echo "unsupported Idunn Yggdrasil actuator request: $action $target" >&2 + exit 2 + ;; +esac diff --git a/deploy/idunn-yggdrasil.service b/deploy/idunn-yggdrasil.service new file mode 100644 index 0000000..2f9fed0 --- /dev/null +++ b/deploy/idunn-yggdrasil.service @@ -0,0 +1,30 @@ +[Unit] +Description=Idunn Yggdrasil-local lifecycle coordinator +After=network-online.target docker.service +Wants=network-online.target +# Deployment runners use Docker, but continuity actuation does not. A hard +# Requires= would let a Docker outage stop Idunn itself, killing daemon +# survival for every target. Want it, order after it, and let individual +# container-backed deploys fail on their own if it is absent. +Wants=docker.service + +[Service] +Type=simple +User=idunn +Group=idunn +# Operational cancellation ceiling only. Target daemons own readiness through +# typed health; this duration must never be interpreted as deployment success. +ExecStart=/usr/local/bin/idunn --swarm-profile yggdrasil-local --repo-root /var/lib/gamecult/idunn --store /var/lib/gamecult/idunn/yggdrasil.keepalive.cc --release-authority-store /srv/bifrost/state/repository-release-authority.cc --deployment-brake-store /var/lib/gamecult/idunn-authority/deployment-brake.cc --deployment-brake-operator-anchor /etc/gamecult/idunn/deployment-brake-operator-anchor.cc --daemon-health-trust-store /etc/gamecult/idunn/daemon-health-trust.cc --rudp-health-bind 10.77.0.1:17870 --service-identity-store /var/lib/gamecult/idunn/idunn-service-identity.cc --public-health-store /var/lib/gamecult/idunn/authenticated-provider-health.cc --public-health-query-bind 127.0.0.1:17871 --command-timeout-seconds 21600 --execute +Restart=always +RestartSec=5 +TimeoutStopSec=20 +KillSignal=SIGINT +UMask=027 +PrivateTmp=yes +ProtectHome=yes +ProtectSystem=full +ReadWritePaths=/etc/gamecult/idunn /etc/gamecult/codex-connector /etc/gamecult/ghostlight-dungeon /etc/systemd/system/codex-connector.service /etc/systemd/system/epiphany-swarm.service /etc/systemd/system/ghostlight-dungeon.service /var/lib/gamecult/idunn /srv/build /srv/voidbot +ReadOnlyPaths=/var/lib/gamecult/idunn-authority + +[Install] +WantedBy=multi-user.target diff --git a/deploy/idunn-yggdrasil.sudoers b/deploy/idunn-yggdrasil.sudoers new file mode 100644 index 0000000..dad6e44 --- /dev/null +++ b/deploy/idunn-yggdrasil.sudoers @@ -0,0 +1,24 @@ +Cmnd_Alias IDUNN_YGGDRASIL_DEPLOY = \ + /usr/local/libexec/idunn-yggdrasil deploy odin *, \ + /usr/local/libexec/idunn-yggdrasil deploy codex-connector *, \ + /usr/local/libexec/idunn-yggdrasil deploy ghostlight *, \ + /usr/local/libexec/idunn-yggdrasil deploy voidbot *, \ + /usr/local/libexec/idunn-yggdrasil deploy heimdall *, \ + /usr/local/libexec/idunn-yggdrasil deploy epiphany-capstone-17 *, \ + /usr/local/libexec/idunn-yggdrasil deploy epiphany *, \ + /usr/local/libexec/idunn-yggdrasil deploy bifrost-persona-feedback *, \ + /usr/local/libexec/idunn-yggdrasil deploy repixelizer *, \ + /usr/local/libexec/idunn-yggdrasil deploy streampixels *, \ + /usr/local/libexec/idunn-yggdrasil deploy gjallar * +Cmnd_Alias IDUNN_YGGDRASIL_RESTART = \ + /usr/local/libexec/idunn-yggdrasil restart odin, \ + /usr/local/libexec/idunn-yggdrasil restart codex-connector, \ + /usr/local/libexec/idunn-yggdrasil restart ghostlight, \ + /usr/local/libexec/idunn-yggdrasil restart voidbot, \ + /usr/local/libexec/idunn-yggdrasil restart heimdall, \ + /usr/local/libexec/idunn-yggdrasil restart epiphany, \ + /usr/local/libexec/idunn-yggdrasil restart repixelizer, \ + /usr/local/libexec/idunn-yggdrasil restart streampixels, \ + /usr/local/libexec/idunn-yggdrasil restart gjallar + +idunn ALL=(root) NOPASSWD: IDUNN_YGGDRASIL_DEPLOY, IDUNN_YGGDRASIL_RESTART diff --git a/docs/authority-map.md b/docs/authority-map.md new file mode 100644 index 0000000..7e26343 --- /dev/null +++ b/docs/authority-map.md @@ -0,0 +1,212 @@ +# Idunn + +Idunn is GameCult's independent deployment-admission and continuity control +plane. It decides which exact workload incarnation may run, write canonical +state, receive traffic, and be kept alive. It does not depend on Odin or any +managed workload in order to start or recover its own durable admitted state. + +Systemd, container runtimes, proxies, and a future Nomad or Kubernetes driver +provide generic execution and routing. Idunn owns the GameCult-specific +decision about which source, release, runtime identity, process write lease, and +route membership are authoritative. + +The detailed contract is +[`idunn-deployment-authority.md`](idunn-deployment-authority.md). + +## Authority map + +- **Owner:** Idunn selects exact source facts under root policy, compiles the + repository declaration against an operator binding, seals materialized + inputs and artifacts, admits one runtime incarnation, separately controls its + process-bound write lease and route membership, and preserves its continuity. +- **Inputs:** the exact raw repository declaration and operator-binding blobs; + private source-driver facts; artifact and external-input materialization + receipts; separate deployment and lifecycle brakes; Odin's observed semantic + topology when a graph-changing decision needs discovery; signed runtime + presence and health; Idunn's current observed-activation projection; and + observations from narrow execution and route drivers. +- **Outputs:** private compiled plans and sealed releases; a release-bound + sanitized Expected projection for Odin/CultMesh; one launch-scoped runtime + activation published only after driver observation; process write-lease and + route decisions; continuity actions; and explicit disagreement or failure + records. +- **Derived state:** dashboards, operator summaries, proxy displays, process + listings, and Odin correlation views are projections. They do not establish + deployment, write, route, presence, or readiness authority. +- **Forbidden writers:** repositories cannot grant host privileges, identities, + trust anchors, state roots, write leases, routes, placement, or rollout + policy. Odin cannot deploy, promote, route, or grant writes. Services cannot + configure themselves Present or Ready. Proxies and process managers cannot + select the admitted incarnation. Raw command strings are not deployment + authority. +- **Shared path:** `idunn up`, scheduled deployment, operator deployment, + restart, crash recovery, and host reboot recovery all use the same admitted + source-to-release identity. Continuity may restart only that identity; it + cannot fetch, rebuild, migrate, or silently change configuration. + +## Repository declaration + +A deployable repository publishes one strict declaration, normally +`deployment/idunn/recipe.toml`. It may contain: + +- literal build, test, and package argv for named runner classes; +- generic pinned HTTPS inputs with SHA-256, runner, and relative destination; +- immutable artifact outputs, optionally pinned to an expected SHA-256; +- a constrained launch contract; +- one health-contract identity; +- an optional typed state contract with state schema generation and slots; +- provided capabilities, dependency kinds, startup requirements, capacity, and + conflicts. + +The recipe does not configure runtime health states. GameCult runtime health +uses the shared `warming`, `active`, `degraded`, and `failed` ontology. Present +and Ready are Odin correlations over independently signed runtime evidence, not +recipe fields. + +Stateless services declare no state block. Their operator binding therefore has +no state root, state-transition policy, or process write-lease record. + +## Operator binding + +The root-admitted operator binding supplies the physical policy a repository +cannot grant itself: + +- source origin, admitted ref, selection policy, checkout, and Gitlink origins; +- pinned runner images, literal-program allowlists, resources, mounts, secrets, + and preconfigured network profiles; +- workload driver, identity, isolation, release root, and runtime root; +- runtime ID, expected signer identity, and trust-anchor store; +- optional state root and state-transition policy; +- an optional process write-lease record exactly when a state slot is + process-bound writable; its sibling `.lock` path is derived; +- optional route driver and stable/private endpoint binding; +- independent deployment and lifecycle brakes; +- one replica on one admitted node in the v1 foundation. + +Route membership is not a write lease. A write lease is not route membership. +Neither may be inferred from a PID, unit, socket, symlink, or health message. + +## Deterministic release foundation + +The compiled plan retains the exact raw declaration and binding blobs and +parses those exact bytes again during validation. Its content address therefore +covers the real inputs rather than caller-supplied digests beside mutable parsed +copies. + +Source-selection facts currently preserve the private driver output: the +exact commit, source tree, recipe blob, admitted ref, and Gitlink tree entries. +The contract does not claim that shape validation proves ancestry, signatures, +or object custody. Those proofs belong to the narrow source driver that will +produce the facts. + +A sealed release binds the plan ID to sorted full artifact receipts and sorted +full external-input materialization receipts. Only a sealed release validated +against its exact plan can derive Expected. Expected includes the plan and +release IDs, runtime and signer identities, health contract, node, route, +executable artifact digest, capabilities, selected dependencies, and the +optional canonical state-contract digest computed from the typed +`StateDeclaration` MessagePack encoding. + +Expected is stable release and topology intent. Each actuator launch receives a +separate tiny `idunn.runtime_activation.v1` containing the Expected digest and +one Idunn-issued opaque runtime-instance ID. Idunn publishes that activation as +current only after the workload driver observes the matching native process and +artifact. Present requires both that Idunn observation and the service's +independent signed presence; either document alone is insufficient. + +Managed dependency candidates are derived from validated Expected documents. +External operator bindings remain expected configuration only; they cannot +impersonate signed presence or readiness. + +### Source resolution and actuation map + +- **Owner:** the Idunn deployment transaction owns the transition from one + observation of a moving admitted ref to runner-readable immutable source. + The source driver resolves through a transaction-unique fetched-ref witness; + the control loop must compile and durably persist the complete + `CompiledDeploymentPlan` before any source freeze or runner actuation. +- **Inputs:** the root-admitted raw operator-binding bytes; the exact repository + declaration blob read from the selected Git tree; one unique source-resolution + ID; the fetched admitted-ref revision; the selected commit and tree; and the + complete Gitlink path, origin, and revision set admitted by that binding. +- **Outputs:** private `ResolvedSource` facts used to compile the plan; a + plan-only exact archive in root-owned transaction storage; and a + `FrozenSourceReceipt` binding the transaction ID, plan ID, and snapshot digest. + The receipt is embedded in the observed `FrozenSource` and must validate + against the plan before a runner may consume the tree. +- **Derived state:** the source-identity checkout, unique fetched ref, temporary + Gitlink checkouts, extracted runner workspaces, and in-memory `FrozenSource` + view are disposable projections. None may replace the persisted plan, + receipt, or sealed release as transaction authority. +- **Forbidden writers:** a repository identity may perform Git and network + reads but cannot write the root-owned frozen tree. A moving ref, mutable + checkout, caller-supplied parsed binding, fresh recipe read, or incomplete + Gitlink subset cannot revise a compiled transaction. Resume may not resolve a + fresh binding or ref, and runner actuation may not accept source without its + plan-bound receipt. +- **Shared path:** initial execution resolves once, compiles and persists the + full plan, freezes only from that plan, persists the receipt, re-observes the + immutable snapshot, and runs the bound builders. Retry and crash recovery use + the same persisted plan and receipt; if freeze must be repeated, it fetches + only the exact planned revisions and never observes the moving ref again. + Transaction source storage is cleaned only after the sealed release has been + durably persisted. +- **Deletion line:** source-owned worktrees and Git metadata do not cross into + actuation storage; source selection does not return a runner-usable mutable + checkout; and no resume path is permitted to reconstruct authority from the + current repository binding or branch head. +- **Verification layer:** focused source-driver tests observe the unique + admitted-ref witness, exact Gitlink-set equality, root ownership, canonical + read-only modes, bounded symlinks, snapshot digest, and plan/receipt binding. + Transaction crash tests must additionally prove that every boundary after + plan persistence resumes without ref resolution and that cleanup cannot run + before sealed-release persistence. + +## Promotion and continuity + +For a routed stateful singleton, Idunn starts the candidate privately, observes +the exact actuator launch, publishes its current activation, and observes its +signed warming presence. It then fences the incumbent writer, grants the +candidate its exact process-bound write lease, and waits for signed active +health plus Ready correlation. It changes and observes route membership only +afterward. Only after the proxy has adopted the candidate may Idunn clear the +promotion fence and drain the incumbent. + +The deployment brake gates changes to source, release, configuration, schema, +unit, or authority binding. The lifecycle brake separately gates restart of an +already-admitted incarnation. Neither target brake may gate Idunn itself or an +unrelated service. + +If Odin is unavailable, Idunn preserves existing routes and continuity for +already-admitted incarnations. It starts no graph-changing deployment or +promotion. A merely sealed candidate waits. An already-fenced transaction may +promote only when its frozen evidence already contains the exact Odin Ready +receipt for this runtime instance and presence digest; otherwise it waits, +rolls back through the same fence when reversible, or fails closed. + +## Operator surface + +The intended entry points are small: + +```text +idunn up ghostlight +idunn up profile:aetheria +idunn up profile:full-gamecult +``` + +Idunn ensures one compatible Odin exists, publishes the desired fleet, resolves +typed capability dependencies, admits candidates, waits for independently +observed presence and readiness, and promotes routes only when the required +graph conditions hold. + +## Current implementation boundary + +`crates/idunn-daemon/src/deployment.rs` and `deployment_plan.rs` implement the +deterministic declaration, binding, plan, release, and Expected contracts. +`drivers.rs` implements the narrow source resolution/freeze, runner, systemd, +write-lease, topology, and route actuator ports. The transaction engine still +must persist and sequence the compiled plan, frozen-source receipt, sealed +release, activation, lease, readiness, route, and admitted generation. Driver +ports are consequences, not a second deployment controller; runtime integration +must not recreate target catalogs, raw deployment commands, root Git inspection, +or a second admission opinion. diff --git a/docs/deployment-authority.md b/docs/deployment-authority.md new file mode 100644 index 0000000..fafa1f2 --- /dev/null +++ b/docs/deployment-authority.md @@ -0,0 +1,355 @@ +# Idunn Deployment Authority + +Idunn is GameCult's deployment, runtime-admission, continuity, and future swarm +control plane. It decides which exact workload incarnation may act. It does not +replace systemd, container runtimes, nginx, or a future general-purpose +orchestrator. + +The architectural test is simple: + +- Where and how a workload runs is generic infrastructure work. Use the + configured systemd, container, proxy, or later Nomad/Kubernetes driver. +- Which GameCult incarnation has authority to serve, write state, or represent + an admitted workload is Idunn work. + +Idunn and Odin are closely integrated but do not share authority. Idunn owns +the admitted physical swarm. Odin owns the discoverable semantic topology of +the Verse. A service owns its signed runtime presence, capabilities, capacity, +and health. CultMesh carries typed projections between them. + +## Authority map + +- **Owner:** Idunn alone selects an exact source, admits its repository recipe + under an operator binding, records the resulting artifact and process + provenance, grants a process-bound state-write lease, separately admits + route membership, and keeps the admitted incarnation alive. +- **Inputs:** root-admitted operator bindings; source-owned strict deployment + declarations; exact Git objects read as the `idunn` identity; signed + deployment or lifecycle-brake state; signed incarnation-bound service + presence and health; Odin's observed capability topology when graph changes + require semantic discovery; and observations from the configured workload + and route drivers. +- **Outputs:** typed CultCache deployment plans and receipts; an Idunn-owned + expected-fleet CultMesh projection; process-bound write grants; admitted + route membership; deployment and continuity decisions; and explicit + expected/present/ready disagreement records. +- **Derived state:** release dashboards, operator summaries, route displays, + capacity summaries, and Odin's correlation views are projections. A target + count, proxy backend, service-manager unit, container label, or configured + endpoint cannot establish admission by itself. +- **Forbidden writers:** target repositories cannot grant themselves users, + secrets, mounts, network, capabilities, resources, host paths, routes, + placement, source refs, or rollout policy. Odin cannot promote a process or + grant state writes. nginx cannot choose membership. systemd and container + runtimes cannot choose the admitted incarnation. Service configuration cannot + impersonate observed presence or readiness. Persisted shell strings and + repository-supplied root programs are not executable authority. +- **Shared paths:** `idunn up`, scheduled deployment, manual deployment, + scaling, restart, host reboot recovery, and crash recovery use the same + source-to-recipe-to-artifact provenance and the same distinct write-lease and + route-membership primitives. + Deployment may change that chain only while the deployment brake admits the + exact transaction. Continuity may restart only the already-admitted chain and + is governed by a separate lifecycle brake. +- **Deletion line:** hard-coded target copies, raw `deploy_command` and + `restart_command` state, `sh -c` actuation, root Git inspection, staged + `/srv/odin/deploy-manifests`, target-specific gamecult-ops deploy programs, + duplicate target units, and stop-incumbent-before-probe rollouts must cease to + decide Ghostlight or CodexConnector deployment before either target is + promoted through this design. + +### Current implementation boundary + +`deployment.rs` and `deployment_plan.rs` are the deterministic contract +foundation only. They parse one strict source declaration and one root-admitted +operator binding from the exact raw blobs retained by the compiled plan, check +private source-selection facts, derive managed providers from validated Expected +documents, and content-address a sealed release. They do not yet +drive Git, runners, systemd, nginx, brakes, CultCache, CultMesh, or the legacy +Idunn supervisor. + +The full operator binding, compiled plan, source facts, and sealed release +are private Idunn control-plane state. They contain host paths and operational +bindings and must never be published as Odin topology. Only the sanitized +`ExpectedIncarnation` projection is shaped for later CultMesh publication. It +is derived only from a sealed release validated against its compiled plan and +names both content addresses and the executable artifact digest. Present and +Ready remain Odin-owned correlations over independently verified runtime +evidence; the deployment-plan module deliberately does not model them. + +## The three truths Odin exposes + +For each workload incarnation, Odin keeps three distinct observations: + +1. **Expected** — Idunn has sealed this exact incarnation and has published its + plan and release identities, admitted runtime and signer identities, health + and optional canonical typed-MessagePack state-contract identities, + endpoints, declared capabilities, and dependency role. +2. **Present** — Idunn has published the launch-scoped activation only after its + actuator observed the exact runtime instance, and that instance has + established its own matching signed CultNet/CultMesh runtime session. +3. **Ready** — the observed incarnation satisfies its signed capability and + health contract and its required dependency graph is ready. + +Correlation never repairs disagreement. Incarnation, runtime identity, endpoint, +schema, capability, and dependency mismatches remain visible and block +promotion. Configuration contributes only expected state; it cannot supply +present or ready state. + +Every Idunn-managed deployment has an Odin-visible expected projection before +dependent services are promoted, and an Odin-observed runtime projection before +they are considered ready. + +## Source-owned deployment declaration + +Each deployable repository publishes one small, reviewable declaration at a +fixed path such as `deployment/idunn/recipe.toml`. Unknown fields are rejected. +The declaration contains no host commands or privileges. It describes: + +- schema and target identity; +- requested runner profiles; +- build and test argv executed inside those runners; +- pinned generic HTTPS inputs and immutable artifact outputs; +- a constrained launch contract naming the packaged executable, literal + arguments, operator-bound argument slots, and required environment names; +- one health contract and, only for stateful services, a state/schema contract; +- capabilities provided, including schema and compatibility information; +- bootstrap, required, optional, shared-infrastructure, private, and external + operator-bound capability dependencies; +- startup ordering, conflicts, migration conditions, and minimum capacity when + they matter. + +Dependencies resolve by typed capability/provider compatibility. A declaration +names a daemon only when identity itself is the contract; ordinary dependencies +must not hard-code a provider instance. + +Runtime health uses the shared `warming`, `active`, `degraded`, and `failed` +ontology. A recipe names only its health contract. It cannot redefine those +states, readiness, or operator-facing detail. Bootstrap dependencies must be +available before process start; the remaining dependency kinds retain their +typed intent without making this foundation a scheduler. + +The declaration may run repository code only inside a runner sandbox admitted +by the operator binding. It cannot publish a host path, Docker argument, +systemd property, nginx fragment, UID, secret path, or capability grant. Idunn +deterministically lowers the constrained launch contract through the configured +workload driver; repository-owned raw unit or container-runtime text is not an +input. + +## Operator binding + +An operator binding is static root-admitted configuration, not observed state. +For one target on one fleet it owns: + +- canonical repository origin, admitted ref, and minimum revision; +- exact runner kind, image digest, and affordances for each requested profile; +- source, cache, output, release, and runtime roots, plus state roots and + transition policy only when the recipe declares state; +- preconfigured runner network profiles, mounts, identities, secrets, + capabilities, resource bounds, and device access; +- workload driver, transient-unit namespace, private endpoint range, and stable + route; +- runtime identity, expected signer identity, and trust-anchor store; +- a process-write-lease record only when the recipe declares process-bound + writable state; its sibling lock path is derived rather than configured; +- route driver and its root-owned configuration destination; +- rollout, drain, irreversible-cut, and retention policy; +- one admitted replica on one named node in v1; later placement and scaling are + delegated to an established orchestrator driver; +- externally supplied capability bindings. + +A recipe request that has no compatible admitted binding fails closed. A +binding can grant less than requested; it cannot silently substitute a +different semantic capability. + +`gamecult-ops` publishes these fleet bindings and provisions host substrate. It +does not own application build or deployment programs. + +## Typed deployment state + +Mutable control-plane state will be CultCache. Runtime integration must use +typed records for: + +- requested service or profile; +- resolved capability graph and exact providers; +- exact source and recipe digest; +- runner binding and artifact digests; +- full materialization receipts for every pinned external input and artifact; +- candidate runtime identity, private endpoint, and signed runtime health; +- launch-scoped runtime activation, published as current only after workload- + driver observation; +- process-bound write lease; +- independently admitted route membership; +- expected, present, and ready correlation; +- promotion, drain, failure, and continuity results. + +Old records containing raw commands are readable history only. No executor may +interpret their command text. + +State transition is also typed authority. The repository may package a +migration artifact and declare its accepted source schemas, destination schema, +and literal/operator-bound argv. The operator binding admits the state roots, +backup root, and whether this rollout permits migration or a fresh-root cut. +Idunn alone issues a fenced one-shot migration grant bound to the exact source, +artifact, state schema generation, and deployment incarnation. A target +migration never receives general deployment or lifecycle authority. + +## Initial drivers and replaceable boundary + +The first Yggdrasil implementation uses: + +- transient systemd units for process lifetime and isolation, regenerated from + Idunn's admitted state after Idunn starts; +- Docker only for admitted build/test/package runners; +- nginx HTTP proxying for Ghostlight; +- nginx stream proxying for CodexConnector's stable TCP endpoint; +- atomic root-owned files for process-bound write leases and separately + generated proxy membership; +- graceful nginx validation and reload for route changes. + +Idunn calls narrow workload, route, and write-lease/fencing driver ports. Those +ports express stage, start, observe, stop, validate route, fence an incumbent, +publish a process-bound write lease, and separately promote route membership. +They do not express GameCult admission decisions. A later Nomad or Kubernetes +driver may implement the same consequences. Idunn must not grow general +bin-packing, distributed scheduling, service-mesh, container-format, +cryptography, or consensus logic; when those become requirements, place +Idunn's admission semantics over an established orchestrator. + +## Candidate promotion + +For a singleton incarnation: + +1. Resolve the required capability graph. Reuse a compatible admitted shared + provider, especially Odin, rather than spawning a duplicate. +2. Publish desired fleet intent. Intent is not an Expected incarnation. +3. Freeze the exact source and recipe as `idunn`, then copy and verify it into a + root-owned actuation stage. The privileged driver never opens Git. +4. Build, test, and package inside the bound runners. Seal the artifact, + declaration, binding, launch contract, and input digests. +5. Derive and publish the release-bound Expected incarnation from the validated + sealed release and its exact compiled plan. +6. Issue one opaque runtime-instance activation bound to the Expected digest, + pass the exact Expected and activation documents through the actuator's + isolated credential mechanism, and start the candidate privately. It has no + stable route or canonical write lease. +7. After the workload driver observes the exact native runtime and executable + artifact, publish that activation as current. Require the candidate's own + signed matching `warming` presence. Render and validate the proposed route + before changing either authority. +8. Open a named promotion transaction and separately fence the incumbent + writer. The incumbent may remain routed but its process-bound write authority + is revoked and it must return a retryable unavailable result. +9. Grant the private candidate its own process-bound write lease. Keep it off + the stable route and wait for signed `active` health and the independently + correlated Ready observation. +10. Publish the candidate backend, gracefully reload the route driver, and + observe that the proxy has adopted it. Proxy lag is explicit transaction + state, not readiness. +11. Clear the promotion fence only after route adoption is observed, then drain + and stop the incumbent. + +The state-write lease and route membership are distinct authorities. Fencing +the incumbent does not grant the candidate or change the route. Granting the +candidate permits that exact private process to write but does not route stable +traffic. Changing the route grants no write authority. The promotion +transaction clears its fence only after the independently observed write and +route authorities name the candidate. A PID, endpoint, symlink, or unit alone +grants neither authority. + +Promotion persists crash-resumable phases: planned, materialized, sealed, +activation-issued, candidate-started, activation-observed, warming, +incumbent-fenced, write-lease-granted, ready-observed, route-observed, +fence-cleared, draining, and complete. A dead Idunn cannot leave a second +writer. Its successor resumes the exact sealed transaction or restores the +prior admitted incarnation through the same fence; it never infers completion +from a symlink or eventual health convergence. + +An irreversible state cut fails closed after the write lease moves. A reversible +cut may restore the incumbent only through the same process-write-lease +primitive and only while its artifact and state contract remain valid. + +## Continuity and outage behavior + +Continuity restarts the exact sealed release, binding, endpoint, runtime +identity, and route membership. Every restart receives a new runtime-instance +activation and, for a state writer, a new process-bound lease only after the new +warming presence is observed. A lease from the dead process is never reused. A +continuity restart does not fetch a branch, rebuild, rerun a migration, alter +the launch contract, or consume the deployment brake. An explicit lifecycle +brake may suspend this actuation without changing deployment authority. + +Idunn starts and recovers from its own durable admitted state. Odin is the first +managed semantic daemon, never an Idunn bootstrap dependency. Odin's initial +admission is the sole graph-bootstrap exception: Idunn uses its root-admitted +local binding, publishes Odin's expected projection into durable CultMesh state, +and waits for Odin's observed activation and signed presence before admitting +dependents. + +When Odin is unavailable, Idunn continues health observation and continuity for +already admitted incarnations and leaves current routes in place. It does not +begin a graph-changing transaction, and a merely sealed candidate may not start +promotion. Only a transaction whose incumbent has already been fenced may +promote when its frozen evidence already contains the exact Odin Ready receipt +for this runtime instance and signed-presence digest. Otherwise it waits, rolls +back through the same fence when reversible, or fails closed; Idunn never +manufactures Ready locally. New deployment, dependency +substitution, promotion, or scaling waits for semantic discovery. When Odin +returns, disagreement is correlated and exposed before graph-changing work +resumes. + +Source selection is explicit per binding: a pinned object, the captured head of +an admitted ref, or a signed release authorization. The current deterministic +foundation retains the exact selected commit, tree, recipe blob, and Gitlink +facts, but shape validation is not proof of Git ancestry, signature authority, +or object custody. Those proofs belong to the narrow Idunn-owned source driver +that will produce these private facts. “Whatever the branch points to when root +runs” is not a selection policy. + +## Garden path + +The ordinary operator surface is: + +```text +install/start Idunn +idunn up ghostlight +idunn up profile:aetheria +idunn up profile:full-gamecult +``` + +`idunn up` ensures a compatible Odin is admitted first, publishes desired fleet +state, resolves the capability graph, stages candidates, waits for signed +presence and readiness, then promotes routes. An “Idunn script” means the small +repository declaration plus this standard command, not another imperative host +deployment program. + +## Verification + +The first live cut is complete only when tests and host observations prove: + +- exact source → recipe → artifact → process provenance; +- an issued but driver-unobserved activation cannot establish Present, and a + prior runtime instance cannot replay presence after restart; +- target requests cannot grant an omitted runner or host affordance; +- old command-bearing records and old target scripts are unexecutable; +- a candidate cannot write before its process-bound lease or receive the stable + route before separate route admission; +- route membership and write leases remain separate records and name the same + incarnation and process before the promotion fence clears; +- expected, present, and ready disagreement blocks promotion and remains + observable; +- replayed or stale signed health cannot satisfy present or ready; +- external operator-bound dependencies still require their declared observed + health/readiness contract; configuration supplies only expected state; +- a compatible shared Odin is reused and a second provider is not spawned; +- Idunn death during every promotion phase resumes or fails closed without a + second writer; +- proxy-driver lag remains visible and cannot be mistaken for route admission; +- the old process rejects writes immediately after fence or lease revocation; +- same-release crash recovery works while the deployment brake is engaged; +- the lifecycle brake can stop continuity without admitting a deployment; +- Odin loss preserves current continuity/routes and blocks graph changes; +- the incumbent remains routed until the candidate has signed `active` health + and is correlated Ready; +- the old unit, release, state root, grant, and route cannot regain authority; +- Ghostlight restart preserves the exact world journal and digest identity. diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..9829bdc --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,325 @@ +# Idunn + +> **Note.** This guide predates Idunn's extraction from Odin on 2026-09-05. +> Build and run commands have been updated from Odin's npm wrappers to direct +> cargo invocations, and paths on the lost `E:` drive have been generalised. +> Anything else describing Odin's tree may still be stale; the README is +> current, this document is the deeper reference. + +Idunn is the keepalive daemon for the Odin swarm. + +In plain language: Idunn is the little service whose job is to know which +GameCult daemons are supposed to be alive, bring them back after a reboot or +crash, watch whether they are still healthy, and raise a clear alarm when a +human needs to intervene. + +It is named for Idunn, the keeper of the apples that keep the gods young. The +job is not glamorous. That is the point. Good infrastructure should make +important things boring. + +## What Idunn Is For + +GameCult has many daemons: Odin, Bifrost, VoidBot, Mimir, Heimdall, Eve +providers, service workers, renderers, bridges, and local machine helpers. Each +one owns its own work, but each one should not have to carry its own private +lifecycle machinery. + +Idunn centralizes that lifecycle work: + +- start known daemons after machine boot; +- ensure deployed daemon artifacts match the desired target revision; +- restart daemons after crashes; +- watch health and freshness signals; +- avoid restarting services when authority is unclear; +- record every deployment/restart request and result as typed state; +- record release targets, deployment artifacts, state migrations, and rollout + results as typed state; +- escalate to an operator through Bifrost when human action is needed. + +The desired shape is simple: + +```text +Odin knows what exists. +Idunn keeps it alive. +Bifrost carries operator/public crossings. +Each daemon owns its own work and health publication. +``` + +## Who It Helps + +Idunn is for anyone running a local GameCult swarm or operating a hosted +GameCult machine. + +For a developer, Idunn should mean fewer mystery services to restart by hand. +For an operator, Idunn should mean health problems become visible, witnessed, +and routed to the right place. For daemon authors, Idunn should mean they can +publish health, transport profiles, and command boundaries over CultNet RUDP +instead of building one more private watchdog. + +## Current State + +Idunn is a Rust daemon inside Odin's Cargo workspace. Starfire and Yggdrasil +run separate host-local swarm profiles. Starfire owns its local adjunct daemons +and remote display/telemetry lanes. Yggdrasil owns its hosted release targets, +including Odin and VoidBot. Idunn starts independently of every managed target, +so an Odin outage cannot remove the organ responsible for recovering Odin. +Each target declares a daemon-owned health contract and keeps +its own interval and deploy/restart authority. Daemon truth comes from typed +CultNet/RUDP publication and daemon-owned boundary stores. Shell scripts remain +deployment, restart, and manual diagnostic lowerings only; they do not satisfy +health. +The scheduler and continuity witness now belong to one Rust process instead of +a PowerShell-herded pile of one-daemon workers. + +VoidBot deployment authority is deliberately absent from `starfire-local`. +Pushing VoidBot's upstream `main` branch is the release input; Yggdrasil-local +Idunn observes `/srv/build/VoidBot` and invokes only +`sudo -n /usr/local/libexec/idunn-yggdrasil deploy voidbot`. The privileged +actuator delegates deploy and restart to the root-owned executable +`/srv/odin/deploy-manifests/voidbot`. A developer checkout is not a deployment +target and agents must not create a local keepalive or deploy path for VoidBot. +Each cycle fetches `origin/main` in `/srv/build/VoidBot` and compares it with +`DEPLOYED_REVISION` in `/srv/voidbot/deploy/deployment.env`. Drift owns the +deployment decision. An unavailable fetch or witness raises an operator alarm +instead of guessing. The privileged manifest must atomically update that +witness only after the new release is verified; Idunn rejects an otherwise +successful command when the witness has not converged. + +```text +scratch/idunn/idunn.keepalive.cc +``` + +## Run It + +From the Idunn repository root: + +```powershell +cargo build --release +cargo run --release --bin idunn -- --daemon demo --rudp-health-bind +``` + +`--rudp-health-bind` is explicit. Bare one-daemon Idunn does not open a +localhost health ingress. + +Install the local Starfire boot watchdog: + +```powershell +.\scripts\install-idunn-startup.ps1 +``` + +That task starts one `idunn.exe` swarm supervisor at user logon. The Rust +runtime owns the target catalog for Odin, Stonks, Muninn, the enforced +Yggdrasil source artifact lanes, and the Nightwing display services. The +current Mimir dashboard is observed but not restarted until its restart +authority is named. + +It also starts `idunn-swarm-deployment-coverage`, which watches the deployment +target catalog in `scripts/idunn-deployment-targets.ps1`. A repo/service is not +allowed to vanish into shrug-space: it is either enforced by Idunn, explicitly +blocked with the missing authority named, external-owned, or not a runtime. + +To record that a daemon has not published health, run a manual target without a +matching `idunn.daemon_health` RUDP publication: + +```powershell +cargo run --release --bin idunn -- --daemon demo --restart-command "echo restart demo" +``` + +Idunn writes a non-actuating missing-publication health record and plans from +that typed absence. It does not run a local health command to decide daemon +truth. + +To actually allow restart/deploy actuation after typed health planning decides +one is needed: + +```powershell +cargo run --release --bin idunn -- --daemon demo --deploy-command "echo deploy demo" --restart-command "echo restart demo" --execute +``` + +To keep watching on a resident interval: + +```powershell +cargo run --release --bin idunn -- --daemon demo --interval-seconds 30 +``` + +To run the built-in Starfire-local swarm profile directly: + +```powershell +cargo run --release --bin idunn -- --swarm-profile starfire-local --repo-root --execute +``` + +The Yggdrasil service runs the hosted profile directly: + +```sh +/usr/local/bin/idunn --swarm-profile yggdrasil-local --repo-root /srv/odin/source --store /var/lib/gamecult/idunn/yggdrasil.keepalive.cc --rudp-health-bind 10.77.0.1:17870 --execute +``` + +Optional store override: + +```powershell +cargo run --release --bin idunn -- --daemon demo --store E:\path\to\idunn.keepalive.cc +``` + +## What Daemons Should Publish + +Idunn should not guess private service truth. A daemon should publish: + +- its service id and Verse id; +- where its durable `.cc` state or witness lives; +- a health or freshness signal; +- the command boundary for deployment or artifact refresh, if one exists; +- the command boundary for restart or recovery, if one exists; +- what operator action is needed when automatic recovery is unsafe. + +If that information is missing, Idunn should fail closed and create an operator +alarm instead of improvising. + +For the repo swarm, deployment ownership begins with catalog coverage. Use: + +```powershell +.\scripts\show-idunn-deployment-targets.ps1 +.\scripts\health-idunn-swarm-deployment-coverage.cmd +``` + +The enforced targets are Nightwing Gjallar plus the Yggdrasil source artifact +lanes whose ops runbooks can produce and verify deployment manifests. Each +enforced target must declare upstream remote/branch, rollout strategy, state +migration authority, and zero-downtime capability. Idunn deploys the declared +declared upstream revision, not arbitrary local `HEAD`. Ghostlight's catalog +entry remains disabled until its Linux body and signed health identity are +admitted. Bifrost is blocked until +its production database migration path is coherent; mobile device installs +remain blocked at their physical approval boundaries; GitHub Pages remains +external-owned. + +## Typed Records + +- `idunn.desired_daemon.v1` +- `idunn.daemon_health.v1` +- `idunn.signed_health_admission.v1` +- `idunn.keepalive_decision.v1` +- `idunn.deployment_request.v2` +- `idunn.current_deployment_request.v1` +- `idunn.deployment_result.v1` +- `idunn.release_target.v1` +- `idunn.deployment_artifact.v1` +- `idunn.state_migration_plan.v1` +- `idunn.state_migration_result.v1` +- `idunn.rollout_plan.v1` +- `idunn.rollout_result.v1` +- `idunn.restart_request.v1` +- `idunn.restart_result.v1` +- `idunn.operator_alarm.v1` +- `idunn.daemon_surgery_plan.v1` +- `idunn.daemon_transport_profile.v1` +- `idunn.command_boundary.v1` +- `idunn.runtime_transport_check.v1` +- `idunn.rudp_health_ingress.v1` + +Idunn publishes one `idunn.daemon_surgery_plan.v1` record per swarm target when +the swarm starts. Those records make the CultNet/RUDP migration queue explicit: +owner, objective, current mechanism, intended authority, cut line, +steps, blockers, severity, and status. + +It also publishes one `idunn.daemon_transport_profile.v1` and one +`idunn.command_boundary.v1` per target. The desired daemon record links to +both. The transport profile names `cultnet.transport.rudp.v0` as the target and +marks daemon publication and daemon-owned witness stores as the health +substrate. The command boundary names restart, deploy, health, and alarm +authority separately; health authority belongs to daemon-published RUDP state. + +At startup Idunn also publishes `idunn.runtime_transport_check.v1`, currently a +loopback CultNet hello over `cultnet.transport.rudp.v0`. That proves Idunn's +Rust body can use the RUDP substrate before it asks the rest of the swarm to +walk through the same door. + +For deploy-enforced targets, startup also publishes `idunn.release_target.v1`, +`idunn.deployment_artifact.v1`, `idunn.state_migration_plan.v1`, and +`idunn.rollout_plan.v1`. During deployment, Idunn runs any declared +daemon-owned migration command before the deploy command; a failed migration +stops the rollout and raises an operator alarm. Zero downtime is recorded only +when the target declares a real in-place, blue/green, or rolling strategy; +otherwise Idunn says `restart-required` and verifies that path honestly. + +Deploy scripts are Idunn actuators. Agents should configure the target catalog, +release target, command boundary, migration command, and daemon publication +surface, then let Idunn run the rollout. Direct script execution without +`IDUNN_ACTUATOR=1` fails on purpose; the little hand reaching for the deploy +button has been removed from the machine. + +`scripts/start-idunn-local.ps1` launches the local swarm with +`--rudp-health-bind 0.0.0.0:17870` so host-local and explicitly configured peers +can publish to the configured Idunn health endpoint. It accepts raw +`idunn.daemon_health` document puts on the canonical CNR0 RUDP `schema` channel +and writes them into the keepalive store. That is daemon-owned health +publication, not restart/deploy authority. On Windows, UDP reset reports from +closed one-shot publishers are treated as nonfatal ingress noise so one accepted +health frame cannot kill the listener. + +Epiphany uses a separate signed runtime-health schema. Idunn joins that document +to the exact latest Bifrost-authorized deployment request and persists +`idunn.signed_health_admission.v1`. The privileged admission verifier reads only +that fresh, current, request-bound record; generic health remains observation. + +When a fresh daemon-published RUDP health record exists for a target, Idunn uses +that record for keepalive planning. The record must match the daemon id, health +contract, RUDP transport, and freshness window. If it is absent or stale, Idunn +publishes a missing-publication observation and raises/plans from that typed +absence instead of running a local health probe. + +The active transport cut is no longer "make the daemons speak RUDP at all." +Nightwing Gjallar now consumes Odin's accepted +`surface:gamecult.network.status` snapshot over CultNet/RUDP, so every active +Starfire-local daemon target now publishes daemon-owned RUDP health plus typed +boundary state. The remaining debt is deleting bridge-only lowerings and +xenos-boundary ingestion shims without letting them reclaim ownership. Odin +still owes removal of hashed-store/C# witness shims once direct interoperable +witness publication is ready. + +Muninn media uses a codec-owned prediction model, not a custom visual-delta +codec. The canonical LAN profile is +`muninn.rudp.low_latency_h264_lan.v1`: NVENC H.264 owns inter-frame prediction, +motion search, residuals, keyframe production, and the actual lossy visual +encoding; CultNet/RUDP owns bounded delivery of typed video access units, audio +packets, receiver feedback, retransmit pressure, expiry, and keyframe requests. +Late media is dropped when it misses the fixed latency budget. Optical flow, +foveation, ROI maps, or stochastic patch experiments may become future Muninn +profiles only after this hardware-codec RUDP path is measured and proven +insufficient. + +Raven Muninn task actions are also an explicit ops invariant: Task Scheduler +must execute `wscript.exe //B //Nologo` hidden launcher actions directly for +`GameCult-Muninn`, `GameCult-Muninn-Activate`, and +`GameCult-Muninn-VideoProof`, never raw `.cmd` wrappers, and the hidden VBS +layer must call `.ps1` launchers directly instead of `cmdPath` trampolines. +Those three live Raven tasks have been repaired and verified. The repair +actuator now uploads its Raven PowerShell body with `sftp` and runs a tiny +cleanup wrapper, so the hidden-task repair does not hit Windows command-line +limits before it can run. The long-running Muninn serve bodies on Raven, +Nightwing, and Starfire now also carry their own `--idunn-rudp-health`, +`--idunn-daemon`, and `--idunn-health-contract` arguments, and live Idunn +accepts those daemon-owned health records directly instead of relying on +one-shot health commands for the truth. + +From here on out, the work is not "teach another daemon to speak." It is +"delete the witness masks and keep ownership where it belongs." + +Rust no longer gets to claim the transport is imaginary: +CultLib's `cultnet-rs` carries the canonical CNR0 `cultnet.transport.rudp.v0` +session path for acknowledged CultNet messages over UDP. That is substrate +only. Idunn still marks daemon profiles as +`migration-required` until each daemon actually publishes health and command +boundary records through that path. + +## Boundaries + +Idunn is not Odin. It does not decide which Verses exist. + +Idunn is not Bifrost. It does not own public crossings, Discord delivery, owner +DMs, or governance transport. + +Idunn is not Eve. It does not own presentation. + +Idunn is the keeper of continuity. If a daemon should be alive, Idunn should +know that, check it, help it recover, and leave a witness trail sharp enough +that nobody has to guess what happened. diff --git a/docs/signed-daemon-health-authority.md b/docs/signed-daemon-health-authority.md new file mode 100644 index 0000000..c2cff0c --- /dev/null +++ b/docs/signed-daemon-health-authority.md @@ -0,0 +1,227 @@ +# Idunn signed daemon-health authority + +## Objective + +Idunn must know which daemon authored a health statement before that statement +can affect lifecycle judgment or operator status. A typed packet is a shape, +not an identity. The current generic `idunn.daemon_health.v1` RUDP path checks +shape, daemon id, contract, self-declared publication source, and freshness but +does not authenticate the publisher. It is diagnostic input until replaced. + +## Owner + +- Each daemon owns its health statement and signs it with its pinned service or + host identity. +- Idunn owns admission, freshness, monotonic lineage, and the outward projection + of current authenticated provider health. Lifecycle judgment remains private + Idunn policy and is not smuggled into that provider-health statement. +- Root deployment configuration owns the trust binding between daemon id, + health contract, source runtime, and signer identity. + +No renderer, transport peer, systemd unit, PID probe, caller boolean, or typed +but unsigned packet owns health. + +Authenticated daemon health is selected before release-source inspection. +Release state may request a rollout of an otherwise healthy body, but a missing +checkout or upstream cannot replace the daemon's liveness statement or block +restart of an unhealthy admitted body. Continuity restart and deployment are +separate authorities: only the latter is gated by the deployment brake. + +## Inputs + +The daemon-authored statement binds: + +- exact daemon id and health contract; +- source runtime id and signer identity; +- observation time, publisher incarnation, and monotonic sequence; +- bounded health state and detail; +- release id, release witness, source revision, and deployment request when the + managed target is release-bound; +- `private_state_exposed=false`; +- an Ed25519 signature over the complete unsigned statement under a + domain-separated signing purpose. + +The Idunn trust binding names the only public key allowed to speak for that +daemon/contract/runtime tuple and whether release identity is required. + +Yggdrasil Odin uses the generic signed contract with daemon +`yggdrasil-odin`, source runtime `odin-yggdrasil`, and a root-provisioned +Ed25519 key readable only by the Odin runtime group. Its health signature owns +liveness. Exact Odin and CultLib revisions remain deployment authority in the +immutable release and `/srv/odin/deploy/deployment.env`; neither Docker health +nor the signed liveness statement may rewrite that witness. + +## Outputs + +Idunn persists two separate current facts per daemon: + +1. one admitted daemon-authored statement, keyed by daemon id, plus its exact + digest in the admission; +2. an Idunn-authored `idunn.authenticated_provider_health_projection.v1` + record binding the trust record, admission, exact provider lineage, and + freshness interval. + +Signed heartbeats are current liveness state, not an append-only event ledger. +Advancing a daemon's publisher sequence atomically replaces that daemon's prior +statement, admission, and compatibility health row. Startup atomically compacts +legacy digest-keyed statement history to the one exact statement named by each +current admission. A missing or digest-mismatched witness fails closed; it is +never reconstructed from process probes or a later heartbeat. + +The surrounding keepalive store follows a separate bounded-retention policy. +It keeps exact current authority plus small per-daemon diagnostic windows; +historical pulse decisions, repeated alarms, and superseded actuation receipts +cannot make authentication fsync cost grow with host age. The current +deployment head and every record required to rejoin active signed health remain +pinned through compaction. + +The outward record exists only while a generic provider-authored admission is +current and still matches its root trust and optional release binding. Missing +publication, release drift, dependency failure, target absence, and recovery +policy are absence of this projection. They are not synthetic provider states. +The projected state is exactly the authenticated provider state. Its only +explanation is a closed Idunn-generated reason code paired to that state: +`authenticated_provider_active`, `_warming`, `_degraded`, or `_failed`. +There is no free-text detail field. + +Consumers such as Epiphany Discord Status receive only the outward projection +through CultNet/CultMesh and verify Idunn's pinned identity. Reading Idunn's +private store is not a consumer protocol. + +The query transport is an explicit, read-only CultNet/RUDP listener enabled by +`--public-health-query-bind `. It has no default and is valid only with +`--swarm-profile`, `--service-identity-store`, and `--public-health-store`. +The listener opens only the dedicated public store and derives its allowlist +from the configured target catalog's daemon/health-contract pairs. It returns +the exact stored projection payload bytes with fixed source runtime +`idunn-daemon` and role `authenticated-provider-health-projector`. It has no +handle to Idunn's private stores and accepts no mutation message. + +## Derived state + +- Process and systemd observations are diagnostic evidence only. +- RUDP source endpoints and connection ids are transport routing only. +- `publication_source`, `transport`, `ready`, and `healthy` strings are display + data unless contained inside a valid signed statement. +- An unsigned legacy health packet may be retained temporarily for diagnostics + but cannot satisfy managed health, prevent recovery, or produce a healthy + outward projection. + +## Forbidden writers + +- The inbound packet cannot choose its own trust anchor. +- A daemon cannot change its configured daemon id, contract, or source runtime + by signing different values. +- Idunn cannot manufacture daemon-authored health from PID, executable, + container, HTTP, systemd, or command exit observations. +- Epiphany, Bifrost, VoidBot, Eve, and Discord cannot infer Idunn acceptance + from a sent packet. +- A generic typed `idunn.daemon_health.v1` packet cannot fall back into + lifecycle authority when signature verification fails or trust is absent. + +## Shared paths + +Every managed daemon uses the same verification primitive: + +`signed statement -> root trust binding -> signature and tuple verification -> +monotonic admission -> target/deployment join -> Idunn outward projection` + +Epiphany's deployed `epiphany.idunn_signed_runtime_health.v0` path is migration +evidence and the first working implementation of the signature half. It is not +the permanent excuse for a service-specific verifier. The generic contract +must absorb its invariants before the special path is deleted. + +## Cut line + +1. Add generic signed-health and trust-binding contracts. +2. Make generic Idunn admission verify the pinned tuple, signature, sequence, + incarnation, freshness, and release requirements. +3. Migrate Epiphany and Bifrost publishers and install their root-owned trust + bindings. +4. Publish and verify Idunn's outward signed authenticated-provider-health + projection using the fixed CultNet signature profile + `idunn.authenticated-provider-health-projection.v1`. +5. Point Discord Status at the outward projections. +6. Delete generic unsigned health from managed-health authority and delete the + Epiphany-only verifier after its last publisher migrates. + +The rebuild is incomplete while an unsigned packet can keep a managed daemon +healthy or while a status consumer must inspect Idunn's private store. + +## Implemented cut (2026-07-19) + +The deployed-lineage source now has the first executable generic admission +path. `idunn.signed_daemon_health.v1` is verified against an exact root-store +daemon/contract/runtime binding, the signer identity is derived from the bound +public key, the Ed25519 signature covers the positional empty-signature +statement under the domain-separated document purpose, and Idunn atomically +persists the signed statement, its generic admission, and the compatibility +health row. Managed-health selection rejoins all three with the current root +binding on every read; trust rotation invalidates old health. Sequence, +incarnation, observation, freshness, and optional deployment lineage advance +under CAS. + +Unsigned `idunn.daemon_health.v1` packets now persist only as +`idunn.unsigned_daemon_health_diagnostic.v1` under `diagnostic:`. +They cannot overwrite the daemon health key, create an admission, reset the +missing-publication clock, suppress recovery, or produce authenticated health. +The evaluation clock is passed through the whole target cycle instead of being +resampled inside the health selector. + +Idunn now also has the local outward projection writer. One publisher opens an +already-enrolled Idunn service identity before workers start, captures an +incarnation-stable root trust snapshot, and writes only +`idunn.authenticated_provider_health_projection.v1` envelopes to a distinct +public store. Each write rejoins the exact health, admission, signed statement, +root binding, and optional current deployment lineage. The signed positional +record has a fixed hash-derived key, a store-monotonic sequence, a process UUID, +and an expiry capped by the provider observation's managed-health silence +window. Missing, legacy, unsigned, drifted, or mutated input cannot write, +refresh, or delete a projection; an old row simply expires for consumers. + +The public projection has a dedicated multi-peer CultNet/RUDP snapshot +listener. Startup synchronously binds the explicit address and validates the +public store before target workers start. Bind failure or public-store +contamination is fatal startup evidence. Each request rereads only that store +and passes exact records through CultNet's read-only snapshot server with the +target-derived `(schema,key)` allowlist. Unknown schema/key requests return no +record. Malformed datagrams or messages are refused per peer without killing +the daemon. No request can write, delete, import, or inspect private Idunn +state. + +This source is deliberately not deployable yet: Bifrost and the remaining +generic publishers still emit the unsigned diagnostic contract, production +root trust and Idunn identity stores have not been installed, and Epiphany +still uses its signed v0 migration path. The query transport and Epiphany +consumer verifier now exist in source, but neither makes an unmigrated +publisher authoritative. Deploying this intermediate source would correctly +classify unmigrated daemons as missing health and could therefore actuate +recovery. Publisher migration and trust enrollment must land before promotion. + +## Verification layer + +Hostile tests must prove: + +- valid shape with no signature is not admitted; +- wrong key, daemon, contract, runtime, release, deployment, or trust record is + not admitted; +- replay, sequence rollback, time rollback, incarnation substitution, and + future/stale observations are not admitted; +- a live PID, existing executable, green HTTP response, or generic `ready` + string cannot repair failed authentication; +- one daemon's trust record cannot authenticate another daemon; +- unsigned legacy health cannot suppress recovery or yield outward healthy; +- outward provider health is signed by Idunn under the fixed, typed purpose and + binds the exact admitted statement; +- missing health, release drift, and dependency failure cannot write or refresh + the projection; any prior row expires instead of being rewritten as an + Idunn-authored provider state; +- caller-selected signing purposes, signer ids, reason strings, and synthetic + states fail closed; +- private detail and filesystem/process internals never enter the operator + projection. +- repeated signed heartbeats keep exactly one signed statement per admitted + daemon rather than growing the monolithic keepalive store; +- startup compaction keeps only the exact statement named by each admission, + is idempotent, and loses cleanly to a concurrent whole-store mutation rather + than overwriting it.