The snapshot MAC in #374 authenticates the session snapshot and deliberately leaves the event log and the tool-call audit log unauthenticated. That exclusion was a decision, not an oversight, but it leaves one attack open and this issue is where it gets closed.
The attack that survives #374
internal/app/approvalreplay.go:33-82 runs at every loadAndReopen. It reads EventLog.Read, filters EvApproval records carrying AllowAlways, correlates each to a ToolCall in the loaded conversation via ev.Approval.Call, and re-drives policy.Learn on that call to re-derive the real permission rule from history.
With #374 shipped, the snapshot half of that path is authenticated: a writer cannot forge the tool call, because the conversation lives inside the MAC'd snapshot. The event half is not. So an adversary who can write the store takes an existing, benign tool call already in the conversation, adds an EvApproval{AllowAlways} naming it to mecatl:events:<id>, and at the next resume the harness learns a durable permission the user never granted.
The replay code is carefully fail-safe against every accident — it filters allow-once and deny, it skips uncorrelated verdicts — and defenceless against a writer, because it takes both of its inputs from the same untrusted place.
Same adversary as #374: someone who can write the store but cannot execute code in a mecatl pod. A leaked Redis credential, pod-network reach on an unauthenticated 6379, a shared instance, an offline backup.
Acceptance criteria
Proof
Notes on shape
A per-record MAC is enough for the attack above: an adversary cannot produce a valid MAC without the key, so forging or modifying a record is blocked. Deletion and truncation are not detected, and that is acceptable here because deletion only ever removes a permission. If truncation detection is ever wanted, a chained MAC covering the previous record's tag gives it, at the cost of making append ordering load-bearing and requiring the cumulative Read to verify the chain.
Reasoning
The snapshot MAC in #374 authenticates the session snapshot and deliberately leaves the event log and the tool-call audit log unauthenticated. That exclusion was a decision, not an oversight, but it leaves one attack open and this issue is where it gets closed.
The attack that survives #374
internal/app/approvalreplay.go:33-82runs at everyloadAndReopen. It readsEventLog.Read, filtersEvApprovalrecords carryingAllowAlways, correlates each to aToolCallin the loaded conversation viaev.Approval.Call, and re-drivespolicy.Learnon that call to re-derive the real permission rule from history.With #374 shipped, the snapshot half of that path is authenticated: a writer cannot forge the tool call, because the conversation lives inside the MAC'd snapshot. The event half is not. So an adversary who can write the store takes an existing, benign tool call already in the conversation, adds an
EvApproval{AllowAlways}naming it tomecatl:events:<id>, and at the next resume the harness learns a durable permission the user never granted.The replay code is carefully fail-safe against every accident — it filters allow-once and deny, it skips uncorrelated verdicts — and defenceless against a writer, because it takes both of its inputs from the same untrusted place.
Same adversary as #374: someone who can write the store but cannot execute code in a mecatl pod. A leaked Redis credential, pod-network reach on an unauthenticated 6379, a shared instance, an offline backup.
Acceptance criteria
EvApprovalrecord that the harness did not write is rejected rather than replayed.mecatl:tools:<id>) is either covered or explicitly named as out of scope. Nothing re-derives authority from it, but it is the trail an incident responder reads, and a writer can rewrite or truncate it.Proof
Notes on shape
A per-record MAC is enough for the attack above: an adversary cannot produce a valid MAC without the key, so forging or modifying a record is blocked. Deletion and truncation are not detected, and that is acceptable here because deletion only ever removes a permission. If truncation detection is ever wanted, a chained MAC covering the previous record's tag gives it, at the cost of making append ordering load-bearing and requiring the cumulative
Readto verify the chain.Reasoning