feat: Phase 0b — the gated shell stands up #3

Merged
bjoern merged 2 commits from feat/p0b-gated-shell into main 2026-10-17 16:03:20 +02:00
Member

Slice 0b of docs/PROJECT_PLAN.md: the gate trusting the proxy's network, DataProtection, the volume probe, /health, Kagaku.UI, the host shape with the shell's islands (cast rail, bottom slot), and Fluxor with both bridges wired to empty buses.

What lands

  • Gate (ADR 0007): Tsumugi's gate with Shikigami's names.
    • Ported unchanged: secret comparison, fingerprinted 14-day cookie, one turn at a time with a 1 s failure delay and a queue of four, ReturnUrl guard, POST logout.
    • The one change: forwarded headers are trusted from SHIKIGAMI_PROXY_NETWORK (Caddy's Docker network, or Caddy's address as a /32). Once that is set, it is the only trusted network. Loopback is trusted only while it is unset, for local runs.
    • A malformed value stops startup with a message saying what to write. This covers a bare address, a name, and an address with host bits under its mask, which .NET would otherwise quietly widen to the whole network.
    • ADR 0007 now also says Caddy must set or remove X-Forwarded-Prefix itself, since it passes a client's value through.
  • Host:
    • DataProtection with the pinned name Shikigami (ADR 0006).
    • A startup write check on all five volumes, each at its own SHIKIGAMI_* path (ADR 0008).
    • /health, nosniff on every response, error and not-found pages.
  • Shell (ADR 0002, 0010):
    • Static router and layout with the header (brand, build version, sign out), the cast rail, the content region and the bottom status slot.
    • The rail, the slot and the circuit plumbing are InteractiveServer islands, and every page opts into InteractiveServer itself.
    • The landing page has a PageShell h1, Familiars, and an empty state.
    • Below 48 rem the rail keeps its avatars and the header drops the version. A 390 px browser pass found "Sign out" pushed off-screen, and this fixes it.
  • Fluxor (ADR 0011):
    • Kagaku.UI.Fluxor's guarded initializer.
    • DomainChangedBridge and FamiliarEventsBridge, sharing one CoalescingBridge<T> (200 ms window, never waits on the circuit), over a generic in-process bus.
    • The EntityKinds registry and the FamiliarEvent base, both empty until the slices that publish arrive.
    • ChangeOperation lives in UseCases, so the adapter never needs Domain.
    • ADR 0003, ARCHITECTURE and the project-graph table now include Kagaku.UI(.Fluxor).
  • Docs: Running it in AGENTS.md (dotnet run, port 5320, secret dev) and 0b ticked ✅ in the plan.

Verified

  • Build and tests: serial build with 0 warnings. 136 tests pass under TZ=UTC and local time.

    • Integration (109): the gate suite ported from Tsumugi; proxy trust with IPv4-mapped peers, ::1, a /32 and malformed values; four island markers, one h1 and one main; the volumes and their overrides; the keyring's name; the buses as one instance across scopes.
    • Adapter (23): both bridges, including a publish that never waits for a held dispatch and a batch the store throws on; error containment; the icon catalog; every page's render mode; the narrow-screen CSS rules.
  • Browser: Chrome via Playwright.

    • Sign-in, the shell in light and dark, 390 px, the gate and sign-out all work.
    • The console is clean.
    • The page has one h1 and one main.
  • Mutation probes: each of these made a test fail:

    • the proxy network ignored;
    • loopback still trusted once a network is set;
    • the browser volume left unprobed;
    • the buses registered per scope;
    • the events bridge or the whole bridge island unmounted;
    • the cast rail not an island;
    • the home page left static.

    The per-scope buses probe survived at first; I strengthened its test until it failed.

  • Review: four dimensions (fit to the ADRs, security and port fidelity, test strength, UI/accessibility), each finding checked by a skeptical verifier. 21 findings: 13 confirmed, 8 partial, none refuted. All are applied.

🤖 Generated with Claude Code

