Skip to content

Latest commit

 

History

227 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TODOforAI Bridge

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.

Design principle

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.

Wire protocol

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.

Layout

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

Build

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

Run

# 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 --help

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

Environment variables

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)

Updates

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 $PPID

No new protocol messages, no in-binary HTTP client, no extra dependencies.

Skills (SKILL.md)

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 by name, first match wins.
  • Walk depth: find -maxdepth 6; hidden entries (.*) are pruned.
  • Only files literally named SKILL.md are 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 description
    • metadata.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.

Notes

  • 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 — set BRIDGE_SHELL to override, otherwise it prefers Git for Windows (bash.exe or sh.exe, the same msys2 binary, so an install whose bash.exe was renamed still works), then a PATH bash.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 to cmd.exe (RUN/tool catalog assume bash semantics — install Git for Windows). step_paused works on Linux, macOS, and Windows; on Windows the passwordPrompt flag 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.

About

TODOforAI Bridge — native C edge agent (PTY relay over Noise-encrypted WebSocket)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages