Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fx on Cloudflare Workers

Runs fx inside a Cloudflare Worker, using libfx's WebAssembly build with just-bash as an in-memory filesystem behind the agent's shell tool.

A Durable Object per session holds a live wasm instance and its filesystem, so the terminal is genuinely interactive across turns rather than one-shot. Raw TUI bytes stream over a WebSocket and xterm.js renders them in the browser.

Design notes

JSPI needs no flag here. libfx's wasm hard-requires JavaScript Promise Integration — fx-sdk.js throws unless WebAssembly.Suspending and WebAssembly.promising exist. workerd exposes both by default, so the agent instantiates directly in the isolate with no child process and no runtime flag. Verified on deployed Workers, not just locally.

No server-side terminal emulator. Because output streams to the browser as raw bytes, nothing here needs to emulate a terminal, type prompts as synthetic keystrokes, or detect turn completion by watching for the screen to stop changing. xterm.js draws whatever fx paints, and keystrokes stream back the other way.

The native addon is irrelevant. libfx.*.node can't run on Workers, so this is wasm throughout.

How it works

  • src/index.js — serves the client at /, upgrades /session to a WebSocket routed to one Durable Object per session id.
  • src/session.js — the FxSession Durable Object. Holds a live fx wasm instance and a just-bash filesystem for as long as the socket is open. Every command the agent runs is echoed to the client from the workspace exec adapter, so the footer reflects commands that actually executed, not what the model claims it ran.
  • src/client.js — xterm.js over the raw byte stream.
  • src/instructions.js — an AGENTS.md seeded into the workspace (see Limitations).

wrangler.jsonc declares {"type":"CompiledWasm"}, so import termWasm from "./fx-term.wasm" yields an already-compiled WebAssembly.Module at isolate start — the compile never bills against the 400 ms startup-CPU budget.

Why the terminal surface, not the headless agent

libfx exposes a clean headless createFxAgent() (ACP/JSON-RPC), which would have given structured events instead of a byte stream. It sends no tools. Verified by intercepting the Gateway request through libfx's fetch adapter:

Surface tools in the request to /v3/ai/language-model
createFxAgent(), libfx 0.0.4 []
createFxAgent(), libfx 0.0.5-dev.285 []
createFxAgent(), libfx 0.0.5 (stable) []
createFxTerminal() [terminal]

The workspace adapter is wired on both, but on the ACP path the model is never told the tool exists, so it can talk and never execute. Hence the terminal surface.

Wire protocol

One WebSocket, two channels.

  • Binary frames — raw fx TUI output, straight into xterm.js.
  • Text frames{type:"status"} · {type:"ready",bootMs,cols,rows} · {type:"command",command,exitCode} · {type:"error",message,fatal?}

Client → server: {type:"input",data} (keystrokes) · {type:"prompt",text} (types a line and presses return) · {type:"interrupt"} (sends \x03)

Run it

npm install                     # postinstall copies fx-term.wasm into src/
cp .dev.vars.example .dev.vars  # then add your key
npm run dev

Deploy:

npx wrangler secret put AI_GATEWAY_API_KEY
npm run deploy

AI_GATEWAY_API_KEY is an AI Gateway API key — the credential fx's wasm expects. fx sends it as a bearer token to ai-gateway.vercel.sh, so it must be a key valid for that gateway.

Measured

Local wrangler dev (workerd, wrangler 4.125.0, compat date 2026-08-01):

WebAssembly.Suspending / promising function / function — no flag
Boot to interactive TUI 36 ms warm
Full turn, 2 shell commands + poem 10 s (↑20 ↓259)
TUI stream 1245 bytes over 21 binary frames for boot + one prompt
just-bash in-isolate echo hi > a.txt && cat a.txt → exit 0
Bundle 6923 KiB raw / 2203 KiB gzip — under the 3 MB free-tier cap, but not by much
Default model zai/glm-5.2
libfx 0.0.5 (fx v0.0.5)

The TUI renders identically to the native CLI: 𝒇x v0.0.5 · Run /help for commands, auto · glm-5.2.

Verified turn — "create a file saying this is a test and then read it and then write a poem":

● 2 tool calls · 2 commands
├ Ran echo "this is a test" > ./test.txt
└ Ran cat ./test.txt

File created and confirmed — test.txt contains this is a test.
And here's your poem: …

10s (↑20 ↓259)

Both commands really executed inside just-bash (exit 0, surfaced as command frames).

Known limitations

  • Project instructions are not discovered. The wasm build implements path_open, fd_readdir and fd_prestat_dir_name as unavailable — fx cannot open a file — and it does not shell out for AGENTS.md either (verified). src/instructions.js seeds one into the vfs so the agent can cat it when asked, but fx logs project instructions action=omitted every turn.
  • No hibernation. The DO uses server.accept() rather than the WebSocket Hibernation API, because a hibernating DO is evicted from memory and neither a JSPI-suspended wasm stack nor the just-bash vfs survives serializeAttachment(). An idle session holds a DO resident.
  • Ephemeral filesystem. The vfs dies with the socket. Prompt history does not: a promptHistoryStore backed by Durable Object storage keeps it across reconnects, and the client pins its session id in localStorage so a reload reattaches to the same DO.
  • !command direct terminal does not work — the TUI reports Direct terminal was not started: unsupported host. That string lives only in the wasm and has no corresponding host import; a direct terminal needs a real PTY, which libfx's README lists among the things the WebAssembly runtime never provides. Use ordinary prompts instead — the agent's own terminal tool works fine. Only the container path would change this.
  • No network or git in the shell. just-bash is pure JS; curl, npm install and git fail.
  • Permissions auto-approved. workspace.permission is allow-sandboxed. Safe only because just-bash is a sandbox with no host or network access.
  • Measurements are from local wrangler dev. JSPI, the TUI and full turns are all verified on deployed Workers too, but the timings above are local.
  • xterm.js loads from jsDelivr, so the client needs network access to that CDN.

Credits

The idea this builds on — running fx's WebAssembly build headlessly with just-bash standing in as an in-memory workspace for the agent's shell tool — is from nicolasmontone/fx-inside-function by @montonenico, which does it inside a Vercel Function. That project also worked out that the terminal surface is the one that actually exposes a shell tool, which saved a lot of time here.

About

Run the fx coding agent inside a Cloudflare Worker — libfx WebAssembly + just-bash, one Durable Object per session, TUI streamed over WebSocket to xterm.js

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages