Lenguaje
Módulos (use / export)
Dividí el código en archivos. export hace público un task/type/let/enum/routes; todo lo demás queda privado. Un módulo es un espacio de nombres de sus exports, importado bajo un alias (keys(math) los lista).
El módulo — mathlib.syn:
export task square(n)
give n * n
export let VERSION be "1.0"
export let STATS be {"calls": 0}
export task counted_square(n)
set STATS["calls"] to STATS["calls"] + 1
give n * n
task private_helper() -- no `export` → not visible to importers
give 42
El entry que lo importa:
-- Doc example: modules (use / export). Imports ./mathlib.syn under the alias `math`.
intent: "doc example: modules"
use "./mathlib.syn" as math
print("math.square(5) = " + text(math.square(5)) + ", VERSION " + math.VERSION)
-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
try
f()
recover err
give err
give nothing
test "import an exported task and let (private_helper is NOT visible)"
assert_eq(math.square(5), 25)
assert_eq(math.VERSION, "1.0")
assert_eq(keys(math), ["square", "VERSION", "STATS", "counted_square"])
assert(contains(error_of(() => math.private_helper()), "export task private_helper"))
test "an exported let is ONE variable: the module's tasks and the importer see the same one"
let before be math.STATS
math.counted_square(3)
assert_eq(math.STATS["calls"], before["calls"] + 1)
set math.STATS["calls"] to 100
math.counted_square(3)
assert_eq(math.STATS["calls"], 101)
test "let snap be mod.X is a snapshot (a value)"
let snap be math.STATS
math.counted_square(2)
assert_eq(math.STATS["calls"], snap["calls"] + 1)
test "set mod.X rebinds the export; tasks and new names cannot be set from outside"
set math.VERSION to "2.0"
assert_eq(math.VERSION, "2.0")
set math.VERSION to "1.0"
assert(contains(error_of(() => set_task()), "cannot replace the module task 'square'"))
assert(contains(error_of(() => set_new()), "module has no export 'NEW'"))
test "every path to the module reaches the same state; parallel workers get their own"
let mods be {"m": math}
set mods.m.STATS["calls"] to 7
assert_eq(math.STATS["calls"], 7)
let r be parallel_map((x) => math.counted_square(x), [1, 2])
assert_eq(r, [1, 4])
assert_eq(math.STATS["calls"], 7)
task set_task()
set math.square to 1
task set_new()
set math.NEW to 1Reglas§
use "./path.syn" as alias— los paths son relativos al archivo que importa; solo.syn, sin URLs/FFI.../sube dentro del proyecto (engine v0.6.20+): el límite es la raíz del proyecto — el directorio del archivo de entrada — así quesrc/site/pages.synpuede haceruse "../core/i18n.syn"; un path que resuelve por encima de la raíz falla (module path escapes the project root; desde el propio archivo de entrada el mensaje sigue siendo el históricoescapes the importing directory). La contención se decide sobre rutas absolutas, así quecd proj && synsema run main.synes exactamente tan seguro como correrlo por ruta absoluta.synsema checkademás avisa (el exit code no cambia, v0.6.20+) cuando unletde nivel superior o un grupoexport routeslleva el mismo nombre que un alias deuse … as— dentro de ese ámbito el nombre resuelve al local, no al módulo.- Las llamadas entre archivos necesitan el prefijo del alias:
math.square(5),math.VERSION. - Leer un nombre que el módulo no exporta es un error que sugiere el arreglo:
the module has no export 'private_helper' — it is defined inside the module but not exported (writeexport task private_helperthere …). - Los imports están cacheados (se cargan una vez), son transitivos y con chequeo de ciclos.
- Un módulo no puede tener
requireniservetop-level — eso va en el archivo entry. Unrequirepor-task dentro de una task del módulo sí está bien. synsema check entry.synresuelve y parsea el grafo de imports completo (paths rotos, ciclos,serve/requireprohibidos en módulos: todo falla el check).
Estado del módulo — un let exportado es una sola variable§
Las listas y los maps son valores en todos lados, pero un módulo no es un valor: un let exportado es una sola variable, la misma que leen y escriben las tasks del propio módulo.
math.counted_square(3)actualizaSTATSdentro del módulo, ymath.STATSlo muestra.set math.STATS["calls"] to 100escribe la variable del módulo, y sus tasks ven el cambio.set math.VERSION to "2.0"rebindea el export, comomath.VERSION = "2.0"en Python.let snap be math.STATSes una foto — un valor que los cambios posteriores no tocan.- Desde afuera, un módulo no gana nombres y sus tasks no se pueden reemplazar:
set math.NEW to 1 → module has no export 'NEW'; set math.square to 1 → cannot replace the module task 'square' from outside the module.
Todo camino a un módulo llega al mismo estado: dos use del mismo archivo, un re-export (export let L be math en otro módulo), o un módulo guardado en un map. Bajo serve y parallel_map cada request o worker tiene su propia copia del estado del módulo; para compartir entre ellos, usá state_*, SQL o remember.
export routes — grupos de rutas que un serve puede montar (mount)§
Un sitio grande no tiene que ser un solo bloque serve. Un módulo exporta un grupo de rutas; el entry lo monta (los cuerpos llaman a los helpers privados del módulo por nombre simple):
-- shop.syn
task fmt(n) -- private
give "$" + text(n)
export routes shop
route "GET /shop"
give html("<h1>" + fmt(99) + "</h1>")
route "POST /shop/buy"
expect body {item: text}
give created(json of request)
-- app.syn
use "./shop.syn" as shop
serve on 8080
mount shop.shop -- or: mount shop.shop at "/store"
Los grupos aceptan entradas route con su propio rate_limit y timeout (engine v0.6.19+: una ruta montada tiene su propia zona de rate limit, y un prefijo de mount es otra zona — antes de v0.6.19 los dos se rechazaban dentro de un grupo); las rutas stream y socket también funcionan dentro de un grupo desde engine v0.6.20 (se montan como una ruta directa, con zona de rate limit propia — antes, synsema check y serve las rechazaban). Una ruta del grupo, o el grupo entero, puede ser private (v0.6.20+: la línea private al principio del cuerpo de la ruta o del grupo): se sirve, pero queda fuera de /llms.txt, /openapi.json, /sitemap.xml y /docs — mirá Descubrimiento. Un requires auth montado sigue exigiendo auth with en el bloque serve, validado al construir el serve. Mirá Servidor HTTP y Construí un sitio web.
Módulos de confianza§
Motor v0.6.46+. Un módulo de confianza es un módulo que carga el host — escrito por una persona — y que el código en el que el host no confía (un programa escrito por un modelo, por ejemplo) sólo puede llamar. Cada task exportada corre con sus propios require, nunca con los de quien la llama. Así, un programa bajo un techo sin declassify no puede publicar los datos de un cliente, pero sí puede llamar t.reply(customer, text), cuya política desclasifica sólo la dirección del propio cliente.
-- tools.syn — written by a person, loaded by the host
export task reply(customer, body)
require declassify("customers")
let dest be declassify(customer.email, "the reply goes to the customer's own address")
give {"to": dest, "length": declassify(length(body), "the length of a reply is public")}
-- app.syn — may be written by a model: under this ceiling it cannot declassify by itself
use "trusted:tools" as t
let c be private({"email": "ana@example.org", "name": "Ana"}, "customers")
print(t.reply(c, "your order shipped")) -- {to: "ana@example.org", length: 18}
synsema run --labels --cap-set stdout --trusted tools=./tools.syn \
--trusted-cap-set "stdout,declassify=customers" app.syn
El mismo programa con print(declassify(c.email, "direct")) se frena en esa línea: `label_violation: declassify is not granted for what this value is private to (customers) …`.
-- Doc example: a trusted module. The HOST loads it (`--trusted tools=./tools.syn`); the program can
-- only call it. Each exported task runs with its own `require`s, bounded by `--trusted-cap-set`.
export let config be {"limit": 3}
export task reply(customer, body)
require declassify("customers")
let dest be declassify(customer.email, "the reply goes to the customer's own address")
give {"to": dest, "length": declassify(length(body), "the length of a reply is public")}
export task limit()
give config.limit
export task shout(t)
give upper(t)
export task apply_to(f, x)
give f(x)-- Doc example: trusted modules. Runs ONLY as
-- synsema test --labels --cap-set stdout --trusted tools=docs/0.6.x/examples-trusted/tools.syn \
-- --trusted-cap-set "stdout,declassify=customers" docs/0.6.x/examples-trusted/trusted.syn
-- The program's own ceiling (`--cap-set stdout`) has no `declassify`: it publishes the customer's
-- address only through the tool a person wrote.
intent: "doc example: trusted modules"
use "trusted:tools" as t
let CUSTOMER be private({"email": "ana@example.org", "name": "Ana"}, "customers")
-- A program task that shadows a builtin: the tool never sees it.
task upper(x)
give "HIJACKED"
task mine(x)
give x
-- To offer a trusted tool to a model, wrap it in a task of the program.
task reply(customer: map, body: text)
give t.reply(customer, body)
-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
try
f()
recover err
give err
give nothing
test "the tool publishes what its policy allows"
let r be t.reply(CUSTOMER, "your order shipped")
assert_eq(r, {"to": "ana@example.org", "length": 18})
test "the tool never sees the program's names"
assert_eq(t.shout("hi"), "HI")
test "arguments are data, not code"
assert(contains(error_of(() => t.apply_to(mine, 1)), "a trusted tool takes data, not code"))
assert(contains(error_of(() => t.apply_to((x) => x, 1)), "a trusted tool takes data, not code"))
test "the program changes only its own copy of the exports"
set t.config to {"limit": 99}
assert_eq(t.config, {"limit": 99})
assert_eq(t.limit(), 3)
test "trusted tasks take positional arguments; wrap them to offer them to a model"
assert(contains(error_of(() => t.shout(t = "x")), "does not accept named arguments"))
assert_eq(tool_spec(reply, "Reply to a customer").params, ["customer: map", "body: text"])
assert_eq(reply(CUSTOMER, "hi").to, "ana@example.org")Banderas§
| Bandera | Qué hace |
|---|---|
--trusted <nombre>=<ruta> | carga <ruta> como trusted:<nombre>; se puede repetir; en synsema run y synsema test. nombre es letras, dígitos, _ o -; la ruta es relativa al directorio de trabajo |
--trusted-cap-set "<lista>" | el techo de todos los módulos de confianza, con la misma sintaxis que --cap-set |
En @synsema/wasm las opciones son trusted: {tools: source} y trusted_ceiling (WASM). Los errores de las banderas: `--trusted takes name=path (a name of letters, digits, '_' or '-'), e.g. --trusted tools=./tools.syn, --trusted tools: cannot read ./x.syn: …, --trusted tools: the name is given twice. synsema serve rechaza la bandera: --trusted runs under synsema run and synsema test for now (a trusted tool is not isolated in the workers of serve yet)`.
Qué puede hacer una task de confianza§
- Sus capacidades son sus propios
require, adentro de cada task exportada. Unrequiretop-level en el módulo es un error de carga:module 'trusted:tools' must not have a top-level 'require': each exported task declares its own. No viene nada de quien llama: ni sus grants, ni su--cap-set, ni unsandboxalrededor de la llamada. Una herramienta que usanetsinrequire net("…")falla como cualquier programa, aunque el programa tenganet; una que lo declara bajo un--trusted-cap-setque no lo nombra falla con… declared but above the host ceiling. declassifyen una task de confianza lo acota--trusted-cap-set. Condeclassify=customerspublicacustomersy nada más. Sin--trusted-cap-setdesclasifica como un programa sin techo — cualquier principal — así que dale un techo a los módulos de confianza. (Medido en v0.6.46: elrequire declassify("customers")de la task documenta la política, pero la cota es el techo de confianza — una task sin eserequireigual desclasifica dentro de él.)- Puede hacer
usede otro módulo de confianza (use "trusted:base" as base). Un módulo de archivo es un error: `a trusted module imports only other trusted modules (use "trusted:…"), not './x.syn': a file module sees the program's names and would run with the tool's capabilities`. - Sus entradas de auditoría llevan
context: "trusted:tools"yfile: "trusted:tools", y sus líneas de log dedeclassifydicenat trusted:tools:<línea>.
Qué puede y qué no puede hacer el programa con él§
- Los argumentos son datos. Una task, una lambda o un builtin entre ellos — también adentro de una lista o de un
ok(…)— es un error, porque correría con las capacidades de la herramienta:trusted:tools.apply: a trusted tool takes data, not code — an argument holds a task, which would run with the tool's capabilities; pass the values it needs instead. - Nunca ve los nombres del programa. Un programa que define
task upper(x)antes o después deluseno cambia lo que corre elupper(…)de la herramienta. - No puede cambiar el módulo.
set t.config to …,set t.config["limit"] to …, hastaset t.reply to …o unset t.extra to …nuevo, cambian sólo la copia de los exports que tiene el programa; las tasks de la herramienta y otrouseven el original. (Un módulo de archivo es lo contrario: ahíset math.STATS[…]escribe la variable del módulo.) - No puede cargar uno por su cuenta. El nombre viene sólo del host, y el programa nunca ve la ruta —
print(t)muestra{reply: builtin:trusted:tools.reply, …}. Un nombre desconocido es un error de carga: `no trusted module named 'tools': the host loads it (synsema run --trusted tools=./file.syn, ortrusted: { tools: source }in wasm); a program cannot load one itself. El mismo archivo sin--trustedfalla igual, también bajosynsema test(como falla de<setup>`). - Sus tasks toman sólo argumentos posicionales.
t.reply(customer = c),call(t.reply, {…})ycall_tool(t.reply, {…})fallan contrusted:tools.reply() does not accept named arguments (got customer = …); pass it by position, ytool_spec(t.reply, …)se rechaza (tool_spec needs a task of the program, not the builtin 'trusted:tools.reply'). Para ofrecerle una herramienta de confianza a un modelo, envolvela en una task del programa y poné el envoltorio en la allow-list y en el catálogo:
use "trusted:tools" as t
task reply(customer: map, body: text)
give t.reply(customer, body)
let catalog be [tool_spec(reply, "Reply to a customer")]
Un call o call_tool cuyo mapa de argumentos tiene un valor privado da un resultado privado, aunque la herramienta haya desclasificado lo que devuelve; llamá al envoltorio directo (reply(c, body)) para quedarte con el resultado público.
Lo que todavía no hace§
Cada una de estas cosas es un error adentro de una herramienta de confianza, y el mensaje dice por qué: definir o lanzar un agente, parallel_map, cron_every/cron_after, un use de un módulo de archivo — todos correrían en otro intérprete al lado de los nombres del programa — y render, con sus include y layouts (el archivo de la plantilla vive donde el programa puede escribirlo, y correría con las capacidades de la herramienta: armá el texto en la herramienta). Por ejemplo: parallel_map: a trusted tool cannot fan out yet — its workers would run with the tool's capabilities next to the program's names; loop over the items in the tool.
Con módulos de confianza el programa entero corre en el tree-walker, sin la VM de bytecode. Medido: un loop de 3 millones de iteraciones, 0,07 s → 0,8 s.