Skip to content

solvi.experimental.compile.sandbox

Code a model wrote, held to pure functions: an ast allowlist, a subprocess with limits under a trusted driver, and a restricted load into this process.

A sandbox for code a model wrote: only pure functions over facts.

from solvi.experimental.compile.sandbox import check, load, run
check(source)                                   # [] when allowed, else the reasons it is refused
ns = load(source)                               # the module's namespace, in this process (refuses what check refuses)
out = run(source, "mypkg.driver:main", payload) # the module run in a subprocess by a trusted driver → its JSON

check reads the module with ast and refuses: an import outside ALLOWED (pure standard-library modules: re, math, datetime, itertools, collections, functools, string, unicodedata, json, fractions, decimal, heapq, bisect, statistics, typing) or a relative one; a name or a definition from FORBIDDEN (open, eval, exec, compile, import, globals, locals, vars, getattr, setattr, delattr, input, breakpoint, exit, quit, help, memoryview, os, sys, subprocess, socket — getattr & co. would reach a dunder by a string); any dunder name or attribute (__class__, __builtins__, __name__).

run executes the module in a fresh python -I subprocess: an empty environment, a scratch working directory, limits on memory (RLIMIT_AS), CPU time and wall-clock time, restricted builtins (the forbidden names removed, __import__ admitting ALLOWED only, plus INTERNAL — _strptime, which datetime.strptime imports at call time). The driver — your code, never the model's — is imported before the module runs; it gets the module's namespace, the payload and the signal module (for a per-item alarm) and returns JSON. A module that does not load, a crash, a run over its limits come back as {"load_error": ...} or {"crash": ...}.

load executes an allowed module in this process with the same restricted builtins, its source registered with linecache under a name made of its hash, so inspect.getsource works and a part's fingerprint is taken from its syntax tree, the same across processes.

What it does not do: it is not a security boundary against a determined attacker once code is loaded into your process — the checks close the usual ways out (imports, files, dunders, reflection), and run puts a process boundary and limits around a module that has not been accepted yet. Load only what passed your checks. A pure function can still loop for ever or allocate without bound in this process: the limits hold in run only.

Refused

Refused(reasons)

Bases: ValueError

The module breaks the sandbox's rules; .reasons lists each one with its line.

check

check(source: str) -> list[str]

The reasons the sandbox refuses this module, each with its line ([] — allowed). See the module docs.

safe_builtins

safe_builtins()

The builtins a sandboxed module sees: the forbidden names removed, __import__ admitting ALLOWED only.

source_name

source_name(source: str) -> str

The file name a module is compiled under: <solvi-sandbox-<sha256 prefix>>, the same for the same text.

load

load(source: str, extra: dict | None = None) -> dict

Execute an allowed module in this process → its namespace. Raises Refused with the reasons when check refuses it. extra: names put into the namespace before it runs (trusted helpers such as Fail).

run

run(source: str, driver: str, payload, *, mem_mb: int = 1024, cpu_s: int = 300, wall_s: int = 600, path=()) -> dict

Run the module in a subprocess under driver ("package.module:function": trusted code, importable in the child from solvi's own folder or the folders in path; it gets (namespace, payload, signal) and returns JSON) → the driver's result, {"load_error": reason} when the module is refused or does not load, or {"crash": reason} (a crash, a limit, no result). The driver module's SANDBOX_EXTRA dict, if any, is put into the namespace.