Skip to content

fix(permissions): stop always-allow/deny persistence from clobbering concurrent config writes#118

Merged
juacker merged 1 commit into
mainfrom
fix/permissions-lost-update
Jul 23, 2026
Merged

fix(permissions): stop always-allow/deny persistence from clobbering concurrent config writes#118
juacker merged 1 commit into
mainfrom
fix/permissions-lost-update

Conversation

@juacker

@juacker juacker commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a lost-update race: persisting an always-allow/always-deny permission decision could silently clobber a concurrent workspace-config write.

Problem

persist_decisions_to_agent (src-tauri/src/commands/permissions.rs) wrote the workspace config via a bare load -> mutate -> save, bypassing the process-wide writer lock in workspace_config::update(). A permission decision racing another config write (starring a workspace, the scheduler re-anchoring next_run_at_unix_ms) could discard that write and refresh the workspace index from the stale snapshot. Same lost-update class already fixed for mark_opened in commands/workspace.rs.

Change (1 file)

  • Read-modify-write now runs inside workspace_config::update(); the index refresh uses the config returned under the lock.
  • Fast path: skip the lock + disk round-trip when decisions contain nothing durable (Once-only, or Always with blank prefixes).
  • The list surgery is extracted into apply_decisions_to_shell_policy(), a pure function over WorkspaceAgent, with 5 new unit tests (allow/deny list moves, idempotency + no duplicates, no-op decisions, prefix trimming).

Deliberate tradeoffs (flagged in review)

  • Redundant identical save when a decision re-applies an existing prefix (update() saves unconditionally): harmless atomic rewrite, kept in favor of the uniform locked helper.
  • Once-only decisions no longer validate workspace/agent existence (fast path returns before the lookup). Previously a stale id failed the whole submit_permission_decision, dropping the oneshot sender and cancelling the awaiting tool even for a plain Allow-once — the new behavior is strictly better.

Follow-up recorded in TODO

Deduplicate the update() + insert_config plumbing shared with update_workspace_config_for_id (commands/workspace.rs) and update_workspace_config (commands/workspace_agents.rs) into one pub(crate) helper.

Verification

  • cargo test --lib: 810 passed. Clippy clean, cargo fmt --check clean.
  • 3 independent reviews (Codex / Claude / Minimax): all production_quality, zero blocker/major findings. Reviewers verified the fast-path predicate exactly matches the mutation's persistence set, error paths abort before save, and lock ordering matches the existing helpers.

Found by Codex during the PR #116 review.

…concurrent config writes

persist_decisions_to_agent wrote the workspace config via a bare
load->mutate->save, bypassing the process-wide writer lock in
workspace_config::update(). A permission decision racing another config
write (starring a workspace, re-anchoring a schedule) could silently
discard that write and refresh the workspace index from the stale
snapshot — the same lost-update class already fixed for mark_opened in
commands/workspace.rs.

- Run the read-modify-write inside workspace_config::update() and refresh
  the index from the config returned under the lock.
- Fast path: skip the lock + disk round-trip entirely when decisions
  contain nothing durable (Once-only, or Always with blank prefixes).
- Extract the list surgery into apply_decisions_to_shell_policy(), a pure
  function over WorkspaceAgent, and unit-test it: allow/deny list moves,
  idempotency (no change reported on re-apply, no duplicates), trimming,
  and no-op decisions.

Found by Codex during the PR #116 review.
@juacker
juacker marked this pull request as ready for review July 23, 2026 10:44
@juacker
juacker merged commit a0ecad9 into main Jul 23, 2026
2 checks passed
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.

1 participant