Referencia
Índice de capacidades
Deny-by-default: se declara con require capability("scope"). Ver Capacidades e intent para el modelo.
-- 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| Capacidad | Gatea | Scope | ¿Auto-otorgada en run? |
|---|---|---|---|
stdout | print / salida | — | sí |
time | leer 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í |
llm | reason/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 LLM | sí |
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 techo | sí (run/conform planos); exigida bajo serve y en modo seguro |
random | entropí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) |
net | http*, 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 |
file | lectura y escritura | ruta: file("/data/**") | no |
file.read / file.write | I/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 |
db | sql/mongo_/redis_ | ruta (SQLite) o URL canónica | no |
secret | secret(...) | 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.com | no |
reveal | reveal(...) | nombre/label (scopeado) | no |
sign | secp256k1_sign / ed25519_sign (blockchain) | nombre del secret de la clave (auditado) | no |
wallet | crear custodia: mnemonic_ / hd_derive / keystore_ | nombre del secret de origen (auditado) | no |
spend | spend(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) |
exec | run (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 |
serve | serve on N | puerto | no (y exigida) |
env | env(...) | nombre / prefijo | no |
stdin | read_line, ask de texto libre; term_open (v0.6.11+ — la terminal interactiva, raw mode) | — | no |
memory | la familia de estado persistente: remember/recall/forget_memory/memory_summary, add_rule/check_rules/get_rules, create_progress/…/resume_point | nombre 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 acota | sí 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.