Skip to content

Expand shell patterns in permission rules at construction time#92

Merged
Sewer56 merged 1 commit intomainfrom
implement-permission-path-expansion
Apr 7, 2026
Merged

Expand shell patterns in permission rules at construction time#92
Sewer56 merged 1 commit intomainfrom
implement-permission-path-expansion

Conversation

@Sewer56
Copy link
Copy Markdown
Member

@Sewer56 Sewer56 commented Apr 7, 2026

Summary

  • Shell patterns (~/, $HOME/, $VAR, ${VAR:-default}) in permission rules are now expanded eagerly when a Rule is constructed, rather than being stored as raw patterns that may never match real paths.
  • Extracts a new expand_pattern() function in normalize.rs that returns Cow<str> (zero-allocation for patterns without shell metacharacters), with expand_shell() reimplemented as a thin wrapper.
  • Adds integration tests verifying tilde, $HOME, and plain patterns all evaluate correctly through the ruleset.

Changed files

File Change
permissions.rs Expand patterns in Rule::new; add integration tests
normalize.rs Extract expand_pattern(), refactor & add unit tests
mod.rs (path, allowed_glob) Increase visibility to pub(crate) for cross-module access

- Add canonical expand_pattern in normalize.rs; expand_shell is now a thin wrapper
- Rule::new() expands ~/, $HOME/, $VAR in patterns with zero-alloc passthrough
- Deduplicate shell expansion tests into normalize.rs; permissions.rs keeps integration tests
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.46%. Comparing base (0119686) to head (e30df5a).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/llm-coding-tools-core/src/permissions.rs 80.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #92      +/-   ##
==========================================
+ Coverage   81.44%   81.46%   +0.02%     
==========================================
  Files         106      106              
  Lines        4343     4349       +6     
==========================================
+ Hits         3537     3543       +6     
  Misses        806      806              
Flag Coverage Δ
async 80.65% <87.50%> (-0.03%) ⬇️
blocking 58.56% <87.50%> (+0.22%) ⬆️

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

Files with missing lines Coverage Δ
...llm-coding-tools-core/src/path/allowed_glob/mod.rs 82.66% <ø> (ø)
...ding-tools-core/src/path/allowed_glob/normalize.rs 100.00% <100.00%> (ø)
src/llm-coding-tools-core/src/path/mod.rs 75.75% <ø> (ø)
src/llm-coding-tools-core/src/permissions.rs 96.58% <80.00%> (-0.77%) ⬇️

... and 4 files with indirect coverage changes

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Walkthrough

The pull request exposes the allowed_glob module and its normalize submodule as crate-visible by changing their visibility from private to pub(crate). A new expand_pattern helper function is introduced in normalize.rs that wraps shellexpand::full() and returns a Cow type for zero-allocation when no shell metacharacters are present. The expand_shell function is refactored to use this helper. The Rule::new constructor in permissions.rs is updated to call expand_pattern on input patterns before storing them, integrating shell-style pattern expansion (tilde and $HOME variables) into rule construction. Tests are added to cover the new expansion functionality.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Expand shell patterns in permission rules at construction time' accurately and concisely summarizes the main change: eager shell pattern expansion in Rule construction.
Description check ✅ Passed The description covers all key changes with clear explanations of the refactoring, new functions, and test additions, though the template is minimal and doesn't require extensive detail.
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 implement-permission-path-expansion

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: 1

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

780-797: Consider adding a test for the expansion failure fallback.

This test verifies patterns without shell syntax still work (good regression guard), but there's no test verifying the behavior when expansion fails. A test like the following would document the fallback behavior:

#[test]
fn rule_with_unset_env_var_stores_unexpanded_pattern() {
    temp_env::with_var("DEFINITELY_NOT_SET_12345", None::<&str>, || {
        let rule = Rule::new("read", "$DEFINITELY_NOT_SET_12345/path/*", PermissionAction::Allow);
        // Documents that unexpanded pattern is stored on failure
        assert_eq!(rule.pattern(), "$DEFINITELY_NOT_SET_12345/path/*");
    });
}