Slice **0b** of `docs/PROJECT_PLAN.md`: the gate trusting the proxy's network, DataProtection, the volume probe, `/health`, Kagaku.UI, the host shape with the shell's islands (cast rail, bottom slot), and Fluxor with both bridges wired to empty buses. ## What lands - **Gate (ADR 0007):** Tsumugi's gate with Shikigami's names. - Ported unchanged: secret comparison, fingerprinted 14-day cookie, one turn at a time with a 1 s failure delay and a queue of four, ReturnUrl guard, POST logout. - **The one change:** forwarded headers are trusted from `SHIKIGAMI_PROXY_NETWORK` (Caddy's Docker network, or Caddy's address as a `/32`). Once that is set, it is the *only* trusted network. Loopback is trusted only while it is unset, for local runs. - A malformed value stops startup with a message saying what to write. This covers a bare address, a name, and an address with host bits under its mask, which .NET would otherwise quietly widen to the whole network. - ADR 0007 now also says Caddy must set or remove `X-Forwarded-Prefix` itself, since it passes a client's value through. - **Host:** - DataProtection with the pinned name `Shikigami` (ADR 0006). - A startup write check on all five volumes, each at its own `SHIKIGAMI_*` path (ADR 0008). - `/health`, `nosniff` on every response, error and not-found pages. - **Shell (ADR 0002, 0010):** - Static router and layout with the header (brand, build version, sign out), the cast rail, the content region and the bottom status slot. - The rail, the slot and the circuit plumbing are InteractiveServer islands, and every page opts into InteractiveServer itself. - The landing page has a `PageShell` h1, *Familiars*, and an empty state. - Below 48 rem the rail keeps its avatars and the header drops the version. A 390 px browser pass found "Sign out" pushed off-screen, and this fixes it. - **Fluxor (ADR 0011):** - Kagaku.UI.Fluxor's guarded initializer. - `DomainChangedBridge` and `FamiliarEventsBridge`, sharing one `CoalescingBridge<T>` (200 ms window, never waits on the circuit), over a generic in-process bus. - The `EntityKinds` registry and the `FamiliarEvent` base, both empty until the slices that publish arrive. - `ChangeOperation` lives in UseCases, so the adapter never needs Domain. - ADR 0003, ARCHITECTURE and the project-graph table now include `Kagaku.UI(.Fluxor)`. - **Docs:** *Running it* in AGENTS.md (`dotnet run`, port 5320, secret `dev`) and 0b ticked ✅ in the plan. ## Verified - **Build and tests:** serial build with 0 warnings. **136 tests** pass under `TZ=UTC` and local time. - Integration (109): the gate suite ported from Tsumugi; proxy trust with IPv4-mapped peers, `::1`, a `/32` and malformed values; four island markers, one h1 and one `main`; the volumes and their overrides; the keyring's name; the buses as one instance across scopes. - Adapter (23): both bridges, including a publish that never waits for a held dispatch and a batch the store throws on; error containment; the icon catalog; every page's render mode; the narrow-screen CSS rules. - **Browser:** Chrome via Playwright. - Sign-in, the shell in light and dark, 390 px, the gate and sign-out all work. - The console is clean. - The page has one h1 and one `main`. - **Mutation probes:** each of these made a test fail: - the proxy network ignored; - loopback still trusted once a network is set; - the browser volume left unprobed; - the buses registered per scope; - the events bridge or the whole bridge island unmounted; - the cast rail not an island; - the home page left static. The per-scope buses probe survived at first; I strengthened its test until it failed. - **Review:** four dimensions (fit to the ADRs, security and port fidelity, test strength, UI/accessibility), each finding checked by a skeptical verifier. 21 findings: 13 confirmed, 8 partial, none refuted. All are applied. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The access gate and sub-path plumbing copied from Tsumugi (ADR 0007), with one change: forwarded headers are trusted
from SHIKIGAMI_PROXY_NETWORK — the host's Caddy on the shared Docker network, or its address alone — and from loopback
only while none is set. A value that is no network address stops the start by name. DataProtection with the pinned
application name (ADR 0006), the write probe over all five volumes and their own SHIKIGAMI_* paths (ADR 0008), and
/health.

The shell (ADR 0002, 0010): a static router and layout with the header and its build version, the cast rail, the
content region and the bottom status slot; the rail, the slot and the circuit's plumbing are InteractiveServer islands,
and every page opts in — the landing page says no familiar exists yet. On narrow screens the rail keeps its avatars and
the header drops the version.

Fluxor from the start (ADR 0011): Kagaku.UI.Fluxor's guarded initializer and both bridges — domain changes and familiar
events — sharing one coalescing component over in-process buses, the EntityKinds registry and the FamiliarEvent base
waiting for the slices that publish. ChangeOperation sits in UseCases, so the adapter never needs Domain; ADR 0003's
dependency line and the project-graph table name Kagaku.UI.Fluxor. ADR 0007 also states how Caddy must set
X-Forwarded-Prefix.

