Skip to content

Added: External directory permissions and cached rulesets#91

Merged
Sewer56 merged 5 commits intomainfrom
add-external-dir-support
Apr 7, 2026
Merged

Added: External directory permissions and cached rulesets#91
Sewer56 merged 5 commits intomainfrom
add-external-dir-support

Conversation

@Sewer56
Copy link
Copy Markdown
Member

@Sewer56 Sewer56 commented Apr 7, 2026

Summary

  • Owned Rule/Ruleset types - Convert from &'a str to Box<str> so rulesets can be cached and shared across the runtime lifetime.
  • Precomputed runtime caches - Build and cache permission rulesets, allowed-tool lists, and callable Task target summaries once at AgentRuntime construction, eliminating repeated rule checks on every tool call.
  • External directory support - Add external directory plumbing to bash, edit, glob, grep, read, and write tools so they can operate on paths outside the primary workspace.
  • Permission evaluation performance - Reverse-scan for last-match-wins semantics, single-rule fast paths, and new benchmarks.

Benchmark Results

Permission evaluation (32-rule cases):

Case Before After Improvement
exact_32_rules 19.40ns 12.62ns 35%
wildcard_subject_32_rules 31.81ns 18.11ns 43%
wildcard_both_32_rules 34.30ns 21.32ns 38%

check_permission improved by 36-42% on the same hot paths.

Runtime Task resolution (64 agents):

Operation Before After Improvement
allowed_tools("caller") 2.36µs 5.6ns 421x
summarize_callable_targets("caller") 4.88µs 1.67µs 2.9x

Sewer56 added 2 commits April 7, 2026 15:02
- Make Rule/Ruleset owned types (Box<str>) so they can be cached across the runtime
- Cache permission rulesets in AgentRuntime to avoid rebuilding on every tool call
- Add external directory support to bash, edit, glob, grep, read, and write tools
- Add PermissionDenied error variant and OptionRulesetExt helper trait
- Add permission evaluation benchmarks
- Reverse-scan permission rules for last-match-wins, add a single-rule fast path, and defer subject hashing until an exact subject match is needed.
- Add a single-rule fast path for glob policy checks and fix the permissions benchmark after the Box<str> API change, extending it to 128-rule cases.
- Precompute allowed tools and callable Task target summaries at runtime build, add `can_delegate_to()` for cached Task validation, and benchmark the runtime Task path directly.
- Measured impact from the new benches: permission evaluation improved by about 33-43% on the common 32-rule cases (`exact_32_rules` 19.40ns -> 12.62ns, `wildcard_subject_32_rules` 31.81ns -> 18.11ns, `wildcard_both_32_rules` 34.30ns -> 21.32ns), while `check_permission` improved by about 36-42% on the same hot paths.
- Measured impact for runtime Task resolution: `allowed_tools("caller")` dropped from 2.36us to 5.6ns at 64 agents and from 10.46us to 5.7ns at 256 agents, while `summarize_callable_targets("caller")` improved from 4.88us to 1.67us at 64 agents and from 21.38us to 6.72us at 256 agents.
- Verified with `./.cargo/verify.sh` and the updated benchmark suite.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 87.36059% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.44%. Comparing base (ffce67d) to head (6a0423a).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/llm-coding-tools-core/benches/permissions.rs 0.00% 12 Missing ⚠️
...m-coding-tools-serdesai/src/agent_runtime/build.rs 88.23% 4 Missing ⚠️
src/llm-coding-tools-serdesai/src/tools/edit.rs 33.33% 4 Missing ⚠️
src/llm-coding-tools-serdesai/src/tools/read.rs 33.33% 4 Missing ⚠️
src/llm-coding-tools-serdesai/src/tools/write.rs 33.33% 4 Missing ⚠️
src/llm-coding-tools-serdesai/src/tools/glob.rs 66.66% 2 Missing ⚠️
src/llm-coding-tools-core/src/permissions.rs 97.87% 1 Missing ⚠️
src/llm-coding-tools-serdesai/src/convert.rs 0.00% 1 Missing ⚠️
src/llm-coding-tools-serdesai/src/tools/bash.rs 75.00% 1 Missing ⚠️
src/llm-coding-tools-serdesai/src/tools/grep.rs 75.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #91      +/-   ##
==========================================
+ Coverage   81.07%   81.44%   +0.36%     
==========================================
  Files         104      106       +2     
  Lines        4175     4343     +168     
