fix: harden event store, serialization, and grant engine; wire router chat

Event log integrity (sole source of truth):
- SqliteEventStore: serialize append/appendAll under a Mutex, enable WAL +
  busy_timeout + synchronous=NORMAL, roll back on any Throwable. Replaces the
  app-computed `SELECT MAX(seq)+1` read-modify-write that dropped events under
  concurrent appends (race was invisible to CI: only the in-memory store and
  single-threaded :memory: tests were ever exercised).
- Serialization: encodeDefaults + ignoreUnknownKeys, explicit @SerialName on all
  46 EventPayload subclasses and event-referenced enum constants, @Serializable on
  RiskLevel/RiskAction. Guards the append-only log against silent corruption from
  future class renames, field removals, or default-value changes.

Approval/grant security (Invariant: approvals cannot widen authority):
- Tier authorization is now a ceiling (request.tier.level <= max granted) instead
  of set membership; SESSION grants bind to a specific toolName instead of matching
  everything; handleCreateGrant rejects empty/over-tier/scopeless grants.

Router chat wiring (Phase 0-2): ChatInput round-trip, StartChatSession from IDLE,
router-panel layout, workflows behind Ctrl+W.

Tests: SqliteEventStore concurrency, serialization round-trip + discriminator
stability + unknown-key tolerance, adversarial grant scope/tier; updated existing
approval and reducer tests for the new grant semantics and StartChatSession effect.
This commit is contained in:
2026-05-28 22:39:15 +04:00
parent 57d2237ba0
commit cdee5f2245
43 changed files with 690 additions and 223 deletions
@@ -29,7 +29,7 @@ class ApprovalEngineEdgeCasesTest {
fun `grants from different scopes apply only to matching identity`() {
val sessionGrant = ApprovalGrant(
id = GrantId("g1"),
scope = GrantScope.SESSION,
scope = GrantScope.SESSION(),
permittedTiers = setOf(Tier.T3),
reason = "",
timestamp = Instant.parse("2026-01-01T00:00:00Z")
@@ -59,7 +59,7 @@ class ApprovalEngineEdgeCasesTest {
val past = Instant.parse("2025-12-31T23:59:59Z")
val expiredGrant = ApprovalGrant(
id = GrantId("g1"),
scope = GrantScope.SESSION,
scope = GrantScope.SESSION(),
permittedTiers = setOf(Tier.T2),
reason = "",
timestamp = past,