atlibs-code-mode
Code mode for agents on 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 webRequest -> 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
transportfor atlibs' Node CLI. - Run on its own, it is an exec handler for the Rust CLI.
- It exports
examples/: a composition in which an agent runs code through code mode.modules/stub-agent.tsis a stand-in for the agent loop, compiled by code mode itself.compose.tswritescomposition.json(node examples/compose.ts | diff - examples/composition.json).agent.mjsruns it.
tests/: compile and run, MCP (including the official SDK client), sandbox escapes, and headless Chrome.lexicons/: the schema ofcom.fedfork.exec.mcp.message.
Set up
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):
npm install @publicdomainrelay/atlibs-code-mode @publicdomainrelay/atlibs
What the agent writes
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.
mainruns once per event: the start, then each call's result. - State.
run.state(Json) carries across events, and so does stdout. - Prelude.
Json,Kind,Runandprintare in scope.Jsonreads withget(key),has(key),at(i),.length(array items, object keys, string characters),.keys,.items,.str,.num,.booland.isNull/.isArray/.isObject/...; it builds withJson.object().set(k, v),push(v)andJson.array([...]). A program that imports from"./abi"itself skips the automatic import. - Description. The tool's description in
tools/listdocuments 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:
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:
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):
<script type="importmap">{ "imports": {
"assemblyscript/asc": "/node_modules/assemblyscript/dist/asc.js",
"assemblyscript": "/node_modules/assemblyscript/dist/assemblyscript.js",
"binaryen": "/node_modules/binaryen/index.js",
"long": "/node_modules/long/index.js",
"@publicdomainrelay/atlibs/": "/node_modules/@publicdomainrelay/atlibs/src/" } }</script>
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:
ATLIBS_TRANSPORTS=../atlibs-code-mode/js/cli.mjs node ../atlibs/javascript/src/server/cli.mjs run <wasm> <module.json> <entry> '<json>'
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.
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 withWASM_TRAP: unreachable. The message is lost (--use abort=). Stdout from earlier events survives inresult.
Test
(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.mjsskips without Chrome (setCHROME). - Without
atlibs. The Rust test inexamples/agent.test.mjsskips without theatlibsCLI onPATH. - Network. None.
Run the example
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.jsonwith its module asagent, once that repo has one. -
Required hooks served by the host: today
hostsHookcounts onlycom.fedfork.exec.*.connect. A module whosecom.fedfork.exec.mcp.messagehook isrequiredtherefore needs a composition edge, even on a host whosecallsprovide it. Proposed atlibs patch: count anycom.fedfork.exec.*NSID the module lists incalls(JShostsHook/unservedHook, Rusthosts_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
streamableHttpbehind the fedproxy ingress forward (com.fedfork.exec.ingress.request).