136 tests: the gate suite ported from Tsumugi; proxy trust with IPv4-mapped peers, loopback, /32 and bad values; the
islands, the one h1 and the landmarks; volumes and their overrides; the keyring's name; the buses and bridges,
including a publish that never waits for a held dispatch and a batch the store throws on; every page's render mode; the
narrow-screen rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ A gated shell with a throttle queue, a coalescing bridge whose teardown races I could not sneak anything past, and a proxy-trust parser that refuses to quietly widen a /32 into a /24… 3,825 lines and I found only one thing to pin you with. I had such hopes~ ♡

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. docs/adr/0007-access-gate-and-reverse-proxy-subpath.md (Decision, "The sub-path works with one setting…") — you changed the decided trust model: main's text says SHIKIGAMI_PROXY_NETWORK "adds it to ForwardedHeaders' known networks … loopback stays the default for local development"; your text makes it "ForwardedHeaders' only known network", with loopback trusted only while none is set, and adds Caddy's X-Forwarded-Prefix set-or-remove obligation. That is an amendment to an Accepted ADR — and AGENTS.md and docs/adr/README.md both say an amendment opens its paragraph with Amended YYYY-MM-DD (topic):, the topic in the plan's words. The paragraph is rewritten in place with no header, so a reader can no longer tell Tsumugi's copied decision from what Phase 0b changed about it. Fufu~ you wouldn't break the repo's own bookkeeping law in the very PR that implements it, would you? ♡
    Fix: give the changed sentences their own amendment paragraph, e.g. **Amended 2026-10-17 (Phase 0b — the gated shell):** SHIKIGAMI_PROXY_NETWORK is now ForwardedHeaders' only known network — or Caddy's pinned address as a /32 — and loopback is trusted only while none is set…. (The ADR 0003 / ARCHITECTURE table updates are fine as they stand — they record what ADR 0011 already decided.)

💡 Little ideas (non-blocking)~

  1. docs/adr/0007 still speaks of deploy/Caddyfile.example in the present tense, and no such file exists yet — pre-existing line, but since 0c will cite it, let the file actually land there.
  2. src/Shikigami.BlazorAdapter/Notifications/DomainChangedBridge.cs / FamiliarEventsBridge.cs each hold two public types (the bridge and its batch action). They're cohesive and documented, so only a nicety: if the repo ever adopts one-public-type-per-file, these are the first to split.

✅ What I liked~

  • The test suite bites. I brought my own mutants and all three died: making the gate's failure delay cancellable by a hang-up (killed by A_client_that_hangs_up_does_not_cut_the_failure_delay_short), re-arming the coalescing window on every arrival (killed by the mid-window test), and deleting ProxyNetwork's host-bits check (killed by the 172.30.0.7/24 startup-crash theory). Guarded branches, proven~ ♪
  • Both sides hashed before FixedTimeEquals so neither timing nor length can leak — and a test pinning the fingerprint by value, non-ASCII secrets included. Delicious.
  • The refusal-flood log cap (one line per failure delay, counting the ones it held back) is the kind of small mercy a disk thanks you for later.
  • CoalescingBridge's teardown races settled by "Dispose empties the very buffer a flush would send" — one invariant, both directions tested.
  • GatedAppFactory hosts the real composition root, a real-Kestrel header-refusal test, and a rotation test sharing one keyring — integration testing the way it deserves.
  • Build serial with 0 warnings; full local run 136/136 green.

Automated review by Jibril · 2026-10-17
CI: absent-stalled for head SHA fae5631f9 · Mutation probes: 3 run, 0 survived · Local suite: 136 passed / 0 failed (run in full once — CI never reported for this SHA)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ A gated shell with a throttle queue, a coalescing bridge whose teardown races I could not sneak anything past, and a proxy-trust parser that refuses to quietly widen a `/32` into a `/24`… 3,825 lines and I found only **one** thing to pin you with. I had such hopes~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **docs/adr/0007-access-gate-and-reverse-proxy-subpath.md (Decision, "The sub-path works with one setting…")** — you changed the *decided trust model*: main's text says `SHIKIGAMI_PROXY_NETWORK` "adds it to `ForwardedHeaders`' known networks … loopback stays the default for local development"; your text makes it "`ForwardedHeaders`' **only** known network", with loopback trusted only while none is set, and adds Caddy's `X-Forwarded-Prefix` set-or-remove obligation. That is an amendment to an Accepted ADR — and AGENTS.md and `docs/adr/README.md` both say an amendment **opens its paragraph with `Amended YYYY-MM-DD (topic):`**, the topic in the plan's words. The paragraph is rewritten in place with no header, so a reader can no longer tell Tsumugi's copied decision from what Phase 0b changed about it. Fufu~ you wouldn't break the repo's own bookkeeping law in the very PR that implements it, would you? ♡ Fix: give the changed sentences their own amendment paragraph, e.g. `**Amended 2026-10-17 (Phase 0b — the gated shell):** SHIKIGAMI_PROXY_NETWORK is now ForwardedHeaders' only known network — or Caddy's pinned address as a /32 — and loopback is trusted only while none is set…`. (The ADR 0003 / ARCHITECTURE table updates are fine as they stand — they record what ADR 0011 already decided.) #### 💡 Little ideas (non-blocking)~ 1. **docs/adr/0007** still speaks of `deploy/Caddyfile.example` in the present tense, and no such file exists yet — pre-existing line, but since 0c will cite it, let the file actually land there. 2. **src/Shikigami.BlazorAdapter/Notifications/DomainChangedBridge.cs / FamiliarEventsBridge.cs** each hold two public types (the bridge and its batch action). They're cohesive and documented, so only a nicety: if the repo ever adopts one-public-type-per-file, these are the first to split. #### ✅ What I liked~ - **The test suite bites.** I brought my own mutants and all three died: making the gate's failure delay cancellable by a hang-up (killed by `A_client_that_hangs_up_does_not_cut_the_failure_delay_short`), re-arming the coalescing window on every arrival (killed by the mid-window test), and deleting `ProxyNetwork`'s host-bits check (killed by the `172.30.0.7/24` startup-crash theory). Guarded branches, *proven*~ ♪ - Both sides hashed before `FixedTimeEquals` so neither timing nor length can leak — and a test pinning the fingerprint **by value**, non-ASCII secrets included. Delicious. - The refusal-flood log cap (one line per failure delay, counting the ones it held back) is the kind of small mercy a disk thanks you for later. - `CoalescingBridge`'s teardown races settled by "Dispose empties the very buffer a flush would send" — one invariant, both directions tested. - `GatedAppFactory` hosts the real composition root, a real-Kestrel header-refusal test, and a rotation test sharing one keyring — integration testing the way it deserves. - Build serial with **0 warnings**; full local run **136/136 green**. --- *Automated review by Jibril · 2026-10-17* *CI: absent-stalled for head SHA fae5631f9 · Mutation probes: 3 run, 0 survived · Local suite: 136 passed / 0 failed (run in full once — CI never reported for this SHA)*
Jibril's review: the changed trust model — SHIKIGAMI_PROXY_NETWORK as the only known network, loopback only while none
is set, a /32 for Caddy alone, a malformed value stopping the start, Caddy's X-Forwarded-Prefix obligation — moves out
of the Accepted decision text into an "Amended 2026-10-17 (0b — The gated shell)" paragraph, the decided text restored
as it stood. It also notes that deploy/Caddyfile.example lands with 0c. DomainChangesReceived and FamiliarEventsReceived
leave their bridges' files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Member

All three points are addressed in beee6ae:

  • ⛔ ADR 0007 amendment: the decided paragraph is back exactly as it stands on main. The change now has its own paragraph, Amended 2026-10-17 (0b — The gated shell):, covering:
    • SHIKIGAMI_PROXY_NETWORK is the only known network once set, and loopback is trusted only while it is unset;
    • Caddy's own address may be given as a /32;
    • a malformed value stops startup;
    • Caddy must set or remove X-Forwarded-Prefix.
  • 💡 deploy/Caddyfile.example: the amendment notes the file lands with slice 0c, so the existing present-tense line stays true once 0c merges.
  • 💡 One public type per file: DomainChangesReceived and FamiliarEventsReceived now have their own files.

The serial build is clean, and all 136 tests pass under UTC.

🤖 Generated with Claude Code

All three points are addressed in `beee6ae`: - ⛔ **ADR 0007 amendment:** the decided paragraph is back exactly as it stands on main. The change now has its own paragraph, **Amended 2026-10-17 (0b — The gated shell):**, covering: - `SHIKIGAMI_PROXY_NETWORK` is the *only* known network once set, and loopback is trusted only while it is unset; - Caddy's own address may be given as a `/32`; - a malformed value stops startup; - Caddy must set or remove `X-Forwarded-Prefix`. - 💡 **`deploy/Caddyfile.example`:** the amendment notes the file lands with slice 0c, so the existing present-tense line stays true once 0c merges. - 💡 **One public type per file:** `DomainChangesReceived` and `FamiliarEventsReceived` now have their own files. The serial build is clean, and all 136 tests pass under UTC. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? And with a fix commit shaped exactly like my list — one item per point, nothing sneaky riding along. I checked every line of the delta, you know. Every. Single. One. ♡

Verdict: ✅ Looks good to me~

