Synsemadocsv0.6.xENES

Operate

WebAssembly

Since v0.6.0 the interpreter also ships as WebAssembly — two artifacts, one pure profile, the same language:

ArtifactTargetForFilesHost hooks
synsema-wasm-wasip1.wasmwasm32-wasip1wasmtime, TEE job runners, any WASI host — a CLI: .wasm + program.synvia WASI preopens (--dir .)none
synsema-wasm-web.wasmwasm32-unknown-unknownembedding: browser, Node/Bun/Deno, Python, Go, edge runtimes — a JSON ABI your app callsnone (data goes through env/source)http, kv, llm, log, sleep — lent by your app

Nobody installs Synsema on the host: the .wasm is the deployable unit, the way a container image is. Both are attached to every release (synsema-wasm-wasip1.wasm, synsema-wasm-web.wasm, each with a .sha256). Try it without installing anything: the docs site's playground runs the interpreter in your browser.

The wasip1 artifact: confidential jobs, TEEs§

# build it once (from a checkout)
rustup target add wasm32-wasip1
cargo build --manifest-path engine/Cargo.toml -p synsema-wasm --target wasm32-wasip1 --profile wasm
# artifact: engine/target/wasm32-wasip1/wasm/synsema-wasm.wasm (~6.7 MB)

# run a program (wasmtime)
wasmtime run --dir . synsema-wasm.wasm program.syn

# run a file's `test` blocks
wasmtime run --dir . synsema-wasm.wasm --test program.syn

# read the program from stdin; pass config/secrets as env
wasmtime run --env ETH_KEY=... synsema-wasm.wasm -   < program.syn

# host ceiling — the SAME flags and parser as `synsema run`
wasmtime run --dir . synsema-wasm.wasm --sandbox program.syn                    # ceiling = [stdout, time]
wasmtime run --dir . synsema-wasm.wasm --cap-set stdout,secret=ETH_* program.syn

synsema-wasm [--test] [--sandbox | --cap-set <list>] [--version] <file.syn | ->. The host ceiling (--sandbox[stdout, time]; --cap-set = name or name=scope, comma-separated) is the same defense-in-depth as in synsema run: a require above it is denied, auto-grants included. An unknown --flag is an error (exit 2), never silently taken as the program path. Exit codes: 0 ok, 1 runtime error / failing test, 2 usage or unreadable program.

No wasmtime at hand? Node ships WASI: node examples/embed/node/run-wasip1.mjs synsema-wasm.wasm program.syn runs the same artifact (Node still flags node:wasi as experimental; it runs the whole binary).

A TEE job (a confidential coprocessor that runs WASM inside an enclave and records the result onchain) is input → pure compute → verifiable output. That is this artifact: read the input, compute, hash/sign the result with the key sealed as a secret under require sign, print the output. The capability manifest doubles as the audit story.

require secret("ETH_KEY")

let resumen be {"suma": sum([120, 180, 95])}
let cuerpo be json_encode(resumen)
let digest be decode(keccak256(cuerpo), "hex")
let addr be eth_address(secret("ETH_KEY"))
print(`resumen={cuerpo}`)
print(`keccak={digest}`)
print(`addr={addr}`)

The embeddable artifact: agents inside apps written in other languages§

The .wasm exports one entry (synsema_call, JSON in / JSON out) and imports three host functions (synsema_host: host_call, host_random_fill, host_now_ms). Any runtime that loads WebAssembly can drive it with ~80 lines of glue — the repository ships three, all exercised by CI:

import { Synsema } from "@synsema/wasm";

const syn = await Synsema.load(new URL("@synsema/wasm/synsema.wasm", import.meta.url));
await syn.ready();

const r = syn.run(`print(keccak256("hola"))`, { env: { KEY: "abc" }, ceiling: "sandbox" });
r.output;   // ["…"]  the program's print lines
r.errors;   // []     parse/runtime errors are data, never exceptions
r.audit;    // every capability check the program made, granted or not

run{ok, output, errors, audit, llm_tokens}; test{passed, failed, lines}; check{ok, errors} (parse + static validation, no execution); handle (below); version. env replaces the .env: secret("KEY")/env("KEY") resolve from it. ceiling takes the same syntax as --cap-set ("stdout,secret=ETH_*") or "sandbox".

What your app lends: http, kv, llm§

The program keeps its manifest; your app decides what it actually gets. Nothing the host lends is reachable without the program's require, and the embedder's ceiling denies above what the host lends. Every check lands in audit.

const store = new Map();
const host = {
  http: (req) => ({ status: 200, headers: [["content-type", "application/json"]], body: "{}" }),
  kv: {
    get: (ns, k) => store.get(ns + "/" + k) ?? null,
    set: (ns, k, v) => store.set(ns + "/" + k, v),
    delete: (ns, k) => store.delete(ns + "/" + k),
    list: (ns) => [...store.keys()].filter((x) => x.startsWith(ns + "/")).map((x) => x.slice(ns.length + 1)),
  },
  llm: (op, prompt) => ({ content: "…", tokens: 12 }),   // plug your SDK here
  log: (line) => console.log(line),
};

