Commit graph

1,323 commits

Author SHA1 Message Date
Meta Vens
5cb2ecbb31 ontology, consumer API, smoke note: enumerated sets match their Rust types
ComponentOp has 34 variants (table rewritten), Precondition 9, ActionMismatch
26, FactStanding 3, EdgeKind 1; CausalBoundary field shapes, PatchAnswer,
and the live state/commit/controller-work schema generations follow the
code. The consumer page lists the ops a confined consumer is refused. The
smoke note points at the Idunn acceptance step instead of the historical
play-agent page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:11:29 +02:00
Meta Vens
64071de685 mvp: enumerated sets name the Rust type that decides them
CommandBody has seven variants, WorldPhase two, EdgeKind one, WorldState
a fixed field set; the page now lists those and marks membership, contract,
archive and general relations as unbuilt. Persistence rows and WorldCommit
fields match journal.rs and lib.rs. The play-agent gate citation points at
the Idunn recipe's acceptance step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:09:50 +02:00
Meta Vens
d3f8975d3d Current-labelled pages cite only live files and state unbuilt features as unbuilt
ontology: drop the two citations of deleted pre-rebuild pages; Session Zero is
named as an unbuilt consumer. mvp: bounded co-op and the archived phase are
marked design intent, since crates/ has neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:02:09 +02:00
Meta Vens
424a20e64b README labels linked design pages honestly; drop dangling system-map citation
The README no longer calls unbuilt or historical pages current design records:
play-agent is historical, session-zero is unbuilt, public-architecture and
articles are unbuilt design and essays, extraction and stock-lenses are closed
records. The authority architecture page loses its citation of a deleted file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:58:09 +02:00
Meta Vens
9de9e53a5f Docs describe the machine on main: README, AGENTS, handoff pointer, live architecture pages
README and AGENTS.md state what exists (sealed kernel, Dungeon, persona
projection, sidecar, browser host) and what does not (the verse, storylets,
cast, episodes). The handoff becomes a pointer to the Eureka mind. The Eve
interface page matches eve.rs (world.create v4, Draft/Active cards, play
card). Live pages cite crates/ghostlight/src/<file> instead of world/*.rs.
The implementation plan gains a historical status line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:50:28 +02:00
Meta Vens
447471825b Drop the dead npm scripts and ignore lines of the parked tree
Removes state:*, prestate:*, *:validate, schema:validate, images:count and
coverage:status (they called the deleted Python tools); keeps dungeon:* and
sdk:*. Description now names the Rust kernel and Dungeon. Drops .gitignore
lines for __pycache__, *.pyc, .venv, state scratch and experiments/images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:45 +02:00
Meta Vens
86977e51f7 Cut six pre-rebuild notes pages
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:13 +02:00
Meta Vens
1f85d1c4d7 Cut 27 pre-rebuild architecture pages and the Aetheria brainstorm
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:13 +02:00
Meta Vens
ea91b33946 Cut the Python store's state/
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:02 +02:00
Meta Vens
a7ed853fcd Cut newspaper output and woodcut assets
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:01 +02:00
Meta Vens
90ec569d53 Cut acceptance, prompts and scripts of the pre-rebuild tree
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:01 +02:00
Meta Vens
62dcc6c5b3 Cut experiments
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:01 +02:00
Meta Vens
058c070a42 Cut pre-rebuild example fixtures
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:01 +02:00
Meta Vens
26623d6d2f Cut the pre-rebuild JSON schemas
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:00 +02:00
Meta Vens
ece28506e6 Cut the Python state store, validators and newspaper typesetter (tools/ except the Eve bridge)
Parked at tag parked/ghostlight-pre-rebuild (ruling ghostlight-verse:ruling:park-pre-rebuild-tree).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:05:00 +02:00
Meta Vens
46df4070a8 Map: Ghostlight verse phase 1, body facts and model page parked/ghostlight-pre-rebuild
Why the 2026-09-30 deploy died (Odin's route), what main's build and
tests say, the play-session findings against main, the live refresh
loop, the host's retired v1 body, and where phase 2's verb sets plug in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:22:47 +02:00
Meta Vens
1e5761d9e0 Target: every event the kernel admits has exactly one render path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:19:26 +02:00
Meta Vens
4441381987 Target: Ghostlight verse campaign (honest and running, factions and cast, the verse loaded)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:02:54 +02:00
Meta Vens
4fee97f089 Map and handoff: Ghostlight deploys from main
Operator ruling 2026-09-30: "Nope, I'd rather be on main". main was
fast-forwarded to the codex/ghostlight-dungeon-mvp tip (eeb3a68) and the
Yggdrasil ghostlight binding now admits refs/heads/main (gamecult-ops
45eb584). Historical cut records keep the branch they landed on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:33:54 +02:00
Meta Vens
eeb3a68042 Steer map and handoff to the live deploy and the fix list
Dungeon is live on Yggdrasil under Idunn v2 (454fc33); the operator ruled
"Stop here, we have shit to fix". Next action is PA.f211-f216 in order,
then the owed Soul pass; legacy-served and deploy-next text retired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:21:30 +02:00
Meta Vens
f23adbbf49 Record the first deploy and play session: PA.f198-PA.f216
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:18:40 +02:00
Meta Vens
454fc33b8e Check Heimdall's session expiry against its refresh expiry, not access
Heimdall's session record lives as long as its refresh token: issueAccessClaim
reports session.expiresAt and refresh.expiresAt as the record's expiry, and
only the signed access claim carries the shorter access expiry. Dungeon
required the session expiry to equal the access claim's exp, so every
completion Heimdall redeemed was refused ("session expiry disagrees with
its signed claim") and no session was ever adopted.

The session expiry must now equal the refresh expiry, and refresh must
outlive the signed access claim. The new test builds the completion in the
shape Heimdall issues; restoring the old comparison fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:37:01 +02:00
Meta Vens
9457410365 Log every refused Eve command with its operation and message
A sign-in completion that Heimdall redeemed was refused by Ghostlight with
no trace outside the browser that received it. The command-result builder
now logs every refusal (not accepted, completed or pending) at warn, with
the operation and the refusal message, never the payload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:35:06 +02:00
Meta Vens
4c83d54964 Send the advertised route transport with the sign-in completion
The page builds heimdall.auth.complete itself rather than through the
vendored client, and its routeHint carried only sourceVersion. Ghostlight
refuses any invocation without the route transport ("invocation metadata
is incomplete"), so every completion was denied before it reached
Heimdall. The page now takes the transport the provider advertises for
ghostlight.eve.commands, as the vendored client takes it from a command
descriptor. Verified on Yggdrasil: the corrected shape reaches Heimdall's
private plane and gets Heimdall's own answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:12:14 +02:00
Meta Vens
e42e1c1b31 Finish browser sign-in by attempt id, and on returning to the tab
Heimdall's Discord callback issues a random completion code distinct from
the attempt id, but the vendored adapter required the two to be equal and
rejected every real return as a malformed witness, so no player could sign
in. Re-vendored from Heimdall e8e2832, which completes by attempt id and
never reads the completion code; the test fixture issues a distinct code as
the server does.

A player who finishes Discord elsewhere and switches back to this tab never
reloads it, so the load-time resume never ran. The page now reruns the same
resume when it becomes visible with an attempt in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:51:20 +02:00
Meta Vens
c5bf090d62 Stop declaring Heimdall as an Idunn dependency
Odin (96a55ec) now computes Ready with an external-operator-binding
dependency not ready, but CultLib's shared contract
(runtime_authority_contracts.rs:797-805) refuses any correlation claiming
Ready while a non-optional dependency is not ready, so Odin rejected every
Ghostlight Active presence (30/30 in the last deploy). The external kind
existed only because Heimdall isn't v2-managed; that reason doesn't survive
contact with Odin's own Ready computation.

Ghostlight's deploy stops declaring Heimdall as an Idunn dependency. The
daemon discovers it through Odin at use instead: HeimdallClient already took
Option<HeimdallDependencyBinding> with None as the non-managed path, so
runtime.rs now passes None directly rather than computing it from a
dependency_bindings field that no longer exists, and the loopback-only,
non-secret-bearing, app-bound HMAC+AES envelope enforcement is unchanged.
When Heimdall becomes a v2 target the dependency returns as `required`
managed.

- deployment/idunn/recipe.toml: deleted the heimdall.command-boundary
  [[dependencies]] block. Ghostlight's own required_environment for
  GHOSTLIGHT_HEIMDALL_APP_SECRET_FILE/GHOSTLIGHT_HEIMDALL_BASE_URL is
  unrelated (Heimdall's HTTP app integration, not the Idunn dependency) and
  stays.
- idunn_health.rs: runtime_dependency_bindings now requires exactly the Odin
  dependency (one, not two). Deleted HEIMDALL_CAPABILITY,
  required_external_dependency and its doc comment, the `heimdall` field on
  RuntimeDependencyBindings, the fixture's external_dependency helper and
  its Heimdall entry, and the recipe-drift test's Heimdall block/assertion
  (now asserts exactly one [[dependencies]] block and fails if a Heimdall
  dependency reappears without the daemon changing to expect it again).
  HeimdallDependencyBinding itself moved to heimdall.rs (see below) rather
  than being deleted outright: it's still the shape
  require_expected_heimdall_boundary checks a discovered boundary against
  when Idunn does supply one, which stays true the moment Heimdall returns
  as a managed dependency.
- heimdall.rs: now owns HeimdallDependencyBinding (moved from
  idunn_health.rs, which no longer has any use for it). No behavior change:
  HeimdallBoundaryLocator::Odin's `expected: Option<HeimdallDependencyBinding>`
  and its None-tolerant checking logic were already exactly the
  "discover through Odin, verify against a managed binding when one exists"
  shape this ruling asks for.
- runtime.rs: expected_heimdall is gone; `HeimdallClient::from_env` now
  takes `None` directly instead of a value computed from a
  dependency_bindings field that no longer exists, with a comment on why.

Mutation: re-added the heimdall.command-boundary [[dependencies]] block to
the recipe -> recipe_matches_the_daemon_it_deploys fails (asserts exactly
one dependency block). Restored; deployment/idunn/recipe.toml is
git-diff-clean after, one clean 7-line deletion.

Verification: cargo test -p ghostlight-dungeon --bin ghostlight-dungeon ->
192 passed, 0 failed, 4 ignored (unchanged count: removed a test helper, not
a test).

Structural delta: 4 files changed, 42 insertions(+), 138 deletions(-) in
Ghostlight (net -96 lines) — a genuine subtraction, not a relocation with
padding; the only addition of substance is the doc comment on the relocated
HeimdallDependencyBinding explaining why it still exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 20:29:00 +02:00
Meta Vens
203dc3658e Retry the initial Active presence through Odin's correlation gap
Odin refuses Active until it has correlated the lease Idunn just granted
("Ready topology state is not supported by correlated evidence"); the
first v2 candidate to get past Warming and the lease died on that single
refusal. Retry it with the helper and window the Warming publish already
uses; the periodic republish already only logs. The cadence constant now
names both presences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:11:26 +02:00
Meta Vens
140445dda8 Let the publisher carry its own custody choice, so the lease-retry tests run on Linux
aca5686's fix was right but its two new tests always called acquire_recent
(custody enforced), and a test lease necessarily lives under a tempdir —
never root-controlled. On the pinned Linux image that failed with "process
write-lease ancestor contains a non-root-controlled directory" instead of
exercising the retry logic; on Windows it passed only because
require_process_write_lease_custody is a deliberate no-op there, which is
exactly the platform gap this whole fix is about. The deploy platform is
Linux, so the tests have to run there for real, not slip through on a
Windows-only no-op.

RuntimePresencePublisher now carries the same enforce_custody choice
ProcessWriteLeaseGuard::acquire_inner already had: true from
from_environment (every production construction path), false from the test
fixture constructor. wait_for_write_lease calls acquire_recent or
acquire_recent_fixture to match. acquire_recent_fixture is no longer
#[cfg(test)]-gated, since production's own wait_for_write_lease source must
reference it (unreachable there, since production publishers always carry
enforce_custody: true, but the symbol has to exist in a release build). No
trait, no env flag — a plain bool threaded through the existing constructor.

Added production_custody_choice_rejects_a_lease_under_a_non_root_tempdir:
builds a publisher the way from_environment does (enforce_custody: true)
via a new fixture_with_state_contract_and_custody helper (the existing
fixture() stays enforce_custody: false, unchanged), points it at the same
empty-revoked-store shape the other two tests use, and asserts
wait_for_write_lease still refuses it with a custody-rejection error rather
than acquiring it. This one is #[cfg(target_os = "linux")]-gated —
correctly, not as a workaround: custody enforcement is architecturally a
root-ownership concept that idunn_health.rs's own code makes a no-op on
every other OS, so there is nothing for the assertion to observe on Windows
regardless of this fix. The two tests from aca5686
(an_empty_revoked_lease_store_times_out_not_not_admitted,
a_lease_written_after_an_empty_revoked_store_still_wins) needed no changes
beyond inheriting enforce_custody: false through fixture() as before — they
were already testing the retry logic, not custody, and now do so honestly on
both platforms since custody enforcement is out of their path entirely.

cargo test -p ghostlight-dungeon --bin ghostlight-dungeon (Windows): 192
passed, 0 failed, 4 ignored, unchanged from aca5686 — the new Linux-gated
test does not compile into this binary here, so it does not appear even as
ignored. Awaiting the Linux run for its count and the custody-rejection
assertion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 20:01:54 +02:00
Meta Vens
aca5686f01 Retry write-lease acquisition, not just its existence check
Live failure under Idunn v2 (deploy up-180cf07f): the candidate died in ~2s
with "process write lease was not admitted: ... zero records". Idunn's own
revocation of a prior candidate's lease leaves the store and its .lock
present but empty (a 22-byte zero-record CultCache file), which
`path.exists() && lock.exists()` cannot distinguish from a real lease.
wait_for_write_lease called acquire_recent exactly once on that signal and
let its failure propagate.

wait_for_write_lease's job is "wait until the exact lease for this
incarnation is admitted", not "wait until something exists at the path". On
each poll where the store exists, it now attempts acquire_recent; on failure
it remembers the error and keeps polling (heartbeats unaffected); only the
outer deadline bails, and that bail carries the last acquisition error as
context for diagnosis. acquire_recent's own exact-match checks are untouched
and stay the authority — nothing here special-cases empty stores by size or
by matching error text, so the same retry covers empty, stale-incarnation,
and not-yet-written stores alike, exactly as asked.

Tests (idunn_health.rs, all run on Windows — decode_write_lease and
acquire_inner's enforce_custody path have a Linux-only branch inside
require_process_write_lease_custody, but that function no-ops on non-Linux,
so this whole path is exercised here too, not gated):
- an_empty_revoked_lease_store_times_out_not_not_admitted: builds a
  present-but-zero-record store via push-then-delete of a throwaway envelope
  (the crate's own public store API, not hand-encoded bytes) — the same
  shape Idunn's revocation leaves. Asserts the wait runs its own full 600ms
  timeout (not an instant "not admitted") and that the timeout error still
  carries "not admitted" in its chain.
- a_lease_written_after_an_empty_revoked_store_still_wins: same empty start,
  but a valid lease for the fixture's own incarnation is written 300ms in
  from a spawned task. Proves the retry re-attempts acquisition on every
  poll, not just the first: the wait returns the written lease.

Mutation: reverted to a single-shot acquire (return on first attempt
regardless of outcome) -> the empty-store test fails in ~1ms with the
pre-fix symptom instead of running its 600ms timeout. Restored; diff-clean
after.

cargo test -p ghostlight-dungeon --bin ghostlight-dungeon: 192 passed, 0
failed, 4 ignored (was 190 before this pass; +2 new tests).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:57:01 +02:00
Meta Vens
a397132223 Tolerate Idunn's activation-publication gap in the daemon's own Warming path
Soul's diagnosis: Idunn publishes Expected and starts the unit before it
observes the running process on a later tick and publishes the activation
(Idunn/src/control_plane.rs). Until that activation exists, Odin refuses
every Warming presence this daemon publishes ("no exact current activation
and provider anchor"), and the daemon's single initial publish_warming()
call, and every heartbeat inside wait_for_write_lease, propagated that
refusal through `?` and killed the process. Muninn tolerates the same gap by
re-publishing every 2s and only logging failures
(Muninn/crates/muninn-daemon/src/main.rs); this cut matches that posture in
Ghostlight, touching only the daemon.

1. initialize_production_admission now retries the initial publish_warming()
   at a 2s cadence (matching Muninn), bounded by the same 120s window
   wait_for_write_lease already used — pulled into one named constant,
   WRITE_LEASE_WAIT_TIMEOUT, reused by both rather than inventing a second
   value. Each retried publish is a genuinely new message, not a duplicate
   Odin would drop: signed_record increments publisher_sequence and refreshes
   observed_at_unix_millis on every call (verified by reading it, not
   assumed). Every failed attempt is logged at warn with its error; the loop
   exits only when the window expires, and returns the accepted presence so
   it carries forward into wait_for_write_lease's recent_warming_proofs
   exactly as the single call used to.

   The retry policy is retry_until_deadline, generic over the attempt
   closure and a sleep/clock seam (no trait hierarchy on the publisher) so
   it's unit-testable without waiting real time out: production passes
   tokio::time::sleep and Instant::now, a test passes a no-op sleep paired
   with a fake clock that only advances when the sleep seam is called.

2. wait_for_write_lease's own heartbeat publish no longer propagates a
   failure through `?`; it logs a warning and keeps waiting, exactly like
   the initial-publish retry, until the wait's own outer deadline.

3. Tests (crates/ghostlight-dungeon/src/runtime.rs, idunn_health.rs):
   - retry_until_deadline_returns_the_first_accepted_value: refused twice
     then accepted succeeds and returns the accepted value, via a fake
     clock — runs in 0.00s, no real waiting.
   - retry_until_deadline_fails_with_the_last_error_after_the_window:
     always-refused fails once the fake clock crosses the deadline,
     carrying the last attempt's own error.
   - a_failed_heartbeat_does_not_abort_the_wait_for_lease: a real fixture
     publisher pointed at a closed port (127.0.0.1:9, the existing fixture
     convention) so every publish fails fast; proves the wait survives past
     one heartbeat interval and only ends at its own outer timeout, not the
     heartbeat's own error. This one uses real timing (~6s) since
     wait_for_write_lease's internal loop has no clock seam and adding one
     was out of scope for this cut; verified by mutation that it would have
     caught the old bug (see below).

Mutations, all verified and reverted (diff-clean after):
   - retry_until_deadline: made the first failure unconditionally fatal
     (`if true || now() >= deadline`) -> both new retry tests fail, the
     first on attempt 1 instead of retrying to attempt 3. Restored.
   - wait_for_write_lease's heartbeat: restored the old `?`-propagating
     form -> a_failed_heartbeat_does_not_abort_the_wait_for_lease fails in
     ~5.6s with "publishing Warming heartbeat ... timed out connecting" (the
     pre-fix symptom), instead of running to the 6s outer deadline. Restored.

Decisions made without a further ruling:
   - Named the shared timeout constant WRITE_LEASE_WAIT_TIMEOUT (both the
     initial-publish retry and wait_for_write_lease's own timeout parameter
     use it) rather than threading one Duration value through both call
     sites by hand.
   - Put retry_until_deadline in runtime.rs, beside
     initialize_production_admission, its only caller — not in
     idunn_health.rs, which owns the publisher/protocol, not retry policy.
   - The heartbeat-tolerance test uses real timing rather than adding a
     clock seam to wait_for_write_lease's own internal loop; that loop's
     shape (poll write-lease file + sleep 100ms + heartbeat check) is
     unrelated to this fix and reworking it for a seam felt like scope creep
     against the operator's instruction to avoid a trait hierarchy.

cargo test -p ghostlight-dungeon --bin ghostlight-dungeon: 190 passed, 0
failed, 4 ignored (was 186 before this pass; +3 new tests here, +1 from
Self's own commits since the last pass).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:38:31 +02:00
Meta Vens
1fd52b7c60 Stop comparing LISTEN_PID to the daemon's own pid
Idunn launches managed targets with PrivatePIDs=yes; systemd writes the
outer-namespace pid into LISTEN_PID, which the daemon cannot observe, so
the first v2 probation process exited at once with "systemd authority FDs
belong to another process". Odin made the same change in 8e16f56.

The exact descriptor count and name order, the root-owned read-only
descriptor checks and the signature verification of their contents bind
the descriptors to Idunn; LISTEN_PID must still be a canonical non-zero
pid. The layout test is now pure and runs on every platform; restoring
the own-pid comparison fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:17:05 +02:00
Meta Vens
9a000e9a30 Declare the build cache environment on every Idunn recipe step
Idunn passes a runner container only the variables its step declares, and
the image's own CARGO_HOME is read-only, so the first v2 build failed
writing cargo's git database for the codex-connector dependency. Rust
steps declare CARGO_HOME and HOME, web steps HOME and NPM_CONFIG_CACHE, as
Odin and Heimdall do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:34:16 +02:00
Meta Vens
4ed5b26688 Retire the Raven-hosted Dungeon launch script
Dungeon runs on Yggdrasil under Idunn v2; Raven hosts only Bonsai and the
link. The scheduled task is unregistered and the secret copies removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:27:19 +02:00
Meta Vens
e1c54c8f28 Fix runtime-bundle gid check and strengthen the recipe-drift test (Soul pass 2)
S1 (critical): require_runtime_bundle and require_root_read_only_file required
gid 0 on the Idunn runtime bundle directory and its documents. Idunn creates
the bundle inside runtime_root, which is setgid to the workload's own group
(so the process write-lease record beside it can share that gid,
Idunn/src/drivers.rs), and hardens only uid 0 + mode (0555 dir, 0444 files) —
never gid. The bundle inherits a real workload group, never 0: the live Odin
bundle is root:odin and Odin accepts it. Every managed daemon on Yggdrasil
would fail this exact check at Warming.

Extracted the rule into two pure functions over (uid, gid, mode) —
is_root_service_read_only_directory and is_root_read_only_file — so it is
testable without a Linux filesystem instead of sitting unexercised behind
#[cfg(target_os = "linux")] the way the real check always has (nothing in
this crate built a chowned/chmodded fixture for it before). gid is accepted
as a parameter but never checked, with a comment saying why. Also routed
require_root_controlled_directory_chain's existing check (which never
required gid 0) through the same directory predicate, for one owner of the
rule instead of two copies of it.

Added root_read_only_accepts_any_gid_and_refuses_group_or_other_write,
covering: any gid accepted, owner-write allowed on directories (0o022 never
checked it), group/other-write refused on both files and directories, wrong
uid refused. Mutation: restored `&& gid == 0` in
is_root_service_read_only_directory, confirmed the any-gid assertions fail,
reverted (diff-clean).

S3: recipe_matches_the_daemon_it_deploys previously matched anywhere in the
recipe text, so it passed with Odin's and Heimdall's dependency kinds
swapped, passed when only [[provides]]'s schema was wrong (the world slot's
own copy was still right), was blind to the world/mesh slot schemas
specifically, and never checked STATE_CONTRACT_SHA256 at all. Added
recipe_blocks/find_block to anchor every assertion to its own table (by id
for state slots, by capability for provides/dependencies), and pinned
EXPECTED_STATE_AND_SLOTS_TEXT beside STATE_CONTRACT_SHA256 — the exact
[state]+[[state.slots]] source text the hash was computed from (LF, matching
the committed git blob, not a Windows checkout's autocrlf) — with a comment
that both constants change together. No TOML parser or msgpack encoder is in
this workspace, so the test cannot recompute the hash itself; pinning the
source text it was computed from is the cheapest honest proxy.

Verified all four of Soul's named mutations now fail correctly and were
reverted (recipe.toml is git-diff-clean after): provides schema changed alone
(world slot untouched) -> fails on the provides block; Odin/Heimdall
dependency kinds swapped -> fails on the Odin block; world slot schema alone
-> fails on the world block (previously the blind spot); an app-sessions
writer/recovery edit -> fails on the STATE_CONTRACT_SHA256 text pin.

Found and fixed one collateral break: my new doc comment for
EXPECTED_STATE_AND_SLOTS_TEXT named `StateDeclaration`, which matched the
existing play::tests::soul_no_forbidden_writer_name_appears_in_dungeons_own_source
pin (it scans non-test production source, including doc comments outside a
`#[cfg(test)]` line, and it forbids the substring "Declaration"). Reworded to
"the recipe's own parsed `[state]` struct" — no functional change.

Verification: cargo test -p ghostlight-dungeon --bin ghostlight-dungeon ->
186 passed, 0 failed, 4 ignored (185 -> 186, +1 for the new pure-predicate
test). cargo test -p ghostlight --lib controllers:: -> 131 passed, 0 failed,
1 ignored, unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:25:12 +02:00
Meta Vens
c716b57730 Cut CodexConnector out of managed admission; fix Heimdall's dependency kind and recipe/binary drift
F1 (critical, Soul): managed admission still bailed without
GHOSTLIGHT_CONTROLLER_CONNECTOR and required Idunn's Expected to carry a
third, connector, dependency that no longer exists anywhere upstream.
idunn_health.rs now reads no connector env, requires exactly Ghostlight's two
real dependencies (Odin, Heimdall), and RuntimeDependencyBindings/
RuntimePresencePublisher carry no connector field. runtime.rs's
open_controller no longer takes an endpoint parameter for a lane it cannot
offer: Dungeon passes None for ConnectorBinding and says why in a comment.
configured_connector_endpoint is deleted. The library's CodexConnector
transport (crates/ghostlight) is untouched, and play.rs's own use of the
codex_connector crate for transcript types is unrelated and untouched.

F3: Heimdall is not an Idunn-v2-managed incarnation (every v2 deploy has
failed; it runs as legacy heimdall.service). Idunn projects an
[[external_capabilities]]-declared provider as provider_authority =
"external-operator-binding" with no provider_expected_projection_sha256
(Idunn's DependencySelection::expected_projection), never the managed shape a
"required" dependency expects. Recipe's Heimdall dependency kind is now
"external-operator-binding"; idunn_health.rs's required_external_dependency
checks exactly that shape. Odin stays a managed shared-infrastructure
dependency.

F2: the recipe disagreed with the binary on every declared contract, so
Warming would fail even with F1/F3 fixed. schema_generation was "world-v2"
against the daemon's "world-v3"; ghostlight.world-service provided
"foundation-v0" against the binary's actual ghostlight.world_state.consumer.v6
/ consumer-v6 (STATE_SCHEMA); controller-work's slot schema was pinned at
"v3" against the code's "v17". Fixed all three against the source constants,
and recomputed idunn_health.rs's STATE_CONTRACT_SHA256 against the corrected
[state] block using Idunn's own canonical_state_contract_sha256 (msgpack +
sha256 of the recipe's StateDeclaration) run against the finalized recipe —
not guessed. Promoted CONTROLLER_WORK_SCHEMA (ghostlight) and STORE_SCHEMA
(ghostlight-dungeon's app_session) from private to pub/pub(crate) so a test
can check itself against them instead of hand-copying version numbers.

Added one Dungeon test, recipe_matches_the_daemon_it_deploys, pinning recipe
strings against the daemon's own constants: state generation, provided
schema/compatibility, all four slot schemas, the exact dependency count and
kinds, and the absence of the retired CodexConnector dependency. No TOML
parser exists in this workspace's Cargo.lock, so it string-matches the exact
values rather than parsing into a struct — it catches value drift but not
field-placement drift a real parse would also catch (documented in the test's
own comment). Mutation-verified: corrupting schema_generation fails it;
reverted (diff-clean).

Verification: cargo test -p ghostlight-dungeon --bin ghostlight-dungeon ->
185 passed, 0 failed, 4 ignored (was 184; +1 for the new test). cargo test -p
ghostlight --lib controllers:: -> 131 passed, 0 failed, 1 ignored, unchanged.
Negative grep for CONNECTOR in crates/ghostlight-dungeon/src: zero survivors.
sudo -n idunn validate on Yggdrasil against the updated recipe + binding:
both ok.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:59:09 +02:00
Meta Vens
06a5edc6b7 Place the acceptance fixture's subjects so the gate can pass
The NoAudience rejection Soul flagged (fresh-workspace-handoff.md) was a
missing position, not a transport problem: all three subjects in
real_local_model_cognition_modes_commit_speech declared position: None, so
the kernel had no audience for the seed speech. Add a Place ("roll-hall")
and set each subject's position to it, copying the pattern already used by
the passing persona-lane fixture (~line 7550). No other test body or
assertion changes.

Verified against real Bonsai over the Raven tunnel: passes in 284.68s,
persona prose "Mara hears Iris. She says, \"I hear you.\"" Mutation
re-verified with open_inference actually reached this time: pointing
LocalBinding at the closed loopback port 127.0.0.1:9 fails at inference
("NarrativePersona acceptance did not complete"), not at NoAudience;
reverted by reverse edit (diff-clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:22:25 +02:00
Meta Vens
22ec118d72 Swap the real cognition acceptance test to the local Bonsai transport
CodexConnector is not a real option for Dungeon inference (operator ruling
2026-09-23): Bonsai 2 reaches Yggdrasil over a restricted reverse SSH tunnel
at loopback 127.0.0.1:18080. Rename
real_codex_connector_cognition_modes_commit_speech to
real_local_model_cognition_modes_commit_speech, open inference with
LocalBinding instead of ConnectorBinding, and read
GHOSTLIGHT_LOCAL_ENDPOINT/GHOSTLIGHT_LOCAL_MODEL_PREFIX in place of the
connector endpoint and credential. Update the Idunn recipe's acceptance step
and service required_environment to match (drop the connector env, add the
elaborator model var the test already required but the recipe omitted), and
drop the gamecult.codex.subscription-inference dependency: Dungeon no longer
depends on CodexConnector for inference. Update the remaining live references
to the old test name in notes/map.

CodexConnector's transport code and the v1 actuator are untouched; both are
separate follow-up cuts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:13:45 +02:00
Meta Vens
6c4a489a10 Steer the map and handoff to the Yggdrasil topology
Operator ruling 2026-09-23: Dungeon on Yggdrasil via Idunn, Bonsai on
Raven over a restricted reverse tunnel on the WireGuard mesh;
CodexConnector is not a Dungeon inference option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:54:40 +02:00
Meta Vens
43f0eb75f7 Record PA.f197: Heimdall documents read as a schema, not as a consumer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:39:57 +02:00
Meta Vens
94ab279ac4 Read Heimdall's documents as its consumer, not its schema (PA.f197)
Dungeon decoded Heimdall's published command boundary and every private
receipt with deny_unknown_fields over the subset of fields it reads.
Heimdall has published owner, lifecycleAuthority, healthPublication,
forbiddenWriters and compatibility on the boundary since June, and a
schema field on every receipt, so sign-in failed at discovery with
"unknown field `owner`" and would have failed again on each receipt.

Heimdall owns those documents; Dungeon now reads the fields it needs and
ignores the rest. The signed PrivateEnvelope, whose shape both sides fix,
stays strict. The new test decodes the shapes Heimdall emits and fails
with the reported error when the boundary is made strict again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:39:29 +02:00
Meta Vens
00475fa400 Record PA.f196: the command result shape the client always rejected
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 14:23:25 +02:00
Meta Vens
0bf36c7a8a Return command results in the shape the contract admits (PA.f196)
The browser validates every result against gamecult.eve.command_result.v1
before it reads a field of it, and that schema is additionalProperties:false
over schema, receipt, transientProjection, pluginPayload and draftDirective.
Dungeon returned a flat object -- state, message, sourceVersion, providerId,
surfaceId, operationId, idempotencyKey, updatedAtUtc at the top level -- so
the client rejected every result whole with "invalid Eve command result: data
must not have additional properties". Sign-in was simply the first command a
player could press.

The per-command detail now rides in the receipt, which is
gamecult.eve.command_receipt.v1 and does admit its own extra fields, so the
sixth argument keeps working as receipt detail rather than a sibling.

The tests read the shape they now produce, and one new test reads the two
contracts off the pinned vendor/eve checkout and checks a real result against
them, rather than restating the allowed keys in an assertion that would only
ever agree with its own author.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 14:22:01 +02:00
Meta Vens
3a56bf3389 Record the Raven playtest bring-up script
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 13:27:45 +02:00
Meta Vens
5d3f296747 Record the pass close-out in evidence and drop the compaction scratch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 11:54:16 +02:00
Meta Vens
ff156f1c0a Point the map at the playtest and the two unbuilt capabilities
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 11:53:51 +02:00
Meta Vens
f256fe011b Reconcile the target with the Body, and say what the Dungeon brief has not built
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 11:53:26 +02:00
Meta Vens
500b13ae74 Write the play agent postmortem
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 11:52:40 +02:00
Meta Vens
9565097c65 Record Soul on Cut 17: the bar is met
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 07:25:57 +02:00
Meta Vens
fe4eb0ed7f Record Cut 17 landing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 07:14:01 +02:00
Meta Vens
07aae19dae Cut 17: the fourth exit decodes before retiring, the card stops going blank mid-round, and a dead accessor is gone (PA.f194-A/D/E)
PA.f194-A: `PlayTurnStore::open`'s catch-all arm retired a row onto its
declared `schema_id` alone, with no decode attempted. An Idunn rollback
across a schema-id move can hand a build a row stamped with a schema id it
has never seen whose bytes still decode cleanly under the current
`PlayTurnStoreState` shape (nothing about the struct changed, only the id).
That arm now attempts the decode first, exactly like the current- and
legacy-schema arms beside it: retires only on a genuine decode failure, and
leaves a readable row untouched with `open` failing loudly instead. The
sidecar's own `reason` field no longer claims "could not be read" when
nothing tried. `retire_unreadable_row`'s own doc comment now states this as
an explicit precondition on its caller. New test
`a_readable_row_declaring_an_unrecognized_future_schema_is_not_retired`
proves the readable case is not retired; the existing
`a_row_declaring_an_unrecognized_future_schema_is_retired_not_refused_forever`
still proves the genuinely undecodable case is. Mutation: reverting the
catch-all arm to retire on the schema id alone (no decode attempt) fails the
new test; reverted after confirming.

PA.f194-D: with the running hint correctly suppressed while a round is
genuinely in flight (PA.f193-D/PA.f194's own `run_in_progress` gate), the
play card rendered zero rows for the whole span between a player submitting
a turn and its own narration/question/refusal landing — a player who
submits sees nothing and has no sign anything is happening. A "resolving"
row now renders whenever `run_in_progress` is true, regardless of `state`,
staying inside invariant 8 (the player's own turn processing, no world
truth). The surviving hint's own text no longer claims "still being
resolved" — false in the only case it renders (`Running`, nothing actively
working on it) — and now says the turn did not finish. New tests
`the_card_shows_a_resolving_row_while_a_round_is_in_flight` and
`the_running_hints_own_text_does_not_claim_the_turn_is_still_in_flight`.
Mutation: removing the resolving-row block, and separately reverting the
hint text, each fails its own new test; reverted after confirming both.

PA.f194-E: `PlayTurnStore::retired_sidecar` (the field cached once at
`open`) has had no production consumer since PA.f193-E moved `/health`'s own
readiness reporting onto `PlayTable::retired_row_sidecar`, which scans the
store's directory fresh on every read instead of trusting a per-process
flag. Deleted the field, its accessor, and the tuple plumbing that carried it
through `open`'s match arms; `retire_unreadable_row`'s own doc comment no
longer credits it with `runtime_readiness` visibility. The two tests that
exercised it now assert on the sidecar file's own presence on disk directly,
which is what they were actually proving.

crates/ghostlight-dungeon --bin ghostlight-dungeon: 182 passed, 0 failed,
4 ignored (186 with --include-ignored and the Eve bridge built).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:11:24 +02:00