fix(permission): persist "always allow" grants so they survive a restart#1331
Conversation
Approvals from an "always allow" reply were pushed into the in-memory `approved` list but never written back to PermissionTable. That list is loaded from the project's row at instance startup, so every app restart (or instance reload) dropped all "always" grants and the next action re-asked — for every tool, not just the browser. Capture the project id in the permission State at load, and on an "always" reply upsert the full cumulative approved ruleset under the project's primary key. On reload the grant is read back and the same target auto-allows with no prompt. Test: new next.test.ts case approves "always", disposes the instance, then reloads the same project directory and asks again — resolves with no prompt. Confirmed it fails (re-asks, times out) with the write reverted. Verification: opencode typecheck clean; permission tests 113 pass / 0 fail.
|
Warning Review limit reached
More reviews will be available in 7 minutes and 30 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesAlways-grant persistence
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces changes to persist "always allow" permission grants to the database, ensuring they survive application restarts and instance reloads. Specifically, it captures the projectID in the permission state and performs an upsert to the PermissionTable when an "always" grant is approved. A new integration test has also been added to verify this persistence behavior. I have no feedback to provide as there are no review comments to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
The "always allow" persistence write replaced the whole project row with the current instance's cached `approved`. Multiple instances can share one project row — project id is the shared git-common-dir, so different worktrees or subdirectories of one repo resolve to the same row — while each keeps its own `approved` loaded at startup. An "always" reply in one instance therefore overwrote the row with only that instance's grants, dropping grants another instance had persisted after it loaded (last-writer-wins data loss). Re-read the current row and union it with `approved` inside one synchronous `Database.use` callback before writing. Single process + no await between the read and write makes the read-merge-write atomic against other fibers, so the row converges to the union of every instance's grants. `mergeRulesets` dedupes by (permission, pattern, action) so the row does not grow unboundedly. Adds a regression test: two sibling instances of one git repo each grant a different "always", and both survive an instance reload. Verified it fails (the first grant re-asks on reload) when the merge is reverted to a whole-row write.
Follow-up: merge persisted grants instead of clobbering (commit 88fde5a)An independent Codex review flagged a lost-update in the first commit that I verified is real: The bug. The persistence write replaced the entire project row with the current instance's cached The fix. Re-read the current row and union it with Test. Added a regression test: two sibling instances of one git repo each grant a different "always"; both survive an instance reload. Confirmed it fails (the first grant re-asks on reload) when the merge is reverted to a whole-row write. Verification: |
Summary
On an "always allow" reply, the granted rules were pushed into the in-memory
approvedlist but never written back toPermissionTable. Capture the project id in the permissionStateat load, and on an "always" reply upsert the full cumulativeapprovedruleset under the project's primary key so the grant persists.Why
The
approvedlist is loaded from the project'sPermissionTablerow at instance startup (permission/index.ts:186), but nothing ever wrote it back —approved.push(...)only mutated memory. So every app restart (or instance reload) dropped all "always allow" grants and the next action re-asked. This affected every tool that supports "always" (edit, bash, browser, …), and was the real reason "always" felt like it never stuck.Related Issue
None — found while investigating the embedded-browser permission prompts (context: PR #1330).
Human Review Status
Pending
Review Focus
reply(): it writes the whole cumulativeapprovedarray underproject_id(the table's primary key) viaonConflictDoUpdate. Confirm that's the right shape and that writing inside thereplyeffect (synchronousDatabase.use, current instance's DB) is correct.existing.info.always.length > 0, so plain "once" replies and empty-alwaysbrowser asks don't write.Risk Notes
Behavior change limited to permission persistence: "always allow" now survives restarts as intended. No schema/migration change —
PermissionTablealready exists and is already read at startup; this only starts writing the column that was already being read. No platform/UI surface touched.Skipped conditional checklist items: visible-UI item (no UI or copy changed); platform item (no Electron/packaging/updater/signing/paths surface — server-side permission persistence).
How To Verify
Screenshots or Recordings
None — no visible UI or copy changed.
Checklist
bug,enhancement,task,documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.app,ui,platform,harness,ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.P0,P1,P2,P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.Pending,Approved by @<reviewer>, orNot required: <reason>(default isPending; "not required" is restricted to bot-authored low-risk PRs).dev, and my PR title and commit messages use Conventional Commits in English.Summary by CodeRabbit