This would make the fallback behavior explicit and serve as documentation for future maintainers.

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

In `@src/llm-coding-tools-core/src/permissions.rs` around lines 780 - 797, Add a
new unit test that verifies the expansion-failure fallback: use
temp_env::with_var to unset an env var, create a Rule via Rule::new("read",
"$DEFINITELY_NOT_SET_12345/path/*", PermissionAction::Allow), and assert that
rule.pattern() returns the original unexpanded string
"$DEFINITELY_NOT_SET_12345/path/*" to document that on expansion failure the raw
pattern is stored; place the test alongside the other rule tests and name it
e.g. rule_with_unset_env_var_stores_unexpanded_pattern.
🤖 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/src/permissions.rs`:
- Around line 131-136: Rule::new currently swallows expand_pattern errors and
stores the unexpanded literal, causing silent mismatches; change Rule::new to
return Result<Rule, ExpandError> (or a suitable error type) instead of
constructing a Rule on expansion failure, propagate this Result to callers such
as from_permission_config in extensions.rs, and update call sites to handle or
surface the error so misconfigured patterns (e.g., missing $HOME) are reported
instead of silently failing; ensure you reference expand_pattern, Rule::new, and
from_permission_config when making the signature and call-site changes.

---

Nitpick comments:
In `@src/llm-coding-tools-core/src/permissions.rs`:
- Around line 780-797: Add a new unit test that verifies the expansion-failure
fallback: use temp_env::with_var to unset an env var, create a Rule via
Rule::new("read", "$DEFINITELY_NOT_SET_12345/path/*", PermissionAction::Allow),
and assert that rule.pattern() returns the original unexpanded string
"$DEFINITELY_NOT_SET_12345/path/*" to document that on expansion failure the raw
pattern is stored; place the test alongside the other rule tests and name it
e.g. rule_with_unset_env_var_stores_unexpanded_pattern.
🪄 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: ea23cbe5-3caf-432c-bc4c-1020180b3e2f

📥 Commits

Reviewing files that changed from the base of the PR and between 0119686 and e30df5a.

📒 Files selected for processing (4)
  • src/llm-coding-tools-core/src/path/allowed_glob/mod.rs
  • src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs
  • src/llm-coding-tools-core/src/path/mod.rs
  • src/llm-coding-tools-core/src/permissions.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: Semver Checks (Serdesai Full+Linux)
  • GitHub Check: Semver Checks (Serdesai Full)
  • GitHub Check: Async macOS
  • GitHub Check: Async Linux
  • GitHub Check: Blocking Windows
  • GitHub Check: Blocking macOS
  • GitHub Check: Async Windows
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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`.
📚 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/path/allowed_glob/mod.rs
  • src/llm-coding-tools-core/src/path/mod.rs
  • src/llm-coding-tools-core/src/permissions.rs
  • src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs
🔇 Additional comments (9)
src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs (4)

29-37: LGTM! Clean extraction of the pattern expansion logic.

The new expand_pattern function correctly wraps shellexpand::full and exposes the Cow return type, enabling zero-allocation passthrough when no shell metacharacters are present. This is a good design for reuse across modules.


43-52: LGTM! Clean refactor using the new helper.

The function correctly delegates to expand_pattern and handles the conversion to PathBuf. The error message now includes more context about what failed.


130-151: Good coverage for tilde expansion.

The #[cfg(not(windows))] guard is appropriate since tilde expansion relies on the Unix HOME environment variable behavior.


175-189: Nice use of parameterized tests for the zero-allocation passthrough.

The rstest cases effectively verify that patterns without shell metacharacters return Cow::Borrowed, confirming the optimization works as intended.

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

6-6: LGTM! Visibility change enables crate-internal reuse.

The pub(crate) visibility is appropriate for sharing expand_pattern with permissions.rs while keeping it internal to the crate.

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

