feat(permissions): add typed persistent tool permission rules#2242
feat(permissions): add typed persistent tool permission rules#2242greyfreedom wants to merge 27 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces typed, persistent tool permission rules and an execution policy engine to manage shell and file tool execution approvals. It replaces legacy flat allow/deny lists with structured rulesets across builtin, agent, and user layers, and adds support for saving approved rules to a sibling permissions.toml file. One issue was identified in crates/tui/src/tui/approval.rs where the unified diff path parser is too naive and does not track hunk state, potentially leading to incorrect permission rules if a patch contains diff-like content.
|
Independent review (Devin): merge-tested against Schema / serde — Precedence — deny-always-wins across layers is implemented correctly in Persistence — writes go through Security / project-local escalation — typed Ask-rule interaction — when Test coverage — precedence, word-boundary deny, and legacy-to-typed migration are all covered. The permissions-file chmod gap and the project v0.8.48 ( |
Hi @Hmbown , thanks for the detailed review. I addressed the remaining issues:
|
|
thank you for this! |
|
Thanks @greyfreedom — typed persistent permission rules are a strong long-term direction. The shape is valuable because permissions should become explainable state, not just scattered booleans. I’m leaving the full PR open because approval semantics are high-blast-radius. The safest next slice is the foundation around |
677035e to
4808647
Compare
Thanks, that makes sense. I’ll split out a smaller foundation PR based on current main that only introduces typed ask records around AskForApproval::Never, while keeping the existing allow/deny behavior unchanged. I’ll leave the larger PR as a reference for the full direction. |
Hi @Hmbown , thanks for the guidance. I split out the safer foundation slice into a smaller PR here: #2401 The new PR focuses only on typed I’ll keep this larger PR open stale and use the smaller PR as the next review target. |
|
Quick harvest update: the safer foundation slice from this work landed in #2404. That gives us typed I’m still not merging this broader PR as-is because the remaining persistent permission UI/routing changes are the high-blast-radius part. The best next step is to build on #2404 in smaller follow-ups: one focused PR for persistence format/loading, then one for approval-flow/TUI wiring with fixtures around deny/trust/ask precedence. This PR is still a useful reference for that shape. |
|
Thanks again @greyfreedom. The safer typed-ask foundation from this work landed in #2404 with credit, so I’m going to keep this larger PR as the reference branch rather than merge it directly. For the next slice, the safest path looks like config/schema-only support for |
|
Thanks again @greyfreedom. I rechecked #2242 against current main. The full branch now conflicts with several areas that have moved on, and the safest typed-ask foundation from this work already landed in #2404 with credit. I’m going to keep this PR open as the reference branch rather than merge it directly. The next harvestable slice is ask-only config/schema loading for permissions.toml, without typed allow/deny or approval UI persistence yet. After that, the persistence UI and runtime routing can come through as smaller reviewable follow-ups. The direction is good; it just needs to keep landing in smaller permission-safe pieces. |
|
Thank you for this. Typed persistent permission rules fit very naturally with the CodeWhale story: users should be able to make deliberate choices once and have the tool remember them transparently. I am leaving this unmerged in the 0.8.48 pass because the branch is dirty against main, not because the idea is off-track. A clean rebase plus a focused verification pass around migration, doctor/config visibility, and the exact wording of any permission prompts would make this a strong next candidate. |
|
Thanks @greyfreedom. We have now harvested the v0.9-safe pieces of this broader permissions work without merging the original branch wholesale. What landed: the typed What remains intentionally out of this harvest: typed allow/deny records, approval-flow routing through persistent typed rules, TUI persistence UI, runtime writing of approval rules, and full glob semantics. Those are still broader persistent-permissions work, so #1186 stays open. Closing this PR as partially harvested/superseded, with credit preserved for the original design and implementation direction. |
Summary
Adds an end-to-end typed tool permission system.
This PR supersedes the earlier split PRs for typed execpolicy rules, approval-flow integration, and TUI persistence UI.
What changed
Add typed permission rules in
execpolicyallow,deny, andaskdecisionsPreserve compatibility with legacy config
auto_allowis converted intoexec_shellallow rulesauto_denyis converted intoexec_shelldeny rulesRoute tool execution through typed rules
apply_patchmulti_tool_use.parallelcallsAdd persistent approval UI support
Store user-generated permission rules in
permissions.tomlconfig.tomlconfig.tomlstill supports manual[[permissions.rules]]Harden path and patch handling
allowrulesCloses #1186
Validation
cargo fmt --allcargo test -p codewhale-execpolicy --all-featurescargo test -p codewhale-tui --all-features typed_permissioncargo test -p codewhale-tui --all-features persistent_permissioncargo test -p codewhale-tui --all-features persist_permissioncargo test -p codewhale-tui --all-features config_loads_sibling_permissions_toml_for_exec_policy_enginecargo test -p codewhale-tui --all-features project_overlay_applies_only_tightening_permission_rulescargo clippy -p codewhale-execpolicy -p codewhale-tui --all-targets --all-features -- -D warningsGreptile Summary
This PR adds a comprehensive end-to-end typed tool permission system:
execpolicygainsToolPermissionRulestructs withallow/deny/askdecisions, shell and file tools are routed through these rules before falling back to approval prompts, and user-approved persistent rules are written to a siblingpermissions.tomlfile rather than pollutingconfig.toml. Legacyauto_allow/auto_denylists are converted into typedexec_shellrules for backwards compatibility.execpolicygainsToolPermissionRule,Ruleset::with_rules, andcheck_tool_permissionwith deny-always-wins and specificity-based precedence; aDiffHeaderStatemachine indispatch.rsandapproval.rscorrectly ignores---/+++lines inside hunk bodies when extracting paths from unified diffs.permissions.tomlviatoml_edit(preserving comments), sets0o600permissions, and adds the in-memory ruleset to the live engine on approval so the new rule takes effect immediately in the current session.auto_allowandAllow-decision rules from project overlays are silently dropped.Confidence Score: 5/5
Safe to merge. The permission rule routing, persistence, and deny-wins priority logic are all correct and well-tested. The two flagged issues are narrow edge cases (octal escape values that git never generates) that carry no security or correctness risk on real input.
The DiffHeaderState machine correctly tracks hunk boundaries so diff-body lines can no longer be misread as file headers. Path-traversal rules are properly rejected before persisting. The deny-always-wins and specificity-based priority logic is consistent throughout the engine, config layer, and approval UI. Only two minor issues were found: an out-of-range octal byte value is silently clamped to 0xFF (a code path git never exercises) and a doc comment names the wrong file. Neither affects production behaviour.
The duplicated
parse_quoted_diff_pathimplementation inapproval.rsanddispatch.rseach contain the same octal clamping nit — both should be fixed together. All other files look good.Important Files Changed
Comments Outside Diff (1)
crates/tui/src/tui/approval.rs, line 4123-4142 (link)parse_unified_diff_permission_pathsin this file scans every line for---/+++prefixes, but has no state-machine to track when the cursor is inside a hunk body. A removed line whose original content starts with--(e.g., a SQL comment-- SELECT foo) appears in the patch as--- SELECT fooand will be treated as a file header, injectingSELECT fooas a path into the persistent allow rule. A crafted patch can exploit this to silently extend a saved rule to cover paths the user never intended to allow.dispatch.rshas aDiffHeaderStatemachine (ExpectOld→ExpectNew→AfterHeader→InHunk) that correctly ignores lines within hunk bodies; this function should use the same logic or share the implementation.crates/tui/src/tui/approval.rs, line 4144-4154 (link)normalize_diff_permission_pathin this file splits on tab then trims, but does not handle Git's quoted path syntax ("a/path with spaces/file.txt").dispatch.rshandles this viadiff_permission_path_token→parse_quoted_diff_path. Whenapply_patchis called with a patch containing a quoted filename, the approval-side rule generator extracts only up to the first whitespace, producing an incorrect persistent allow rule that will never match the actual path.crates/tui/src/tui/approval.rs, line 3948-3953 (link)exec_shellWhen
exec_shell_waitis approved with 's', the generated rule setstool = "exec_shell_wait". Future calls via the plainexec_shelltool won't match this rule. Consider normalising all shell-variant tool names to"exec_shell"when building the persistent rule so the saved rule covers all invocation styles.crates/tui/src/tui/approval.rs, line 4011-4017 (link)permission_decision_labelduplicated across two modulesAn identical helper is already defined in
crates/tui/src/commands/config.rs. Moving it tocodewhale_execpolicyand re-exporting would eliminate the duplication.Reviews (5): Last reviewed commit: "fix(tui): remove unused approval constru..." | Re-trigger Greptile