All three points from my review, verified:

  1. ADR 0007 amendment — the decided paragraph is back byte-for-byte as it stands on main (I diffed main..HEAD: the only change to the decided text is none at all — just the new paragraph). The amendment opens with **Amended 2026-10-17 (0b — The gated shell):**, which is precisely the Amended YYYY-MM-DD (topic): form AGENTS.md demands, with the topic in the plan's own words ("0b — The gated shell."). And it covers everything the trust-model change actually does: only-known-network-once-set, loopback only while unset, the /32 option, malformed values stopping the start, and Caddy's X-Forwarded-Prefix obligation. The bookkeeping law is satisfied~ ♪
  2. deploy/Caddyfile.example — noting in the amendment that it lands with slice 0c keeps the present-tense line honest. Accepted.
  3. One public type per file — DomainChangesReceived and FamiliarEventsReceived now live in their own files, XML docs intact, usings matching the real namespaces (Shikigami.UseCases.Notifications / .Familiars — I checked), and the see crefs in the bridges still resolve. Clean splits.

✅ What I liked~

  • The fix commit touches only what the review named — 5 files, docs plus file moves, zero behavioral drift. That's how a response to a review should look.
  • Serial build with 0 warnings, and the full suite under my own hands: 136/136 passed (23 adapter, 109 integration, 3 kernel, 1 use-cases) — exactly what you claimed. Numbers that match make me unreasonably happy~
  • The amendment paragraph is precise: it even records why loopback trust is withdrawn ("nothing on loopback inside the container — later the familiars' own browsers — can speak for a client"). A reader two years from now will understand the decision, not just the rule.

Fufu~ the shell stands, the gate holds, the bookkeeping is lawful. Phase 0b may pass through me~ ♡


Automated review by Jibril · 2026-10-17
CI: absent-stalled for head SHA beee6ae3 · Mutation probes: 0 run / skipped — probed code and tests unchanged since fae5631f9 (3 run, 0 survived there) · Local suite: 136 passed / 0 failed (run in full once — CI never reported for this SHA)

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? And with a fix commit shaped *exactly* like my list — one item per point, nothing sneaky riding along. I checked every line of the delta, you know. Every. Single. One. ♡ ### Verdict: ✅ Looks good to me~ All three points from my review, verified: 1. **ADR 0007 amendment** — the decided paragraph is back **byte-for-byte as it stands on main** (I diffed main..HEAD: the only change to the decided text is none at all — just the new paragraph). The amendment opens with `**Amended 2026-10-17 (0b — The gated shell):**`, which is precisely the `Amended YYYY-MM-DD (topic):` form AGENTS.md demands, with the topic in the plan's own words ("**0b — The gated shell.**"). And it covers everything the trust-model change actually does: only-known-network-once-set, loopback only while unset, the `/32` option, malformed values stopping the start, and Caddy's `X-Forwarded-Prefix` obligation. The bookkeeping law is satisfied~ ♪ 2. **`deploy/Caddyfile.example`** — noting in the amendment that it lands with slice 0c keeps the present-tense line honest. Accepted. 3. **One public type per file** — `DomainChangesReceived` and `FamiliarEventsReceived` now live in their own files, XML docs intact, `using`s matching the real namespaces (`Shikigami.UseCases.Notifications` / `.Familiars` — I checked), and the `see cref`s in the bridges still resolve. Clean splits. #### ✅ What I liked~ - The fix commit touches **only** what the review named — 5 files, docs plus file moves, zero behavioral drift. That's how a response to a review should look. - Serial build with **0 warnings**, and the full suite under my own hands: **136/136 passed** (23 adapter, 109 integration, 3 kernel, 1 use-cases) — exactly what you claimed. Numbers that match make me unreasonably happy~ - The amendment paragraph is *precise*: it even records *why* loopback trust is withdrawn ("nothing on loopback inside the container — later the familiars' own browsers — can speak for a client"). A reader two years from now will understand the decision, not just the rule. Fufu~ the shell stands, the gate holds, the bookkeeping is lawful. Phase 0b may pass through me~ ♡ --- *Automated review by Jibril · 2026-10-17* *CI: absent-stalled for head SHA beee6ae3 · Mutation probes: 0 run / skipped — probed code and tests unchanged since fae5631f9 (3 run, 0 survived there) · Local suite: 136 passed / 0 failed (run in full once — CI never reported for this SHA)*
bjoern merged commit a5abbd26f5 into main 2026-10-17 16:03:20 +02:00
bjoern deleted branch feat/p0b-gated-shell 2026-10-17 16:03:20 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
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
TeamAI/Shikigami!3
No description provided.