Only a step's declared `required_environment` is forwarded to the runner
container. Odin's steps declared none, so CARGO_HOME and HOME never arrived
and cargo fell back to the image's read-only /usr/local/cargo, failing to
fetch the rev-pinned CultLib dependency.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Odin's recipe declared `[state]` with a `topology` slot but its launch
contract passed `--store` as an absolute path. Idunn refuses that pairing:
a workload whose recipe declares state must bind a state root, and binding a
state root requires the launch contract to carry a `state_root` argument. No
operator binding for Odin could have been admitted. This never surfaced
because `idunn validate` was run recipe-only, so `admit` never saw it.
The rule is Idunn's on purpose -- it is what makes Idunn the owner of where a
target's state lives, so it can create and harden the root. Odin naming its
own absolute store path routed around that: a binding could have placed the
store outside the root Idunn prepared.
So the daemon now takes `--state-root` and derives the store from the slot
the recipe already declares. The store path is no longer supplied by anyone;
`--store` is rejected, and a test pins that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Static lowering landed in GameCult/Hermodr: a library and CLI rendering a
surface graph to standalone HTML through the same renderer the browser uses,
with command controls inert because a static page has no back-channel.
All five phases of the Hermodr extraction are complete. This document stays as
the record of what left Odin and why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hermodr is a browser lowering, not an all-seer organ. It lived here as Node
inside a Rust repository, sharing a module directory with the coordinator, and
until this week it ran its own provider ingress and accepted publications into a
private store. The ingress went first on purpose: extracting it beforehand would
have relocated a second acceptance authority and called that progress.
Removed: the daemon, its provider subscription source and state stream, both
test files, four lifecycle scripts, the persona avatar, the hermodr:start
script, and src/odin/utils.cjs, whose only remaining consumer was Hermodr and
which it used for one of six functions.
src/odin/ falls from fourteen modules to eleven. The CommonJS surface falls from
sixteen files to twelve. What is left is the coordinator, the Rust organs, and
two adapters declaring contracts other repositories own.
The plan document stays as the record of what left and why. Phase 5, static
lowering, is Hermodr's business now.
5 tests pass, syntax check clean over twelve files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
defineOdinDocuments was 536 lines defining 45 CultCache document types for Eve,
Ghostlight, Heimdall, Idunn, Muninn, Sleipnir, Aetheria, VoidBot and Stonks.
The name was the only part of it that was Odin's.
The catalog is now cultcache-ts's defineSwarmDocuments, verified byte-identical
to what this file produced. What remains here is 26 lines resolving CultLib and
re-exporting under the name Odin's consumers already call, so odin-coordinator
and the two discovery tests are untouched.
This was the last shared dependency between Odin and Hermodr. utils.cjs now has
exactly one consumer, hermodr-daemon, using exactly one of its six functions, so
it leaves with Hermodr rather than staying as a library nobody here reads.
CULTLIB_ROOT is honoured with the sibling walk as fallback, matching the health
adapter and hermodr-daemon.
22 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan said move idunn-rudp.cjs to Idunn. Reading it first showed that would
relocate 311 lines of generic CultNet transport into a Rust repository and hand
Odin a fragile sibling-path dependency. The split was taken instead: transport
to CultLib beside the RUDP session it builds on, constants documented by Idunn,
a 65-line adapter left in Odin declaring that contract.
Phases 1 and 2 are both complete. documents.cjs is now the only genuinely shared
dependency standing between Odin and a future Hermodr repository, which makes it
Phase 3's single remaining question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
idunn-rudp.cjs was 324 lines of which almost none were about Idunn. The
transport moved to CultLib as cultnet-ts's signed-daemon-health module, beside
the RUDP session it was already built on. What is left here is 65 lines: the
three constants naming Idunn's wire contract, and the call shapes Odin's
consumers already use.
The constants are declared, not decided. Idunn defines them in
docs/signed-daemon-health-authority.md, which until today did not state the
connection id at all -- the value describing how to reach Idunn existed only in
this consumer. Declaring two constants against a documented contract is not
duplicated authority; duplicating 311 lines of transport across Odin and a
future Hermodr repository would have been.
The module also stops resolving CultLib by walking up three directories from its
own location. It now honours CULTLIB_ROOT with that walk as a fallback, matching
what hermodr-daemon.cjs already did. The old assumption held only while Odin sat
beside CultLib, and Erycina broke on exactly that when it moved.
The signed-health test now supplies IDUNN_HEALTH_CONTRACT to the publisher it
builds by hand, and still asserts the literal wire values, so a change to the
contract constants fails loudly rather than being absorbed.
Verified: 22 tests pass across six files, including signature verification
against the relocated implementation, and the syntax check is clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 2 landed: Hermodr reads providers through Odin-advertised routes instead
of accepting publications into a private store.
Phase 1 did not, and the plan now says why instead of leaving a specification I
had already found to be wrong. Reading idunn-rudp.cjs before moving it changed
the answer: 13 of its 324 lines mention Idunn and most are error strings. The
Idunn-owned content is two constants. The other 311 lines are generic CultNet
transport, and moving them into a Rust repository with no Node packaging would
also hand Odin a cross-repo sibling-path dependency of exactly the kind that
broke Erycina's provider publisher when that repository moved. It buys nothing
until Hermodr leaves in Phase 4, because both consumers still live here.
Three ranked options recorded. The one I would take splits the transport into
CultLib beside cultnet-ts and leaves Idunn owning its connection ID and schema
name, which removes the sibling-path bug rather than relocating it. That is
larger than this phase assumed and touches CultLib, so it wants a decision
rather than being folded in quietly.
Also dropped idunn:build and idunn:start from package.json. They invoke
cargo -p idunn-daemon, and that crate left Odin with the extraction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hermodr constructed its own OdinLivePublicationSource, opened its own
provider-session ingress on its own bind address with its own token, and served
that private store to browsers. A surface reached a browser only if its provider
bound to Hermodr specifically, which made a required ingress out of something
named a bridge, and meant a provider had to publish twice: once where the Verse
expects it, and once into Hermodr so an operator could watch their own command
land.
Providers own their state. Odin advertises where it lives. Hermodr now resolves
a provider through the catalog and reads that provider directly, which is what
the command path has always done: findProviderCommandRoute looks a route up in
the catalog and publishes intent to the provider's own endpoint. The state leg
now matches the intent leg, and the round trip closes without the provider
publishing anywhere twice.
ProviderSubscriptionSource presents the same narrow port the store did, so
HermodrStateStreamRegistry is untouched and browsers keep their sequence numbers
and stale/reconnected transitions. Its read is injected, which is what makes the
lifecycle semantics testable without a live Verse.
One invariant is worth naming because getting it wrong is silent: a failed read
is not a withdrawal. A provider restart or a dropped packet must not render as
the provider retracting state it still holds, so read errors are reported and
the previous value stands, while genuine absence withdraws. Both are tested.
Route resolution is cached for thirty seconds and the state is not cached at
all, since routes change slowly and state is the thing being watched. A wedged
peer is dropped so the next poll reconnects.
Deleted: the ingress construction, --provider-session-bind and
--provider-session-token, live-publication-source.cjs,
provider-session-ingress.cjs, their test, parseBind, and three imports left
unused. src/odin/ falls from fourteen modules to twelve. The removed options and
env vars now fail loudly through the existing rejectRemovedOptions path in both
spellings parseArgs can produce.
Polling is a first cut, not the destination. CultMesh ships
cultMeshPollingDocumentWatcher for exactly this shape, and the cadence is
--provider-poll-interval-ms. A push subscription belongs here later; it does not
change the authority, which is the point of this commit.
Verified: 22 tests pass across six files, syntax check clean over sixteen
CommonJS files. Not verified: behaviour against a live provider over RUDP, which
needs a running Verse and is an operator step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous revision assigned provider ingress and live-publication acceptance
to Odin, and described Hermodr as a second Odin holding a parallel instance of
the same store. Both claims were wrong, and the second was asserted without
checking.
Odin is a rendezvous organ. It publishes what state is available, what schema
speaks it, and where to reach it. It is not in the data path, and it never
carried this state: neither odin-coordinator.cjs nor any Odin test references
OdinLivePublicationSource or provider-session-ingress.cjs. Those modules are
Hermodr's own state machinery wearing Odin's name and filed under src/odin/,
which is what made it read as sanctioned.
So there is no second Odin. There was never a first one for this state. The cut
is not to hand acceptance back to Odin, which does not want it, but to delete it
and have Hermodr resolve providers through the catalog and subscribe to them
directly.
The command path already models this correctly. findProviderCommandRoute looks a
route up in the catalog and publishes to the provider's own endpoint, and
Erycina's surface names Odin as discoveryOwner while pointing canonicalSurfaceUri
at Erycina's own address. The retrofitted leg got the architecture right; the
original state leg did not.
Dependency picture corrected by actual reference counts. Only documents.cjs and
idunn-rudp.cjs are genuinely shared with the coordinator. utils.cjs,
live-publication-source.cjs, and provider-session-ingress.cjs are Hermodr-only
and leave with it or are deleted, which shrinks src/odin/ from fourteen modules
to ten and removes the documents.cjs decision from the critical path of
everything else.
Negative checks now include the one that matters for the corrected model: with
Odin stopped, existing provider subscriptions must keep delivering. If they do
not, Odin is in the data path and the plan failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hermodr is described as the Eve-to-browser lowering bridge. It is also a second
provider ingress and a second acceptance authority: main() constructs its own
OdinLivePublicationSource, binds its own provider session with its own token,
accepts publications into that private Map, and serves them to browsers. Odin's
coordinator holds a separate instance over the same providers with no
reconciliation. A surface reaches a browser only if its provider binds to
Hermodr, which makes a required ingress out of something named a bridge.
The intent path is not the problem and is kept as-is. POST /hermodr/commands/eve
is generic across providers, wraps intent as gamecult.eve.command.v1, stamps
itself as the relay rather than the author, publishes to the provider's
advertised route, and returns a receipt. Eve's client dispatches controls
through it and refuses anything that is not a correlated receipt in state
reconciled.
The defect is that the loop does not close. Intent leaves by CultMesh to the
owner of the consequence; the consequence returns only through Hermodr's private
ingress. A provider must publish twice for the Verse to be correct and for the
operator to see their own action land. Cutting the acceptance authority is what
closes it: with the state leg subscribed to Odin-accepted state, a command's
effect returns by the same route as every other change.
The plan orders the work so the authority cut happens in place, before any file
moves, because an extraction that carries the split brain into a new repository
has only changed its address. Static lowering lands last and as a library with a
CLI, since emitting bytes from a graph earns no daemon, and requiring one would
reintroduce the dependency static lowering exists to remove.
Records five open dependencies, three phases of subtraction, and negative checks
that prove the old path can no longer accept, no longer override, and no longer
paper over Odin being absent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The extraction removed vendor/cultmesh-rs and left crates/idunn-daemon as an
empty directory shell. Git does not track empty directories, so `git status`
was clean while the tree still differed from HEAD -- the shells are gone now.
Cargo.toml still excluded vendor/cultcache-rs and vendor/cultnet-rs, both
absent since Odin started consuming CultLib. An exclude for a path that does
not exist is a compensator with nothing left to compensate for.
cargo metadata resolves; odin-core still 22/22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Idunn lived as a crate inside the repository of a service it manages, which
inverted the authority it exists to hold. It is now GameCult/Idunn with its
full 136-commit history; this merge removes the old body from Odin.
Cut: crates/idunn-daemon and vendor/cultmesh-rs. Odin keeps its own Idunn
projection (crates/odin-core/src/idunn.rs), its RUDP client, and its recipe
under deployment/idunn -- those are Odin's view of a managed relationship,
not Idunn's authority.
Conflicts resolved:
- crates/idunn-daemon/src/main.rs, scripts/linux/idunn-yggdrasil: deleted,
per the extraction. Both live in GameCult/Idunn.
- docs/architecture.md: took the branch. Main still described Gjallar as
Nightwing-resident under E:\Projects\Gjallar; it runs on Yggdrasil from
/srv/gjallar/current, confirmed against the host.
- src/odin/documents.cjs: union. All three hunks were pure branch-side
additions of the ghostlight.schema_catalog type; markers stripped rather
than taking --theirs, to preserve auto-merged HEAD changes elsewhere in
the file. node --check passes.
Verified: odin-core 22/22 on Windows; odin-daemon type-checks clean for
x86_64-unknown-linux-gnu, which is its real target -- it is unconditionally
Unix (systemd socket activation via File::from_raw_fd) and does not build
on Windows by design.
main is tagged pre-idunn-extraction-main: the last tree containing the
swarm keepalive supervision and the July Bifrost Persona feedback target,
neither of which exists on the branch or in GameCult/Idunn. See the merge
notes for why that matters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository was public but carried no license, so default copyright applied
and all rights were reserved. Readers could look at the code and had no right to
run, copy, modify, or self-host it. GitHub's terms grant forking within GitHub
and nothing past that.
This matters now because Erycina is being prepared for founding creators to
self-host, and its intended one-click deployment installs Idunn to bring up
Odin, Heimdall, and Erycina together. Erycina is MIT. A creator accepting that
deploy would otherwise be handed dependencies they have no license to run, which
makes "self-hosted" true only for us.
MIT to match CultLib, VoidBot, and Eve, and the stated intent for GameCult
projects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the extraction. Unit files and documentation went to the Idunn
repository, where a service's own deployment surface belongs. Operator scripts
went to gamecult-ops, which is where fleet tooling lives.
To Idunn: scripts/linux/idunn-yggdrasil{,.service,.sudoers}, docs/idunn.md,
docs/idunn-deployment-authority.md,
docs/idunn-signed-daemon-health-authority.md, src/Idunn/README.md.
To gamecult-ops/scripts/idunn/: the eight PowerShell and cmd operator tools --
deployment targets, startup install, swarm coverage health, operator alarm
notification, local start.
Staying in Odin, deliberately: crates/odin-core/src/idunn.rs, Odin's own
projection of Idunn state for aggregation; src/odin/idunn-rudp.cjs, its client
for talking to Idunn; test/idunn-signed-health.test.cjs, its own integration
test; and deployment/idunn/recipe.toml, which despite the path builds
odin-daemon and is Odin's recipe expressed in Idunn's format.
docs/transport-shortcut-inventory.md is a dated evidence ledger citing several
of the moved script paths. Its entries are left as written and a path note
added at the top -- rewriting a ledger to match a later reorganisation would
destroy the record it exists to keep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Idunn now lives at https://github.com/GameCult/Idunn with its full 136-commit
history. Odin no longer contains the deployment authority that manages it.
Also removes vendor/cultmesh-rs, the private fork that had diverged from
CultLib in both directions. Its unique capability -- the RUDP document server
and the message_id correlation rework -- was reconciled into CultLib first, so
this deletes a duplicate rather than a capability. The four crates that
consumed it by path (muninn-daemon, odin-core, odin-daemon, sleipnir-daemon)
now consume CultLib at c13b6ba like everything else.
What deliberately stays in Odin: crates/odin-core/src/idunn.rs, which is Odin's
own projection of Idunn state for aggregation and is Odin's to own;
src/odin/idunn-rudp.cjs, its client for talking to Idunn; and
deployment/idunn/recipe.toml, which despite the path is Odin's own deployment
recipe expressed in Idunn's format -- it builds odin-daemon.
odin-core: 22 passed, 0 failed, unchanged across the cut. odin-daemon still
fails to compile on Windows for the pre-existing Unix-only std::os::fd reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IdunnDaemonHealthTrustBindingRecord and its schema constant moved to
cultnet-rs (GameCult/CultLib@c13b6ba). odin-core now imports them from
cultnet_rs instead of defining them, and idunn-daemon/provisioning.rs
pulls all six provider-health symbols from cultnet_rs directly, merged
into its existing cultnet_rs use block. Dropped the odin-core path
dependency from idunn-daemon/Cargo.toml — nothing in the crate reaches
odin_core:: anymore. Bumped the CultLib git rev across all six
consuming manifests to the new commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves all six CultLib pins from cbde940 to ae0ac0d, which carries the
reconciled cultmesh-rs. The delta is three commits and near-nothing in the
packages Odin consumes: a license line, two lines in cultnet-rs replication,
and one additive pub fn.
The dev-dependency is a real pre-existing defect, not fallout from the bump.
odin-core's tests use rmp_serde in crates/odin-core/src/idunn.rs but never
declared it, so the lib-test target has never compiled. Verified pre-existing:
9 errors at the old pin, 8 at the new. With the dep declared, odin-core tests
compile and 22 pass -- including idunn_records_round_trip_through_cultmesh,
which matters for the extraction this branch is preparing.
Note for anyone checking this on Windows: odin-daemon does not build here and
that is expected, not a regression. It imports std::os::fd and from_raw_fd,
which are Unix-only. Identical failure at both pins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The drift was 94 hunks across 4 files, not 94 files as I described it earlier.
muninn-daemon carries almost all of it; sleipnir-daemon needed four lines.
Checked rather than assumed. Ignoring whitespace, the only remaining deltas are
trailing commas rustfmt adds when it wraps an argument list, and braces removed
from single-expression closure bodies. Both are semantics-preserving.
sleipnir-daemon's change is whitespace and nothing else.
Everything except the Unix-only odin-daemon builds; muninn and sleipnir pass 162
tests, with the same pre-existing media-channel failure.
vendor/cultmesh-rs is its own workspace so `cargo fmt` does not reach it, but it
reports no drift of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This had been sitting uncommitted in the worktree. It is real work rather than
stray edits: RouteBinding gains stable_socket and an NginxStreamUdp driver so a
route can be actuated over UDP, control_plane and drivers grow the Idunn-owned
projection and bounded RUDP transport, and crates/odin-daemon arrives as a new
member of the workspace.
The deployment recipe comes with it, including the required_gitlinks fix the
core-stack repoint made necessary: it named vendor/cultnet-rs, which no longer
exists now that CultNet comes from CultLib, so Idunn would have refused to admit
the target. The list is empty because Odin has no submodules left; the core
stack is pinned by revision in Cargo.lock inside the frozen source tree.
Everything except odin-daemon builds here. odin-daemon reads systemd listen
file descriptors through std::os::fd and cannot compile on Windows by design, so
Linux CI is its first real witness. muninn and sleipnir pass 162 tests; the one
failure expects an Unreliable media channel that no CultNet lineage has ever
declared, and predates this.
Committed rather than left in flight. rustfmt reports 94 files across the repo,
which predates this work, so formatting is left for its own pass instead of
being buried in a 4,000-line diff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CultNet submodule and the in-tree CultCache copy are gone. Both now come
from CultLib, and vendor/cultmesh-rs is the only fork left -- it too consumes
CultLib, so exactly one version of each crate spans the graph. That mattered:
mixing sources would have put two cultnet-rs crates in one build, and the types
would not have unified.
The in-flight CultMesh RUDP document server comes with it because it is a
prerequisite, not a coincidence. The previously committed CultMesh calls
assume_connected(), a CultNet method Odin's fork carried and CultLib does not,
so the old code cannot build against CultLib at all. The newer code no longer
calls it. Verified by reverting the file and watching it fail.
Three API differences needed adapting: CultLib's transport options carry a
reconnect_policy the fork lacked (7 sites), its create_ack builds the packet
straight from ack state and so cannot fail, and its transport profile owns each
channel's reliable expiry.
Everything but odin-daemon builds; odin-daemon is Unix-only and does not compile
on this machine either way. muninn and sleipnir pass 162 tests. The one failure,
rudp_media_transport_options_follow_low_latency_profile, expects an Unreliable
media channel while every CultNet lineage including both revisions Odin had
pinned declares it Reliable, so it failed before this change too.
The in-flight idunn-daemon work is deliberately left uncommitted; the committed
tree was verified to build without it. Also left alone: the untracked
odin-daemon crate and deployment recipe, and the root manifest, whose now-dead
vendor excludes are inert. That recipe still requires the vendor/cultnet-rs
gitlink and will refuse admission until that line is dropped -- the fix is in
the working tree, waiting on that directory being tracked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit fixed the target catalog and the operator alarm, but the
actuators the catalog invokes carried the same E: paths. Idunn would have
resolved every target and then failed on the first restart or deploy it ran,
for exactly the reason the alarm failed.
Rebased 41 references across 27 scripts:
- .cmd bodies invoke their sibling .ps1 through %~dp0 instead of an absolute
drive path, and restart-weksa.cmd reaches its sibling repo the same way.
- .ps1 parameter defaults and assignments derive from $PSScriptRoot: one level
up for paths inside Odin, two for sibling repos under the projects root.
Left deliberately alone: $RemoteViliRoot and $StateRoot in restart-vili.ps1.
Both are consumed inside the EncodedCommand block that restart-vili ssh's to
raven, so they describe raven's filesystem, not starfire's. Whether raven also
moved off E: is an operator question, not something to infer from here.
Verified: all 46 scripts under scripts/ parse clean, and the derivation
resolves to F:\Projects\Odin and F:\Projects on this host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The projects root moved from E: to F:, but Idunn's starfire-local surfaces
still named E:\Projects. Local Idunn died on 2026-08-16 with "fatal: cannot
change to 'E:\Projects\Heimdall'" and every one of its 16 operator alarms
also failed (0xfffd0000), because notify-idunn-operator-alarm.cmd shelled to
an E: path that no longer existed. The supervisor was down for 18 days and
could not say so.
Rather than swap one hardcoded drive letter for another, derive the projects
root the way this catalog already derives its repo root:
- notify-idunn-operator-alarm.cmd invokes its sibling via %~dp0.
- notify-idunn-operator-alarm.ps1 resolves Bifrost from the projects root.
- idunn-deployment-targets.ps1 gains $projectsRoot and rebases 19 LocalPath
values onto it. The remote /srv/build/VoidBot path is left alone.
Verified: the catalog resolves 15 of 20 targets to real directories, the
remote target is untouched, and the alarm chain now runs to exit 0 emitting
a valid gamecult.operator_dm_request.v1 under IDUNN_OPERATOR_ALARM_DRY_RUN.
Still broken, not addressed here: stonks, vili, StreamPixels and repixelizer
have no directory under F:\Projects, and the Bifrost publisher's non-dry path
throws "definition.schema.parse is not a function" against the current
CultMesh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>