Make Idunn independently usable: unit files, docs, and a public entry point
Brings across everything an operator needs to run Idunn without reference to Odin. deploy/ carries the worked systemd installation -- unit, operator wrapper, and the narrow sudoers grant. docs/ carries the four documents that were scattered across Odin's tree. The README is new and is the public entry point the repo lacked: what Idunn decides, how to build and run it, the authority model in the form doctrine asks for, how to install it as a service, and where the deeper documents are. Two corrections made while moving: Requires=docker.service becomes Wants=. Deployment runners use Docker; continuity actuation does not. A hard requirement lets a Docker outage stop Idunn itself and take daemon survival down with it, which is precisely the invariant Idunn exists to hold. This is live on yggdrasil today and should be changed there too. The guide still told operators to run from E:\Projects\Odin -- a drive that no longer exists -- via npm wrappers that belonged to Odin. Paths generalised, commands moved to direct cargo invocations, and a note added that the document predates the extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cc5ff98245
commit
e79ad7bc99
8 changed files with 1674 additions and 38 deletions
156
README.md
156
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 <runtime> --release-id <sha> --deployment-id <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.
|
||||
|
|
|
|||
383
deploy/idunn-yggdrasil
Normal file
383
deploy/idunn-yggdrasil
Normal file
|
|
@ -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
|
||||
30
deploy/idunn-yggdrasil.service
Normal file
30
deploy/idunn-yggdrasil.service
Normal file
|
|
@ -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
|
||||
24
deploy/idunn-yggdrasil.sudoers
Normal file
24
deploy/idunn-yggdrasil.sudoers
Normal file
|
|
@ -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
|
||||
212
docs/authority-map.md
Normal file
212
docs/authority-map.md
Normal file
|
|
@ -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 `<filename>.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.
|
||||
355
docs/deployment-authority.md
Normal file
355
docs/deployment-authority.md
Normal file
|
|
@ -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.
|
||||
325
docs/guide.md
Normal file
325
docs/guide.md
Normal file
|
|
@ -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 <idunn-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 <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.
|
||||
227
docs/signed-daemon-health-authority.md
Normal file
227
docs/signed-daemon-health-authority.md
Normal file
|
|
@ -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 <addr>`. 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:<daemon>`.
|
||||
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.
|
||||
Loading…
Reference in a new issue