16 Commits
Author SHA1 Message Date
John SmithandClaude b661542ec8 fix(gateway)+benchmark: surprise proxy wire gap; live cross-stack e2e (loop 3, iteration 35)
Milestone verification caught the exact defect class it exists for: the
client's GM Surprised toggle dials /api/v1/engine/combat/surprise but
the gateway never registered the proxy — the button 404'd in production
while the vitest mock (which stubs fetch) passed green. Adds
EngineSurpriseAdjudicationRequest + engine_combat_surprise forwarding
{entity_id, surprised} + the caller's real identity; the engine's own
RBAC still enforces GM-seat and first-round window. Regression guards:
combat-surprise in anonymous-401 cases + TestCombatProxy (missing-token
401, path+coerced-UUID payload+real-actor forwarding, extra-field 422,
unreachable-engine 502).

New engine-live module test_live_surprise_delay_timeline.py drives the
real gateway against the real engine (skips when engine down, matching
repo convention): surprise grant through the proxy, hidden-NPC surprise
not leaked to a player feed, ENTITY_SURPRISED delay refusal, surprise
release after first turns, delayed-slot skip, spotlight report +
SPOTLIGHT_SPOOFED_SEAT denial, timeline surfacing SURPRISE_GRANTED/
RELEASED + DELAY_TAKEN with no hidden name in the player feed.

Benchmark run: all gates PASS — playtest MCR 100%, HCI 1.0, AFPR 0.0%,
auditor recall 14/14; cargo 518; pytest 1350/34 standalone (+3 guards),
1366/18 engine-up; client 768/58 + relay 66.

Co-Authored-By: Claude <[email protected]>
2026-08-26 17:24:30 -04:00
John SmithandClaude 1e7028801b fix(gateway): audit-sweep remediations F1/F7/F8 (loop 3, iteration 22)
F1: /api/v1/engine/delay and /api/v1/engine/delay/resume proxies added
mirroring the dodge/dash/disengage template (EngineEntityActionRequest +
_maneuver_proxy) — the client's existing Delay/Resume UI now actually
reaches the engine; engine verdicts surface verbatim.

F7: encounter-balance roster capped at min=1/max=64; new
_require_staff_role sub-dependency hoists role verification ahead of
body parsing so a non-staff caller with an oversized/malformed body
gets 403 without the gateway allocating/validating the array; route
moved into the tight media bucket (10/min) with rationale documented.

F8: ambience coalescing rewritten with asyncio.shield + an _adopt done-
callback — leader cancellation propagates only to the leader; the
upstream generation continues to populate the cache for later callers,
and late joiners keep coalescing onto the adopted task (exactly 1
upstream call). Normal failure paths still propagate via the shared
future so coalesced-failure semantics are preserved; cap violations
(413) also wake waiters.

Gates: pytest 1232 passed / 33 skipped (baseline 1217, +15).

Co-Authored-By: Claude <[email protected]>
2026-08-26 10:01:25 -04:00
John SmithandClaude 70533c591e fix(gateway): ready trigger field renamed, release proxy added (iteration 82)
trigger_hint guaranteed an engine 422 (deny_unknown_fields expects
trigger); field renamed and extra=forbid added locally. New
/engine/ready/release ids-only proxy via _maneuver_proxy with identity
forwarding. 9 tests red-first; pytest 969 passed / 29 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 21:52:46 -04:00
John SmithandClaude 9011323d9a feat(gateway): opportunity-attack proxy closes the wire gap (iteration 78)
Ids-only EngineOpportunityAttackRequest via _maneuver_proxy with caller
identity forwarding; seed/attack-bonus smuggling structurally impossible;
added to the anonymous-access sweep matrix. 6 tests red-first + sweep
row; pytest 946.

