bridge
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 10 GitHub stars
Code Fail
- rm -rf — Recursive force deletion command in install.sh
- network request — Outbound network request in install.sh
- rm -rf — Recursive force deletion command in test/pty_teardown_probe.sh
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
TODOforAI Bridge — native C edge agent (PTY relay over Noise-encrypted WebSocket)
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 |
policy.c / .h |
Device policy: workspace confinement + Landlock jail |
| 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 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.
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 — 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.
Device policy (optional)
Confines the backend to listed workspaces on this machine. Local-only file,
never written by the wire. No file ⇒ full user access (today's behaviour).
todoforai-bridge policy init # allow cwd
todoforai-bridge policy add ~/repo/x
todoforai-bridge policy list # restart bridge to apply
~/.config/todoforai/policy.json (or /etc/todoforai/policy.json, wins):
{ "jail": true, "workspaces": ["/home/me/repo/x"] }
read_file/write_file/ RUNcwdmust be inside a workspace
(openat2 RESOLVE_BENEATH— symlink swaps can't escape).- Linux 6.2+: RUN shells and tool probes are Landlock-jailed.
$HOMEis
dark except workspaces, toolchain/cache dirs (RW) and shell rc files (RO)."jail": falsedisables (needed on older kernels). - macOS / Windows: path checks only; the shell is not confined.
- Fail closed: bad policy file ⇒ all paths denied, no shells, reason on
stderr.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found