Runtimes & bridge
The Runtimes page (/space/runtimes) documents where your agents actually execute: the local/mobile bridge daemon for on-device work, paired devices, cloud sandboxes on Cloudflare Workers, and the upcoming Docker/SSH runtimes.
New to the AI side? Read AI concepts from scratch first — agents need tools to act, and this page is about where and how those tools run safely.
The concepts behind runtimes
Section titled “The concepts behind runtimes”Agents act through tools, and tools need somewhere to execute. This page splits that into two risks the platform has to solve:
- Where can the agent act? The bridge runs actions on your machine (files, shell, processes, network) or your phone (notifications, SMS, contacts); cloud sandboxes run code in ephemeral Docker jails in our private cloud; Docker/SSH runtimes will target containers and remote boxes. Each is a different execution surface.
- How do we keep that safe? An autonomous model calling
desktop.shell.executeis genuinely dangerous, so every capability is explicitly scoped and consent-gated (consent_prompt/consent_response), audited indevice_action_log, and guarded by a destructive-command filter. The connection is outbound-only WebSocket, so even the daemon itself never accepts inbound traffic.
The bridge is also where the local-first promise physically lives: sync.chat.push mirrors your history into the device’s SQLite, so the data exists on your hardware, not only in a cloud index.
The bridge daemon (local environment connector)
Section titled “The bridge daemon (local environment connector)”The SynthHires Bridge is an ultra-lightweight desktop/mobile daemon that lets agents execute tasks and commands on your own machine — without uploading your files to the cloud and without opening ports in your router.
- Platforms: Windows (exe), macOS (universal), Linux (raw binary,
.deb,.rpm,.AppImagevia/api/downloads/desktop), NixOS (flake — never the raw binary). No Android APK / iOS /.dmgis published by CI today. - Modelo de conexión: 100% WebSocket solo saliente a la plataforma (sin puertos de entrada), TLS 1.3.
- Seguridad: filtro de comandos destructivos, acciones con consentimiento, log de auditoría. El emparejamiento exige sesión real (no invitada):
POST /api/devices/pair/startdevuelve{code, pairingId, backend}con TTL 5 min; el daemon lo canjea enPOST /api/devices/pair/completepor{deviceId, token, wsUrl}(token 30 días, solo hash en DB). - Status polling: the page polls the daemon’s own HTTP status endpoint (
http://127.0.0.1:7333/status) — the authoritative source forpaired, WebSocketconnected, RTT,versionand the hostosidentity (exact distro from/etc/os-release: NixOS vs Ubuntu vs Fedora, kernel, arch, libc) — and falls back to/api/deviceswhen the daemon isn’t reachable from the browser (e.g. mobile or production). - Status states: checking → waiting for pairing → reconnecting → connected (with RTT) → offline / needs login.
The wire protocol
Section titled “The wire protocol”The web side of the protocol lives in src/lib/agent/bridge-protocol.ts (version 1), mirrored serde-camelCase on the Rust daemon. Frames are JSON messages over WebSocket:
| Frame | Purpose |
|---|---|
hello / hello_ack |
Handshake with protocol version + capabilities |
heartbeat / heartbeat_ack |
Liveness with RTT measurement |
scope_update |
Capability scope changes (grant/revoke) |
resume |
Reconnect with session resume |
action_request / action_cancel |
Request or cancel a device action |
consent_prompt / consent_response |
Human-in-the-loop consent dialog |
action_result / action_stream |
Action completion (or streaming output) |
revoke |
Device/scope revocation |
error |
Protocol errors with close codes |
Capabilities
Section titled “Capabilities”The protocol exposes scoped capabilities (BRIDGE_CAPABILITIES), each requiring explicit consent:
- Desktop:
desktop.shell.execute,desktop.fs.read/write/delete/verify/list/watch,desktop.process.list/kill,desktop.network.fetch. - Sync:
sync.chat.push(mirror conversations to the device’s SQLite). - Mobile:
mobile.notifications.read/dismiss,mobile.sms.read/send,mobile.contacts.read,mobile.location.read,mobile.automation.perform,mobile.clipboard.read/write.
Pairing
Section titled “Pairing”Pairing happens through /space/connections (Devices tab) or the runtimes page: download the bridge, run it, and use “Pair new” with a pairing code (pairing_codes table). Paired devices appear in DeviceList, with online/offline state, RTT and an audit log of consented actions (device_action_log).
Pairing REQUIRES a real logged-in session — guest sessions get 403 login_required and can never own devices. The /api/devices/pair/complete + /api/devices/ws legs carry no user cookie by design (the unpaired daemon has no session yet): they authenticate with the one-time pairing code and the resulting device token respectively.
Local vs remote pairing
Section titled “Local vs remote pairing”- Same machine (local): the GUI exe (or
synthhires-bridge run) plus the browser’shttp://127.0.0.1:7333/status|/pairauto-pair and thesynthhires://pair?code=…&pairing_id=…&backend=…deep link handle everything. Open/space/connections→ copy the code → the local daemon pairs itself. - Remote machine / headless terminal / VM (different host):
127.0.0.1:7333is NOT reachable from your browser, so auto-pair cannot work. Copy theCODEshown in Connections, note thebackendorigin the web displays (prod vs staging vs local dev), then on the remote host run:synthhires-bridge pair "<BACKEND>" <CODE> [--pairing-id <ID>]followed bysynthhires-bridge run(or the systemd unit frominstall.sh). The connection afterwards is 100% outbound WebSocket — no inbound ports. Revoke from the web (Revoke) or locally (synthhires-bridge unpair).
Install matrix
Section titled “Install matrix”| Host | How |
|---|---|
| Windows | Download the .exe from the edge release and run it (GUI + tray). Same binary runs headless: synthhires-bridge --headless run. |
| macOS / generic Linux | Download the asset for your arch from /api/downloads/desktop (or the edge release), chmod +x, then pair + run. curl …/install.sh | bash -s -- --backend <URL> --systemd automates this incl. a user systemd unit. |
| NixOS | Do NOT use the raw binary (it will not run: FHS + dynamic libs). Expert path via the flake in the bridge repo: nix run github:planobinario/synth-hires-bridge, or declaratively with services.synth-hires-bridge.enable = true; (user systemd service running synth-hires-bridge run). CI builds the synthhires-bridge-nixos artifact. |
| Docker / VM | Headless image (ENTRYPOINT ["synthhires-bridge"]): pair/run via docker exec, HOME=/data persists state.json. |
Paired devices
Section titled “Paired devices”DeviceList shows every paired device with connection status and lets you revoke access at any time. Device records live in devices + device_tokens; revoking kills the token and closes the WebSocket.
Cloud sandboxes (private cloud, Docker)
Section titled “Cloud sandboxes (private cloud, Docker)”The Sandboxes tab manages ephemeral Docker sandboxes in our private cloud
(sandboxd, reached in dev through the -L 8789 SSH
tunnel; SANDBOX_* in .dev.vars, token server-side only):
- One container per sandbox, created on demand (~300ms warm cache), destroyed on TTL expiry, explicit DELETE, or daemon shutdown.
- Images: stock
python/node(stdlib only) +ds(numpy, pandas, matplotlib, openpyxl, chromium, playwright — built once on the host, no registry). ds asks for 1 GB RAM (chromium doesn’t fit 512 MB). - Hardened:
--cap-drop=ALL,no-new-privileges, read-only rootfs, tmpfs/scratch(the only RW, 256MB) +/tmpnoexec, pids-limit (64, 256 for ds — Chromium threads abort below ~100), memory+swap and CPU caps (default 512MB/0.5 CPU, max 1GB/1 CPU), nobody user,--initreaper,network=noneby default, log-driver none. - Egress allowlist: package managers (pip, npm, cargo, go, apt) and explicit
hosts from a curated registry list (pypi, npmjs, crates, golang, ubuntu,
github, huggingface) upgrade the sticky box once from
nonetoweblist(bridge + filtering egress proxy with per-flow audit on the daemon), with automatic/scratchmigration (fail-closed past 100 files / 8 MiB — oversized boxes refuse the upgrade instead of losing data). Any other host, raw IP, or secret in a networked command is refused before touching a box; every weblist decision is audit-logged. Daemon-side nftables would be stricter than the proxy, but the daemon deliberately holds no net_admin: proxy + harness pre-check is the designed boundary, documented as such. - Skill recipes ride inside the jail (
/mnt/skills/cloudshell, read-only): data analysis, office docs, headless-browser patterns. - Deliverables: the dispatcher collects
*.pngfrom/scratch/outputs(max 3, ≤400 KB) and shows them to the user; the model gets names+sizes. Non-image files (PDF, office, csv) attach inline when small (download + preview) and stay fetchable on demand (GET /api/sandbox/outputs) when large — a generated file is never stranded in the jail. Inline deliverables also persist to local IndexedDB at completion, so downloads and previews survive reloads and jail TTL expiries without touching the server. argv-only exec (no shell string), cwd jailed to/scratch(strict: exactly/scratchor a real child, no..), 1–300s timeouts enforced bytimeout(1)KILL, 256 KiB/stream output caps, post-exec stray sweep (runs as nobody — nothing in the container ever runs as uid 0).- Multi-tenant safety: global pool max 6 sandboxes / 4 concurrent execs,
per-owner quota max 2 (
X-Sandbox-User= web userId, both server-side enforced and advertised via/v1/health), exec/delete ownership enforced (403 on mismatch), single static Bearer fail-closed. - Prod path: Workers reach sandboxd over TLS at
https://sandbox.synthhires.comthrough a DEDICATED Cloudflare Tunnel (cloudflared-sandboxon the host: outbound-only, no open ports, no shared reverse-proxy, no LE to renew — the sandbox shares nothing with the host’s other purposes) withSANDBOX_*as Worker secrets — the local-L 8789SSH tunnel is dev-only and never exists at the edge. Rotate the Bearer withnode scripts/sync-sandbox-token.mjs. - Edge posture for the API hostname: Cloudflare bot heuristics (Bot Fight
Mode, Browser Integrity Check) MUST stay off for
sandbox.*— they 1010 machine clients (including Workers subrequests). The marketing site keeps Turnstile for form abuse instead. - Sticky per conversation: with (owner, conversationId) the jail is REUSED
across turns (
/scratchsurvives within the 600 s TTL) via thecloud_sandbox_sessionsaffinity hint — misses and 404/410 recreate. One-shot without conversation scope. - Chat uploads: attachments auto-deposit into
/scratch/uploads(read-only) right after each send with files (POST /api/sandbox/attachments, idempotent by name, 10 files / 1 MiB each / 6 MiB total). The model lists that dir before assuming nothing arrived. - UX honesty: chat steps show an “ephemeral cloud” badge (never confused with your PC), structured error cards with stateless Retry for transient failures, and Runtimes → Sandboxes shows the live pool with per-session Release. The running copy stays neutral until the result lands.
- Explicit runtime:
cloud_shelltargets the cloud jail ON PURPOSE (user asks for cloud/VM, untrusted code, or no paired Desktop) — with or without a paired bridge.bridge_desktop_shellstays the PC tool (its silent cloud fallback remains only as a safety net). - Sandboxes are the default remote execution target when no local bridge
is paired. Prod (Cloudflare Workers) needs sandboxd exposed over TLS
(e.g. cloudflared) +
SANDBOX_*secrets — the local tunnel does not exist at the edge; until then the cloud branch is dev-only.
The context window idea shows up here too: agent code runs in a resource-capped, isolated runtime rather than on your machine, so a runaway loop is contained instead of touching your environment.
Docker & SSH (coming soon)
Section titled “Docker & SSH (coming soon)”| Runtime | Timeline | Description |
|---|---|---|
| Docker | Q3 2026 | Pull any OCI image and run agents inside ephemeral containers — ideal for CI pipelines and reproducible builds |
| SSH | Q4 2026 | Connect to any machine (workstation, VPS, bare metal) with full terminal access and consent-gated commands |
Local-first storage strategy
Section titled “Local-first storage strategy”The bridge participates in the local-first storage model: sync.chat.push mirrors chat history into the device’s local SQLite, so your full message/code/diff history exists on your hardware even when the cloud index only carries the light per-conversation rows (~200 B each) needed for cross-device sidebar sync.