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, sign, wallet, spend, memory, y random — es deny-by-default (sí, random también: es para tokens/nonces; y memory también: el estado persistente del agente escribe archivos, así que se declara — require memory("name"), ver Memoria y estado). 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).
spend — la declaración auditada de dinero
spend(monto, unidad, motivo) declara un gasto externo antes de que tu programa haga la llamada de pago real (la API de un PSP, un exchange, un builder de transacciones). No mueve dinero por sí misma — escribe PRIMERO la entrada forense del ledger y aplica el techo del host:
require spend("USD") -- deny-by-default SIEMPRE, scope = unidad
let total be spend(50, "USD", "reembolso orden 42") -- → total acumulado de la unidad
print(spend_total("USD")) -- introspección, sin capability
- El scope es la unidad, cualquier string:
"USD","EUR","ETH","creditos"— nombre exacto o prefijo confinal (spend("PFX_")), comosecret.require spendpelado (cualquier unidad) funciona pero avisa. - Jamás auto-otorgada — ni siquiera bajo
run(comosign/wallet/memory). Denegada dentro desandbox; una task despachada concall_tooltiene que declararla ella misma. - Ledger, fail-loud: todo intento (concedido o denegado) se agrega a
spend.logen el directorio de audit ($SYNSEMA_AUDIT_DIRo~/.synsema/audit) con el monto decimal canónico, la unidad, el motivo entre comillas,file:liney el programa. Si la entrada no se puede escribir, el gasto falla — sin auditoría no hay gasto. - Techo del host:
SYNSEMA_SPEND_CEILING="USD:500,ETH:0.1"(una variable, paresunidad:montoseparados por comas, resuelta environ del proceso >.env). Excederlo es un error duro catchable (try/recover) — después de él, no sigas con la llamada de pago externa. El acumulador es por proceso y monotónico; la política temporal (por día, por cliente) es de tu programa, leyendospend.log. - Los montos van por el camino decimal exacto — los centavos jamás acumulan error binario de float.
spenddevuelve el total acumulado nuevo de la unidad;spend_total(unidad)lo lee (0 si no se usó).
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.