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:
@@ -21,10 +21,10 @@ class GrantSemanticsTest {
|
||||
|
||||
@Test
|
||||
fun `grant permits exact tier - auto_approved`() {
|
||||
val req = request("r1", Tier.T2)
|
||||
val req = request("r1", Tier.T2).copy(toolName = "shell")
|
||||
val grant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
scope = GrantScope.SESSION(toolName = "shell"),
|
||||
permittedTiers = setOf(Tier.T2),
|
||||
reason = "test",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
@@ -45,7 +45,7 @@ class GrantSemanticsTest {
|
||||
val req = request("r1", Tier.T3)
|
||||
val grant = ApprovalGrant(
|
||||
id = GrantId("g1"),
|
||||
scope = GrantScope.SESSION,
|
||||
scope = GrantScope.SESSION(),
|
||||
permittedTiers = setOf(Tier.T2),
|
||||
reason = "test",
|
||||
timestamp = Instant.parse("2026-01-01T00:00:00Z")
|
||||
@@ -63,18 +63,18 @@ class GrantSemanticsTest {
|
||||
|
||||
@Test
|
||||
fun `multiple grants - any match is enough`() {
|
||||
val req = request("r1", Tier.T4)
|
||||
val req = request("r1", Tier.T4).copy(toolName = "write_file")
|
||||
val grants = listOf(
|
||||
ApprovalGrant(
|
||||
GrantId("g1"),
|
||||
GrantScope.SESSION,
|
||||
GrantScope.SESSION(toolName = "shell"),
|
||||
setOf(Tier.T3),
|
||||
"",
|
||||
Instant.parse("2026-01-01T00:00:00Z")
|
||||
),
|
||||
ApprovalGrant(
|
||||
GrantId("g2"),
|
||||
GrantScope.SESSION,
|
||||
GrantScope.SESSION(toolName = "write_file"),
|
||||
setOf(Tier.T4),
|
||||
"",
|
||||
Instant.parse("2026-01-01T00:00:00Z")
|
||||
|
||||
Reference in New Issue
Block a user