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, and random — is deny-by-default (yes, random too: it's for tokens/nonces). 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).
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.