Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Meta Vens
8e1de662c7 Write map: C5 Hands landed on a branch; Soul pending the operator's go
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:04:24 +02:00
Meta Vens
f901b38fb7 Write map: CultLib side landed (Cut D, R1, put-serve); C5 next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:41:04 +02:00
Meta Vens
665dc0856c Recovery map: Bifrost container lane deleted and its legacy route retired on Yggdrasil
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:50:45 +02:00
Meta Vens
2fa1bffc38 Recovery map: R1 landed at CultLib daedfdd6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:13:56 +02:00
Meta Vens
1262fcdcb5 Recovery map: live zero-byte stores moved aside; ops merged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:49:11 +02:00
Meta Vens
3854bb5eaf Recovery map: R1 batch 3 Soul (merge); symlink, fsync and encode-refusal follow-ups; live-file order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:50:57 +02:00
Meta Vens
07501c7c6e Write map: Q5-A and Q5-B ruled A (withhold ACK until answered; hold puts on a stall)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:05:55 +02:00
Meta Vens
95522dcd0e Write map: Q5 cut map (C5 deferred put reply, W0 pin bump, W1 writer thread); Q5-A, Q5-B open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:03:15 +02:00
Meta Vens
47850dfcd6 Write map: Q5 ruled A (writer thread with group commit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:43:46 +02:00
Meta Vens
59ef4da9dd Write map: Soul on batch 3 (not fit to deploy); Q5 where the durable write runs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:34:36 +02:00
Meta Vens
57cf8df181 Recovery map: R1 batch 2 Soul verdict (hold); zero-byte live stores block consumer pins
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:20:40 +02:00
Meta Vens
80af7ef041 Odin map: Q3 B supersedes the Cut 2 effect row; Cut 2 deploys with Cut 3 Odin half
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:43:41 +02:00
Meta Vens
e7fbbbcade Store recovery map: R1 status; Muninn reset script must move with its next CultLib bump
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:23:56 +02:00
Meta Vens
cdc57bda2d Store recovery map: Q-R1..R6 ruled; Idunn watchdog reporting to Discord is a new campaign
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:40:44 +02:00
Meta Vens
1d9bc0246d Map Odin store repair and recovery
Operator: "None of these options involve repair and recovery, which is what
ultimately needs to happen." Q-R1..R6 open; R1 (CultLib typed read failures,
temp cleanup) does not depend on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:30:37 +02:00
Meta Vens
77373cb563 Odin map: Q3 B (durable put ACK); foreign-write handling must repair and recover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:06:06 +02:00
soul-probe
8c84857670 Odin map: Q2 decode result; the oversize records are dead Ghostlight residue
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:22:58 +02:00
soul-probe
ae8fa5571f Odin map: Q1 yes (refuse unservable puts), Q2 yes (decode a store copy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:09:31 +02:00
soul-probe
4b525b463c Map Odin's write pattern: memory working set, one write per interval
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:39:32 +02:00
soul-probe
44951a1f07 Merge Odin fixes 2-4: one bad record never stops Odin, peers write only what they own
A record that does not decode is skipped per record (catalog, startup).
Every failure of Odin's own authority ends it (temporary, until Idunn
cut A3); an early clock and a real file-read error are retried. The
catalog no longer serves correlations or the watermark. Peer puts are an
evidence-based allow-list, closing a peer-reachable presence freeze.
Three Soul passes; deploy gate GO against the live type inventory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 06:17:18 +02:00
soul-probe
dc46be205a Odin: the projection-unreadable mark is found through anyhow context layers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:23:54 +02:00
soul-probe
5dd5b4a863 Odin: peer namespaces are the ones a source publishes, and only a failed file read is retried
The allow-list drops nine guessed namespaces and gains gamecult.model,
gamecult.aetheria and the two gamecult.media_stream_* types (they are not
under a dotted gamecult.media segment, so they are whole types), each with its
publisher named. The own-projection failure rule now retries only what the
file read itself marked, so a truncated Expected record ends Odin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:19:46 +02:00
soul-probe
c0735f17aa Odin: providers write only their own namespaces, own-projection I/O errors are retried, the lingering test no longer races the idle timeout
Peer writes (Soul P-B, confirmed): persist_generic_document blocked six
authority types, so a peer could put an odin.topology_publisher_watermark keyed
odin holding the largest sequence; every heartbeat then failed to reserve its
sequence, and the record survived restarts. Generic puts are now an allow-list
of provider namespaces (PEER_DOCUMENT_NAMESPACES). Odin's own records and
Idunn's projected authority live in namespaces no provider can name, so they
are refused by construction. The same predicate decides what the catalog
serves back, replacing the correlation special case: the watermark, and any
future Odin-internal type, is no longer public.

Own projection: a plain I/O error reading Odin's own projection (permission
denied, not found, interrupted) or a missing file is survived and retried; an
error the reader decoded or validated and did not accept stays fatal
(PresenceAuthorityRefused).

Log flood: survive() now logs through the RuntimeState LogGate, once per
subject per minute, instead of printing every pass.

Flake: self_presence_lands_while_lingering_publishers_hold_every_session failed
on Yggdrasil because 64 peers connect over about 9 s and the fsync-bound passes
that follow took 4 to 20 s under disk pressure, so the earliest sessions passed
the server's default 30 s idle timeout and expired before the final
session_count assertion (44 of 64, measured). No peer ever failed to connect.
The test's world now serves with a one hour idle timeout, since the property is
a full table at heartbeat time, and it logs any peer that is not admitted.

Tests: provider puts of every Odin-owned kind are refused and presence keeps
advancing; the catalog serves only presence and provider documents; namespaces
match whole segments; I/O versus decode of the own projection, and a missing
projection file through the serving loop; repeated failures are counted, not
printed again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 03:57:40 +02:00
soul-probe
64e64df3bd Odin test: a refresh before the anchor's binding still records the time disagreement
Removing the incoming-claim condition from the clock exemption in reconcile
survived: only the admission side was pinned. A refresh has no claim to refuse,
so with the clock before the anchor's binding it must still write the
correlation carrying the provider-trust-anchor-time disagreement.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 03:57:39 +02:00
soul-probe
2f0d1d4bcb Odin test: the lingering-publisher test admits until 64 are held, then refuses one
A peer that times out on a loaded host no longer changes the count, and the six refused peers no longer wait out their deadline while the first sessions idle out.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:14:27 +02:00
soul-probe
c81fc38ecc Odin tests: the catalog test expects the watermark record, the lingering test counts from a first pass
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:07:22 +02:00
soul-probe
8c33a18729 Odin: repair a mis-spliced edit in the sink handle (the previous commit did not compile)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:04:00 +02:00
soul-probe
2e4ccc40ad Odin: a bad record is skipped, own-authority failures are fatal, correlations leave the catalog
A1 the catalog and startup skip a record that does not decode (logged); startup
reads only presences keyed under Odin's own target.
A2 every failure to establish Odin's own projected authority is
PresenceAuthorityRefused (temporary rule in survive).
A3 a clock that precedes the provider anchor's binding is retried, not refused.
C  the catalog no longer serves correlations; the store still writes them.
B  test peers connect while the server is polled without timers; 2 s deadline.
6  repeating refresh and catalog-skip lines are logged once per minute per subject.
Soul's probes P1-P3 become assertion tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:01:48 +02:00
soul-probe
59667f4648 Odin tests: Soul's probes P1-P3 for catalog collision, one bad record, and undecodable own anchor
P1 two incarnations of one target, P2 one undecodable presence, P3 an
undecodable own provider anchor. All three fail at c34d167; the fixes follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:54:48 +02:00
soul-probe
c34d167f74 Merge Odin session-table fix: Odin survives a full session table
Odin admits its own presence straight into its store instead of over
RUDP, survives transient failures, and ends only on write-lease loss or
a refused own authority (temporary, until Idunn cut A3). Merged to stop
a live crash loop on Yggdrasil (2026-09-30 00:55 CEST): the deployed
Odin timed out publishing to itself through a full session table and
Idunn stopped restarting it after three failures. Open Soul findings
(A1 startup decode, A2 rule gaps, C catalog collision, flaky harness
test) are pre-existing in the deployed build and are fixed next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:21:12 +02:00
soul-probe
68b666c2e5 Merge X1: delete Odin's dead lifecycle residue (-7,037 lines)
odin-core idunn.rs and idunn.*/bifrost schemas, the retired JS
coordinator, dead deploy/restart/health scripts, lifecycle claims in
docs. Soul: nothing deleted is reached by the deployed odin-daemon, the
Hermodr container or any live store. Idunn map cut X1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:48:07 +02:00
soul-probe
106dfb8327 Docs: Idunn owns deployment and daemon survival; Odin does discovery only
README and architecture.md called Idunn "Odin's keepalive organ", listed
crates/idunn-daemon (not in this repo) and the deleted odin-core idunn.rs
as its body, described the Node coordinator's process lifecycle, refresh
loop and Docker/ADB probes as Odin's runtime body, and pointed at the
deleted start-odin.ps1. Rewritten to the live truth: Idunn builds, activates,
restarts and brakes every target, Odin included; odin-daemon reads Idunn's
projection, admits presences, publishes correlation records and answers RUDP
requests; process lifecycle is not Odin's.

The dated ledger (transport-shortcut-inventory.md), notes/ and the Hermodr
extraction plan are history and are left as written.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:25:20 +02:00
soul-probe
779c24ba80 package.json: drop scripts that named the deleted coordinator and a missing crate
start (node src/odin-coordinator.cjs), check and test (JS gates over the
deleted src/ and test/), muninn:build and muninn:start (muninn-daemon is
not in this workspace). gjallar:persona and rust:test stay. This belonged
in the Node coordinator deletion commit and was left unstaged there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:23:07 +02:00
soul-probe
99f9a22fa4 odin-core: keep the cultnet-rs dependency (cultmesh_documents! expands to it)
The previous commit dropped cultnet-rs from odin-core's manifest as
unused; cultmesh_documents! emits cultnet_rs paths, so the crate needs it
in scope. Restore the dependency and its lockfile entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:22:45 +02:00
soul-probe
092ff997b4 Delete deploy, restart, health and start/stop scripts for other services
Odin does not deploy, restart or health-probe anything: Idunn owns
deployment and daemon survival, and every service in this set is either
Idunn-deployed from its own repo recipe or archived. Removed from
scripts/: deploy-yggdrasil-{heimdall,repixelizer,source-app,streampixels}
(.ps1/.cmd, armed behind IDUNN_ACTUATOR=1) and the linux heimdall deploy;
restart-* (odin, stonks, vili, weksa, nightwing eve dashboard and browser
reference; several already exit 2 or throw as archived); health-* (odin,
voidbot, stonks, vili, weksa, mimir, nightwing, raven capture mux,
yggdrasil heimdall/repixelizer/streampixels/source-app; archived stubs and
orphans); start-odin.ps1 and stop-odin.ps1 (launched the deleted Node
coordinator).

