Synsemadocsv0.6.xENES

Operación

WebAssembly

Desde v0.6.0 el intérprete también se distribuye como WebAssembly — dos artefactos, un perfil puro, el mismo lenguaje:

ArtefactoTargetParaArchivosHooks del host
synsema-wasm-wasip1.wasmwasm32-wasip1wasmtime, runners de jobs en TEE, cualquier host WASI — una CLI: .wasm + programa.synpor preopens WASI (--dir .)ninguno
synsema-wasm-web.wasmwasm32-unknown-unknownembeber: navegador, Node/Bun/Deno, Python, Go, runtimes edge — una ABI JSON que tu app llamaninguno (los datos viajan por env/fuente)http, kv, llm, log, sleep — los presta tu app

Nadie instala Synsema en el host: el .wasm es la unidad desplegable, como lo es una imagen de contenedor. Los dos van adjuntos a cada release (synsema-wasm-wasip1.wasm, synsema-wasm-web.wasm, cada uno con su .sha256). Probalo sin instalar nada: el playground del sitio corre el intérprete en tu navegador.

El artefacto wasip1: jobs confidenciales, TEEs§

# compilarlo una vez (desde un checkout)
rustup target add wasm32-wasip1
cargo build --manifest-path engine/Cargo.toml -p synsema-wasm --target wasm32-wasip1 --profile wasm
# artefacto: engine/target/wasm32-wasip1/wasm/synsema-wasm.wasm (~6,7 MB)

# correr un programa (wasmtime)
wasmtime run --dir . synsema-wasm.wasm programa.syn

# correr los bloques `test` de un archivo
wasmtime run --dir . synsema-wasm.wasm --test programa.syn

# leer el programa de stdin; config/secretos por env
wasmtime run --env ETH_KEY=... synsema-wasm.wasm -   < programa.syn

# techo del host — las MISMAS flags y el mismo parser que `synsema run`
wasmtime run --dir . synsema-wasm.wasm --sandbox programa.syn                   # techo = [stdout, time]
wasmtime run --dir . synsema-wasm.wasm --cap-set stdout,secret=ETH_* programa.syn

synsema-wasm [--test] [--sandbox | --cap-set <lista>] [--version] <archivo.syn | ->. El techo del host (--sandbox[stdout, time]; --cap-set = nombre o nombre=scope, separados por coma) es la misma defensa en profundidad que en synsema run: un require por encima se deniega, auto-grants incluidos. Una --flag desconocida es error (exit 2), nunca se toma en silencio como la ruta del programa. Códigos de salida: 0 ok, 1 error de runtime / test fallido, 2 uso o programa ilegible.

¿Sin wasmtime a mano? Node trae WASI: node examples/embed/node/run-wasip1.mjs synsema-wasm.wasm programa.syn corre el mismo artefacto (Node sigue marcando node:wasi como experimental; corre el binario entero).

