# pi-monty-sandbox

Safe, high-performance Python execution sandbox extension for [Pi Coding Agent](https://pi.dev) powered by [Pydantic Monty](https://github.com/pydantic/monty) (a memory-safe sandboxed Python VM written in Rust).

Available on the [Pi Package Catalog](https://pi.dev/packages).

## Features

- **Strict Isolation**: Code executes inside a crash-isolated subprocess sandbox with no direct access to host network, host environment variables, or host filesystem.
- **Stateful REPL**: Variables, functions, and imports persist across tool calls within a session.
- **Sub-millisecond Speed**: Near-instant cold start (~5ms) compared to seconds for Docker or micro-VMs.
- **Self-Healing Guidance**: Clear actionable hints when code exceeds sandbox capabilities, steering the LLM to the right solution.
- **Resource Limits**: Configurable execution timeouts and memory limits enforced inside the Rust VM (128 MB memory cap, per-call execution backstop, session budgets for cumulative sleep time and host-call suspensions).
- **Type Checking**: Automatic syntax and type checking diagnostics via Monty (`ty`).
- **Live Output Streaming**: Captures `print()` stdout/stderr and streams it to the Pi TUI.
- **Custom TUI Rendering**: Styled execution headers, progress indicators, and traceback rendering.
- **Convenient Controls**: Reset the REPL session anytime via the `/pyreset` slash command or `reset_session: true` argument.
- **Read-only Bridge (opt-in)**: pi's own `read`/`grep`/`find`/`ls` tools become callable Python functions inside the sandbox (`await grep("TODO")`) — loop, filter and aggregate workspace data in one snippet; intermediate results never enter model context, only what the code `print()`s. Statically type-checked against bridge stubs before execution.

## Installation

Install globally into Pi:

```bash
pi install npm:pi-monty-sandbox
```

Or install project-locally:

```bash
pi install npm:pi-monty-sandbox -l
```

Or test instantly without installing:

```bash
pi -e npm:pi-monty-sandbox
```

## When to Use: `python_sandbox` vs `bash`

| Task | Recommended Tool | Why |
| --- | --- | --- |
| Math calculations & combinatorics | `python_sandbox` | Sub-millisecond execution, zero side effects, no subprocess overhead |
| Algorithmic reasoning & logic verification | `python_sandbox` | Safe scratchpad, stateful across multi-turn prompts |
| JSON / String parsing & transformations | `python_sandbox` | Instant in-memory data processing |
| Regex testing & text manipulation | `python_sandbox` | Isolated, no risk of runaway processes |
| Multi-file search / aggregation (count, filter, compare across many files) | `python_sandbox` (with bridge) | Bridged `grep`/`read`/`find`/`ls` run in a loop inside one snippet; only the aggregated `print()` output enters context — a task that needs 20 direct tool round-trips becomes one call |
| Async logic verification (`asyncio.run`, `gather`) | `python_sandbox` | Built-in async coroutine support |
| Scripts requiring external packages (`numpy`, `pandas`, `requests`) | `bash` | Monty sandbox has no pip package access |
| Workspace file edits / Git operations | `bash` | Monty sandbox has no host filesystem access |
| Network requests / API scraping | `bash` | Monty sandbox has no network access |

## Supported Python Subset

Monty is a custom, memory-safe Python interpreter written in Rust. It supports Python core syntax with the following subset:

- **Supported Standard Library Modules**:
  `math`, `re`, `json`, `itertools` (all but `tee`), `collections` (`deque`, `Counter`, `defaultdict`, `namedtuple`), `dataclasses` (`@dataclass`), `datetime`, `functools` (`reduce`, `partial`), `pathlib`, `asyncio` (`run`, `gather`, `sleep`), `base64`, `binascii`, `os` (path functions), `sys`, `typing`, `unicodedata`, `random` (MT19937: `seed`, `randint`, `choice`, `sample`, `shuffle`, distributions), `time` (`time`, `monotonic`, `sleep`, `strftime`, `strptime`, `gmtime`, ...), `copy` (`copy`, `deepcopy`).
  Native values include complex numbers (`1 + 2j`) and `Ellipsis`; results render in Python style (`(1+2j)`, `datetime.datetime(...)`, `datetime.timedelta(...)`).
- **Unsupported Modules**:
  `string`, `csv`, `heapq`, `hashlib`, `uuid`, `io`, `urllib`, `enum` (use `bash` if needed).
- **Unsupported Syntax**:
  No class inheritance (`class A(B)`), no method decorators (`@property`, `@classmethod`), no generators (`yield`), no `del` keyword (use `.pop()`).
- **Sleeps are capped**:
  A single `time.sleep()` / `asyncio.sleep()` blocks for at most 5 seconds and then returns early (`time.sleep(10)` resumes after ~5 s). Use `bash` for real waits. `time.struct_time` is not available as a module name (the conversion functions still work).

Randomness and complex numbers:

```python
import random
random.seed(42)                       # deterministic across runs
rolls = [random.randint(1, 6) for _ in range(10)]

z = (1 + 2j) * (3 - 1j)
print(z)                              # --> (5+5j)
```

## Configuration

Optional features are controlled by environment variables set where pi runs (e.g. in your shell profile or `~/.pi/agent/.env`):

| Variable | Default | Effect |
| --- | --- | --- |
| `PI_MONTY_BRIDGE=1` | off | Bridge pi's read-only tools (`read`, `grep`, `find`, `ls`) into the sandbox as Python functions. Only what the code `print()`s returns to the model; every bridged call is logged as a `[bridge] name(args)` line in the streamed output. Default `timeout_ms` rises to 15000 while enabled. |
| `PI_MONTY_MOUNT_WORKSPACE=1` | off | Mount the workspace directory read-only at `/workspace`, so plain `open()`/`os.listdir` work against project files. Writes raise `PermissionError`. Independent of the bridge. |

With the bridge enabled, the model is told it can call `await read(path, offset, limit)`, `await grep(pattern, path=..., glob=..., ignoreCase=..., literal=..., context=..., limit=...)`, `await find(pattern, path=..., limit=...)`, `await ls(path=..., limit=...)` — positional arguments first, keyword arguments after. Bridge calls are type-checked before execution via monty's `ty` against generated `async def` stubs, so wrong argument types fail as compiler diagnostics instead of tracebacks.

Example:

```python
# One snippet instead of ~30 direct tool round-trips:
import re
todos = await grep("TODO|FIXME", path="src")
counts = {}
for line in todos.splitlines():
    file = line.split(":")[0]
    counts[file] = counts.get(file, 0) + 1
for f, n in sorted(counts.items(), key=lambda kv: -kv[1]):
    print(f"{n:3} {f}")
```

## Tool Specification: `python_sandbox`

### Parameters

| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `code` | `string` | Pure Python code snippet to execute | *Required* |
| `timeout_ms` | `integer` | Execution timeout in milliseconds (max `30000`) | `5000` |
| `reset_session` | `boolean` | Clear variables and start a fresh REPL session | `false` |

### Commands

- `/pyreset`: Resets the Python REPL sandbox session (clearing all defined variables and imported modules).

## Example Usage in Pi

Ask the agent:

> *"Calculate 2^256 mod 1000000007 using python_sandbox."*

Or maintain state across turns:

1. *"Define a function `calc_pi(n)` in python_sandbox."*
2. *"Call `calc_pi(1000)` and print the result."*

## Security Model

Pi by design has no built-in sandboxing (built-in bash and file tools run with host user privileges). `pi-monty-sandbox` provides a dedicated safe computing layer for mathematical calculations, data parsing, algorithmic problem solving, and untrusted Python execution.

The optional read-only bridge does **not** expand the attack surface: it exposes exactly the tools the agent can already invoke directly (`read`/`grep`/`find`/`ls`), just callable from Python code — so loops over many calls cost zero extra model round-trips. Mutating tools (`bash`, `edit`, `write`), environment variables, and network are never bridged; the workspace mount is enforced read-only by monty's Rust VM.

## License

MIT
