Synsemadocsv0.6.xENES

Capabilities & Security

Sandboxing untrusted code

The rest of the security model (require, per-task scoping) assumes you wrote the code. This page is the opposite: running code you don't trust — an LLM's output, a user's plugin, a public playground. Two tools: one from inside your program, one from outside.

1. The sandbox block — isolate a part of your program§

Wrap the piece that handles untrusted input. Inside, all capabilities are stripped — no net/file/db/secret/exec, only pure computation + print. A require inside is a no-op (it can't re-grant to escape).

let payload be fetch("https://api.external.com/data")   -- untrusted input
sandbox
    let result be transform(payload)   -- if `transform` is exploited, it can touch NOTHING

It's also an expression:

let clean be sandbox validate(untrusted_input)   -- isolated, returns a value

Use it when part of your code must process something dangerous but must not reach the outside.

sandbox.syn
-- Doc example: the `sandbox` block — isolate a piece of code with NO capabilities.
-- As an expression it computes and returns; net/file/db/secret/exec are stripped inside.
-- Plus run_program: Synsema running Synsema in a child process under a ceiling ∩ the parent's.
intent: "doc example: sandbox block + run_program"
require sandbox_run

task risky_pure(n)
    give n * n

-- sandbox as an EXPRESSION: isolated, returns the value (run shows it)
print("sandbox result: " + text(sandbox risky_pure(7)))    -- sandbox result: 49

test "sandbox runs pure computation and returns the value"
    assert_eq(sandbox risky_pure(7), 49)
    assert_eq(sandbox (40 + 2), 42)

-- run_program: the child's result — and its audit — come back as a VALUE, not a log to parse.
test "run_program runs a child under a ceiling; its result is a value"
    let r be run_program("print(2 + 3)", {"ceiling": "stdout", "profile": "pure", "timeout": 20})
    assert(r["ok"])
    assert_eq(r["output"], ["5"])
    assert_eq(r["exit"], 0)

test "the child can never exceed the parent: exec it wasn't lent is denied, with a structured audit"
    -- the child asks for exec; the parent didn't lend it, so run() is denied and the child fails.
    let r be run_program("run(\"echo\", [\"x\"])", {"ceiling": "stdout", "profile": "native", "timeout": 20})
    assert_eq(r["ok"], false)
    assert(some(r["audit"], (e) => contains(e["capability"], "exec") and not e["granted"]))

2. The host ceiling — --sandbox / --cap-set§

When you run a whole program you didn't write, the person running synsema sets a ceiling the code can't exceed, no matter what it declares:

synsema run  --sandbox program.syn                       # 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 three layers together§

LayerWho restrictsFor
require cap("scope")the code (declares what it needs)code you trust
sandbox blockthe code (isolates a part of itself)code you trust
--sandbox / --cap-setthe host (from outside)code you DON'T trust

They compose — the most restrictive wins.

3. run_program — Synsema running Synsema under a ceiling§

The pattern above — write a file, exec the synsema binary, parse stdout — is what run_program replaces (engine v0.6.14+). It runs another Synsema program in a child process of the same binary, under a ceiling that is the intersection with the parent's, and returns its result — and its audit — as a value. No exec, no synsema on the PATH, no stderr parsing:

require sandbox_run                         -- deny-by-default, no scope
require net("registry.npmjs.org")           -- what you'll LEND, you must hold

let r be run_program(code, {
    "ceiling": "stdout,net=registry.npmjs.org",   -- --cap-set syntax, or "sandbox", or "none"
    "profile": "pure",                            -- "pure" (default) or "native"
    "env":     {"TOOL": "package"},               -- REPLACES the child's environment
    "timeout": 30,                                -- seconds (default 30); on timeout the tree is killed
    "cwd":     "/path/to/work"                    -- default: the parent's cwd
})
-- r = {"ok": bool, "output": [lines], "errors": [text], "audit": [entries], "exit": n|nothing,
--      "timed_out": bool, "llm_tokens": n}

The child can never exceed the parent. Its effective ceiling is opts.ceiling ∩ what the parent can actually grant — asking for more is not an error, it's trimmed, and the parent's audit records it (reason: "above parent ceiling"). So net=* under a parent that only holds net("registry.npmjs.org") collapses to nothing. What the parent doesn't require, it can't lend.

gone unless you pass them). A secret value in env is refused — reveal() it explicitly.

cancellation (a serve request timeout, agent_stop) does too.

(SYNSEMA_RUN_PROGRAM_MAX_DEPTH, default 4).

arbitrary exec); under the wasm profile it isn't (no child processes).

This is the safe runner for a playground, an MCP run_synsema tool, or an agent executing code it generated: the ceiling is enforced by the child process itself, and the audit is data.

4. The pure profile — a second wall§

--cap-set is one wall: the ceiling. --profile pure is a second, independent one — the same wall the WebAssembly build has always had, now on the native binary:

synsema run --profile pure --cap-set "stdout,net=api.example.com" program.syn

Under pure, every OS-facing builtin does not exist as such: the names are still bound (never Undefined variable), but they fail with the truth of the environment — read_file: not available in the pure profile — this run has no filesystem, and likewise for run, the ws_/socket family, the databases, cron_ and the process hub (select/proc_*/watch). This holds regardless of the ceiling: a bug in the interpreter can't hand out access to something that isn't registered.

What stays under pure: the whole language, math, JSON/CSV, charts, hashing, the pure blockchain and web-auth surface, fetch/http_* with net (pure is "no local machine", not "no network"), the LLM ops with llm, secret/reveal/env, agents/spawn/parallel_map (in-process), remember/recall without disk (in-memory for the run), run_program, and reading a synsema build bundle (a bundled asset is the program, not the filesystem). The full "included vs not" table is on the WASM page — the two profiles are the same wall.

Two walls, not one: the ceiling, and the builtin that isn't there.

The three layers together§

Add the pure profile and both host tools compose with the code's own:

LayerWho restrictsFor
require cap("scope")the code (declares what it needs)code you trust
sandbox blockthe code (isolates a part of itself)code you trust
--sandbox / --cap-setthe host (ceiling, from outside)code you DON'T trust
--profile purethe host (the second wall)code you DON'T trust
run_program(…, {ceiling, profile})the program (a child ⊆ itself)code it doesn't trust

The most restrictive wins.

Why it matters for agents: an agent that writes and runs Synsema can execute its own code with run_program under a ceiling it can't escape, and read the audit back as a value — so "run the code the LLM generated" is safe by construction. For a public deploy, wrap it in an OS container as a further layer (defense in depth). This is how this site's playground and its MCP run_synsema tool work.