Skip to content

fix: path-scope shell approval targets for out-of-workspace paths - #3591

Open
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3589-20260802-0850
Open

fix: path-scope shell approval targets for out-of-workspace paths#3591
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3589-20260802-0850

Conversation

@praisonai-triage-agent

@praisonai-triage-agent praisonai-triage-agent Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #3589

Summary

The built-in execute_command shell tool bypassed the workspace-containment boundary that file tools enforce: its approval scope/session grants flowed through build_permission_target as a flat bash:<command>, so a broad bash:* / "allow shell" / session grant silently authorised commands touching paths anywhere on the host (~/.ssh, /etc, sibling repos).

This makes build_permission_target (approval/utils.py) emit a distinct shell:external-path:<path> target whenever a shell command touches a path that resolves outside the workspace root. Because the target lives in a different namespace from bash:, a broad bash:*/"allow shell"/session grant no longer covers it, and the offending path(s) are named so the grant is path-scoped β€” mirroring the file tools' edit:<path> / write:<path> targets.

Why minimal / lightweight

  • The PermissionManager already has full shell workspace-boundary machinery (_check_shell_command + _external_dir_target + command_parser). The only gap was the scope/session-target layer used by the built-in shell tool.
  • Reuses existing primitives β€” permissions.command_parser.parse_command (path extraction) and tools.path_safety.resolve_within_root (the same resolver file tools use). No new modules, no new Agent params, no new exports.
  • Default-on and fail-closed (parse/resolve failures return [] so target derivation never breaks a call; the downstream boundary gate still applies). Explicit opt-out via PRAISONAI_SHELL_WORKSPACE_BOUNDARY=0 for trusted sandboxed/CI runs. Workspace root defaults to $PRAISONAI_WORKSPACE_ROOT or cwd.

Behaviour

Command Target
git status -s bash:git status -s (unchanged)
cat ./notes.txt bash:cat ./notes.txt (in-workspace)
cat /etc/passwd shell:external-path:/etc/passwd
echo hi > /tmp/evil.txt shell:external-path:/tmp/evil.txt

Test plan

  • New tests in tests/unit/approval/test_scoped_approval.py cover in-workspace (stays bash:), out-of-workspace read, redirect target, distinct-path scoping, and env opt-out.
  • tests/unit/approval + tests/unit/permissions pass (3 pre-existing unrelated ConsoleBackend naming failures confirmed present on a clean tree).

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added workspace-boundary detection for shell commands.
    • Commands accessing paths outside the configured workspace now receive more specific approval targets.
    • Added an option to disable workspace-boundary checks when needed.
    • Redirects to external paths are also detected.
  • Bug Fixes

    • Improved approval targeting for shell commands that access files outside the workspace.

 #3589)

Make build_permission_target emit a distinct shell:external-path:<path>
target when a shell command touches a path outside the workspace root,
so a broad bash:*/"allow shell"/session grant cannot silently authorise
out-of-workspace access. Reuses the existing command_parser and
path_safety.resolve_within_root primitives (no new modules/params).
Default-on and fail-closed, with a PRAISONAI_SHELL_WORKSPACE_BOUNDARY
opt-out for trusted sandboxed/CI runs.

Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@MervinPraison MervinPraison added the pipeline/blocked:ci Blocked: CI not green on HEAD label Aug 2, 2026
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more β†’

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account β†’

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us β†’

@MervinPraison MervinPraison added pipeline/blocked:manual-review Blocked: requires manual review pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet pipeline/final-claude-pending Reviews done; waiting for FINAL @claude labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 030e9f06-49a4-48c9-bd09-8cc71dfd662a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
πŸ“ Walkthrough

Walkthrough

This change adds workspace-boundary detection for shell permission targets. Commands that access external paths receive path-specific targets. Boundary checks can be disabled through an environment variable. Tests cover in-workspace, external, redirect, and opt-out cases.

Changes

Workspace-bound shell approval

