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.
-- 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
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). Under serve, even those are required.
Scopes
- Host:
net("api.x.com"),net(".x.com")(subdomains),net("")/ barerequire net= any. - Path:
file("/data/*");filegrants read+write,file.read/file.writefor least-privilege. - Name prefix:
secret("APP_")coversAPP_KEY,APP_DB, … (trailingonly).
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:
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_")), likesecret. Barerequire spend(any unit) works but warns. - Never auto-granted — not even under
run(likesign/wallet/memory). Denied insidesandbox; a task dispatched withcall_toolmust declare it itself. - Ledger, fail-loud: every attempt (granted or denied) is appended to
spend.login the audit directory ($SYNSEMA_AUDIT_DIRor~/.synsema/audit) with the canonical decimal amount, unit, quoted reason,file:lineand 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-separatedunit:amountpairs, 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, readingspend.log. - Amounts take the exact decimal path — cents never accumulate binary float error.
spendreturns 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:
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:
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
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+timeonly (compute +print). For "just run this and show the output."--cap-set "<list>"— a tailored ceiling: comma-separatednameorname=scope. When the code legitimately needs something (a scratch file, an in-memory DB) but must not get more.
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 deniesexecand realnet— try a snippet withrequire exec(...)and press Run. For a public deploy, combine it with an OS container (defense in depth).
intent
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 for credentials.