---
slug: 20-capabilities
title: Capacidades e intent
description: Synsema es deny-by-default — nada llega a la red, el filesystem, la base de datos o los secretos salvo que declares la capacidad con require. Scopes, sandbox e intent.
example_ids: [capabilities]
---

# 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).

```synsema
-- 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

```synsema
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](/es/0.6.x/61-memory)**). Bajo `serve`, incluso esas se exigen.

## Scopes

- **Host:** `net("api.x.com")`, `net("*.x.com")` (subdominios), `net("*")` / `require net` pelado = cualquiera.
- **Ruta:** `file("/data/*")`; `file` concede lectura+escritura, `file.read` / `file.write` para mínimo privilegio.
- **Prefijo de nombre:** `secret("APP_*")` cubre `APP_KEY`, `APP_DB`, … (solo `*` al 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:

```synsema
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 con `*` final (`spend("PFX_*")`), como `secret`. `require spend` pelado (cualquier unidad) funciona pero avisa.
- **Jamás auto-otorgada** — ni siquiera bajo `run` (como `sign`/`wallet`/`memory`). Denegada dentro de `sandbox`; una task despachada con `call_tool` tiene que declararla ella misma.
- **Ledger, fail-loud:** todo intento (concedido o denegado) se agrega a `spend.log` en el directorio de audit (`$SYNSEMA_AUDIT_DIR` o `~/.synsema/audit`) con el monto decimal canónico, la unidad, el motivo entre comillas, `file:line` y 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, pares `unidad:monto` separados 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, leyendo `spend.log`.
- Los montos van por el camino **decimal** exacto — los centavos jamás acumulan error binario de float. `spend` devuelve 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:

```synsema
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:

```sh
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
```

El error nombra a quién puede arreglarlo: una llamada que el programa nunca declaró dice *missing capability — add `require …`*; una que el programa **sí** declaró pero el techo bloquea dice *declared but above the host ceiling — the program cannot fix this; the host must widen the ceiling* (así un agente que repara su propio código nunca entra en loop). Detalles y los campos del audit: [Sandboxing](22-sandbox), [WASM](72-wasm).

**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: solo `stdout` + `time` (cómputo + `print`). Para "solo corré esto y mostrame la salida".
- **`--cap-set "<lista>"`** — un techo **a medida**: `name` o `name=scope` separados por coma. Cuando el código legítimamente necesita algo (un archivo scratch, una DB en memoria) pero no debe obtener más. `--cap-set none` es un techo vacío — nada, ni siquiera `stdout` (bajo un techo, `stdout` es una capability real: `--cap-set` sin él niega la salida).

Dos controles más del host se componen con el techo (engine v0.6.14+): **`--profile pure`** — un
segundo muro independiente donde cada builtin de filesystem/exec/socket/db/cron simplemente no está,
sin importar el techo; y **`--audit json|<path>|fd:N`** — una línea JSON por chequeo de capability,
para ver exactamente qué tocó un programa (ver [Observabilidad](63-observability)). `conform` también
respeta todo esto. Y desde adentro de un programa, **`require sandbox_run`** le permite correr *otro*
programa Synsema con `run_program(source, {ceiling, profile, env, timeout})` bajo un techo que es la
intersección con el propio — el hijo nunca puede exceder al padre. Modelo completo: [Sandboxing](22-sandbox).

**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 **deniega `exec` y `net` real** — probá un snippet con `require exec(...)` y tocá Run. Para un deploy público, combinalo con un contenedor de SO (defensa en profundidad).

## `intent`

```synsema
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](/es/0.6.x/21-secrets)** para credenciales.