Layer / File(s) Summary
External path detection and target integration
src/praisonai-agents/praisonaiagents/approval/utils.py
Shell commands are parsed for external write targets, path arguments, and path-based executables. External accesses produce shell:external-path:<path> targets. Parsing or resolution failures retain the existing bash:<command> target.
Boundary behavior validation
src/praisonai-agents/tests/unit/approval/test_scoped_approval.py
Tests cover in-workspace commands, external paths, external redirects, and disabling boundary detection with PRAISONAI_SHELL_WORKSPACE_BOUNDARY=0.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly and concisely describes the change to shell approval targets for paths outside the workspace.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ 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 claude/issue-3589-20260802-0850

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.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The revision preserves shell command identity in approval targets so command-specific deny patterns continue to match external-path commands.

  • Keeps execute_command and acp_execute_command targets in the bash:<command> namespace.
  • Adds regression coverage for command-specific denies and workspace-boundary behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported command-deny bypass is resolved by retaining the bash command target.

Important Files Changed

Filename Overview
src/praisonai-agents/praisonaiagents/approval/utils.py Preserves command-bearing shell targets, resolving the previously reported namespace-based deny bypass.
src/praisonai-agents/tests/unit/approval/test_scoped_approval.py Adds focused regression tests showing external commands retain bash identity and command-specific denies still win.

Reviews (2): Last reviewed commit: "fix: preserve shell command identity in ..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/praisonai-agents/praisonaiagents/approval/utils.py`:
- Around line 107-108: Update the path-inspection exception handling in the
relevant target-derivation function so parser, resolver, or lazy-import failures
return a distinct unresolved external target rather than an empty list. Ensure
the downstream logic at the bash target construction does not convert that
failure sentinel into a bash:<command> target or grant approval, and add tests
covering forced parser and resolver failures.

In `@src/praisonai-agents/tests/unit/approval/test_scoped_approval.py`:
- Around line 97-143: Add feature-level coverage alongside the existing scoped
approval tests: include a smoke test and a real agentic test that instantiate
Agent, call agent.start() with a real prompt, invoke the LLM, and print the
complete output. Keep the deterministic unit tests unchanged and exercise the
workspace-boundary behavior through the public agent flow.
πŸͺ„ 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c10d7b8a-8f44-41c7-a4b9-c74a493e5be5

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 119b551 and b2589fe.

πŸ“’ Files selected for processing (2)
  • src/praisonai-agents/praisonaiagents/approval/utils.py
  • src/praisonai-agents/tests/unit/approval/test_scoped_approval.py

Comment on lines +107 to +108
except Exception: # noqa: BLE001 β€” never break target derivation
return []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Fail closed when path inspection fails.

Lines 107-108 convert a parser, resolver, or lazy-import failure into []. Lines 151-154 then produce bash:<command>. A broad bash:* or session grant can then approve a command that accesses an external path.

Return a distinct unresolved external target on inspection failure. Do not fall back to bash:. Add tests that force parser and resolver failures.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/approval/utils.py` around lines 107 -
108, Update the path-inspection exception handling in the relevant
target-derivation function so parser, resolver, or lazy-import failures return a
distinct unresolved external target rather than an empty list. Ensure the
downstream logic at the bash target construction does not convert that failure
sentinel into a bash:<command> target or grant approval, and add tests covering
forced parser and resolver failures.

Comment on lines +97 to +143
def test_in_workspace_shell_stays_bash(self, tmp_path, monkeypatch):
# A command touching only in-workspace paths keeps its ``bash:`` target.
from praisonaiagents.approval.utils import build_permission_target

monkeypatch.chdir(tmp_path)
target = build_permission_target(
"execute_command", {"command": "cat ./notes.txt"}
)
assert target == "bash:cat ./notes.txt"

def test_out_of_workspace_shell_uses_external_target(self, tmp_path, monkeypatch):
# A command touching a path OUTSIDE the workspace root must earn a
# distinct ``shell:external-path:<path>`` target so a broad ``bash:*``
# / session grant cannot silently authorise it.
from praisonaiagents.approval.utils import build_permission_target

monkeypatch.chdir(tmp_path)
target = build_permission_target(
"execute_command", {"command": "cat /etc/passwd"}
)
assert target == "shell:external-path:/etc/passwd"
# A different escaping path yields a different target (path-scoped).
other = build_permission_target(
"execute_command", {"command": "cat ~/.ssh/id_rsa"}
)
assert other.startswith("shell:external-path:")
assert other != target

def test_redirect_out_of_workspace_flagged(self, tmp_path, monkeypatch):
from praisonaiagents.approval.utils import build_permission_target

monkeypatch.chdir(tmp_path)
target = build_permission_target(
"execute_command", {"command": "echo hi > /tmp/evil.txt"}
)
assert target == "shell:external-path:/tmp/evil.txt"

def test_boundary_opt_out_env(self, tmp_path, monkeypatch):
# The explicit opt-out (sandboxed/CI) restores the plain ``bash:`` target.
from praisonaiagents.approval.utils import build_permission_target

monkeypatch.chdir(tmp_path)
monkeypatch.setenv("PRAISONAI_SHELL_WORKSPACE_BOUNDARY", "0")
target = build_permission_target(
"execute_command", {"command": "cat /etc/passwd"}
)
assert target == "bash:cat /etc/passwd"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟠 Major | πŸ—οΈ Heavy lift

Add the required feature-level tests.

This PR adds deterministic unit tests. It does not add the required smoke test or real agentic test for this feature. Add a test where an Agent calls agent.start() with a real prompt, invokes the LLM, and prints the full output.

As per coding guidelines, β€œEvery feature requires both smoke tests and a real agentic test in which an Agent calls agent.start() with a real prompt, invokes the LLM, and prints the full output.”

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/tests/unit/approval/test_scoped_approval.py` around
lines 97 - 143, Add feature-level coverage alongside the existing scoped
approval tests: include a smoke test and a real agentic test that instantiate
Agent, call agent.start() with a real prompt, invoke the LLM, and print the
complete output. Keep the deterministic unit tests unchanged and exercise the
workspace-boundary behavior through the public agent flow.

