- Grounding: NarrativeGenerateRequest gains engine_session_id /
target_entity_id. When a session is named, _resolve_audit_inputs fetches
the live snapshot and the auditor reconciles against IT: lethality fields
come from the live entity (a payload claiming death while the engine says
25 HP can no longer smuggle a kill past Vector 3), entity counts come
from the live snapshot, named-but-missing targets are rejected as ghost
entities, and an unreachable engine fails closed with
WORLD_INSPECTOR_UNAVAILABLE instead of auditing stale client claims.
- Streaming: tokens are now HELD until the sentence they belong to has
passed the auditor; violating sentences and everything after them are
never emitted — only the corrective SYSTEM frame and the cut.
- Lore continuity: Vector 4 was dead code (nothing ever populated its
lore_* payload keys). The auditor now derives (subject, predicate,
object) triples from canon entity names and predicate verbs appearing in
the draft itself, including reflexive assertions like "Oakhaven Keep
still stands". Payload triples still take precedence.
Co-Authored-By: Claude <[email protected]>
Unparseable input used to silently yield a combat-ready fake: AC 14,
HP 30, invented "Claw Strike" action with +5 / 2d6+3. Core fields now
stay absent when not parsed, every missing field is reported in
warnings, and responses carry parse_ok. Strict mode (new flag on
HomebrewParseRequest) rejects with 422 instead of returning a
half-invented statblock. Placeholder lists are copied per call to stop
cross-parse state leaks.
Co-Authored-By: Claude <[email protected]>
The step-4 report block had escaped quotes inside f-strings passed through
python -c, a guaranteed SyntaxError that suppressed live MCR/HCI/AFPR output
entirely. Replaced with a quoted-delimiter heredoc so the Python reaches the
interpreter byte-for-byte, and pinned AUTH_SECRET so the fail-closed engine
and the playtest signer agree.
First honest run then exposed a harness bug: the runner kept attacking its
training dummy after killing it (~100 turns of TARGET_ALREADY_DEAD rejections
dragging MCR to 76%). The runner now replaces the dummy when it drops, so
MCR measures action acceptance against living targets. Result: MCR 100%,
HCI 1.0, AFPR 0.0%, trust boundary 8/8.
Co-Authored-By: Claude <[email protected]>
- 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]>
- narrative stream: extract_srd_context() scans player intents against the
SRD 5.2 bestiary/grimoire and injects stat-block facts (AC/HP/actions,
spell level/school/description) into both live LLM prompts and the
deterministic mock generator — the DM can no longer contradict the rules
- new GET /api/v1/compendium/lore-lookup for client-side fact resolution
- safety: X-card now applies a real ledger rewind on the engine session
(POST /sessions/{id}/safety/x-card) via the orchestrator, recording
SAFETY_REWIND_APPLIED audit events; client wires the SafetyModal through
- voice: removed the random speaking simulation from WebRTCMeshManager;
local mic RMS (getUserMedia + AnalyserNode) drives the real speaking
ring; remote peers stay silent until genuine P2P audio lands
- tests: +7 grounding/x-card suite (53 pytest total pass), cargo clean,
client ~1s build, MCR 100% / HCI 1.0 / AFPR 0%
Co-Authored-By: Claude <[email protected]>