Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

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.

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.execute is genuinely dangerous, so every capability is explicitly scoped and consent-gated (consent_prompt/consent_response), audited in device_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, .AppImage via /api/downloads/desktop), NixOS (flake — never the raw binary). No Android APK / iOS / .dmg is 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/start devuelve {code, pairingId, backend} con TTL 5 min; el daemon lo canjea en POST /api/devices/pair/complete por {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 for paired, WebSocket connected, RTT, version and the host os identity (exact distro from /etc/os-release: NixOS vs Ubuntu vs Fedora, kernel, arch, libc) — and falls back to /api/devices when 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 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

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

  • Same machine (local): the GUI exe (or synthhires-bridge run) plus the browser’s http://127.0.0.1:7333/status|/pair auto-pair and the synthhires://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:7333 is NOT reachable from your browser, so auto-pair cannot work. Copy the CODE shown in Connections, note the backend origin the web displays (prod vs staging vs local dev), then on the remote host run: synthhires-bridge pair "<BACKEND>" <CODE> [--pairing-id <ID>] followed by synthhires-bridge run (or the systemd unit from install.sh). The connection afterwards is 100% outbound WebSocket — no inbound ports. Revoke from the web (Revoke) or locally (synthhires-bridge unpair).
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.

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.

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) + /tmp noexec, 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, --init reaper, network=none by 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 none to weblist (bridge + filtering egress proxy with per-flow audit on the daemon), with automatic /scratch migration (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 *.png from /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 /scratch or a real child, no ..), 1–300s timeouts enforced by timeout(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.com through a DEDICATED Cloudflare Tunnel (cloudflared-sandbox on 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) with SANDBOX_* as Worker secrets — the local -L 8789 SSH tunnel is dev-only and never exists at the edge. Rotate the Bearer with node 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 (/scratch survives within the 600 s TTL) via the cloud_sandbox_sessions affinity 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_shell targets the cloud jail ON PURPOSE (user asks for cloud/VM, untrusted code, or no paired Desktop) — with or without a paired bridge. bridge_desktop_shell stays 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.

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

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.