Source: Coding guidelines

Comment on lines +152 to +153
if external:
return f"{_SHELL_EXTERNAL_PREFIX}:{','.join(external)}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security External targets bypass command denies

When a command covered by a command-specific deny rule such as bash:rm * references an out-of-workspace path, this replaces the command-bearing target with shell:external-path:<path>, so the deny pattern no longer matches and the prohibited operation can be authorized through the approval path.

How this was verified: The generated shell:external-path:/tmp/file target cannot match bash:rm * under the full-target fnmatch rule matching.

Knowledge Base Used: praisonai-agents Core Library

@MervinPraison

Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK β€” never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params β€” only if absolutely required; we already support many params.
  6. Repo routing: agent-callable tools β†’ PraisonAI-Tools; lifecycle plugins β†’ PraisonAI-Plugins; optional sandbox backends β†’ PraisonAI-Plugins (praisonai.sandbox entry point) β€” request changes if wrongly added to praisonaiagents/

Phase 2: FIX Valid Issues
7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
8. Also independently identify and fix any gaps or issues you find in the changed code β€” do not rely only on prior reviewer feedback
9. Push all code fixes directly to THIS branch (do NOT create a new PR)
10. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
11. If all issues are resolved, approve the PR / close the Issue
12. If blocking issues remain, request changes / leave clear action items

@MervinPraison MervinPraison added pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:cooldown Blocked: post-push or @claude cooldown and removed pipeline/final-claude-pending Reviews done; waiting for FINAL @claude pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet labels Aug 2, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task β€”β€” View job


Final Architecture Review β€” PR #3591 βœ… Fixed & pushed

Verdict: the reviewers found a real security regression that this PR introduced. Fixed by reverting to the correct, already-existing architecture.

What was actually wrong (verified empirically)

build_permission_target replaced bash:<command> with shell:external-path:<path> for out-of-workspace commands. Two defects:

  1. πŸ”΄ Deny bypass (Greptile + CodeRabbit β€” VALID): the new target discards command identity, so a hard deny: bash:rm * no longer matched. Probe confirmed rm /tmp/x (external) resolved to ASK instead of DENY β€” a user could approve a command an admin explicitly denied.
  2. πŸ”΄ Namespace collision (independent finding): PermissionManager._SHELL_PREFIXES = ("bash:", "shell:"), so shell:external-path:... was fed into _check_shell_command and parsed as a bogus command (external-path:/tmp/evil.txt treated as an executable).

Root cause / why the fix is a revert, not more code

The out-of-workspace boundary is already fully implemented in PermissionManager (_check_shell_command + _external_dir_target). With a workspace_root configured, the existing machinery already does exactly what the PR wanted β€” proven by probe on a plain bash:<command> target:

