File size: 6,418 Bytes
c1b8df7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""
executor.py β€” Sandbox Verification Engine (Stream B / Mastery 1: Coding)

Provides a secure, isolated Python subprocess executor that runs
LLM-generated code snippets against unit tests with strict timeouts.

The Coding Critic uses this to emit a binary reward signal:
  PASS  β†’ the generated code is functionally correct  (reward = +1)
  FAIL  β†’ the code has runtime errors or test failures (reward = -1)

Security model:
  - Code runs in a *child* subprocess β€” never eval()/exec() in the parent.
  - stdout/stderr are captured; the parent process is never blocked beyond
    ``timeout`` seconds.
  - A minimal ``__builtins__`` restriction note is included in the harness
    (full sandboxing requires OS-level isolation such as Docker; see
    ``infra/docker/Dockerfile.sandbox``).
"""

import subprocess
import sys
import tempfile
import textwrap
import time
from pathlib import Path
from typing import Optional


# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------

DEFAULT_TIMEOUT: int = 10        # seconds before the subprocess is killed
MAX_OUTPUT_CHARS: int = 8_000    # truncate stdout/stderr beyond this


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def run_code(
    code: str,
    test_code: Optional[str] = None,
    timeout: int = DEFAULT_TIMEOUT,
    python_executable: str = sys.executable,
) -> dict:
    """
    Execute *code* in an isolated subprocess, optionally appending *test_code*.

    The combined script is written to a temporary file, executed by the
    *python_executable* interpreter, and the results are returned as a dict.

    Args:
        code:               The LLM-generated Python source to execute.
        test_code:          Optional unit-test source appended after *code*.
                            Should use ``assert`` statements or ``unittest``/
                            ``pytest`` style. If provided, any ``AssertionError``
                            is treated as a FAIL.
        timeout:            Maximum wall-clock seconds allowed for execution.
        python_executable:  Path to the Python interpreter (defaults to the
                            running interpreter so the same environment is used).

    Returns:
        A dict with keys:
          - ``"status"``       : ``"pass"`` | ``"fail"`` | ``"timeout"`` | ``"error"``
          - ``"stdout"``       : captured standard output (str, truncated)
          - ``"stderr"``       : captured standard error  (str, truncated)
          - ``"return_code"``  : process exit code (int), or ``None`` on timeout
          - ``"elapsed_ms"``   : wall-clock milliseconds spent
          - ``"label"``        : 1 = pass, 0 = fail  (for Critic reward)
    """
    # Build the harness script
    harness_parts = [textwrap.dedent(code)]
    if test_code:
        harness_parts.append("\n\n# --- TEST HARNESS ---\n")
        harness_parts.append(textwrap.dedent(test_code))
    full_script = "\n".join(harness_parts)

    # Write to a temp file (auto-deleted after execution)
    with tempfile.NamedTemporaryFile(
        mode="w",
        suffix=".py",
        prefix="pace_sandbox_",
        delete=False,
        encoding="utf-8",
    ) as tmp:
        tmp.write(full_script)
        tmp_path = Path(tmp.name)

    start = time.perf_counter()
    try:
        proc = subprocess.run(
            [python_executable, str(tmp_path)],
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        elapsed_ms = int((time.perf_counter() - start) * 1000)

        stdout = proc.stdout[:MAX_OUTPUT_CHARS]
        stderr = proc.stderr[:MAX_OUTPUT_CHARS]
        return_code = proc.returncode

        if return_code == 0:
            status = "pass"
            label = 1
        else:
            status = "fail"
            label = 0

    except subprocess.TimeoutExpired:
        elapsed_ms = timeout * 1000
        stdout = ""
        stderr = f"Execution timed out after {timeout}s."
        return_code = None
        status = "timeout"
        label = 0

    except Exception as exc:
        elapsed_ms = int((time.perf_counter() - start) * 1000)
        stdout = ""
        stderr = str(exc)
        return_code = -1
        status = "error"
        label = 0

    finally:
        tmp_path.unlink(missing_ok=True)

    return {
        "status": status,
        "stdout": stdout,
        "stderr": stderr,
        "return_code": return_code,
        "elapsed_ms": elapsed_ms,
        "label": label,
    }


def batch_run(
    code_test_pairs: list[tuple[str, Optional[str]]],
    timeout: int = DEFAULT_TIMEOUT,
) -> list[dict]:
    """
    Run multiple (code, test_code) pairs sequentially and return a list of
    result dicts. Useful for batch evaluation during Critic training.

    Args:
        code_test_pairs: List of (code, test_code) tuples.
        timeout:         Per-execution timeout in seconds.

    Returns:
        List of result dicts as returned by :func:`run_code`.
    """
    return [run_code(code, test, timeout=timeout) for code, test in code_test_pairs]


# ---------------------------------------------------------------------------
# CLI smoke-test
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    print("=== Sandbox Executor β€” Smoke Test ===\n")

    # Test 1: correct code
    result = run_code(
        code="def add(a, b): return a + b",
        test_code="assert add(2, 3) == 5\nassert add(-1, 1) == 0\nprint('Tests passed.')",
    )
    print(f"[PASS expected]  status={result['status']}  label={result['label']}")
    print(f"  stdout: {result['stdout'].strip()}")

    # Test 2: buggy code (assertion will fail)
    result = run_code(
        code="def add(a, b): return a - b",   # bug: subtraction instead of addition
        test_code="assert add(2, 3) == 5",
    )
    print(f"\n[FAIL expected]  status={result['status']}  label={result['label']}")
    print(f"  stderr: {result['stderr'].strip()[:200]}")

    # Test 3: infinite loop β†’ timeout
    result = run_code(
        code="while True: pass",
        timeout=2,
    )
    print(f"\n[TIMEOUT expected]  status={result['status']}  label={result['label']}")