syn.run(`require memory("agenda")\nremember("preference", "dark mode", ["ui"])`, { host, filename: "agenda.syn" });
syn.run(`require memory("agenda")\nprint(recall(search="dark")[0]["content"])`, { host, filename: "agenda.syn" });
syn.run(`require net("api.example")\nprint(fetch("https://api.example/ping")["status"])`, { host });
syn.run(`require llm\nprint(reason about "the weather")\nprint(llm_usage())`, { host });
syn.run(`require llm\nprint(reason about "x")`, { host, ceiling: "stdout" });   // denied: the ceiling wins
HookBacksNotes
`http(req) → {status, headers, body \error}`fetch/http_, and the blockchain read-side RPC (eth_balance, solana_, algorand_, btc_)called after the net(host) gate, with the same URL canonicalization as the native binary; req = `{method, url, headers, body \body_base64, timeout}`
kv.get/set/delete/list(ns, key)persistent agent memory — remember/recall/forget_memory, rules, progress — and state_*memory lives under namespace memory:<declared name> (the declared name is the identity, as in the native .db); state_* under state. memory_summary() reports Backend: host-kv. recall searches by substring/tags, as the in-memory store does natively
llm(op, prompt) → {content, tokens}reason/decide/analyze/generate; llm_available() becomes truellm_usage() sums the tokens you report
log(line)the runtime's warnings (bare require reveal …)stderr does not exist in a browser
sleep(secs) or truesleep() and the RPC confirmation pollsa Worker blocks with Atomics.wait; a browser main thread cannot

Without a hook, the builtin fails with the truth: fetch: … this host provides no http transport (wasm profile) — the embedder can offer one through the http host hook, or run the program with the native synsema binary; memory "agenda" is declared but this host provides no durable storage; LLM ops fall back to the core's offline placeholders.

Async hosts (browser fetch, IndexedDB, LLM SDKs)§

The interpreter is synchronous. runAsync/testAsync/handleAsync run it in a Worker and block on Atomics.wait over a SharedArrayBuffer while your Promises resolve on the main thread — responses larger than the buffer travel in chunks. Node/Bun/Deno work out of the box; browsers need cross-origin isolation (Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp). Without it, use the sync API with sync hooks.

const r = await syn.runAsync(program, {
  host: { async http(req) { const res = await fetch(req.url, { method: req.method }); return { status: res.status, headers: res.headers, body: await res.text() }; } },
});
await syn.close();

serve without sockets: edge handler mode§

Cloudflare Workers, Fastly Compute, Fermyon Spin, Vercel Edge all load a .wasm and call a handler per request. handle(source, request) is that handler: your platform passes the request, gets a response.

const app = `require serve(8080)
serve on 8080
    auth with check_token
    errors with shape_error
    route "GET /hello/:name"
        give {"hi": params.name, "visits": state_incr("visits")}
    route "POST /items" requires auth
        give created({"by": request.user.id, "body": request.json})
    route "GET /doc/:id"
        give content(page([heading(1, "Doc"), prose(params.id)], {"title": "Doc"}))`;

const res = syn.handle(app, { method: "GET", path: "/hello/ana?x=1", headers: { accept: "application/json" }, body: "" }, { host });
res.status; res.content_type; res.headers; res.body;   // res.log = the handler's print lines

The program is prepared once per instance (parse, top-level, route table) and reused across requests — only the request changes. Routes by specificity, :param/rest, query, request (json, form, cookies, user), auth with (token, or token + request), errors with, 404/405 with Allow, expect → 400, content() negotiated by suffix or Accept, redirect, with_header/set_cookie, collection pagination, and state_ durable through your kv all work as in the native server — including the two rules that keep it the same language: require serve(port) is still mandatory (the manifest, not the socket), and every request runs on a snapshot of the globals (a set on a global inside a handler does not persist to the next request; shared state goes through state_*). Not in handler mode (the platform does them before calling you): stream (SSE) and proxy to answer 501, rate limits and static mounts are ignored (the mount logs a warning), host blocks (vhosts) and mount of exported route groups are rejected with a clear error, and TLS/ACME terminate at the host.

Time, randomness, files, size§

now() comes from the host clock (Date.now(), time.time(), time.Now()); random(), token(), mnemonic_generate and every signature nonce come from the host's entropy (crypto.getRandomValues, os.urandom, crypto/rand) — the cryptographic doctrine does not change. There is no filesystem: read_file and friends fail saying so. The artifact is ~7.5 MB (2.6 MB gzip), CI enforces a 5 MB gzip budget. A program error is data (errors[]); a trap (an interpreter panic) discards the instance and the glue recreates it.

The same language, an environment that grants less§

The wasm profile is not a dialect. It is the full language in an environment that grants only what the host lends — exactly what the deny-by-default model already expresses. Included in both artifacts, byte-identical to the native binary (CI diffs the probes under wasmtime and through the embed API under Node on every push): the full language, tasks, types, match, try/recover, enums, modules, templates; the numeric tower and arrays; text, regex, JSON, CSV, stats; charts and PNG/PDF export; hashing, HMAC, secret; the whole pure blockchain side (eth_address, ABI, EIP-191/712, tx_eip1559, Solana/Algorand encoding, Bitcoin builder/PSBT, gated *_sign, HD wallets); web-auth pure side (password hashing, JWT, TOTP, oidc_verify with an inline JWKS); sandbox, intent, per-tool capability scoping, the host ceiling; the response helpers and the content() vocabulary; multi-agent (agent/spawn/share/observe/signal/ wait_for) in-process; parallel_map/chunk sequential (same order and fail-fast semantics, no thread pool).

Not in either artifact — the names exist and fail with the truth of the environment, never with Undefined variable:

FamilyBuiltinsError
WebSocket, TLS identityws_*, mtls_identity… not available in the wasm profile — this build has no network sockets (WebSocket/TLS identity need an event loop and a process)
Databasesdb_open/db_close, sql/sql_exec/sql_batch/sql_tables/paged, mongo_, redis_… — this build has no database drivers … (in edge, reach D1/Neon/Upstash over http)
Croncron_*… — this build has no scheduler threads … (the host schedules; a job is invoked)
Real threadsspawn, parallel_mapkeep their semantics, run in-process / sequentially