Scenario Result
bash:* allow + external cat /etc/passwd ASK βœ… (PR goal, already works)
bash:* allow + in-workspace ALLOW βœ… (no regression)
deny: bash:rm * + external rm /tmp/x DENY βœ… (deny identity preserved)

The PR re-implemented boundary detection at the wrong layer (the approval-target string) and broke deny in the process. The correct SDK-lightweight fix is to keep build_permission_target returning the verbatim bash:<command> and let the existing PermissionManager boundary gate enforce scoping.

I also rejected two tempting-but-wrong expansions as scope creep / regressions:

  • Defaulting workspace_root to cwd inside PermissionManager.__init__ β†’ broke a documented backward-compat contract (test_no_workspace_root_unchanged); reverted.
  • Adding a default_workspace_root() helper wired into the approval flow β†’ dead code with no live enforcement consumer (enforcement check() on the command target runs in the consumer's manager, e.g. praisonai-code, which is out of Python-package scope); removed.

Files modified

  • src/praisonai-agents/praisonaiagents/approval/utils.py β€” removed the shell:external-path namespace + _shell_external_paths helper (and now-unused os/List imports); shell tools map back to bash:<command>, preserving command identity.
  • src/praisonai-agents/tests/unit/approval/test_scoped_approval.py β€” replaced the target-mangling assertions with TestShellWorkspaceBoundary covering: broad bash:* does not cover external paths (ASK), in-workspace stays ALLOW, command-specific deny still fires on external paths (regression guard), and backward-compat with no workspace_root.

Skipped (with reason)

  • CodeRabbit "real agentic test" (agent.start() + live LLM): external-path targeting is a pure deterministic string/permission helper with no LLM in the loop; a live-LLM test adds cost/flakiness without exercising this logic. Covered deterministically instead.
  • CodeRabbit "fail-closed on parse failure": moot β€” the _shell_external_paths helper that could fail was removed. PermissionManager._external_dir_target (the live boundary path) already fails closed (gates behind external_dir: on import/resolve failure).
  • 3 pre-existing ConsoleBackend naming test failures β€” confirmed present on the clean committed tree (stash-verified); unrelated to this PR.

Tests

tests/unit/approval/test_scoped_approval.py + tests/unit/permissions: 202 passed. Full tests/unit/approval: only the 3 pre-existing unrelated failures remain.

Verdict: βœ… Approve

The introduced deny-bypass and namespace collision are resolved; the change is now minimal, backward-compatible, and relies on the existing hardened PermissionManager boundary machinery. No new params, modules, or exports added.
β€’ Branch: claude/issue-3589-20260802-0850 β€’ commit 308d98443

The previous approach replaced the bash:<command> approval target with a
shell:external-path:<path> target for out-of-workspace commands. This both
broke command-specific deny rules (a hard `deny: bash:rm *` no longer matched,
regressing DENY -> ASK and letting a user approve a denied command) and
collided with the existing `shell:` shell-prefix namespace in
PermissionManager.check().

The out-of-workspace boundary is already fully implemented in
PermissionManager (`_check_shell_command` + `_external_dir_target`): with a
workspace_root configured, a broad `bash:*` allow escalates external paths to
ASK while a command-specific deny still fires. build_permission_target now
keeps the verbatim `bash:<command>` target so that machinery applies and
command identity is preserved.

- approval/utils.py: drop the redundant shell:external-path namespace and the
  _shell_external_paths helper; shell tools map back to bash:<command>.
- tests: replace target-mangling assertions with boundary-enforcement tests
  that verify (a) broad bash:* does not cover external paths, (b) in-workspace
  stays allowed, (c) command-specific deny still fires on external paths, and
  (d) backward-compat with no workspace_root.

Fixes the deny-bypass flagged by Greptile and CodeRabbit.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison added pipeline/blocked:stale-final Blocked: FINAL stale after new commits and removed pipeline/blocked:stale-final Blocked: FINAL stale after new commits labels Aug 2, 2026
@praisonai-triage-agent praisonai-triage-agent Bot removed pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:cooldown Blocked: post-push or @claude cooldown labels Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:manual-review Blocked: requires manual review

Projects

None yet

1 participant