Extract Hermodr from Odin
Hermodr is the Eve browser lowering. It was an organ of Odin: Node inside a Rust
repository, sharing a common module directory with Odin's coordinator, and until
recently running its own provider-session ingress and accepting publications
into a private store.
The ingress was removed first, deliberately. An extraction that carried it would
have moved a second acceptance authority into a repository of its own and called
that progress. What moved here is a bridge: it resolves providers through Odin's
catalog, reads those providers directly, and relays operator intent to
provider-advertised command routes.
Contents: the daemon, the provider subscription source and its lifecycle tests,
the state stream registry, the browser catalog tests, the four lifecycle
scripts, and the persona avatar.
Three dependencies on Odin were resolved rather than carried. The swarm document
catalog now comes from CultLib's cultcache-ts alongside defineDocumentType.
Idunn's health contract is declared locally in idunn-health.cjs, over the generic
transport that also moved to CultLib, with sourceRuntimeId "hermodr" rather than
Odin's. parseArgs is seventeen lines in args.cjs, because Hermodr used one of
that utility module's six functions and carrying the other five would have been
carrying a library nobody here reads.
MIT, matching CultLib, Odin, Idunn, Eve and Erycina, so the deployment chain is
one a creator may lawfully run end to end.
Verified: 17 tests pass, every module loads with CultLib resolving from the new
location, and no import references Odin. Not verified: behaviour against a live
provider over RUDP, which needs a running Verse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:44:53 +02:00
# Hermodr
Hermodr is the Eve browser lowering. It resolves providers through Odin's
catalog, reads those providers for their state, renders their surfaces to
browsers, and relays operator intent back as typed CultMesh command documents.
It owns the browser edge. It does not own truth.
## Authority
- **Providers** own their state and serve it. Hermodr reads; it never accepts.
- **Odin** owns discovery: what exists, what schema speaks it, where to reach
it. Odin is not in the data path, and nothing here reads state * from * Odin.
- **Hermodr** owns session, transport, lowering, and the relay of intent.
Everything it holds about provider state is a projection carrying a sequence
number. Its staleness marks are display facts, not withdrawal decisions.
A lowering runtime cannot make something true by rendering it.
## The round trip
Intent leaves by `POST /hermodr/commands/eve` , which resolves a `cultmesh://`
target from the request or the provider catalog, wraps the body as
`gamecult.eve.command.v1` , stamps `publishedBy: "hermodr-browser-lowering"` , and
publishes to the provider's own advertised route. The provider decides whether
to act. Eve's client refuses any response that is not a correlated
`gamecult.eve.command_receipt.v1` in state `reconciled` .
Consequence returns as newly published provider state, by the same subscription
as every other change. That is the whole point of the split: intent and
consequence travel the same road in opposite directions, and neither passes
through a store Hermodr owns.
## Running it
```bash
npm start -- --host 127.0.0.1 --port 8798
```
Needs a reachable Odin catalog and `CULTLIB_ROOT` pointing at a CultLib
checkout. Provider read cadence is `--provider-poll-interval-ms` , default 2000.
```bash
npm test # unit tests, no live Verse required
npm run check
```
Add static lowering: an Eve surface rendered ahead of time to standalone HTML
The live lowering binds to a provider over CultMesh and updates. That is right
for an operator dashboard and structurally unavailable to everyone else, because
an anonymous visitor to a public page is not a Verse participant and never will
be. Rendering at publish time takes the mesh out of the delivery path, so a
published surface becomes bytes any web server can hand out.
It renders through renderEveSurface, the same function the browser calls, under
jsdom. The alternative was a second emitter walking the same graph, which would
drift from the live path and stay drifted until someone compared two pages side
by side. Eve already renders this way in its own tests, so the harness is
borrowed rather than invented.
Controls that would publish a command are disabled, greyed, and labelled. A
static page has no back-channel and a button that looks live is lying about one.
They are marked rather than removed: deleting them would misrepresent the
surface, when the truth is that the capability exists and is not reachable from
a snapshot.
No daemon. The point of static lowering is that a published surface stops
depending on Hermodr running, and a lowering reachable only by starting a server
would have handed that dependency straight back. jsdom is needed only here and
only at build time, resolving from this repository's install or from the Eve
checkout that already carries it.
29 tests pass. The suite covers the negative checks the extraction plan asked
for: no operable command control survives, no live endpoint appears in the page,
a title carrying markup is escaped rather than injected, and lowering the same
surface twice gives identical bytes, which is what would let a later conformance
check diff static output against the live renderer.
Verified end to end against Erycina's real operator surface: all four panels
render and its Run ingest control arrives inert.
One thing found while writing the fixtures: the renderer silently drops
component kinds it does not know. A fixture inventing one tests nothing, and the
first version of these tests did exactly that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 20:19:24 +02:00
## Static lowering
The live lowering binds to a provider and updates, which is right for an
operator dashboard and structurally unavailable to anyone else: an anonymous
visitor to a public page is not a Verse participant and never will be. Rendering
a surface ahead of time removes the mesh from the delivery path, so the page
becomes bytes any web server can hand out — no mesh, no runtime, no daemon.
```bash
npm run lower -- surface.json --out page.html
cat surface.json | node src/lower-surface.cjs > page.html
```
It renders through the same `renderEveSurface` the browser uses, under jsdom,
rather than through a second emitter walking the same graph. A separate emitter
would drift, and the drift would stay invisible until someone compared two pages
side by side.
**Controls that would publish a command are rendered disabled and labelled.** A
static page has no back-channel; a button that looks live is lying about one.
They are marked rather than deleted, because deleting them would misrepresent
the surface — the capability exists, it just is not reachable from a snapshot.
jsdom is needed only for this, and only at build time. It resolves from
Hermodr's own install if present, otherwise from the Eve checkout, which already
carries it for the renderer's own tests.
Extract Hermodr from Odin
Hermodr is the Eve browser lowering. It was an organ of Odin: Node inside a Rust
repository, sharing a common module directory with Odin's coordinator, and until
recently running its own provider-session ingress and accepting publications
into a private store.
The ingress was removed first, deliberately. An extraction that carried it would
have moved a second acceptance authority into a repository of its own and called
that progress. What moved here is a bridge: it resolves providers through Odin's
catalog, reads those providers directly, and relays operator intent to
provider-advertised command routes.
Contents: the daemon, the provider subscription source and its lifecycle tests,
the state stream registry, the browser catalog tests, the four lifecycle
scripts, and the persona avatar.
Three dependencies on Odin were resolved rather than carried. The swarm document
catalog now comes from CultLib's cultcache-ts alongside defineDocumentType.
Idunn's health contract is declared locally in idunn-health.cjs, over the generic
transport that also moved to CultLib, with sourceRuntimeId "hermodr" rather than
Odin's. parseArgs is seventeen lines in args.cjs, because Hermodr used one of
that utility module's six functions and carrying the other five would have been
carrying a library nobody here reads.
MIT, matching CultLib, Odin, Idunn, Eve and Erycina, so the deployment chain is
one a creator may lawfully run end to end.
Verified: 17 tests pass, every module loads with CultLib resolving from the new
location, and no import references Odin. Not verified: behaviour against a live
provider over RUDP, which needs a running Verse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:44:53 +02:00
## Known shape
Provider state is read by polling, wrapped in lifecycle semantics that emit
snapshot, update, and withdrawn events with sequence numbers. Polling is a first
cut: CultMesh ships `cultMeshPollingDocumentWatcher` for exactly this shape, and
a push subscription belongs here eventually. It does not change the authority,
which is why the swap was made before the move.
One invariant is easy to get wrong and quiet when you do: **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. Read errors are reported and the
previous value stands; only genuine absence withdraws. Both paths are tested.
## History
Hermodr was an organ of Odin, and until recently it ran its own provider-session
ingress and accepted publications into a private store. A surface reached a
browser only if its provider bound to Hermodr specifically, so a provider had to
publish twice: once where the Verse expected it, and once into Hermodr so an
operator could watch their own command land.
That ingress is gone. The extraction happened after it was removed, so what
moved here was a bridge rather than a second acceptance authority wearing one's
name.
`GameCult/Odin` holds the coordinator and the Rust organs. `GameCult/CultLib`
holds the swarm document catalog and the CultNet transports this depends on.