Synsemadocsv0.6.xENES

Referencia

Índice de capacidades

Deny-by-default: se declara con require capability("scope"). Ver Capacidades e intent para el modelo.

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
CapacidadGateaScope¿Auto-otorgada en run?
stdoutprint / salida—sí
timeleer el reloj: now, sleep, y los verificadores/emisores que sellan o chequean un vencimiento (jwt_sign/jwt_verify, totp/totp_verify, captoken_*, http_sign/http_signature_verify, oidc_verify — cada uno acepta también el tiempo explícito). No format_time/parse_time/date_parts ni los tipos de fecha: nunca leen el reloj (puros desde v0.6.29)—sí
llmreason/decide/analyze/generate, llm_step (incl. egress al proveedor)—; v0.6.46+ con etiquetas: llm("P") declara de quién son los datos privados que pueden llegar al modelo, y el host lo permite por proveedor (--cap-set llm=P@<host del proveedor>, @local, @embedder en wasm; un ítem sin @ no deja pasar datos privados) — ver Primitivas LLMsí
judge (v0.6.25+)el bloque judge (Judge), y decide cuando SYNSEMA_JUDGE_DECIDE=1. Capacidad propia: llm no la concede ni al revés (clasificar y generar son derechos distintos). Denegada bajo --deterministic; siempre offline en un guest wasm— (sin scope); v0.6.46+ con etiquetas: judge("P") + el judge=P@<host|local> del host para datos privados; en wasm judge ahora se chequea contra el techosí (run/conform planos); exigida bajo serve y en modo seguro
randomentropía del sistema operativo: random() y random_int(lo, hi) sin generador, random_bytes, token, push_vapid_keys (v0.6.15+), ecdh_keypair (v0.6.20+). Un generador con semilla — rng(seed), g(), random(g), random_int(g, lo, hi), random_normal, shuffle, choice, sample, rng_spawn — es puro y no necesita nada (v0.6.29+)—no (tokens/nonces)
nethttp*, fetch, ws_connect, push_send (v0.6.15+ — host del endpoint de la suscripción: el push service)host: net("api.x"), net(".x"), net(""); host:puerto cuando se escribe un puerto — entonces solo ese puerto (v0.6.29+: net("localhost:8545"), net("[::1]:8545")); una URL se guarda como su host (y puerto), sin credenciales, ruta ni query; v0.6.46+: un comodín (require net, net(""), net(".x.com"), --cap-set net, sandbox under, un token) llega sólo a direcciones públicas — un host privado necesita su nombre, su dirección o un CIDR (net("10.0.0.0/8"))no
filelectura y escrituraruta: file("/data/**")no
file.read / file.writeI/O de mínimo privilegio; file.read también gatea watch(path) (v0.6.9+ — mirar un árbol es leerlo); v0.6.20+: file.read(".") gatea cwd(), file.read gatea los archivos from de zip_create/tar_create y las fonts de svg_to_; file.write gatea delete_file/delete_dir (cada ruta borrada) y zip_extract/tar_extract (cada ruta escrita); v0.6.45+: file.read("<dir>/*") gatea un montaje static de <dir> y grep sobre una carpeta, file.read gatea { include }/{ layout } desde disco, los dos archivos de tls cert … key … en un bloque serve, y edit_file (con file.write)glob de ruta por segmento: */? dentro de una carpeta, cruza carpetas; v0.6.46+: un watch recursivo necesita file.read("<dir>/") (uno no recursivo, sólo la carpeta)no
dbsql/mongo_/redis_ruta (SQLite) o URL canónicano
secretsecret(...)nombre: secret("APP_*"); v0.6.46+ ligado a hosts: require secret("K") for net("api.example.com") (entonces puede ir en una URL, query, body o form, sólo ahí); techo del host --cap-set secret=K@api.example.comno
revealreveal(...)nombre/label (scopeado)no
signsecp256k1_sign / ed25519_sign (blockchain)nombre del secret de la clave (auditado)no
walletcrear custodia: mnemonic_ / hd_derive / keystore_nombre del secret de origen (auditado)no
spendspend(monto, unidad, motivo) — declaración auditada de dinero (spend.log, techo SYNSEMA_SPEND_CEILING)nombre de unidad o prefijo con * final: spend("USD")no (jamás — como sign)
execrun (one-shot) y proc_spawn (proceso vivo, v0.6.7+; pty: true v0.6.8+ — sin capability extra, un pty no da poder del SO que un pipe no tenga)nombre del comando; v0.6.46+: con cwd, un comando relativo se resuelve adentro y el grant nombra esa ruta (run("./tool", [], 60, {"cwd": "sub"}) → exec("./sub/tool"))no
serveserve on Npuertono (y exigida)
envenv(...)nombre / prefijono
stdinread_line, ask de texto libre; term_open (v0.6.11+ — la terminal interactiva, raw mode)—no
memoryla familia de estado persistente: remember/recall/forget_memory/memory_summary, add_rule/check_rules/get_rules, create_progress/…/resume_pointnombre declarado = la identidad del .db: memory("agent-name") (prefijo en el techo: memory=shop-*)no (escribe archivos; la declaración ES la identidad)
attest (v0.6.24+)attest(opts) / attest_key(propósito) / attestation_key() — pedirle a la plataforma (AWS Nitro, TDX/SEV-SNP, dstack) un documento o una clave sellada. attestation_document() y attestation_verify() no están gateados: leer la propia identidad publicada y verificar el documento de alguien no tocan nada— (sin scope)no — y ausente de todo techo empaquetado, así que --deterministic la niega por sí solo
declassify (v0.6.46+)con etiquetas, declassify(valor, por_qué) que quita un principal (Etiquetas)principal o glob: --cap-set declassify=customers; require declassify("P") la declara (un require declassify desnudo avisa) pero no la acotasí sin techo (como unsafe en tu propio crate: explícita, con su motivo, listada entera por synsema code check --json); con techo, sólo los principales que nombra; --sandbox, --deterministic y sandbox nunca la conceden (sandbox under {"declassify": …} la delega). Sin --labels no se chequea
sandbox_run (v0.6.14+)run_program(source, opts) — corre otro programa Synsema en un proceso hijo bajo un techo ∩ el del padre— (sin scope)no

Notas: synsema check compara scopes con el mismo matcher del runtime (v0.6.30+: file.read("./") cubre read_file("x.csv") en los dos). Los scopes de ruta son globs por segmento* (v0.6.45+): y ? matchean dentro de un segmento de la ruta, cruza carpetas y ./data/ cubre también ./data misma; ningún comodín matchea .. ni salta de una ruta relativa a una absoluta; el disco entero es la capacidad sin scope (require file.read), no ""; C:/ cubre el disco. Los scopes de ruta son fieles (los escapes con .. se deniegan; se comparan sin distinguir mayúsculas en Windows/macOS, salvo en una carpeta que sí las distingue, como las que crea WSL) y se juzgan donde de verdad lleva la ruta (v0.6.45+): un link que es parte de la grant funciona, un symlink o junction debajo de ella que lleva afuera se rechaza (a link (symlink or junction) on the way leads outside it), y grep no sigue links a carpetas — detalles en Capacidades; ~/ expande al home en scopes y rutas (v0.6.20+). sandbox despoja todo. Un require por-task acota la task a solo lo que declara (∩ el programa). Bajo serve/modo seguro, incluso las auto-otorgadas deben declararse. En --cap-set, none es un techo vacío (nada, ni siquiera stdout). Lo que el programa lee vía un template (render/include/layout de un archivo en disco*) también lo gatea file.read, como read_file — los templates horneados en un binario synsema build son parte del programa y no necesitan capability.