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 ¶
Bases: ValueError
The module breaks the sandbox's rules; .reasons lists each one with its line.
check ¶
The reasons the sandbox refuses this module, each with its line ([] — allowed). See the module docs.
safe_builtins ¶
The builtins a sandboxed module sees: the forbidden names removed, __import__ admitting ALLOWED only.
source_name ¶
The file name a module is compiled under: <solvi-sandbox-<sha256 prefix>>, the same for the same text.
load ¶
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.