Native bridge runtime. Connects user machines, Firecracker sandboxes, and cloud VMs to the TODOforAI backend over an encrypted WebSocket channel.
The same binary runs everywhere. Location is a deployment detail; the protocol is uniform.
Bridges are islands by default. One guaranteed capability: "accept commands from the backend over WebSocket and execute them." Everything else — SSH, port exposure, overlay networking, language runtimes — is configured by the AI at the user's request by running commands through this same channel.
TCP → WebSocket → Noise_NX_25519_ChaChaPoly_BLAKE2b → JSON
No TLS. No OpenSSL. All crypto handled by Noise (monocypher + blake2b,
vendored in noise/).
After the Noise handshake, each binary WS frame carries one encrypted JSON message. First encrypted message from edge is auth:
{"type":"auth","deviceId":"dev_...","secret":"..."}Device credentials are provisioned via todoforai-bridge login (stored on disk by
the shared c-core login helper). Then the v2 multi-session protocol:
identity(→ server, once)exec/input/resize/signal/kill(← server)output/exit/error(→ server)
Server side: backend/src/api/ws/handlers/BridgeHandler.ts.
| File | Purpose |
|---|---|
main.c |
Event loop, session table, command dispatch |
subcmd.c / .h |
CLI subcommands: login / enroll / whoami |
conn.c / conn.h |
TCP + WS client handshake + Noise_NX initiator |
util.c / util.h |
Base64 + SHA-1 (for WS-Accept) |
pty.c / pty.h |
forkpty session: read/write/resize/signal |
identity.c / .h |
Host identity gathering (uname, pwd, cwd) |
tools.c / .h |
Probe installed CLI tools via scan_tools function-call |
| ws.c / ws.h | RFC 6455 WebSocket client (sync connect, poll loop) |
| json.c / json.h | Minimal JSON parser/writer + base64 |
| noise/ | Vendored noise.c + monocypher (BLAKE2b) |
Only libc + libutil (for forkpty, in libc on macOS).
# Dynamic build (default system cc) — ~77 KB stripped, libc only
make
./build/todoforai-bridge --help
# Static musl build via `zig cc` — ~90 KB, single-file, zero deps
make static
# Windows x64 build via `zig cc` (mingw-w64) — ~150 KB
make release-windows-x64# First time: provision device credentials (opens browser / device flow)
./build/todoforai-bridge login [--device-name NAME]
# Then connect — defaults to api.todofor.ai:80 (Noise is end-to-end;
# no TLS on the wire — typically Cloudflare/nginx terminates 443 in front)
./build/todoforai-bridge
# Custom server
./build/todoforai-bridge --host 127.0.0.1 --port 4000
# Local-dev: point at a backend on a different host/port
# (Noise TCP RPC port — defaults to api.todofor.ai:4100; for `bun run dev` use 14100)
./build/todoforai-bridge login --host 127.0.0.1 --port 14100
./build/todoforai-bridge enroll --host 127.0.0.1 --port 14100
# Firecracker sandbox: presence of enroll.token=... in /proc/cmdline
# routes to DeviceType.SANDBOX path (?deviceType=SANDBOX).
# Show version / help
./build/todoforai-bridge --version
./build/todoforai-bridge --helpThe backend's Noise static public key (the trust anchor for the encrypted
channel) is learned during login via the Noise_NX handshake and persisted
to credentials.json as backendPubkey. All later connections (daemon run,
enroll) pin against it. No flag, no env, no hardcoded default — same flow in
dev, prod, and self-hosted. If the server's identity changes (key rotation,
new deployment), login again to re-learn.
CLI flags take precedence; env vars are fallbacks for non-interactive deployments (sandbox init, systemd units, CI).
| Variable | Used by | Equivalent flag | Purpose |
|---|---|---|---|
NOISE_BACKEND_HOST |
run, login, enroll | --host |
Backend hostname (default api.todofor.ai) |
NOISE_BACKEND_PORT |
login, enroll | --port |
Noise TCP RPC port (default 4100, dev 14100) |
BRIDGE_PORT |
run | --port |
Bridge HTTP/WS port (default 80, dev 4000) |
The bridge has no HTTP/download logic of its own. Updates ride on the
existing RUN channel (see DeviceService.updateBridge in the backend):
the server sends a shell command that re-runs the sha256-verified
installer over the running binary (rename() over a running binary is
fine on POSIX) with --service — so a systemd/launchd supervisor is
ensured; the installer exits nonzero when none is available, stopping
the chain before the kill — then kills the bridge; the supervisor
relaunches it on the new binary.
Command shape (authoritative version lives in DeviceService.updateBridge) —
note $PPID (the bridge) is the reliable way to find the executable;
inside the RUN shell, $0 is the shell itself:
exe=$(readlink -f /proc/$PPID/exe 2>/dev/null || readlink -f "$(command -v todoforai-bridge)" 2>/dev/null || command -v todoforai-bridge) \
&& [ -x "$exe" ] \
&& i=$(mktemp) && curl -fsSL https://todofor.ai/bridge -o "$i" \
&& sh "$i" --prefix "$(dirname "$exe")" --service \
&& kill $PPIDNo new protocol messages, no in-binary HTTP client, no extra dependencies.
The TODOforAI agent discovers local skills on a bridge the same way it does on
an edge — by walking well-known locations and reading SKILL.md files. Both
.agents/skills and .claude/skills are scanned, per scope:
| Scope | Path | Source |
|---|---|---|
repo |
<workspace-root>/{.agents,.claude}/skills/** |
one per workspace root |
user |
$HOME/{.agents,.claude}/skills/** |
resolved on the bridge device |
Discovery rules (mirror edge/bun/src/skills.ts):
- Priority order: repo
.agents→ repo.claude→ user; skills are deduped byname, first match wins. - Walk depth:
find -maxdepth 6; hidden entries (.*) are pruned. - Only files literally named
SKILL.mdare accepted. - Only the first 8 KiB of each file is read for metadata.
- Frontmatter (YAML between leading
---lines) provides:name— skill identifier (falls back to parent directory name)description— full descriptionmetadata.short-description— optional shorter line preferred in the prompt
On a bridge there's no get_skills RPC: the agent runs find + head over
the same machine_exec channel used for shell tools, base64-encoding paths and
file heads so the wire stays ASCII-clean. The full body is fetched on demand via
the Skill tool, which reads the file via base64 < <path> like ReadTool.
Required tools on the bridge: sh, find, head, base64. Standard on
GNU/BSD/macOS userspace; no extra installation needed.
- POSIX + Windows (ConPTY, Win10 1809+). Windows build via
make release-windows-x64(zig cc + mingw-w64). On Windows the bridge spawns a POSIX shell inside ConPTY — setBRIDGE_SHELLto override, otherwise it prefers Git for Windows (bash.exeorsh.exe, the same msys2 binary, so an install whosebash.exewas renamed still works), then a PATHbash.exe/sh.exe(skipping the System32 WSL launcher, which isn't a usable POSIX shell here), then a pinned busybox-w32 it provisions itself into%USERPROFILE%\.todoforai\shell\sh.exe, falling back tocmd.exe(RUN/tool catalog assume bash semantics — install Git for Windows).step_pausedworks on Linux, macOS, and Windows; on Windows thepasswordPromptflag is always 0 (the child's ECHO state isn't exposed through the ConPTY API). - Session cap defaults to 256 concurrent PTYs. When full, RUNs evict the least-recently-used idle session (running sessions are never evicted).
- WebSocket uses plain
ws://— TLS is replaced by Noise end-to-end. Typically deployed behind nginx/Cloudflare which terminates external TLS on 443 and forwards plain WS to the backend; the Noise channel runs through it unchanged.