# atlibs-code-mode Code mode for agents on [atlibs](https://knotless.dev/johnandersen777.bsky.social/atlibs) hosts (knotless://johnandersen777.bsky.social/atlibs). It is an MCP server with one tool, `run_assemblyscript`. The agent writes AssemblyScript, which code mode compiles with `asc` 0.28.20 in a worker, in a page or in Node. The result runs as an atlibs module on the host's own engine: - no imports - 64 MiB of memory - 5 s per event - the run's budget Its calls go through the host's call and are held to an allowlist. The agent uses it to call the host's other MCP servers and filter what their tools return, not to make network calls of its own. This is the approach socialweb-computer-emdash's studio uses (`asc --runtime stub --use abort=`, an `abi.ts` of JSON helpers), turned into a tool. The server takes one MCP JSON-RPC 2.0 message (spec 2025-11-25, with 2025-06-18 and 2025-03-26 also accepted) and returns its response. Hosts reach it through one NSID: | NSID | input | output | |---|---|---| | `com.fedfork.exec.mcp.message` | one JSON-RPC message (`initialize`, `ping`, `tools/list`, `tools/call`, notifications) | the JSON-RPC response; `null` for a notification | Unknown methods return `-32601` and unknown tools return `-32602`. A failed run is a `tools/call` result with `isError: true`. The schema is in `lexicons/com/fedfork/exec/mcp/message.json`. ## Layout - `js/index.js`: `codeMode({ ctx, call, allow, budget })` returns `{ message, handler, calls, provides }`. - `compile(code)` turns source into wasm. - `streamableHttp(message)` serves the same messages over Streamable HTTP, as a web `Request -> Response`. - `js/compile-worker.js`: the compiler, one worker per compile. - `js/abi.js`: the prelude every program compiles against (`Json`, `Run`, `print`). - `js/cli.mjs`: Node only. - It exports `transport` for atlibs' Node CLI. - Run on its own, it is an exec handler for the Rust CLI. - `examples/`: a composition in which an agent runs code through code mode. - `modules/stub-agent.ts` is a stand-in for the agent loop, compiled by code mode itself. - `compose.ts` writes `composition.json` (`node examples/compose.ts | diff - examples/composition.json`). - `agent.mjs` runs it. - `tests/`: compile and run, MCP (including the official SDK client), sandbox escapes, and headless Chrome. - `lexicons/`: the schema of `com.fedfork.exec.mcp.message`. ## Set up ```bash git clone https://knotless.dev/johnandersen777.bsky.social/atlibs ../atlibs && git -C ../atlibs checkout -q 88b2ba8 # the tests' browser helper and Rust CLI npm ci # assemblyscript, and @publicdomainrelay/atlibs for Node ``` Or use it from npm (Node >= 25; `@publicdomainrelay/atlibs` is a peer dependency): ```bash npm install @publicdomainrelay/atlibs-code-mode @publicdomainrelay/atlibs ``` ## What the agent writes ```ts export function main(run: Run): void { if (run.start) { print("hello " + run.input.get("who").str); // one tools/call to the host's marque MCP server (its hook NSID), as a JSON-RPC message const call = Json.object().set("name", Json.string("list_domains")).set("arguments", Json.object()); run.call("d", "com.fedfork.exec.mcp.marque", Json.object().set("jsonrpc", Json.string("2.0")).set("id", Json.number(1)).set("method", Json.string("tools/call")).set("params", call)); return; } // a call's result is the next event: run.id, run.ok, run.output (here the JSON-RPC response), run.error run.finish(run.output.get("result").get("structuredContent").get("domains").at(0).get("domain")); } ``` `tools/call { name: "run_assemblyscript", arguments: { code, input } }` returns one text block holding `{ stdout, result, error? }`. - **Events.** `main` runs once per event: the start, then each call's result. - **State.** `run.state` (Json) carries across events, and so does stdout. - **Prelude.** `Json`, `Kind`, `Run` and `print` are in scope. `Json` reads with `get(key)`, `has(key)`, `at(i)`, `.length` (array items, object keys, string characters), `.keys`, `.items`, `.str`, `.num`, `.bool` and `.isNull`/`.isArray`/`.isObject`/...; it builds with `Json.object().set(k, v)`, `push(v)` and `Json.array([...])`. A program that imports from `"./abi"` itself skips the automatic import. - **Description.** The tool's description in `tools/list` documents this API for the model, and names the NSIDs it may call. ## Plug it into a host `codeMode` takes the host's own pieces: | option | what it is | |---|---| | `ctx` | the host's context: `createPluginContext` in a page, `createNodeContext` in Node. Its wasm engine runs the code. | | `call` | the host's call: `hostCall(...)`, `policyCall(...)`, or the instance's call. | | `allow` | the NSIDs the code may call, as a module's `calls`. The default is none. | | `budget` | `runModule`'s budget for each run. | **A composition (browser or Node).** Route the agent's hook to a handler: ```js import { codeMode } from "@publicdomainrelay/atlibs-code-mode"; import { hostCall } from "@publicdomainrelay/atlibs/run-module/host-call.js"; // the code calls the host's MCP servers (here marque's, a message(jsonrpc) function), not raw XRPC const mcp = codeMode({ ctx, call: hostCall({ ctx, calls: { "com.fedfork.exec.mcp.marque": ({ input }) => marque.message(input) } }), allow: ["com.fedfork.exec.mcp.marque"] }); await runCompose(ctx, composition, "agent", "run", input, { handlers: { mcp: mcp.handler } }); ``` The edge is `c.route(agent, "com.fedfork.exec.mcp.message", c.handler("mcp"), …)` (see `examples/compose.ts`). **A host call (runModule, no composition).** A module that lists the NSID in its `calls`, without a hook, reaches the server through the host's call: ```js hostCall({ ctx, xrpc, calls: { ...transports, ...mcp.calls } }) ``` **A page.** Workers do not see the page's import map. Map `asc` and its dependencies in the page's import map, and atlibs beside them; `compile` resolves them there and hands the URLs to its worker (see `tests/browser.test.mjs`): ```html ``` The compiler and Binaryen are about 14 MB, fetched on the first compile. **atlibs' Node CLI.** Load it as a transport. Its code may call nothing: ```bash ATLIBS_TRANSPORTS=../atlibs-code-mode/js/cli.mjs node ../atlibs/javascript/src/server/cli.mjs run '' ``` **The Rust host (`atlibs` CLI).** The compiler is JavaScript, so the Rust host reaches code mode as an exec handler. Node compiles and runs the code, with no calls. The agent module itself runs on wasmtime. ```bash node examples/agent.mjs # builds examples/modules/stub-agent.wasm atlibs run examples/composition.json agent run '{"input":{"code":"export function main(run: Run): void { run.finish(Json.number(7)); }"}}' \ --handler "mcp=exec:$(which node) $PWD/js/cli.mjs" --allow-exec ``` A Rust host can already run precompiled wasm (a program `compile` produced) as an ordinary module. No in-process Rust crate is provided. ## The sandbox | what | limit | enforced by | |---|---|---| | imports | none: no host functions, `console`, `Math.random` (`env.seed`) or `Date` | refused after compile, and again by the host | | memory | 1024 pages (64 MiB): `memory.grow` past it traps | the host (`capMemory` in JS, `StoreLimits` in wasmtime), and `--maximumMemory 1024` | | time | 5 s per event; a runaway loop is `WASM_TIMEOUT` | the host's worker termination (JS), or epochs (wasmtime) | | run | atlibs' budget: calls, events, wall time, in flight | `runModule` (`budget` option) | | calls | only `allow`, through the host's `call` and its session | `runModule` (the module's `calls`) | | files | the prelude, the entry and the program; any other import is a compile error | `compile`'s `readFile` | | source | 65536 characters | `compile` | | compile time | 20 s, then the worker is terminated | `compile` | | compiler crash | `asc` calls `process.exit(1)` on a stack overflow (deep generics); this ends only its worker | `compile` | | compile heap (Node) | 512 MB | the worker's `resourceLimits` | | stdout | 65536 characters, kept across events | the prelude | - **Stub runtime.** The code runs with no garbage collector: memory is never freed during an event, and each event starts on a fresh instance. - **Throw.** `throw`, and any trap, ends the run with `WASM_TRAP: unreachable`. The message is lost (`--use abort=`). Stdout from earlier events survives in `result`. ## Test ```bash (cd ../atlibs && cargo host) # optional: the atlibs CLI for the Rust exec-handler test node --test tests/*.test.mjs examples/*.test.mjs ``` - **Without Chrome.** `tests/browser.test.mjs` skips without Chrome (set `CHROME`). - **Without `atlibs`.** The Rust test in `examples/agent.test.mjs` skips without the `atlibs` CLI on `PATH`. - **Network.** None. ## Run the example ```bash node examples/agent.mjs # the default program reads atproto.com's repo publicly node examples/agent.mjs 'export function main(run: Run): void { print("hi"); }' ``` ## Open - [ ] The atlibs-asyncai agent loop in place of the stub agent: `examples/composition.json` with its module as `agent`, once that repo has one. - [ ] Required hooks served by the host: today `hostsHook` counts only `com.fedfork.exec.*.connect`. A module whose `com.fedfork.exec.mcp.message` hook is `required` therefore needs a composition edge, even on a host whose `calls` provide it. Proposed atlibs patch: count any `com.fedfork.exec.*` NSID the module lists in `calls` (JS `hostsHook`/`unservedHook`, Rust `hosts_hook`/`unserved_hook`). - [ ] The Rust exec handler gives the code no calls. In-process Rust code mode with the host's call would need a crate on `atlibs-host`'s transport seam running precompiled wasm. - [ ] A warm compile worker. Today each compile spawns a fresh one, and asc loads in about 0.3 s. - [ ] Serve `streamableHttp` behind the fedproxy ingress forward (`com.fedfork.exec.ingress.request`).