feat: add ExecApprovalsStore write path and wire coordinator side effects - #526
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 18, 2026, 9:09 PM ET / 01:09 UTC. Summary Reproducibility: not applicable. This is a feature PR adding an internal exec-approvals write path, so the relevant evidence is PR-head code review and the contributor's filesystem-backed runtime proof rather than reproducing a current-main bug. Review metrics: 3 noteworthy metrics.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the internal write-path slice after maintainer acceptance of the persisted policy change and green required validation, while preserving the no-command-text and migration-before-write behavior. Do we have a high-confidence way to reproduce the issue? Not applicable. This is a feature PR adding an internal exec-approvals write path, so the relevant evidence is PR-head code review and the contributor's filesystem-backed runtime proof rather than reproducing a current-main bug. Is this the best way to solve the issue? Yes, broadly. The store/coordinator ownership boundary matches the existing pipeline, and the latest head addresses the earlier command-text and side-effect isolation concerns; remaining work is maintainer acceptance of the security-policy persistence slice and completed validation. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c0514bd2d026. Label changesLabel 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?
|
|
@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:
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
First-pass triage: holding this one rather than merging. A dual-model review found enough security-boundary/correctness risk that I don’t think it clears the quick-merge bar yet:
Suggested follow-up: avoid persisting full command text (or store only a redacted/structured summary), and wrap approval metadata side effects so they log/fail-soft without changing the approval decision. |
|
Both findings addressed in the latest commit (90b36db): 1. lastUsedCommand removed. The field is gone from 2. Side effects isolated. Each side effect ( Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com |
…ation
lastUsedCommand persisted the full command text in exec-approvals.json,
leaking tokens or secrets embedded in command arguments. Remove the field
from ExecAllowlistEntry and the command parameter from RecordAllowlistUseAsync;
lastUsedAt and lastResolvedPath provide sufficient operational metadata.
Approval-store side effects (PersistAllowlistEntriesAsync, RecordAllowlistUsageAsync)
were unguarded, so an unexpected exception could cause an already-approved command
to be reported as InternalError. Each side effect in both the pre-approved (pass1)
and post-prompt (step 8) paths is now wrapped in its own best-effort try/catch so
a failure in one does not skip the other and never flips an allow to a deny.
Add DoesNotContain("lastUsedCommand") assertion to the store round-trip test
as a regression guard against accidental reintroduction of the field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
df28407 to
ad4021d
Compare
…-write-path # Conflicts: # tests/OpenClaw.Shared.Tests/ExecApprovalsStoreTests.cs
UpdateFileAsync loaded the target file directly, so with a custom state dir configured a write could create a fresh exec-approvals.json at the target path while an unmigrated legacy file still existed. That would permanently block TryMigrateLegacyFile and silently orphan the legacy configuration. Mirror EnsureFileAsync: run migration first and refuse to write when the legacy file is unreadable (Blocked), consistent with the existing fail-closed read semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
Conflict in ExecApprovalsStoreTests.cs: upstream added two tilde-expansion tests (ResolveAsync_TildeOnlyStateDir, ResolveAsync_TildePrefixedOpenClawHome) at the same point where the write-path tests end. Both blocks kept.
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, resolvedPath)— updateslastUsedAtandlastResolvedPathfor every matching entry after a final allow. No command text is persisted. 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.try/catchso a failure in one does not skip the other and cannot flip an already-decided allow intoInternalError.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.No command text reaches disk.
lastUsedAt(Unix ms) andlastResolvedPathprovide sufficient operational metadata without exposing tokens or secrets embedded in command arguments.Testing
2252 OpenClaw.Shared tests passing, 0 failures (head da0b255). Build: 0 warnings, 0 errors (
TreatWarningsAsErrorsclean).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), round-trip JSON validation, and a regression guard asserting
lastUsedCommandis absent from the persisted file.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 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 — current head (da0b255),
lastUsedCommandabsent:The entry
idis identical between the two invocations, proving on-disk dedup.lastUsedAtandlastResolvedPathappear only after the second invocation, provingRecordAllowlistUseAsyncfires on the allowlist-hit path. NolastUsedCommandfield is present at any stage.Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com