@synsema/wasm 0.6.45 → 0.6.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,7 @@ const r = await syn.runAsync(code, {
34
34
  host,
35
35
  labels: true, // information-flow labels on
36
36
  input: { var: "customer", value: row, sources: { card: ["customer"] } },
37
+ result: "out", // r.result = the final value of `out`
37
38
  timeoutMs: 2000, // the Worker is stopped past this
38
39
  maxSteps: 5_000_000, // interpreter steps, deterministic
39
40
  maxMemoryMb: 64, // memory the run reserves
@@ -44,19 +45,30 @@ const r = await syn.runAsync(code, {
44
45
  private value, or a branch that depended on one, never reaches `print`, your `http` hook
45
46
  (`fetch`/`http_*`), your `kv` (`state_*`, `remember`), your `llm` or your `sleep` (its number
46
47
  crosses to the host): the call fails with `label_violation` before the hook runs.
47
- `r.private_seen` says whether one went through.
48
+ `r.private_seen` says whether one went through; then `r.steps` is `null` (how many steps the
49
+ run took depended on the data). `sources` needs `labels: true`.
50
+ - `result: "out"` returns the final value of the global `out` in `r.result`, with its labels: a
51
+ private part comes back as `{"$private": ["customer"], "value": …}`. `r.declassify` lists each
52
+ `declassify` the run made. Without `labels`, `result` is plain JSON.
48
53
  - `timeoutMs` (async mode): `0` means no deadline; otherwise milliseconds, up to 2147483647,
49
- counted from when the call starts running, not while it waits in the queue. Past it the call fails with a clear error and its Worker is replaced. The server's own
50
- thread never blocks. A host hook with no deadline of its own that never answers keeps its Worker
51
- busy unless the call has a `timeoutMs`.
54
+ counted from when the call starts running, not while it waits in the queue. Past it the call
55
+ fails with a clear error and its Worker is replaced. The server's own thread never blocks. A
56
+ host hook already running (a `fetch`) is not cut; its result is discarded. A host hook with no
57
+ deadline of its own that never answers keeps its Worker busy unless the call has a `timeoutMs`.
52
58
  - `maxSteps` and `maxMemoryMb` (both modes): the run ends with `ok: false` and an error that says
53
59
  which limit it hit — never a trap. `maxSteps` counts interpreter steps from the start of the run
54
60
  (in `handle`, of each request); `maxMemoryMb` counts the memory the run reserves, not what the
55
- instance already held.
61
+ instance already held. Both are positive integers: `0` is an error, not "no limit". No call
62
+ inherits the limits of the previous one.
56
63
  - Concurrent async calls wait in one queue and go to the first free Worker (up to `workers: n`). A
57
64
  Worker that does not start in `startMs` (an option of `Synsema.load`/`loadNode`, default 30 s) is
58
- stopped. In the sync API a trap
59
- recreates the instance by itself.
65
+ stopped. A Worker that dies fails the call it was running (not retried: it may have had
66
+ effects) and is replaced. `close()` is final: what waits fails with "closed".
67
+ - A host hook that returns something that is not JSON (a `BigInt`, a circular object) or throws a
68
+ value without a message counts as that hook failing in that call, in both APIs — not a hung
69
+ Worker or a trapped module.
70
+ - In the sync API a trap recreates the instance by itself (the call throws
71
+ `synsema: the module trapped (…); the instance was recreated`; the next call works).
60
72
 
61
73
  ## Lending capabilities (the host)
62
74
 
@@ -77,7 +89,25 @@ syn.run(`require llm\nprint(reason about "the weather")`, { host, ceiling: "stdo
77
89
  ```
78
90
 
79
91
  - `kv` backs persistent agent memory (`remember`/`recall`, rules, progress — namespace
80
- `memory:<declared name>`) and `state_*` (namespace `state`).
92
+ `memory:<declared name>`) and `state_*` (namespace `state`). `delete` and `list` are optional.
93
+ - `kv.cas(ns, key, expected, value)` (optional) is compare-and-swap: write `value` only if the
94
+ stored text is exactly `expected` (`null` = the key does not exist) and return `true` or
95
+ `false` — anything else fails the call with `kv.cas must return true or false`. Your store must
96
+ compare and write in one step (a transaction, a conditional put):
97
+
98
+ ```js
99
+ cas: (ns, k, expected, v) => {
100
+ if ((store.get(ns + "/" + k) ?? null) !== expected) return false;
101
+ store.set(ns + "/" + k, v);
102
+ return true;
103
+ },
104
+ ```
105
+
106
+ `state_merge(key, map)` and `state_cas(key, expected, new)` depend on it. With a host `kv`
107
+ that has `cas` they are atomic, also across instances sharing the store. With a host `kv`
108
+ without `cas` they fail saying so (`the host's kv does not offer cas (kv.cas) …`) instead of
109
+ risking a lost write. With no host `kv`, the state belongs to the wasm instance and is atomic:
110
+ it lasts across that instance's `handle` requests, and each `run` starts with an empty state.
81
111
  - `llm` backs `reason`/`decide`/`analyze`/`generate`; `llm_usage()` sums the tokens you report.
82
112
  - `http` backs `fetch`/`http_*` and the blockchain read-side RPC — after the `net(host)`
83
113
  gate, with the same URL canonicalization as the native binary.
@@ -120,8 +150,12 @@ validates without running; `syn.version()` is the artifact's release tag.
120
150
  ## Files, time, randomness
121
151
 
122
152
  This artifact has **no filesystem** (`read_file` says so; pass data through `env` or the
123
- source). `now()` comes from `Date.now()`; `random()`, `token()` and every signature nonce
124
- come from `crypto.getRandomValues`. For file-based confidential jobs use the wasip1 build
153
+ source). `now()` comes from `Date.now()`. Each call asks the host once for 32 bytes of
154
+ entropy (`crypto.getRandomValues`), which seed that run's own ChaCha generator: `random()`,
155
+ `random_bytes(n)`, `token()` and RSA blinding draw from it and never ask the host again, so how
156
+ much randomness a run used does not show. With `labels`, `now()` returns the clock read at the
157
+ start of the run and at each interaction with the host (a hook call), so when and how often the
158
+ program read the clock does not show either. For file-based confidential jobs use the wasip1 build
125
159
  under wasmtime (see the Synsema docs, *WebAssembly*).
126
160
 
127
161
  ## Other languages
package/index.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface HttpRequestPayload {
11
11
  body_base64?: string;
12
12
  /** Segundos. */
13
13
  timeout: number;
14
+ /**
15
+ * v0.6.46: `"error"` cuando la request lleva un secret (en la URL, la query, el body o un header:
16
+ * `fetch` sólo quita `Authorization` al cambiar de host). Pasalo a `fetch` (`{redirect: req.redirect}`): un redirect no puede llevar la clave
17
+ * a otro host. Un hook que lo ignore queda fuera de lo que el motor puede imponer.
18
+ */
19
+ redirect?: "error";
14
20
  }
15
21
 
16
22
  export interface HttpResponsePayload {
@@ -48,9 +54,26 @@ export interface LlmResult {
48
54
  * En la API síncrona los hooks deben ser síncronos; en `*Async` pueden devolver Promise.
49
55
  */
50
56
  export interface Host {
57
+ /**
58
+ * v0.6.46: en nativo, un `require net` con comodín sólo llega a IPs públicas (el motor resuelve
59
+ * y chequea la IP antes de conectar). Acá la conexión es tuya y el motor no ve la IP: si el
60
+ * programa no es tuyo, resolvé el nombre y chequeá la IP a la que vas a conectar (un nombre como
61
+ * `127.0.0.1.nip.io` resuelve al loopback), y conectá a ésa. Negá loopback (`127.0.0.0/8`,
62
+ * `::1`), las redes privadas (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, CGNAT
63
+ * `100.64.0.0/10`, ULA `fc00::/7`), link-local (`169.254.0.0/16` con la metadata de la nube
64
+ * `169.254.169.254`, `fe80::/10`), multicast y los rangos de documentación; una IPv4 dentro de una
65
+ * IPv6 (`::ffff:a.b.c.d`) cuenta como esa IPv4. El hook no sabe si el programa nombró el host
66
+ * (`require net("db")`): si tus programas necesitan llegar a una red privada, nombrá vos esos
67
+ * destinos en el hook.
68
+ */
51
69
  http?(req: HttpRequestPayload): HttpResponsePayload | Promise<HttpResponsePayload>;
52
70
  kv?: KvStore;
53
- llm?(op: string, prompt: string): string | LlmResult | Promise<string | LlmResult>;
71
+ /**
72
+ * `info.principals`: los dueños de lo privado que lleva el prompt (vacío con datos públicos).
73
+ * Sólo llega al hook si el techo lo permite (`llm=<principal>@embedder`). Un hook que lanza hace
74
+ * fallar esa llamada con su mensaje.
75
+ */
76
+ llm?(op: string, prompt: string, info: { principals: string[] }): string | LlmResult | Promise<string | LlmResult>;
54
77
  log?(line: string): void | Promise<void>;
55
78
  /** Una función, o `true` para la pausa por defecto (Atomics.wait / setTimeout). */
56
79
  sleep?: ((secs: number) => void | Promise<void>) | true;
@@ -99,6 +122,17 @@ export interface RunOptions {
99
122
  * Igual que `maxSteps`. Un entero positivo.
100
123
  */
101
124
  maxMemoryMb?: number;
125
+ /**
126
+ * v0.6.46 — módulos de confianza: `{ nombre: fuente }`. El programa los importa con
127
+ * `use "trusted:nombre" as t` y sólo puede llamar a sus tasks exportadas, que corren con SUS
128
+ * `require` (acotados por `trustedCeiling`) y nada de lo del programa. Es la forma de dejar que
129
+ * código escrito por un modelo, bajo un `ceiling` sin `declassify`, publique sólo lo que la
130
+ * política de una persona publica. Los argumentos de una task de confianza son datos (una task
131
+ * o una lambda entre ellos es error).
132
+ */
133
+ trusted?: Record<string, string>;
134
+ /** El techo de los módulos de confianza, con la gramática de `ceiling` (`"stdout,declassify=clientes"`). */
135
+ trustedCeiling?: string;
102
136
  }
103
137
 
104
138
  export interface AuditEntry {
@@ -119,6 +153,8 @@ export interface RunResult {
119
153
  llm_tokens: number;
120
154
  /** v0.6.42 — el código de salida: el de `exit(code)` si el programa lo pidió, si no 0 (ok) o 1. */
121
155
  exit: number;
156
+ /** Los pasos del intérprete que llevó la corrida; `null` con `labels` si tocó un valor o una rama privada. */
157
+ steps: number | null;
122
158
  /** Con `result`: el valor final de esa variable (las partes privadas como `{"$private": [...]}`). */
123
159
  result?: unknown;
124
160
  /** Con `labels`: cada `declassify` que corrió (quién, qué principal, dónde). */
package/index.mjs CHANGED
@@ -14,6 +14,11 @@
14
14
  // En el navegador requiere cross-origin isolation (COOP/COEP) para SharedArrayBuffer.
15
15
  //
16
16
  // No hay estado global: cada `load()` es una instancia (memoria lineal propia).
17
+ //
18
+ // v0.6.46 (E7): la conexión del `http` del host es del embebedor, y el motor no ve a qué IP va. En
19
+ // nativo un `net` con comodín sólo llega a IPs públicas; acá eso lo tiene que hacer tu hook:
20
+ // resolver el nombre, chequear la IP a la que conecta (los rangos están en `Host.http`, en
21
+ // index.d.ts) y conectar a ésa. El hook no sabe si el programa nombró el host.
17
22
 
18
23
  const te = new TextEncoder();
19
24
  const td = new TextDecoder();
@@ -122,7 +127,7 @@ export function dispatchHostSync(host, kind, payload) {
122
127
  ? casReply(syncOnly("kv.cas", host.kv.cas(payload.ns, payload.key, payload.expected ?? null, payload.value)))
123
128
  : undefined;
124
129
  case "llm":
125
- return typeof host.llm === "function" ? normalizeLlm(syncOnly("llm", host.llm(payload.op, payload.prompt))) : undefined;
130
+ return typeof host.llm === "function" ? normalizeLlm(syncOnly("llm", host.llm(payload.op, payload.prompt, { principals: payload.principals ?? [] }))) : undefined;
126
131
  case "log":
127
132
  if (typeof host.log !== "function") return undefined;
128
133
  syncOnly("log", host.log(payload.line));
@@ -165,7 +170,7 @@ export async function dispatchHostAsync(host, kind, payload) {
165
170
  ? casReply(await host.kv.cas(payload.ns, payload.key, payload.expected ?? null, payload.value))
166
171
  : undefined;
167
172
  case "llm":
168
- return typeof host.llm === "function" ? normalizeLlm(await host.llm(payload.op, payload.prompt)) : undefined;
173
+ return typeof host.llm === "function" ? normalizeLlm(await host.llm(payload.op, payload.prompt, { principals: payload.principals ?? [] })) : undefined;
169
174
  case "log":
170
175
  if (typeof host.log !== "function") return undefined;
171
176
  await host.log(payload.line);
@@ -344,7 +349,15 @@ function limitResult(request) {
344
349
  };
345
350
  }
346
351
  if (request.op === "test") return { ok: false, passed: 0, failed: 0, lines: [MEMORY_LIMIT], audit: [] };
347
- return { ok: false, output: [], errors: [MEMORY_LIMIT], audit: [], llm_tokens: 0, exit: 1 };
352
+ const r = { ok: false, output: [], errors: [MEMORY_LIMIT], audit: [], llm_tokens: 0, exit: 1 };
353
+ // v0.6.46 (E6): con etiquetas, que la corrida se haya cortado por memoria puede depender de un
354
+ // valor privado (un bit por corrida, como cualquier error suyo): el host lo trata como sensible,
355
+ // igual que una corrida que terminó con `private_seen`. Sin saber si tocó algo privado, sí.
356
+ if (request.labels) {
357
+ r.steps = null;
358
+ r.private_seen = true;
359
+ }
360
+ return r;
348
361
  }
349
362
 
350
363
  /**
@@ -370,6 +383,9 @@ function requestOf(op, source, opts = {}) {
370
383
  // se descartaba por falsy y la corrida quedaba sin límite.
371
384
  if (opts.maxSteps != null) r.max_steps = opts.maxSteps;
372
385
  if (opts.maxMemoryMb != null) r.max_memory_mb = opts.maxMemoryMb;
386
+ // v0.6.46 (E3b): módulos de confianza del embebedor y su techo.
387
+ if (opts.trusted) r.trusted = opts.trusted;
388
+ if (opts.trustedCeiling) r.trusted_ceiling = opts.trustedCeiling;
373
389
  return r;
374
390
  }
375
391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synsema/wasm",
3
- "version": "0.6.45",
3
+ "version": "0.6.46",
4
4
  "description": "Synsema — the programming language for AI agents — embedded as WebAssembly: run .syn programs inside browsers, Node/Bun/Deno and edge runtimes, lending them http/kv/llm from your app",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/synsema.wasm CHANGED
Binary file