[arch] hero_osis: contexts as data, strict resolution, drop auto-generated single-bin #43

Open
opened 2026-04-30 20:11:59 +00:00 by mik-tf · 1 comment
Owner

Premise

Two recurring bugs and one sister issue (#41, #42, lhumina_code/home#203) all trace back to the same architectural choice: the canonical context list lives as a hardcoded CLI default in a binary that is itself auto-generated.

This issue is the architecture review — the durable fix that would make all three sister issues moot.

What's wrong with the current shape

Contexts are data, but live as config

The list root,default,geomind,incubaid,threefold,… describes the actual state of the demo. It is data — populated by the seed step, persisted in OSIS storage, queried by the UI. But the dispatcher learns about the same names from the cli.contexts CLI string default, baked into the binary at compile time. So:

  • Adding a new context requires editing source.
  • Removing a context requires editing source.
  • A fresh deploy with no --contexts flag silently registers the wrong set.
  • Unknown X-Hero-Context headers fall back to root instead of returning a clear error.

The dispatcher silently falls back to root

Even if cli.contexts were correct, the per-context routing layer accepts unknown headers and quietly serves them from root. This is the actual bug behind #41 — MOCK_CONTEXTS expansion only papers over it. The real fix is: unknown context → 400 Bad Request, with a clear message listing valid contexts. Silent fallback is the security-and-correctness footgun, not the missing default list.

The per-domain bin is string-pasted Rust code

crates/hero_osis_server/src/bin/hero_osis.rs is auto-generated by build.rs from per-domain crates. The header literally says "DO NOT EDIT — regenerate via cargo build." Every fix to that file is a time bomb against the next regen (lhumina_code/home#203). String-pasted code generation is also harder to debug, harder to grep, and bypasses the type checker for the seam.

Proposal

Three changes, separable but related:

1. Contexts as data

Boot path:

  • Server starts with no context registry.
  • On first request to OSIS storage, contexts are loaded from a known location (sqlite table, or ~/hero/var/hero_osis/contexts.toml, or via osis.contexts.list RPC against the base domain).
  • New contexts are added via API (osis.contexts.create) or seed step, not by editing CLI defaults.
  • cli.contexts flag is removed or repurposed as a development override only.

2. Strict context resolution

  • X-Hero-Context: <unknown> → 400 Bad Request (or 404 if we want to be HTTP-purist about it). Body lists valid contexts.
  • No silent fallback to root. Ever.
  • Existing callers that rely on no-header → root behavior either get an explicit fallback config flag, or are migrated to send the header.

3. Real workspace composition, not codegen

  • Per-domain crates expose a stable register_handlers(server: &mut OServer) function.
  • The bin is a hand-written file that imports each domain crate and calls its register_handlers. Standard Rust, no codegen, type-checked, greppable.
  • build.rs may still emit data (e.g., a generated registry list, OpenRPC spec) — but never code that is the entry point.

Each of the three is independently shippable:

  • (1) and (2) together make the demo correct without code changes per environment.
  • (3) makes (1) and (2) durable across regenerations and removes a whole class of drift bugs.

Why this is "long-term," not coping

Once shipped:

  • Contexts can be added/removed on a running system. No rebuild, no redeploy, no edit-and-regen.
  • A typo in a context name fails loudly, not silently. Bugs caught in dev, not in browser.
  • The codegen-drift issue (lhumina_code/home#203) loses its biggest case study.
  • The MOCK_CONTEXTS workaround can be deleted entirely.

Acceptance

  • Contexts loaded from persistent storage on server boot, not from CLI defaults.
  • Unknown X-Hero-Context returns 400 (or 404) with explicit error body. Tested.
  • crates/hero_osis_server/src/bin/hero_osis.rs becomes a hand-written file imported from per-domain crates. The "DO NOT EDIT" header is gone. Diff is reviewable in PR.
  • All three sister issues (#41, #42, home#203 — for this case at least) are closable.
  • Migration runbook for environments that relied on the silent-fallback behavior.

Cross-references

  • Closes the architectural root cause behind #41 and #42.
  • Removes the canonical case study for lhumina_code/home#203.
  • Discovered live 2026-04-30 during the contexts-routing investigation on herodemo.

Signed-off-by: mik-tf

## Premise Two recurring bugs and one sister issue (https://forge.ourworld.tf/lhumina_code/hero_osis/issues/41, https://forge.ourworld.tf/lhumina_code/hero_osis/issues/42, https://forge.ourworld.tf/lhumina_code/home/issues/203) all trace back to the same architectural choice: **the canonical context list lives as a hardcoded CLI default in a binary that is itself auto-generated**. This issue is the architecture review — the durable fix that would make all three sister issues moot. ## What's wrong with the current shape ### Contexts are data, but live as config The list `root,default,geomind,incubaid,threefold,…` describes the actual state of the demo. It is data — populated by the seed step, persisted in OSIS storage, queried by the UI. But the dispatcher learns about the same names from the `cli.contexts` CLI string default, baked into the binary at compile time. So: - Adding a new context requires editing source. - Removing a context requires editing source. - A fresh deploy with no `--contexts` flag silently registers the wrong set. - Unknown `X-Hero-Context` headers fall back to root instead of returning a clear error. ### The dispatcher silently falls back to root Even if `cli.contexts` were correct, the per-context routing layer accepts unknown headers and quietly serves them from root. This is the actual bug behind https://forge.ourworld.tf/lhumina_code/hero_osis/issues/41 — MOCK_CONTEXTS expansion only papers over it. The real fix is: unknown context → 400 Bad Request, with a clear message listing valid contexts. Silent fallback is the security-and-correctness footgun, not the missing default list. ### The per-domain bin is string-pasted Rust code `crates/hero_osis_server/src/bin/hero_osis.rs` is auto-generated by `build.rs` from per-domain crates. The header literally says "DO NOT EDIT — regenerate via cargo build." Every fix to that file is a time bomb against the next regen (https://forge.ourworld.tf/lhumina_code/home/issues/203). String-pasted code generation is also harder to debug, harder to grep, and bypasses the type checker for the seam. ## Proposal Three changes, separable but related: ### 1. Contexts as data Boot path: - Server starts with no context registry. - On first request to OSIS storage, contexts are loaded from a known location (sqlite table, or `~/hero/var/hero_osis/contexts.toml`, or via `osis.contexts.list` RPC against the `base` domain). - New contexts are added via API (`osis.contexts.create`) or seed step, not by editing CLI defaults. - `cli.contexts` flag is removed or repurposed as a development override only. ### 2. Strict context resolution - `X-Hero-Context: <unknown>` → 400 Bad Request (or 404 if we want to be HTTP-purist about it). Body lists valid contexts. - No silent fallback to root. Ever. - Existing callers that rely on no-header → root behavior either get an explicit fallback config flag, or are migrated to send the header. ### 3. Real workspace composition, not codegen - Per-domain crates expose a stable `register_handlers(server: &mut OServer)` function. - The bin is a hand-written file that imports each domain crate and calls its `register_handlers`. Standard Rust, no codegen, type-checked, greppable. - `build.rs` may still emit *data* (e.g., a generated registry list, OpenRPC spec) — but never code that is the entry point. Each of the three is independently shippable: - (1) and (2) together make the demo correct without code changes per environment. - (3) makes (1) and (2) durable across regenerations and removes a whole class of drift bugs. ## Why this is "long-term," not coping Once shipped: - Contexts can be added/removed on a running system. No rebuild, no redeploy, no edit-and-regen. - A typo in a context name fails loudly, not silently. Bugs caught in dev, not in browser. - The codegen-drift issue (https://forge.ourworld.tf/lhumina_code/home/issues/203) loses its biggest case study. - The MOCK_CONTEXTS workaround can be deleted entirely. ## Acceptance - [ ] Contexts loaded from persistent storage on server boot, not from CLI defaults. - [ ] Unknown `X-Hero-Context` returns 400 (or 404) with explicit error body. Tested. - [ ] `crates/hero_osis_server/src/bin/hero_osis.rs` becomes a hand-written file imported from per-domain crates. The "DO NOT EDIT" header is gone. Diff is reviewable in PR. - [ ] All three sister issues (#41, #42, home#203 — for this case at least) are closable. - [ ] Migration runbook for environments that relied on the silent-fallback behavior. ## Cross-references - Closes the architectural root cause behind https://forge.ourworld.tf/lhumina_code/hero_osis/issues/41 and https://forge.ourworld.tf/lhumina_code/hero_osis/issues/42. - Removes the canonical case study for https://forge.ourworld.tf/lhumina_code/home/issues/203. - Discovered live 2026-04-30 during the contexts-routing investigation on herodemo. Signed-off-by: mik-tf
mik-tf self-assigned this 2026-04-30 20:11:59 +00:00
Author
Owner

Source location confirmed (session 52)

The line behind the silent context fallback is in crates/hero_osis_server/src/server/unified_server.rs:591-608:

let hero_context = headers
    .get("X-Hero-Context")
    .and_then(|v| v.to_str().ok())
    // … resolution …
    .unwrap_or_else(|| "root".to_string());

Unknown context names degrade silently to "root" rather than returning HTTP 400.

For the strict-resolution implementation, replace unwrap_or_else with an explicit registry lookup against the configured HERO_CONTEXTS list (per D-01) that returns 400 Bad Request (or 404) on unknown context name and propagates the error up. This also closes the silent-fallback variant of hero_osis#42.

Spotted during the docs_hero Phase 1 source-grounded read; reconciliation memo memory/investigation_roadmap_reconciliation.md (session 52).

## Source location confirmed (session 52) The line behind the silent context fallback is in [`crates/hero_osis_server/src/server/unified_server.rs:591-608`](https://forge.ourworld.tf/lhumina_code/hero_osis/src/branch/development/crates/hero_osis_server/src/server/unified_server.rs): ```rust let hero_context = headers .get("X-Hero-Context") .and_then(|v| v.to_str().ok()) // … resolution … .unwrap_or_else(|| "root".to_string()); ``` Unknown context names degrade silently to `"root"` rather than returning HTTP 400. For the strict-resolution implementation, replace `unwrap_or_else` with an explicit registry lookup against the configured `HERO_CONTEXTS` list (per [D-01](https://forge.ourworld.tf/lhumina_code/hero_demo)) that returns `400 Bad Request` (or 404) on unknown context name and propagates the error up. This also closes the silent-fallback variant of [hero_osis#42](https://forge.ourworld.tf/lhumina_code/hero_osis/issues/42). Spotted during the docs_hero Phase 1 source-grounded read; reconciliation memo `memory/investigation_roadmap_reconciliation.md` (session 52).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_osis#43
No description provided.