Skip to content

Read .py attachments instead of executing them (PythonReader.parse) - #36

Open
EvolveAegis wants to merge 1 commit into
metauto-ai:mainfrom
EvolveAegis:fix/py-reader-read-not-exec
Open

Read .py attachments instead of executing them (PythonReader.parse)#36
EvolveAegis wants to merge 1 commit into
metauto-ai:mainfrom
EvolveAegis:fix/py-reader-read-not-exec

Conversation

@EvolveAegis

Copy link
Copy Markdown

This PR makes PythonReader.parse return the file's source as text instead of running it.

The sink

At commit c23a827f561c934ce21dd950408f7606aa4a8821 (current main HEAD), swarm/environment/tools/reader/readers.py:277 (PythonReader.parse):

completed_process = subprocess.run(["python", file_path], capture_output=True, text=True, check=True)

READER_MAP[".py"] = PythonReader() (readers.py:357). On the default GAIA graphs the FileAnalyse node auto-reads task attachments: FileAnalyse.file_analyse (swarm/environment/operations/file_analyse.py:67) calls GeneralReader.read, which calls FileReader.read_file, which dispatches to PythonReader.parse. experiments/run_gaia.py:102 pulls those attachments from an operator-selected local dataset dir (args.dataset_files), so a .py attachment is executed during a normal "read the file" step — unsandboxed, no timeout, at the agent's privileges. check=True only raises after the interpreter has already run the file's side effects. The other readers parse their input; this one executes it.

Changes

PythonReader.parse returns the source text:

class PythonReader(Reader):
    def parse(self, file_path: Path) -> str:
        return Path(file_path).read_text(errors="replace")

The subprocess call and the surrounding exec-result / error branches are removed. GeneralReader.read (readers.py:397-403) currently indexes the .py result as a 3-tuple (file_content[0] / [1] / [2]), so that branch has to be updated to treat the result as a plain string — otherwise a string return is indexed character-by-character and the .py output formatting silently breaks. After the change it matches the existing txt/json/etc. branch (readers.py:409):

if suffix in ['py', 'java', 'cpp', 'c', 'js', 'css', 'html', 'htm', 'xml']:
    files_content += f'\nThe {suffix} file contains:\n---\n{file_content}\n---'

If executing .py attachments is genuinely intended somewhere, it should be explicit opt-in behind a real sandbox (separate process, no network, timeout=, dropped privileges), not the default for "read this file".

Repro

Run against commit c23a827f561c934ce21dd950408f7606aa4a8821:

NONCE="poc_$(date +%s)"
printf 'open("/tmp/canary_%s","w").write("hit")\n' "$NONCE" > /tmp/attachment_$NONCE.py
cd <repo root>
python - <<PY
from pathlib import Path
from swarm.environment.tools.reader.readers import FileReader
out = FileReader().read_file(f"/tmp/attachment_$NONCE.py")   # str, not Path
print("canary file present:", Path(f"/tmp/canary_$NONCE").exists())
PY

Observed output:

canary file present: True

read_file is annotated file_path: Path (readers.py:367) but calls file_path.split(".") (readers.py:368), so it requires a str; passing a Path raises AttributeError: 'PosixPath' object has no attribute 'split'. The annotation is worth correcting alongside this PR.

After this PR, read_file returns the source text and /tmp/canary_* is not created.

A test is included: reading a .py whose top level writes a marker file must not produce the marker; parse returns the file text.

Scope

This is defense-in-depth. The runner takes attachments from an operator-selected local dataset dir, so turning read-is-exec into an attack needs the operator to point the agent at an untrusted dataset/upload dir — the read-is-exec primitive itself (default-reachable, unsandboxed) is real regardless. The interpreter is hardcoded as python (not sys.executable), so on hosts without python on PATH the FileNotFoundError is swallowed by the existing generic except and nothing runs; the conda quickstart provides python, but that limits which hosts are affected.

PythonReader.parse ran attachments with subprocess.run(["python", file])
during a normal file-read step, unsandboxed. Return the source text
instead, like the other readers, and treat .py as a text file in
GeneralReader.read. Corrects the read_file annotation to str (it calls
.split('.')). Adds a test that reading a .py file does not execute it.
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.

2 participants