Reference
Capabilities index
Deny-by-default: declare with require capability("scope"). See Capabilities & intent for the model.
-- 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
require net("http://localhost:8545/") -- a URL is stored as its host; an EXPLICIT port is part of the grant (v0.6.29+)
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 where_am_i()
-- cwd() reveals the host's directory layout: it needs file.read(".") — not declared here → denied (engine v0.6.20+)
give cwd()
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)
assert_error(where_am_i)
task key_under_wrong_block()
-- the program DID declare secret("APP_*"), but this block only lists `net`: denied
sandbox under {"net": "x.com"}
give read_app_key()
test "sandbox under: a least-privilege block keeps only what it lists"
-- what the block lists stays (still gated by the program's own require)…
sandbox under {"secret": "APP_*"}
assert(read_app_key())
-- …what it doesn't list is denied even though the program declared it
assert_error(key_under_wrong_block)
task other_port()
-- the grant is localhost:8545; port 8546 is another scope → denied before any socket opens
give http_get("http://localhost:8546/")
test "a port written in a net grant is part of the grant"
assert_error(other_port)
task stamp()
give format_time(1750000000, "%Y-%m-%d")
task clock()
give now()
task seeded_die()
give random_int(rng(42), 1, 6)
task os_die()
give random_int(1, 6)
test "what never reads the clock or the OS needs no capability (v0.6.29+)"
-- `sandbox` strips every capability, so what still runs inside it is pure
assert_eq(sandbox stamp(), "2025-06-15") -- formatting a time is not reading the clock
assert_error(() => sandbox clock()) -- now() is
assert_eq(sandbox rng(42)(), 0.7739560485559633) -- a seeded generator: numpy's default_rng(42)
assert_eq(sandbox seeded_die(), seeded_die()) -- same seed, same numbers
assert_error(os_die) -- OS entropy is `random`, never declared here| Capability | Gates | Scope | Auto-granted in run? |
|---|---|---|---|
stdout | print / output | — | yes |
time | reading the clock: now, sleep, and the verifiers/emitters that stamp or check an expiry (jwt_sign/jwt_verify, totp/totp_verify, captoken_*, http_sign/http_signature_verify, oidc_verify — each also accepts the time explicitly). Not format_time/parse_time/date_parts or the date types: they never read the clock (pure since v0.6.29) | — | yes |
llm | reason/decide/analyze/generate, llm_step (incl. provider egress) | —; v0.6.46+ with labels: llm("P") declares whose private data may reach the model, and the host allows it per provider (--cap-set llm=P@<provider host>, @local, @embedder in wasm; an item without @ allows no private data) — see LLM primitives | yes |
judge (v0.6.25+) | the judge block (Judge) — and decide when SYNSEMA_JUDGE_DECIDE=1. Its own capability: llm does not grant it and vice versa (classifying and generating are different rights). Denied under --deterministic; always offline in a wasm guest | — (no scope); v0.6.46+ with labels: judge("P") + the host's judge=P@<host|local> for private data; in wasm judge is now checked against the ceiling | yes (plain run/conform); required under serve and in secure mode |
random | OS entropy: random() and random_int(lo, hi) without a generator, random_bytes, token, push_vapid_keys (v0.6.15+), ecdh_keypair (v0.6.20+). A seeded generator — rng(seed), g(), random(g), random_int(g, lo, hi), random_normal, shuffle, choice, sample, rng_spawn — is pure and needs nothing (v0.6.29+) | — | no (tokens/nonces) |
net | http*, fetch, ws_connect, push_send (v0.6.15+ — host of the subscription's endpoint: the push service) | host: net("api.x"), net(".x"), net(""); host:port when a port is written — then only that port (v0.6.29+: net("localhost:8545"), net("[::1]:8545")); a URL is stored as its host (and port), without credentials, path or query; v0.6.46+: a wildcard (require net, net(""), net(".x.com"), --cap-set net, sandbox under, a token) reaches only public addresses — a private host needs its name, its address or a CIDR (net("10.0.0.0/8")) | no |
file | read and write | path: file("/data/**") | no |
file.read / file.write | least-privilege I/O; file.read also gates watch(path) (v0.6.9+ — watching a tree is reading it); v0.6.20+: file.read(".") gates cwd(), file.read gates the from files of zip_create/tar_create and the fonts of svg_to_; file.write gates delete_file/delete_dir (every path removed) and zip_extract/tar_extract (every path written); v0.6.45+: file.read("<dir>/*") gates a static mount of <dir> and grep on a folder, file.read gates { include }/{ layout } from disk, both files of tls cert … key … in a serve block, and edit_file (with file.write) | path glob by segment: */? within one folder, across folders; v0.6.46+: a recursive watch needs file.read("<dir>/") (a non-recursive one, only the folder) | no |
db | sql/mongo_/redis_ | path (SQLite) or canonical URL | no |
secret | secret(...) | name: secret("APP_*"); v0.6.46+ bound to hosts: require secret("K") for net("api.example.com") (then it may go in a URL, query, body or form, only there); host ceiling --cap-set secret=K@api.example.com | no |
reveal | reveal(...) | name/label (scoped) | no |
sign | secp256k1_sign / ed25519_sign (blockchain) | key secret's name (audited) | no |
wallet | create custody: mnemonic_ / hd_derive / keystore_ | source secret's name (audited) | no |
spend | spend(amount, unit, reason) — audited money declaration (spend.log, ceiling SYNSEMA_SPEND_CEILING) | unit name or trailing-* prefix: spend("USD") | no (never — like sign) |
exec | run (one-shot) and proc_spawn (live process, v0.6.7+; pty: true v0.6.8+ — no extra capability, a pty grants no OS power a pipe lacks) | command name; v0.6.46+: with cwd, a relative command is resolved inside it and the grant names that path (run("./tool", [], 60, {"cwd": "sub"}) → exec("./sub/tool")) | no |
serve | serve on N | port | no (and required) |
env | env(...) | name / prefix | no |
stdin | read_line, free-text ask; term_open (v0.6.11+ — the interactive terminal, raw mode) | — | no |
memory | persistent state family: remember/recall/forget_memory/memory_summary, add_rule/check_rules/get_rules, create_progress/…/resume_point | declared name = the .db identity: memory("agent-name") (ceiling prefix: memory=shop-*) | no (writes files; the declaration IS the identity) |
attest (v0.6.24+) | attest(opts) / attest_key(purpose) / attestation_key() — asking the platform (AWS Nitro, TDX/SEV-SNP, dstack) for a document or a sealed key. attestation_document() and attestation_verify() are not gated: reading your own published identity and verifying someone's document touch nothing | — (no scope) | no — and absent from every packaged ceiling, so --deterministic denies it on its own |
declassify (v0.6.46+) | with labels, declassify(value, why) removing a principal (Labels) | principal or glob: --cap-set declassify=customers; require declassify("P") declares it (a bare require declassify warns) but does not bound it | yes without a ceiling (like unsafe in your own crate: explicit, with its reason, listed in full by synsema code check --json); with a ceiling only the principals it names; --sandbox, --deterministic and sandbox never grant it (sandbox under {"declassify": …} delegates it). Without --labels it is not checked |
sandbox_run (v0.6.14+) | run_program(source, opts) — run another Synsema program in a child process under a ceiling ∩ the parent's | — (no scope) | no |
Notes: synsema check matches scopes with the runtime's own matcher (v0.6.30+: file.read("./") covers read_file("x.csv") in both). Path scopes are globs by segment* (v0.6.45+): and ? match within one path segment, crosses folders and ./data/ also covers ./data itself; no wildcard matches .. or jumps from a relative path to an absolute one; the whole disk is the capability with no scope (require file.read), not ""; C:/ covers the drive. Path scopes are faithful (.. escapes denied; matched case-insensitively on Windows/macOS, except in a case-sensitive folder such as those WSL creates) and judged where a path really leads (v0.6.45+): a link that is part of the grant works, a symlink or junction below it that leads out is refused (a link (symlink or junction) on the way leads outside it), and grep does not follow links to folders — details in Capabilities; ~/ expands to the home directory in scopes and paths (v0.6.20+). sandbox strips everything. A per-task require narrows a task to only what it declares (∩ the program). Under serve/secure mode, even the auto-granted ones must be declared. In --cap-set, none is an empty ceiling (nothing, not even stdout). What the program reads via a template (render/include/layout of a disk* file) is gated by file.read too, like read_file — templates baked into a synsema build binary are part of the program and need no capability.