~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-code-mode

0
0
main
201 lines · 10.1 KB Raw History

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:

NSIDinputoutput
com.fedfork.exec.mcp.messageone 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

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. 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:

optionwhat it is
ctxthe host's context: createPluginContext in a page, createNodeContext in Node. Its wasm engine runs the code.
callthe host's call: hostCall(...), policyCall(...), or the instance's call.
allowthe NSIDs the code may call, as a module's calls. The default is none.
budgetrunModule'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

whatlimitenforced by
importsnone: no host functions, console, Math.random (env.seed) or Daterefused after compile, and again by the host
memory1024 pages (64 MiB): memory.grow past it trapsthe host (capMemory in JS, StoreLimits in wasmtime), and --maximumMemory 1024
time5 s per event; a runaway loop is WASM_TIMEOUTthe host's worker termination (JS), or epochs (wasmtime)
runatlibs' budget: calls, events, wall time, in flightrunModule (budget option)
callsonly allow, through the host's call and its sessionrunModule (the module's calls)
filesthe prelude, the entry and the program; any other import is a compile errorcompile's readFile
source65536 characterscompile
compile time20 s, then the worker is terminatedcompile
compiler crashasc calls process.exit(1) on a stack overflow (deep generics); this ends only its workercompile
compile heap (Node)512 MBthe worker's resourceLimits
stdout65536 characters, kept across eventsthe 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

(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

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).