Synsemadocsv0.6.xENES

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.

sandbox.syn
-- 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

Las tres capas juntas§

CapaQuién restringePara
require cap("scope")el código (declara lo que necesita)código que confiás
bloque sandboxel código (aísla una parte de sí mismo)código que confiás
--sandbox / --cap-setel 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.

.env desaparecen salvo que se los pases). Un valor secret en env se rechaza — reveal()-lo explícitamente.

cancelación del padre (un timeout de request bajo serve, agent_stop) también.

tope (SYNSEMA_RUN_PROGRAM_MAX_DEPTH, default 4).

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:

CapaQuién restringePara
require cap("scope")el código (declara lo que necesita)código que confiás
bloque sandboxel código (aísla una parte de sí mismo)código que confiás
--sandbox / --cap-setel host (techo, desde afuera)código que NO confiás
--profile pureel 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 con run_program bajo 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 MCP run_synsema.