Un job en TEE (un coprocesador confidencial que corre WASM dentro de un enclave y registra el resultado onchain) es entrada → cómputo puro → salida verificable. Eso es este artefacto: leer la entrada, computar, hashear/firmar el resultado con la clave sellada como secret bajo require sign, imprimir la salida. El manifiesto de capabilities es a la vez la historia de auditoría.

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}`)

El artefacto embebible: agentes dentro de apps hechas en otros lenguajes§

El .wasm exporta una entrada (synsema_call, JSON entra / JSON sale) e importa tres funciones del host (synsema_host: host_call, host_random_fill, host_now_ms). Cualquier runtime que cargue WebAssembly lo maneja con ~80 líneas de glue — el repositorio trae tres, todas ejercitadas por 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;   // ["…"]  las líneas de print del programa
r.errors;   // []     los errores de parse/runtime son datos, nunca excepciones
r.audit;    // cada chequeo de capability que hizo el programa, concedido o no

run{ok, output, errors, audit, llm_tokens}; test{passed, failed, lines}; check{ok, errors} (parse + validación estática, sin ejecutar); handle (abajo); version. env reemplaza al .env: secret("KEY")/env("KEY") resuelven de ahí. ceiling acepta la sintaxis de --cap-set ("stdout,secret=ETH_*") o "sandbox".

Lo que tu app presta: http, kv, llm§

El programa conserva su manifiesto; tu app decide qué recibe de verdad. Nada de lo que el host presta se alcanza sin el require del programa, y el ceiling del embebedor deniega por encima de lo que el host presta. Cada chequeo queda en 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 }),   // acá va tu SDK
  log: (line) => console.log(line),
};

syn.run(`require memory("agenda")\nremember("preference", "modo oscuro", ["ui"])`, { host, filename: "agenda.syn" });
syn.run(`require memory("agenda")\nprint(recall(search="oscuro")[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 "el clima")\nprint(llm_usage())`, { host });
syn.run(`require llm\nprint(reason about "x")`, { host, ceiling: "stdout" });   // denegado: el techo manda
HookRespaldaNotas
`http(req) → {status, headers, body \error}`fetch/http_ y el read-side RPC de blockchain (eth_balance, solana_, algorand_, btc_)se llama después del gate net(host), con la misma canonización de URL que el binario nativo; req = `{method, url, headers, body \body_base64, timeout}`
kv.get/set/delete/list(ns, key)la memoria persistente del agente — remember/recall/forget_memory, reglas, progress — y state_*la memoria vive bajo el namespace memory:<nombre declarado> (el nombre declarado es la identidad, como en el .db nativo); state_* bajo state. memory_summary() reporta Backend: host-kv. recall busca por substring/tags, como el store en memoria nativo
llm(op, prompt) → {content, tokens}reason/decide/analyze/generate; llm_available() pasa a truellm_usage() suma los tokens que reportás
log(line)los avisos del runtime (bare require reveal …)en un navegador no hay stderr
sleep(secs) o truesleep() y los polls de confirmación RPCun Worker bloquea con Atomics.wait; el main thread de un navegador no puede

Sin el hook, el builtin falla con la verdad: 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; las ops LLM caen a los placeholders offline del core.

Hosts asíncronos (fetch del navegador, IndexedDB, SDKs de LLM)§

El intérprete es síncrono. runAsync/testAsync/handleAsync lo corren en un Worker y bloquean con Atomics.wait sobre un SharedArrayBuffer mientras tus Promises resuelven en el main thread — las respuestas más grandes que el buffer viajan en chunks. Node/Bun/ Deno funcionan sin más; el navegador necesita cross-origin isolation (Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp). Sin eso, usá la API síncrona con hooks síncronos.

const r = await syn.runAsync(programa, {
  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 sin sockets: modo handler para edge§

Cloudflare Workers, Fastly Compute, Fermyon Spin, Vercel Edge cargan un .wasm y llaman un handler por request. handle(source, request) es ese handler: tu plataforma le pasa el request y recibe la respuesta.

const app = `require serve(8080)
serve on 8080
    auth with check_token
    errors with shape_error
    route "GET /hola/:nombre"
        give {"hola": params.nombre, "visitas": state_incr("visitas")}
    route "POST /items" requires auth
        give created({"por": 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: "/hola/ana?x=1", headers: { accept: "application/json" }, body: "" }, { host });
res.status; res.content_type; res.headers; res.body;   // res.log = las líneas de print del handler

El programa se prepara una vez por instancia (parse, top-level, tabla de rutas) y se reusa entre requests — sólo cambia el request. Rutas por especificidad, :param/resto, query, request (json, form, cookies, user), auth with (token, o token + request), errors with, 404/405 con Allow, expect → 400, content() negociado por sufijo o Accept, redirect, with_header/set_cookie, paginación de colecciones y state_ durable a través de tu kv funcionan como en el server nativo — incluidas las dos reglas que lo mantienen como el mismo lenguaje: require serve(puerto) sigue siendo obligatorio (el manifiesto, no el socket), y cada request corre sobre un snapshot de los globales (un set sobre un global dentro de un handler no persiste al request siguiente; el estado compartido va por state_*). No están en modo handler (la plataforma los hace antes de llamarte): stream (SSE) y proxy to responden 501, rate limits y static se ignoran (el mount deja un aviso en el log), los bloques host (vhosts) y el mount de grupos de rutas exportados se rechazan con un error claro, y TLS/ACME terminan en el host.

Tiempo, azar, archivos, tamaño§

now() sale del reloj del host (Date.now(), time.time(), time.Now()); random(), token(), mnemonic_generate y cada nonce de firma salen de la entropía del host (crypto.getRandomValues, os.urandom, crypto/rand) — la doctrina criptográfica no cambia. No hay filesystem: read_file y compañía fallan diciéndolo. El artefacto pesa ~7,5 MB (2,6 MB gzip); CI impone un presupuesto de 5 MB gzip. Un error del programa es dato (errors[]); un trap (un panic del intérprete) descarta la instancia y el glue la recrea.

El mismo lenguaje, un entorno que otorga menos§

El perfil wasm no es un dialecto. Es el lenguaje completo en un entorno que otorga sólo lo que el host presta — exactamente lo que el modelo deny-by-default ya expresa. Incluido en los dos artefactos, byte a byte idéntico al binario nativo (CI diffea las sondas bajo wasmtime y a través de la API de embebido bajo Node en cada push): el lenguaje completo, tasks, tipos, match, try/recover, enums, módulos, templates; la torre numérica y arrays; texto, regex, JSON, CSV, estadística; charts y export PNG/PDF; hashing, HMAC, secret; todo el lado puro de blockchain (eth_address, ABI, EIP-191/712, tx_eip1559, encoding Solana/Algorand, builder/PSBT de Bitcoin, *_sign gateados, HD wallets); el lado puro de web-auth (password hashing, JWT, TOTP, oidc_verify con JWKS inline); sandbox, intent, scoping de capabilities por tool, el techo del host; los helpers de respuesta y el vocabulario content(); multi-agente (agent/spawn/share/ observe/signal/wait_for) in-process; parallel_map/chunk secuencial (mismo orden y semántica fail-fast, sin pool de threads).

En ninguno de los dos artefactos — los nombres existen y fallan con la verdad del entorno, nunca con Undefined variable:

FamiliaBuiltinsError
WebSocket, identidad TLSws_*, mtls_identity… not available in the wasm profile — this build has no network sockets (WebSocket/TLS identity need an event loop and a process)
Bases de datosdb_open/db_close, sql/sql_exec/sql_batch/sql_tables/paged, mongo_, redis_… — this build has no database drivers … (en edge, llegá a D1/Neon/Upstash por http)
Croncron_*… — this build has no scheduler threads … (el host agenda; el job se invoca)
Threads realesspawn, parallel_mapconservan su semántica, corren in-process / secuencial