Vault root: read through an environment lookup, pinned by a test; fix stale seed comment; README says restart

seed_vault_root_from becomes seed_vault_root_from_environment(lookup): the
only place naming GHOSTLIGHT_SEED_VAULT_ROOT. Open passes std::env::var_os.
A test pins that the root comes from that variable only. The comment at the
bridge test's no-world.seed assertion names AppState.seed_vault_root.
README notes the root is read once at startup. Builds: all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 02:15:46 +02:00
commit 367e2eb50f
2 changed files with 52 additions and 16 deletions

View file

@ -38,7 +38,8 @@ context window, so they survive restarts and long stories.
(`crates/ghostlight/src/controllers.rs`). (`crates/ghostlight/src/controllers.rs`).
- Vault evidence for seeding comes from a read-only markdown directory reader - Vault evidence for seeding comes from a read-only markdown directory reader
(`crates/ghostlight/src/vault.rs`). The Dungeon's seed lane reads the (`crates/ghostlight/src/vault.rs`). The Dungeon's seed lane reads the
directory named by `GHOSTLIGHT_SEED_VAULT_ROOT` directory named by `GHOSTLIGHT_SEED_VAULT_ROOT`, read once at startup, so
changing it needs a restart
(`crates/ghostlight-dungeon/src/runtime.rs`). (`crates/ghostlight-dungeon/src/runtime.rs`).
- Other programs can propose changes to the characters they control through - Other programs can propose changes to the characters they control through
`POST /cultnet/world-patch` (`crates/ghostlight-dungeon/src/runtime.rs`, `POST /cultnet/world-patch` (`crates/ghostlight-dungeon/src/runtime.rs`,

View file

@ -95,7 +95,7 @@ struct AppState {
runtime_health: Option<RuntimeHealthOwner>, runtime_health: Option<RuntimeHealthOwner>,
revisions: broadcast::Sender<u64>, revisions: broadcast::Sender<u64>,
fatal: mpsc::UnboundedSender<String>, fatal: mpsc::UnboundedSender<String>,
/// The seed lane's vault root, read once at open (`seed_vault_root_from`). /// The seed lane's vault root, read once at open (`seed_vault_root_from_environment`).
/// The surface offers the seed controls on whether this is `Some`, and /// The surface offers the seed controls on whether this is `Some`, and
/// `seed_once` takes its root from it, so the two cannot disagree about /// `seed_once` takes its root from it, so the two cannot disagree about
/// whether seeding exists on this server. /// whether seeding exists on this server.
@ -448,7 +448,7 @@ pub(crate) async fn run(state_root_binding: Option<PathBuf>) -> anyhow::Result<(
revisions, revisions,
fatal, fatal,
runtime_health: None, runtime_health: None,
seed_vault_root: seed_vault_root_from(std::env::var_os(SEED_VAULT_ROOT_ENVIRONMENT)), seed_vault_root: seed_vault_root_from_environment(|name| std::env::var_os(name)),
}; };
require_no_runtime_custody_failure(&mut fatal_events)?; require_no_runtime_custody_failure(&mut fatal_events)?;
publish_projection(&state).await?; publish_projection(&state).await?;
@ -1707,12 +1707,15 @@ async fn maintain_mesh_projection(state: AppState) {
/// scope inside it and never the root. /// scope inside it and never the root.
const SEED_VAULT_ROOT_ENVIRONMENT: &str = "GHOSTLIGHT_SEED_VAULT_ROOT"; const SEED_VAULT_ROOT_ENVIRONMENT: &str = "GHOSTLIGHT_SEED_VAULT_ROOT";
/// The vault root a configured value names, if any. Open reads /// The vault root the environment names, if any. Open passes the process
/// `SEED_VAULT_ROOT_ENVIRONMENT` once and passes the value here; this is the /// environment as `lookup` and the result lives in `AppState`, so the root is
/// only place that decides what counts as configured, so an empty or /// read once at startup and changing it needs a restart. This is the only place
/// whitespace-only value is no configuration wherever the state is built. /// that names the variable and decides what counts as configured, so an empty
fn seed_vault_root_from(value: Option<std::ffi::OsString>) -> Option<PathBuf> { /// or whitespace-only value is no configuration wherever the state is built.
value fn seed_vault_root_from_environment(
lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> Option<PathBuf> {
lookup(SEED_VAULT_ROOT_ENVIRONMENT)
.filter(|root| !root.to_string_lossy().trim().is_empty()) .filter(|root| !root.to_string_lossy().trim().is_empty())
.map(PathBuf::from) .map(PathBuf::from)
} }
@ -3177,8 +3180,8 @@ mod tests {
let approved = post(&fixture.state, &fixture.cookie, approve_intents.into_iter().next().unwrap()).await; let approved = post(&fixture.state, &fixture.cookie, approve_intents.into_iter().next().unwrap()).await;
assert_eq!(approved["receipt"]["state"], "accepted", "world.approve via the real client: {approved}"); assert_eq!(approved["receipt"]["state"], "accepted", "world.approve via the real client: {approved}");
// No test fixture in this crate configures `GHOSTLIGHT_SEED_VAULT_ROOT`, // No fixture sets `AppState.seed_vault_root`, so the Draft surface
// so the Draft surface offers no seeding to click at all (PA.f213). // offers no seeding to click at all (PA.f213).
let approved_surface = get(&fixture.state, &fixture.cookie, "/api/eve/surfaces/ghostlight.play").await; let approved_surface = get(&fixture.state, &fixture.cookie, "/api/eve/surfaces/ghostlight.play").await;
assert!( assert!(
!serde_json::to_string(&approved_surface).unwrap().contains("world.seed"), !serde_json::to_string(&approved_surface).unwrap().contains("world.seed"),
@ -6680,28 +6683,60 @@ mod tests {
); );
} }
/// The root the server opens with comes from `GHOSTLIGHT_SEED_VAULT_ROOT`
/// and from nothing else the environment holds.
///
/// Mutation: look up another name, or ignore the answer, in
/// `seed_vault_root_from_environment`.
#[test]
fn the_vault_root_is_read_from_the_seed_variable_only() {
use std::ffi::OsString;
let environment = |entries: &'static [(&'static str, &'static str)]| {
move |name: &str| {
entries
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| OsString::from(*value))
}
};
assert_eq!(
seed_vault_root_from_environment(environment(&[(
"GHOSTLIGHT_SEED_VAULT_ROOT",
"/vault"
)])),
Some(PathBuf::from("/vault"))
);
assert_eq!(
seed_vault_root_from_environment(environment(&[
("GHOSTLIGHT_VAULT_ROOT", "/other"),
("HOME", "/home"),
])),
None
);
}
/// What counts as a configured vault root is decided in one place. An /// What counts as a configured vault root is decided in one place. An
/// absent, empty or whitespace-only value is no configuration, so a /// absent, empty or whitespace-only value is no configuration, so a
/// server started with a blank variable offers no seeding and refuses a /// server started with a blank variable offers no seeding and refuses a
/// forged `world.seed` with the same fixed message as an unset one, which /// forged `world.seed` with the same fixed message as an unset one, which
/// never echoes the value it was started with. /// never echoes the value it was started with.
/// ///
/// Mutation: drop the `.trim()` in `seed_vault_root_from` and the /// Mutation: drop the `.trim()` in `seed_vault_root_from_environment` and the
/// whitespace cases below answer `Some`. /// whitespace cases below answer `Some`.
#[tokio::test] #[tokio::test]
async fn a_blank_vault_root_is_no_configuration_and_its_refusal_echoes_nothing() { async fn a_blank_vault_root_is_no_configuration_and_its_refusal_echoes_nothing() {
use std::ffi::OsString; use std::ffi::OsString;
assert_eq!(seed_vault_root_from(None), None); assert_eq!(seed_vault_root_from_environment(|_| None), None);
for blank in ["", " ", " for blank in ["", " ", "
"] { "] {
assert_eq!( assert_eq!(
seed_vault_root_from(Some(OsString::from(blank))), seed_vault_root_from_environment(|_| Some(OsString::from(blank))),
None, None,
"{blank:?} counted as a configured vault root" "{blank:?} counted as a configured vault root"
); );
} }
assert_eq!( assert_eq!(
seed_vault_root_from(Some(OsString::from("/vault"))), seed_vault_root_from_environment(|_| Some(OsString::from("/vault"))),
Some(PathBuf::from("/vault")) Some(PathBuf::from("/vault"))
); );
@ -6721,7 +6756,7 @@ mod tests {
.await; .await;
let draft = fixture.state.world.snapshot().await.unwrap(); let draft = fixture.state.world.snapshot().await.unwrap();
let mut state = fixture.state.clone(); let mut state = fixture.state.clone();
state.seed_vault_root = seed_vault_root_from(Some(OsString::from(" "))); state.seed_vault_root = seed_vault_root_from_environment(|_| Some(OsString::from(" ")));
let denied = post( let denied = post(
&state, &state,
&fixture.cookie, &fixture.cookie,