Capacidades y seguridad
Sandbox de código no confiable
El resto del modelo de seguridad (require, scope por-task) asume que vos escribiste el código. Esta página es lo opuesto: ejecutar código que no confiás — la salida de un LLM, el plugin de un usuario, un playground público. Dos herramientas: una desde adentro de tu programa, otra desde afuera.
1. El bloque sandbox — aislar una parte de tu programa§
Envolvé la parte que maneja input no confiable. Adentro, se despojan todas las capacidades — nada de net/file/db/secret/exec, solo cómputo puro + print. Un require adentro es un no-op (no puede re-otorgar para escapar).
let payload be fetch("https://api.external.com/data") -- input no confiable
sandbox
let result be transform(payload) -- si `transform` es explotado, NO puede tocar nada
También es una expresión:
let clean be sandbox validate(untrusted_input) -- aislado, devuelve un valor
Usalo cuando una parte de tu código debe procesar algo peligroso pero no debe llegar al exterior.
-- Doc example: the `sandbox` block — isolate a piece of code with NO capabilities.
-- As an expression it computes and returns; net/file/db/secret/exec are stripped inside.
-- Plus run_program: Synsema running Synsema in a child process under a ceiling ∩ the parent's.
intent: "doc example: sandbox block + run_program"
require sandbox_run
task risky_pure(n)
give n * n
-- sandbox as an EXPRESSION: isolated, returns the value (run shows it)
print("sandbox result: " + text(sandbox risky_pure(7))) -- sandbox result: 49
test "sandbox runs pure computation and returns the value"
assert_eq(sandbox risky_pure(7), 49)
assert_eq(sandbox (40 + 2), 42)
-- run_program: the child's result — and its audit — come back as a VALUE, not a log to parse.
test "run_program runs a child under a ceiling; its result is a value"
let r be run_program("print(2 + 3)", {"ceiling": "stdout", "profile": "pure", "timeout": 20})
assert(r["ok"])
assert_eq(r["output"], ["5"])
assert_eq(r["exit"], 0)
test "the child can never exceed the parent: exec it wasn't lent is denied, with a structured audit"
-- the child asks for exec; the parent didn't lend it, so run() is denied and the child fails.
let r be run_program("run(\"echo\", [\"x\"])", {"ceiling": "stdout", "profile": "native", "timeout": 20})
assert_eq(r["ok"], false)
assert(some(r["audit"], (e) => contains(e["capability"], "exec") and not e["granted"]))2. El techo del host — --sandbox / --cap-set§
Cuando corrés un programa entero que no escribiste, quien corre synsema impone un techo que el código no puede exceder, declare lo que declare:
synsema run --sandbox program.syn # solo stdout + time
synsema run --cap-set "stdout,db=:memory:" program.syn # un techo a medida
synsema test --cap-set "stdout,time,random,secret" tests.syn
--sandbox= el techo mínimo útil (stdout+time).--cap-set none= nada en absoluto, ni siquierastdout.--cap-set "<lista>"= vos decidís exactamente hasta dónde (nameoname=scope).- La regla:
caps ⊆ require ∩ techo— el código nunca sube por encima. Los auto-grants (llmtambién) se filtran, y aplica al preámbulo del programa (las sentencias antes deserve on), a los agentes spawneados y a los workers deparallel_map. stdoutes una capability real bajo un techo (v0.6.14+): un--cap-setsinstdoutniega la salida en el primerprint/show/log.--sandboxlo incluye. Sin ningún techo, la salida sigue libre.- Acotá
file/dbo das de más:file=scratch_*,db=:memory:. Unrenderde un template en disco lee un archivo, así que también necesitafile.read(los templates empaquetados y losinclude/layoutanidados no). conformrespeta estos mismos flags (v0.6.14+) —synsema conform --cap-set "…" app.synvuelca{ok, out, err}con las denegaciones enerr.- El error nombra a quién puede arreglarlo. Una llamada que el programa nunca declaró falla con missing capability — add
require …; una que el programa sí declaró pero el techo bloquea falla con declared but above the host ceiling — the program cannot fix this; the host must widen the ceiling. Un agente que repara su propio código a partir del primer mensaje nunca entra en loop con el segundo (agregando elrequireque ya tiene). El audit lleva la misma distinción (reason) másorigin: "program" | "runtime"— ver WASM para los campos, y--audit jsonen Observabilidad.
Las tres capas juntas§
| Capa | Quién restringe | Para |
|---|---|---|
require cap("scope") | el código (declara lo que necesita) | código que confiás |
bloque sandbox | el código (aísla una parte de sí mismo) | código que confiás |
--sandbox / --cap-set | el host (desde afuera) | código que NO confiás |
Se componen — gana la más restrictiva.
3. run_program — Synsema corriendo Synsema bajo un techo§
El patrón de arriba — escribir un archivo, exec del binario synsema, parsear stdout — es lo que run_program reemplaza (engine v0.6.14+). Corre otro programa Synsema en un proceso hijo del mismo binario, bajo un techo que es la intersección con el del padre, y devuelve su resultado — y su audit — como un valor. Sin exec, sin synsema en el PATH, sin parsear stderr:
require sandbox_run -- deny-by-default, sin scope
require net("registry.npmjs.org") -- lo que vas a PRESTAR, lo tenés que tener
let r be run_program(code, {
"ceiling": "stdout,net=registry.npmjs.org", -- sintaxis de --cap-set, o "sandbox", o "none"
"profile": "pure", -- "pure" (default) o "native"
"env": {"TOOL": "package"}, -- REEMPLAZA el entorno del hijo
"timeout": 30, -- segundos (default 30); al vencer se mata el árbol
"cwd": "/path/to/work" -- default: el cwd del padre
})
-- r = {"ok": bool, "output": [lines], "errors": [text], "audit": [entries], "exit": n|nothing,
-- "timed_out": bool, "llm_tokens": n}
El hijo nunca puede exceder al padre. Su techo efectivo es opts.ceiling ∩ lo que el padre puede realmente otorgar — pedir más no es un error, se recorta, y el audit del padre lo registra (reason: "above parent ceiling"). Así net=* bajo un padre que solo tiene net("registry.npmjs.org") colapsa a nada. Lo que el padre no hace require, no lo puede prestar.
envreemplaza por completo el entorno del hijo (las keys LLM del padre y los secretos del
.env desaparecen salvo que se los pases). Un valor secret en env se rechaza — reveal()-lo explícitamente.
profilenunca sube por encima del padre (un padrepurefuerza un hijopure).timeout(obligatorio, default 30 s) mata todo el árbol de procesos del hijo; la propia
cancelación del padre (un timeout de request bajo serve, agent_stop) también.
- La recursión está permitida si el techo del hijo incluye
sandbox_run; la profundidad tiene
tope (SYNSEMA_RUN_PROGRAM_MAX_DEPTH, default 4).
- Bajo
--profile pureel hijo está disponible (spawnea el engine bajo un techo — no unexec
arbitrario); bajo el perfil wasm no lo está (sin procesos hijo).
Este es el runner seguro para un playground, una tool MCP run_synsema, o un agente que ejecuta código que generó: el techo lo impone el propio proceso hijo, y el audit es dato.
4. El perfil pure — un segundo muro§
--cap-set es un muro: el techo. --profile pure es un segundo, independiente — el mismo muro que el build de WebAssembly siempre tuvo, ahora en el binario nativo:
synsema run --profile pure --cap-set "stdout,net=api.example.com" program.syn
Bajo pure, cada builtin que mira al SO no existe como tal: los nombres siguen ligados (nunca Undefined variable), pero fallan con la verdad del entorno — read_file: not available in the pure profile — this run has no filesystem, e igual para run, la familia ws_/socket, las bases de datos, cron_ y el hub de procesos (select/proc_*/watch). Esto vale sin importar el techo: un bug en el intérprete no puede repartir acceso a algo que no está registrado.
Lo que queda bajo pure: el lenguaje entero, la matemática, JSON/CSV, charts, hashing, el lado puro de blockchain y web-auth, fetch/http_* con net (pure es "sin máquina local", no "sin red"), las ops de LLM con llm, secret/reveal/env, agentes/spawn/parallel_map (in-process), remember/recall sin disco (en memoria para la corrida), run_program, y leer un bundle de synsema build (un asset empaquetado es el programa, no el filesystem). La tabla completa de "incluido vs no" está en la página WASM — los dos perfiles son el mismo muro.
Dos muros, no uno: el techo, y el builtin que no está.
Las tres capas juntas§
Sumá el perfil pure y las dos herramientas del host se componen con las del propio código:
| Capa | Quién restringe | Para |
|---|---|---|
require cap("scope") | el código (declara lo que necesita) | código que confiás |
bloque sandbox | el código (aísla una parte de sí mismo) | código que confiás |
--sandbox / --cap-set | el host (techo, desde afuera) | código que NO confiás |
--profile pure | el host (el segundo muro) | código que NO confiás |
run_program(…, {ceiling, profile}) | el programa (un hijo ⊆ sí mismo) | código que él no confía |
Gana la más restrictiva.
Por qué importa para agentes: un agente que escribe y corre Synsema puede ejecutar su propio código conrun_programbajo un techo que no puede escapar, y leer el audit de vuelta como un valor — así "correr el código que generó el LLM" es seguro por construcción. Para un deploy público, envolvelo en un contenedor de SO como capa adicional (defensa en profundidad). Así funcionan el playground de este sitio y su tool MCPrun_synsema.