# Sidecar Architecture The sidecar is the bridge between agor's Rust backend and AI provider APIs. Because the Claude Agent SDK, OpenAI Codex SDK, and Ollama API are JavaScript/TypeScript libraries, they cannot run inside Rust or WebKit2GTK's webview. Instead, the Rust backend spawns child processes (sidecars) that handle AI interactions and communicate back via stdio NDJSON. --- ## Overview ``` Rust Backend (SidecarManager) | +-- Spawns child process (Deno preferred, Node.js fallback) +-- Writes QueryMessage to stdin (NDJSON) +-- Reads response lines from stdout (NDJSON) +-- Emits Tauri events for each message +-- Manages lifecycle (start, stop, crash recovery) | v Sidecar Process (one of): +-- claude-runner.mjs -> @anthropic-ai/claude-agent-sdk +-- codex-runner.mjs -> @openai/codex-sdk +-- ollama-runner.mjs -> native fetch to localhost:11434 ``` --- ## Provider Runners Each provider has its own runner file in `sidecar/`, compiled to a standalone ESM bundle in `sidecar/dist/` by esbuild. The runners are self-contained — all dependencies (including SDKs) are bundled into the `.mjs` file. ### Claude Runner (`claude-runner.ts` -> `claude-runner.mjs`) The primary runner. Uses `@anthropic-ai/claude-agent-sdk` query() function. **Startup sequence:** 1. Reads NDJSON messages from stdin in a loop 2. On `query` message: resolves Claude CLI path via `findClaudeCli()` 3. Calls SDK `query()` with options: prompt, cwd, permissionMode, model, settingSources, systemPrompt, additionalDirectories, worktreeName, pathToClaudeCodeExecutable 4. Streams SDK messages as NDJSON to stdout 5. On `stop` message: calls AbortController.abort() **Claude CLI detection (`findClaudeCli()`):** Checks paths in order: `~/.local/bin/claude` -> `~/.claude/local/claude` -> `/usr/local/bin/claude` -> `/usr/bin/claude` -> `which claude`. If none found, emits `agent_error` immediately. **Session resume:** Passes `resume: sessionId` to the SDK. **Multi-account support:** When `claudeConfigDir` is provided, it is set as `CLAUDE_CONFIG_DIR` in the SDK's env option. **Worktree isolation:** When `worktreeName` is provided, it is passed as `extraArgs: { worktree: name }` to the SDK, which translates to `--worktree ` on the CLI. ### Codex Runner (`codex-runner.ts` -> `codex-runner.mjs`) Uses `@openai/codex-sdk` via dynamic import (graceful failure if not installed). **Key differences from Claude:** - Authentication via `CODEX_API_KEY` environment variable - Sandbox mode mapping: `bypassPermissions` -> `full-auto`, `default` -> `suggest` - Session resume via thread ID - No profile/skill support - ThreadEvent format parsed by `codex-messages.ts` ### Ollama Runner (`ollama-runner.ts` -> `ollama-runner.mjs`) Direct HTTP to Ollama's REST API — zero external dependencies. **Key differences:** - No SDK — uses native `fetch()` to `http://localhost:11434/api/chat` - Health check on startup (`GET /api/tags`) - Supports Qwen3's `` tags for reasoning display - Configurable: host, model, num_ctx, temperature - Cost is always $0 (local inference) - No subagent support, no profiles, no skills --- ## Communication Protocol ### Messages from Rust to Sidecar (stdin) ```typescript // Query -- start a new agent session { "type": "query", "session_id": "uuid", "prompt": "Fix the bug in auth.ts", "cwd": "/home/user/project", "provider": "claude", "model": "claude-sonnet-4-6", "permission_mode": "bypassPermissions", "resume_session_id": "previous-uuid", // optional "system_prompt": "You are an architect...", // optional "claude_config_dir": "~/.config/switcher-claude/work/", // optional "setting_sources": ["user", "project"], // optional "additional_directories": ["/shared/lib"], // optional "worktree_name": "session-123", // optional "provider_config": { ... }, // provider-specific blob "extra_env": { "BTMSG_AGENT_ID": "manager-1" } // optional } // Stop -- abort a running session { "type": "stop", "session_id": "uuid" } ``` ### Messages from Sidecar to Rust (stdout) The sidecar writes one JSON object per line (NDJSON). Claude messages follow the same format as the Claude CLI's `--output-format stream-json`. --- ## Environment Variable Stripping When agor is launched from within a Claude Code terminal session, parent `CLAUDE*` environment variables must not leak to the sidecar. The solution is **dual-layer stripping**: 1. **Rust layer (primary):** `SidecarManager` calls `env_clear()` on the child process command, then explicitly sets only needed variables. 2. **JavaScript layer (defense-in-depth):** Each runner strips provider-specific variables (Claude: `CLAUDE*` except `CLAUDE_CODE_EXPERIMENTAL_*`; Codex: `CODEX*`; Ollama: `OLLAMA*` except `OLLAMA_HOST`). The `extra_env` field in AgentQueryOptions allows injecting specific variables (like `BTMSG_AGENT_ID`) after stripping. --- ## Sidecar Lifecycle ### Startup SidecarManager is initialized during Tauri app setup. No sidecar processes spawn until the first agent query. ### Runtime Resolution `resolve_sidecar_for_provider(provider)` finds the appropriate runner: 1. Looks for `{provider}-runner.mjs` in sidecar dist directory 2. Checks for Deno first, then Node.js 3. Returns `SidecarCommand` struct with runtime binary and script path Deno preferred (~50ms cold-start vs ~150ms for Node.js). ### Crash Recovery (SidecarSupervisor) See [production/hardening.md](../production/hardening.md) for details. Exponential backoff (1s-30s cap), max 5 restart attempts, `SidecarHealth` enum. ### Shutdown On app exit, SidecarManager sends stop messages to all active sessions and kills remaining child processes. `Drop` implementation ensures cleanup even on panic. --- ## Build Pipeline ```bash npm run build:sidecar # Internally runs esbuild 3 times: # sidecar/claude-runner.ts -> sidecar/dist/claude-runner.mjs # sidecar/codex-runner.ts -> sidecar/dist/codex-runner.mjs # sidecar/ollama-runner.ts -> sidecar/dist/ollama-runner.mjs ``` Each bundle is standalone ESM with all dependencies included. Built `.mjs` files are included as Tauri resources in `tauri.conf.json`. --- ## Message Adapter Layer On the frontend, raw sidecar messages pass through a provider-specific adapter before reaching the agent store: ``` Sidecar stdout -> Rust SidecarManager -> Tauri event -> agent-dispatcher.ts -> message-adapters.ts (registry) -> claude-messages.ts / codex-messages.ts / ollama-messages.ts -> AgentMessage[] (common type) -> agents.svelte.ts store ``` The `AgentMessage` type is provider-agnostic. The adapter layer is the only code that understands provider-specific formats. ### Test Coverage - `claude-messages.test.ts` — 25 tests - `codex-messages.test.ts` — 19 tests - `ollama-messages.test.ts` — 11 tests