Skip to content

feat: exec approvals store write path and coordinator side effects#505

Closed
AlexAlves87 wants to merge 2 commits into
openclaw:masterfrom
AlexAlves87:feat/exec-approvals-store-write
Closed

feat: exec approvals store write path and coordinator side effects#505
AlexAlves87 wants to merge 2 commits into
openclaw:masterfrom
AlexAlves87:feat/exec-approvals-store-write

Conversation

@AlexAlves87

@AlexAlves87 AlexAlves87 commented May 22, 2026

Copy link
Copy Markdown
Contributor

What

Adds the write path to ExecApprovalsStore and wires the two side-effect calls into ExecApprovalsCoordinator.

Store — new public API:

  • AddAllowlistEntryAsync(agentId, pattern) — persists a new allowlist entry after an AllowAlways prompt decision. Deduplicates on write (OrdinalIgnoreCase). Returns true if present after the call (added or already there), false on empty pattern or I/O failure. New entries carry {id, pattern} only — lastUsedAt is absent on creation, matching macOS addAllowlistEntry behavior, and is stamped by RecordAllowlistUseAsync on first use.
  • RecordAllowlistUseAsync(agentId, pattern, command, resolvedPath) — updates lastUsedAt, lastUsedCommand, and lastResolvedPath for every matching entry after a final allow. Returns false if pattern not found or on I/O failure.

Store — private infrastructure:

  • UpdateFileAsync(mutate) — load → mutate → atomic save, serialized by the existing SemaphoreSlim. Never throws. Refuses to overwrite a malformed file. Handles transient IOException on the atomic move as a degraded path: logs Warn, no retry.

Coordinator — side effects wired:

  • RecordAllowlistUsageAsync fires 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.
  • PersistAllowlistEntriesAsync fires only after pass2 = Allow and followupDecision == AllowAlways, strictly outside the _promptLock block.
  • Both helpers are best-effort: a store I/O failure is absorbed and does not affect the final Allow result.

Not wired in production yet: the coordinator is still not referenced in any production src/ file. The ProductionWiring_CoordinatorNotReferencedInSrc test remains green.

Design notes

Side effects fire strictly after the final allow decision is confirmed — not before the second evaluator pass as in the macOS socket path. This is a deliberate structural safety choice: the macOS sequence is "persist before pass2" (and relies on the proof that Evaluate(context, AllowAlways) always produces Allow); this implementation prefers safety-by-structure over proof-by-argument.

Pattern validation in AddAllowlistEntryAsync is 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 (using FileShare.Read so LoadFile succeeds but File.Move is blocked), 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.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

AlexAlves87 and others added 2 commits May 22, 2026 08:20
…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>
@AlexAlves87 AlexAlves87 changed the title feat: exec approvals store write path and coordinator side effects (PR8) feat: exec approvals store write path and coordinator side effects May 22, 2026
@shanselman

Copy link
Copy Markdown
Contributor

Thanks for putting this together. The useful piece here is the exec-approvals write path: persisting Allow Always decisions into exec-approvals.json and recording allowlist usage after a command is finally allowed. That closes the loop between “we can read/evaluate/prompt” and “the user’s durable approval actually gets saved and can be shown/maintained later.”

The branch has drifted pretty far from current master, though, so a direct update would be hard to review safely and risks pulling unrelated stale changes forward.

Could you open a fresh PR from current master that ports only this focused scope?

  • ExecApprovalsStore write methods for adding allowlist entries and recording usage
  • coordinator side effects after a final allow decision
  • focused store/coordinator tests for those write paths

That should make this much easier to review and land while keeping the credit and direction from your work here.

@AlexAlves87

Copy link
Copy Markdown
Contributor Author

Superseded by #526 — fresh branch from current master with the same focused scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants