A native desktop bridge that connects the OS Loop web application with local system capabilities. Built with Tauri v2 and Rust.
The bridge exposes an HTTP + WebSocket server on localhost that the web app discovers automatically. It provides MCP server proxying, filesystem access, and sleep prevention — all through a well-defined contract.
os-loop-bridge/
├── crates/bridge-core/ # Pure Rust library — contract types, server, all business logic
├── src-tauri/ # Tauri v2 shell — thin wrapper that starts the bridge
├── src/ # Frontend (status/settings UI)
└── docs/ # Contract specification and guides
The project is structured as a Rust workspace with two crates:
bridge-core— Contains all testable logic: contract types, HTTP/WS server (Axum), session management, security, and fixed-port configuration. Runs with plaincargo test, no Tauri dependency needed.src-tauri— Thin Tauri v2 wrapper that spawns the bridge server on app startup and registers theosloop://custom protocol.
- Rust (edition 2021, stable toolchain)
- Node.js >= 18
- macOS, Windows, or Linux
- Tauri v2 system dependencies — see Tauri prerequisites
# Clone the repository
git clone <repo-url>
cd os-loop-bridge
# Install the Tauri CLI
cargo install tauri-cli@^2
# Run in development mode
cargo tauri devThe desktop shell uses Tauri's built-in dev server for the static frontend in src/, so there is no separate frontend process to start on port 1420.
The bridge binds to a single persisted localhost listening port. The default port is 47832 and the bridge never scans for fallback ports.
The desktop status window shows the configured port, the currently active bound port, bind state, any bind error, and a recent localhost connection log so users can inspect handshakes, authenticated route usage, WebSocket upgrades, and 4xx/5xx failures.
If the configured port is busy, startup fails explicitly on that same port, the configured value stays persisted, and osloop://ensure-server retries that same port instead of mutating it.
# Run all bridge-core tests
cargo test -p bridge-core
# Run all workspace tests
cargo test --workspace# Debug build
cargo build --workspace
# Release build
cargo tauri build| Module | Path | Description |
|---|---|---|
contract |
crates/bridge-core/src/contract/ |
Types, constants, and event definitions matching the bridge contract |
config |
crates/bridge-core/src/config/ |
Persisted listening-port and app configuration |
security |
crates/bridge-core/src/security/ |
Origin policy (dev/prod) and CORS |
session |
crates/bridge-core/src/session/ |
Token-based session management with expiry |
server |
crates/bridge-core/src/server/ |
Axum HTTP router, route handlers, auth middleware, WebSocket |
capabilities |
crates/bridge-core/src/capabilities/ |
Capability enumeration |
protocol |
crates/bridge-core/src/protocol/ |
osloop:// URL parsing |
ui |
crates/bridge-core/src/ui/ |
UI state model for the desktop status window |
- Installation — Prerequisites, building, and platform bundles
- Development — Dev mode, listening port, testing, architecture
- Custom Protocol —
osloop://URLs and platform registration - Bridge Contract — Full API specification
The full contract specification lives in docs/osloop-bridge-contract.md.
Key points:
- Protocol version:
osloop.bridge.v1 - Discovery: The web app calls
GET /api/v1/healthon the persisted configured port, defaulting to47832. - Authentication:
POST /api/v1/session/handshakereturns a session token. Authenticated routes require theX-OSLOOP-BRIDGE-TOKENheader. - WebSocket:
GET /ws/v1/events?sessionToken=<token>for real-time events. - Custom protocol:
osloop://open,osloop://focus,osloop://ensure-server,osloop://settings - Strict routing: clients must use the canonical
/api/v1/...contract. The desktop connection log highlights malformed requests, including accidental/api/v1/api/v1/...duplication.
| Route | Auth | Description |
|---|---|---|
GET /api/v1/health |
No | Health check and discovery |
POST /api/v1/session/handshake |
No | Create a session |
POST /api/v1/session/refresh |
No | Refresh a session |
GET /api/v1/bridge/info |
Yes | Bridge metadata |
GET /api/v1/capabilities |
Yes | List capabilities |
GET /api/v1/mcp/servers |
Yes | List MCP servers |
POST /api/v1/mcp/servers |
Yes | Register an MCP server |
PATCH /api/v1/mcp/servers/:serverId |
Yes | Update an MCP server |
DELETE /api/v1/mcp/servers/:serverId |
Yes | Remove an MCP server |
POST /api/v1/mcp/servers/:serverId/connect |
Yes | Connect to an MCP server |
POST /api/v1/mcp/servers/:serverId/disconnect |
Yes | Disconnect from an MCP server |
GET /api/v1/mcp/servers/:serverId/capabilities |
Yes | Get MCP server capabilities |
POST /api/v1/mcp/call |
Yes | Call an MCP tool |
GET /api/v1/awake/status |
Yes | Sleep prevention status |
POST /api/v1/awake/acquire |
Yes | Prevent system sleep |
POST /api/v1/awake/release |
Yes | Release sleep lock |
GET /api/v1/filesystem/targets |
Yes | List filesystem targets |
POST /api/v1/filesystem/select |
Yes | Open file/directory picker |
POST /api/v1/filesystem/read |
Yes | Read file content |
POST /api/v1/filesystem/write |
Yes | Write file content |
GET /api/v1/system/info |
Yes | OS, shell, and package manager info |
GET /api/v1/system/tools |
Yes | List known/installed tools |
POST /api/v1/system/tools/locate |
Yes | Locate tools by name |
POST /api/v1/system/tools/install |
Yes | Install a tool (always requires approval) |
GET /api/v1/system/tools/recipes |
Yes | List install recipes |
POST /api/v1/system/tools/recipes/resolve |
Yes | Resolve recipe for platform |
POST /api/v1/system/commands/execute |
Yes | Execute a system command |
GET /api/v1/system/commands/runs |
Yes | List command runs |
GET /api/v1/system/commands/runs/:runId |
Yes | Get run details (stdout/stderr) |
POST /api/v1/system/commands/runs/:runId/terminate |
Yes | Terminate a running command |
GET /api/v1/system/permissions/commands |
Yes | List whitelist rules |
GET /api/v1/system/permissions/commands/:ruleId |
Yes | Get whitelist rule |
DELETE /api/v1/system/permissions/commands/:ruleId |
Yes | Delete whitelist rule |
POST /api/v1/system/permissions/prompts/:requestId/resolve |
Yes | Resolve pending permission prompt |
GET /api/v1/bridge/runtime |
Yes | Runtime config (port, tray, timeout) |
mcp_proxy, filesystem, awake, system_info, system_tools, tool_install_recipes, health, version, command_permissions, tray_mode
Real-time events via GET /ws/v1/events?sessionToken=<token>. Includes bridge lifecycle, MCP, awake, filesystem, system tool install/inventory, system command execution (started/stdout/stderr/completed/failed/terminated), permission prompts/decisions, whitelist updates, and runtime state changes. Full list in contract spec.
The bridge is the final authority for command execution and tool installation:
- Commands may be: allowed by existing whitelist, allowed once, allowed and whitelisted, or rejected.
- Installs always require explicit user approval — they can never be whitelisted.
- Rejections return structured codes:
rejected_by_user,bridge_policy_denied,invalid_request. - Whitelist rules match against template fields (with
__OSL_SECRET[...]__placeholders), never resolved secret values. - Users can inspect and revoke whitelist rules via the permissions API.
The bridge is secret-unaware by design. It receives already-resolved values from the web app and executes them. Secret interpolation (replacing __OSL_SECRET[...]__ placeholders with decrypted values) and output redaction (replacing secret values back to placeholders in stdout/stderr) are handled entirely by the web app before/after bridge interaction. The bridge never stores, manages, or has knowledge of secret IDs or placeholder syntax.
The bridge can run in the system tray (notification area). When tray mode is enabled:
- Window close hides the app to tray instead of quitting
- Permission prompts appear via tray notifications with a 60-second timeout
bridge.runtime.updatedevents reflect tray state changes- Tray menu: "Show Status", "Quit"
Command execution and tool installation default to 300,000ms (5 minutes). The web app can override this per-request via the timeoutMs field. No maximum cap is enforced by the bridge. Stdout/stderr are buffered up to 10 MB per stream. Runs exceeding the buffer are marked partial_output: true. WebSocket events continue streaming regardless of the buffer limit.
- Tauri v2 — Desktop app framework
- Axum — HTTP/WebSocket server
- Tokio — Async runtime
- Serde — Serialization
See LICENSE for details.