Kept: write-gjallar-persona-cc.mjs (persona tooling, package.json
gjallar scripts) and the nightwing usb-move helpers, wasapi loopback,
firewall and wireguard scripts, none of which is a lifecycle actuator.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:22:19 +02:00
soul-probe
06282c47d8 Delete the retired Node coordinator, its tests and npm entry points
src/odin-coordinator.cjs and src/odin/* (probes.cjs docker/adb host
probes, state.cjs, provider-ingress.cjs with its 120 s TTL) were retired
when the Rust odin-daemon became the deployed body (state/map.yaml: the
Node coordinator that held 17871 is retired). The recipe builds only
odin-daemon; nothing in the Rust crate graph loads a .cjs file. The live
Yggdrasil hermodr container runs from the immutable release
/srv/odin/current, not from this tree.

Removes src/, test/ (4 test files for that JS), scripts/check-syntax.cjs
(the JS syntax gate), and the package.json scripts start, check, test,
muninn:build, muninn:start (the last two named a crate that is not in the
workspace). gjallar:persona and rust:test stay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:21:30 +02:00
soul-probe
6aba9a8801 Delete odin-core's Idunn lifecycle planner and idunn.* schemas
crates/odin-core/src/idunn.rs (plan_keepalive: observe, alarm, deploy or
restart from IdunnDaemonHealthRecord) had no caller: odin-daemon does not
depend on odin-core, and the deployed recipe builds only odin-daemon.
Idunn owns deployment and daemon survival; nothing in Idunn reads these
records (its own live in cultnet-rs).

Also removes from documents.rs the idunn.* and bifrost release-authority
schema constants and records, their OdinDocuments registrations, the
cultnet re-exports of Idunn health types, and the tests that only
exercised them. The muninn.* copies stay. cultnet-rs and rmp-serde drop
out of odin-core's manifest and lockfile entry (no remaining user).

Muninn pins odin-core at 3e96c6c and is unaffected until its next bump,
which must drop IdunnDaemonHealthRecord.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:20:38 +02:00
68 changed files with 2774 additions and 7303 deletions

1
Cargo.lock generated
View file

@ -591,7 +591,6 @@ dependencies = [
"cultmesh-rs",
"cultnet-rs",
"pretty_assertions",
"rmp-serde",
"serde",
"serde_json",
"tempfile",

101
README.md
View file

@ -70,75 +70,47 @@ Local package surfaces:
## Idunn
Idunn is Odin's keepalive organ: the daemon package that should keep the known
swarm alive after Odin has accepted where each daemon lives and what authority
path may touch it. Individual daemons publish health and command boundaries;
Idunn brings them up after reboots or crashes, watches health, and escalates
operator-needed failures through Bifrost's CultMesh bridge. VoidBot owner-DM
delivery is a demoted compatibility actuator, not the owner; the command belongs
in Bifrost's Verse. Odin sees the daemons; Idunn keeps the apples from rotting.
Agents do not deploy daemons directly. They configure Idunn's target catalog,
release targets, migration commands, and command boundaries so Idunn can run
the shared rollout primitive and leave typed witnesses behind.
Deployment consequence has one final owner: immediately before every migration,
deploy, or restart process spawn, Idunn re-opens the root-owned deployment-brake
CultCache and the dedicated operator public anchor. A released record must bind
the canonical runtime (`yggdrasil` for `yggdrasil-local`), exact source revision
(or `restart:<daemon>`), exact request id, operator, signature, and short expiry.
Missing, corrupt, engaged, expired, foreign, wrong-scope, substituted, or
badly-signed state fails closed. Planning and lifecycle-command publication do
not bypass or cache this decision. The store and its sibling `.lock` live in
`/var/lib/gamecult/idunn-authority`, a root-owned `root:idunn` `0750` directory;
both files are `root:idunn` `0640`, and the daemon's systemd sandbox exposes the
directory read-only. Idunn holds the lock shared from its final snapshot read
through process spawn, so an engage cannot be defeated by previously captured
released bytes. A release is not one-shot: its exact request/revision grant
covers each migration and deploy spawn in that rollout until its short expiry;
every spawn independently reopens and validates the same grant.
Idunn (`GameCult/Idunn`) owns deployment and daemon survival for every GameCult
target, Odin included: it builds from the target's recipe, activates and
restarts the admitted incarnation, and holds the deployment and lifecycle
brakes. Odin owns none of that. Odin does discovery, schema awareness,
rendezvous, and interface aggregation, and it reads Idunn's topology projection
to learn which incarnations exist. Agents do not deploy daemons directly: they
configure the target's recipe and binding and let Idunn run the transaction.
Local package surfaces:
- Organ contract: `docs/idunn.md`
- User README: `src/Idunn/README.md`
- Rust daemon crate: `crates/idunn-daemon`
- Rust lifecycle logic: `crates/odin-core/src/idunn.rs`
- Runtime store: `scratch/idunn/idunn.keepalive.cc`
- Hosted VoidBot release: upstream `main` -> Yggdrasil-local Idunn -> the
root-owned `/srv/odin/deploy-manifests/voidbot` actuator. Local checkouts are
development and diagnostics only; do not create a local deployment.
- Hosted Odin release: branch `codex/ygg-idunn-independent-bootstrap`, built
`cargo --locked` so `Cargo.lock` is the CultLib pin -> one immutable
`/srv/odin/releases/<commit>` body -> `/srv/odin/current`. Odin signs its own
liveness to Idunn, while Idunn starts independently and owns later restart or
deployment actuation.
- Operator escalation: Bifrost-owned CultMesh crossing; current compatibility
delivery calls `scripts\notify-idunn-operator-alarm.ps1`, which asks Bifrost
to publish a typed `gamecult.operator_dm_request.v1` CultMesh command document
only after Idunn raises an alarm
- Deployment recipe: `deployment/idunn/recipe.toml`
- Operator binding: `/etc/gamecult/idunn/bindings/odin.toml` on the host, not in
this repository
- Live deployment and admission map: `state/map.yaml`
- Idunn's contract, brakes, and runbooks: the `GameCult/Idunn` repository
## Authority Map
- Owner: Odin owns the network-wide Verse registry, schema catalog index, translation map, and accepted provider catalog/proxy surfaces.
- Inputs: CultMesh/CultNet peer announcements, schema catalog responses, daemon
health/provider publications over `cultnet.transport.rudp.v0`, local
Docker/ADB debug facts, and provider-owned Eve/CultUI surfaces.
health/provider publications over `cultnet.transport.rudp.v0`, Idunn's
read-only topology projection, and provider-owned Eve/CultUI surfaces.
- Outputs: CultCache-backed Odin state, CultMesh documents, and CultNet
schema/catalog messages. Browser, GUI, TUI, and framebuffer renderers lower
those documents outside Odin instead of asking Odin to host web surfaces.
- Derived state: Gjallar's aggregate surface is derived from Odin/provider state; Eve clients derive pixels from that aggregate.
- Forbidden writers: renderers do not probe the network or decide Verse truth; individual projects do not maintain private incompatible discovery ledgers once Odin can see them.
- Shared paths: human dashboards, worker schedulers, Verse bootstrap code, and compact TUI views consume the same registry and schema catalog.
- Deletion line: old per-host coordinator scripts should be deleted or reduced to deployment wrappers that start Odin.
- Not Odin's: process lifecycle, restart, deployment, and daemon survival belong to Idunn.
## Run Locally On Starfire
## Runtime Body
```powershell
.\scripts\start-odin.ps1 -IdunnRudpHealth $env:IDUNN_RUDP_HEALTH
```
Odin runs as `odin-daemon` under Idunn on Yggdrasil, built from
`deployment/idunn/recipe.toml`. There is no local start script. The daemon:
`-IdunnRudpHealth`, `ODIN_IDUNN_RUDP_HEALTH`, or `IDUNN_RUDP_HEALTH` must name
the Idunn RUDP health endpoint. Odin does not assume a localhost health target.
- reads Idunn's read-only projection to learn which incarnations of which
targets exist;
- admits provider presences over RUDP for those incarnations and persists them
through CultMesh/CultCache;
- publishes signed runtime-topology correlation records;
- answers CultNet/RUDP snapshot and document requests on its route.
Odin's native document catalog is addressed by CultMesh URI. Concrete RUDP
bootstrap is configured behind CultMesh URI resolution by the operator or by
@ -152,28 +124,9 @@ That URI accepts typed document publication and schema/catalog requests through
the shared CultMesh runtime. Consumers that need Odin's accepted surface can
still request the current CultNet snapshot after CultMesh resolves the transport.
Legacy browser/deck lowerers must consume Odin's CultMesh state through their
own lowering process. Odin no longer hosts browser-deck surfaces or publishes
deck URLs as discovery seed material.
State and logs live under ignored `scratch/odin/`.
## Current First Body
The first executable is deliberately narrow:
- publishes provider catalog `odin.providers`;
- persists the latest surface through local CultMesh/CultCache when `CultLib` packages are available at `E:\Projects\CultLib\packages`;
- writes `scratch/odin/latest-surface.json` only when `--write-debug-surface-json` or `ODIN_WRITE_DEBUG_SURFACE_JSON=1` is explicitly supplied;
- observes Starfire Docker and Periwinkle ADB as local debug/edge facts;
- derives remote Verse presence from provider-owned CultMesh/CultNet
advertisements and interface records, not TCP/SSH/systemd probes.
- publishes explicit `verse` and `service` nodes for compact Eve/CultUI lowerers.
- ingests provider-owned Eve/CultUI dashboards, including `mimir.live.stats` and `voidbot.swarm`, and embeds them as Odin `interface` nodes;
- accepts live `gamecult.eve.provider_advertisement.v1` announcements through Odin's CultMesh/RUDP rendezvous path so daemons can announce schemas, surfaces, commands, nested Verses, and style capabilities without Odin scraping private dashboards;
- accepts explicit local debug imports only when `--interfaceBindingStore` / `ODIN_INTERFACE_BINDING_STORES` entries are written as `cultmesh-store:file://...` URIs; raw filesystem paths are not discovery configuration;
- preserves provider semantic addresses such as `asgard.starfire.bifrost/eve/tui` and `asgard.starfire.bifrost/eve/gui`, with CultNet routes carried as transport metadata rather than identity;
- persists operator tiling intent as `odin.interface_layout.v1` in the Odin CultMesh store; ignored `scratch/odin/interface-layout.json` is migration input only.
Browser and deck lowerers consume Odin's CultMesh state through their own
lowering process. Odin does not host browser-deck surfaces or publish deck URLs
as discovery seed material.
Provider advertisements and CultNet/RUDP transport profiles are the discovery
path. External host probes, product health checks, port probes, and renderer

View file

@ -13,11 +13,10 @@ path = "src/lib.rs"
anyhow = "1"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultcache-rs" }
cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultmesh-rs" }
serde_json = "1"
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultnet-rs" }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
rmp-serde = "1.3"
pretty_assertions = "1.4"
tempfile = "3"

File diff suppressed because it is too large Load diff

View file

@ -1,672 +0,0 @@
use crate::documents::{
IdunnDaemonHealthRecord, IdunnDeploymentRequestRecord, IdunnDesiredDaemonRecord,
IdunnKeepaliveDecisionRecord, IdunnOperatorAlarmRecord, IdunnRestartRequestRecord,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IdunnPlan {
pub decision: IdunnKeepaliveDecisionRecord,
pub deployment_request: Option<IdunnDeploymentRequestRecord>,
pub restart_request: Option<IdunnRestartRequestRecord>,
pub operator_alarm: Option<IdunnOperatorAlarmRecord>,
}
pub fn plan_keepalive(
desired: &IdunnDesiredDaemonRecord,
health: &IdunnDaemonHealthRecord,
now: impl Into<String>,
) -> IdunnPlan {
let now = now.into();
let decision_id = format!("{}:{}", desired.daemon_id, now);
if !desired.enabled {
return IdunnPlan {
decision: decision(&decision_id, desired, "noop", "daemon is disabled", &now),
deployment_request: None,
restart_request: None,
operator_alarm: None,
};
}
if is_healthy(&health.state) {
return IdunnPlan {
decision: decision(&decision_id, desired, "observe", "daemon is healthy", &now),
deployment_request: None,
restart_request: None,
operator_alarm: None,
};
}
if health.state == "warming" {
return IdunnPlan {
decision: decision(
&decision_id,
desired,
"observe",
"daemon is warming under provider-owned progress authority",
&now,
),
deployment_request: None,
restart_request: None,
operator_alarm: None,
};
}
if health.state == "degraded" || health.state == "dependency-unavailable" {
let alarm_id = format!("alarm:{}:{}", desired.daemon_id, now);
let reason = format!(
"health is {}; local restart/deploy is not the owner of this failure: {}",
health.state, health.detail
);
return IdunnPlan {
decision: decision(&decision_id, desired, "alarm", &reason, &now),
deployment_request: None,
restart_request: None,
operator_alarm: Some(IdunnOperatorAlarmRecord {
alarm_id,
daemon_id: desired.daemon_id.clone(),
severity: "operator-action-required".to_string(),
reason,
escalation_target: "bifrost.operator-notification".to_string(),
raised_at: now,
}),
};
}
if let Some(command) = desired.deploy_command.as_deref() {
if !command.trim().is_empty() && health.state == "stale-deployment" {
let request_id = format!("deploy:{}:{}", desired.daemon_id, now);
return IdunnPlan {
decision: decision(
&decision_id,
desired,
"deploy",
&format!(
"health is {}; deployment authority is available",
health.state
),
&now,
),
deployment_request: Some(IdunnDeploymentRequestRecord {
request_id,
daemon_id: desired.daemon_id.clone(),
command: command.to_string(),
authority: desired.authority.clone(),
requested_at: now,
repository_full_name: String::new(),
upstream_ref: String::new(),
source_revision: String::new(),
release_authority_id: String::new(),
release_authority_envelope_sha256: String::new(),
requires_bifrost_authority: false,
}),
restart_request: None,
operator_alarm: None,
};
}
}
if health.state == "stale-deployment" {
let alarm_id = format!("alarm:{}:{}", desired.daemon_id, now);
let reason = format!(
"health is stale-deployment under {}; no deploy command authority is available",
desired.health_contract
);
return IdunnPlan {
decision: decision(&decision_id, desired, "alarm", &reason, &now),
deployment_request: None,
restart_request: None,
operator_alarm: Some(IdunnOperatorAlarmRecord {
alarm_id,
daemon_id: desired.daemon_id.clone(),
severity: "operator-action-required".to_string(),
reason,
escalation_target: "bifrost.operator-notification".to_string(),
raised_at: now,
}),
};
}
match desired.restart_command.as_deref() {
Some(command) if !command.trim().is_empty() => {
let request_id = format!("restart:{}:{}", desired.daemon_id, now);
IdunnPlan {
decision: decision(
&decision_id,
desired,
"restart",
&format!("health is {}; restart authority is available", health.state),
&now,
),
restart_request: Some(IdunnRestartRequestRecord {
request_id,
daemon_id: desired.daemon_id.clone(),
command: command.to_string(),
authority: desired.authority.clone(),
requested_at: now,
}),
deployment_request: None,
operator_alarm: None,
}
}
_ => {
let alarm_id = format!("alarm:{}:{}", desired.daemon_id, now);
let reason = format!(
"health is {}; no restart command authority is available",
health.state
);
IdunnPlan {
decision: decision(&decision_id, desired, "alarm", &reason, &now),
deployment_request: None,
restart_request: None,
operator_alarm: Some(IdunnOperatorAlarmRecord {
alarm_id,
daemon_id: desired.daemon_id.clone(),
severity: "operator-action-required".to_string(),
reason,
escalation_target: "bifrost.operator-notification".to_string(),
raised_at: now,
}),
}
}
}
}
fn is_healthy(state: &str) -> bool {
matches!(state, "active" | "healthy" | "ok" | "running")
}
fn decision(
decision_id: &str,
desired: &IdunnDesiredDaemonRecord,
action: &str,
reason: &str,
decided_at: &str,
) -> IdunnKeepaliveDecisionRecord {
IdunnKeepaliveDecisionRecord {
decision_id: decision_id.to_string(),
daemon_id: desired.daemon_id.clone(),
action: action.to_string(),
reason: reason.to_string(),
authority: desired.authority.clone(),
decided_at: decided_at.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::documents::{
IDUNN_COMMAND_BOUNDARY_SCHEMA, IDUNN_DAEMON_SURGERY_PLAN_SCHEMA,
IDUNN_DAEMON_TRANSPORT_PROFILE_SCHEMA, IDUNN_DEPLOYMENT_ARTIFACT_SCHEMA,
IDUNN_DESIRED_DAEMON_SCHEMA, IDUNN_RELEASE_TARGET_SCHEMA, IDUNN_ROLLOUT_PLAN_SCHEMA,
IDUNN_STATE_MIGRATION_PLAN_SCHEMA, IDUNN_SWARM_SURGERY_PLAN_SCHEMA,
IdunnCommandBoundaryRecord, IdunnDaemonSurgeryPlanRecord,
IdunnDaemonTransportProfileRecord, IdunnDeploymentArtifactRecord, IdunnReleaseTargetRecord,
IdunnRolloutPlanRecord, IdunnRuntimeTransportCheckRecord, IdunnStateMigrationPlanRecord,
IdunnSwarmSurgeryPlanRecord, OdinDocuments,
};
use anyhow::Result;
use cultmesh_rs::{CultMesh, CultMeshNodeOptions};
fn desired(restart_command: Option<&str>) -> IdunnDesiredDaemonRecord {
IdunnDesiredDaemonRecord {
daemon_id: "voidbot".to_string(),
verse_id: "local".to_string(),
name: "VoidBot".to_string(),
enabled: true,
health_command: None,
restart_command: restart_command.map(ToString::to_string),
authority: "idunn-supervisor-command".to_string(),
max_silence_seconds: 60,
observed_at: "2026-06-04T00:00:00Z".to_string(),
deploy_command: None,
health_contract: "test.cultnet-rudp-health".to_string(),
transport_profile_id: "transport:voidbot".to_string(),
command_boundary_id: "command-boundary:voidbot".to_string(),
}
}
fn health(state: &str) -> IdunnDaemonHealthRecord {
IdunnDaemonHealthRecord {
daemon_id: "voidbot".to_string(),
state: state.to_string(),
detail: "unit daemon publication".to_string(),
observed_at: "2026-06-04T00:00:01Z".to_string(),
health_contract: "test.cultnet-rudp-health".to_string(),
publication_source: "debug-command".to_string(),
transport: "debug.local-command".to_string(),
}
}
#[test]
fn healthy_daemon_is_observed_without_restart() {
let plan = plan_keepalive(
&desired(Some("npm start")),
&health("active"),
"2026-06-04T00:00:02Z",
);
assert_eq!(plan.decision.action, "observe");
assert!(plan.deployment_request.is_none());
assert!(plan.restart_request.is_none());
assert!(plan.operator_alarm.is_none());
}
#[test]
fn warming_daemon_is_observed_without_actuation_or_promotion() {
let mut desired = desired(Some("systemctl restart epiphany"));
desired.deploy_command = Some("deploy epiphany".to_string());
let plan = plan_keepalive(&desired, &health("warming"), "2026-07-17T00:00:02Z");
assert_eq!(plan.decision.action, "observe");
assert!(plan.decision.reason.contains("provider-owned progress"));
assert!(plan.deployment_request.is_none());
assert!(plan.restart_request.is_none());
assert!(plan.operator_alarm.is_none());
}
#[test]
fn unhealthy_daemon_with_restart_authority_requests_restart() {
let plan = plan_keepalive(
&desired(Some("npm start")),
&health("failed"),
"2026-06-04T00:00:02Z",
);
assert_eq!(plan.decision.action, "restart");
assert!(plan.deployment_request.is_none());
assert_eq!(plan.restart_request.unwrap().command, "npm start");
assert!(plan.operator_alarm.is_none());
}
#[test]
fn unhealthy_daemon_with_deploy_authority_requests_deployment_before_restart() {
let mut desired = desired(Some("systemctl restart gjallar"));
desired.deploy_command = Some("deploy gjallar".to_string());
let plan = plan_keepalive(
&desired,
&health("stale-deployment"),
"2026-06-04T00:00:02Z",
);
assert_eq!(plan.decision.action, "deploy");
assert_eq!(plan.deployment_request.unwrap().command, "deploy gjallar");
assert!(plan.restart_request.is_none());
assert!(plan.operator_alarm.is_none());
}
#[test]
fn failed_daemon_with_deploy_and_restart_authority_restarts_instead_of_redeploying() {
let mut desired = desired(Some("systemctl restart gjallar"));
desired.deploy_command = Some("deploy gjallar".to_string());
let plan = plan_keepalive(&desired, &health("failed"), "2026-06-04T00:00:02Z");
assert_eq!(plan.decision.action, "restart");
assert!(plan.deployment_request.is_none());
assert_eq!(
plan.restart_request.unwrap().command,
"systemctl restart gjallar"
);
assert!(plan.operator_alarm.is_none());
}
#[test]
fn dependency_unavailable_raises_alarm_instead_of_redeploying_or_restarting() {
let mut desired = desired(Some("systemctl restart gjallar"));
desired.deploy_command = Some("deploy gjallar".to_string());
let plan = plan_keepalive(
&desired,
&health("dependency-unavailable"),
"2026-06-04T00:00:02Z",
);
assert_eq!(plan.decision.action, "alarm");
assert!(plan.deployment_request.is_none());
assert!(plan.restart_request.is_none());
assert!(plan.operator_alarm.is_some());
}
#[test]
fn stale_deployment_without_deploy_authority_raises_alarm_instead_of_restarting() {
let plan = plan_keepalive(
&desired(Some("systemctl restart old-app")),
&health("stale-deployment"),
"2026-06-04T00:00:02Z",
);
assert_eq!(plan.decision.action, "alarm");
assert!(plan.deployment_request.is_none());
assert!(plan.restart_request.is_none());
assert!(plan.operator_alarm.is_some());
}
#[test]
fn unhealthy_daemon_without_authority_raises_operator_alarm() {
let plan = plan_keepalive(&desired(None), &health("failed"), "2026-06-04T00:00:02Z");
assert_eq!(plan.decision.action, "alarm");
assert!(plan.deployment_request.is_none());
assert!(plan.restart_request.is_none());
assert_eq!(
plan.operator_alarm.unwrap().escalation_target,
"bifrost.operator-notification"
);
}
#[test]
fn idunn_records_round_trip_through_cultmesh() -> Result<()> {
let temp = tempfile::tempdir()?;
let store_path = temp.path().join("idunn.cc");
let desired = desired(Some("npm start"));
let health = health("failed");
let plan = plan_keepalive(&desired, &health, "2026-06-04T00:00:02Z");
let request = plan.restart_request.unwrap();
let mut node = CultMesh::create_node(
&store_path,
OdinDocuments,
CultMeshNodeOptions {
runtime_id: "idunn-test".to_string(),
pull_on_start: true,
},
)?;
node.put(&desired.daemon_id, &desired)?;
node.put(&health.daemon_id, &health)?;
node.put(&plan.decision.decision_id, &plan.decision)?;
node.put(&request.request_id, &request)?;
let surgery_plan = IdunnDaemonSurgeryPlanRecord {
plan_id: "surgery:voidbot".to_string(),
daemon_id: "voidbot".to_string(),
severity: "high".to_string(),
status: "transport-surgery-required".to_string(),
owner: "VoidBot internal provider stack".to_string(),
objective: "Publish VoidBot internal daemon health over CultNet/RUDP".to_string(),
current_mechanism: "daemon-published RUDP health".to_string(),
intended_authority: "daemon-owned CultNet/RUDP health record".to_string(),
cut_line: "daemon health must be published over CultNet/RUDP".to_string(),
steps: vec![
"update CultLib".to_string(),
"publish RUDP health".to_string(),
],
blockers: vec!["runtime CultLib update".to_string()],
updated_at: "2026-06-04T00:00:02Z".to_string(),
};
let swarm_surgery_plan = IdunnSwarmSurgeryPlanRecord {
plan_id: "swarm-surgery:starfire-local".to_string(),
profile: "starfire-local".to_string(),
status: "active-transport-migration".to_string(),
owner: "Idunn swarm supervisor".to_string(),
objective: "Move daemon awareness to CultNet/RUDP".to_string(),
current_mechanism: "daemon-published RUDP health required".to_string(),
invariants: vec!["daemon truth is typed RUDP state".to_string()],
phases: vec!["install Muninn RUDP health".to_string()],
current_phase: "Phase 2".to_string(),
next_target: "starfire-muninn".to_string(),
cut_line: "daemon health must be published over CultNet/RUDP".to_string(),
verification_layer: "CultMesh keepalive store".to_string(),
updated_at: "2026-06-04T00:00:02Z".to_string(),
};
node.put(&swarm_surgery_plan.plan_id, &swarm_surgery_plan)?;
node.put(&surgery_plan.plan_id, &surgery_plan)?;
let transport_profile = IdunnDaemonTransportProfileRecord {
profile_id: "transport:voidbot".to_string(),
daemon_id: "voidbot".to_string(),
target_transport: "cultnet.transport.rudp.v0".to_string(),
current_transport: "missing-daemon-published-rudp-health".to_string(),
state: "migration-required".to_string(),
health_contract: "test.cultnet-rudp-health".to_string(),
publication_schema: "idunn.daemon_health.v1".to_string(),
debug_mechanism: "none".to_string(),
cut_line: "daemon health must be published over CultNet/RUDP".to_string(),
observed_at: "2026-06-04T00:00:02Z".to_string(),
};
node.put(&transport_profile.profile_id, &transport_profile)?;
let command_boundary = IdunnCommandBoundaryRecord {
boundary_id: "command-boundary:voidbot".to_string(),
daemon_id: "voidbot".to_string(),
owner: "idunn-supervisor-command-boundary".to_string(),
restart_authority: "idunn-supervisor-command.restart".to_string(),
deploy_authority: "none".to_string(),
health_authority: "daemon-published-rudp-health".to_string(),
alarm_authority: "bifrost.operator-notification".to_string(),
command_lowerings: vec!["npm start".to_string()],
forbidden_authority: "debug probes do not own truth".to_string(),
observed_at: "2026-06-04T00:00:02Z".to_string(),
};
node.put(&command_boundary.boundary_id, &command_boundary)?;
let release_target = IdunnReleaseTargetRecord {
target_id: "release-target:voidbot".to_string(),
daemon_id: "voidbot".to_string(),
repo: "VoidBot".to_string(),
repo_path: "E:\\Projects\\VoidBot".to_string(),
upstream_remote: "origin".to_string(),
upstream_branch: "main".to_string(),
desired_revision: "abc123".to_string(),
deployed_revision: "old123".to_string(),
artifact_strategy: "source-archive-from-upstream-main".to_string(),
rollout_strategy: "restart-after-verified-build".to_string(),
state_migration_authority: "daemon-owned-command".to_string(),
zero_downtime_capability: "restart-required".to_string(),
status: "tracked".to_string(),
observed_at: "2026-06-04T00:00:02Z".to_string(),
repository_full_name: "GameCult/VoidBot".to_string(),
upstream_ref: "refs/heads/main".to_string(),
release_authority_id: "release:GameCult/VoidBot:refs/heads/main:abc123".to_string(),
release_authority_envelope_sha256: "authority-sha".to_string(),
release_authority_status: "authorized".to_string(),
requires_bifrost_authority: true,
observed_upstream_revision: "abc123".to_string(),
minimum_source_revision: None,
};
let artifact = IdunnDeploymentArtifactRecord {
artifact_id: "artifact:voidbot:main".to_string(),
daemon_id: "voidbot".to_string(),
source_revision: "abc123".to_string(),
source_branch: "main".to_string(),
source_remote: "origin".to_string(),
artifact_kind: "source-archive-from-upstream-main".to_string(),
artifact_uri: "built-by-deploy-command".to_string(),
sha256: "pending-deploy-command".to_string(),
built_at: "2026-06-04T00:00:02Z".to_string(),
release_authority_id: release_target.release_authority_id.clone(),
release_authority_envelope_sha256: release_target
.release_authority_envelope_sha256
.clone(),
};
let migration_plan = IdunnStateMigrationPlanRecord {
plan_id: "migration:voidbot:main".to_string(),
daemon_id: "voidbot".to_string(),
from_schema_version: "deployed-state".to_string(),
to_schema_version: "target-revision-state".to_string(),
authority: "daemon-owned-migrator".to_string(),
command: "voidbot migrate-state".to_string(),
strategy: "backup-then-daemon-migrator".to_string(),
backup_required: true,
zero_downtime_required: false,
status: "planned".to_string(),
planned_at: "2026-06-04T00:00:02Z".to_string(),
};
let rollout_plan = IdunnRolloutPlanRecord {
plan_id: "rollout:voidbot:main".to_string(),
daemon_id: "voidbot".to_string(),
desired_revision: "abc123".to_string(),
deployed_revision: "old123".to_string(),
strategy: "restart-after-verified-build".to_string(),
phases: vec![
"fetch upstream main".to_string(),
"verify health".to_string(),
],
migration_plan_id: migration_plan.plan_id.clone(),
artifact_id: artifact.artifact_id.clone(),
status: "planned".to_string(),
planned_at: "2026-06-04T00:00:02Z".to_string(),
};
let release_payload = rmp_serde::to_vec(&release_target)?;
assert_eq!(&release_payload[..3], &[0xdc, 0, 22]);
let mut pre_floor_payload = release_payload.clone();
assert_eq!(pre_floor_payload.pop(), Some(0xc0));
pre_floor_payload[2] = 21;
assert!(
rmp_serde::from_slice::<IdunnReleaseTargetRecord>(&pre_floor_payload).is_err(),
"release-target v3 must not decode a pre-floor positional body"
);
node.put(&release_target.target_id, &release_target)?;
node.put(&artifact.artifact_id, &artifact)?;
node.put(&migration_plan.plan_id, &migration_plan)?;
node.put(&rollout_plan.plan_id, &rollout_plan)?;
let runtime_check = IdunnRuntimeTransportCheckRecord {
check_id: "idunn-runtime-rudp-loopback".to_string(),
runtime_id: "idunn-daemon".to_string(),
transport: "cultnet.transport.rudp.v0".to_string(),
state: "available".to_string(),
detail: "loopback acknowledged".to_string(),
observed_at: "2026-06-04T00:00:02Z".to_string(),
};
node.put(&runtime_check.check_id, &runtime_check)?;
let reloaded = CultMesh::create_node(
&store_path,
OdinDocuments,
CultMeshNodeOptions {
runtime_id: "idunn-test-reloaded".to_string(),
pull_on_start: true,
},
)?;
assert_eq!(
reloaded
.documents()
.binding("idunn.desired_daemon")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_DESIRED_DAEMON_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.swarm_surgery_plan")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_SWARM_SURGERY_PLAN_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.daemon_surgery_plan")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_DAEMON_SURGERY_PLAN_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.daemon_transport_profile")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_DAEMON_TRANSPORT_PROFILE_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.command_boundary")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_COMMAND_BOUNDARY_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.release_target")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_RELEASE_TARGET_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.deployment_artifact")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_DEPLOYMENT_ARTIFACT_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.state_migration_plan")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_STATE_MIGRATION_PLAN_SCHEMA)
);
assert_eq!(
reloaded
.documents()
.binding("idunn.rollout_plan")
.and_then(|binding| binding.payload_schema_version.clone())
.as_deref(),
Some(IDUNN_ROLLOUT_PLAN_SCHEMA)
);
let reloaded_desired =
reloaded.get_required::<IdunnDesiredDaemonRecord>(&desired.daemon_id)?;
assert_eq!(reloaded_desired.transport_profile_id, "transport:voidbot");
assert_eq!(
reloaded_desired.command_boundary_id,
"command-boundary:voidbot"
);
assert_eq!(
reloaded
.get_required::<IdunnKeepaliveDecisionRecord>(&plan.decision.decision_id)?
.action,
"restart"
);
assert_eq!(
reloaded
.get_required::<IdunnSwarmSurgeryPlanRecord>(&swarm_surgery_plan.plan_id)?
.next_target,
"starfire-muninn"
);
assert_eq!(
reloaded
.get_required::<IdunnDaemonSurgeryPlanRecord>(&surgery_plan.plan_id)?
.cut_line,
"daemon health must be published over CultNet/RUDP"
);
assert_eq!(
reloaded
.get_required::<IdunnDaemonTransportProfileRecord>(&transport_profile.profile_id)?
.target_transport,
"cultnet.transport.rudp.v0"
);
assert_eq!(
reloaded
.get_required::<IdunnCommandBoundaryRecord>(&command_boundary.boundary_id)?
.health_authority,
"daemon-published-rudp-health"
);
assert_eq!(
reloaded
.get_required::<IdunnRuntimeTransportCheckRecord>(&runtime_check.check_id)?
.state,
"available"
);
assert_eq!(
reloaded
.get_required::<IdunnReleaseTargetRecord>(&release_target.target_id)?
.upstream_branch,
"main"
);
assert_eq!(
reloaded
.get_required::<IdunnRolloutPlanRecord>(&rollout_plan.plan_id)?
.migration_plan_id,
"migration:voidbot:main"
);
assert_eq!(
reloaded
.get_required::<IdunnRestartRequestRecord>(&request.request_id)?
.command,
"npm start"
);
Ok(())
}
}

View file

@ -1,13 +1,11 @@
pub mod discovery;
pub mod documents;
pub mod idunn;
pub mod pipeline;
pub mod ports;
pub mod repository;
pub use discovery::*;
pub use documents::*;
pub use idunn::*;
pub use pipeline::*;
pub use ports::*;
pub use repository::*;

View file

@ -250,12 +250,36 @@ impl CultCacheIdunnProjectionSource {
if !self.path.is_file() {
return Ok(None);
}
Ok(Some(
SingleFileMessagePackBackingStore::new(&self.path).pull_all_read_only_snapshot()?,
))
SingleFileMessagePackBackingStore::new(&self.path)
.pull_all_read_only_snapshot()
.map(Some)
.map_err(|error| {
// The store's decode step reports its failures as text, so an
// I/O error in the chain here is from opening or reading the
// file and from nothing the file said.
if error.chain().any(|cause| cause.is::<std::io::Error>()) {
error.context(ProjectionUnreadable)
} else {
error
}
})
}
}
/// The projection file could not be opened or read. Marks the read step only:
/// a failure while decoding or validating what was read never carries it,
/// whatever it wraps.
#[derive(Debug)]
pub struct ProjectionUnreadable;
impl std::fmt::Display for ProjectionUnreadable {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("Idunn's projection file could not be read")
}
}
impl std::error::Error for ProjectionUnreadable {}
fn provider_anchor(
entries: &[CultCacheEnvelope],
target: &str,
@ -756,6 +780,15 @@ where
.context(PresenceAuthorityRefused(
"the projected runtime authority cannot be verified",
))?;
// A host whose clock has not reached the anchor's binding (booting
// before time sync) cannot yet tell whether the anchor is good. That is
// not a refusal of the authority, so it must not end Odin: it retries.
if authority.is_none()
&& incoming.is_some()
&& only_clock_precedes_anchor(&projection, &projection_disagreements, now)
{
bail!("the clock ({now}) precedes the provider trust anchor's binding; retrying");
}
let (lease_sha256, lease_disagreement) = classify_current_lease(&projection)?;
let query = OdinStoreQuery {
incarnation: IncarnationRef::of(&projection.expected)?,
@ -1274,6 +1307,8 @@ fn dependency_permits_ready(kind: &str, ready: bool) -> bool {
matches!(kind, "optional" | "external-operator-binding") || ready
}
const PROVIDER_ANCHOR_TIME_CODE: &str = "provider-trust-anchor-time";
fn classify_runtime_authority(
projection: &IdunnRuntimeProjection,
idunn_anchor: &ServiceIdentityTrustAnchor,
@ -1334,7 +1369,7 @@ fn classify_runtime_authority(
.is_some_and(|expires_at| now >= expires_at)
{
disagreements.push(OdinTopologyDisagreement {
code: "provider-trust-anchor-time".into(),
code: PROVIDER_ANCHOR_TIME_CODE.into(),
expected: Some(format!("current-at:{now}")),
observed: Some(format!(
"bound:{};expires:{}",
@ -1404,6 +1439,23 @@ fn classify_runtime_authority(
Ok((authority, disagreements))
}
/// The projected provider anchor is not yet bound at `now` and nothing else
/// disagrees, so a later reading of the same projection would verify.
fn only_clock_precedes_anchor(
projection: &IdunnRuntimeProjection,
disagreements: &[OdinTopologyDisagreement],
now: u64,
) -> bool {
projection.provider_anchor.as_ref().is_some_and(|anchor| {
anchor.bound_at_unix_millis > now
&& !anchor
.expires_at_unix_millis
.is_some_and(|expires_at| now >= expires_at)
}) && disagreements
.iter()
.all(|disagreement| disagreement.code == PROVIDER_ANCHOR_TIME_CODE)
}
fn classify_current_lease(
projection: &IdunnRuntimeProjection,
) -> Result<(Option<String>, Option<OdinTopologyDisagreement>)> {
@ -2558,6 +2610,82 @@ mod tests {
Ok(())
}
fn is_refused(error: &anyhow::Error) -> bool {
error.downcast_ref::<PresenceAuthorityRefused>().is_some()
}
/// A host whose clock has not reached the anchor's binding (booting before
/// time sync) is retried, not refused: the same claim lands once the clock
/// catches up. The anchor is bound at NOW - 100.
#[test]
fn a_clock_before_the_anchors_binding_is_retried_not_refused() -> Result<()> {
let world = TestWorld::new()?;
let service = world.service("ghostlight", Vec::new(), false)?;
let claim = service.signed_presence(1, |_| {})?;
let error = world
.engine(NOW - 200)
.admit_presence("ghostlight", &claim, NOW - 200)
.unwrap_err();
assert!(!is_refused(&error), "{error:#}");
world
.engine(NOW)
.admit_presence("ghostlight", &claim, NOW)?;
Ok(())
}
/// The clock exemption is only for the binding time. An anchor that has
/// expired, or one that also disagrees with Expected, is a refusal.
#[test]
fn only_a_clock_before_the_binding_is_exempt_from_refusal() -> Result<()> {
let world = TestWorld::new()?;
let mut service = world.service("ghostlight", Vec::new(), false)?;
let claim = service.signed_presence(1, |_| {})?;
service.projection.provider_anchor.as_mut().unwrap().expires_at_unix_millis =
Some(NOW - 50);
world.publish_projection(&service.projection)?;
let error = world
.engine(NOW)
.admit_presence("ghostlight", &claim, NOW)
.unwrap_err();
assert!(is_refused(&error), "expired: {error:#}");
service.projection.provider_anchor.as_mut().unwrap().expires_at_unix_millis = None;
service.projection.provider_anchor.as_mut().unwrap().runtime_id = "other-runtime".into();
world.publish_projection(&service.projection)?;
let error = world
.engine(NOW - 200)
.admit_presence("ghostlight", &claim, NOW - 200)
.unwrap_err();
assert!(is_refused(&error), "early and mismatched: {error:#}");
Ok(())
}
/// The clock exemption belongs to a claim being admitted. A refresh has no
/// claim to refuse: with the clock before the binding it still writes the
/// incarnation's correlation, carrying the time disagreement, and neither
/// fails nor is refused.
#[test]
fn a_refresh_before_the_anchors_binding_records_the_time_disagreement() -> Result<()> {
let world = TestWorld::new()?;
let service = world.service("ghostlight", Vec::new(), false)?;
let signed = world
.engine(NOW - 200)
.refresh(&service.incarnation()?)?
.expect("Idunn projects the incarnation");
let correlation = decode_signed(&signed)?;
assert!(
correlation
.disagreements
.iter()
.any(|disagreement| disagreement.code == PROVIDER_ANCHOR_TIME_CODE),
"{:?}",
correlation.disagreements
);
assert!(!correlation.ready);
Ok(())
}
fn snapshot_with_stored_presence(
service: &TestService,
stored: AdmittedPresence,

File diff suppressed because it is too large Load diff

View file

@ -46,35 +46,24 @@ The first Rust core lives in `crates/odin-core`:
- `repository.rs`: `OdinRepository` abstraction, in-memory mock repository, and
CultMesh-backed repository.
The Rust spine owns the future architecture. The CommonJS daemon remains the
legacy operational body until each organ crosses this typed boundary.
The deployed body is `crates/odin-daemon`, built and run by Idunn from
`deployment/idunn/recipe.toml`. `crates/odin-core` holds the typed record spine
that the rest of the machine grows into.
## Runtime Body
Odin's executable body is split by ownership:
- `crates/odin-core`: Rust target core. Owns typed Odin documents, ingest
ports, normalization, and CultMesh/CultCache repository boundaries. This is
the replacement spine; JavaScript remains legacy runtime scaffolding until
each organ has crossed the typed boundary.
- `src/odin-coordinator.cjs`: process lifecycle, serialized refresh loop,
persistence, health, and transport wiring. Refreshes must not overlap because
a refresh publishes Odin's daemon health.
- `src/odin/config.cjs`: runtime paths, refresh intervals, and CultLib module path setup.
- `src/odin/documents.cjs`: CultCache/CultMesh document definitions accepted by Odin.
- `src/odin/idunn-rudp.cjs`: daemon-owned Odin provider health publication to
Idunn over the canonical CultNet RUDP `schema` channel.
- `src/odin/probes.cjs`: local Docker/ADB debug lowerings for Starfire and
Android edge visibility. Remote Verse health is not probed here; it comes
from provider-owned CultMesh/CultNet records.
- `src/odin/interfaces.cjs`: provider advertisements, CultMesh interface
bindings, accepted interface projection, and renderer-lowering metadata.
Renderer routes are lowerings outside Odin, not Odin-hosted transport.
- `src/odin/layout.cjs`: `odin.interface_layout.v1` read/write and merge policy.
- `src/odin/marquee.cjs`: canonical marquee tape assembly from Stonks securities and ordered VoidBot poem lines.
- `src/odin/surface.cjs`: `gamecult.eve.surface.v1` tree projection.
- `src/odin/state.cjs`: one refresh's input records into Odin's provider catalog/proxy state.
The entrypoint is not allowed to grow new probe, surface, provider, layout, or renderer policy. If a new owner is needed, name the owner and its invariant before adding code.
- `crates/odin-daemon`: the deployed process. Reads Idunn's read-only topology
projection, admits provider presences over RUDP for the incarnations Idunn
projects, persists them through CultMesh/CultCache, publishes signed
runtime-topology correlation records, and answers CultNet/RUDP snapshot and
document requests. Deployment, restart, and survival of this process are
Idunn's.
- `crates/odin-core`: typed Odin documents, ingest ports, normalization, and
CultMesh/CultCache repository boundaries.
If a new owner is needed, name the owner and its invariant before adding code.
The accepted document registry is also the persistence admission boundary.
Unknown schemas may be observed for diagnostics but are not silently persisted
@ -94,19 +83,15 @@ the typed `gjallar.overview` Eve surface. Eve clients own graphical, terminal,
and framebuffer lowering. Gjallar must not own the underlying registry, probe,
provider truth, client pixels, or translation decisions.
Idunn is the named keepalive organ for daemon continuity. Its current Rust
body lives in `crates/idunn-daemon` and `crates/odin-core/src/idunn.rs`. Idunn may read
Odin-owned service records and provider advertisements, then bring daemons up
after reboots or crashes, watch health, emit keepalive observations, restart
requests, denied-action records, and operator alarms. When human action is
needed, Idunn uses CultMesh to request a Bifrost-owned operator
notification crossing. VoidBot's `voidbot.operator-dm` command `owner.dm.send`
is a demoted compatibility delivery actuator, not the owner. The target command
lives in Bifrost's Verse; any still-VoidBot delivery path must be invoked by
Bifrost or documented as migration debt. Idunn must not own Verse discovery,
schema truth, provider dashboards, identity grants, Discord delivery, owner-DM
delivery, or renderer layout. Keepalive loops belong in Idunn, not Odin's
coordinator or individual daemons.
Idunn (`GameCult/Idunn`) owns deployment and daemon survival, including Odin's.
It builds from each target's recipe, activates and restarts the admitted
incarnation, and holds the deployment and lifecycle brakes. Odin publishes what
it can see and reads Idunn's topology projection; it does not start, restart,
deploy, or health-probe any daemon, and no Odin code path keeps a process alive.
When human action is needed, Idunn uses CultMesh to request a Bifrost-owned
operator notification crossing. Idunn must not own Verse discovery, schema
truth, provider dashboards, identity grants, Discord delivery, owner-DM
delivery, or renderer layout.
Muninn is the portable local telemetry Verse assembler. Its Rust body lives in
the `GameCult/Muninn` repository and publishes `muninn.telemetry_surface.v1`
@ -213,19 +198,10 @@ This is not a reporting nicety. It is how Odin prevents services from becoming
private little islands with separate websites, dashboards, state formats, and
separate command languages.
## Current Service Surface
## Service Surface
Odin currently publishes service squares for:
- Starfire: Odin, Docker, ADB, Odin CultCache, and running Docker containers.
- Nightwing: SSH, Eve broker, Eve browser reference, visible TUI, Docker unit state, and NVIDIA GPU state.
- EVE: SSH/native Eve reachability.
- Periwinkle: ADB reachability.
- Periwinkle: local ADB edge status plus any provider-owned surface or advertisement Odin receives through CultMesh/Odin discovery.
- Raven: SSH reachability.
- Yggdrasil: compatibility host reachability plus nginx,
StreamPixels, Heimdall, Repixelizer, and Bifrost systemd state until those
daemon surfaces publish health and command boundaries over CultNet RUDP.
Odin's service records come from provider-owned presences and advertisements
received through CultMesh/RUDP, not from host probes.
Gjallar consumes Odin's accepted CultMesh/Eve state and composes every visible
provider surface into one typed aggregate on Yggdrasil. EveCanvas, browser, and
@ -250,8 +226,8 @@ interactive language; they are not separate dashboard products. Huginn's `.cc`
inspection surface is the current clean example: Huginn inspects CultCache bytes
and emits Eve DSL, while Eve or any other runtime owns presentation.
Provider advertisements are the promotion path out of probing. Odin's CJS
document set already accepts `gamecult.eve.provider_advertisement.v1` alongside
Provider advertisements are the promotion path out of probing. Odin's document
registry already accepts `gamecult.eve.provider_advertisement.v1` alongside
`gamecult.eve.interface_binding.v1` and `gamecult.eve.surface_state.v1`.
Daemons should publish advertisements that name service id, Verse id, schema
catalog, `.cc` witnesses, Eve surface keys, command boundaries, nested Verses,
@ -290,18 +266,6 @@ identity.
The canonical contract lives in
`E:\Projects\Eve\docs\provider-advertisement-contract.md`.
Odin persists operator layout intent as `odin.interface_layout.v1` in the Odin CultMesh store under record key `odin.providers`. The ignored `scratch/odin/interface-layout.json` file is migration input only; Odin no longer writes layout truth back to that sidecar. Layout intents name the provider id and request focus, move, resize, or visibility changes; renderers are input devices for those intents, not local layout owners.
Odin now derives dense top-level layout intent from each provider's retained
`surface.root` tree: element count, leaf count, branch count, depth, text-cell
pressure, and list-like branches. Provider explicit preferred sizes are capped
at the Odin wrapper boundary unless the current intent is fullscreen, so stale
layout files cannot keep empty panels huge. The renderer should use
`props.tree`, `props.layout.signalWeight`, and `props.packing` to allocate space
to nested signal, then recursively lower provider children. Flattening a
provider surface into one log/list is a compatibility failure when the retained
tree has children.
## Observation Surface
Odin no longer tails Mimir JSONL artifacts for observation truth. Device and

570
docs/store-recovery-cut.md Normal file
View file

@ -0,0 +1,570 @@
# Odin store repair and recovery: cut map
**Operator rulings, 2026-09-30:**
- **Q-R1:** "You got it". A foreign file under a running Odin is set aside and Odin continues.
- **Q-R2:** "Yep". Keep the previous generation on disk.
- **Q-R3:** "Start empty". Nothing decodable at activation means Odin starts empty.
- **Q-R4:** "Yep, and Idunn needs better reporting capabilities so it can serve as watchdog. I ought to get a message
about this on Discord if it ever happens."
- Stop writing after the third set-aside in 10 minutes, and keep serving.
- **New requirement:** Idunn watchdog reporting that reaches the operator on Discord. It is mapped as its own
campaign and is not an Odin cut.
- **Q-R5:** "That's fine, Odin still runs after all, right?" Yes. Odin keeps serving, and the store condition is
published as its own typed record, not as a degraded presence.
- **Q-R6:** "Approved". Both drills.
**R1 LANDED: CultLib main `daedfdd6` (2026-09-30).** The merged tree `0e6891b3` was verified on Yggdrasil in every
runtime, including the four-runtime store interop test. That test runs close to its 30 s per-command limit under
load (`cult-cache.test.ts:1407`), so it is fragile: follow-up. Consumer pins still gate their own bumps: Bifrost only
with R3b's TS fsync; Muninn and Idunn with their error-text matches.
**R1 status (Self, 2026-09-30).** Built on CultLib `hands/cultcache-read-failures` (`c0b56e06`..`b7908668`), then
fix batch 2 (`..2d7a3c78`). Soul's verdict on batch 2 was to hold the merge. Batch 3 is in Hands, fixing:
- **Blocker, F1 (zero-byte stores on Yggdrasil).**
- Two zero-byte stores are live: `/srv/bifrost/persona-feedback/{inbox/observations,outbox/deliveries}.cc`.
- They are pre-created by `touch` in gamecult-ops `install-bifrost-persona-feedback-native-yggdrasil.sh:42,46`
and `bootstrap-bifrost-persona-feedback-yggdrasil.sh:22,29`.
- Bifrost `persona-feedback.mjs serve` and Epiphany `epiphany-swarm` crash-loop on them once their CultLib pins
move past R1.
- **Neither pin may move until the scripts stop pre-creating stores and Self has moved the live files aside.**
- **F4.** Rust errors echo record values through serde's text (`lib.rs:753-758`). The header is echoed without bound.
- **F6.** A dangling symlink reads as absent, and the writer then replaces it.
- **F7.** The same NotFound-only rule in Python's `JsonLinesBackingStore` and in TS `pullAll`'s repair write.
- **M7.** A header-prefix fixture.
Recorded for later cuts:
- **F3 → R2/R3b.** TS, Python and C# decode the whole buffer before reading the header. A truncated newer-format
store (20..n-1 bytes) is "unsupported format" in Rust and a raw decode error elsewhere.
- **F5 → R3.** C# `ReadSnapshot` reads ENOTDIR/EISDIR as an empty store (`FileInfo.Exists`, `CultCache.cs:3439`).
C# `DirectoryMessagePackBackingStore` `File.Exists` (`:409`, `:497`) is the same. This is a live disagreement between
runtimes until R3.
- **F8 → R3b.** Python leaves its staging file behind when a write fails (`stores.py:124-128`).
- **Tooling.**
- `packages/cultcache-rs` has no committed `Cargo.lock`, so the map's `cargo test --locked` cannot run.
- The cultcache-ts interop test needs `CARGO_TARGET_DIR` unset under ygg-verify.
- **Soul on batch 3 (`2d7a3c78..3c9e195b`, gamecult-ops `bca7978`), 2026-09-30: merge both.** Pre-merge batch 4 is in
Hands: fix the README symlink claim (S1), close value echoes in the redb and typed-decode paths (S2) and in the
Python/TS non-string key echo (S3), make the unlock policy coherent (S9), classify encode refusals separately from
`NotReplaced` (S10), and fix a test timeout (S11). The ops branch deletes the guard test (S6) and the dead compose
layout (S5).
- **Open for R3:** writes through a symlinked store path replace the link, so a store behind a link to a mounted
volume moves into the container layer. R3 decides resolve or refuse.
- **Open for R3b:** Python and TS never fsync. After F1 their first create-by-rename is not covered by ext4
`auto_da_alloc`, so a crash can leave zero bytes, which every post-R1 reader refuses forever. **Bifrost's CultLib
pin moves past R1 only together with R3b's TS fsync.**
- **For W0 (Odin):** classify a write failure through `CultCacheStoreWriteFailed.kind` together with its `io::Error`,
never retry on `NotReplaced` alone (S10).
- Deployed Epiphany is `d2ca6630`, using a vendored cultcache-rs (`ba6a4874`) that reads a missing store as empty.
All Epiphany units have been inactive since 2026-08-22.
- **Done 2026-09-30 20:48 CEST (Self).** gamecult-ops merged at `3fa22aa`. The inbox is setgid 2750. All three
zero-byte stores were renamed in place and moved to `/root/zero-byte-stores-20260930/` on Yggdrasil, with
`manifest.txt` recording owner, mode and mtime. None had gained bytes. All three Bifrost units stayed active, and
persona-feedback logged nothing and did not restart.
- The compose/container lane (the compose file, two deploy scripts, the `.ps1` uploader, two runbooks) is dead:
the live Bifrost units run native `dotnet` and `node`. **Deleted 2026-09-30, operator: "delete the Bifrost lane".**
gamecult-ops `fdb4356`. Its live trigger, the Idunn legacy actuator route `deploy bifrost-persona-feedback`, was
retired in Idunn `7793b5b` and installed on Yggdrasil at 21:50 CEST (sha256 `d95fa08c...`; the previous binary is
at `/root/idunn-yggdrasil.pre-bifrost-retire-20260930`). The manifest and `/srv/bifrost/compose` were moved to
`/srv/retired/retired-2026-09-30/`. All three Bifrost units stayed active. **Follow-up:** persona-feedback has no
Idunn deploy route now. Give it a real Idunn binding.
- Live zero-byte stores to move aside after the ops merge, in Soul's order (setgid on inbox, then rename in place,
then relocate): `observations.cc`, `deliveries.cc`, and a third, `/var/lib/gamecult/epiphany/idunn-provider-health-admission.cc`.
- **Text matches.** gamecult-ops `scripts/idunn/start-idunn-local.ps1:143` matches the same removed text as Muninn
(below). It must change with Idunn's next CultLib bump.
- **Consumer follow-up that must travel with the next Muninn CultLib bump:**
`Muninn scripts/restart-starfire-muninn.ps1:144` matches `*failed to decode MessagePack*` to trigger
`Reset-CorruptMuninnStore`. R1 removes that text, so the reset would stop firing silently. The script must match
the typed `CultCacheStoreUnreadable` kind, or the new wording, in the same change.
- C#'s truncated-store exception (`EndOfStreamException`) is an `IOException`, the same I/O-versus-undecodable
overlap. It goes with R3's C# work.
Status: cut map, Imagination pass 1 (Opus), 2026-09-30. Nothing has landed. This map owns the means for repairing
and recovering `/var/lib/gamecult/odin/topology.cc`. It follows `docs/write-pattern-cut.md` (Cuts 1-3) and starts
from that campaign's landed head.
Anchors:
- Odin `hands/odin-write` at `cda2087` (Cuts 1-2 plus two test commits). Q3 B (peer puts fsynced before ACK) is
being finished on that branch now. **Re-anchor every Odin `file:line` below on the head that lands; the line
numbers here are `cda2087`.**
- CultLib `6d0cc963`. `packages/cultcache-rs` is byte-identical to Odin's pin `3bf1c0ce`, so the Rust line numbers
hold at both.
- Idunn `a96ad9d`, Heimdall `e8e2832`, gamecult-ops `runbooks/gamecult-state-backup.md` as of 2026-09-30.
Operator, 2026-09-30, on the three options Self offered for an undecodable foreign write (degraded but loud, end
Odin, unchanged): **"None of these options involve repair and recovery, which is what ultimately needs to happen."**
Open: Q-R1 to Q-R6 (section 8). D1 (section 6, O1) is a Self default the operator may overrule.
## Short answer
Odin already holds the one thing a repair needs: its in-memory working set, which is authoritative for everything
it owns while it holds the write lease. Today it never uses it for repair. It ends on a decodable foreign write, and
it silently stops persisting on an undecodable one. At activation it has no working set and no second copy, so it
crash-loops.
The repair has one rule: **a store file that is not what Odin last wrote is set aside, never deleted and never
merged, and Odin writes its working set over it under the lease.** At activation Odin has no working set, so it
falls back to a previous generation kept on disk, and then to an empty store that providers refill. The
correlation sequence gets a time floor, so no rollback can recreate the watermark deadlock. Recovery is published as
a typed record in Odin's own catalog. The mechanism (typed read failures, a set-aside exchange, a retained previous
generation, no staging file left behind by a failed write) belongs to CultLib, because every consumer of the
single-file store has the same gap.
## 1. Body facts (probed)
### 1.1 Probe: cultcache-rs single-file store under fault
The probe was a scratch crate at CultLib `3bf1c0ce`, `scratchpad/odin-recovery-probe` (Imagination session), commit
`b19eba9`, run through the stopgap on Yggdrasil in slot 2. It ran with `--tmpfs /small:size=64k --tmpfs /ro:ro`,
using `trap '' XFSZ; ulimit -f 40` for the file-size case. The log is `scratchpad/odin-recovery-probe.log`.
| Fault | Result |
|---|---|
| P1: file truncated to half | `compare_exchange_snapshot` and `pull_all` return `Err`, **not** `Ok(false)`. There is no `io::Error` in the chain; the decode error is flattened to text (`lib.rs:720-725`). The message is wrong: "invalid type: string \"cultcache.store.v1\", expected struct CultCacheEnvelope". `store_header` (`lib.rs:317-341`) deserializes the *whole* array to find the header, so a truncated v1 file has no header and falls through to the legacy decoder. The file is unchanged by the failed exchange |
| P2: 12 bytes of garbage appended | `pull_all` returns `Ok`, all 3 records. `compare_exchange_snapshot(expected)` returns **`Ok(true)` and silently drops the tail**. `rmp_serde::from_slice` ignores trailing bytes. C#'s `DeserializeSnapshot` (`CultDocumentMessagePackSerialization.cs:179-215`) never checks for the end either |
| P2b: one byte flipped inside a payload | `pull_all` returns `Ok` (payloads are opaque bytes). `compare_exchange_snapshot` returns **`Ok(false)`**. To Odin, bit rot is a *decodable foreign write* |
| P3: header `cultcache.store.v4...` | `Err` with a distinct message, "not readable; this runtime reads v1 only". It is distinguishable only by its text |
| P4: another handle wrote a valid snapshot | `Ok(false)` |
| P5: `hard_link(topology.cc, topology.cc.displaced.<ts>.x)`, then an exchange through the store | The link keeps the old bytes exactly, and the store writes the new file. The staging sweep (`remove_abandoned_staging_files`, `lib.rs:2730-2766`) removes a `<name>.<uuid>.tmp` beside it and leaves the displaced name alone |
| read-only filesystem, no store | `pull_all` returns `Ok(0)`. The exchange fails opening the lock: `io::Error` `ReadOnlyFilesystem` (os 30) |
| `RLIMIT_FSIZE` 40 KiB, 200 KB write | `Err` `io::Error` `FileTooLarge` (os 27). The old file is intact and still decodes. **A 40,960 B partial staging file is left behind.** The next attempt sweeps it and leaves a new one |
| 64 KiB tmpfs, 200 KB write (ENOSPC) | `Err` `StorageFull` (os 28). The old file is intact. **A 61,440 B partial staging file is left behind: all of the filesystem's remaining space.** The same holds on every retry |
What follows from the probe:
- **Decodable is not the same as legitimate.** A flipped payload byte is "decodable and foreign", and appended
garbage is "decodable and ours". The decodable/undecodable split cannot be what decides recovery. One rule has to
cover both: *not what Odin last wrote*.
- **A write that fails for lack of space holds the space it failed on.** `/var/lib/gamecult` is shared by every
service on Yggdrasil. Odin retries once per second (and, under Q3 B, on every put), so it re-takes whatever
another service frees. C# deletes its temp file in a `finally` (`CultCache.cs:3496-3531`); Rust does not
(`write_all_unlocked`, `lib.rs:741-768`). This is a Rust parity defect.
- **The atomic write keeps no previous generation.** `write_all_unlocked` stages, fsyncs, renames over the file and
fsyncs the directory. After the rename, the old inode is gone. The brief's "last good checkpoint kept by the
atomic write" does not exist on disk. The only last-good copy is Odin's in-memory `flushed` snapshot
(`lib.rs:506-510`), and it dies with the process.
### 1.2 Odin today (`cda2087`)
| Case | Path | Outcome |
|---|---|---|
| Decodable foreign write while serving | `MemoryOdinTopologyStore::flush` `lib.rs:613-626`: `compare_exchange_snapshot(flushed, current)` returns `false`, so `ForeignStoreWrite`. `survive` `main.rs:879-895` returns it, and Odin ends | Idunn restarts Odin (`control_plane.rs:68-71`: 6 restarts per hour, the wait doubling from 5 s). The new Odin **loads the foreign file as truth**. Odin's own unflushed changes, and every record of Odin's that the foreign file lacks, are lost without a trace. A flipped payload byte (P2b) takes this path |
| Undecodable foreign write while serving | the same exchange returns `Err` (decode). `survive` logs it and retries every second, forever. Nothing is persisted again | **Idunn's crossing breaks too.** `receive` (`Idunn drivers.rs:5005-5030`) reads the same file and propagates the error to `admit_latest_topology` (`control_plane.rs:7903-7907`) and `refresh_admitted_topology` (`:6040`). Every Odin-correlated deployment and refresh fails until the file is readable, which is never |
| Corrupt file at activation | `try_activate` `main.rs:246-297` calls `MemoryOdinTopologyStore::load` `lib.rs:519-538`, which calls `pull_all()?`. `main.rs:762` `while !try_activate()?` exits the process | Crash loop until Idunn's budget is spent. Then Idunn reports once, to its log only (`control_plane.rs:5709-5723`, `report_once` `:4599-4610` is `eprintln`), and stops. The Verse rendezvous is down |
| Disk full or read-only | the exchange returns `Err` (io). `survive` logs and retries every second | Odin serves from memory. Each attempt leaves a partial staging file (1.1). A stop or deploy in this state loses everything unflushed. Nothing typed says so |
Detection latency for a foreign write: `flush` writes only when dirty (`main.rs:307-314`). Odin's own heartbeat
(`HEARTBEAT_INTERVAL` 5 s, `main.rs:69`) dirties the set, so a foreign write is met at the latest one heartbeat plus
one flush interval (about 6 s) after it happens. No polling read is needed, and Cut 1's rule (no reads while
serving) stands.
### 1.3 Other sources of truth, and what they can recover
| Source | Holds | Freshness | Recovers |
|---|---|---|---|
| Odin's working set (W) | everything, including unflushed changes | live | everything, but only while this process lives and holds the lease |
| `flushed` (in memory) | the last snapshot Odin wrote | at most one interval old | nothing more than W; it is the comparison base, not a store |
| previous generation on disk (new, O3 and R3) | the snapshot before the last one this store's owner verified and replaced | one write old | everything except the last write's delta, including publish-once documents, presence history and the watermark |
| nightly backup (`gamecult-state-backup.md`) | `/var/lib/gamecult/odin` whole, shared-lock copy | up to 24 h old, 3 days on Yggdrasil, 14 days plus 8 weekly on Raven | publish-once documents. **Never automatically**: "It never becomes live authority, and nothing restores it automatically"; "restore a store only after stopping its owner" |
| providers republishing | presences (2.7-12 s), Muninn documents (2 s), ads (about 30 s); Heimdall HEAD republishes `heimdall.command_boundary` and its three siblings every 60 s (`Heimdall src/index.ts:19,35-39`, gated on its write lease) | seconds to a minute | every document whose publisher republishes |
| Idunn's projection plus presences | correlations | one refresh pass (250 ms) after the presences return | correlations. They are a projection and never need restoring |
| Idunn's `odin_publisher_sequence_cursor` (`control_plane.rs:1456,1537,6081`) | the highest correlation sequence Idunn admitted, per target | live | nothing directly (Idunn owns it), but it is why the watermark must never roll back |
Premise correction: `heimdall.command_boundary` is not publish-once at Heimdall HEAD. A `stored_at` of 09-23
proves only that the publisher's own `stored_at` has not changed, because Odin stores the publisher's
`stored_at` (`persist_generic_document`, `main.rs:1330-1347`). Whether the deployed Heimdall re-puts is
unverified; O3's outstanding-republish list measures it.
**The watermark is the one record whose loss is not self-healing.** Idunn refuses any correlation sequence at or
below its cursor (`control_plane.rs:1763,1785`). An Odin that starts from an empty store, from the previous
generation, or from a backup restarts a target's count below that cursor. That is the deadlock documented at
`lib.rs:33-49`: Idunn refuses every correlation until the count climbs past the old mark, and the refusal
withdraws the correlation before it can climb. Every recovery source except W has this defect. O1 removes it.
Other facts the design leans on:
- **Idunn fences before it grants.** `CultCacheWriteLeaseDriver` (`Idunn drivers.rs:4050-4205`): "revocation
follows an exact incumbent stop", and grant is a nonblocking exchange from empty. A second *Odin* cannot hold
the lease while the first is still writing, so a foreign writer seen by a lease-holding Odin is not an Odin.
- **Idunn has a lifecycle brake** (`idunn-provision lifecycle-brake-engage|release|status`,
`provisioning.rs:95-97`; `odin-lifecycle-brake.cc` per `runbooks/odin-yggdrasil.md:29-30`). An engaged brake
denies continuity restarts (`control_plane.rs:5771-5774`, `:6219`). This is the operator's hold for a manual
restore.
- **A non-`active` Odin presence fans out.** `reconcile` sets `ready` only when the presence state is `active`
(`lib.rs:946`), and `dependency_evidence` (`lib.rs:1184ff`) makes every managed dependent of Odin not Ready.
The presence schema allows `degraded` (`cultnet-rs runtime_authority_contracts.rs:193`), but using it would
be a readiness statement about every dependent, not a store report.
- **The catalog serves only peer types and presences** (`public_document`, `main.rs:590-640`). No `odin.*` record
is served today, and neither Odin nor Idunn publishes an Eve surface. F3 in the write map (the unowned
`surface:gamecult.network.status`) is where an Eve lowering belongs.
## 2. Ends
1. No single fault, whether a foreign write of any kind, a corrupt file, or a failed write, ends Odin or leaves it
silently unpersisted. Only losing the write lease ends it.
2. A store file that is not what Odin last wrote is preserved byte for byte beside the store and never deleted,
merged or adopted by any automatic path. The owner (Odin under the lease) writes its own state over it.
3. At activation a corrupt store is recovered from the best source on the host (previous generation, then empty),
and never from the backup automatically.
4. No recovery can make a correlation sequence repeat or fall below one Idunn has admitted.
5. Every repair, and every stretch in which Odin is serving without persisting, is typed state that can be
queried through Odin's catalog while it is happening, including when the disk refuses writes.
Invariants carried over: one writer (the lease, checked immediately before every write); Idunn's lockless
crossing reads a whole `.cc` snapshot at `topology.cc`; the path never becomes absent (Idunn reads an absent file
as an empty snapshot, which withdraws every correlation); no reads of the store while serving; one bad record
never stops Odin.
## 3. Identity, lifecycle, authority
| Persistent kind | Named by | Over time | Decided by |
|---|---|---|---|
| store file `topology.cc` | the state root plus `TOPOLOGY_SLOT` | replaced whole by the owner's exchange; never absent once created | Odin under the lease |
| displaced file (new) | `topology.cc.displaced.<UTC yyyymmddThhmmssZ>.<uuid>`, same directory (it must not end in `.tmp`) | created by a set-aside exchange as a hard link to the replaced inode; never modified; never deleted by any program. Travels to Raven with the nightly backup | created by Odin through CultLib; **disposed of only by the operator** |
| previous generation (new, Q-R2) | `topology.cc.previous` | replaced on every *verified* exchange (current == expected) by a hard link to the outgoing inode; **not** rotated by a set-aside exchange, so it always holds a generation that some writer verified before replacing | CultLib mechanism; Odin opts in |
| store condition record (new) | type `odin.store_condition`, schema `odin.store_condition.v1`, key `topology` | one record held in W; rewritten when the condition changes; flushed with the store; served by the catalog | Odin |
| correlation sequence floor (new rule, not a record) | per target, in the existing watermark | next = max(mark + 1, prior + 1, now in unix ms) | Odin |
| backup snapshot | `/var/backups/gamecult-state/<date>/` and Raven | nightly; never live authority | the operator restores it, with the lifecycle brake engaged (runbook) |
## 4. Authority map (the whole campaign)
- **Owner:** Odin, while it holds the current process-write lease, decides what `topology.cc` holds, when it is
set aside, and what activation recovers from. CultLib owns the mechanism (read classification, set-aside
exchange, previous generation, staging cleanup) and no policy. Idunn owns whether Odin runs (continuity budget,
lifecycle brake, deploys). The operator owns restores from backup and the disposal of displaced files.
- **Inputs:** W; `flushed`; the lease (read immediately before every write); at activation, `topology.cc` and then
`topology.cc.previous`; the clock (the sequence floor).
- **Outputs:** one atomic `topology.cc`; displaced files; `topology.cc.previous`; the `odin.store_condition` record
in the catalog.
- **Derived state:** `topology.cc` is a checkpoint and Idunn's crossing, never an input while serving. The
displaced files are evidence only: nothing reads them automatically. The previous generation is read only by
activation recovery. The condition record is a report and decides nothing.
- `ForeignStoreWrite` **is no longer an owner of Odin's life**. A foreign write becomes a set-aside event.
- **Forbidden writers and deciders:**
- Any path that ends Odin on a store condition. `survive`'s `ForeignStoreWrite` arm (`main.rs:884`) is
deleted; `WriteLeaseLost` stays, and so does the temporary `PresenceAuthorityRefused` rule.
- Any automatic delete of a displaced or previous file.
- Any merge of records from a foreign or displaced file into W.
- Any automatic restore from the backup.
- Idunn writing or repairing `topology.cc`.
- Any second writer of the file inside Odin: activation never writes. It prepares W and `flushed`, and the one
`flush` makes the first write through the same set-aside exchange.
- **Shared paths:**
- The interval flush, the Q3 B per-put flush, the stop flush and the activation-recovery first flush all go
through one primitive, `replace_snapshot_setting_aside`.
- Every path that reserves a correlation sequence goes through the one floored reservation.
- **Deletion line:** `ForeignStoreWrite` (`lib.rs:630-641`) and its `survive` arm are deleted before the set-aside
exchange is wired in. The `Err` escape in `load` (`lib.rs:521`, `pull_all()?`) is replaced by the recovery
ladder, not wrapped.
## 5. Recovery by case
| Case | Detected by | Action | Recovered from | Loss bound | Visible as |
|---|---|---|---|---|---|
| **A. Decodable foreign write while serving** (a hand edit, a tool, a restore made against the runbook, or bit rot in a payload per P2b) | the next flush: current != `flushed` | the lease is checked; the file is set aside (hard link); W is written; Odin keeps serving | W | none of Odin's own records. The foreign file's content is preserved, not adopted | condition event `displaced`, reason `mismatch`, with path, sha256, bytes and time |
| **B. Undecodable foreign write while serving** (truncation, garbage, a foreign format, and after R2 trailing bytes too) | the next flush: current does not decode | the same | W | none. Idunn's crossing is unreadable for at most one heartbeat plus one flush (about 6 s), instead of forever | the same, reason `undecodable` |
| **A/B repeated** (a live foreign writer) | a third set-aside inside 10 minutes in one activation | Odin stops writing and keeps serving; puts are not ACKed (Q3 B: ACK means durable); no new displaced files (Q-R4) | W stays in memory | nothing while the process lives | condition `contested`, since when, with the displaced files |
| **C1. Undecodable store at activation** | `load` gets `Undecodable` | W is built from `topology.cc.previous` if it decodes; `flushed` is set to "unreadable", so the first flush sets the corrupt file aside and writes W | the previous generation | the last write's delta: one interval of Odin bookkeeping, or one fsynced put. **A put ACKed as durable can be lost here; say so in Q3 B's record** | event `recovered-previous`, plus an outstanding-republish list: peer records in the previous generation not yet re-put by their publisher, cleared as they arrive |
| **C2. Nothing decodable at activation** | `.previous` absent or undecodable too | start empty (Q-R3); the first flush sets the corrupt file aside | providers republish; correlations re-derive in one pass; the sequence floor holds | every publish-once document; presence history (F4 makes this harmless) | event `started-empty`; outstanding list is `unknown`. The operator's path is the backup, with the lifecycle brake engaged |
| **C3. Unsupported format at activation** (a newer runtime wrote v4, or a downgrade) | `load` gets `UnsupportedFormat` | refuse to activate. The file is untouched and nothing is set aside | the deploy or rollback that caused it; Idunn owns that | none | Idunn's continuity budget and the Odin journal. This is a deployment fault, not a store fault |
| **C4. I/O error reading the store at activation** (EIO, EACCES after a restore with the wrong owner, EISDIR) | `load` gets an `io::Error` | refuse to activate. Nothing is set aside: **only bytes that were read are ever judged** | the operator fixes the host or the mode | none | as C3 |
| **D. Write fails while serving** (ENOSPC, EDQUOT, EFBIG, EROFS, EIO) | the exchange returns an `io::Error` while staging or renaming | nothing is set aside; the staging file is removed (R1); W is kept; retry at the flush interval; puts are not ACKed; Odin keeps serving | W, when the fault clears | nothing, unless Odin is stopped or redeployed before the fault clears (follow-up I-F1) | condition `unpersisted` since T, with the last error kind (`no-space`, `read-only`, `io`) and the dirty-record count, served from memory |
| **D'. Stop while unpersisted** | the stop flush fails | log it and stop, as today (`main.rs:794-799`) | the next activation loads the last good file | the unflushed delta | the final journal line only. I-F1 is the owner fix |
Rollback safety, for C1, C2 and an operator restore: every source except W carries a watermark below Idunn's
cursor. The floor (O1) makes the next sequence exceed every sequence published before, whatever the store holds.
## 6. Cuts
Order: the Rust substrate gaps first (R1), then the watermark floor (O1, independent and small), the set-aside
primitive (R3), Odin's serving repair (O2), activation recovery (O3), parity hardening (R2, R3b), and finally the
runbook and drill (X1). Each cut is its own branch and its own Soul pass. Odin verification:
`cargo test --locked -p odin-daemon` from the Odin root. CultLib Rust: `cd packages/cultcache-rs && cargo test
--locked`. All of it runs on Yggdrasil through the stopgap. **A permission-based test is unproven there (root).**
The injections below avoid permissions: truncation, a directory at the store path (EISDIR), tmpfs size and
read-only options, and `RLIMIT_FSIZE`.
### R1. cultcache-rs: typed read failures, no staging file left behind, a header that survives truncation (CultLib)
- **Repo/branch:** CultLib `hands/cultcache-rs-store-faults` from `main`. Depends on nothing.
- **Deletes first:** the flattening `map_err(|error| anyhow!(...))` in `read_all_unlocked` (`lib.rs:720-725`);
`store_header`'s whole-array visitor (`lib.rs:317-341`).
- **Adds:**
- `pub struct CultCacheStoreUnreadable { path, kind: Undecodable | UnsupportedFormat }`, carried in the error
chain of every read of a single-file store whose bytes were read and not accepted, so consumers can
`downcast_ref`. An `io::Error` stays an `io::Error`, and the two never overlap.
- `store_header` reads only the array header and the first string, reusing `read_array_header` and
`read_string` (`lib.rs:2661-2700`). A truncated v1 file then reports as an undecodable v1 file, not as legacy.
- `write_all_unlocked` removes its own staging file on every failure after creating it. This is C# parity with
`CultCache.cs:3525-3530`.
- **Rules that must die under their own mutation:**
- a truncated store is `Undecodable`, and a v4 header is `UnsupportedFormat`;
- an `io::Error` read failure is never classified `Unreadable`;
- after a failed write, the directory holds no `<name>.*.tmp`.
- **Verification:**
- Tests describe behaviour. The staging-cleanup test forces the failure without permissions. One option is a
child process under `RLIMIT_FSIZE` with SIGXFSZ ignored (the probe's method); another is a path whose parent
fills a size-capped tmpfs, if the test runner can mount one. Hands picks one and names it.
- `cargo mutants --in-diff`.
- **Estimate:** about -30 / +70.
### O1. The correlation sequence has a time floor (Odin) — default D1
- **Repo/branch:** Odin `hands/odin-sequence-floor` from the landed `hands/odin-write` head. Depends on nothing
else.
- **Change:** `reserve_publisher_sequence` (`lib.rs:702-723`) returns max(mark + 1, prior + 1, now in unix ms),
and records it as the mark. The doc comment at `lib.rs:33-49` gains the rule: "A mark read from any older copy
of the store is a floor, never the next value; the clock makes every rollback safe."
- **Why a floor and not a read of Idunn's cursor:** it needs no new crossing. It holds for every recovery source,
including the backup, and the host clock is already trusted for presence freshness.
- **Assumption:** the clock does not step back across a rollback by more than the rollback's age. Soul checks that
Idunn accepts a jump: its rule is "strictly above the cursor" (`control_plane.rs:1763,1785`). No test may pin a
gap limit.
- **Verification:**
- Existing tests pinning counts (`withdrawing_a_correlation_does_not_reset_its_publisher_sequence`
`lib.rs:1996-2014`) are restated as order properties.
- New test: a store rolled back to an earlier snapshot, then one refresh, yields a correlation sequence greater
than every sequence published before the rollback.
- New test: two reservations inside one millisecond are strictly increasing.
- The clock must be injectable at the reservation. At least one mutant must be a function of the time (an offset
or a scale), with probe values where it would show.
- **Estimate:** about -5 / +25.
### R3. The set-aside exchange and the previous generation (CultLib, Rust and C#; TS and Python in R3b)
- **Repo/branch:** CultLib `hands/cultcache-set-aside` from R1. Q-R2 decides the previous-generation half.
- **Adds, on `SingleFileMessagePackBackingStore`:**
- `replace_snapshot_setting_aside(expected, replacements) -> Result<SnapshotReplacement>`, with
`SnapshotReplacement::{Exchanged, SetAside { displaced: PathBuf, sha256, bytes, reason: Mismatch |
Undecodable }}`. It makes one exclusive lock hold, in this order:
1. stage the replacement (write and fsync);
2. read current;
3. if current equals expected, rename the staged file over (rotating `.previous` first when enabled);
4. if the read was an `io::Error`, remove the staging file, return the error and change nothing;
5. otherwise hard-link current to the displaced name, fsync the directory, rename the staged file over, and
fsync the directory again.
Staging comes first, so a full disk sets nothing aside. The path is never absent: a link, never a rename. A
set-aside never rotates `.previous`.
- Opt-in previous generation: a constructor option (`retaining_previous_generation`, or Hands' spelling of it)
plus `pull_previous_generation() -> Result<Vec<CultCacheEnvelope>>`, read under the shared lock and classified
like any read. Off by default, because existing consumers must not grow files they did not ask for.
- Filesystems without hard links: return an explicit error naming the filesystem. Do not fall back to a
copy-then-rename, which would make the path briefly hold new content while the evidence is still being
written. Windows NTFS links work; the Windows path is **not yet reached** by the Yggdrasil suite and is
recorded as such.
- C# reference: the same operation on `SingleFileBackingStore` (`CultCache.cs:3268ff`), with the same names
across runtimes.
- **Verification (Rust and C#):**
- a mismatch preserves the foreign bytes exactly and writes the replacement;
- an undecodable current is preserved and replaced;
- an I/O read failure (a directory at the path) changes nothing and leaves no staging file;
- a staging failure (size cap) changes nothing and sets nothing aside;
- the path is readable at every instant: a reader thread loops `pull_all_read_only_snapshot` through 1,000
exchanges and never sees an absent file or an empty snapshot;
- `.previous` rotates only on a verified exchange;
- two set-asides in the same second get distinct names;
- mutation tools: `cargo mutants --in-diff`, and Stryker.NET on the C# diff.
- **R3b (parity, not blocking Odin):** cultcache-ts `single-file-messagepack-backing-store.ts:34` and cultcache-py
`stores.py:83` gain the same operation and option, with a shared fixture test.
- **Estimate:** Rust about +140, C# about +120, R3b about +200.
### O2. A foreign store file while serving is set aside, not fatal (Odin)
- **Repo/branch:** Odin `hands/odin-store-repair` from O1, with a CultLib pin bump to R3. Needs Q-R1 and Q-R4.
- **Deletes first:** `ForeignStoreWrite` (`lib.rs:630-641`) and its arm in `survive` (`main.rs:884`). The test
`a_flush_writes_only_changes_and_never_over_a_foreign_file` (`lib.rs:2055-2090`) and the `survive` tests at
`main.rs:2842-2844` and `:3110` pin the old rule: restate them, do not delete them.
- **Changes:**
- `MemoryOdinTopologyStore::flush` (`lib.rs:613-626`) calls `replace_snapshot_setting_aside(flushed, current)`.
On `SetAside` it records a `displaced` event and counts it toward the contested bound. On an I/O failure it
records `unpersisted` and returns the error, which `survive` logs and retries as today.
- `RuntimeState::flush` (`main.rs:307-314`) keeps its lease check immediately before the write.
- The Q3 B per-put path uses this same flush. Hands re-anchors on the Q3 B head.
- **Adds:**
- The `odin.store_condition` record (schema in `odin-core` `documents.rs`), held in W. It carries:
- `generation` and `written_by`, updated inside the flush before the snapshot is taken;
- `unpersisted_since` and `last_write_error`;
- `contested_since`;
- a bounded list of the last 16 recovery events: kind, time, displaced path, sha256 and bytes, source, records
loaded, and the outstanding list (bounded).
- One arm in `public_document` (`main.rs:590-608`) that serves exactly this type. Providers still cannot write
it: `is_peer_document_type` is unchanged.
- **Rules that must die under their own mutation:**
- a flush never leaves the store dirty (the condition record is written inside the flush, not after it);
- a set-aside is preceded by a lease check;
- the third set-aside inside the window stops writing, and the second does not;
- after `contested`, no file is written or created;
- on an I/O failure, nothing is set aside;
- the condition is served while writes fail.
- **Authority map:** as in section 4. The deletion line is the `ForeignStoreWrite` type and arm.
- **Verification (fault injection, each at the file layer, not a log line):**
- T-A: activate, serve, write a different valid snapshot behind Odin. After the next pass the file decodes to
W, the displaced file's bytes equal the foreign bytes, `serving_pass` returned `Ok`, the catalog is unchanged,
and the served condition names the displaced file and its sha256.
- T-A2 (P2b): flip one payload byte behind Odin; the same outcome, reason `mismatch`.
- T-B: truncate the file behind Odin; the same outcome, reason `undecodable`, and
`pull_all_read_only_snapshot` (Idunn's crossing) decodes the file after one pass.
- T-B-latency: with no puts, the set-aside happens within one heartbeat plus one flush interval.
- T-contested: three foreign writes inside the window make exactly two set-asides and a third `contested`
report. After that the file and directory are untouched by further passes, and Odin still serves.
- T-D: replace the store path with a directory (EISDIR, which works as root). Nothing is set aside, no staging
file remains, the condition is `unpersisted` and is served, and a put is not ACKed. Remove the directory, and
the next pass writes W, clears `unpersisted` and ACKs.
- T-lease: the lease withdrawn before a flush that would set aside means no link, no write, and Odin ends
(`WriteLeaseLost`).
- Negative: `rg -n "ForeignStoreWrite" crates` matches nothing; `rg -n "remove_file|remove_dir" crates/odin-daemon/src`
matches nothing new outside tests.
- `cargo mutants --in-diff`.
- **Estimate:** about -40 / +160.
### O3. Activation recovers a corrupt store (Odin)
- **Repo/branch:** Odin `hands/odin-store-activation` from O2. Needs Q-R2 and Q-R3.
- **Change:** `MemoryOdinTopologyStore::load` (`lib.rs:519-538`) becomes the recovery ladder:
| `load` result | Outcome |
|---|---|
| `Ok` | as today |
| absent | empty, as today |
| `Unreadable::Undecodable` | try `pull_previous_generation()`, then empty (Q-R3); `flushed` becomes a sentinel meaning "the file is not a snapshot Odin can compare", so the first flush's exchange sets it aside |
| `Unreadable::UnsupportedFormat` | return the error: refuse |
| `io::Error` | return the error: refuse |
Activation itself never writes. The first `serving_pass` flush (`main.rs:833-842`, due at once) writes W. That
set-aside is recorded under the activation event and is not counted toward the contested bound.
- **Adds:** the outstanding-republish list. It holds the peer-type records present in the recovered previous
generation, each cleared when its publisher re-puts it. After 10 minutes the remainder is marked "not
republished", which is the live answer to "who is publish-once". For `started-empty` the list is `unknown`.
- **Rules that must die under their own mutation:**
- Unsupported-format and I/O failures never set aside, never fall back, and never start.
- A recovered activation writes before it serves its first catalog snapshot, or it serves the recovered set and
marks the condition before the first write. Hands picks one and states it.
- **Verification:**
- T-C1: a truncated `topology.cc` and a valid `.previous` activate with the previous content. The first pass
writes it and sets the corrupt file aside byte-exact, and the condition is `recovered-previous` with its
outstanding list. Re-putting one listed document clears it.
- T-C2: both undecodable (Q-R3 A) start empty. Both files are preserved, the condition is `started-empty`, and
the next correlation sequence is above the floor.
- T-C3: a v4 header refuses to activate. The file is byte-identical afterwards, and there is no displaced file
and no `.previous` change.
- T-C4: a directory at the store path refuses, as C3.
- T-restart: a recovered Odin stopped and restarted loads its own first write normally, with no second
set-aside.
- `cargo mutants --in-diff`.
- **Estimate:** about -10 / +110.
### R2. Single-file decoders reject trailing bytes, in every runtime (CultLib)
- **Repo/branch:** CultLib `hands/cultcache-trailing-bytes`. Independent of Odin; land it before O3 ships so that
an appended file is caught as undecodable, not overwritten silently (P2).
- **Change:** C# `DeserializeSnapshot` (`CultDocumentMessagePackSerialization.cs:179-215`) checks `reader.End`.
Rust `decode_store_snapshot` (`lib.rs:2583ff`) and the legacy path check that the slice is consumed. TS and
Python do the same. **One release**: a runtime that accepts what another rejects is a parity split.
- **Verification:** one shared fixture (a valid store plus one byte) is rejected as undecodable by all four
runtimes, and the valid store is accepted by all four.
- **Estimate:** about +40 plus tests across four runtimes.
### X1. Runbook and recovery drill (gamecult-ops, then live)
- **Repo:** gamecult-ops `runbooks/odin-yggdrasil.md` gains a "Store recovery" section:
- how to read `odin.store_condition` from the catalog;
- what displaced and previous files mean, and that disposing of them is the operator's decision;
- the manual restore: engage Odin's lifecycle brake, stop the `idunn-odin-*` unit, set the live file aside with
`ln` (never `rm`), extract the snapshot's `topology.cc` with its owner, mode and `.lock`, release the brake,
and read the condition. The sequence floor makes an old watermark safe.
- **Drill D-1 (off-live, after O3; needs the Q2-style approval, Q-R6):** extract the newest Yggdrasil snapshot's
`topology.cc` into a container on Yggdrasil. Activate a test-harness Odin on it through the existing fixture
authority. Then truncate the copy and activate again with the extracted file as `.previous`. Report the records
per type, the outstanding list and the sequence floor. Delete the copy afterwards.
- **Drill D-2 (live, operator-scheduled, Q-R6), after O3 deploys:**
1. Record the baseline: catalog types, Idunn correlated targets, route timeouts.
2. Engage the lifecycle brake and stop Odin.
3. `ln topology.cc /var/tmp/odin-drill-keep.cc` (the evidence of the evidence), then truncate `topology.cc` in
place to half.
4. Release the brake and let Idunn restart Odin.
5. Pass when all of these hold:
- Odin is serving within one restart;
- the condition is `recovered-previous`, with a displaced file whose sha256 equals the truncated file's;
- every Odin-correlated target is Ready again within 60 s;
- there are 0 route-challenge timeouts in the next hour;
- the catalog's type set equals the baseline after 10 minutes, or the outstanding list names every
difference.
6. Then run D-2b, a foreign write while serving: `ln` the live file aside, then truncate it. It passes when the
condition shows `displaced` within about 6 s and Idunn's correlation reads never fail for longer than that
(the journal).
## 7. Subtraction ledger (estimate)
| Cut | Removed | Added | Formats and targets |
|---|---|---|---|
| R1 | about 30 | about 70 | none |
| O1 | about 5 | about 25 | none |
| R3 | 0 | about 260 (Rust 140, C# 120); R3b about 200 | two sibling-file conventions (`.displaced.*`, `.previous`); no format change |
| O2 | about 40 | about 160 | one Odin schema, `odin.store_condition.v1` |
| O3 | about 10 | about 110 | none |
| R2 | 0 | about 40 plus tests | none (it tightens the decoders) |
The campaign is net additive, at about +570 without R3b. It buys ends 1-5, which the operator asked for, and
removes one fatal path and one silent path. Every addition in CultLib is a capability a general consumer of the
single-file store would expect, and two of them (staging cleanup, and the header that survives truncation) are
defects.
## 8. Operator questions
- **Q-R1. A store file changed underneath a running Odin (decodable or not).**
- Context: Idunn fences the old Odin before granting the lease, so the other writer is never an Odin. It is a
tool, a hand edit, a restore made against the runbook, or disk corruption; P2b shows a flipped byte looks
exactly like a foreign write. Today a decodable change ends Odin, and the restart adopts the foreign file
silently. An undecodable change stops persistence forever and breaks Idunn's crossing.
- A: set the file aside (kept byte for byte, never deleted), write Odin's working set over it, keep serving,
and report it as typed state.
- B: set it aside, write the working set, then end Odin so a fresh process starts from the rewritten file.
- **Recommended: A.** B's restart protects nothing that the lease check before the write does not already
protect, and costs a route outage.
- **Q-R2. Keep one previous generation on disk (`topology.cc.previous`)?**
- Context: without it, a store that is corrupt at activation has no on-host source except "empty", and the
backup is up to 24 h old and needs the operator. It costs one hard link per write (a directory update, no data
written) and one more file of the store's size. It is a CultLib option, off by default for other consumers.
- A: yes. B: no; activation recovery goes straight to empty.
- **Recommended: A.** It is the only source that recovers publish-once documents, presence history and
correlations without the operator. Its loss bound is one write.
- **Q-R3. Activation finds nothing decodable: no usable `topology.cc`, no usable previous generation.**
- Context: presences and most catalog documents return within seconds to a minute, and correlations re-derive
in one refresh pass. Publish-once documents (if any exist, see 1.3) stay missing until their publisher
re-puts them, or until the operator restores from the backup.
- A: start empty automatically, set the corrupt file aside, and report `started-empty`.
- B: refuse to start, so Odin crash-loops until the operator restores. Every service's rendezvous is down
meanwhile.
- **Recommended: A.** A down Odin loses everything the Verse needs; an empty one loses only what nobody
republishes, and the restore path stays open.
- **Q-R4. A foreign writer keeps writing (a third set-aside within 10 minutes).**
- Context: repeated repair against a live writer makes a displaced file every second.
- A: stop writing, keep serving from memory, withhold put ACKs, and report `contested` until the operator
finds the writer. The working set survives.
- B: end Odin. Idunn restarts it, and the restart loads whatever the foreign writer last wrote.
- **Recommended: A.** B hands the store to the unknown writer. A is loud and loses nothing that is in memory.
- **Q-R5. Should a store condition change Odin's presence to `degraded`?**
- Context: a non-`active` Odin presence makes every managed dependent of Odin not Ready (`lib.rs:946`,
`dependency_evidence`). The store condition does not affect serving.
- A: no. The presence stays `active`, and the condition record is the signal.
- B: `degraded` while `unpersisted` or `contested`.
- **Recommended: A.** Otherwise a full disk becomes an outage of every dependent, and a restart that Idunn might
take in response would destroy the only complete copy.
- **Q-R6. The drills.**
- D-1: decode a copy of last night's backup `topology.cc` in a container on Yggdrasil, the same terms as Q2 for
the write map, with the copy deleted afterwards.
- D-2: after O3 deploys, a scheduled live drill that truncates the real `topology.cc` behind the lifecycle brake,
keeping a hard link of the original.
- **Recommended: yes to both**, D-1 before O3 ships and D-2 in a window the operator names.
## 9. Follow-ups outside this map
- **I-F1 (Idunn):** a deploy or a stop of Odin while `odin.store_condition` says `unpersisted` or `contested`
destroys the only complete copy. Idunn could read the condition from the catalog and hold the deployment
(deployment brake, never continuity). This is Idunn's decision, raised with the Idunn map.
- **I-F2 (Idunn):** "Idunn stopped restarting admitted odin" is a log line (`report_once`, `control_plane.rs:4599`),
not typed state. It belongs to Idunn's own operator surface.
- **F3 (from the write map):** the Eve lowering of `odin.store_condition` belongs to Odin's operator surface.
This map publishes the typed record only.
- **H-F1 (Heimdall):** confirm whether the deployed Heimdall re-puts its Odin documents every 60 s. O3's
outstanding list measures it after any recovery.
- **Q3 B's record in the write map:** "an ACK means durable" holds against crashes, not against a corruption at
activation, which loses at most the last write (C1).

1008
docs/write-pattern-cut.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,10 +7,7 @@
"scripts": {
"gjallar:persona": "node scripts/write-gjallar-persona-cc.mjs inspect",
"gjallar:persona:write": "node scripts/write-gjallar-persona-cc.mjs write",
"rust:test": "cargo test --workspace",
"check": "node scripts/check-syntax.cjs",
"test": "node --test test/*.test.cjs",
"start": "node src/odin-coordinator.cjs"
"rust:test": "cargo test --workspace"
},
"engines": {
"node": ">=20"

View file

@ -1,34 +0,0 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const repoRoot = path.resolve(__dirname, "..");
const files = [];
collect(path.join(repoRoot, "src"));
for (const file of files) {
const result = spawnSync(process.execPath, ["--check", file], {
cwd: repoRoot,
encoding: "utf8",
});
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.status !== 0) process.exit(result.status);
}
console.log(`Checked ${files.length} CommonJS files.`);
function collect(directory) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
collect(fullPath);
} else if (entry.isFile() && entry.name.endsWith(".cjs")) {
files.push(fullPath);
}
}
}

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo This deployment wrapper is an Idunn actuator. Configure Idunn; do not deploy manually.
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo This deployment wrapper is an Idunn actuator. Configure Idunn; do not deploy manually.
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0deploy-yggdrasil-heimdall.ps1

View file

@ -1,156 +0,0 @@
param(
[string] $SshTarget = "yggdrasil",
[string] $RepoRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Heimdall"),
[string] $CultLibRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "CultLib"),
[string] $AppUser = "heimdall",
[string] $RemoteAppHome = "/srv/heimdall",
[string] $RemoteTarballName = "heimdall-source.tar",
[string] $RemoteCultLibTarballName = "heimdall-cultlib.tar",
[string] $DeployScript = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "gamecult-ops\scripts\deploy-heimdall.sh"),
[string] $CheckScript = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "gamecult-ops\scripts\check-heimdall.sh"),
[string] $UpstreamRemote = "origin",
[string] $UpstreamBranch = "main",
[string] $CultLibUpstreamRemote = "origin",
[string] $CultLibUpstreamBranch = "main"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "This deployment script is an Idunn actuator. Agents must configure Idunn release targets and let Idunn run deployment; do not invoke deploy scripts manually."
}
foreach ($path in @($RepoRoot, $CultLibRoot, $DeployScript, $CheckScript)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "Required path not found: $path"
}
}
$sourceRef = "$UpstreamRemote/$UpstreamBranch"
$cultLibSourceRef = "$CultLibUpstreamRemote/$CultLibUpstreamBranch"
git -C $RepoRoot fetch --prune $UpstreamRemote $UpstreamBranch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for $RepoRoot $sourceRef" }
git -C $CultLibRoot fetch --prune $CultLibUpstreamRemote $CultLibUpstreamBranch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for $CultLibRoot $cultLibSourceRef" }
$heimdallCommit = (git -C $RepoRoot rev-parse $sourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($heimdallCommit)) {
throw "Could not determine Heimdall git revision."
}
$cultLibCommit = (git -C $CultLibRoot rev-parse $cultLibSourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($cultLibCommit)) {
throw "Could not determine CultLib git revision."
}
$scratch = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\idunn-deploy") "yggdrasil-heimdall"
$sourceTarPath = Join-Path $scratch $RemoteTarballName
$cultLibTarPath = Join-Path $scratch $RemoteCultLibTarballName
$manifestPath = Join-Path $scratch "deployment-manifest.txt"
New-Item -ItemType Directory -Force -Path $scratch | Out-Null
git -C $RepoRoot archive --format=tar --output=$sourceTarPath $sourceRef
if ($LASTEXITCODE -ne 0) {
throw "git archive failed for $RepoRoot $sourceRef"
}
if (Test-Path -LiteralPath $cultLibTarPath) {
Remove-Item -LiteralPath $cultLibTarPath -Force
}
git -C $CultLibRoot archive --format=tar --output=$cultLibTarPath $cultLibSourceRef packages/cultnet-ts packages/cultcache-ts
if ($LASTEXITCODE -ne 0) {
throw "CultLib tar build failed for $CultLibRoot $cultLibSourceRef"
}
$sourceHash = (Get-FileHash $sourceTarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$cultLibHash = (Get-FileHash $cultLibTarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$deployedAt = [DateTimeOffset]::UtcNow.ToString("O")
@(
"schema=gamecult.idunn.deployment_manifest.v1"
"appId=yggdrasil-heimdall"
"repoRoot=$RepoRoot"
"upstreamRemote=$UpstreamRemote"
"upstreamBranch=$UpstreamBranch"
"sourceRef=$sourceRef"
"gitCommit=$heimdallCommit"
"artifact=$RemoteTarballName"
"sha256=$sourceHash"
"cultLibRepoRoot=$CultLibRoot"
"cultLibUpstreamRemote=$CultLibUpstreamRemote"
"cultLibUpstreamBranch=$CultLibUpstreamBranch"
"cultLibSourceRef=$cultLibSourceRef"
"cultLibGitCommit=$cultLibCommit"
"cultLibArtifact=$RemoteCultLibTarballName"
"cultLibSha256=$cultLibHash"
"deployedAtUtc=$deployedAt"
) | Set-Content -Encoding ASCII -LiteralPath $manifestPath
$remoteDeploy = "/home/gamecultadmin/$([IO.Path]::GetFileName($DeployScript))"
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
$remoteSourceTarball = "/home/gamecultadmin/$RemoteTarballName"
$remoteCultLibTarball = "/home/gamecultadmin/$RemoteCultLibTarballName"
$remoteManifest = "/home/gamecultadmin/yggdrasil-heimdall-deployment-manifest.txt"
$remoteRunner = "/home/gamecultadmin/run-yggdrasil-heimdall-deploy.sh"
$targetSourceTarball = "$RemoteAppHome/$RemoteTarballName"
$targetCultLibTarball = "$RemoteAppHome/$RemoteCultLibTarballName"
$targetManifest = "$RemoteAppHome/deployment-manifest.txt"
$sftpBatchPath = Join-Path $scratch "yggdrasil-heimdall-upload.sftp"
$runnerPath = Join-Path $scratch "run-yggdrasil-heimdall-deploy.sh"
$runnerScript = @"
#!/usr/bin/env bash
set -euo pipefail
sudo -n install -d -o '$AppUser' -g '$AppUser' '$RemoteAppHome'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteSourceTarball' '$targetSourceTarball'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteCultLibTarball' '$targetCultLibTarball'
sudo -n install -m 644 '$remoteManifest' '$targetManifest'
chmod +x '$remoteDeploy' '$remoteCheck'
sudo -n env GAMECULT_IDUNN_REMOTE_ACTUATOR=1 IDUNN_COMMAND_AUTHORITY=idunn-daemon bash '$remoteDeploy'
check_ok=0
for i in `$(seq 1 30); do
if sudo -n bash '$remoteCheck'; then
check_ok=1
break
fi
sleep 2
done
if [ "`$check_ok" -ne 1 ]; then
sudo -n bash '$remoteCheck'
exit 1
fi
"@.Replace("`r`n", "`n")
[System.IO.File]::WriteAllText($runnerPath, $runnerScript, [System.Text.Encoding]::ASCII)
@(
"put ""$sourceTarPath"" ""$remoteSourceTarball"""
"put ""$cultLibTarPath"" ""$remoteCultLibTarball"""
"put ""$DeployScript"" ""$remoteDeploy"""
"put ""$CheckScript"" ""$remoteCheck"""
"put ""$manifestPath"" ""$remoteManifest"""
"put ""$runnerPath"" ""$remoteRunner"""
) | Set-Content -Encoding ASCII -LiteralPath $sftpBatchPath
try {
& sftp.exe -b $sftpBatchPath $SshTarget
if ($LASTEXITCODE -ne 0) {
throw "sftp upload failed for yggdrasil-heimdall"
}
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "chmod +x '$remoteRunner' && bash '$remoteRunner'"
$remoteExit = $LASTEXITCODE
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "rm -f '$remoteRunner'"
if ($remoteExit -ne 0) {
throw "remote deploy/check failed for yggdrasil-heimdall"
}
}
finally {
Remove-Item -LiteralPath $runnerPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $sftpBatchPath -Force -ErrorAction SilentlyContinue
}
Write-Host "yggdrasil-heimdall deployed with CultLib snapshot."
Write-Host " heimdallCommit=$heimdallCommit"
Write-Host " cultLibCommit=$cultLibCommit"
Write-Host " artifactSha256=$sourceHash"
Write-Host " cultLibSha256=$cultLibHash"

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo This deployment wrapper is an Idunn actuator. Configure Idunn; do not deploy manually.
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo This deployment wrapper is an Idunn actuator. Configure Idunn; do not deploy manually.
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0deploy-yggdrasil-repixelizer.ps1

View file

@ -1,161 +0,0 @@
param(
[string] $SshTarget = "yggdrasil",
[string] $RepoRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "repixelizer"),
[string] $CultLibRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "CultLib"),
[string] $AppUser = "repixelizer",
[string] $RemoteAppHome = "/srv/repixelizer",
[string] $RemoteTarballName = "repixelizer-source.tar",
[string] $RemoteCultLibTarballName = "repixelizer-cultlib.tar",
[string] $DeployScript = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "gamecult-ops\scripts\deploy-repixelizer-gui.sh"),
[string] $CheckScript = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "gamecult-ops\scripts\check-repixelizer-gui.sh"),
[string] $UpstreamRemote = "origin",
[string] $UpstreamBranch = "main",
[string] $CultLibUpstreamRemote = "origin",
[string] $CultLibUpstreamBranch = "main"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "This deployment script is an Idunn actuator. Agents must configure Idunn release targets and let Idunn run deployment; do not invoke deploy scripts manually."
}
foreach ($path in @($RepoRoot, $CultLibRoot, $DeployScript, $CheckScript)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "Required path not found: $path"
}
}
$sourceRef = "$UpstreamRemote/$UpstreamBranch"
$cultLibSourceRef = "$CultLibUpstreamRemote/$CultLibUpstreamBranch"
git -C $RepoRoot fetch --prune $UpstreamRemote $UpstreamBranch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for $RepoRoot $sourceRef" }
git -C $CultLibRoot fetch --prune $CultLibUpstreamRemote $CultLibUpstreamBranch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for $CultLibRoot $cultLibSourceRef" }
$repixelizerCommit = (git -C $RepoRoot rev-parse $sourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($repixelizerCommit)) {
throw "Could not determine Repixelizer git revision."
}
$cultLibCommit = (git -C $CultLibRoot rev-parse $cultLibSourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($cultLibCommit)) {
throw "Could not determine CultLib git revision."
}
$scratch = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\idunn-deploy") "yggdrasil-repixelizer"
$sourceTarPath = Join-Path $scratch $RemoteTarballName
$cultLibTarPath = Join-Path $scratch $RemoteCultLibTarballName
$manifestPath = Join-Path $scratch "deployment-manifest.txt"
New-Item -ItemType Directory -Force -Path $scratch | Out-Null
git -C $RepoRoot archive --format=tar --output=$sourceTarPath $sourceRef
if ($LASTEXITCODE -ne 0) {
throw "git archive failed for $RepoRoot $sourceRef"
}
if (Test-Path -LiteralPath $cultLibTarPath) {
Remove-Item -LiteralPath $cultLibTarPath -Force
}
# All three Python packages. CultLib split packages/cultcache-py into
# cultcache-py / cultnet-py / cultmesh-py; module names did not change, so
# archiving only the first still succeeds and ships an incomplete library
# that fails later as an ImportError, far from this line.
git -C $CultLibRoot archive --format=tar --output=$cultLibTarPath $cultLibSourceRef packages/cultcache-py packages/cultnet-py packages/cultmesh-py
if ($LASTEXITCODE -ne 0) {
throw "CultLib tar build failed for $CultLibRoot $cultLibSourceRef"
}
$sourceHash = (Get-FileHash $sourceTarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$cultLibHash = (Get-FileHash $cultLibTarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$deployedAt = [DateTimeOffset]::UtcNow.ToString("O")
@(
"schema=gamecult.idunn.deployment_manifest.v1"
"appId=yggdrasil-repixelizer"
"repoRoot=$RepoRoot"
"upstreamRemote=$UpstreamRemote"
"upstreamBranch=$UpstreamBranch"
"sourceRef=$sourceRef"
"gitCommit=$repixelizerCommit"
"artifact=$RemoteTarballName"
"sha256=$sourceHash"
"cultLibRepoRoot=$CultLibRoot"
"cultLibUpstreamRemote=$CultLibUpstreamRemote"
"cultLibUpstreamBranch=$CultLibUpstreamBranch"
"cultLibSourceRef=$cultLibSourceRef"
"cultLibGitCommit=$cultLibCommit"
"cultLibArtifact=$RemoteCultLibTarballName"
"cultLibSha256=$cultLibHash"
"deployedAtUtc=$deployedAt"
) | Set-Content -Encoding ASCII -LiteralPath $manifestPath
$remoteDeploy = "/home/gamecultadmin/$([IO.Path]::GetFileName($DeployScript))"
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
$remoteSourceTarball = "/home/gamecultadmin/$RemoteTarballName"
$remoteCultLibTarball = "/home/gamecultadmin/$RemoteCultLibTarballName"
$remoteManifest = "/home/gamecultadmin/yggdrasil-repixelizer-deployment-manifest.txt"
$remoteRunner = "/home/gamecultadmin/run-yggdrasil-repixelizer-deploy.sh"
$targetSourceTarball = "$RemoteAppHome/$RemoteTarballName"
$targetCultLibTarball = "$RemoteAppHome/$RemoteCultLibTarballName"
$targetManifest = "$RemoteAppHome/deployment-manifest.txt"
$sftpBatchPath = Join-Path $scratch "yggdrasil-repixelizer-upload.sftp"
$runnerPath = Join-Path $scratch "run-yggdrasil-repixelizer-deploy.sh"
$runnerScript = @"
#!/usr/bin/env bash
set -euo pipefail
sudo -n install -d -o '$AppUser' -g '$AppUser' '$RemoteAppHome'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteSourceTarball' '$targetSourceTarball'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteCultLibTarball' '$targetCultLibTarball'
sudo -n install -m 644 '$remoteManifest' '$targetManifest'
chmod +x '$remoteDeploy' '$remoteCheck'
sudo -n env GAMECULT_IDUNN_REMOTE_ACTUATOR=1 IDUNN_COMMAND_AUTHORITY=idunn-daemon bash '$remoteDeploy'
check_ok=0
for i in `$(seq 1 30); do
if sudo -n bash '$remoteCheck'; then
check_ok=1
break
fi
sleep 2
done
if [ "`$check_ok" -ne 1 ]; then
sudo -n bash '$remoteCheck'
exit 1
fi
"@.Replace("`r`n", "`n")
[System.IO.File]::WriteAllText($runnerPath, $runnerScript, [System.Text.Encoding]::ASCII)
@(
"put ""$sourceTarPath"" ""$remoteSourceTarball"""
"put ""$cultLibTarPath"" ""$remoteCultLibTarball"""
"put ""$DeployScript"" ""$remoteDeploy"""
"put ""$CheckScript"" ""$remoteCheck"""
"put ""$manifestPath"" ""$remoteManifest"""
"put ""$runnerPath"" ""$remoteRunner"""
) | Set-Content -Encoding ASCII -LiteralPath $sftpBatchPath
try {
& sftp.exe -b $sftpBatchPath $SshTarget
if ($LASTEXITCODE -ne 0) {
throw "sftp upload failed for yggdrasil-repixelizer"
}
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "chmod +x '$remoteRunner' && bash '$remoteRunner'"
$remoteExit = $LASTEXITCODE
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "rm -f '$remoteRunner'"
if ($remoteExit -ne 0) {
throw "remote deploy/check failed for yggdrasil-repixelizer"
}
}
finally {
Remove-Item -LiteralPath $runnerPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $sftpBatchPath -Force -ErrorAction SilentlyContinue
}
Write-Host "yggdrasil-repixelizer deployed with CultLib snapshot."
Write-Host " repixelizerCommit=$repixelizerCommit"
Write-Host " cultLibCommit=$cultLibCommit"
Write-Host " artifactSha256=$sourceHash"
Write-Host " cultLibSha256=$cultLibHash"

View file

@ -1,137 +0,0 @@
param(
[Parameter(Mandatory = $true)] [string] $AppId,
[Parameter(Mandatory = $true)] [string] $RepoRoot,
[Parameter(Mandatory = $true)] [string] $AppUser,
[Parameter(Mandatory = $true)] [string] $RemoteAppHome,
[Parameter(Mandatory = $true)] [string] $RemoteTarballName,
[Parameter(Mandatory = $true)] [string] $DeployScript,
[Parameter(Mandatory = $true)] [string] $CheckScript,
[string] $SshTarget = "yggdrasil",
[string] $UpstreamRemote = "origin",
[string] $UpstreamBranch = "main",
[string] $StateMigrationScript = ""
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1") {
throw "This deployment script is an Idunn actuator. Agents must configure Idunn release targets and let Idunn run deployment; do not invoke deploy scripts manually."
}
if (-not (Test-Path -LiteralPath $RepoRoot)) {
throw "Repo root not found: $RepoRoot"
}
if (-not (Test-Path -LiteralPath $DeployScript)) {
throw "Deploy script not found: $DeployScript"
}
if (-not (Test-Path -LiteralPath $CheckScript)) {
throw "Check script not found: $CheckScript"
}
if (-not [string]::IsNullOrWhiteSpace($StateMigrationScript) -and -not (Test-Path -LiteralPath $StateMigrationScript)) {
throw "State migration script not found: $StateMigrationScript"
}
git -C $RepoRoot fetch --prune $UpstreamRemote $UpstreamBranch
if ($LASTEXITCODE -ne 0) {
throw "git fetch failed for $RepoRoot $UpstreamRemote/$UpstreamBranch"
}
$sourceRef = "$UpstreamRemote/$UpstreamBranch"
$commit = (git -C $RepoRoot rev-parse $sourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($commit)) {
throw "Could not determine git revision for $RepoRoot $sourceRef"
}
$scratch = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\idunn-deploy") $AppId
$tarPath = Join-Path $scratch $RemoteTarballName
$manifestPath = Join-Path $scratch "deployment-manifest.txt"
New-Item -ItemType Directory -Force -Path $scratch | Out-Null
git -C $RepoRoot archive --format=tar --output=$tarPath $sourceRef
if ($LASTEXITCODE -ne 0) {
throw "git archive failed for $RepoRoot $sourceRef"
}
$hash = (Get-FileHash $tarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$deployedAt = [DateTimeOffset]::UtcNow.ToString("O")
@(
"schema=gamecult.idunn.deployment_manifest.v1"
"appId=$AppId"
"repoRoot=$RepoRoot"
"upstreamRemote=$UpstreamRemote"
"upstreamBranch=$UpstreamBranch"
"sourceRef=$sourceRef"
"gitCommit=$commit"
"artifact=$RemoteTarballName"
"sha256=$hash"
"deployedAtUtc=$deployedAt"
) | Set-Content -Encoding ASCII -LiteralPath $manifestPath
$remoteDeploy = "/home/gamecultadmin/$([IO.Path]::GetFileName($DeployScript))"
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
$remoteMigration = if ([string]::IsNullOrWhiteSpace($StateMigrationScript)) { "" } else { "/home/gamecultadmin/$([IO.Path]::GetFileName($StateMigrationScript))" }
$remoteTarball = "/home/gamecultadmin/$RemoteTarballName"
$remoteManifest = "/home/gamecultadmin/$AppId-deployment-manifest.txt"
$remoteRunner = "/home/gamecultadmin/run-$AppId-deploy.sh"
$targetTarball = "$RemoteAppHome/$RemoteTarballName"
$targetManifest = "$RemoteAppHome/deployment-manifest.txt"
$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10", $SshTarget)
$scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10")
$runnerPath = Join-Path $scratch "run-$AppId-deploy.sh"
$runnerScript = @"
#!/usr/bin/env bash
set -euo pipefail
sudo -n install -d -o '$AppUser' -g '$AppUser' '$RemoteAppHome'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteTarball' '$targetTarball'
sudo -n install -m 644 '$remoteManifest' '$targetManifest'
chmod +x '$remoteDeploy' '$remoteCheck'
if [ -n '$remoteMigration' ]; then
chmod +x '$remoteMigration'
sudo -n bash '$remoteMigration'
fi
sudo -n bash '$remoteDeploy'
check_ok=0
for i in `$(seq 1 30); do
if sudo -n bash '$remoteCheck'; then
check_ok=1
break
fi
sleep 2
done
if [ "`$check_ok" -ne 1 ]; then
sudo -n bash '$remoteCheck'
exit 1
fi
"@.Replace("`r`n", "`n")
[System.IO.File]::WriteAllText($runnerPath, $runnerScript, [System.Text.Encoding]::ASCII)
scp.exe @scpArgs $tarPath "${SshTarget}:$remoteTarball"
if ($LASTEXITCODE -ne 0) { throw "scp source tarball failed for $AppId" }
scp.exe @scpArgs $DeployScript "${SshTarget}:$remoteDeploy"
if ($LASTEXITCODE -ne 0) { throw "scp deploy script failed for $AppId" }
scp.exe @scpArgs $CheckScript "${SshTarget}:$remoteCheck"
if ($LASTEXITCODE -ne 0) { throw "scp check script failed for $AppId" }
if (-not [string]::IsNullOrWhiteSpace($StateMigrationScript)) {
scp.exe @scpArgs $StateMigrationScript "${SshTarget}:$remoteMigration"
if ($LASTEXITCODE -ne 0) { throw "scp state migration script failed for $AppId" }
}
scp.exe @scpArgs $manifestPath "${SshTarget}:$remoteManifest"
if ($LASTEXITCODE -ne 0) { throw "scp manifest failed for $AppId" }
scp.exe @scpArgs $runnerPath "${SshTarget}:$remoteRunner"
if ($LASTEXITCODE -ne 0) { throw "scp runner script failed for $AppId" }
ssh.exe @sshArgs "chmod +x '$remoteRunner' && bash '$remoteRunner'"
$remoteExit = $LASTEXITCODE
ssh.exe @sshArgs "rm -f '$remoteRunner'"
if ($remoteExit -ne 0) {
throw "remote deploy/check failed for $AppId"
}
Remove-Item -LiteralPath $runnerPath -Force -ErrorAction SilentlyContinue
Write-Host "$AppId deployed to Yggdrasil from $sourceRef at $commit."
Write-Host " artifactSha256=$hash"

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo This deployment wrapper is an Idunn actuator. Configure Idunn; do not deploy manually.
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo This deployment wrapper is an Idunn actuator. Configure Idunn; do not deploy manually.
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0deploy-yggdrasil-streampixels.ps1

View file

@ -1,156 +0,0 @@
param(
[string] $SshTarget = "yggdrasil",
[string] $RepoRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "StreamPixels"),
[string] $CultLibRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "CultLib"),
[string] $AppUser = "streampixels",
[string] $RemoteAppHome = "/srv/streampixels",
[string] $RemoteTarballName = "streampixels-source.tar",
[string] $RemoteCultLibTarballName = "streampixels-cultlib.tar",
[string] $DeployScript = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "gamecult-ops\scripts\deploy-streampixels-preview.sh"),
[string] $CheckScript = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "gamecult-ops\scripts\check-streampixels-preview.sh"),
[string] $UpstreamRemote = "origin",
[string] $UpstreamBranch = "main",
[string] $CultLibUpstreamRemote = "origin",
[string] $CultLibUpstreamBranch = "main"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "This deployment script is an Idunn actuator. Agents must configure Idunn release targets and let Idunn run deployment; do not invoke deploy scripts manually."
}
foreach ($path in @($RepoRoot, $CultLibRoot, $DeployScript, $CheckScript)) {
if (-not (Test-Path -LiteralPath $path)) {
throw "Required path not found: $path"
}
}
$sourceRef = "$UpstreamRemote/$UpstreamBranch"
$cultLibSourceRef = "$CultLibUpstreamRemote/$CultLibUpstreamBranch"
git -C $RepoRoot fetch --prune $UpstreamRemote $UpstreamBranch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for $RepoRoot $sourceRef" }
git -C $CultLibRoot fetch --prune $CultLibUpstreamRemote $CultLibUpstreamBranch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for $CultLibRoot $cultLibSourceRef" }
$streamPixelsCommit = (git -C $RepoRoot rev-parse $sourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($streamPixelsCommit)) {
throw "Could not determine StreamPixels git revision."
}
$cultLibCommit = (git -C $CultLibRoot rev-parse $cultLibSourceRef).Trim()
if ([string]::IsNullOrWhiteSpace($cultLibCommit)) {
throw "Could not determine CultLib git revision."
}
$scratch = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\idunn-deploy") "yggdrasil-streampixels"
$sourceTarPath = Join-Path $scratch $RemoteTarballName
$cultLibTarPath = Join-Path $scratch $RemoteCultLibTarballName
$manifestPath = Join-Path $scratch "deployment-manifest.txt"
New-Item -ItemType Directory -Force -Path $scratch | Out-Null
git -C $RepoRoot archive --format=tar --output=$sourceTarPath $sourceRef
if ($LASTEXITCODE -ne 0) {
throw "git archive failed for $RepoRoot $sourceRef"
}
if (Test-Path -LiteralPath $cultLibTarPath) {
Remove-Item -LiteralPath $cultLibTarPath -Force
}
git -C $CultLibRoot archive --format=tar --output=$cultLibTarPath $cultLibSourceRef package.json package-lock.json scripts/run-typescript-workspaces.mjs contracts/cultnet packages/cultcache-ts packages/cultnet-ts packages/cultmesh-ts packages/cultmesh-browser
if ($LASTEXITCODE -ne 0) {
throw "CultLib tar build failed for $CultLibRoot $cultLibSourceRef"
}
$sourceHash = (Get-FileHash $sourceTarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$cultLibHash = (Get-FileHash $cultLibTarPath -Algorithm SHA256).Hash.ToLowerInvariant()
$deployedAt = [DateTimeOffset]::UtcNow.ToString("O")
@(
"schema=gamecult.idunn.deployment_manifest.v1"
"appId=yggdrasil-streampixels"
"repoRoot=$RepoRoot"
"upstreamRemote=$UpstreamRemote"
"upstreamBranch=$UpstreamBranch"
"sourceRef=$sourceRef"
"gitCommit=$streamPixelsCommit"
"artifact=$RemoteTarballName"
"sha256=$sourceHash"
"cultLibRepoRoot=$CultLibRoot"
"cultLibUpstreamRemote=$CultLibUpstreamRemote"
"cultLibUpstreamBranch=$CultLibUpstreamBranch"
"cultLibSourceRef=$cultLibSourceRef"
"cultLibGitCommit=$cultLibCommit"
"cultLibArtifact=$RemoteCultLibTarballName"
"cultLibSha256=$cultLibHash"
"deployedAtUtc=$deployedAt"
) | Set-Content -Encoding ASCII -LiteralPath $manifestPath
$remoteDeploy = "/home/gamecultadmin/$([IO.Path]::GetFileName($DeployScript))"
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
$remoteSourceTarball = "/home/gamecultadmin/$RemoteTarballName"
$remoteCultLibTarball = "/home/gamecultadmin/$RemoteCultLibTarballName"
$remoteManifest = "/home/gamecultadmin/yggdrasil-streampixels-deployment-manifest.txt"
$remoteRunner = "/home/gamecultadmin/run-yggdrasil-streampixels-deploy.sh"
$targetSourceTarball = "$RemoteAppHome/$RemoteTarballName"
$targetCultLibTarball = "$RemoteAppHome/$RemoteCultLibTarballName"
$targetManifest = "$RemoteAppHome/deployment-manifest.txt"
$sftpBatchPath = Join-Path $scratch "yggdrasil-streampixels-upload.sftp"
$runnerPath = Join-Path $scratch "run-yggdrasil-streampixels-deploy.sh"
$runnerScript = @"
#!/usr/bin/env bash
set -euo pipefail
sudo -n install -d -o '$AppUser' -g '$AppUser' '$RemoteAppHome'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteSourceTarball' '$targetSourceTarball'
sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteCultLibTarball' '$targetCultLibTarball'
sudo -n install -m 644 '$remoteManifest' '$targetManifest'
chmod +x '$remoteDeploy' '$remoteCheck'
sudo -n env GAMECULT_IDUNN_REMOTE_ACTUATOR=1 IDUNN_COMMAND_AUTHORITY=idunn-daemon bash '$remoteDeploy'
check_ok=0
for i in `$(seq 1 30); do
if sudo -n bash '$remoteCheck'; then
check_ok=1
break
fi
sleep 2
done
if [ "`$check_ok" -ne 1 ]; then
sudo -n bash '$remoteCheck'
exit 1
fi
"@.Replace("`r`n", "`n")
[System.IO.File]::WriteAllText($runnerPath, $runnerScript, [System.Text.Encoding]::ASCII)
@(
"put ""$sourceTarPath"" ""$remoteSourceTarball"""
"put ""$cultLibTarPath"" ""$remoteCultLibTarball"""
"put ""$DeployScript"" ""$remoteDeploy"""
"put ""$CheckScript"" ""$remoteCheck"""
"put ""$manifestPath"" ""$remoteManifest"""
"put ""$runnerPath"" ""$remoteRunner"""
) | Set-Content -Encoding ASCII -LiteralPath $sftpBatchPath
try {
& sftp.exe -b $sftpBatchPath $SshTarget
if ($LASTEXITCODE -ne 0) {
throw "sftp upload failed for yggdrasil-streampixels"
}
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "chmod +x '$remoteRunner' && bash '$remoteRunner'"
$remoteExit = $LASTEXITCODE
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "rm -f '$remoteRunner'"
if ($remoteExit -ne 0) {
throw "remote deploy/check failed for yggdrasil-streampixels"
}
}
finally {
Remove-Item -LiteralPath $runnerPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $sftpBatchPath -Force -ErrorAction SilentlyContinue
}
Write-Host "yggdrasil-streampixels deployed with CultLib snapshot."
Write-Host " streamPixelsCommit=$streamPixelsCommit"
Write-Host " cultLibCommit=$cultLibCommit"
Write-Host " artifactSha256=$sourceHash"
Write-Host " cultLibSha256=$cultLibHash"

View file

@ -1,3 +0,0 @@
@echo off
echo health-mimir-eve-dashboard.cmd archived: Mimir Eve dashboard health is daemon-published over CultNet/RUDP as mimir.cultnet-rudp-provider-health. Do not probe localhost HTTP for daemon health. 1>&2
exit /b 2

View file

@ -1,3 +0,0 @@
@echo off
echo health-nightwing-eve-browser-reference.cmd archived: Nightwing Eve browser reference health is daemon-published over CultNet/RUDP as nightwing.cultnet-rudp-browser-reference-health. Use health-nightwing-eve-browser-reference.ps1 only as an explicit witness inspection, not Idunn health truth.
exit /b 2

View file

@ -1,335 +0,0 @@
param(
[string] $SshTarget = "nightwing",
[string] $ExpectedDocumentRoot = "/opt/gamecult/eve-browser-reference",
[string] $RemoteCultCacheWitness = "/var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc",
[int] $MaxWitnessAgeSeconds = 120
)
$ErrorActionPreference = "Stop"
function Fail-IdunnHealth {
param(
[string] $State,
[string] $Message
)
Write-Output "idunn.health.state=$State"
throw $Message
}
function Invoke-SshText {
param(
[string] $Command,
[string] $FailureMessage,
[string] $FailureState = "failed"
)
$output = ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $SshTarget $Command
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($output)) {
Fail-IdunnHealth -State $FailureState -Message $FailureMessage
}
return $output
}
function Get-CultCacheInspection {
param(
[string] $WitnessCopyPath,
[string] $InspectorPackageJson
)
$nodeScript = @'
const fs = require("fs");
const { createRequire } = require("module");
const witnessCopyPath = process.argv[2];
const inspectorPackageJson = process.argv[3];
const requireCultCacheInspection = createRequire(inspectorPackageJson);
const { inspectCultCacheBytes } = requireCultCacheInspection("cultcache-ts");
const inspection = inspectCultCacheBytes(witnessCopyPath, fs.readFileSync(witnessCopyPath));
function firstRecord(schemaName) {
return inspection.records.find((record) => record.schemaName === schemaName) ?? null;
}
function previewArray(record) {
return Array.isArray(record?.payloadPreview) ? record.payloadPreview : [];
}
function normalizeRecord(schemaName) {
const record = firstRecord(schemaName);
if (!record) {
return null;
}
const preview = previewArray(record);
switch (schemaName) {
case "mimir.eve_browser_reference_manifest":
return {
schemaName: record.schemaName,
storedAt: record.storedAt,
daemonId: preview[0] ?? "",
siteTitle: preview[1] ?? "",
documentRoot: preview[2] ?? "",
entryPoint: preview[3] ?? "",
port: preview[4] ?? 0,
assetCount: Array.isArray(preview[5]) ? preview[5].length : 0,
surfaceTransport: preview[6] ?? "",
};
case "mimir.eve_browser_reference_static_surface":
return {
schemaName: record.schemaName,
storedAt: record.storedAt,
daemonId: preview[0] ?? "",
updatedAtUtc: preview[1] ?? "",
documentRoot: preview[2] ?? "",
entryPoint: preview[3] ?? "",
assetCount: Array.isArray(preview[4]) ? preview[4].length : 0,
fixtureCount: Array.isArray(preview[5]) ? preview[5].length : 0,
currentCutLine: preview[6] ?? "",
};
case "mimir.eve_browser_reference_command_boundary":
return {
schemaName: record.schemaName,
storedAt: record.storedAt,
daemonId: preview[0] ?? "",
updatedAtUtc: preview[1] ?? "",
mode: preview[2] ?? "",
writesAccepted: preview[3] === true,
operatorInputAuthority: preview[4] ?? "",
lifecycleAuthority: preview[5] ?? "",
acceptedCommands: Array.isArray(preview[6]) ? preview[6] : [],
rejectedCommands: Array.isArray(preview[7]) ? preview[7] : [],
};
case "mimir.eve_browser_reference_transport_profile":
return {
schemaName: record.schemaName,
storedAt: record.storedAt,
daemonId: preview[0] ?? "",
updatedAtUtc: preview[1] ?? "",
currentState: preview[2] ?? "",
inputTransport: preview[3] ?? "",
outputTransport: preview[4] ?? "",
healthContract: preview[5] ?? "",
idunnRudpHealth: preview[6] ?? "",
witnessPath: preview[7] ?? "",
currentCutLine: preview[8] ?? "",
};
case "idunn.daemon_health":
return {
schemaName: record.schemaName,
storedAt: record.storedAt,
daemonId: preview[0] ?? "",
state: preview[1] ?? "",
detail: preview[2] ?? "",
observedAt: preview[3] ?? "",
healthContract: preview[4] ?? "",
publicationSource: preview[5] ?? "",
transport: preview[6] ?? "",
};
default:
return {
schemaName: record.schemaName,
storedAt: record.storedAt,
};
}
}
console.log(JSON.stringify({
format: inspection.format,
catalogSchemaVersions: inspection.catalog.map((entry) => entry.schemaVersion),
manifest: normalizeRecord("mimir.eve_browser_reference_manifest"),
staticSurface: normalizeRecord("mimir.eve_browser_reference_static_surface"),
commandBoundary: normalizeRecord("mimir.eve_browser_reference_command_boundary"),
transportProfile: normalizeRecord("mimir.eve_browser_reference_transport_profile"),
daemonHealth: normalizeRecord("idunn.daemon_health"),
}));
'@
$inspectionJson = $nodeScript | node - $WitnessCopyPath $InspectorPackageJson
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($inspectionJson)) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness inspection failed."
}
try {
return $inspectionJson | ConvertFrom-Json
} catch {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache inspection returned malformed JSON."
}
}
ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $SshTarget "systemctl is-active --quiet nightwing-eve-browser-reference.service"
if ($LASTEXITCODE -ne 0) {
Fail-IdunnHealth -State "failed" -Message "nightwing-eve-browser-reference.service is not active on Nightwing."
}
$cultCacheStat = Invoke-SshText `
-Command "stat -c '%s %Y %U %G' '$RemoteCultCacheWitness' 2>/dev/null" `
-FailureMessage "Nightwing Eve browser reference CultCache witness is missing at $RemoteCultCacheWitness."
$cultCacheParts = $cultCacheStat.Trim() -split '\s+'
if ($cultCacheParts.Length -lt 4) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness stat output was malformed: $cultCacheStat"
}
$cultCacheSize = [int64]$cultCacheParts[0]
$cultCacheModifiedUtc = [DateTimeOffset]::FromUnixTimeSeconds([int64]$cultCacheParts[1])
$cultCacheOwner = $cultCacheParts[2]
$cultCacheGroup = $cultCacheParts[3]
$cultCacheAgeSeconds = ([DateTimeOffset]::UtcNow - $cultCacheModifiedUtc).TotalSeconds
if ($cultCacheSize -le 0) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness at $RemoteCultCacheWitness is empty."
}
if ($cultCacheAgeSeconds -gt $MaxWitnessAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness is stale ($([math]::Round($cultCacheAgeSeconds))s old)."
}
if ($cultCacheOwner -ne "metacrat" -or $cultCacheGroup -ne "metacrat") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference CultCache witness owner is ${cultCacheOwner}:$cultCacheGroup, expected metacrat:metacrat."
}
try {
$cultCacheInspectorPackageJson = (Resolve-Path (Join-Path $PSScriptRoot "..\..\CultLib\packages\cultcache-ts\package.json")).Path
} catch {
Fail-IdunnHealth -State "failed" -Message "CultCache inspector package is missing from the local Odin body."
}
$cultCacheWitnessCopy = Join-Path ([System.IO.Path]::GetTempPath()) ("nightwing-eve-browser-reference-" + [Guid]::NewGuid().ToString("N") + ".cc")
try {
$remoteCultCacheBase64 = Invoke-SshText `
-Command "base64 -w0 '$RemoteCultCacheWitness' 2>/dev/null" `
-FailureMessage "Nightwing Eve browser reference CultCache witness could not be copied from Nightwing."
[System.IO.File]::WriteAllBytes($cultCacheWitnessCopy, [Convert]::FromBase64String($remoteCultCacheBase64.Trim()))
$inspection = Get-CultCacheInspection -WitnessCopyPath $cultCacheWitnessCopy -InspectorPackageJson $cultCacheInspectorPackageJson
} catch [FormatException] {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness copy was not valid base64."
} finally {
if (Test-Path $cultCacheWitnessCopy) {
Remove-Item $cultCacheWitnessCopy -Force -ErrorAction SilentlyContinue
}
}
if ($inspection.format -ne "cultcache.store.v1") {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness has unexpected format '$($inspection.format)'."
}
$requiredCultCacheSchemas = @(
"mimir.eve_browser_reference_manifest.v1",
"mimir.eve_browser_reference_static_surface.v1",
"mimir.eve_browser_reference_command_boundary.v1",
"mimir.eve_browser_reference_transport_profile.v1",
"idunn.daemon_health.v1"
)
$publishedCultCacheSchemas = @($inspection.catalogSchemaVersions)
foreach ($schema in $requiredCultCacheSchemas) {
if ($publishedCultCacheSchemas -notcontains $schema) {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference CultCache witness is missing schema '$schema'."
}
}
if ($null -eq $inspection.manifest) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness is missing its manifest record."
}
if ($inspection.manifest.documentRoot -ne $ExpectedDocumentRoot) {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference manifest document root is '$($inspection.manifest.documentRoot)', expected '$ExpectedDocumentRoot'."
}
if ($inspection.manifest.entryPoint -ne "index.html") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference manifest entry point is '$($inspection.manifest.entryPoint)', expected 'index.html'."
}
if ($inspection.manifest.surfaceTransport -ne "static-http-lowering") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference manifest transport is '$($inspection.manifest.surfaceTransport)', expected 'static-http-lowering'."
}
if ($null -eq $inspection.staticSurface) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness is missing its static-surface record."
}
if ($inspection.staticSurface.documentRoot -ne $ExpectedDocumentRoot) {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference static-surface document root is '$($inspection.staticSurface.documentRoot)', expected '$ExpectedDocumentRoot'."
}
if ($null -eq $inspection.commandBoundary) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness is missing its command boundary record."
}
if ($inspection.commandBoundary.mode -ne "static-read-only-lowering") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference command boundary mode is '$($inspection.commandBoundary.mode)', expected 'static-read-only-lowering'."
}
if ($inspection.commandBoundary.writesAccepted) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference command boundary unexpectedly accepts writes."
}
if ($inspection.commandBoundary.lifecycleAuthority -ne "idunn-supervisor-command.restart") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference lifecycle authority is '$($inspection.commandBoundary.lifecycleAuthority)', expected 'idunn-supervisor-command.restart'."
}
if ($null -eq $inspection.transportProfile) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness is missing its transport profile record."
}
if ($inspection.transportProfile.currentState -ne "partial-rudp-health-and-provider-store-live") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference transport profile state is '$($inspection.transportProfile.currentState)', expected 'partial-rudp-health-and-provider-store-live'."
}
if ($inspection.transportProfile.inputTransport -ne "http-static-lowering") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference input transport is '$($inspection.transportProfile.inputTransport)', expected 'http-static-lowering'."
}
if ($inspection.transportProfile.outputTransport -ne "daemon-owned-cultcache-boundary-store + daemon-published-rudp-health") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference output transport is '$($inspection.transportProfile.outputTransport)', expected 'daemon-owned-cultcache-boundary-store + daemon-published-rudp-health'."
}
if ($inspection.transportProfile.healthContract -ne "nightwing.cultnet-rudp-browser-reference-health") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference health contract is '$($inspection.transportProfile.healthContract)', expected 'nightwing.cultnet-rudp-browser-reference-health'."
}
if ($inspection.transportProfile.idunnRudpHealth -ne "idunn.health-published-separately") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference transport profile Idunn health publication is '$($inspection.transportProfile.idunnRudpHealth)', expected 'idunn.health-published-separately'."
}
if ($inspection.transportProfile.witnessPath -ne $RemoteCultCacheWitness) {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference transport profile witness path is '$($inspection.transportProfile.witnessPath)', expected '$RemoteCultCacheWitness'."
}
if ($null -eq $inspection.daemonHealth) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference CultCache witness is missing its daemon-health record."
}
if ($inspection.daemonHealth.daemonId -ne "nightwing-eve-browser-reference") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference daemon-health record advertises daemon '$($inspection.daemonHealth.daemonId)', expected 'nightwing-eve-browser-reference'."
}
if ($inspection.daemonHealth.state -ne "healthy") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference daemon-health state is '$($inspection.daemonHealth.state)', expected 'healthy'."
}
if ($inspection.daemonHealth.healthContract -ne "nightwing.cultnet-rudp-browser-reference-health") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference daemon-health contract is '$($inspection.daemonHealth.healthContract)', expected 'nightwing.cultnet-rudp-browser-reference-health'."
}
if ($inspection.daemonHealth.publicationSource -ne "daemon-published") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference daemon-health publication source is '$($inspection.daemonHealth.publicationSource)', expected 'daemon-published'."
}
if ($inspection.daemonHealth.transport -ne "cultnet.transport.rudp.v0") {
Fail-IdunnHealth -State "degraded" -Message "Nightwing Eve browser reference daemon-health transport is '$($inspection.daemonHealth.transport)', expected 'cultnet.transport.rudp.v0'."
}
try {
$daemonHealthObservedAtUtc = [DateTimeOffset]::Parse($inspection.daemonHealth.observedAt)
} catch {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference daemon-health observedAt is malformed: '$($inspection.daemonHealth.observedAt)'."
}
$daemonHealthAgeSeconds = ([DateTimeOffset]::UtcNow - $daemonHealthObservedAtUtc.ToUniversalTime()).TotalSeconds
if ($daemonHealthAgeSeconds -gt $MaxWitnessAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "Nightwing Eve browser reference daemon-health record is stale ($([math]::Round($daemonHealthAgeSeconds))s old)."
}
Write-Host "Nightwing Eve browser reference is active, and its daemon-owned CultCache witness now certifies manifest, boundary, and daemon-health truth directly."

View file

@ -1,3 +0,0 @@
@echo off
echo health-nightwing-eve-dashboard.cmd archived: Nightwing Eve dashboard broker health is not daemon truth. Rebuild typed CultMesh/Odin dashboard state before restoring a health check. 1>&2
exit /b 2

View file

@ -1 +0,0 @@
throw "health-nightwing-eve-dashboard.ps1 archived: Nightwing Eve dashboard broker health is not daemon truth. Inspect typed CultMesh/Odin dashboard state after the publisher is rebuilt."

View file

@ -1,3 +0,0 @@
@echo off
echo health-odin.cmd archived: Odin health is daemon-published over CultNet/RUDP as odin.cultnet-rudp-provider-health. Use health-odin.ps1 only as an explicit daemon witness inspection, not Idunn health truth.
exit /b 2

View file

@ -1,68 +0,0 @@
param(
[string] $StateDir = (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\odin"),
[int] $MaxSilenceSeconds = 120,
[string] $CultNetRudpBind = "0.0.0.0:17871"
)
$ErrorActionPreference = "Stop"
$pidPath = Join-Path $StateDir "odin.pid"
$outLog = Join-Path $StateDir "odin.out.log"
$errLog = Join-Path $StateDir "odin.err.log"
$cachePath = Join-Path $StateDir "odin.ccmp"
if (-not (Test-Path -LiteralPath $pidPath)) {
throw "Odin PID file is missing at $pidPath"
}
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
if ($pidText -notmatch '^\d+$') {
throw "Odin PID file is invalid at ${pidPath}: '$pidText'"
}
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $pidText" -ErrorAction SilentlyContinue
if ($null -eq $process) {
throw "Odin process $pidText is not running"
}
foreach ($pattern in @(
'node.exe',
'src\odin-coordinator.cjs',
"--stateDir $StateDir",
"--cultnet-rudp-bind $CultNetRudpBind",
'--idunn-daemon odin',
'--idunn-health-contract odin.cultnet-rudp-provider-health'
)) {
if ($process.CommandLine -notlike "*$pattern*") {
throw "Odin process command line is missing expected segment ${pattern}: $($process.CommandLine)"
}
}
foreach ($logPath in @($outLog, $errLog)) {
if (-not (Test-Path -LiteralPath $logPath)) {
throw "Odin log is missing at $logPath"
}
}
$newestLogWriteUtc = @($outLog, $errLog) |
ForEach-Object { (Get-Item -LiteralPath $_).LastWriteTimeUtc } |
Sort-Object -Descending |
Select-Object -First 1
$logAgeSeconds = ([DateTime]::UtcNow - $newestLogWriteUtc).TotalSeconds
if ($logAgeSeconds -gt $MaxSilenceSeconds) {
throw "Odin logs have been silent for $([math]::Round($logAgeSeconds))s, exceeding $MaxSilenceSeconds seconds"
}
if (-not (Test-Path -LiteralPath $cachePath)) {
throw "Odin CultMesh store is missing at $cachePath"
}
$rudpPort = [int](($CultNetRudpBind -split ':')[-1])
$udpListener = Get-NetUDPEndpoint -LocalPort $rudpPort -ErrorAction SilentlyContinue |
Where-Object { $_.OwningProcess -eq [int]$pidText } |
Select-Object -First 1
if ($null -eq $udpListener) {
throw "Odin is not listening on UDP port $rudpPort as process $pidText"
}
Write-Host "Odin healthy: pid=$pidText cultmesh_rudp_udp=$rudpPort store=$cachePath log_age=$([math]::Round($logAgeSeconds))s"

View file

@ -1,3 +0,0 @@
@echo off
echo health-raven-capture-mux.cmd archived: Raven capture mux no longer owns Muninn daemon health; Muninn publishes muninn.cultnet-rudp-remote-telemetry-health over CultNet/RUDP.
exit /b 2

View file

@ -1,2 +0,0 @@
Write-Error "health-raven-capture-mux.ps1 archived: Raven capture mux no longer owns Muninn daemon health; Muninn publishes muninn.cultnet-rudp-remote-telemetry-health over CultNet/RUDP."
exit 2

View file

@ -1,3 +0,0 @@
@echo off
echo health-stonks.cmd archived: Stonks health is daemon-published over CultNet/RUDP as stonks.cultnet-rudp-market-health. Do not probe localhost HTTP for daemon health. 1>&2
exit /b 2

View file

@ -1,3 +0,0 @@
@echo off
echo health-vili.cmd archived: Vili health is daemon-published over CultNet/RUDP as vili.cultnet-rudp-animation-health. Do not probe HTTP for daemon health. 1>&2
exit /b 2

View file

@ -1,3 +0,0 @@
@echo off
echo health-voidbot.cmd archived: VoidBot health is daemon-published over CultNet/RUDP as voidbot.cultnet-rudp-stack-health. Use health-voidbot.ps1 only as an explicit witness inspection, not Idunn health truth. 1>&2
exit /b 2

View file

@ -1,181 +0,0 @@
param(
[string] $VoidBotRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "VoidBot"),
[int] $MaxOrchestratorAgeSeconds = 180,
[int] $MaxCultMeshStoreAgeSeconds = 180
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Fail-IdunnHealth {
param(
[string] $State,
[string] $Message
)
Write-Output "idunn.health.state=$State"
throw $Message
}
function Read-JsonFile {
param([Parameter(Mandatory = $true)][string] $Path)
if (-not (Test-Path -LiteralPath $Path)) {
return $null
}
$raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
if ([string]::IsNullOrWhiteSpace($raw)) {
return $null
}
return $raw | ConvertFrom-Json
}
function Get-JsonPropertyValue {
param(
[Parameter(Mandatory = $false)] $Object,
[Parameter(Mandatory = $true)][string] $Name,
[Parameter(Mandatory = $false)] $Default = $null
)
if ($null -eq $Object) {
return $Default
}
$property = $Object.PSObject.Properties[$Name]
if ($null -eq $property) {
return $Default
}
return $property.Value
}
function Get-ProcessOrFail {
param(
[Parameter(Mandatory = $true)][string] $Role,
[Parameter(Mandatory = $true)][int] $ProcessId
)
$process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
if ($null -eq $process) {
Fail-IdunnHealth -State "failed" -Message "VoidBot $Role process PID $ProcessId is not running."
}
return $process
}
function Assert-StoreContains {
param(
[Parameter(Mandatory = $true)][string] $StorePath,
[Parameter(Mandatory = $true)][string[]] $Tokens
)
$rg = (Get-Command rg.exe -ErrorAction SilentlyContinue).Source
if ([string]::IsNullOrWhiteSpace($rg)) {
Fail-IdunnHealth -State "degraded" -Message "ripgrep is required to inspect the VoidBot CultMesh witness store."
}
foreach ($token in $Tokens) {
& $rg -a -q --fixed-strings -- $token $StorePath
if ($LASTEXITCODE -ne 0) {
Fail-IdunnHealth -State "degraded" -Message "VoidBot CultMesh witness store is missing '$token'."
}
}
}
$runtimePath = Join-Path $VoidBotRoot ".voidbot\status\runtime-stack.json"
$orchestratorPath = Join-Path $VoidBotRoot ".voidbot\status\gamecult-orchestrator.json"
$cultMeshStorePath = Join-Path $VoidBotRoot ".voidbot\status\cultmesh\voidbot-swarm-state.cc"
$runtime = Read-JsonFile -Path $runtimePath
if ($null -eq $runtime) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime state is missing at $runtimePath."
}
if (-not [bool](Get-JsonPropertyValue -Object $runtime -Name "ready" -Default $false)) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime stack reports ready=false."
}
$runtimeStage = [string](Get-JsonPropertyValue -Object $runtime -Name "stage" -Default "unknown")
if ($runtimeStage -ne "ready") {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime stack stage is '$runtimeStage', expected 'ready'."
}
$bot = Get-JsonPropertyValue -Object $runtime -Name "bot"
$worker = Get-JsonPropertyValue -Object $runtime -Name "worker"
$botPid = if ($null -ne $bot) { [int](Get-JsonPropertyValue -Object $bot -Name "pid" -Default 0) } else { 0 }
$workerPid = if ($null -ne $worker) { [int](Get-JsonPropertyValue -Object $worker -Name "pid" -Default 0) } else { 0 }
if ($botPid -le 0) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime state does not contain a live bot PID."
}
if ($workerPid -le 0) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime state does not contain a live worker PID."
}
$botProcess = Get-ProcessOrFail -Role "bot" -ProcessId $botPid
$workerProcess = Get-ProcessOrFail -Role "worker" -ProcessId $workerPid
$orchestrator = Read-JsonFile -Path $orchestratorPath
if ($null -eq $orchestrator -or $null -eq $orchestrator.organs) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator state is missing at $orchestratorPath."
}
$surfaceState = $orchestrator.organs.PSObject.Properties["voidbot-swarm-surface"]
if ($null -eq $surfaceState) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator does not report the voidbot-swarm-surface organ."
}
$surfaceState = $surfaceState.Value
if ($surfaceState.lastStatus -ne "ok") {
Fail-IdunnHealth -State "degraded" -Message "VoidBot swarm surface organ lastStatus is '$($surfaceState.lastStatus)', expected 'ok'."
}
if ([string]::IsNullOrWhiteSpace($surfaceState.lastFinishedAt)) {
Fail-IdunnHealth -State "failed" -Message "VoidBot swarm surface organ has no lastFinishedAt timestamp."
}
$surfaceFinishedAt = [DateTimeOffset]::Parse($surfaceState.lastFinishedAt).ToUniversalTime()
$surfaceAgeSeconds = ([DateTimeOffset]::UtcNow - $surfaceFinishedAt).TotalSeconds
if ($surfaceAgeSeconds -gt $MaxOrchestratorAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "VoidBot swarm surface organ is stale ($([math]::Round($surfaceAgeSeconds))s old)."
}
$orchestratorFile = Get-Item -LiteralPath $orchestratorPath -ErrorAction SilentlyContinue
if ($null -eq $orchestratorFile) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator state file is missing at $orchestratorPath."
}
$orchestratorAgeSeconds = ([DateTimeOffset]::UtcNow - [DateTimeOffset]$orchestratorFile.LastWriteTimeUtc).TotalSeconds
if ($orchestratorAgeSeconds -gt $MaxOrchestratorAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator state file is stale ($([math]::Round($orchestratorAgeSeconds))s old)."
}
$cultMeshStore = Get-Item -LiteralPath $cultMeshStorePath -ErrorAction SilentlyContinue
if ($null -eq $cultMeshStore) {
Fail-IdunnHealth -State "failed" -Message "VoidBot CultMesh witness store is missing at $cultMeshStorePath."
}
if ($cultMeshStore.Length -le 0) {
Fail-IdunnHealth -State "failed" -Message "VoidBot CultMesh witness store at $cultMeshStorePath is empty."
}
$cultMeshStoreAgeSeconds = ([DateTimeOffset]::UtcNow - [DateTimeOffset]$cultMeshStore.LastWriteTimeUtc).TotalSeconds
if ($cultMeshStoreAgeSeconds -gt $MaxCultMeshStoreAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "VoidBot CultMesh witness store is stale ($([math]::Round($cultMeshStoreAgeSeconds))s old)."
}
Assert-StoreContains -StorePath $cultMeshStorePath -Tokens @(
"voidbot.provider_advertisement_catalog.v0",
"idunn.command_boundary.v1",
"idunn.daemon_transport_profile.v1",
"voidbot.providers",
"voidbot.discord",
"voidbot.reddit",
"voidbot.archive",
"voidbot.source",
"voidbot.repo_face",
"voidbot.swarm",
"command-boundary:voidbot",
"transport:voidbot"
)
Write-Host "VoidBot runtime is ready, bot PID $($botProcess.Id) and worker PID $($workerProcess.Id) are live, and the daemon-owned CultMesh witness store is fresh."

View file

@ -1,3 +0,0 @@
@echo off
echo health-weksa.cmd archived: Weksa health is daemon-published over CultNet/RUDP as weksa.cultnet-rudp-provider-health. Do not run local command health for daemon truth. 1>&2
exit /b 2

View file

@ -1,3 +0,0 @@
@echo off
echo health-yggdrasil-heimdall.cmd archived: Heimdall health is daemon-published over CultNet/RUDP as heimdall.cultnet-rudp-provider-health. Use health-yggdrasil-source-app.ps1 only as an explicit deployment witness, not Idunn health truth.
exit /b 2

View file

@ -1,3 +0,0 @@
@echo off
echo health-yggdrasil-repixelizer.cmd archived: Repixelizer health is daemon-published over CultNet/RUDP as repixelizer.cultnet-rudp-service-health. Use health-yggdrasil-source-app.ps1 only as an explicit deployment witness, not Idunn health truth.
exit /b 2

View file

@ -1,43 +0,0 @@
param(
[Parameter(Mandatory = $true)] [string] $AppId,
[Parameter(Mandatory = $true)] [string] $RepoRoot,
[Parameter(Mandatory = $true)] [string] $RemoteAppHome,
[Parameter(Mandatory = $true)] [string] $CheckScript,
[string] $SshTarget = "yggdrasil"
)
$ErrorActionPreference = "Stop"
$localCommit = (git -C $RepoRoot rev-parse HEAD).Trim()
if ([string]::IsNullOrWhiteSpace($localCommit)) {
throw "Could not determine git revision for $RepoRoot"
}
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
$scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10")
$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10", $SshTarget)
scp.exe @scpArgs $CheckScript "${SshTarget}:$remoteCheck" | Out-Null
if ($LASTEXITCODE -ne 0) { throw "scp check script failed for $AppId" }
$manifest = ssh.exe @sshArgs "cat '$RemoteAppHome/deployment-manifest.txt' 2>/dev/null"
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($manifest)) {
throw "$AppId deploy manifest is missing on Yggdrasil."
}
$remoteCommit = ""
foreach ($line in $manifest) {
if ($line -like "gitCommit=*") {
$remoteCommit = $line.Substring("gitCommit=".Length).Trim()
}
}
if ($remoteCommit -ne $localCommit) {
throw "$AppId deploy is stale. remote=$remoteCommit local=$localCommit"
}
ssh.exe @sshArgs "chmod +x '$remoteCheck' && sudo -n bash '$remoteCheck'"
if ($LASTEXITCODE -ne 0) {
throw "$AppId remote health check failed."
}
Write-Host "$AppId is active and deployed at $localCommit."

View file

@ -1,3 +0,0 @@
@echo off
echo health-yggdrasil-streampixels.cmd archived: StreamPixels health is daemon-published over CultNet/RUDP as streampixels.cultnet-rudp-service-health. Use health-yggdrasil-source-app.ps1 only as an explicit deployment witness, not Idunn health truth.
exit /b 2

View file

@ -1,77 +0,0 @@
#!/bin/sh
set -eu
if [ "$(id -u)" -ne 0 ]; then
echo "deploy-yggdrasil-heimdall must run as root through Idunn" >&2
exit 1
fi
source_tar=/srv/heimdall/heimdall-source.tar
cultlib_tar=/srv/heimdall/heimdall-cultlib.tar
releases=/srv/heimdall/app/releases
current=/srv/heimdall/app/current
shared_cultlib="$releases/CultLib"
compose=/srv/compose/yggdrasil-apps.yaml
for required in "$source_tar" "$cultlib_tar" "$compose"; do
if [ ! -f "$required" ]; then
echo "missing audited Heimdall deployment input: $required" >&2
exit 1
fi
done
artifact_sha=$(sha256sum "$source_tar" | awk '{print $1}')
release="$releases/sha-${artifact_sha}"
previous=$(readlink -f "$current" 2>/dev/null || true)
if [ ! -f "$release/.idunn-build-complete" ]; then
staging="$release.staging.$$"
rm -rf "$staging"
install -d -o heimdall -g heimdall "$staging" "$shared_cultlib"
tar -xf "$source_tar" -C "$staging"
rm -rf "$shared_cultlib/packages/cultcache-ts" "$shared_cultlib/packages/cultnet-ts"
tar -xf "$cultlib_tar" -C "$shared_cultlib"
docker run --rm \
-v /srv/heimdall:/srv/heimdall \
-w "$staging" \
node:24.14.1-bookworm-slim@sha256:b506e7321f176aae77317f99d67a24b272c1f09f1d10f1761f2773447d8da26c \
sh -c 'corepack enable && pnpm install --frozen-lockfile && pnpm typecheck && pnpm test && pnpm build && pnpm prune --prod'
printf '%s\n' "$artifact_sha" > "$staging/.idunn-build-complete"
chown -R heimdall:heimdall "$staging" "$shared_cultlib"
mv "$staging" "$release"
fi
ln -sfn "$release" "$current"
chown -h heimdall:heimdall "$current"
if ! docker compose -f "$compose" up -d --force-recreate heimdall; then
if [ -n "$previous" ] && [ -d "$previous" ]; then
ln -sfn "$previous" "$current"
chown -h heimdall:heimdall "$current"
docker compose -f "$compose" up -d --force-recreate heimdall || true
fi
exit 1
fi
healthy=0
for _ in $(seq 1 30); do
if curl -fsS http://127.0.0.1:4100/healthz >/dev/null; then
healthy=1
break
fi
sleep 2
done
if [ "$healthy" -ne 1 ]; then
echo "Heimdall failed health verification after deploying $artifact_sha" >&2
if [ -n "$previous" ] && [ -d "$previous" ]; then
ln -sfn "$previous" "$current"
chown -h heimdall:heimdall "$current"
docker compose -f "$compose" up -d --force-recreate heimdall || true
fi
exit 1
fi
echo "Heimdall deployed audited source artifact $artifact_sha"

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-nightwing-eve-browser-reference.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-nightwing-eve-browser-reference.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-nightwing-eve-browser-reference.ps1

View file

@ -1,17 +0,0 @@
param(
[string] $SshTarget = "nwroot",
[string] $Service = "nightwing-eve-browser-reference.service"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-nightwing-eve-browser-reference.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $SshTarget "systemctl restart $Service && systemctl is-active --quiet $Service"
if ($LASTEXITCODE -ne 0) {
throw "Remote restart failed for $Service on $SshTarget with exit code $LASTEXITCODE"
}
Write-Host "Restarted $Service on $SshTarget."

View file

@ -1,3 +0,0 @@
@echo off
echo restart-nightwing-eve-dashboard.cmd archived: Mimir.EveDashboard's HTTP/WebSocket broker is cut. Rebuild a typed CultMesh/Odin dashboard publisher before restoring Idunn lifecycle authority. 1>&2
exit /b 2

View file

@ -1 +0,0 @@
throw "restart-nightwing-eve-dashboard.ps1 archived: Mimir.EveDashboard's HTTP/WebSocket broker is cut. Rebuild a typed CultMesh/Odin dashboard publisher before restoring Idunn lifecycle authority."

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-odin.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-odin.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-odin.ps1

View file

@ -1,46 +0,0 @@
param(
[string] $StateDir = (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\odin"),
[string] $CultNetRudpBind = "0.0.0.0:17871",
[string] $IdunnRudpHealth = $(if ($env:ODIN_IDUNN_RUDP_HEALTH) { $env:ODIN_IDUNN_RUDP_HEALTH } else { $env:IDUNN_RUDP_HEALTH }),
[string] $IdunnDaemon = "odin",
[string] $IdunnHealthContract = "odin.cultnet-rudp-provider-health"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-odin.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$stopScript = Join-Path $PSScriptRoot "stop-odin.ps1"
$startScript = Join-Path $PSScriptRoot "start-odin.ps1"
$powerShellExe = Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe"
if (-not (Test-Path -LiteralPath $powerShellExe)) {
throw "PowerShell executable is missing at $powerShellExe"
}
$startArgs = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $startScript,
"-StateDir", $StateDir,
"-CultNetRudpBind", $CultNetRudpBind,
"-IdunnDaemon", $IdunnDaemon,
"-IdunnHealthContract", $IdunnHealthContract
)
if (-not [string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
$startArgs += @("-IdunnRudpHealth", $IdunnRudpHealth)
}
& $powerShellExe -NoProfile -ExecutionPolicy Bypass -File $stopScript -StateDir $StateDir
if ($LASTEXITCODE -ne 0) {
throw "stop-odin.ps1 failed with exit code $LASTEXITCODE"
}
& $powerShellExe @startArgs
if ($LASTEXITCODE -ne 0) {
throw "start-odin.ps1 failed with exit code $LASTEXITCODE"
}

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-stonks.cmd is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-stonks.cmd is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-stonks.ps1

View file

@ -1,21 +0,0 @@
$ErrorActionPreference = "Stop"
$stonksRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Stonks")
$stateDir = Join-Path $stonksRoot "scratch\stonks"
$pidPath = Join-Path $stateDir "stonks.pid"
$startScript = Join-Path $stonksRoot "scripts\start-stonks.ps1"
if (Test-Path -LiteralPath $pidPath) {
$pidText = (Get-Content -LiteralPath $pidPath -Raw).Trim()
if ($pidText -match "^\d+$") {
$process = Get-Process -Id ([int] $pidText) -ErrorAction SilentlyContinue
if ($null -ne $process) {
Stop-Process -Id $process.Id -Force
Start-Sleep -Milliseconds 500
}
}
Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startScript
exit $LASTEXITCODE

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-vili.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-vili.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0restart-vili.ps1" %*

View file

@ -1,287 +0,0 @@
param(
[string] $RavenHost = "raven",
[string] $LocalViliRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Vili"),
[string] $LocalCultLibRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "CultLib"),
[string] $RemoteViliRoot = "E:\Projects\Vili",
[int] $Port = 8824,
[string] $HostName = "0.0.0.0",
[string] $StateRoot = "E:\Projects\Vili\.vili",
[string] $IdunnRudpHealth = $(if ($env:VILI_IDUNN_RUDP_HEALTH) { $env:VILI_IDUNN_RUDP_HEALTH } else { $env:IDUNN_RUDP_HEALTH }),
[string] $IdunnDaemon = "vili",
[string] $IdunnHealthContract = "vili.cultnet-rudp-animation-health",
[int] $IdunnHealthIntervalSeconds = 15
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-vili.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by VILI_IDUNN_RUDP_HEALTH, IDUNN_RUDP_HEALTH, or -IdunnRudpHealth; no WireGuard endpoint default is allowed."
}
$localFiles = @(
(Join-Path $LocalViliRoot "package.json"),
(Join-Path $LocalViliRoot "package-lock.json"),
(Join-Path $LocalViliRoot "scripts\idunn-rudp.cjs"),
(Join-Path $LocalViliRoot "scripts\install-vili-task.ps1"),
(Join-Path $LocalViliRoot "scripts\restart-vili-daemon.cmd"),
(Join-Path $LocalViliRoot "scripts\restart-vili-daemon.ps1"),
(Join-Path $LocalViliRoot "scripts\run-hidden-powershell.vbs"),
(Join-Path $LocalViliRoot "scripts\run-vili-daemon.ps1"),
(Join-Path $LocalViliRoot "scripts\start-vili-daemon.ps1"),
(Join-Path $LocalViliRoot "scripts\vili-daemon.mjs"),
(Join-Path $LocalCultLibRoot "packages\cultnet-ts"),
(Join-Path $LocalCultLibRoot "packages\cultcache-ts"),
(Join-Path $LocalCultLibRoot "node_modules\ajv"),
(Join-Path $LocalCultLibRoot "node_modules\fast-deep-equal"),
(Join-Path $LocalCultLibRoot "node_modules\fast-uri"),
(Join-Path $LocalCultLibRoot "node_modules\json-schema-traverse"),
(Join-Path $LocalCultLibRoot "node_modules\require-from-string"),
(Join-Path $LocalViliRoot "node_modules\@msgpack")
)
foreach ($path in $localFiles) {
if (-not (Test-Path -LiteralPath $path)) {
throw "Required Vili runtime path not found: $path"
}
}
$syncId = [guid]::NewGuid().ToString("N")
$remoteSyncRootPs = "$RemoteViliRoot\.odin-vili-sync-$syncId"
$remoteSyncRootSftp = ($remoteSyncRootPs -replace "\\", "/")
$remoteTempPs = "C:\Windows\Temp\odin-raven-vili-restart-$syncId.ps1"
$remoteTempSftp = ($remoteTempPs -replace "\\", "/")
$remoteBundlePs = "C:\Windows\Temp\odin-raven-vili-bundle-$syncId.zip"
$remoteBundleSftp = ($remoteBundlePs -replace "\\", "/")
$localRemoteScript = Join-Path $env:TEMP "odin-raven-vili-restart-$syncId.ps1"
$localSftpBatch = Join-Path $env:TEMP "odin-raven-vili-restart-$syncId.sftp"
$localBundleRoot = Join-Path $env:TEMP "odin-raven-vili-bundle-$syncId"
$localBundleScripts = Join-Path $localBundleRoot "scripts"
$localBundleNodeModules = Join-Path $localBundleRoot "node_modules"
$localBundleZip = Join-Path $env:TEMP "odin-raven-vili-bundle-$syncId.zip"
$remoteScript = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
`$remoteRoot = "$RemoteViliRoot"
`$syncRoot = "$remoteSyncRootPs"
`$scriptsRoot = Join-Path `$remoteRoot "scripts"
`$stateRoot = "$StateRoot"
`$nodeModulesRoot = Join-Path `$remoteRoot "node_modules"
`$pidPath = Join-Path `$stateRoot "vili-daemon.pid"
try {
Expand-Archive -LiteralPath "$remoteBundlePs" -DestinationPath `$syncRoot -Force
if (-not (Test-Path -LiteralPath `$syncRoot)) {
throw "Uploaded sync root not found at `$syncRoot"
}
if (Test-Path -LiteralPath `$pidPath) {
`$existingPid = (Get-Content -LiteralPath `$pidPath -Raw -ErrorAction SilentlyContinue).Trim()
if (`$existingPid -match '^\d+$') {
`$process = Get-Process -Id ([int] `$existingPid) -ErrorAction SilentlyContinue
if (`$null -ne `$process) {
Stop-Process -Id `$process.Id -Force
Start-Sleep -Seconds 1
}
}
Remove-Item -LiteralPath `$pidPath -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path `$scriptsRoot | Out-Null
New-Item -ItemType Directory -Force -Path `$stateRoot | Out-Null
Copy-Item -LiteralPath (Join-Path `$syncRoot "package.json") -Destination (Join-Path `$remoteRoot "package.json") -Force
Copy-Item -LiteralPath (Join-Path `$syncRoot "package-lock.json") -Destination (Join-Path `$remoteRoot "package-lock.json") -Force
`$scriptFiles = @(
"idunn-rudp.cjs",
"install-vili-task.ps1",
"restart-vili-daemon.cmd",
"restart-vili-daemon.ps1",
"run-hidden-powershell.vbs",
"run-vili-daemon.ps1",
"start-vili-daemon.ps1",
"vili-daemon.mjs"
)
foreach (`$name in `$scriptFiles) {
Copy-Item -LiteralPath (Join-Path `$syncRoot "scripts\`$name") -Destination (Join-Path `$scriptsRoot `$name) -Force
}
if (Test-Path -LiteralPath `$nodeModulesRoot) {
Remove-Item -LiteralPath `$nodeModulesRoot -Recurse -Force
}
Move-Item -LiteralPath (Join-Path `$syncRoot "node_modules") -Destination `$nodeModulesRoot -Force
& powershell.exe @(
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", (Join-Path `$scriptsRoot "install-vili-task.ps1"),
"-TaskName", "GameCult\Vili",
"-Port", "$Port",
"-HostName", "$HostName",
"-StateRoot", "$StateRoot",
"-IdunnRudpHealth", "$IdunnRudpHealth",
"-IdunnDaemon", "$IdunnDaemon",
"-IdunnHealthContract", "$IdunnHealthContract",
"-IdunnHealthIntervalSeconds", "$IdunnHealthIntervalSeconds"
)
& powershell.exe @(
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", (Join-Path `$scriptsRoot "restart-vili-daemon.ps1"),
"-Port", "$Port",
"-HostName", "$HostName",
"-StateRoot", "$StateRoot",
"-IdunnRudpHealth", "$IdunnRudpHealth",
"-IdunnDaemon", "$IdunnDaemon",
"-IdunnHealthContract", "$IdunnHealthContract",
"-IdunnHealthIntervalSeconds", "$IdunnHealthIntervalSeconds"
)
`$task = Get-ScheduledTask -TaskName "Vili" -TaskPath "\GameCult\" -ErrorAction Stop
`$action = @(`$task.Actions)[0]
if (`$action.Execute -notmatch '(^|\\)wscript\.exe$') {
throw "GameCult\Vili action executes `$(`$action.Execute), expected wscript.exe"
}
if (`$action.Arguments -notlike "*run-hidden-powershell.vbs*") {
throw "GameCult\Vili action arguments do not reference run-hidden-powershell.vbs"
}
`$taskHealthChecks = @(
(`$action.Arguments -like "*-IdunnRudpHealth*"),
(`$action.Arguments -like "*$IdunnRudpHealth*"),
(`$action.Arguments -like "*-IdunnDaemon*"),
(`$action.Arguments -like "*$IdunnDaemon*"),
(`$action.Arguments -like "*-IdunnHealthContract*"),
(`$action.Arguments -like "*$IdunnHealthContract*")
)
if (`$taskHealthChecks -contains `$false) {
throw "GameCult\Vili action arguments do not carry the Idunn RUDP health contract."
}
`$process = Get-CimInstance Win32_Process |
Where-Object { `$_.CommandLine -like '*vili-daemon.mjs*' } |
Select-Object -First 1
if (`$null -eq `$process) {
throw "Vili process is not running after restart."
}
`$processHealthChecks = @(
(`$process.CommandLine -like "*--idunn-rudp-health*"),
(`$process.CommandLine -like "*$IdunnRudpHealth*"),
(`$process.CommandLine -like "*--idunn-daemon*"),
(`$process.CommandLine -like "*$IdunnDaemon*"),
(`$process.CommandLine -like "*--idunn-health-contract*"),
(`$process.CommandLine -like "*$IdunnHealthContract*")
)
if (`$processHealthChecks -contains `$false) {
throw "Vili process command line does not include the Idunn RUDP health arguments."
}
`$operatorStatePath = Join-Path `$stateRoot "operator-state.json"
`$deadline = (Get-Date).AddSeconds(30)
`$state = `$null
while ((Get-Date) -lt `$deadline) {
if (Test-Path -LiteralPath `$operatorStatePath) {
try {
`$state = Get-Content -LiteralPath `$operatorStatePath -Raw | ConvertFrom-Json
if (`$null -ne `$state.idunnRudpHealth -and `$state.idunnRudpHealth.status -eq "published") {
break
}
} catch {
}
}
Start-Sleep -Seconds 1
}
if (`$null -eq `$state -or `$null -eq `$state.idunnRudpHealth) {
throw "Vili daemon state file did not report idunnRudpHealth after restart."
}
if (`$state.idunnRudpHealth.status -ne "published") {
throw "Vili idunnRudpHealth status is `$(`$state.idunnRudpHealth.status): `$(`$state.idunnRudpHealth.error)"
}
Write-Host "Vili Raven runtime repaired and restarted with Idunn RUDP health."
Write-Host "ProcessId=`$(`$process.ProcessId)"
Write-Host "TaskArguments=`$(`$action.Arguments)"
Write-Host "HealthStatus=`$(`$state.idunnRudpHealth.status)"
Write-Host "HealthContract=`$(`$state.idunnRudpHealth.contract)"
} finally {
Remove-Item -LiteralPath `$syncRoot -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath "$remoteBundlePs" -Force -ErrorAction SilentlyContinue
}
"@
$batchLines = @(
"put ""$localBundleZip"" ""$remoteBundleSftp""",
"put ""$localRemoteScript"" ""$remoteTempSftp"""
)
try {
$moduleCopies = @(
@{ Source = (Join-Path $LocalCultLibRoot "packages\cultnet-ts"); Destination = (Join-Path $localBundleNodeModules "cultnet-ts") },
@{ Source = (Join-Path $LocalCultLibRoot "packages\cultcache-ts"); Destination = (Join-Path $localBundleNodeModules "cultcache-ts") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\ajv"); Destination = (Join-Path $localBundleNodeModules "ajv") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\fast-deep-equal"); Destination = (Join-Path $localBundleNodeModules "fast-deep-equal") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\fast-uri"); Destination = (Join-Path $localBundleNodeModules "fast-uri") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\json-schema-traverse"); Destination = (Join-Path $localBundleNodeModules "json-schema-traverse") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\require-from-string"); Destination = (Join-Path $localBundleNodeModules "require-from-string") },
@{ Source = (Join-Path $LocalViliRoot "node_modules\@msgpack"); Destination = (Join-Path $localBundleNodeModules "@msgpack") }
)
New-Item -ItemType Directory -Force -Path $localBundleScripts, $localBundleNodeModules | Out-Null
Copy-Item -LiteralPath (Join-Path $LocalViliRoot "package.json") -Destination (Join-Path $localBundleRoot "package.json") -Force
Copy-Item -LiteralPath (Join-Path $LocalViliRoot "package-lock.json") -Destination (Join-Path $localBundleRoot "package-lock.json") -Force
$scriptCopies = @(
"idunn-rudp.cjs",
"install-vili-task.ps1",
"restart-vili-daemon.cmd",
"restart-vili-daemon.ps1",
"run-hidden-powershell.vbs",
"run-vili-daemon.ps1",
"start-vili-daemon.ps1",
"vili-daemon.mjs"
)
foreach ($scriptName in $scriptCopies) {
Copy-Item -LiteralPath (Join-Path $LocalViliRoot "scripts\$scriptName") -Destination (Join-Path $localBundleScripts $scriptName) -Force
}
foreach ($copy in $moduleCopies) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $copy.Destination) | Out-Null
Copy-Item -LiteralPath $copy.Source -Destination $copy.Destination -Recurse -Force
}
if (Test-Path -LiteralPath $localBundleZip) {
Remove-Item -LiteralPath $localBundleZip -Force
}
Compress-Archive -Path (Join-Path $localBundleRoot "*") -DestinationPath $localBundleZip -CompressionLevel Optimal
Set-Content -LiteralPath $localRemoteScript -Encoding ASCII -Value $remoteScript
Set-Content -LiteralPath $localSftpBatch -Encoding ASCII -Value ($batchLines -join "`r`n")
& sftp.exe -b $localSftpBatch $RavenHost
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$remoteRunner = @"
`$ErrorActionPreference = "Stop"
try {
& "$remoteTempPs"
`$code = `$LASTEXITCODE
} finally {
Remove-Item -LiteralPath "$remoteTempPs" -Force -ErrorAction SilentlyContinue
}
exit `$code
"@
$encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner))
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedRunner"
} finally {
Remove-Item -LiteralPath $localRemoteScript, $localSftpBatch -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $localBundleZip -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $localBundleRoot -Recurse -Force -ErrorAction SilentlyContinue
}
$restartExit = $LASTEXITCODE
exit $restartExit

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-weksa.cmd is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-weksa.cmd is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
call %~dp0..\..\weksa\scripts\restart-weksa-daemon.cmd

View file

@ -1,68 +0,0 @@
param(
[string] $StateDir = (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\odin"),
[string] $CultNetRudpBind = "0.0.0.0:17871",
[string] $IdunnRudpHealth = $(if ($env:ODIN_IDUNN_RUDP_HEALTH) { $env:ODIN_IDUNN_RUDP_HEALTH } else { $env:IDUNN_RUDP_HEALTH }),
[string] $IdunnDaemon = "odin",
[string] $IdunnHealthContract = "odin.cultnet-rudp-provider-health",
[switch] $Foreground
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$scriptPath = Join-Path $repoRoot "src\odin-coordinator.cjs"
$pidPath = Join-Path $StateDir "odin.pid"
$outLog = Join-Path $StateDir "odin.out.log"
$errLog = Join-Path $StateDir "odin.err.log"
$nodeExe = (Get-Command node.exe -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source)
if (-not $nodeExe) {
$nodeExe = "C:\Program Files\nodejs\node.exe"
}
if (-not (Test-Path -LiteralPath $nodeExe)) {
throw "Node.js executable is missing; checked PATH and $nodeExe"
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Odin Idunn health publication requires -IdunnRudpHealth, ODIN_IDUNN_RUDP_HEALTH, or IDUNN_RUDP_HEALTH; no localhost default is assumed."
}
New-Item -ItemType Directory -Force -Path $StateDir | Out-Null
if (Test-Path $pidPath) {
$oldPid = (Get-Content -Raw $pidPath).Trim()
if ($oldPid -match "^\d+$") {
$old = Get-Process -Id ([int] $oldPid) -ErrorAction SilentlyContinue
if ($old) {
throw "Odin already appears to be running as PID $oldPid."
}
}
Remove-Item -LiteralPath $pidPath -Force
}
$env:NODE_PATH = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "CultLib\packages")
$args = @(
$scriptPath,
"--stateDir", $StateDir,
"--cultnet-rudp-bind", $CultNetRudpBind,
"--idunn-rudp-health", $IdunnRudpHealth,
"--idunn-daemon", $IdunnDaemon,
"--idunn-health-contract", $IdunnHealthContract
)
if ($Foreground) {
& $nodeExe @args
exit $LASTEXITCODE
}
$proc = Start-Process -FilePath $nodeExe -ArgumentList $args -WorkingDirectory $repoRoot -WindowStyle Hidden -PassThru -RedirectStandardOutput $outLog -RedirectStandardError $errLog
$proc.Id | Set-Content -Encoding ASCII -LiteralPath $pidPath
Start-Sleep -Seconds 1
if ($proc.HasExited) {
$detail = ""
if (Test-Path $outLog) { $detail += Get-Content -Raw $outLog }
if (Test-Path $errLog) { $detail += Get-Content -Raw $errLog }
throw "Odin exited immediately with code $($proc.ExitCode).`n$detail"
}
Write-Host "Odin started as PID $($proc.Id)."
Write-Host "CultMesh/RUDP document catalog: $CultNetRudpBind"

View file

@ -1,22 +0,0 @@
param(
[string] $StateDir = (Join-Path (Split-Path -Parent $PSScriptRoot) "scratch\odin")
)
$ErrorActionPreference = "Stop"
$pidPath = Join-Path $StateDir "odin.pid"
if (-not (Test-Path $pidPath)) {
Write-Host "No Odin PID file found at $pidPath."
exit 0
}
$odinPid = (Get-Content -Raw $pidPath).Trim()
if ($odinPid -match "^\d+$") {
$process = Get-Process -Id ([int] $odinPid) -ErrorAction SilentlyContinue
if ($process) {
Stop-Process -Id $process.Id
Write-Host "Stopped Odin PID $odinPid."
}
}
Remove-Item -LiteralPath $pidPath -Force

View file

@ -1,212 +0,0 @@
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { buildConfig, loadCultRuntime } = require("./odin/config.cjs");
const { createCultMeshRudpDocumentServer } = require("./odin/cultnet-rudp.cjs");
const { defineOdinDocuments } = require("./odin/documents.cjs");
const { createInterfaceDiscovery } = require("./odin/interfaces.cjs");
const { createIdunnRudpHealthPublisher, publishIdunnRudpHealth } = require("./odin/idunn-rudp.cjs");
const { createLayoutStore } = require("./odin/layout.cjs");
const { createLiveProviderRegistry } = require("./odin/provider-ingress.cjs");
const { createStateBuilder } = require("./odin/state.cjs");
const config = buildConfig(process.argv.slice(2));
fs.mkdirSync(config.stateDir, { recursive: true });
const idunnRudpHealthPublisher = createIdunnRudpHealthPublisher(config.idunnRudpHealth);
const cultRuntime = loadCultRuntime();
if (cultRuntime.error) {
console.error("CultMesh runtime unavailable; durable mesh catalog disabled:", cultRuntime.error.message);
}
const documents = defineOdinDocuments(cultRuntime.defineDocumentType);
const allDocumentDefinitions = Object.values(documents).filter(Boolean);
const layoutStore = createLayoutStore({
definition: documents.interfaceLayoutDefinition,
getNode: async () => meshNodePromise ? await meshNodePromise : null,
layoutPath: config.layoutPath,
});
const liveProviderRegistry = createLiveProviderRegistry();
const interfaceDiscovery = createInterfaceDiscovery({
CultMesh: cultRuntime.CultMesh,
documents,
interfaceBindingStores: config.interfaceBindingStores,
liveProviderRegistry,
});
const stateBuilder = createStateBuilder({
cachePath: config.cachePath,
gamecultTextDocumentStorePath: config.gamecultTextDocumentStorePath,
interfaceDiscovery,
layoutStore,
stonksBurstSize: config.stonksBurstSize,
});
let meshNodePromise = null;
let cultMeshRudpDocumentServer = null;
let currentState = stateBuilder.buildPendingState("Coordinator starting");
let lastRefresh = {
completedAt: null,
durationMs: null,
error: null,
startedAt: null,
};
let lastIdunnRudpHealthPublishedAt = null;
let observedRudpDocuments = 0;
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
async function main() {
if (cultRuntime.CultMesh && documents.surfaceDefinition) {
meshNodePromise = createDurableSurfaceNode();
}
if (config.cultnetRudpBind) {
cultMeshRudpDocumentServer = createCultMeshRudpDocumentServer({
CultMesh: cultRuntime.CultMesh,
bind: config.cultnetRudpBind,
documents,
getCache: async () => {
if (!meshNodePromise) {
throw new Error("CultMesh runtime is unavailable; Odin cannot serve a CultMesh/RUDP document catalog.");
}
return (await meshNodePromise).cache;
},
onDocumentPutRaw: async (document) => {
if (observedRudpDocuments < 40) {
observedRudpDocuments += 1;
console.error(
`Odin RUDP document #${observedRudpDocuments} schema=${document.schemaId} key=${document.recordKey} source=${document.sourceRuntimeId || "unknown"} remote=${document.remote?.address}:${document.remote?.port}`,
);
}
liveProviderRegistry.ingestDocument(document, document.remote);
await persistRudpDocumentPut(document);
},
});
await cultMeshRudpDocumentServer.start();
console.log(`CultMesh/RUDP document catalog: ${config.cultnetRudpBind}`);
}
console.log(`Durable surface cache: ${config.cachePath}`);
await refresh();
scheduleRefresh();
}
async function persistRudpDocumentPut(document) {
if (!meshNodePromise || !document?.schemaId || !document?.recordKey) return;
const definition = documentDefinitionForSchema(document.schemaId);
if (!definition) return;
const node = await meshNodePromise;
await node.put(definition, document.recordKey, normalizeRudpPayload(document.payload));
}
function documentDefinitionForSchema(schemaId) {
return allDocumentDefinitions.find((definition) =>
definition?.schemaId === schemaId
|| definition?.schemaVersion === schemaId
|| definition?.schemaName === schemaId
|| definition?.type === schemaId
);
}
function normalizeRudpPayload(payload) {
if (Array.isArray(payload) && payload.length === 1 && payload[0] && typeof payload[0] === "object" && !Array.isArray(payload[0])) {
return payload[0];
}
return payload;
}
function scheduleRefresh() {
setTimeout(() => {
refresh()
.catch((error) => console.error("refresh failed:", error))
.finally(() => scheduleRefresh());
}, config.intervalMs);
}
async function createDurableSurfaceNode() {
try {
return await cultRuntime.CultMesh.createNode(config.cachePath, { documents: allDocumentDefinitions });
} catch (error) {
if (!fs.existsSync(config.cachePath)) {
throw error;
}
const corruptPath = `${config.cachePath}.corrupt-${new Date().toISOString().replace(/[:.]/g, "-")}`;
fs.renameSync(config.cachePath, corruptPath);
console.error(`CultMesh surface cache was unreadable and has been quarantined: ${corruptPath}`);
console.error(`CultMesh surface cache read error: ${error.message}`);
return cultRuntime.CultMesh.createNode(config.cachePath, { documents: allDocumentDefinitions });
}
}
async function refresh() {
const started = Date.now();
lastRefresh = {
completedAt: null,
durationMs: null,
error: null,
startedAt: new Date(started).toISOString(),
};
try {
currentState = await stateBuilder.buildState();
await persistState(currentState);
lastRefresh = {
completedAt: new Date().toISOString(),
durationMs: Date.now() - started,
error: null,
startedAt: new Date(started).toISOString(),
};
await publishOdinHealth("active", `Odin provider catalog refreshed in ${lastRefresh.durationMs}ms`);
} catch (error) {
lastRefresh = {
completedAt: new Date().toISOString(),
durationMs: Date.now() - started,
error: error.message,
startedAt: new Date(started).toISOString(),
};
await publishOdinHealth("failed", `Odin provider refresh failed: ${error.message}`);
throw error;
}
}
async function publishOdinHealth(state, detail) {
if (!idunnRudpHealthPublisher) return;
try {
await publishIdunnRudpHealth(idunnRudpHealthPublisher, {
state,
detail,
observedAt: new Date().toISOString(),
});
lastIdunnRudpHealthPublishedAt = Date.now();
} catch (error) {
const lastPublishedAgeMs = lastIdunnRudpHealthPublishedAt === null
? Number.POSITIVE_INFINITY
: Date.now() - lastIdunnRudpHealthPublishedAt;
if (lastPublishedAgeMs > Math.max(60_000, config.intervalMs * 4)) {
console.error("Idunn RUDP health publish failed:", error.message);
}
}
}
async function persistState(state) {
if (config.writeDebugSurfaceJson) {
fs.writeFileSync(path.join(config.stateDir, "latest-surface.json"), JSON.stringify(state, null, 2), "utf8");
}
if (!meshNodePromise || !documents.surfaceDefinition) {
return;
}
try {
const node = await meshNodePromise;
await node.put(documents.surfaceDefinition, config.surfaceKey, state);
await node.flush?.(true);
} catch (error) {
console.error("CultMesh snapshot write failed:", error.message);
}
}

View file

@ -1,73 +0,0 @@
"use strict";
const Module = require("module");
const path = require("path");
const { parseArgs } = require("./utils.cjs");
const repoRoot = path.resolve(__dirname, "..", "..");
process.env.NODE_PATH = [
path.resolve(repoRoot, "..", "CultLib", "packages"),
process.env.NODE_PATH || "",
].filter(Boolean).join(path.delimiter);
Module._initPaths();
function buildConfig(argv) {
const args = parseArgs(argv);
const cultnetRudpBind = args["cultnet-rudp-bind"] ? String(args["cultnet-rudp-bind"]) : "";
const intervalMs = Number(args.intervalMs || 5000);
const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin");
const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp");
const layoutPath = args.layoutPath || path.join(stateDir, "interface-layout.json");
const writeDebugSurfaceJson = Boolean(args.writeDebugSurfaceJson || args["write-debug-surface-json"] || process.env.ODIN_WRITE_DEBUG_SURFACE_JSON === "1");
if (args.observationLogPath || args.observationFreshSeconds) {
throw new Error("Odin no longer tails Mimir observation logs. Publish observation records through CultMesh/Odin provider discovery instead.");
}
const gamecultTextDocumentStorePath = args.gamecultTextDocumentStorePath || path.join(repoRoot, "..", "VoidBot", ".voidbot", "private", "gamecult-text-documents.cc");
const stonksBurstSize = Math.max(1, Number(args.stonksBurstSize || 8));
const idunnRudpHealth = args["idunn-rudp-health"] ? {
endpoint: String(args["idunn-rudp-health"]),
daemonId: String(args["idunn-daemon"] || "odin"),
healthContract: String(args["idunn-health-contract"] || "odin.cultnet-rudp-provider-health"),
privateKeyPath: args["idunn-health-private-key"]
? String(args["idunn-health-private-key"])
: "",
sourceRuntimeId: String(args["idunn-source-runtime"] || "odin-coordinator"),
} : null;
const interfaceBindingStores = String(
args.interfaceBindingStore ||
process.env.ODIN_INTERFACE_BINDING_STORES ||
"",
)
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
return {
args,
cachePath,
cultnetRudpBind,
gamecultTextDocumentStorePath,
interfaceBindingStores,
idunnRudpHealth,
intervalMs,
layoutPath,
repoRoot,
stateDir,
stonksBurstSize,
surfaceKey: "surface:gamecult.network.status",
writeDebugSurfaceJson,
};
}
function loadCultRuntime() {
try {
const { CultMesh } = require("cultmesh-ts");
const { defineDocumentType } = require("cultcache-ts");
return { CultMesh, defineDocumentType, error: null };
} catch (error) {
return { CultMesh: null, defineDocumentType: null, error };
}
}
module.exports = { buildConfig, loadCultRuntime, repoRoot };

View file

@ -1,115 +0,0 @@
"use strict";
const path = require("path");
const { createRequire } = require("module");
const { createSocket } = require("dgram");
const requireCultNet = createRequire(path.resolve(
__dirname,
"..",
"..",
"..",
"CultLib",
"packages",
"cultnet-ts",
"package.json",
));
const {
CultNetDocumentRegistry,
decodeRudpPacket,
defineCultNetDocumentBinding,
} = requireCultNet("./dist/index.js");
const ODIN_RUDP_CONNECTION_ID = 0x0d1d0002;
function createCultMeshRudpDocumentServer(options) {
if (!options?.bind) return null;
if (!options.CultMesh?.createRudpDocumentServer) {
throw new Error("Odin requires a CultMesh runtime with createRudpDocumentServer.");
}
if (!options.documents?.surfaceDefinition) {
throw new Error("Odin CultMesh/RUDP document server requires a surface document definition.");
}
if (typeof options.getCache !== "function") {
throw new Error("Odin CultMesh/RUDP document server requires an async getCache function.");
}
const bind = parseEndpoint(options.bind);
const socket = createSocket(bind.host.includes(":") ? "udp6" : "udp4");
let observedPackets = 0;
socket.on("message", (wire, remote) => {
if (observedPackets >= 20) return;
observedPackets += 1;
try {
const packet = decodeRudpPacket(wire);
console.error(
`Odin RUDP observed packet #${observedPackets} ${packet.packetType} connection=${packet.connectionId.toString(16)} channel=${packet.channelId} from ${remote.address}:${remote.port} bytes=${wire.length}`,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(
`Odin RUDP observed undecodable packet #${observedPackets} from ${remote.address}:${remote.port} bytes=${wire.length}: ${message}`,
);
}
});
const registry = new CultNetDocumentRegistry(
Object.values(options.documents)
.filter(Boolean)
.map((definition) => defineCultNetDocumentBinding({ definition })),
);
const server = options.CultMesh.createRudpDocumentServer(
"odin-cultmesh-catalog",
ODIN_RUDP_CONNECTION_ID,
{
bindHost: bind.host,
bindPort: bind.port,
documents: registry,
socket,
getCache: options.getCache,
onError: (error) => {
const message = error instanceof Error ? error.message : String(error);
if (typeof options.logError === "function") {
options.logError(`CultMesh/RUDP document server error: ${message}`);
return;
}
console.error(`CultMesh/RUDP document server error: ${message}`);
},
onDocumentPutRaw: (document) => {
if (typeof options.onDocumentPutRaw === "function") {
return options.onDocumentPutRaw(document);
}
return undefined;
},
},
);
return {
bind,
start: () => server.start(),
close: () => server.close(),
};
}
function parseEndpoint(value) {
const text = String(value || "").trim();
const ipv6 = text.match(/^\[([^\]]+)\]:(\d+)$/);
if (ipv6) return { host: ipv6[1], port: parsePort(ipv6[2]) };
const index = text.lastIndexOf(":");
if (index <= 0) {
throw new Error(`CultMesh/RUDP bind must be host:port, got "${value}".`);
}
return { host: text.slice(0, index), port: parsePort(text.slice(index + 1)) };
}
function parsePort(value) {
const port = Number(value);
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
throw new Error(`CultMesh/RUDP port is invalid: ${value}`);
}
return port;
}
module.exports = {
createCultMeshRudpDocumentServer,
};

View file

@ -1,26 +0,0 @@
"use strict";
// The swarm document-type catalog, as Odin consumes it.
//
// The 45 definitions used to live here. Almost none of them are Odin's: they
// describe documents that cross service boundaries, and every service that
// decodes another's documents needs the same ones. They lived here because Odin
// was written first. They now live in CultLib as cultcache-ts's
// defineSwarmDocuments, beside the defineDocumentType that builds them, so
// Hermodr can leave without forking a second definition of one wire format.
const path = require("path");
const { createRequire } = require("module");
const cultLibRoot = process.env.CULTLIB_ROOT
? path.resolve(process.env.CULTLIB_ROOT)
: path.resolve(__dirname, "..", "..", "..", "CultLib");
const requireCultCache = createRequire(path.resolve(cultLibRoot, "packages", "cultcache-ts", "package.json"));
const { defineSwarmDocuments } = requireCultCache("./dist/index.js");
// Odin's consumers know this name. The catalog is not Odin's, but the call is.
const defineOdinDocuments = defineSwarmDocuments;
module.exports = { defineOdinDocuments, defineSwarmDocuments };

View file

@ -1,65 +0,0 @@
"use strict";
// Idunn health publication, as Odin consumes it.
//
// The transport used to live here: 300-odd lines of socket handling, RUDP
// handshaking, Ed25519 signing, and packet receipt, of which almost nothing was
// about Idunn. That is generic CultNet client work and now lives in CultLib as
// cultnet-ts's signed-daemon-health module.
//
// What remains is what Odin actually needs to know: Idunn's wire contract, and
// the shape of the calls its consumers already make. The constants are defined
// by Idunn in docs/signed-daemon-health-authority.md. They are declared here,
// not decided here; if they change, that document changes first.
const path = require("path");
const { createRequire } = require("module");
const cultLibRoot = process.env.CULTLIB_ROOT
? path.resolve(process.env.CULTLIB_ROOT)
: path.resolve(__dirname, "..", "..", "..", "CultLib");
const requireCultNet = createRequire(path.resolve(cultLibRoot, "packages", "cultnet-ts", "package.json"));
const {
CULTNET_RUDP_PROTOCOL_ID,
createSignedDaemonHealthPublisher,
publishSignedDaemonHealth,
signedDaemonHealthPayload,
signedDaemonHealthSigningMessage,
} = requireCultNet("./dist/index.js");
// Idunn's contract. Owner: GameCult/Idunn, docs/signed-daemon-health-authority.md.
const IDUNN_HEALTH_CONTRACT = Object.freeze({
connectionId: 0x1d0d0001,
signedSchemaId: "idunn.signed_daemon_health.v1",
unsignedSchemaId: "idunn.daemon_health",
messageIdPrefix: "odin-health",
});
function createIdunnRudpHealthPublisher(options) {
if (!options) return null;
return createSignedDaemonHealthPublisher({
sourceRuntimeId: "odin-coordinator",
...options,
contract: IDUNN_HEALTH_CONTRACT,
});
}
const publishIdunnRudpHealth = publishSignedDaemonHealth;
const signedHealthPayload = signedDaemonHealthPayload;
// The signing purpose is Idunn's signed schema id, so a statement signed for
// Idunn cannot verify as another service's health.
function signingMessage(payload) {
return signedDaemonHealthSigningMessage(IDUNN_HEALTH_CONTRACT.signedSchemaId, payload);
}
module.exports = {
CULTNET_RUDP_PROTOCOL_ID,
IDUNN_HEALTH_CONTRACT,
createIdunnRudpHealthPublisher,
publishIdunnRudpHealth,
signedHealthPayload,
signingMessage,
};

File diff suppressed because it is too large Load diff

View file

@ -1,279 +0,0 @@
"use strict";
const fs = require("fs");
const { clampNumber, positiveNumber, stableId } = require("./utils.cjs");
const DEFAULT_LAYOUT_ID = "odin.providers";
function createLayoutStore({ definition, getNode, layoutPath } = {}) {
let cachedLayout = null;
let attemptedMigration = false;
return {
async readLayout() {
const meshLayout = await readCultMeshLayout();
if (meshLayout) return meshLayout;
if (!attemptedMigration) {
attemptedMigration = true;
const migrated = readJsonMigrationLayout(layoutPath);
if (migrated) {
cachedLayout = migrated;
await this.writeLayout(migrated).catch(() => undefined);
return normalizeLayout(migrated);
}
}
return cachedLayout || defaultLayout();
},
async writeLayout(layout) {
const normalized = normalizeLayout(layout);
cachedLayout = normalized;
const node = await layoutNode();
if (!node || !definition) {
throw new Error("Odin CultMesh layout store unavailable; refusing to write layout sidecar fallback.");
}
await node.put(definition, DEFAULT_LAYOUT_ID, normalized);
await node.flush?.(true);
},
async applyClientCommand(command) {
if (!command || command.type !== "odin-layout-intent" || !command.providerId) return;
const layout = await this.readLayout();
layout.updatedAt = new Date().toISOString();
layout.tiles ||= {};
const current = layout.tiles[command.providerId] || defaultLayoutFor({ providerId: command.providerId }, []);
const next = { ...current };
if (command.action === "focus") {
Object.assign(next, fullscreenLayoutIntent(command.providerId, -100));
} else if (command.action === "resize") {
next.w = clampNumber((next.w || 1) + Number(command.dw || 0), 1, 4);
next.h = clampNumber((next.h || 1) + Number(command.dh || 0), 1, 4);
} else if (command.action === "move") {
next.x = clampNumber((next.x || 0) + Number(command.dx || 0), 0, 12);
next.y = clampNumber((next.y || 0) + Number(command.dy || 0), 0, 12);
} else if (command.action === "toggle") {
next.visible = !next.visible;
} else {
return;
}
layout.tiles[command.providerId] = next;
await this.writeLayout(layout);
},
};
async function readCultMeshLayout() {
const node = await layoutNode();
if (!node || !definition) return null;
const record = await node.get(definition, DEFAULT_LAYOUT_ID);
const normalized = record ? normalizeLayout(record) : null;
if (normalized) cachedLayout = normalized;
return normalized;
}
async function layoutNode() {
return typeof getNode === "function" ? await getNode() : null;
}
}
function defaultLayout() {
return { schema: "odin.interface_layout.v1", layoutId: DEFAULT_LAYOUT_ID, tiles: {} };
}
function normalizeLayout(layout) {
return {
...defaultLayout(),
...(layout && typeof layout === "object" ? layout : {}),
schema: "odin.interface_layout.v1",
layoutId: DEFAULT_LAYOUT_ID,
tiles: layout && typeof layout.tiles === "object" && layout.tiles ? layout.tiles : {},
};
}
function readJsonMigrationLayout(layoutPath) {
if (!layoutPath) return null;
try {
return normalizeLayout(JSON.parse(fs.readFileSync(layoutPath, "utf8")));
} catch {
return null;
}
}
function defaultLayoutFor(entry, interfaces) {
const index = Math.max(0, interfaces.findIndex((candidate) => candidate.providerId === entry.providerId));
const intent = surfaceLayoutIntent(entry);
const preferredHeight = positiveNumber(intent.preferredHeight, entry.providerId === "voidbot.swarm" ? 18 : 12);
const minHeight = positiveNumber(intent.minHeight, 6);
const preferredWidth = positiveNumber(intent.preferredWidth, 72);
const minWidth = positiveNumber(intent.minWidth, 36);
const priority = Number.isFinite(intent.priority) ? intent.priority : index;
return {
tileId: stableId(entry.providerId || "interface"),
visible: true,
priority,
x: index % 2,
y: Math.floor(index / 2),
w: 1,
h: Math.max(1, Math.ceil(preferredHeight / 12)),
minWidth,
minHeight,
preferredWidth,
preferredHeight,
density: intent.density || "dense",
viewportMode: intent.viewportMode || "nested-scroll",
signalWeight: intent.signalWeight || 1,
tree: intent.tree || null,
};
}
function fullscreenLayoutIntent(tileId, priority = -100) {
return {
tileId: stableId(tileId || "fullscreen"),
visible: true,
priority,
x: 0,
y: 0,
w: 4,
h: 4,
minWidth: 96,
minHeight: 36,
preferredWidth: 192,
preferredHeight: 48,
density: "comfortable",
viewportMode: "fullscreen",
};
}
function mergeLayoutIntent(existing, entry, interfaces) {
const base = existing && typeof existing === "object"
? { ...existing }
: defaultLayoutFor(entry, interfaces);
const intent = surfaceLayoutIntent(entry);
const fallback = defaultLayoutFor(entry, interfaces);
const denseTree = isDenseLayoutStrategy(intent.layoutStrategy);
const minWidth = positiveNumber(intent.minWidth, positiveNumber(base.minWidth, fallback.minWidth));
const minHeight = positiveNumber(intent.minHeight, positiveNumber(base.minHeight, fallback.minHeight));
const preferredWidth = positiveNumber(intent.preferredWidth, positiveNumber(base.preferredWidth, fallback.preferredWidth));
const preferredHeight = positiveNumber(intent.preferredHeight, positiveNumber(base.preferredHeight, fallback.preferredHeight));
const desiredW = Math.max(1, Math.min(4, Math.ceil(preferredWidth / 72)));
const desiredH = Math.max(1, Math.min(4, Math.ceil(preferredHeight / 12)));
return {
...base,
minWidth,
minHeight,
preferredWidth,
preferredHeight,
h: denseTree && intent.viewportMode !== "fullscreen" ? desiredH : Math.max(positiveNumber(base.h, fallback.h), desiredH),
w: denseTree && intent.viewportMode !== "fullscreen" ? desiredW : Math.max(positiveNumber(base.w, fallback.w), desiredW),
priority: Number.isFinite(intent.priority) ? Math.min(Number(base.priority ?? intent.priority), intent.priority) : base.priority,
density: intent.density || base.density || fallback.density,
viewportMode: intent.viewportMode || base.viewportMode || fallback.viewportMode,
layoutStrategy: intent.layoutStrategy || base.layoutStrategy || fallback.layoutStrategy,
signalWeight: intent.signalWeight || base.signalWeight || fallback.signalWeight || 1,
tree: intent.tree || base.tree || fallback.tree || null,
};
}
function surfaceLayoutIntent(entry) {
const root = entry?.surface?.root;
if (!root || typeof root !== "object") return {};
const layout = root.layout && typeof root.layout === "object" ? root.layout : {};
const props = root.props && typeof root.props === "object" ? root.props : {};
const propLayout = props.layout && typeof props.layout === "object" ? props.layout : {};
const tree = analyzeElementTree(root);
const derived = denseTreeLayoutIntent(tree);
return normalizeDenseSurfaceIntent({ ...derived, ...propLayout, ...layout, tree }, derived);
}
function analyzeElementTree(root) {
const stats = {
elementCount: 0,
leafCount: 0,
branchCount: 0,
maxDepth: 0,
textCells: 0,
listLikeBranchCount: 0,
kindCounts: {},
};
visit(root, 1);
stats.signalWeight = Math.max(1, stats.branchCount * 2 + stats.leafCount + Math.ceil(stats.textCells / 48));
return stats;
function visit(element, depth) {
if (!element || typeof element !== "object") return;
stats.elementCount += 1;
stats.maxDepth = Math.max(stats.maxDepth, depth);
const kind = String(element.kind || "unknown");
stats.kindCounts[kind] = (stats.kindCounts[kind] || 0) + 1;
const text = textValue(element);
stats.textCells += text.length + Math.max(0, text.split(/\r?\n/).length - 1) * 8;
const children = Array.isArray(element.children) ? element.children.filter((child) => child && typeof child === "object") : [];
if (!children.length) {
stats.leafCount += 1;
return;
}
stats.branchCount += 1;
if (children.length >= 4 && children.every((child) => String(child.kind || "") === "text" && !(Array.isArray(child.children) && child.children.length))) {
stats.listLikeBranchCount += 1;
}
for (const child of children) visit(child, depth + 1);
}
}
function denseTreeLayoutIntent(tree) {
const signal = Math.max(1, tree.signalWeight || 1);
const depthPressure = Math.max(0, tree.maxDepth - 2);
const textPressure = Math.sqrt(Math.max(1, tree.textCells) / 18);
const branchPressure = Math.sqrt(Math.max(1, tree.branchCount));
return {
minWidth: Math.min(72, 28 + depthPressure * 7),
minHeight: Math.min(18, 5 + depthPressure * 2),
preferredWidth: Math.min(144, 44 + Math.sqrt(signal) * 7 + depthPressure * 5),
preferredHeight: Math.min(36, 8 + textPressure + branchPressure * 3 + tree.listLikeBranchCount * 2),
density: "dense",
viewportMode: "nested-scroll",
layoutStrategy: "dense-tree",
signalWeight: signal,
};
}
function normalizeDenseSurfaceIntent(intent, derived) {
const viewportMode = intent.viewportMode || derived.viewportMode;
if (viewportMode === "fullscreen") {
return intent;
}
return {
...intent,
minWidth: Math.min(72, positiveNumber(intent.minWidth, derived.minWidth)),
minHeight: Math.min(18, positiveNumber(intent.minHeight, derived.minHeight)),
preferredWidth: Math.min(144, positiveNumber(intent.preferredWidth, derived.preferredWidth)),
preferredHeight: Math.min(36, positiveNumber(intent.preferredHeight, derived.preferredHeight)),
density: intent.density || derived.density || "dense",
viewportMode,
layoutStrategy: intent.layoutStrategy || derived.layoutStrategy || "dense-tree",
signalWeight: intent.signalWeight || derived.signalWeight || 1,
};
}
function isDenseLayoutStrategy(strategy) {
const value = String(strategy || "");
return value === "dense-tree" || value.startsWith("dense-");
}
function textValue(element) {
if (typeof element.text === "string") return element.text;
const props = element.props && typeof element.props === "object" ? element.props : {};
if (typeof props.text === "string") return props.text;
if (typeof props.title === "string") return props.title;
return "";
}
module.exports = {
analyzeElementTree,
createLayoutStore,
defaultLayoutFor,
denseTreeLayoutIntent,
fullscreenLayoutIntent,
mergeLayoutIntent,
surfaceLayoutIntent,
};

View file

@ -1,111 +0,0 @@
"use strict";
const fs = require("fs");
const {
CultCache,
SingleFileMessagePackBackingStore,
defineDocumentRegistry,
defineDocumentType,
} = require("cultcache-ts");
const textDocumentSetDefinition = defineDocumentType({
type: "gamecult.text_document_set",
global: true,
schema: {
parse(value) {
if (!value || typeof value !== "object") {
throw new Error("gamecult.text_document_set must be an object");
}
return value;
},
},
});
async function buildMarqueeText({ interfaces, textDocumentStorePath, stonksBurstSize = 8 }) {
const [securities, stanzas] = await Promise.all([
Promise.resolve(stonksSegments({ interfaces })),
poetryStanzas(textDocumentStorePath),
]);
return stanzaBurstTape(stanzas, securities, stonksBurstSize).join(" / ");
}
function stonksSegments({ interfaces }) {
return stonksSegmentsFromInterfaces(interfaces);
}
function stonksSegmentsFromInterfaces(interfaces) {
const stonks = interfaces.find((entry) => String(entry.providerId || "").toLowerCase() === "stonks.market");
const explicit = String(stonks?.surface?.root?.props?.marqueeText || "").trim();
return splitSegments(explicit).slice(0, 16);
}
async function poetryStanzas(textDocumentStorePath) {
if (!textDocumentStorePath || !fs.existsSync(textDocumentStorePath)) return [];
try {
const cache = CultCache.builder()
.withRegistry(defineDocumentRegistry(textDocumentSetDefinition))
.withGenericStore(new SingleFileMessagePackBackingStore(textDocumentStorePath))
.build();
await cache.pullAllBackingStores();
const documentSet = cache.getGlobal(textDocumentSetDefinition);
return array(documentSet?.documents).flatMap(documentStanzas);
} catch {
return [];
}
}
function documentStanzas(document) {
const stanzas = [];
let stanza = [];
for (const line of array(document?.lines)) {
if (isStanzaMarker(line)) {
pushStanza(stanzas, stanza);
stanza = [];
continue;
}
const text = array(line).join(" / ").replace(/\s+/g, " ").trim();
if (text) stanza.push(text);
}
pushStanza(stanzas, stanza);
return stanzas;
}
function pushStanza(stanzas, stanza) {
if (stanza.length) stanzas.push(stanza.join(" / "));
}
function stanzaBurstTape(stanzas, securities, burstSize) {
const output = [];
const stonkBurstSize = Math.max(1, Number(burstSize) || 8);
const stanzaList = stanzas.length ? stanzas : [];
const securityList = securities.length ? securities : [];
const cycles = Math.max(stanzaList.length, securityList.length ? Math.ceil(securityList.length / stonkBurstSize) : 0);
for (let index = 0; index < cycles; index += 1) {
if (stanzaList.length) output.push(stanzaList[index % stanzaList.length]);
for (let offset = 0; offset < stonkBurstSize && securityList.length; offset += 1) {
output.push(securityList[(index * stonkBurstSize + offset) % securityList.length]);
}
}
return output;
}
function splitSegments(text) {
return String(text || "")
.split(/\s+\/\s+/)
.map((segment) => segment.replace(/\s+/g, " ").trim())
.filter(Boolean);
}
function isStanzaMarker(line) {
const parts = array(line);
return parts.length === 1 && String(parts[0]).trim().toLowerCase() === "[stanza]";
}
function array(value) {
return Array.isArray(value) ? value : [];
}
module.exports = {
buildMarqueeText,
};

View file

@ -1,48 +0,0 @@
"use strict";
const fs = require("fs");
const path = require("path");
const { execFile } = require("child_process");
const { promisify } = require("util");
const execFileAsync = promisify(execFile);
async function dockerSnapshot() {
try {
const { stdout } = await execFileAsync("docker", ["ps", "--format", "{{json .}}"], { timeout: 3000 });
const containers = stdout.split(/\r?\n/)
.filter(Boolean)
.map((line) => JSON.parse(line))
.map((item) => ({
name: item.Names,
image: item.Image,
status: item.Status,
ports: item.Ports,
}));
return { state: "ok", containers, error: "" };
} catch (error) {
return { state: "error", containers: [], error: error.message };
}
}
async function adbSnapshot() {
const adb = path.join(process.env.LOCALAPPDATA || "", "Android", "Sdk", "platform-tools", "adb.exe");
if (!fs.existsSync(adb)) {
return { state: "missing", devices: [], error: `missing ${adb}` };
}
try {
const { stdout } = await execFileAsync(adb, ["devices"], { timeout: 3000 });
const devices = stdout.split(/\r?\n/)
.map((line) => line.match(/^(\S+)\s+(device|offline|unauthorized)$/))
.filter(Boolean)
.map((match) => ({ serial: match[1], state: match[2] }));
return { state: "ok", devices, error: "" };
} catch (error) {
return { state: "error", devices: [], error: error.message };
}
}
module.exports = {
adbSnapshot,
dockerSnapshot,
};

View file

@ -1,278 +0,0 @@
"use strict";
function createLiveProviderRegistry(options = {}) {
const maxAgeMs = Math.max(1_000, Number(options.maxAgeMs || 120_000));
const records = new Map();
function ingestDocument(document, source = {}) {
if (!document?.schemaId || !document?.recordKey) {
return;
}
const now = Date.now();
records.set(`${document.schemaId}:${document.recordKey}`, {
...document,
receivedAtMs: now,
source,
});
prune(now);
}
function snapshot() {
const now = Date.now();
prune(now);
const advertisements = new Map();
const surfaces = new Map();
const commandBoundaries = new Map();
const transportProfiles = new Map();
for (const record of records.values()) {
const payload = normalizeDocumentPayload(record);
switch (record.schemaId) {
case "gamecult.eve.provider_advertisement":
case "gamecult.eve.provider_advertisement.v1":
if (payload?.providerId) {
advertisements.set(String(payload.providerId), { ...record, payload });
}
break;
case "gamecult.eve.surface_state":
case "gamecult.eve.surface_state.v0":
case "gamecult.eve.surface_state.v1":
if (surfaceProviderId(payload)) {
surfaces.set(String(surfaceProviderId(payload)), { ...record, payload });
}
break;
default: {
const providerId = payload.provider_id || payload.providerId;
if (!providerId) break;
if (String(record.schemaId).endsWith(".command_boundary")) {
commandBoundaries.set(String(providerId), payload);
} else if (String(record.schemaId).endsWith(".transport_profile")) {
transportProfiles.set(String(providerId), payload);
}
break;
}
}
}
const providerAdvertisements = [...advertisements.values()]
.map((record) => normalizeProviderAdvertisementRecord(record, commandBoundaries, transportProfiles))
.filter(Boolean)
.sort((left, right) => String(left.id).localeCompare(String(right.id)));
const interfaces = providerAdvertisements
.map((provider) => {
const surfaceRecord = surfaces.get(String(provider.id));
if (!surfaceRecord?.payload?.surface?.root) {
return null;
}
return liveProviderInterface(provider, surfaceRecord.payload, surfaceRecord.source);
})
.filter(Boolean)
.sort((left, right) => String(left.providerId).localeCompare(String(right.providerId)));
return { interfaces, providerAdvertisements };
}
function prune(now = Date.now()) {
for (const [key, record] of records) {
if (now - record.receivedAtMs > maxAgeMs) {
records.delete(key);
}
}
}
return {
ingestDocument,
snapshot,
};
}
function normalizeDocumentPayload(record) {
const payload = record?.payload;
switch (record?.schemaId) {
case "gamecult.eve.surface_state":
case "gamecult.eve.surface_state.v0":
case "gamecult.eve.surface_state.v1":
if (Array.isArray(payload) && payload.length >= 5) {
return {
provider_id: payload[0],
title: payload[1],
version: payload[2],
updated_at: payload[3],
surface: payload[4],
};
}
break;
default:
break;
}
let normalized = payload;
for (let depth = 0; depth < 4; depth += 1) {
if (Array.isArray(normalized) && normalized.length === 1 && normalized[0] && typeof normalized[0] === "object") {
normalized = normalized[0];
continue;
}
if (normalized?.value && typeof normalized.value === "object" && !Array.isArray(normalized.value)) {
normalized = normalized.value;
continue;
}
break;
}
return normalized || {};
}
function surfaceProviderId(surfaceState) {
return surfaceState?.providerId || surfaceState?.provider_id || null;
}
function surfaceTitle(surfaceState) {
return surfaceState?.title || surfaceState?.Title || null;
}
function surfaceVersion(surfaceState) {
return surfaceState?.version || surfaceState?.Version || 0;
}
function surfaceUpdatedAt(surfaceState) {
return surfaceState?.updatedAt || surfaceState?.updated_at || surfaceState?.UpdatedAt || null;
}
function normalizeProviderAdvertisementRecord(record, commandBoundaries, transportProfiles) {
const advertisement = record.payload;
const providerId = advertisement?.providerId || advertisement?.provider?.id || advertisement?.id;
if (!providerId) {
return null;
}
const endpoints = sanitizeProviderEndpoints(advertisement.endpoints);
const routes = sanitizeProviderEndpoints(advertisement.routes);
const providerEndpoint = advertisement.cultMeshAddress
|| providerTypedEndpoint(routes)
|| providerTypedEndpoint(endpoints)
|| typedProviderEndpoint(advertisement.provider?.endpoint)
|| `cultmesh:live:${providerId}`;
return {
id: providerId,
title: advertisement.title || advertisement.provider?.title || providerId,
description: advertisement.description || "Provider-owned CultNet/RUDP advertisement.",
version: String(advertisement.version || 0),
endpoint: providerEndpoint,
canonicalService: advertisement.canonicalService || null,
locatedService: advertisement.locatedService || null,
cultMeshAddress: advertisement.cultMeshAddress || null,
endpoints,
routes,
inputStreams: Array.isArray(advertisement.inputStreams) ? advertisement.inputStreams : [],
activeStreams: Array.isArray(advertisement.activeStreams) ? advertisement.activeStreams : [],
availableSources: Array.isArray(advertisement.availableSources) ? advertisement.availableSources : [],
transportEndpoint: providerTypedEndpoint(routes)
|| typedProviderEndpoint(advertisement.provider?.endpoint)
|| providerEndpoint,
capabilities: advertisement.provider?.capabilities || advertisement.capabilities || [],
operatorState: null,
commandBoundary: commandBoundaries.get(String(providerId)) || null,
transportProfile: transportProfiles.get(String(providerId)) || null,
usesCultMesh: false,
transport: "CultNet/RUDP provider advertisement",
status: advertisement.status || "unknown",
updatedAt: advertisement.updatedAt || record.storedAt || new Date(record.receivedAtMs).toISOString(),
source: sourceLabel(record.source),
commandSurface: advertisement.commandSurface || null,
};
}
function typedProviderEndpoint(endpoint) {
const value = typeof endpoint === "string" ? endpoint.trim() : "";
if (!value) {
return null;
}
return value.startsWith("cultmesh:") ? value : null;
}
function normalizeProviderEndpoints(endpoints) {
if (!Array.isArray(endpoints)) {
return [];
}
return endpoints.map((entry) => {
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
return entry;
}
return {
transport: "witness",
address: String(entry || ""),
};
});
}
function sanitizeProviderEndpoints(endpoints) {
return normalizeProviderEndpoints(endpoints).filter((entry) => {
const transport = String(entry?.transport || "").toLowerCase();
const address = String(entry?.address || "").trim();
if (address.startsWith("cultmesh:")) {
return true;
}
if (transport.includes("cultmesh") || transport.includes("cultcache")) {
return true;
}
return false;
});
}
function providerTypedEndpoint(endpoints) {
return sanitizeProviderEndpoints(endpoints).find((entry) =>
String(entry?.address || "").startsWith("cultmesh:")
)?.address || null;
}
function liveProviderInterface(provider, surfaceState, source) {
const surface = surfaceState.surface || null;
return {
providerId: provider.id,
title: surfaceTitle(surfaceState) || provider.title || provider.id,
state: "active",
detail: `${surface?.root?.kind || surface?.root?.type || "surface"} ${countSurfaceNodes(surface, surfaceState)} nodes via live CultNet/RUDP announcement`,
version: surfaceVersion(surfaceState),
updatedAt: surfaceUpdatedAt(surfaceState) || provider.updatedAt || new Date().toISOString(),
source: sourceLabel(source),
manifest: provider,
canonicalService: provider.canonicalService || null,
locatedService: provider.locatedService || null,
cultMeshAddress: provider.cultMeshAddress || null,
endpoints: sanitizeProviderEndpoints(provider.endpoints),
routes: sanitizeProviderEndpoints(provider.routes),
commandBoundary: provider.commandBoundary || null,
transportProfile: provider.transportProfile || null,
surface,
};
}
function countSurfaceNodes(surface, state = null) {
if (Array.isArray(state?.nodes)) {
return state.nodes.length;
}
if (!surface?.root) {
return 0;
}
let count = 0;
const stack = [surface.root];
while (stack.length > 0) {
const node = stack.pop();
if (!node || typeof node !== "object") continue;
count += 1;
if (Array.isArray(node.children)) {
for (const child of node.children) stack.push(child);
}
}
return count;
}
function sourceLabel(source) {
if (!source?.address) {
return "cultnet-rudp:unknown";
}
return `cultnet-rudp://${source.address}:${source.port}`;
}
module.exports = {
createLiveProviderRegistry,
};

View file

@ -1,216 +0,0 @@
"use strict";
const fs = require("fs");
const { dashboardUnavailable } = require("./interfaces.cjs");
const {
adbSnapshot,
dockerSnapshot,
} = require("./probes.cjs");
const { buildMarqueeText } = require("./marquee.cjs");
const { buildPendingSurface, buildSurface } = require("./surface.cjs");
const { stableId } = require("./utils.cjs");
function createStateBuilder({ cachePath, gamecultTextDocumentStorePath, interfaceDiscovery, layoutStore, stonksBurstSize }) {
let version = 0;
function buildPendingState(message) {
return {
type: "dashboard-state",
schema: "mimir.eve_dashboard_state.v1",
providerId: "odin.providers",
title: "Odin Provider Catalog",
version,
updatedAt: new Date().toISOString(),
selectedNodeId: "coordinator-starting",
lutPreset: "terminal",
nodes: [],
providerCatalog: [],
surface: buildPendingSurface(message),
};
}
async function buildState() {
version += 1;
const observedAt = new Date().toISOString();
const [
docker,
adb,
discovery,
] = await Promise.all([
dockerSnapshot(),
adbSnapshot(),
interfaceDiscovery.discoverAll(),
]);
const discoveredInterfaces = discovery.interfaces;
const providerAdvertisements = discovery.providerAdvertisements;
const interfaceById = new Map(discoveredInterfaces.map((entry) => [entry.providerId, entry]));
const interfaces = [...interfaceById.values()];
const marqueeText = await buildMarqueeText({ interfaces, textDocumentStorePath: gamecultTextDocumentStorePath, stonksBurstSize });
const interfaceSummary = interfaces.map((entry) => `${entry.providerId}:${entry.state}`).join(", ");
const layout = await layoutStore.readLayout();
const verseEvidence = [...providerAdvertisements, ...interfaces];
const voidBotDashboard = interfaceById.get("voidbot.swarm") || dashboardUnavailable("voidbot.swarm", "discovery", "not discovered");
const mimirLiveStats = interfaceById.get("mimir.live.stats") || dashboardUnavailable("mimir.live.stats", "discovery", "not discovered");
const periwinkleServices = [
...adbFaultServices(adb),
];
const verses = [
verse("starfire.local", "Starfire", "coordinator", "active", ["docker", "adb", "eve-provider"], [
service("odin", "Odin all-seer", "active", "asgard.starfire.odin/eve/tui"),
service("docker", "Docker", docker.state === "ok" ? "active" : "warn", `${docker.containers.length} running`),
service("adb", "ADB transport", adbState(adb), adbDetail(adb)),
service("cultcache", "Odin CultCache", fs.existsSync(cachePath) ? "active" : "waiting", require("path").basename(cachePath)),
service("interfaces", "Eve interfaces", interfaces.some((entry) => entry.surface?.root) ? "active" : "waiting", interfaceSummary || "no provider surfaces"),
service("voidbot-swarm", "VoidBot Swarm", voidBotDashboard.state, voidBotDashboard.detail),
service("mimir-live-stats", "Mimir Live Stats", mimirLiveStats.state, mimirLiveStats.detail),
...docker.containers.map((container) => service(`docker-${container.name}`, container.name, "active", container.image)),
]),
verse("nightwing.local", "Nightwing", "dashboard-renderer", bodyStatus(verseEvidence, "nightwing"), ["eve-tui", "gpu-worker"], bodyServices(verseEvidence, "nightwing")),
verse("eve.ipad", "EVE", "ios-client", bodyStatus(verseEvidence, "eve"), ["native-eve"], bodyServices(verseEvidence, "eve")),
periwinkleServices.length ? verse("periwinkle.android", "Periwinkle", "android-client", periwinkleStatus(periwinkleServices), ["sensor-edge", "adb-transport"], periwinkleServices) : null,
verse("raven.local", "Raven", "local-peer", bodyStatus(verseEvidence, "raven"), ["cultmesh-provider"], bodyServices(verseEvidence, "raven")),
verse("yggdrasil.ops", "Yggdrasil", "ops-host", bodyStatus(verseEvidence, "yggdrasil"), ["cultmesh-provider"], bodyServices(verseEvidence, "yggdrasil")),
].filter(Boolean);
return {
type: "dashboard-state",
schema: "mimir.eve_dashboard_state.v1",
providerId: "odin.providers",
title: "Odin Provider Catalog",
version,
updatedAt: observedAt,
selectedNodeId: "verse-starfire.local",
lutPreset: "terminal",
nodes: verses.map((entry, index) => ({
id: `verse-${entry.verseId}`,
label: `${entry.name}\n${entry.role}`,
kind: "cultmesh-verse",
visible: true,
x: -0.8 + (index % 3) * 0.8,
y: -0.45 + Math.floor(index / 3) * 0.55,
z: 0,
rotation: 0,
scale: 1,
width: 0.42,
height: 0.2,
health: entry.status,
detail: entry.capabilities.join(", "),
})),
providerCatalog: providerAdvertisements,
surface: buildSurface({ observedAt, docker, adb, verses, interfaces, layout, marqueeText }),
};
}
return { buildPendingState, buildState };
}
function verse(verseId, name, role, status, capabilities, services = []) {
return { verseId, name, role, status, capabilities, services };
}
function service(id, name, state, detail = "") {
return { id: stableId(id), name, state, detail: String(detail || "") };
}
function bodyStatus(entries, body) {
const matches = bodyEntries(entries, body);
if (!matches.length) return "waiting";
if (matches.some((entry) => providerState(entry) === "active")) return "active";
if (matches.some((entry) => providerState(entry) === "warn")) return "warn";
return "waiting";
}
function bodyServices(entries, body) {
const matches = bodyEntries(entries, body);
if (!matches.length) {
return [service(`${body}-cultmesh-witnesses`, `${titleCase(body)} CultMesh witnesses`, "waiting", "no provider advertisement or interface discovered")];
}
return matches
.slice(0, 8)
.map((entry) => service(
`${body}-${entry.providerId || entry.id || entry.title || "provider"}`,
entry.title || entry.providerId || entry.id || `${titleCase(body)} provider`,
providerState(entry),
providerDetail(entry),
));
}
function bodyEntries(entries, body) {
const token = String(body).toLowerCase();
return entries.filter((entry) => providerBodyText(entry).includes(token));
}
function providerBodyText(entry) {
return [
entry.providerId,
entry.id,
entry.title,
entry.canonicalService,
entry.locatedService,
entry.cultMeshAddress,
entry.verseId,
entry.source,
].filter(Boolean).join(" ").toLowerCase();
}
function providerState(entry) {
const state = String(entry.state || entry.status || "").toLowerCase();
if (["active", "ok", "ready", "running", "published"].includes(state)) return "active";
if (["warn", "warning", "degraded"].includes(state)) return "warn";
if (["failed", "error", "offline"].includes(state)) return "warn";
return entry.surface?.root ? "active" : "waiting";
}
function providerDetail(entry) {
return [
entry.detail,
entry.cultMeshAddress,
entry.locatedService,
entry.source,
].filter(Boolean).join(" / ") || "provider-owned CultMesh discovery";
}
function titleCase(value) {
const text = String(value || "");
return text ? text[0].toUpperCase() + text.slice(1) : "";
}
function adbState(adb) {
if (adb.state !== "ok") return adb.state;
return adbFaultDevices(adb).length ? "warn" : "ok";
}
function adbDetail(adb) {
if (adb.error) return adb.error;
if (adb.devices.length === 0) return "no devices";
return adb.devices.map((device) => `${device.serial}:${device.state}`).join(", ");
}
function adbFaultDevices(adb) {
return adb.devices.filter((device) => device.state !== "device");
}
function adbFaultServices(adb) {
if (adb.state !== "ok") {
return [service("adb-transport", "ADB transport", adb.state, adbDetail(adb))];
}
return adbFaultDevices(adb).map((device) =>
service(`adb-${device.serial}`, "ADB device fault", device.state, `${device.serial}:${device.state}`),
);
}
function periwinkleStatus(services) {
if (services.some((entry) => entry.state === "active")) return "active";
if (services.some((entry) => entry.state === "stale")) return "stale";
if (services.some((entry) => entry.state === "offline" || entry.state === "unauthorized" || entry.state === "error" || entry.state === "missing")) return "warn";
return "waiting";
}
module.exports = {
createStateBuilder,
service,
verse,
};

View file

@ -1,212 +0,0 @@
"use strict";
const { analyzeElementTree, fullscreenLayoutIntent, mergeLayoutIntent } = require("./layout.cjs");
const { stableId } = require("./utils.cjs");
function buildSurface({ observedAt, docker, adb, verses, interfaces, layout, marqueeText = "" }) {
const activeInterfaces = interfaces.filter((entry) => entry?.surface?.root);
return {
schema: "gamecult.eve.surface.v1",
id: "gamecult.network.status.surface",
title: "Odin",
root: {
id: "network-root",
kind: "dashboard",
props: {
title: "Odin Provider Catalog",
observedAt,
summary: `${activeInterfaces.length} Eve surfaces`,
marqueeText,
layout: fullscreenLayoutIntent("odin.providers", -100),
presentation: {
theme: "bifrost-pride",
frame: "animated-rainbow",
dividerMotion: "bifrost-rainbow-dance",
intent: "Odin preserves provider-owned surfaces for catalog/proxy access; Gjallar owns Nightwing overview composition and lowering.",
},
},
children: [
...activeInterfaces.map((entry) => {
const tree = analyzeElementTree(entry.surface.root);
return {
id: `interface-${stableId(entry.providerId)}`,
kind: "interface",
props: {
title: entry.title || entry.providerId,
verseUri: verseUri(entry),
canonicalService: entry.canonicalService || "",
locatedService: entry.locatedService || "",
cultMeshAddress: entry.cultMeshAddress || "",
endpoints: entry.endpoints || [],
routes: entry.routes || [],
providerId: entry.providerId,
source: entry.source,
status: entry.state,
detail: entry.detail,
overviewSignal: hasOverviewSignal(entry),
version: entry.version,
updatedAt: entry.updatedAt,
layout: mergeLayoutIntent(layout.tiles?.[entry.providerId], entry, interfaces),
tree,
packing: {
strategy: "nested-dense-signal",
flattening: "forbidden-when-surface-root-has-children",
listPolicy: "group-or-tree; never render provider signal as a single log/list blob",
},
},
children: [entry.surface.root],
};
}),
],
},
assets: [],
};
}
function hasOverviewSignal(entry) {
const root = entry?.surface?.root;
if (!root) return false;
const providerId = String(entry.providerId || "").toLowerCase();
const explicit = root.props?.overview || entry.manifest?.overview;
if (explicit?.visible === false) return false;
if (explicit?.visible === true || explicit?.signal === "live-ops") return true;
const text = surfaceText(root);
if (providerId.includes("voidbot.swarm")) return /\bctb\b|\bs[0-9.]+\s+h[0-9.]+/i.test(text);
if (providerId.includes("stonks.market")) return stonksHasOverviewSignal(root, text);
if (providerId.includes("mimir.stream.layout")) return false;
if (providerId.includes("spotiverse")) return spotiverseHasOverviewSignal(root, text);
if (providerId.includes("streampixels")) return streamPixelsHasOverviewSignal(root, text);
if (providerId.includes("mimir.live.stats")) return mimirHasOverviewSignal(root, text);
if (/\b(rate limit|playing:\s*none|queue:\s*empty|unavailable|not discovered)\b/i.test(text)) return false;
return hasMetricElement(root) || /\b(users?|viewers?|events?|traffic|ingest|throughput|latency|dropout|queue|pressure|saturation|error|fault|warning)\b/i.test(text);
}
function stonksHasOverviewSignal(root, text) {
if (root.props?.overview?.visible === true) return true;
return /\b(Market pulse|BITCOIN|ETHEREUM|SOLANA|DOGECOIN|equities|crypto|vol|request traffic|source health)\b/i.test(text);
}
function spotiverseHasOverviewSignal(root, text) {
const status = String(root.props?.status || "").toLowerCase();
if (status === "warn" && /\brate limit\b/i.test(text)) return false;
if (/\bplaying:\s*none\b/i.test(text)) return false;
if (/\bqueue:\s*empty|queue:\s*empty or unavailable\b/i.test(text)) return false;
return /\bplaying:\s*(?!none\b).+|\bqueue:\s*(?!empty\b|0\b).+|\brequests?:\s*[1-9]|\bvolume:\s*[1-9]/i.test(text);
}
function streamPixelsHasOverviewSignal(root, text) {
return [
/\b(live\s*)?(users?|viewers?)\s*[:=]?\s*[1-9]/i,
/\bevents?(\s*traffic|\s*\/\s*s|\s*per\s*sec)?\s*[:=]?\s*[1-9]/i,
/\bingest\b.*\b(saturation|pressure|traffic|throughput|[1-9][0-9]*(\.[0-9]+)?)\b/i,
/\b(queue|backlog|pressure)\s*[:=]?\s*[1-9]/i,
/\b(errors?|dropped|5xx)\s*[:=]?\s*[1-9]/i,
/\bthroughput\s*[:=]?\s*[1-9]/i,
].some((pattern) => pattern.test(text));
}
function mimirHasOverviewSignal(root, text) {
return [
/\bdropout\b.*\b[1-9]/i,
/\bstale\s+streams?\s*[:=]?\s*[1-9]/i,
/\blatency\b.*\b([2-9][0-9]{2,}|[1-9]\d+\.\d+ms)\b/i,
/\bconfidence\b.*\b0\.[0-4]/i,
].some((pattern) => pattern.test(text));
}
function surfaceText(root) {
const parts = [];
visitSurface(root, (node) => {
const props = node.props || {};
for (const key of ["title", "label", "text", "status", "detail", "summary", "value"]) {
if (props[key] !== undefined && props[key] !== null) parts.push(String(props[key]));
}
});
return parts.join("\n");
}
function hasMetricElement(root) {
let found = false;
visitSurface(root, (node) => {
if (node.kind === "metric") found = true;
});
return found;
}
function visitSurface(node, visitor) {
if (!node || typeof node !== "object") return;
visitor(node);
for (const child of Array.isArray(node.children) ? node.children : []) {
visitSurface(child, visitor);
}
}
function buildPendingSurface(message) {
return {
schema: "gamecult.eve.surface.v1",
id: "gamecult.network.status.surface",
title: "Odin",
root: stack("root", [pane("Coordinator", [text("status", message)])]),
assets: [],
};
}
function stack(id, children) {
return { id, kind: "stack", props: {}, children };
}
function pane(title, children) {
return { id: stableId(`pane-${title}`), kind: "pane", props: { title }, children };
}
function card(id, children, tone = "default") {
return { id, kind: "card", props: { title: id }, style: { tone }, children };
}
function text(id, value) {
return { id, kind: "text", props: { text: value }, children: [] };
}
function metric(id, label, value, tone) {
return { id, kind: "metric", props: { label, text: `${label}: ${value}`, value, tone }, children: [] };
}
function verseUri(entry) {
if (entry.cultMeshAddress) return String(entry.cultMeshAddress);
if (entry.locatedService) return String(entry.locatedService);
const providerId = String(entry.providerId || "").trim();
if (hasBodyPrefix(providerId)) return providerId;
const body = sourceBody(entry.source);
if (!body || providerId.startsWith(`${body}.`)) return providerId;
return `${body}.${providerId}`;
}
function hasBodyPrefix(providerId) {
return ["starfire", "yggdrasil", "nightwing", "eve", "periwinkle", "raven"].some((body) => providerId.startsWith(`${body}.`));
}
function sourceBody(source) {
const value = String(source || "");
if (value.startsWith("cultmesh:")) return "starfire";
return "";
}
function adbTone(adb) {
if (adb.state !== "ok") return "warn";
return adb.devices.some((device) => device.state !== "device") ? "warn" : "ok";
}
module.exports = {
buildPendingSurface,
buildSurface,
card,
hasOverviewSignal,
metric,
pane,
stack,
text,
};

View file

@ -1,21 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { defineOdinDocuments } = require("../src/odin/documents.cjs");
test("Odin persists Ghostlight's public schema catalog envelope", () => {
const documents = defineOdinDocuments((definition) => definition);
const catalog = documents.ghostlightSchemaCatalogDefinition;
assert.equal(catalog.type, "ghostlight.schema_catalog");
assert.equal(catalog.schemaId, "ghostlight.schema_catalog.v1");
assert.equal(catalog.schemaVersion, "ghostlight.schema_catalog.v1");
assert.equal(catalog.name({ providerId: "gamecult.ghostlight.dungeon" }), "gamecult.ghostlight.dungeon");
});
test("Odin's unavailable-runtime projection includes the Ghostlight catalog slot", () => {
const documents = defineOdinDocuments(null);
assert.equal(documents.ghostlightSchemaCatalogDefinition, null);
});

View file

@ -1,23 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { defineOdinDocuments } = require("../src/odin/documents.cjs");
test("Odin registers the typed Heimdall access discovery documents", () => {
const documents = defineOdinDocuments((definition) => definition);
assert.equal(
documents.evePluginAdvertisementDefinition.schemaId,
"gamecult.eve.plugin_advertisement.v1",
);
assert.equal(
documents.heimdallCommandBoundaryDefinition.schemaId,
"heimdall.command_boundary.v1",
);
assert.equal(
documents.heimdallTransportProfileDefinition.schemaId,
"heimdall.transport_profile.v1",
);
});

View file

@ -1,67 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const test = require("node:test");
const {
IDUNN_HEALTH_CONTRACT,
signedHealthPayload,
signingMessage,
} = require("../src/odin/idunn-rudp.cjs");
test("Odin signs the complete canonical provider-health statement", () => {
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
const publicDer = publicKey.export({ type: "spki", format: "der" });
const rawPublicKey = Buffer.from(publicDer.subarray(publicDer.length - 32));
const signerIdentityId = crypto
.createHash("sha256")
.update(Buffer.from("gamecult.provider-health.identity.v1\0"))
.update(rawPublicKey)
.digest("hex");
const publisher = {
daemonId: "yggdrasil-odin",
healthContract: "odin.cultnet-rudp-provider-health",
privateKey,
publisherIncarnationId: "24e0dc7f-6a92-4aa8-9f56-fb513d265e13",
publisherSequence: 0,
signerIdentityId,
sourceRuntimeId: "odin-yggdrasil",
contract: IDUNN_HEALTH_CONTRACT,
};
const signed = signedHealthPayload(
publisher,
{ state: "active", detail: "catalog current" },
"2026-08-21T12:34:56.789Z",
);
assert.equal(signed.statement[0], "idunn.signed_daemon_health.v1");
assert.equal(signed.statement[1], "yggdrasil-odin");
assert.equal(signed.statement[2], "odin.cultnet-rudp-provider-health");
assert.equal(signed.statement[3], "odin-yggdrasil");
assert.equal(signed.statement[8], 1);
assert.equal(signed.statement[9], 1787315696789);
assert.equal(signed.statement[15].byteLength, 64);
assert.equal(
crypto.verify(
null,
signingMessage(signed.unsignedPayload),
publicKey,
Buffer.from(signed.statement[15]),
),
true,
);
const tampered = Buffer.from(signed.unsignedPayload);
tampered[tampered.length - 1] ^= 1;
assert.equal(
crypto.verify(
null,
signingMessage(tampered),
publicKey,
Buffer.from(signed.statement[15]),
),
false,
);
});

View file

@ -1,29 +0,0 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { createLiveProviderRegistry } = require("../src/odin/provider-ingress.cjs");
test("live provider registry unwraps Rust compatibility records", () => {
const registry = createLiveProviderRegistry();
registry.ingestDocument({
schemaId: "gamecult.eve.provider_advertisement.v1",
recordKey: "muninn.telemetry.nightwing",
payload: [{
value: {
providerId: "muninn.telemetry.nightwing",
title: "Muninn Nightwing Telemetry",
status: "active",
cultMeshAddress: "asgard.nightwing.muninn/telemetry",
capabilities: ["muninn.move_evidence_stream"],
},
}],
}, { address: "10.77.0.3", port: 40000 });
const snapshot = registry.snapshot();
assert.equal(snapshot.providerAdvertisements.length, 1);
assert.equal(snapshot.providerAdvertisements[0].id, "muninn.telemetry.nightwing");
assert.deepEqual(snapshot.providerAdvertisements[0].capabilities, ["muninn.move_evidence_stream"]);
assert.equal(snapshot.providerAdvertisements[0].source, "cultnet-rudp://10.77.0.3:40000");
});