Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions skillopt/envs/spreadsheetbench/codegen_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,17 +260,34 @@ def _build_codex_driver() -> str:
return (
"import pathlib\n"
"import re\n"
"import sys\n"
"import traceback\n\n"
"import subprocess\n"
"import sys\n\n"
'INPUT_PATH = "input.xlsx"\n'
'OUTPUT_PATH = "output.xlsx"\n'
"code = pathlib.Path('solution.py').read_text(encoding='utf-8')\n"
"code = re.sub(r'^\\s*(INPUT_PATH|OUTPUT_PATH)\\s*=\\s*.+$', '', code, flags=re.MULTILINE)\n"
"globals_dict = {'__name__': '__main__', 'INPUT_PATH': INPUT_PATH, 'OUTPUT_PATH': OUTPUT_PATH}\n"
"try:\n"
" exec(compile(code, 'solution.py', 'exec'), globals_dict, globals_dict)\n"
"except Exception:\n"
" traceback.print_exc()\n"
"# Write patched code to a temporary file and run it in a clean subprocess\n"
"# (avoids exec/compile of untrusted LLM-generated code in the current process).\n"
"_patched = pathlib.Path('_driver_runner.py')\n"
"_patched.write_text(\n"
" f'INPUT_PATH = {INPUT_PATH!r}\\nOUTPUT_PATH = {OUTPUT_PATH!r}\\n' + code,\n"
" encoding='utf-8',\n"
")\n"
"import os as _os\n"
"_safe_env = {\n"
" 'PATH': _os.environ.get('PATH', '/usr/bin:/bin'),\n"
" 'HOME': str(pathlib.Path('_driver_runner.py').parent.resolve()),\n"
"}\n"
"if _os.name == 'nt':\n"
Comment on lines +277 to +281
" _safe_env['SYSTEMROOT'] = _os.environ.get('SYSTEMROOT', '')\n"
" _safe_env['TEMP'] = _safe_env['HOME']\n"
" _safe_env['TMP'] = _safe_env['HOME']\n"
"_safe_env = {k: v for k, v in _safe_env.items() if v}\n"
"_res = subprocess.run([sys.executable, str(_patched)], capture_output=True, text=True, env=_safe_env)\n"
"if _res.returncode != 0:\n"
" import traceback as _tb\n"
" print(_res.stdout, end='')\n"
" print(_res.stderr, end='')\n"
" sys.exit(2)\n"
)

Expand Down
15 changes: 15 additions & 0 deletions skillopt/envs/spreadsheetbench/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,27 @@ def run_generated_code(code: str, input_path: str, output_path: str, timeout: in
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f:
f.write(script)
tmp = f.name
# Build a minimal environment so the generated code cannot read API keys,
# cloud credentials, or other secrets from the current process environment.
import platform as _platform
_safe_env: dict[str, str] = {
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
"HOME": os.path.dirname(output_path),
"TMPDIR": tempfile.gettempdir(),
}
if _platform.system() == "Windows":
_safe_env["SYSTEMROOT"] = os.environ.get("SYSTEMROOT", "")
_safe_env["TEMP"] = tempfile.gettempdir()
_safe_env["TMP"] = tempfile.gettempdir()
# Drop empty entries (env dict values must be non-empty strings)
_safe_env = {k: v for k, v in _safe_env.items() if v}
try:
proc = subprocess.run(
[sys.executable, tmp],
capture_output=True,
text=True,
timeout=timeout if timeout and timeout > 0 else None,
env=_safe_env,
)
if proc.returncode != 0:
return False, (proc.stdout + "\n" + proc.stderr).strip()
Expand Down
46 changes: 46 additions & 0 deletions tests/test_executor_env_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for subprocess environment isolation in the spreadsheet executor.

``run_generated_code`` runs LLM-generated Python in a child process. To avoid
leaking API keys / cloud credentials into untrusted generated code, the child
must run with a minimal, scrubbed environment rather than inheriting the
parent process environment. These tests assert that scrubbing behaviour.
"""
from __future__ import annotations

import os

from skillopt.envs.spreadsheetbench.executor import run_generated_code
Comment on lines +10 to +12


# User code that records whether a given env var is visible to the child.
_PROBE = (
"import os\n"
"with open(OUTPUT_PATH, 'w', encoding='utf-8') as _f:\n"
" _f.write(os.environ.get('SUPER_SECRET_TOKEN', 'ABSENT'))\n"
)


def test_secret_env_not_visible_to_generated_code(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("SUPER_SECRET_TOKEN", "leak-me-please")
out = tmp_path / "out.txt"

ok, err = run_generated_code(_PROBE, str(tmp_path / "in.xlsx"), str(out))

assert ok, err
assert out.read_text(encoding="utf-8") == "ABSENT"


def test_path_still_available_to_generated_code(tmp_path) -> None:
# PATH must be preserved so the child can still locate the interpreter's
# tooling; only sensitive vars are dropped.
probe = (
"import os\n"
"with open(OUTPUT_PATH, 'w', encoding='utf-8') as _f:\n"
" _f.write('YES' if os.environ.get('PATH') else 'NO')\n"
)
out = tmp_path / "out.txt"

ok, err = run_generated_code(probe, str(tmp_path / "in.xlsx"), str(out))

assert ok, err
assert out.read_text(encoding="utf-8") == "YES"