Synsemadocsv0.6.xENES

Reference

Capabilities index

Deny-by-default: declare with require capability("scope"). See Capabilities & intent for the model.

capabilities.syn
-- 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
CapabilityGatesScopeAuto-granted in run?
stdoutprint / output—yes
timereading 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
llmreason/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 primitivesyes
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 ceilingyes (plain run/conform); required under serve and in secure mode
randomOS 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)
nethttp*, 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
fileread and writepath: file("/data/**")no
file.read / file.writeleast-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
dbsql/mongo_/redis_path (SQLite) or canonical URLno
secretsecret(...)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.comno
revealreveal(...)name/label (scoped)no
signsecp256k1_sign / ed25519_sign (blockchain)key secret's name (audited)no
walletcreate custody: mnemonic_ / hd_derive / keystore_source secret's name (audited)no
spendspend(amount, unit, reason) — audited money declaration (spend.log, ceiling SYNSEMA_SPEND_CEILING)unit name or trailing-* prefix: spend("USD")no (never — like sign)
execrun (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
serveserve on Nportno (and required)
envenv(...)name / prefixno
stdinread_line, free-text ask; term_open (v0.6.11+ — the interactive terminal, raw mode)—no
memorypersistent state family: remember/recall/forget_memory/memory_summary, add_rule/check_rules/get_rules, create_progress/…/resume_pointdeclared 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 ityes 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.