10-10: LGTM! Required visibility adjustment for the module hierarchy.

This change, along with the pub(crate) mod normalize; change, establishes the import path crate::path::allowed_glob::normalize::expand_pattern used in permissions.rs.

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

57-59: LGTM! Necessary imports for the expansion integration.


738-755: Good test coverage for tilde expansion when HOME is set.

The test correctly verifies that a ~/projects/* pattern matches the expanded path.


757-778: Good test coverage for $HOME expansion.

The test correctly verifies that $HOME/.config/* expands and matches real paths.

Comment on lines +131 to +136
let pattern_box: Box<str> = pattern.into();
let pattern: Box<str> = match expand_pattern(&pattern_box) {
Ok(Cow::Borrowed(_)) => pattern_box,
Ok(Cow::Owned(s)) => s.into_boxed_str(),
Err(_) => pattern_box,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are other callers of Rule::new that might be affected by silent fallback
rg -nP --type=rust '\bRule::new\s*\(' -C2

Repository: Sewer56/llm-coding-tools

Length of output: 16756


🏁 Script executed:

#!/bin/bash
# First, let's look at the Rule::new implementation in detail (lines 125-150)
sed -n '125,150p' src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 988


🏁 Script executed:

#!/bin/bash
# Now let's check the expand_pattern function to understand its error handling
rg -nP 'fn expand_pattern|pub fn expand_pattern' -A10 src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if there are tests specifically for pattern expansion failures
rg -nP 'expand_pattern.*Err|test.*expand|test.*unset' -C3 src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Look at extensions.rs to see how patterns from YAML are passed to Rule::new
cat -n src/llm-coding-tools-agents/src/extensions.rs | sed -n '40,70p'

Repository: Sewer56/llm-coding-tools

Length of output: 1218


🏁 Script executed:

#!/bin/bash
# Search for expand_pattern definition more broadly
rg -n 'expand_pattern' src/llm-coding-tools-core/src/permissions.rs | head -20

Repository: Sewer56/llm-coding-tools

Length of output: 201


🏁 Script executed:

#!/bin/bash
# Check the imports at the top of permissions.rs to see where expand_pattern comes from
head -70 src/llm-coding-tools-core/src/permissions.rs | cat -n

Repository: Sewer56/llm-coding-tools

Length of output: 3261


🏁 Script executed:

#!/bin/bash
# Look for the expand_pattern function in all files
fd -e rs -x rg -l 'fn expand_pattern' {}

Repository: Sewer56/llm-coding-tools

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Check tests that use pattern expansion - look around lines 734-797 mentioned in the scratchpad
sed -n '730,800p' src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 2614


🏁 Script executed:

#!/bin/bash
# Now examine the expand_pattern function to understand when it returns Err
cat src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs | head -100

Repository: Sewer56/llm-coding-tools

Length of output: 3048


🏁 Script executed:

#!/bin/bash
# Look for expand_pattern's full implementation
rg -n 'pub fn expand_pattern' -A30 src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for expansion failures (e.g. with unset vars)
rg -nP 'expand_pattern|Err\(' src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs | head -30

Repository: Sewer56/llm-coding-tools

Length of output: 775


🏁 Script executed:

#!/bin/bash
# Look for the full expand_pattern_should_return_error_on_failure test
sed -n '192,200p' src/llm-coding-tools-core/src/path/allowed_glob/normalize.rs

Repository: Sewer56/llm-coding-tools

Length of output: 298


🏁 Script executed:

#!/bin/bash
# Check if there are any tests in permissions.rs that test expansion failures in Rule::new
rg -nP 'test.*expansion.*fail|test.*unset.*var|Err.*expand' src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check git log to see if there's any discussion about the fallback behavior
git log --oneline -n 20 -- src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 147


🏁 Script executed:

#!/bin/bash
# Look at the git commit to understand the design intent
git show e30df5a --stat | head -20

Repository: Sewer56/llm-coding-tools

Length of output: 1150


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or docs explaining why the error is silently ignored
sed -n '125,150p' src/llm-coding-tools-core/src/permissions.rs | grep -A5 -B5 'Err'

Repository: Sewer56/llm-coding-tools

Length of output: 583


🏁 Script executed:

#!/bin/bash
# Verify the exact behavior: create a simple test scenario
# Check how rules are evaluated - do they store the unexpanded pattern?
rg -n 'fn evaluate|pattern_hash' src/llm-coding-tools-core/src/permissions.rs | head -15

Repository: Sewer56/llm-coding-tools

Length of output: 1004


🏁 Script executed:

#!/bin/bash
# Let's verify: if expansion fails, the unexpanded pattern gets stored
# Check if there are any tests that verify this behavior explicitly
rg -nP 'UNSET|expansion.*fail|fallback' -B5 -A5 src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Double-check the commit message and PR intent
git show e30df5a --format=fuller | head -40

Repository: Sewer56/llm-coding-tools

Length of output: 1358


🏁 Script executed:

#!/bin/bash
# Check if there's any logging or warning infrastructure in place for this
rg -n 'log::|warn!|tracing::' src/llm-coding-tools-core/src/permissions.rs | head -10

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Let's verify the actual behavior by checking what happens with an unexpanded pattern
# Check if there are any runtime tests or examples that might show this scenario
rg -nP 'HOME.*unset|expansion.*fail|unexpanded' -B3 -A3 src/llm-coding-tools-core/src/

Repository: Sewer56/llm-coding-tools

Length of output: 875


🏁 Script executed:

#!/bin/bash
# Final check: look at the actual matching logic to confirm unexpanded patterns won't match
sed -n '255,290p' src/llm-coding-tools-core/src/permissions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 1345


🏁 Script executed:

#!/bin/bash
# Let's check if there are integration tests that verify the behavior when vars are unset
rg -nP '#\[test\]' -A20 src/llm-coding-tools-core/src/permissions.rs | grep -A20 'unset\|UNSET\|fail'

Repository: Sewer56/llm-coding-tools

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check the extensions.rs tests to see if they test expansion failures
sed -n '64,150p' src/llm-coding-tools-agents/src/extensions.rs

Repository: Sewer56/llm-coding-tools

Length of output: 2946


Silent fallback on pattern expansion failure may cause permission rules to silently fail matching.

When expand_pattern fails (e.g., $UNSET_VAR or HOME unset), the original unexpanded pattern is stored in the rule. This causes silent mismatches:

  • A rule created with Rule::new("read", "~/workspace/*", Allow) when HOME is unset will store the literal ~/workspace/*
  • When evaluated against a real path like /home/user/workspace/file.txt, it will never match because the literal pattern won't match the expanded path

This is particularly problematic because from_permission_config in extensions.rs passes user-supplied patterns from YAML configuration directly to Rule::new. A config like:

permissions:
  read:
    "~/workspace/*": allow

will silently break if HOME is unset at rule construction time, with no warning to the user.

Consider either:

  1. Logging a warning when expansion fails, so users can diagnose misconfigurations
  2. Returning Result from Rule::new to surface expansion failures to callers (similar to how expand_shell handles errors)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/llm-coding-tools-core/src/permissions.rs` around lines 131 - 136,
Rule::new currently swallows expand_pattern errors and stores the unexpanded
literal, causing silent mismatches; change Rule::new to return Result<Rule,
ExpandError> (or a suitable error type) instead of constructing a Rule on
expansion failure, propagate this Result to callers such as
from_permission_config in extensions.rs, and update call sites to handle or
surface the error so misconfigured patterns (e.g., missing $HOME) are reported
instead of silently failing; ensure you reference expand_pattern, Rule::new, and
from_permission_config when making the signature and call-site changes.

@Sewer56 Sewer56 merged commit 5494544 into main Apr 7, 2026
22 checks passed
@Sewer56 Sewer56 deleted the implement-permission-path-expansion branch April 7, 2026 18:55
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