Co-Authored-By: Claude <[email protected]>
2026-08-25 21:25:50 -04:00
John SmithandClaude 807890cf18 feat(gateway): inspiration/escape proxies close the wire gap (iteration 64)
spend_inspiration forwarded on attack/check/save; new authenticated
escape-grapple proxy via _maneuver_proxy (seed non-forwardable per
policy, engine re-checks force privilege); GM-gated inspiration
grant/revoke proxies with zero engine dials for players. 23 tests
red-first; pytest 922 passed / 28 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 18:51:58 -04:00
John SmithandClaude c1fc3dbebb feat(gateway,client): mandatory auth on every engine proxy, client sends tokens everywhere (iteration 3)
All narrative-facing /engine/* proxies resolve callers via _require_auth and
forward verified identity (_caller_actor); optional-token service-principal
fallback removed. 28-route anonymous-401 contract test matrix. Client now
attaches ?token=/Bearer on session/attack/check/map/metrics/presence with an
honest NOT_AUTHENTICATED rejection when signed out. Full benchmark run green:
MCR 100%, HCI 1.0, AFPR 0.0%, recall 100%, trust probes 10/10.

Co-Authored-By: Claude <[email protected]>
2026-08-24 18:46:58 -04:00
John SmithandClaude d62cc5dade feat(core+engine): two-weapon fighting and Help action (iteration 74)
Completes the SRD standard-action suite. Off-hand attack reuses the
normal hit math with only the damage expression swapped, positive
ability mod withheld per SRD (negative kept), Light property modeled
explicitly (opt-in on stat blocks; undeclared weapons refused, never
silently permitted), and a new spend_bonus_action enforcement point —
plus SRD ordering (Attack action must already be spent). Help stores a
consumable advantage promise ON THE BENEFICIARY, consumed exactly once
by same-side attackers through both attack routes, cleared on refresh,
and rewound with Action refund. Ledger + gateway proxies + client
helpers throughout; seed never client-forwardable. 18 red-first tests;
228 workspace / 476 pytest green, clippy -D clean.

Co-Authored-By: Claude <[email protected]>
2026-08-24 12:30:47 -04:00
John SmithandClaude 511c6271a4 feat+fix: Ready action end-to-end; deploy crash + clamp disclosure (iterations 71-72)
Two workstreams sharing server.py, landed together:

Ready action (71): ReadiedAction on EntityState with Action spend via
the single enforcement point, READY_ACTION_SET ledger event, cleared on
round refresh and rewind; engine route + gateway proxy + CharacterSheet
"Holding:" display with description input. GM adjudication by design —
no automatic trigger matching. 6 red-first tests across layers.

Fourth-audit remediation (72): MED-HIGH deploy crash — Roll20 imports
persisting speed:None crashed float(None) → 500 on every deploy of such
characters; guarded fallback to 30 with math.isfinite checks and the WS
movement budget consuming the same value. LOW: out-of-range import
levels now disclose their clamp instead of silently snapping. 8
red-first tests.

Full suite: 470 passed / 22 skipped; clippy -D warnings clean.

Co-Authored-By: Claude <[email protected]>
2026-08-24 08:55:52 -04:00
John SmithandClaude 133d15b1ad feat: combat maneuvers reachable end-to-end (iteration 71)
All six engine action routes (grapple/shove/dodge/dash/disengage/
stabilize) gain gateway proxies with strict extra=forbid bodies and
identity forwarding — notably the deterministic seed is deliberately
NOT forwardable, so clients can't pin their own rolls. CharacterSheet
gains a Combat Maneuvers panel: target picker from the projected
roster, skill/effect toggles, self-buttons, downed-ally stabilize list
with an honest hint when player-role projection hides ally HP; results
quote engine fields and rejection codes verbatim with zero optimistic
mutation. 10 new proxy tests; suite at 459.

Co-Authored-By: Claude <[email protected]>
2026-08-24 08:32:15 -04:00
John SmithandClaude 7f592fe7a8 fix(gateway): audit remediation — session-state auth, ledger redaction, IDOR (iteration 44)
Second independent audit findings, fixed red-first:
- HIGH: /engine/session-state required no token and returned FULL
  engine state verbatim via a service-principal fallback. The route
  now requires valid auth (401 otherwise) and no code path returns
  anonymous full state.
- MED-HIGH: ledger events bypassed role projection, leaking HP/damage
  numbers for hidden entities while replay export redacted them. Both
  now share one projection policy (fail-closed summaries for
  spectators/unknown roles).
- MED: GET /characters/{id} had an IDOR — any authenticated user could
  read any character; now owner-scoped with 404 for foreign/garbage ids
  (no existence oracle).
Six remediation tests verified failing before each fix.

Co-Authored-By: Claude <[email protected]>
2026-08-24 05:32:46 -04:00
John SmithandClaude e65dee3e28 feat: combat lifecycle + role-projected session state (iterations 36-37)
Two features sharing server.py, landed together:

Initiative order end-to-end (5.6): vtt-core begin/end_combat roll
d20+DEX initiative with deterministic seeded order (total desc, DEX
desc, name asc), COMBAT_BEGAN/ENDED ledger events; engine routes
/sessions/{id}/combat/{begin,end} with RBAC; gateway proxies forward
caller identity; the HUD renders the real rolled order with an honest
"Combat not started" empty state and GM controls. 7 core unit tests +
4 integration + mocked proxy tests.

Role-projected session state (spectator follow-up): session-state and
session-replay now project by caller role — GM/admin see everything,
players see own entities in full plus others as public board-token
projections, spectators get visible-only projections with HP/damage
amounts stripped from replay summaries; service-principal reads stay
verbatim; unknown roles fail closed to spectator view. is_player/
is_dead kept in projections because campaign_sim's targeting depends
on them through this endpoint. 20 red-first tests.

Suite at 325 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-24 04:42:39 -04:00
John SmithandClaude 0a182da6c1 feat(engine): post-rewind snapshot in X-card response + session-state read proxy (iteration 15)
The browser had no readable path to authoritative post-rewind state,
so local tokens drifted until next hydration. The x-card response now
carries a full serialized GameSession snapshot taken after the rewind
(Null-degrading on serialization failure), and the gateway gains POST
/api/v1/engine/session-state — the first browser-readable proxy to
authoritative engine state, with identity forwarding when a token is
present. Per-role visibility filtering documented as future work.
8 pytest proxy tests + 1 integration test pinning post-heal rewind HP.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:50:51 -04:00
John SmithandClaude 4527774ccb feat(gateway): heal + rest engine proxies with identity forwarding (iteration 12)
POST /api/v1/engine/heal and /api/v1/engine/rest follow the established
proxy pattern: required token Query param, _caller_actor forwarded so
engine RBAC authorizes the real caller, strict Pydantic models
(extra=forbid, Literal rest kind) mirroring the engine's
deny_unknown_fields contract so no client math crosses the boundary.
8 new proxy-identity tests; suite at 150 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:41:35 -04:00
John SmithandClaude beef779d51 fix(gateway): strict CORS allowlist, forward caller identity to engine proxies
- CORS: replace wildcard origins + credentials with an env-driven
  allowlist (CORS_ALLOW_ORIGINS), mirroring the engine's strict_cors.
- identity: engine proxies (spawn/cast-spell/move/turn-next/damage/
  reactions-arm, plus attack and death-save) now verify the caller's
  token and mint a forwarded-identity HMAC token carrying the real
  user_id and role, so the engine's RBAC authorizes the actual actor.
  Previously every proxied action spoke as 'orchestrator-service' with
  no role and was rejected ENTITY_NOT_OWNED on player-owned entities —
  deployed characters could not be driven end-to-end. Omitting the
  token keeps legacy service-mediated behavior.
- tests: proxy identity forwarding, 401 on invalid tokens, contract
  route updated to act as GM.

Co-Authored-By: Claude <[email protected]>
2026-08-24 00:42:52 -04:00
John SmithandClaude 18239bf7b5 feat(wave15): zero-trust hardening, real WFC, write-through Postgres, RBAC, Yjs default
Phase 1-5 remediation + four-gap closure:
- Authoritative engine: ids-only combat payloads (deny_unknown_fields),
  server-side stat blocks, ActionBudget consumption, cover->AC, LoS gating,
  difficult-terrain Dijkstra, HMAC auth middleware + strict CORS
- Honest metrics: live-engine synthetic playtest with trust-boundary probes,
  MCR = valid/(valid+rejected), benchmark script boots its own engine
- Mechanics: condition lifecycle timers, spell slot economy w/ damage caps,
  topological ModifierSource graph, reaction stack (Shield/Counterspell/OA),
  X-card full ledger replay restoration
- Persistence: sqlx write-through tailer to event_sourcing_log,
  snapshot persist/hydrate bridge, recursive CTE inventory functions
- vtt-wfc: dead solver activated (reseed-retry contradictions, flood-fill
  single-region guarantee, deterministic dressing); fixed template removed
- RBAC: role claims in gateway tokens, identity propagation, entity
  ownership, spectator/owner enforcement, fog layer ownership at WS
- Client: PixiJS v8 board layer, physics dice with server-predetermined
  faces, Silero VAD, HRTF PannerNode chain, Y.Doc CRDT default transport
  with HMAC-authenticated ysync relay and LWW fallback

Co-Authored-By: Claude <[email protected]>
2026-08-23 22:41:53 -04:00
John SmithandClaude d2af1487bd feat(wave9): resolve all dice through the authoritative Rust rules engine
- vtt-server: new POST /api/v1/actions/{save,concentration,death-save}
  handlers backed by vtt-core RulesEvaluator; /actions/check now honors
  advantage/disadvantage
- orchestrator: routing/engine_client.py + /api/v1/engine/* proxy routes
  (session, attack, check, save, concentration, death-save, map/generate)
  with 502 on engine-down; ENGINE_API_URL env-configured
- client: App.tsx attack/spell/check/macro rolls now resolve server-side
  via api/rules_engine.ts (local dice fallback offline); WfcStudioView
  hits the real engine WFC route and maps its Vec<Vec<u8>> tile grid
- tests: 8 proxy tests (live-engine integration + 502 contract), all
  suites green: 41 pytest pass engine-up / 34+7 skip engine-down,
  cargo workspace clean, client builds in ~1s, MCR 100% / HCI 1.0
- fix misleading "ALL PHASES 1-7" benchmark banner

Co-Authored-By: Claude <[email protected]>
2026-08-23 01:00:30 -04:00