---
slug: 20-capabilities
title: Capabilities & intent
description: Synsema is deny-by-default — nothing reaches the network, filesystem, database or secrets unless you declare the capability with require. Scopes, sandbox, and intent.
example_ids: [capabilities]
---

# Capabilities & intent

Synsema is **deny-by-default**. Nothing touches the network, filesystem, database, secrets, or shell unless you **declare the capability** with `require`. Forget it and the operation cannot run — the interpreter refuses, even if the code asks.

This is what makes running untrusted code (an LLM's output, a doc playground) safe **by construction**.

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

## Declare what you need

```synsema
require net("api.example.com")    -- one host
require file.read("/data/*")      -- read-only, under /data
require db("./store.db")          -- this database
require secret("STRIPE_KEY")      -- this secret
```

A `fetch` to any host you didn't declare is blocked. A `read_file` outside the scope is blocked — the path scope is **faithful**: a `..` escape normalizes and is denied.

## Auto-granted vs. must-declare

Under `run`/`test`, only **`stdout` / `time` / `llm`** are auto-granted. Everything else — `net`, `file`, `db`, `secret`, `exec`, `serve`, `reveal`, `sign`, `wallet`, `spend`, `memory`, and **`random`** — is deny-by-default (yes, `random` too: it's for tokens/nonces; and `memory` too: persistent agent state writes files, so it must be declared — `require memory("name")`, see **[Memory & state](/en/0.6.x/61-memory)**). Under `serve`, even those are required.

## Scopes

- **Host:** `net("api.x.com")`, `net("*.x.com")` (subdomains), `net("*")` / bare `require net` = any.
- **Path:** `file("/data/*")`; `file` grants read+write, `file.read` / `file.write` for least-privilege.
- **Name prefix:** `secret("APP_*")` covers `APP_KEY`, `APP_DB`, … (trailing `*` only).

## `spend` — the audited money declaration

`spend(amount, unit, reason)` declares an **external spend** before your program makes the actual payment call (a PSP API, an exchange, a transaction builder). It does not move money itself — it writes the forensic ledger entry FIRST and enforces the host's ceiling:

```synsema
require spend("USD")                        -- deny-by-default ALWAYS, scoped by unit
let total be spend(50, "USD", "refund order 42")   -- → the unit's accumulated total
print(spend_total("USD"))                   -- introspection, no capability needed
```

- **Scope = the unit**, any string: `"USD"`, `"EUR"`, `"ETH"`, `"credits"` — exact name or a trailing-`*` prefix (`spend("PFX_*")`), like `secret`. Bare `require spend` (any unit) works but warns.
- **Never auto-granted** — not even under `run` (like `sign`/`wallet`/`memory`). Denied inside `sandbox`; a task dispatched with `call_tool` must declare it itself.
- **Ledger, fail-loud:** every attempt (granted or denied) is appended to `spend.log` in the audit directory (`$SYNSEMA_AUDIT_DIR` or `~/.synsema/audit`) with the canonical decimal amount, unit, quoted reason, `file:line` and program. If the entry cannot be written, the spend **fails** — no audit, no spend.
- **Host ceiling:** `SYNSEMA_SPEND_CEILING="USD:500,ETH:0.1"` (one variable, comma-separated `unit:amount` pairs, resolved process environ > `.env`). A breach is a **hard, catchable error** (`try`/`recover`) — after it, do **not** proceed with the external payment call. The accumulator is per-process and monotonic; time-windowed policy (per day, per customer) is your program's job, reading `spend.log`.
- Amounts take the exact **decimal** path — cents never accumulate binary float error. `spend` returns the unit's new accumulated total; `spend_total(unit)` reads it (0 if unused).

## Per-task capabilities

A task can declare its own narrower `require` — it can only reach what it declares, even if the program is broader:

```synsema
task fetch_orders()
    require net("api.shop.com")
    give fetch("https://api.shop.com/orders")    -- can ONLY reach api.shop.com
```

## `sandbox`

A `sandbox` block strips **all** capabilities for the untrusted body inside it — a `require` within is a no-op.

## Host ceiling — `--sandbox` / `--cap-set` (running code you don't trust)

`require`, per-task scoping and `sandbox` all assume **you wrote the code**. When you didn't — running an **LLM-generated** program, a user's plugin, or a public playground — the **host** (whoever runs `synsema`) imposes a ceiling the code cannot exceed, no matter what it declares:

```sh
synsema run  --sandbox program.syn                       # ceiling = stdout + time only
synsema run  --cap-set "stdout,db=:memory:" program.syn  # a tailored ceiling
synsema test --cap-set "stdout,time,random,secret" tests.syn
```

The error names who can fix it: a call the program never declared says *missing capability — add `require …`*; a call the program **did** declare but the ceiling blocks says *declared but above the host ceiling — the program cannot fix this; the host must widen the ceiling* (so an agent repairing its own code never loops). Details and the audit fields: [Sandboxing](22-sandbox), [WASM](72-wasm).

**Three layers — who restricts, and when:**

| Layer | Who restricts | Use when |
|---|---|---|
| `require cap("scope")` | the **code** declares what it needs | you trust the code |
| `sandbox` block | the **code** isolates part of itself | you trust the code |
| **`--sandbox` / `--cap-set`** | the **host** imposes a ceiling from outside | you **don't** trust the code |

**`--sandbox` vs `--cap-set`:**
- **`--sandbox`** — the strictest useful ceiling: `stdout` + `time` only (compute + `print`). For "just run this and show the output."
- **`--cap-set "<list>"`** — a **tailored** ceiling: comma-separated `name` or `name=scope`. When the code legitimately needs something (a scratch file, an in-memory DB) but must not get more. `--cap-set none` is an empty ceiling — nothing, not even `stdout` (under a ceiling, `stdout` is a real capability: `--cap-set` without it denies output).

Two more host controls compose with the ceiling (engine v0.6.14+): **`--profile pure`** — a second,
independent wall where every filesystem/exec/socket/db/cron builtin simply isn't there, regardless of
the ceiling; and **`--audit json|<path>|fd:N`** — a JSON line per capability check, for seeing exactly
what a program touched (see [Observability](63-observability)). `conform` honors all of these too. And
from inside a program, **`require sandbox_run`** lets it run *another* Synsema program with
`run_program(source, {ceiling, profile, env, timeout})` under a ceiling that is the intersection with
its own — the child can never exceed the parent. Full model: [Sandboxing](22-sandbox).

**The rule:** `caps_effective ⊆ require ∩ ceiling`. A `require net("*")` under `--cap-set "net=api.mock"` gets **nothing** — the code can never rise above the ceiling. It only ever *removes*; auto-grants (`stdout`/`time`/`llm`) are filtered too, and it propagates to spawned **agents** and `parallel_map` workers.

**Scope your `file`/`db`** or you give too much: a bare `--cap-set "…,file"` lets the code read any absolute path — use `file=scratch_*` (a prefix) or `db=:memory:` so it only touches what you mean.

> This site's playground uses exactly this: every **Run**/**Test** runs with a ceiling that allows compute, `secret`, in-memory SQL and scratch files, but **denies `exec` and real `net`** — try a snippet with `require exec(...)` and press Run. For a public deploy, combine it with an OS container (defense in depth).

## `intent`

```synsema
intent: "Read customer data and generate reports"
```

`intent` is **descriptive** (any language) and **frozen at startup** — a prompt injection cannot widen it. Security comes from capabilities, never from the prose. See **[Secrets](/en/0.6.x/21-secrets)** for credentials.
