Capacidades e intent
Synsema es deny-by-default. Nada toca la red, el filesystem, la base de datos, los secretos ni la shell salvo que declares la capacidad con require. Si la olvidás, la operación no corre — el intérprete la rechaza, aunque el código la pida.
Esto es lo que hace seguro por construcción correr código no confiado (la salida de un LLM, un playground de docs).
-- Doc example: deny-by-default capabilities + faithful scope.
-- Uses `secret` because it proves the model with no network/disk side effects.
intent: "doc example: capabilities and intent"
require secret("APP_*") -- name-prefix scope: covers APP_KEY, APP_DB, ... only
task read_app_key()
-- APP_KEY is under the declared APP_* scope → allowed (still redacted, as always)
give text(secret("APP_KEY", "demo")) == "secret(APP_KEY)"
task read_unscoped()
-- DB_PASSWORD is NOT under APP_* → denied at the capability check (before any use)
give secret("DB_PASSWORD")
print("APP_KEY is in scope → " + text(read_app_key()))
test "a capability you declared (in scope) is allowed"
assert(read_app_key())
test "anything outside the declared scope is denied (deny-by-default)"
assert_error(read_unscoped)
Declará lo que necesitás
require net("api.example.com") -- un host
require file.read("/data/*") -- solo lectura, bajo /data
require db("./store.db") -- esta base de datos
require secret("STRIPE_KEY") -- este secreto
Un fetch a cualquier host que no declaraste se bloquea. Un read_file fuera del scope se bloquea — el scope de ruta es fiel: un escape con .. se normaliza y se deniega.
Auto-otorgadas vs. a declarar
Bajo run/test, solo se auto-otorgan stdout / time / llm. Todo lo demás — net, file, db, secret, exec, serve, reveal, y random — es deny-by-default (sí, random también: es para tokens/nonces). Bajo serve, incluso esas se exigen.
Scopes
- Host:
net("api.x.com"),net(".x.com")(subdominios),net("")/require netpelado = cualquiera. - Ruta:
file("/data/*");fileconcede lectura+escritura,file.read/file.writepara mínimo privilegio. - Prefijo de nombre:
secret("APP_")cubreAPP_KEY,APP_DB, … (soloal final).
Capacidades por task
Una task puede declarar su propio require más acotado — solo alcanza lo que declara, aunque el programa sea más amplio:
task fetch_orders()
require net("api.shop.com")
give fetch("https://api.shop.com/orders") -- SOLO puede llegar a api.shop.com
sandbox
Un bloque sandbox despoja todas las capacidades del cuerpo no confiado que contiene — un require adentro es un no-op.
Techo del host — --sandbox / --cap-set (ejecutar código que NO confiás)
require, el scope por-task y sandbox asumen que vos escribiste el código. Cuando no fue así — correr un programa generado por un LLM, el plugin de un usuario, o un playground público — el host (quien corre synsema) impone un techo que el código no puede exceder, declare lo que declare:
synsema run --sandbox program.syn # techo = stdout + time solamente
synsema run --cap-set "stdout,db=:memory:" program.syn # un techo a medida
synsema test --cap-set "stdout,time,random,secret" tests.syn
Tres capas — quién restringe, y cuándo:
| Capa | Quién restringe | Usar cuando |
|---|---|---|
require cap("scope") | el código declara lo que necesita | confiás en el código |
bloque sandbox | el código aísla una parte de sí mismo | confiás en el código |
--sandbox / --cap-set | el host impone un techo desde afuera | no confiás en el código |
--sandbox vs --cap-set:
--sandbox— el techo mínimo útil: solostdout+time(cómputo +print). Para "solo corré esto y mostrame la salida".--cap-set "<lista>"— un techo a medida:nameoname=scopeseparados por coma. Cuando el código legítimamente necesita algo (un archivo scratch, una DB en memoria) pero no debe obtener más.
La regla: caps_efectivas ⊆ require ∩ techo. Un require net("") bajo --cap-set "net=api.mock" obtiene nada — el código nunca sube por encima del techo. Solo resta*; los auto-grants (stdout/time/llm) también se filtran, y se propaga a los agentes spawneados y a los workers de parallel_map.
Acotá tu file/db o das de más: un --cap-set "…,file" pelado deja al código leer cualquier ruta absoluta — usá file=scratch_* (un prefijo) o db=:memory: para que solo toque lo que querés.
El playground de este sitio usa exactamente esto: cada Run/Test corre con un techo que permite cómputo,secret, SQL en memoria y archivos scratch, pero deniegaexecynetreal — probá un snippet conrequire exec(...)y tocá Run. Para un deploy público, combinalo con un contenedor de SO (defensa en profundidad).
intent
intent: "Leer datos de clientes y generar reportes"
intent es descriptivo (cualquier idioma) y se congela al arrancar — una inyección de prompt no puede ampliarlo. La seguridad viene de las capacidades, nunca de la prosa. Mirá Secretos para credenciales.