Skip to content

[Bug] Confine the autonomous-loop file tools to the agent workspace (#1791) - #1815

Open
ayaangazali wants to merge 1 commit into
kyegomez:masterfrom
ayaangazali:security/confine-file-tools-to-workspace
Open

[Bug] Confine the autonomous-loop file tools to the agent workspace (#1791)#1815
ayaangazali wants to merge 1 commit into
kyegomez:masterfrom
ayaangazali:security/confine-file-tools-to-workspace

Conversation

@ayaangazali

Copy link
Copy Markdown
Contributor

Fixes #1791

Problem

The built-in file tools in max_loops="auto" have no path confinement. The model picks file_path itself, so any prompt injection reaching the loop, a fetched web page, a file the agent was asked to summarise, a sub-agent's task string, becomes arbitrary local file read, overwrite or deletion under the host process's privileges.

Reproduced on master 06413ebc with a tmp workspace and a secret one level above it:

escape path: ../../../outside_secret.txt
read_file via ../    -> LEAKED
read_file /etc/hosts -> LEAKED

Six tools shared the same two-hole resolver, read_file_tool, create_file_tool, update_file_tool, list_directory_tool, delete_file_tool and grep_tool:

        if not os.path.isabs(file_path):
            workspace_dir = agent._get_agent_workspace_dir()
            full_path = os.path.join(workspace_dir, file_path)   # ".." never normalised
        else:
            full_path = file_path                                 # absolute taken verbatim

Fix

One resolver, six call sites, no per-tool guards:

def _resolve_in_workspace(agent: Any, path: str) -> str:
    workspace = os.path.realpath(agent._get_agent_workspace_dir())
    full_path = (
        path
        if path and os.path.isabs(path)
        else os.path.join(workspace, path or "")
    )
    resolved = os.path.realpath(full_path)

    if resolved != workspace and not resolved.startswith(workspace + os.sep):
        raise ValueError(
            f"Path is outside the agent workspace and was refused: {path}"
        )

    return resolved

realpath is what makes it hold: it collapses .. and follows symlinks before the containment test, so neither can step outside. Each tool's six-line resolve block becomes one line, and the ValueError lands in the except Exception each tool already has, so the model gets the usual error string rather than a traceback. Net +43/-45 in one source file.

Absolute paths are still accepted when they point inside the workspace, so the check is containment rather than "relative only" and nothing legitimate is lost:

read inside.txt            OK
read <abs path in ws>      OK
list workspace             OK
grep workspace             OK
create in workspace        OK
read ../../../secret       blocked
read /etc/hosts            blocked
delete ../../../secret     blocked   (file still present afterwards)

swarms/tools/computer_use.py already has the right primitive in _check_realpath, but create_computer_use_tools has no call sites in swarms/, so the loop never saw it. I kept this fix local to autonomous_loop_utils.py rather than rewiring the loop onto that module, which is a much larger change.

This is also the piece that makes #1751's proposed default policy safe: that issue auto-approves read_file, grep and list_directory, which under master means auto-approving arbitrary filesystem reads.

Test

New tests/structs/test_autonomous_loop_file_confinement.py. There is no existing test module for autonomous_loop_utils.py, so this is a new file rather than an addition to one. No agent and no network: the workspace is a tmp_path and the agent is a mock that only answers _get_agent_workspace_dir.

Two tests. The first drives all six tools at an escape path, by .. and by absolute path, and also asserts the secret's contents are unchanged after create_file and update_file are pointed at it. The second pins that ordinary workspace access still works, including absolute paths inside it.

On master:

AssertionError: read_file relative did not refuse the escape: SECRET-OUTSIDE

Both pass on this branch. tests/structs/test_async_subagent.py, the only other suite touching these tools, is unchanged. tests/structs/test_agent.py is 21 failed / 41 passed / 8 errors both before and after, all pre-existing.

black --check and ruff check clean at line-length 70.

I use Claude Code to help me work through these and I reproduce every claim before opening anything. Since this one is a hardening change rather than a crash fix, if you would rather it warn instead of refuse while you assess the blast radius, that is a one-line change and I will send it.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 5, 2026 21:24
@ayaangazali
ayaangazali requested a review from kyegomez as a code owner August 5, 2026 21:24

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Hello there, thank you for opening an PR ! 🙏🏻 The team was notified and they will get back to you asap.

@ayaangazali
ayaangazali force-pushed the security/confine-file-tools-to-workspace branch from 198830a to 528d244 Compare August 6, 2026 17:17
@ayaangazali

Copy link
Copy Markdown
Contributor Author

Rebased onto current master 16afc758 (it was based on 06413ebc, before #1813 merged), and re-verified everything against the new base:

  • both new tests pass on this branch
  • with only autonomous_loop_utils.py reverted to master, the first one fails again: AssertionError: read_file relative did not refuse the escape: SECRET-OUTSIDE
  • tests/structs/test_async_subagent.py, the only other suite touching these tools, is 13 failed / 34 passed on master and on this branch alike
  • black --check and ruff check clean on both files I touch, verified with the versions CI pins (black==24.2.0, ruff==0.2.1), not my local ones

On the red checks, for the record, since none of them are caused by this PR. Pulled from this PR's own fresh job logs:

Master itself is red on the same set, so the signal here is "unchanged from base" rather than "passing".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug][Autonomous loop][File tools accept ../ and absolute paths, so the agent can read, write and delete anywhere on the filesystem]

2 participants