==========================================
+ Hits         3385     3537     +152     
- Misses        790      806      +16     
Flag Coverage Δ
async 80.67% <87.21%> (+0.39%) ⬆️
blocking 58.34% <52.34%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/llm-coding-tools-agents/src/extensions.rs 100.00% <100.00%> (ø)
src/llm-coding-tools-agents/src/runtime/builder.rs 79.16% <ø> (ø)
src/llm-coding-tools-agents/src/runtime/state.rs 100.00% <100.00%> (ø)
src/llm-coding-tools-agents/src/runtime/task.rs 100.00% <100.00%> (+2.08%) ⬆️
src/llm-coding-tools-core/src/error.rs 42.85% <ø> (ø)
...-coding-tools-core/src/path/allowed_glob/policy.rs 86.48% <100.00%> (+0.77%) ⬆️
src/llm-coding-tools-core/src/permissions_ext.rs 100.00% <100.00%> (ø)
...-coding-tools-core/src/tools/bash/blocking_impl.rs 88.50% <100.00%> (+0.41%) ⬆️
src/llm-coding-tools-core/src/tools/bash/mod.rs 71.92% <ø> (ø)
...llm-coding-tools-core/src/tools/bash/tokio_impl.rs 89.21% <100.00%> (+0.32%) ⬆️
... and 16 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Sewer56 Sewer56 marked this pull request as ready for review April 7, 2026 16:13
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7dbf89ad-4514-48a5-a84d-d359f018a45b

📥 Commits

Reviewing files that changed from the base of the PR and between 4448e5b and 6a0423a.

📒 Files selected for processing (1)
  • src/llm-coding-tools-core/benches/permissions.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/llm-coding-tools-core/benches/permissions.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Async Windows
  • GitHub Check: Async macOS
  • GitHub Check: Blocking Windows

Walkthrough

The PR implements a permissions-first refactor across crates: Rule and Ruleset move to owned types; a new OptionRulesetExt::check and ToolError::PermissionDenied are added; core APIs gain permission-aware helpers. Filesystem and utility tools (read, write, edit, glob, grep, bash) accept settings carrying optional Ruleset values and perform permission checks. Agent runtime now precomputes per-agent permission Rulesets and builds cached lookups for allowed tools and callable task targets with new query APIs and fast delegation checks. Several Criterion benchmarks for permission evaluation and runtime task caches are added.

Possibly related PRs

  • PR 32: Directly modifies core permissions internals (permissions.rs) and Rule/Ruleset storage/evaluation semantics, overlapping the owned-type refactor.
  • PR 33: Touches the agents crate and the RulesetExt API, overlapping agent-side trait/signature changes and runtime permission caching.
  • PR 36: Alters permissions evaluation logic in permissions.rs, closely related to the Ruleset behavior and matching semantics introduced here.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Added: External directory permissions and cached rulesets' accurately describes the main changes in the PR, though 'Added' could be more specific.
Description check ✅ Passed The description is well-structured with clear sections (Summary, Benchmark Results) explaining what was changed and the performance improvements achieved, though it lacks detail on external directory support specifics.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-external-dir-support

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/llm-coding-tools-core/src/tools/glob.rs (1)

171-177: ⚠️ Potential issue | 🔴 Critical

Denied directories are still traversed.

Lines 171-177 skip every directory before any ruleset evaluation, and Lines 203-210 only check the leaf file path. That means deny("/repo/secret") never prevents the walk from entering /repo/secret; with a broader allow("*"), descendant files can still pass the later check and be returned. Directory permissions need to be enforced before descent so denied subtrees are pruned, not just filtered after the walk.

Also applies to: 203-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm-coding-tools-core/src/tools/glob.rs` around lines 171 - 177, The code
currently skips directories unconditionally via entry.file_type() / ft.is_dir(),
which allows denied directories (e.g., deny("/repo/secret")) to be traversed and
only filters leaves later; change the logic so that when ft.is_dir() you
evaluate the ruleset/permission check for that directory path (the same check
used for files) and if the ruleset denies the directory, skip/descent (continue)
to prune the subtree, otherwise allow descent; apply the same fix to the later
leaf-only block so directory entries are checked against deny/allow rules before
walking into them (i.e., enforce deny rules on directories before descent, not
just on leaf file acceptance).
src/llm-coding-tools-core/src/tools/grep.rs (1)

357-372: ⚠️ Potential issue | 🔴 Critical

Denied directories are still searchable.

Lines 357-372 only apply the ruleset after the walker has already reached a file entry. An exact deny on a nested directory path is therefore ignored, so grep can still descend into that subtree and return matches from files that are only covered by a broad allow rule. The directory subject needs to be checked before descent, with denied subtrees pruned from the walk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm-coding-tools-core/src/tools/grep.rs` around lines 357 - 372, The
permission check is happening after the walker already descends into entries;
instead, use a pre-descent filter so denied directories are pruned. Modify the
walk initiation to call filter_entry (or equivalent pre-visit hook) and inside
it call settings.permission() and ruleset.is_allowed(grep_meta::NAME, subject)
on the entry.path() (string lossied) so directories that return false are not
descended into; keep the existing per-file permission check for files but remove
relying on post-descent logic to prevent searching denied subtrees.
🧹 Nitpick comments (2)
src/llm-coding-tools-core/src/path/allowed_glob/policy.rs (1)

85-88: Add explicit regression tests for single-rule fast-path semantics.

Lines 85-88 introduce a distinct execution path; please add direct coverage for a single Deny rule (match + non-match cases) to lock behavior.

✅ Suggested test additions
 #[test]
 fn invalid_glob_pattern_fails() {
     let result = GlobPolicy::builder().allow("[invalid");
     assert!(result.is_err());
 }
+
+#[test]
+fn single_deny_rule_should_deny_match_and_non_match() {
+    let policy = GlobPolicy::builder()
+        .deny("secret/**")
+        .unwrap()
+        .build()
+        .unwrap();
+
+    assert!(!policy.is_allowed("secret/file.txt"));
+    assert!(!policy.is_allowed("public/file.txt"));
+}
+
+#[test]
+fn single_allow_rule_should_allow_only_matches() {
+    let policy = GlobPolicy::builder()
+        .allow("src/**")
+        .unwrap()
+        .build()
+        .unwrap();
+
+    assert!(policy.is_allowed("src/lib.rs"));
+    assert!(!policy.is_allowed("tests/test.rs"));
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm-coding-tools-core/src/path/allowed_glob/policy.rs` around lines 85 -
88, Add regression tests that exercise the single-rule fast path by creating a
Policy (or the type that holds self.rules) with exactly one rule whose action is
RuleAction::Deny, then assert the evaluator that uses
matcher.is_match(normalized_path) returns false when the glob matches and true
when it does not; ensure both match and non-match cases are covered so the
branch that checks if let [(matcher, action)] = self.rules.as_slice() behaves
correctly for Deny rules and normalized_path handling.
src/llm-coding-tools-agents/src/runtime/state.rs (1)

121-151: Consider exposing borrowed cache views as well.

Both accessors still clone their cached Vec on every lookup. Adding borrowed variants (&[ToolCatalogEntry] / &[TaskTargetSummary]) or storing Arc<[T]> would keep this hot path allocation-free and preserve more of the win from precomputing the caches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm-coding-tools-agents/src/runtime/state.rs` around lines 121 - 151, The
current accessors allowed_tools and summarize_callable_targets clone Vecs from
the caches (allowed_tools_by_caller, callable_target_summaries_by_caller) on
every lookup; change the API to provide allocation-free borrowed views or shared
refs: add new methods (e.g., allowed_tools_ref(caller_name: &str) ->
&[ToolCatalogEntry] and summarize_callable_targets_ref(caller_name: &str) ->
&[TaskTargetSummary]) that return slices into the cached Vecs, or alternatively
change the cache value types to Arc<[T]> and add returning methods that return
Arc<[T]> to avoid cloning; update callers to use the new borrowed/shared-view
methods and keep the existing cloning methods (allowed_tools,
summarize_callable_targets) for backwards compatibility if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/llm-coding-tools-agents/benches/runtime_task.rs`:
- Around line 97-99: The benchmark uses a non-existent agent ID for the
"can_delegate_miss" case, so update the test data to target an existing misc
agent (e.g., change "misc-003" to "misc-002") wherever "can_delegate_miss" is
configured inside bench_runtime_task_caches (and the duplicate case around the
other occurrence referenced in the comment), ensuring the fixture and the
benchmark target the same existing agent ID so the test measures a permission
miss rather than a lookup of a nonexistent agent.

In `@src/llm-coding-tools-core/benches/permissions.rs`:
- Around line 54-57: The function benchmark_cases() documentation is out of
sync: it claims wildcard cases exist only at sizes 1 and 32 but the
implementation also includes wildcard_both_128_rules (and similar 128-rule cases
around lines 96-100); update the comment above benchmark_cases() to enumerate
the actual rule-set sizes covered (1, 32, and 128) and mention the presence of
wildcard-both (and other wildcard) cases so the doc matches the implemented
PermissionCase variants.

In `@src/llm-coding-tools-core/src/tools/bash/mod.rs`:
- Around line 94-95: Update the documentation for BashSettings to include the
permission-denied error: in the `# Errors` section for the module/type that
documents BashSettings and the `permission: Option<&'a Ruleset>` field, add a
note that operations may return `ToolError::PermissionDenied` when a command
string is blocked by the provided ruleset; reference `BashSettings` and
`ToolError::PermissionDenied` so the API docs clearly show the new error case.

In `@src/llm-coding-tools-serdesai/src/convert.rs`:
- Around line 80-88: The mapping for CoreError::PermissionDenied is incorrect:
change the conversion to produce a runtime execution failure instead of a
validation error by mapping PermissionDenied to SerdesError::execution_failed
(instead of SerdesError::validation_error) using the same parameters (tool_name,
None, and the formatted "Permission denied for tool '{}' on subject '{}'"
message); update the conversion in the match arm that handles
CoreError::PermissionDenied so PermissionDenied follows the same
execution/runtime failure path as Io/Execution/Timeout errors.

---

Outside diff comments:
In `@src/llm-coding-tools-core/src/tools/glob.rs`:
- Around line 171-177: The code currently skips directories unconditionally via
entry.file_type() / ft.is_dir(), which allows denied directories (e.g.,
deny("/repo/secret")) to be traversed and only filters leaves later; change the
logic so that when ft.is_dir() you evaluate the ruleset/permission check for
that directory path (the same check used for files) and if the ruleset denies
the directory, skip/descent (continue) to prune the subtree, otherwise allow
descent; apply the same fix to the later leaf-only block so directory entries
are checked against deny/allow rules before walking into them (i.e., enforce
deny rules on directories before descent, not just on leaf file acceptance).

In `@src/llm-coding-tools-core/src/tools/grep.rs`:
- Around line 357-372: The permission check is happening after the walker
already descends into entries; instead, use a pre-descent filter so denied
directories are pruned. Modify the walk initiation to call filter_entry (or
equivalent pre-visit hook) and inside it call settings.permission() and
ruleset.is_allowed(grep_meta::NAME, subject) on the entry.path() (string
lossied) so directories that return false are not descended into; keep the
existing per-file permission check for files but remove relying on post-descent
logic to prevent searching denied subtrees.

---

Nitpick comments:
In `@src/llm-coding-tools-agents/src/runtime/state.rs`:
- Around line 121-151: The current accessors allowed_tools and
summarize_callable_targets clone Vecs from the caches (allowed_tools_by_caller,
callable_target_summaries_by_caller) on every lookup; change the API to provide
allocation-free borrowed views or shared refs: add new methods (e.g.,
allowed_tools_ref(caller_name: &str) -> &[ToolCatalogEntry] and
summarize_callable_targets_ref(caller_name: &str) -> &[TaskTargetSummary]) that
return slices into the cached Vecs, or alternatively change the cache value
types to Arc<[T]> and add returning methods that return Arc<[T]> to avoid
cloning; update callers to use the new borrowed/shared-view methods and keep the
existing cloning methods (allowed_tools, summarize_callable_targets) for
backwards compatibility if needed.

In `@src/llm-coding-tools-core/src/path/allowed_glob/policy.rs`:
- Around line 85-88: Add regression tests that exercise the single-rule fast
path by creating a Policy (or the type that holds self.rules) with exactly one
rule whose action is RuleAction::Deny, then assert the evaluator that uses
matcher.is_match(normalized_path) returns false when the glob matches and true
when it does not; ensure both match and non-match cases are covered so the
branch that checks if let [(matcher, action)] = self.rules.as_slice() behaves
correctly for Deny rules and normalized_path handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87471eed-c0b1-4998-82ba-e191facd7e8d

📥 Commits

Reviewing files that changed from the base of the PR and between ffce67d and 853426f.

📒 Files selected for processing (31)
  • src/llm-coding-tools-agents/Cargo.toml
  • src/llm-coding-tools-agents/benches/runtime_task.rs
  • src/llm-coding-tools-agents/src/extensions.rs
  • src/llm-coding-tools-agents/src/runtime/builder.rs
  • src/llm-coding-tools-agents/src/runtime/state.rs
  • src/llm-coding-tools-agents/src/runtime/task.rs
  • src/llm-coding-tools-core/Cargo.toml
  • src/llm-coding-tools-core/benches/permissions.rs
  • src/llm-coding-tools-core/src/error.rs
  • src/llm-coding-tools-core/src/lib.rs
  • src/llm-coding-tools-core/src/path/allowed_glob/policy.rs
  • src/llm-coding-tools-core/src/permissions.rs
  • src/llm-coding-tools-core/src/permissions_ext.rs
  • src/llm-coding-tools-core/src/tools/bash/blocking_impl.rs
  • src/llm-coding-tools-core/src/tools/bash/mod.rs
  • src/llm-coding-tools-core/src/tools/bash/tokio_impl.rs
  • src/llm-coding-tools-core/src/tools/edit.rs
  • src/llm-coding-tools-core/src/tools/glob.rs
  • src/llm-coding-tools-core/src/tools/grep.rs
  • src/llm-coding-tools-core/src/tools/mod.rs
  • src/llm-coding-tools-core/src/tools/read.rs
  • src/llm-coding-tools-core/src/tools/write.rs
  • src/llm-coding-tools-serdesai/src/agent_runtime/build.rs
  • src/llm-coding-tools-serdesai/src/convert.rs
  • src/llm-coding-tools-serdesai/src/task/handle.rs
  • src/llm-coding-tools-serdesai/src/tools/bash.rs
  • src/llm-coding-tools-serdesai/src/tools/edit.rs
  • src/llm-coding-tools-serdesai/src/tools/glob.rs
  • src/llm-coding-tools-serdesai/src/tools/grep.rs
  • src/llm-coding-tools-serdesai/src/tools/read.rs
  • src/llm-coding-tools-serdesai/src/tools/write.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Blocking Linux
  • GitHub Check: Blocking Windows
  • GitHub Check: Async Linux
  • GitHub Check: Async macOS
  • GitHub Check: Blocking macOS
  • GitHub Check: Semver Checks (Serdesai Full)
  • GitHub Check: Async Windows
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-04-07T11:44:05.110Z
Learnt from: CR
Repo: Sewer56/llm-coding-tools PR: 0
File: src/AGENTS.md:0-0
Timestamp: 2026-04-07T11:44:05.110Z
Learning: If relevant to your review task, read `.cargo/performance.md` for performance guidelines

Applied to files:

  • src/llm-coding-tools-agents/Cargo.toml
  • src/llm-coding-tools-core/Cargo.toml
  • src/llm-coding-tools-agents/benches/runtime_task.rs
📚 Learning: 2026-03-28T02:14:04.465Z
Learnt from: Sewer56
Repo: Sewer56/llm-coding-tools PR: 69
File: src/llm-coding-tools-bubblewrap/src/profile/validation.rs:57-67
Timestamp: 2026-03-28T02:14:04.465Z
Learning: In `src/llm-coding-tools-bubblewrap/src/profile/` (Rust, llm-coding-tools-bubblewrap crate), the `Builder` API paths (workspace, synthetic_home, cache_root, mount lists, overlays, etc.) are always set by trusted application/operator code — the library consumer is the trusted party. Path normalization and `..`-component hardening in validators like `validate_absolute_path` is therefore NOT required to defend against traversal attacks. Untrusted input (LLM-generated shell commands) only enters through `wrap_command`/`execute_command_with_mode`, not through the `Builder`.

Applied to files:

  • src/llm-coding-tools-core/src/tools/bash/blocking_impl.rs
  • src/llm-coding-tools-core/src/tools/bash/tokio_impl.rs
  • src/llm-coding-tools-serdesai/src/tools/bash.rs
  • src/llm-coding-tools-serdesai/src/tools/read.rs
  • src/llm-coding-tools-serdesai/src/agent_runtime/build.rs
  • src/llm-coding-tools-serdesai/src/tools/glob.rs
  • src/llm-coding-tools-core/src/tools/read.rs
🔇 Additional comments (33)
src/llm-coding-tools-core/src/path/allowed_glob/policy.rs (1)

79-80: #[inline] on the hot path looks good.

Line 79 is a reasonable optimization hint for this small, frequently-invoked permission check.

src/llm-coding-tools-core/src/error.rs (1)

58-65: PermissionDenied variant is a solid addition.

Error shape and message formatting are consistent with the rest of ToolError and fit the new permission-check flow cleanly.

src/llm-coding-tools-core/Cargo.toml (1)

128-130: Benchmark target registration looks good.

The permissions bench entry is configured consistently with existing Criterion bench targets (harness = false).

src/llm-coding-tools-agents/Cargo.toml (1)

46-48: runtime_task benchmark target wiring is clean.

Manifest update is consistent with existing bench configuration and supports the runtime-cache perf work.

src/llm-coding-tools-core/src/tools/bash/blocking_impl.rs (1)

47-50: Blocking-path permission enforcement is correctly integrated.

The pre-execution permission.check(...) and the new denial test provide good coverage for the new authorization behavior.

Also applies to: 260-290

src/llm-coding-tools-core/src/tools/mod.rs (1)

23-23: Re-export updates are appropriate.

Exposing EditSettings and WriteSettings here keeps the public tools surface consistent with the settings-based APIs.

Also applies to: 35-35

src/llm-coding-tools-core/src/tools/bash/tokio_impl.rs (1)

105-108: Async-path permission gating looks correct.

The check is enforced before execution, and the new test validates denial behavior end-to-end for tokio mode.

Also applies to: 302-333

src/llm-coding-tools-serdesai/src/task/handle.rs (2)

1-148: LGTM - Clean migration to cached delegation API.

The refactoring correctly replaces on-the-fly Ruleset construction with the cached runtime.can_delegate_to() API. The test coverage is comprehensive, covering unknown targets, primary agent rejection, permission denied scenarios, and max depth enforcement.


134-145: The implicit-allow behavior differs from the old Ruleset pattern: clarify the design choice.

The guard at line 134 (caller.permission.contains_key(task_meta::NAME)) means that if the caller has no task permission entry at all, the delegation check is skipped entirely, allowing delegation by default.

However, this is a new feature (all task-related files are newly added in this commit). If the code had followed the old pattern of calling Ruleset::is_allowed with empty rules, the result would have been false (restrictive), not permissive. The current design intentionally uses a permissive default—no permission rules = implicit allow.

Ensure this implicit-allow semantic is intentional and documented (in comments or design notes) rather than an unintended consequence of the permission check guard.

src/llm-coding-tools-serdesai/src/tools/grep.rs (2)

75-85: LGTM - Clean builder pattern for permission configuration.

The with_permission method follows the established builder pattern used across other tools, properly delegates to GrepSettings::with_permission, and includes #[must_use] for safety.


100-111: LGTM - Correct reference-based settings passing.

The change from moving self.search_settings to passing &self.search_settings is correct and aligns with the core API change. This allows the tool to be called multiple times without consuming the settings.

src/llm-coding-tools-serdesai/src/tools/bash.rs (3)

54-56: LGTM - Well-documented permission field.

The permission field is properly typed as Option<Arc<Ruleset>> with clear documentation. The Arc allows efficient sharing of the ruleset across tool instances without copying.


154-164: LGTM - Consistent builder pattern.

The with_permission method matches the pattern used in GrepTool and GlobTool, with proper #[must_use] annotation and clear documentation.


216-226: LGTM - Correct permission passing to BashSettings.

Using self.permission.as_deref() correctly converts Option<Arc<Ruleset>> to Option<&Ruleset> for the BashSettings struct, maintaining the reference without unnecessary cloning.

src/llm-coding-tools-agents/src/runtime/builder.rs (1)

154-184: LGTM - Excellent cache identity verification test.

The test correctly validates that:

  1. The same Arc instance is returned for repeated calls (Arc::ptr_eq)
  2. The cached ruleset correctly evaluates permissions (is_allowed)

Using Arc::ptr_eq is the right approach to verify caching rather than just value equality.

src/llm-coding-tools-core/src/lib.rs (2)

18-18: LGTM - Public permission extension module.

Exposing permissions_ext publicly enables consumers to use the OptionRulesetExt trait for permission checking on optional rulesets.


37-40: LGTM - Consistent re-export expansion.

Adding EditSettings and WriteSettings to the public re-exports aligns with the existing pattern for other tool settings types (GrepSettings, GlobSettings, etc.).

src/llm-coding-tools-agents/src/extensions.rs (1)

40-61: LGTM - Clean migration to owned Ruleset.

The signature change from Ruleset<'a> to owned Ruleset aligns with the PR's core change to use Box<str> for rule storage. The implementation correctly:

  • Preserves IndexMap iteration order for rule precedence
  • Uses with_capacity for allocation efficiency
  • Converts string references to owned storage via Rule::new
src/llm-coding-tools-serdesai/src/tools/glob.rs (2)

64-74: LGTM - Consistent permission builder pattern.

The with_permission method follows the established pattern across tools, with proper #[must_use] annotation and delegation to GlobSettings::with_permission.


89-97: LGTM - Simplified error handling flow.

The refactored call implementation is cleaner, with a straightforward flow:

  1. Parse request (early return on error)
  2. Execute glob_files (early return on error)
  3. Return formatted output

Removing the intermediate result variable and explicit match improves readability.

src/llm-coding-tools-serdesai/src/agent_runtime/build.rs (4)

85-87: LGTM - Well-documented permission caching field.

The permission field documentation clearly explains the None case for backward compatibility with agents that have no permissions configured.


128-131: Good optimization: filtering empty rulesets.

The .filter(|ruleset| !ruleset.is_empty()) optimization avoids unnecessary permission checks when no rules are configured, correctly mapping empty rulesets to None (permissive mode).


169-225: LGTM - Consistent permission wiring across tools.

The permission is correctly cloned and passed to each tool that supports path access control:

  • ReadTool via with_settings_and_permission
  • WriteTool, EditTool, GlobTool, GrepTool, BashTool via .with_permission(permission.clone())

The Arc::clone() is efficient (reference count increment only), and each tool correctly shares the same underlying Ruleset instance.


227-246: WebFetch and Todo tools intentionally skip permission rulesets.

WebFetchTool, TodoRead, and TodoWrite do not receive permission rulesets by design:

  • WebFetchTool operates on URLs rather than filesystem paths and is initialized with with_settings() only
  • TodoRead and TodoWrite are pre-built stateless tools created via create_todo_tools() and cloned without permission filtering
  • TaskTool delegates validation through its own runtime model (can_delegate_to in TaskHandle), checking delegation permissions independently

This design is consistent with each tool's operational scope.

src/llm-coding-tools-serdesai/src/tools/edit.rs (1)

41-43: Permission-aware settings flow is wired correctly.

The constructor split (newwith_settings) and with_permission builder cleanly propagate settings into edit_file, which keeps policy evaluation centralized in core.

Also applies to: 45-73, 90-90

src/llm-coding-tools-serdesai/src/tools/read.rs (1)

79-105: ReadTool permission injection path looks good.

with_settings_and_permission plus read_file(&self.resolver, args, &self.settings) makes runtime-injected permissions explicit and keeps behavior consistent with the core read API.

Also applies to: 125-126

src/llm-coding-tools-core/src/tools/edit.rs (1)

83-132: Core edit permission gating is correctly implemented.

EditSettings is cleanly modeled, and the pre-I/O settings.permission().check(...) plus denied-path test gives good coverage of the new access-control behavior.

Also applies to: 150-155, 258-288

src/llm-coding-tools-serdesai/src/tools/write.rs (1)

41-43: WriteTool settings/permission plumbing is solid.

The move to WriteSettings plus with_permission keeps the serdes layer aligned with core permission enforcement and avoids per-call policy reconstruction.

Also applies to: 45-73, 91-92

src/llm-coding-tools-core/src/permissions_ext.rs (1)

30-35: Nice centralization of optional-ruleset checks.

This trait removes repeated permission boilerplate at call sites, and the tests validate the intended None => allow, Some => evaluate behavior.

Also applies to: 37-53, 61-93

src/llm-coding-tools-core/src/tools/write.rs (1)

27-76: Write permission enforcement is correctly placed and covered.

Checking permissions immediately after path resolution (before directory creation and writes) is the right ordering, and the denied-path test exercises the new failure mode.

Also applies to: 87-92, 158-187

src/llm-coding-tools-agents/src/runtime/task.rs (1)

86-116: Nice one-pass cache construction.

Building both per-caller maps from a single sorted agent snapshot and the prebuilt ruleset map keeps the cached views deterministic and avoids repeating permission resolution on the runtime lookup path.

src/llm-coding-tools-core/src/permissions.rs (1)

249-285: The fast path looks sound.

The empty/single-rule cases plus the reverse scan preserve last-match-wins semantics while avoiding unnecessary subject hashing on the common miss paths.

Also applies to: 405-429

src/llm-coding-tools-core/src/tools/read.rs (1)

318-324: Verify the permission subject is canonicalized before open.

Line 319 derives the permission subject from the resolved path string, and Line 324 opens that path afterward. If PathResolver::resolve is lexical-only and open_buffered follows symlinks, an allowed symlink or alias path can still reach a denied target. The same escape would affect the other path-scoped tools wired the same way in this PR.

Run this to inspect the resolver and open helpers; you want to see canonicalization or explicit symlink rejection before the subject is authorized/opened:

#!/bin/bash
set -euo pipefail

rg -n -C4 --type=rust 'trait PathResolver|struct AbsolutePathResolver|impl .*PathResolver|fn resolve\(' src
rg -n -C4 --type=rust 'fn open_buffered|canonicalize|symlink_metadata|read_link|O_NOFOLLOW|follow_links' src/llm-coding-tools-core/src

Comment thread src/llm-coding-tools-agents/benches/runtime_task.rs
Comment thread src/llm-coding-tools-core/benches/permissions.rs
Comment thread src/llm-coding-tools-core/src/tools/bash/mod.rs
Comment thread src/llm-coding-tools-serdesai/src/convert.rs Outdated
Sewer56 added 2 commits April 7, 2026 17:28
- Map PermissionDenied errors to execution_failed in serdesai
- Document PermissionDenied in bash module docstrings
- Fix typo in runtime_task benchmark comment
- Update permissions benchmark documentation

Ref: #91
The allowed_tools and summarize_callable_targets methods now return
&[T] instead of cloning Vecs on every lookup. Callers needing ownership
explicitly call .to_vec() or .clone(). This eliminates allocations for
read-only access patterns used by tests and iteration.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/llm-coding-tools-core/benches/permissions.rs (1)

126-143: Consider adding denied-path benchmark coverage.

bench_check_permission currently exercises only the allowed path (Line 139). Adding at least one denied fixture would better track regressions for PermissionDenied hot-path behavior introduced in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm-coding-tools-core/benches/permissions.rs` around lines 126 - 143,
bench_check_permission only measures the allowed path via
OptionRulesetExt::check; add a denied-path benchmark to cover PermissionDenied
hot path by adding at least one denied fixture to the benchmark_cases data (or
create a separate denied_case) and then add a bench_with_input invocation in
bench_check_permission that uses BenchmarkId::new("denied_ruleset", case.name)
and calls .check(...) expecting an Err (use a small assertion that it returns a
PermissionDenied) so the denied code path is exercised; locate the fixtures in
benchmark_cases and the benchmark function bench_check_permission to implement
this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/llm-coding-tools-core/benches/permissions.rs`:
- Around line 28-38: The function build_ruleset currently claims rule_count
"must be at least 1" but coerces 0 into a one-rule fixture; enforce the
precondition instead: add an assertion like assert!(rule_count >= 1, "rule_count
must be >= 1") at the start of build_ruleset, remove the .max(1) use in
Ruleset::with_capacity and pass rule_count directly, and replace the
saturating_sub loop range (for idx in 0..rule_count.saturating_sub(1)) with an
explicit 0..(rule_count - 1) iteration so the code fails fast when rule_count is
0; use the function name build_ruleset and the Ruleset::with_capacity call to
locate the changes.

---

Nitpick comments:
In `@src/llm-coding-tools-core/benches/permissions.rs`:
- Around line 126-143: bench_check_permission only measures the allowed path via
OptionRulesetExt::check; add a denied-path benchmark to cover PermissionDenied
hot path by adding at least one denied fixture to the benchmark_cases data (or
create a separate denied_case) and then add a bench_with_input invocation in
bench_check_permission that uses BenchmarkId::new("denied_ruleset", case.name)
and calls .check(...) expecting an Err (use a small assertion that it returns a
PermissionDenied) so the denied code path is exercised; locate the fixtures in
benchmark_cases and the benchmark function bench_check_permission to implement
this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aca8d33a-dcb4-4b1d-aa2b-517f3234b764

📥 Commits

Reviewing files that changed from the base of the PR and between 853426f and 4448e5b.

📒 Files selected for processing (8)
  • src/llm-coding-tools-agents/benches/runtime_task.rs
  • src/llm-coding-tools-agents/src/runtime/state.rs
  • src/llm-coding-tools-core/benches/permissions.rs
  • src/llm-coding-tools-core/src/tools/bash/blocking_impl.rs
  • src/llm-coding-tools-core/src/tools/bash/mod.rs
  • src/llm-coding-tools-core/src/tools/bash/tokio_impl.rs
  • src/llm-coding-tools-serdesai/src/agent_runtime/build.rs
  • src/llm-coding-tools-serdesai/src/convert.rs
✅ Files skipped from review due to trivial changes (1)
  • src/llm-coding-tools-agents/benches/runtime_task.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/llm-coding-tools-serdesai/src/convert.rs
  • src/llm-coding-tools-core/src/tools/bash/mod.rs
  • src/llm-coding-tools-agents/src/runtime/state.rs
  • src/llm-coding-tools-core/src/tools/bash/blocking_impl.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Async macOS
  • GitHub Check: Async Windows
  • GitHub Check: Blocking Windows
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-28T02:14:04.465Z
Learnt from: Sewer56
Repo: Sewer56/llm-coding-tools PR: 69
File: src/llm-coding-tools-bubblewrap/src/profile/validation.rs:57-67
Timestamp: 2026-03-28T02:14:04.465Z
Learning: In `src/llm-coding-tools-bubblewrap/src/profile/` (Rust, llm-coding-tools-bubblewrap crate), the `Builder` API paths (workspace, synthetic_home, cache_root, mount lists, overlays, etc.) are always set by trusted application/operator code — the library consumer is the trusted party. Path normalization and `..`-component hardening in validators like `validate_absolute_path` is therefore NOT required to defend against traversal attacks. Untrusted input (LLM-generated shell commands) only enters through `wrap_command`/`execute_command_with_mode`, not through the `Builder`.

Applied to files:

  • src/llm-coding-tools-core/src/tools/bash/tokio_impl.rs
  • src/llm-coding-tools-serdesai/src/agent_runtime/build.rs
🔇 Additional comments (10)
src/llm-coding-tools-core/src/tools/bash/tokio_impl.rs (4)

9-10: LGTM!

Imports are appropriate for the new permission check functionality.


96-109: LGTM!

The permission check is correctly placed before workdir/timeout validation for fail-fast behavior. The OptionRulesetExt::check implementation ensures backward compatibility by returning Ok(()) when permission is None. Documentation accurately reflects the new error variant.


303-334: LGTM!

The test correctly validates the last-match-wins semantics by placing a deny rule after an allow-all rule, then asserting that the specific command is blocked. The assertion properly pattern-matches the PermissionDenied variant with the expected tool name.


275-278: LGTM!

Test imports and settings updates are consistent across all test functions. The permission: None maintains backward compatibility while satisfying the new struct field requirement.

Also applies to: 289-294

src/llm-coding-tools-serdesai/src/agent_runtime/build.rs (6)

17-29: LGTM!

The added imports for Ruleset and Arc are necessary for the new permission caching functionality, and the import cleanup looks correct.


85-88: LGTM!

The Option<Arc<Ruleset>> type is appropriate for shared, cached permission data. Using Arc enables efficient cloning for multiple tools, and Option handles the backward-compatibility case for agents without permissions.


114-139: LGTM!

The permission caching logic is well-designed:

  • Using runtime.permission_ruleset(name).filter(|rs| !rs.is_empty()) correctly converts empty rulesets to None for backward compatibility.
  • The .to_vec() calls are necessary since the runtime APIs now return slices.

162-175: LGTM!

The permission caching reuse is efficient. Cloning Option<Arc<Ruleset>> is O(1) (atomic reference count increment), and the call to ReadTool::with_settings_and_permission correctly matches the signature from the relevant code snippet.


177-218: LGTM!

The permission wiring is applied consistently to all filesystem-related tools (Write, Edit, Glob, Grep, Bash). The pattern .with_permission(permission.clone()) is clean and efficient with Arc. Correctly, WebFetch does not receive permission as it operates on URLs rather than filesystem paths.


230-239: LGTM!

The Task tool now correctly uses the pre-computed callable_target_summaries from PreparedBuild, consistent with the caching pattern introduced in this PR. The .is_empty() guard appropriately prevents creating a Task tool when there are no callable targets.

Comment thread src/llm-coding-tools-core/benches/permissions.rs Outdated
…cing 0

Add assertion that rule_count >= 1, remove .max(1) on with_capacity, and
replace saturating_sub with plain subtraction so the function fails fast
on invalid input.
@Sewer56 Sewer56 merged commit 0119686 into main Apr 7, 2026
22 checks passed
@Sewer56 Sewer56 deleted the add-external-dir-support branch April 7, 2026 16:56
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