Skip to content

Shell/exec tool bypasses the workspace-containment boundary enforced for file tools — add path-scoped, workspace-aware approval for command execution #3589

Description

@MervinPraison

Summary

The built-in file tools (read/write/edit/delete) resolve every path through the shared workspace-containment resolver in praisonaiagents/tools/path_safety.py (resolve_within_root/is_within_root) and receive path-scoped permission targets (edit:<path>, write:<path>) via approval/utils.py:build_permission_target. The built-in shell/exec tool does neither. ShellTool.execute (praisonaiagents/tools/shell_tools.py) hands the command straight to a subprocess behind a single blanket @require_approval(risk_level="critical") gate, never consults path_safety, and its cwd can even fall back to the user's home directory.

The result is an inconsistent security posture: an agent can read, mutate or exfiltrate files anywhere on the host (~/.ssh, /etc, sibling repositories) through the shell tool, even though the SDK guarantees workspace containment for its file tools. For CLI-first, production agent runs this is a surprising containment bypass.

Current behaviour

  • File tools — path resolved and constrained to the workspace root (path_safety.py:25 resolve_within_root); per-path approval target built via approval/utils.py:67 build_permission_targetedit:<path> / write:<path> (see _FILE_TOOL_PREFIXES).
  • Shell tooltools/shell_tools.py:47 @require_approval(risk_level="critical") is the only gate; no import of path_safety; the command body and any file paths inside it are never extracted or checked; cwd accepts arbitrary/home-directory locations (shell_tools.py:107 home-dir fallback). Once a broad shell approval (or a command-prefix allow) is granted, subsequent commands are not re-evaluated against which paths they touch.

Desired behaviour

  • Before executing a shell command, extract the filesystem paths it will touch and classify each as in-workspace or out-of-workspace using the existing path_safety resolver.
  • Commands touching only in-workspace paths follow the normal approval flow; commands touching paths outside the resolved workspace root raise a distinct approval target (e.g. shell:external-path) that names the offending path(s), so a broad "allow shell" grant does not silently authorise operations outside the project.
  • Honour a configurable workspace root (default cwd/git root, consistent with the resolver), remain fail-closed, and stay backward-compatible with an explicit opt-out for sandboxed/CI runs.

Layer placement

  • Primary layer: core (src/praisonai-agents/praisonaiagents/)
  • Why not core: this is core — the shell tool, path_safety, the approval registry and build_permission_target all live in praisonaiagents.
  • Why not wrapper: the wrapper only surfaces flags; fixing it there would leave direct SDK users (who never touch the CLI) unprotected, and the containment primitive being bypassed is a core one.
  • Why not tools (praisonai-tools): the affected shell tool is a built-in in praisonaiagents.tools, not the optional integrations package; duplicating a built-in there is explicitly disallowed.
  • Why not plugins: a policy plugin could add an extra check, but the defect is that a built-in tool ignores the SDK's own containment guarantee; the guarantee must live with the tool + approval core, not an optional plugin a user may never install.
  • Secondary touch (optional): a small command-argument path-extraction helper alongside path_safety.py.
  • 3-way surface (CLI + YAML + Python): yes — Python (Agent(...) tool/exec config), YAML (agent/tool policy), CLI (praisonai run).

Proposed approach

Add a command-argument analyser that identifies filesystem paths in a shell command (starting with the common file-consuming commands and redirections), resolve each with path_safety.resolve_path, and split into in-root vs escaping sets. Extend approval/utils.build_permission_target to emit a shell path-scoped target, and have ShellTool.execute request an out-of-workspace approval whenever any path escapes the root. Gate the whole path behind a config flag defaulting to on (fail-closed), with an explicit opt-out for trusted/sandboxed environments.

Resolution sketch

  • tools/shell_tools.py: before subprocess launch, run the analyser; if escaping paths exist, request approval with a distinct target and the path list; otherwise proceed as today.
  • tools/path_safety.py (or a sibling): extract_command_paths(command) -> list[str], reusing resolve_within_root.
  • approval/utils.py: add a shell → path-scoped prefix so shell commands earn the same path-scoped grants as file tools.
  • Config: an ExecutionConfig/tool-config flag (e.g. shell_workspace_boundary: bool = True) with env/CLI opt-out.

Severity

High — closes a workspace-containment bypass that contradicts the SDK's own guarantee for file tools, and directly affects the production safety of CLI-first agent runs.

Validation

  • tools/path_safety.py:25 resolve_within_root, :37 is_within_root — the shared containment resolver used by file tools.
  • approval/utils.py:67 build_permission_target — file tools get edit:<path>/write:<path>; shell is absent from _FILE_TOOL_PREFIXES.
  • tools/shell_tools.py:47 blanket @require_approval(risk_level="critical"); no path_safety import; :107 cwd home-directory fallback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions