feat: add ExecApprovalsStore write path and wire coordinator side effects#526
feat: add ExecApprovalsStore write path and wire coordinator side effects#526AlexAlves87 wants to merge 6 commits into
Conversation
…ects (PR8)
Implements the store write path (AddAllowlistEntryAsync, RecordAllowlistUseAsync)
and wires the side-effect calls into ExecApprovalsCoordinator.
Side effects fire strictly after the final allow decision is confirmed (both
pass1 pre-approved and post-pass2 branches). Best-effort: never throw; any
I/O failure degrades to a logged warning. UpdateFileAsync refuses to write
a malformed file. SemaphoreSlim serializes intra-process writes.
Pattern validation is non-empty only, matching macOS parity. New entries
carry {id, pattern} only — lastUsedAt is absent on creation and stamped by
RecordAllowlistUseAsync on first use (macOS addAllowlistEntry parity).
Rail 19 preserved: coordinator not referenced in any production src/ file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strip rail codes, PR numbers, research doc references, D-labels, CVE/ADR tags, and other planning labels from all ExecApprovals source files. No logic changed — comments only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Codex review: needs maintainer review before merge. Reviewed May 26, 2026, 1:14 PM ET / 17:14 UTC. Summary Reproducibility: not applicable. as a feature PR. The branch includes a concrete runtime proof command and captured output showing real filesystem writes and lastUsed metadata updates for the coordinator/store slice. Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land this contained store/coordinator slice after maintainer security review, then require separate production-wiring proof when the coordinator is connected to the live app. Do we have a high-confidence way to reproduce the issue? Not applicable as a feature PR. The branch includes a concrete runtime proof command and captured output showing real filesystem writes and lastUsed metadata updates for the coordinator/store slice. Is this the best way to solve the issue? Yes, this appears to be the narrowest maintainable slice for the missing write path: it keeps production wiring out, persists only after confirmed allow, and covers the previous wildcard bucket gap. Maintainers still need to accept the security-boundary semantics before merge. AGENTS.md: found, but no applicable review policy affected this item. Codex review notes: model gpt-5.5, reasoning high; reviewed against 9de9b5ba0f8a. Label changesLabel changes:
Label justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
ClawSweeper PR egg ✨ Hatched: 🌱 uncommon Clockwork Patch Peep Hatch commandComment Hatchability rules:
Rarity: 🌱 uncommon. What is this egg doing here?
|
…ce and lastUsed recording End-to-end coordinator/store test using real filesystem I/O. Surfaces the on-disk exec-approvals.json content at three points (initial, post-AllowAlways, post-allowlist-hit) via ITestOutputHelper so the JSON is visible under `dotnet test ... --logger "console;verbosity=detailed"`. Demonstrates both side-effect paths: AllowAlways persists a new entry, and a later allowlist hit records lastUsed* metadata against the same entry id (dedup). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞👀 Command router queued. I will update this comment with the next step. |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
ResolveReadOnly merges entries from agents["*"] into the resolved allowlist for any concrete agent, so a hit can be authorized by the wildcard bucket. RecordAllowlistUseAsync was only searching the concrete agent bucket, so wildcard-authorized executions never accumulated lastUsed* metadata. The method now iterates both the concrete agent bucket and "*", updating metadata wherever the pattern matches. If agentId is already "*", only the wildcard bucket is searched (no double-pass). Tests added: - Store: wildcard-only bucket hit updates metadata. - Store: same pattern in both buckets updates both entries. - Coordinator: end-to-end regression with allowlist living under "*". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What
Adds the write path to
ExecApprovalsStoreand wires the two side-effect calls intoExecApprovalsCoordinator, closing the persistence loop for the exec approvals V2 pipeline.Store — new public API:
AddAllowlistEntryAsync(agentId, pattern)— persists a new allowlist entry after an AllowAlways prompt decision. Deduplicates on write (OrdinalIgnoreCase). Returnstrueif the entry is present after the call (added or already there),falseon empty pattern or I/O failure. New entries carry{id, pattern};lastUsedAtis stamped later byRecordAllowlistUseAsyncon first successful use (matches macOS parity).RecordAllowlistUseAsync(agentId, pattern, command, resolvedPath)— updateslastUsedAt,lastUsedCommand, andlastResolvedPathfor every matching entry after a final allow. Returnsfalseif pattern not found or on I/O failure.Store — private infrastructure:
UpdateFileAsync(mutate)— load → mutate → atomic save, serialized by the existingSemaphoreSlim. Never throws. Refuses to overwrite a malformed file. Handles transientIOExceptionon the atomic move as a degraded path: logsWarn, no retry.Coordinator — side effects wired:
RecordAllowlistUsageAsyncfires on both allow exit points: the pass1 pre-approved branch and the post-pass2 branch. Both are required to cover the common allowlist-satisfied case.PersistAllowlistEntriesAsyncfires only after pass2 = Allow andfollowupDecision == AllowAlways, strictly outside the_promptLockblock.Allowresult.Not wired in production yet: the coordinator is still not referenced in any production
src/file. TheProductionWiring_CoordinatorNotReferencedInSrctest remains green.Design notes
Side effects fire strictly after the final allow decision is confirmed — not before the second evaluator pass. This is a deliberate structural safety choice: the guarantee is structural rather than relying on proof that
Evaluate(context, AllowAlways)always produces Allow.Pattern validation in
AddAllowlistEntryAsyncis non-empty only, matching macOS parity. Basename-only patterns are inert at match time but not rejected at persist time.Testing
145 tests passing, 0 failures. 24 new tests added in this change (17 store, 7 coordinator).
Store tests cover: success paths, dedup, not-found, malformed-file refusal, I/O failure degradation on both mutators, concurrency (5 concurrent writes produce a single entry), and round-trip JSON validation.
Coordinator tests cover: AllowAlways persistence, non-allowlist security guard, duplicate pattern dedup, allowlist usage recording, allowlist-not-satisfied guard, pass1 pre-approved path, and fallback path with allowlist satisfied.
Real behavior proof
End-to-end coordinator/store runtime proof using real filesystem I/O (test
RuntimeProof_AllowAlways_PersistsAndRecordsLastUsedintests/OpenClaw.Shared.Tests/ExecApprovalsCoordinatorTests.cs).Scope clarification: this is slice runtime proof, not full production runtime proof. The coordinator is intentionally not wired in production yet. A follow-up production wiring slice (
SetV2Handler(coordinator)+ feature flag) connects the coordinator, and the WinUI prompt dialog is a separate slice after that. UI-driven proof against the live app is only meaningful once those land.Reproduce locally:
Captured output (initial / post-AllowAlways / post-allowlist-hit):
The entry
idis identical between the two invocations, proving on-disk dedup. ThelastUsedAt/lastUsedCommand/lastResolvedPathfields appear only after the second invocation, provingRecordAllowlistUseAsyncfires on the allowlist-hit path.Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com