# DESIGN: Sidecar Tier — v0.14.x ## Status: Proposed ## Problem Starlark is the right sandbox for most extension logic: declarative, safe, fast to start, no deployment pipeline. But some workloads cannot run in Starlark: - **ML inference** — Python + PyTorch/TF model loading, GPU access. - **Media transcoding** — ffmpeg, ImageMagick, Whisper. - **Language servers** — LSP processes for code-workspace. - **Local git** — clone, diff, merge operations on workspace directories. - **Custom runtimes** — anything needing system libraries, native code, or long-running processes. - **Personal local bridges** — a user's laptop exposing filesystem, terminal, clipboard, or local compute to the Armature instance. Today, the `http` module lets Starlark call external APIs (`api.http` permission, domain allowlists, SSRF protection). This covers cloud services (OpenAI, S3, external webhooks) but not **co-located processes** that need access to Armature's data, events, and workspace filesystem, nor **personal processes** on a user's own machine. The sidecar tier bridges this gap: out-of-process extensions that connect inward to the kernel, authenticate, register capabilities, and participate in the extension ecosystem as first-class citizens — at both the **instance level** (shared infrastructure) and the **user level** (personal local tools). **If done wrong, the consequences are severe:** - Wrong auth model → arbitrary processes access kernel APIs. - Wrong lifecycle → resource leaks, zombie processes, orphaned registrations. - Wrong API surface → every sidecar extension inherits the debt. - Wrong isolation → a runaway sidecar takes down the cluster. - Wrong discovery → fragile deployments that break on restart. - Wrong scoping → user sidecars see other users' data. This is invisible infrastructure. Users never see it. But every extension that needs native code depends on the contract being right. ## Non-Goals - **Kernel manages sidecar processes.** The kernel does not start, stop, or restart sidecars. That's the deployment layer's job (K8s, Docker Compose, systemd, or a human running a binary). The kernel discovers sidecars that connect to it. - **Sidecar marketplace.** Sidecar packages are installed like any other `.pkg` file. The sidecar binary ships separately (container image, standalone binary, pip package). The manifest declares the sidecar endpoint; the operator deploys it. - **Arbitrary bidirectional streaming.** Sidecars communicate via HTTP/JSON request-response and WebSocket events. No gRPC, no custom binary protocols. KISS. - **Hot code reload.** Sidecar updates require process restart. The kernel detects the reconnection and re-registers capabilities. - **Multi-tenant sidecar isolation.** Instance-level sidecars run at the instance level, not per-user. User-level isolation is handled by user sidecars (v0.14.5), which are scoped to the owning user's identity. --- ## Architecture ### Connect-Inward Model The defining property: **sidecars connect TO the kernel, not the other way around.** The kernel never reaches outward to discover or contact a sidecar. This eliminates: - K8s RBAC for the kernel to discover pods. - Service mesh configuration. - DNS-based service discovery. - Any notion of the kernel "managing" external processes. A sidecar is just a process that knows the kernel's address and has credentials to authenticate. It could be a container in the same pod, a separate deployment, a systemd service on the same machine, or a process on a developer's laptop connected via tunnel. ``` ┌─────────────────────────────────────────────────────┐ │ Deployment Layer (K8s / Docker / systemd / manual) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ instance │ │ instance │ │ instance │ │ │ │ sidecar: │ │ sidecar: │ │ sidecar: │ │ │ │ ml-infer │ │ ffmpeg │ │ git-ops │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ connect inward │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────┐ │ │ │ Armature Kernel │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ Sidecar Registry (PG) │ │ │ │ │ │ ml-infer: instance, 3 caps │ │ │ │ │ │ ffmpeg: instance, 2 caps │ │ │ │ │ │ git-ops: instance, 4 caps │ │ │ │ │ │ jeff-local: user:jeff, 3 caps │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ └────────────────────────────────▲─────────┘ │ │ │ │ └───────────────────────────────────┼──────────────────┘ │ connect inward ┌─────┴──────┐ │ user │ │ sidecar: │ │ jeff-local │ │ (laptop) │ └────────────┘ ``` ### Two Scopes **Instance sidecars** — shared infrastructure. Deployed by the operator. Any user's requests can invoke their capabilities. An ML inference sidecar, a transcoding service, a git operations worker. **User sidecars** — personal processes. Run by an individual user on their own machine. Only the owning user's requests can invoke their capabilities. A local file bridge, a personal Ollama instance, a script that reacts to Armature events. Both use the same connect-inward mechanism, same registration flow, same capability contract. The difference is the identity binding and access scoping. ### Two Authentication Modes #### Mode A: Registration Token For simple deployments (Docker Compose, single machine, dev/test). 1. Admin generates an instance sidecar token: `armature sidecar token create --package ml-inference` User generates a personal sidecar token (from settings UI or CLI): `armature sidecar token create --personal` 2. Token is a signed JWT with claims: `{package_id, scope, user_id, exp}`. Instance tokens: `scope: "instance"`, `user_id: null`. User tokens: `scope: "user"`, `user_id: ""`. 3. Sidecar starts with the token as env: `ARMATURE_TOKEN=ey...` 4. Sidecar calls `POST /api/v1/sidecar/register`. 5. Kernel validates JWT, creates registry entry with appropriate scope. #### Mode B: mTLS For production deployments with certificate infrastructure. 1. Certificate CN encodes identity: Instance: `sidecar:ml-inference` User: `sidecar-user:jeff:local-bridge` 2. Kernel's `MTLSNativeProvider` parses the CN prefix and resolves to the appropriate sidecar identity. Both modes produce the same internal identity: a `SidecarIdentity` struct with `{package_id, scope, user_id, node_id, registered_at}`. ### Sidecar Registry ```sql CREATE TABLE IF NOT EXISTS sidecar_registry ( sidecar_id TEXT PRIMARY KEY, package_id TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'instance', user_id TEXT, endpoint TEXT NOT NULL, heartbeat TIMESTAMPTZ DEFAULT now(), capabilities JSONB DEFAULT '[]', stats JSONB DEFAULT '{}', registered_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_sidecar_pkg ON sidecar_registry (package_id); CREATE INDEX idx_sidecar_user ON sidecar_registry (user_id) WHERE user_id IS NOT NULL; ``` `scope` is `'instance'` or `'user'`. `user_id` is NULL for instance sidecars, set for user sidecars. **Lifecycle (mirrors cluster registry):** - **Register:** `POST /api/v1/sidecar/register` with capability manifest. Kernel creates/updates registry row. - **Heartbeat:** `POST /api/v1/sidecar/heartbeat` every N seconds. - **Sweep:** Kernel's heartbeat loop sweeps stale sidecar entries alongside node entries. 3× heartbeat interval = stale. - **Self-eviction:** Heartbeat returns 404 → re-register or exit. - **Deregister:** `DELETE /api/v1/sidecar/deregister` on graceful shutdown. ### Capability Registration On registration, the sidecar declares capabilities: ```json POST /api/v1/sidecar/register { "package_id": "ml-inference", "endpoint": "http://ml-inference:8080", "capabilities": [ { "name": "embed", "description": "Generate text embeddings", "input_schema": { "type": "object", "properties": { "text": { "type": "string" }, "model": { "type": "string", "default": "all-MiniLM-L6-v2" } }, "required": ["text"] }, "timeout_seconds": 30 }, { "name": "classify", "description": "Zero-shot text classification", "input_schema": { ... }, "timeout_seconds": 60 } ] } ``` ### Sidecar SDK (Starlark) New `sidecar` Starlark module. Permission: `sidecar.call`. ```python # Call an instance sidecar capability result = sidecar.call("ml-inference", "embed", {"text": "Hello"}) # Check availability available = sidecar.available("ml-inference") # List capabilities caps = sidecar.capabilities("ml-inference") ``` User sidecar calls are routed automatically. When a request originates from user Jeff, `sidecar.call("user-bridge", "fs.search", {...})` hits Jeff's personal sidecar. The same call from user Alice would hit Alice's personal `user-bridge` sidecar (or fail if she doesn't have one). The Starlark caller doesn't specify the user — the kernel resolves it from the request context. ### Sidecar SDK (Frontend) ```js const result = await sw.sidecar.call('ml-inference', 'embed', { text: 'Hello, world' }); const available = await sw.sidecar.available('user-bridge'); ``` Frontend calls go through the kernel API. The kernel proxies to the appropriate sidecar based on scope + user identity. ### Sidecar API Contract (Inbound to Kernel) | Endpoint | Purpose | Auth | |----------|---------|------| | `POST /api/v1/sidecar/register` | Register + declare capabilities | Token or mTLS | | `POST /api/v1/sidecar/heartbeat` | Heartbeat + stats | Token or mTLS | | `DELETE /api/v1/sidecar/deregister` | Graceful shutdown | Token or mTLS | | `GET /api/v1/ext/{pkg}/*` | Read extension data (scoped) | Token or mTLS | | `POST /api/v1/ext/{pkg}/*` | Write extension data (scoped) | Token or mTLS | | `POST /api/v1/sidecar/emit` | Emit events | Token or mTLS | | `GET /api/v1/sidecar/subscribe` | WebSocket event subscription | Token or mTLS | | `POST /api/v1/sidecar/lib/{pkg}/{fn}` | Cross-package function call | Token or mTLS | **Scoping rules:** - Instance sidecars: can access `/api/v1/ext/{own_package}/*` only. - User sidecars: can access `/api/v1/ext/{own_package}/*` scoped to the owning user's data within that package. The sidecar sees the same data the user would see through the UI. - Event emission scoped to the sidecar's package prefix. ### Sidecar API Contract (Outbound from Kernel) | Endpoint | Purpose | |----------|---------| | `POST {sidecar_endpoint}/api/v1/exec/{capability}` | Execute a capability | | `GET {sidecar_endpoint}/api/v1/health` | Health check | The `/exec/{capability}` endpoint receives: ```json { "input": { ... }, "context": { "user_id": "...", "package_id": "...", "request_id": "..." } } ``` Returns: ```json { "output": { ... }, "error": null } ``` ### Resource Limits What the kernel enforces at the proxy layer: - **Timeout:** Per-capability, from capability declaration. - **Request rate:** Per-sidecar, configurable in admin. - **Response size:** Default 10MB, configurable per-sidecar. - **Concurrent calls:** Default 10, configurable per-sidecar. User sidecars have separate (typically lower) defaults: - **User sidecar rate limit:** Default 30/min (vs 100/min instance). - **User sidecar concurrency:** Default 3 (vs 10 instance). - **User sidecar response size:** Default 5MB (vs 10MB instance). Admin can adjust user sidecar defaults globally. Individual users cannot override admin limits. ### Manifest Integration Instance sidecar manifest: ```json { "id": "ml-inference", "type": "full", "tier": "sidecar", "version": "1.0.0", "sidecar": { "image": "gobha/armature-ml-inference:latest", "required": true, "health_endpoint": "/api/v1/health", "env_hints": { "ARMATURE_URL": "Kernel URL (auto-populated)", "ARMATURE_TOKEN": "Registration token", "MODEL_PATH": "Path to model files" } } } ``` User sidecar manifest (installed as a regular package, sidecar runs on user's machine): ```json { "id": "user-bridge", "type": "full", "tier": "sidecar", "version": "1.0.0", "sidecar": { "scope": "user", "required": false, "download_url": "https://github.com/armature/user-bridge/releases", "env_hints": { "ARMATURE_URL": "Your Armature instance URL", "ARMATURE_TOKEN": "Generate from Settings → Sidecars" } }, "surfaces": ["/user-bridge/settings"] } ``` `sidecar.scope: "user"` tells the kernel this is a user sidecar package. The package installs normally (admin or self-install depending on permissions), but the sidecar binary runs on the user's machine. ### Tool Meta-Tool Integration When `llm-bridge` assembles tools for a completion: 1. Instance-level actions from `sw.actions.list()` → available to all. 2. Instance sidecar capabilities → available to all. 3. **Requesting user's personal sidecar capabilities** → available to that user only. This means different users get different tool sets. Jeff has `user-bridge` running with `fs.search`, `terminal.exec`, and `clipboard.get`. Alice doesn't. When Jeff asks "Hey Max, find the auth file on my desktop," Max has `user-bridge.fs.search` in his tool set. When Alice asks the same, that tool doesn't exist — Max tells her he can't access her local files. The same AI persona, different capabilities per user, zero configuration. ### Admin UI **Instance sidecar management:** - Sidecar health status per registered instance sidecar. - Capabilities list with call counts, error rates, latency. - Token management: generate, revoke, list. - Per-sidecar rate limit and concurrency configuration. **User sidecar admin controls:** - Permission: `sidecar.connect_personal` — admin grants to specific users or groups. - Capability allowlist for user sidecars — admin restricts which capabilities user sidecars can declare. - Global user sidecar resource limits (rate, concurrency, response size). - Monitoring: list of all connected user sidecars with owner, endpoint, capabilities, last heartbeat. - Kill switch: admin can revoke any user sidecar token or sweep a specific user's sidecars. **User settings (for users with `sidecar.connect_personal`):** - Generate personal sidecar token. - List personal connected sidecars with health status. - Revoke own tokens. - Download link / instructions for user sidecar binaries (from package manifest `download_url`). ### Monitoring Kernel-side metrics: - `sidecar_call_total{package, capability, scope, status}` — counter. - `sidecar_call_duration_seconds{package, capability, scope}` — histogram. - `sidecar_heartbeat_age_seconds{package, scope}` — gauge. - `sidecar_active{package, scope}` — gauge. - `sidecar_user_count` — gauge (number of connected user sidecars). --- ## User Sidecar Use Cases ### Local Bridge (Desktop Commander Pattern) A thin agent on the user's laptop that exposes local capabilities: ```json { "capabilities": [ { "name": "fs.search", "description": "Search local filesystem by filename or content", "input_schema": { "properties": { "query": { "type": "string" }, "path": { "type": "string", "default": "~" }, "content_search": { "type": "boolean", "default": false } }, "required": ["query"] }, "timeout_seconds": 15 }, { "name": "fs.read", "description": "Read a local file", "input_schema": { "properties": { "path": { "type": "string" }, "max_bytes": { "type": "integer", "default": 1048576 } }, "required": ["path"] }, "timeout_seconds": 10 }, { "name": "terminal.exec", "description": "Execute a shell command", "input_schema": { "properties": { "command": { "type": "string" }, "cwd": { "type": "string", "default": "~" }, "timeout": { "type": "integer", "default": 30 } }, "required": ["command"] }, "timeout_seconds": 60 }, { "name": "clipboard.get", "description": "Read clipboard contents", "input_schema": {}, "timeout_seconds": 5 } ] } ``` **Tool meta-tool flow:** ``` User: "Hey Max, find the database migration script I was working on yesterday and add it to my notes" llm-bridge assembles (for this user): Layer 5 tools: [...instance tools..., user-bridge.fs.search, user-bridge.fs.read, notes.create, ...] LLM: 1. Tool: user-bridge.fs.search({query: "migration", path: "~/code"}) 2. Result: [{path: "~/code/armature/migrations/015_sidecar.sql", ...}] 3. Tool: user-bridge.fs.read({path: "~/code/armature/migrations/015_sidecar.sql"}) 4. Result: {content: "CREATE TABLE IF NOT EXISTS sidecar_registry..."} 5. Tool: notes.create({title: "Migration 015 - Sidecar Registry", body: "```sql\nCREATE TABLE..."}) 6. Text: "Found your sidecar migration script and saved it to Notes." ``` Three extensions orchestrated: user's local filesystem, instance-level notes, all through the same tool meta-tool pattern. ### Personal Compute (Local Ollama) User runs Ollama on their workstation with a GPU: ```json { "capabilities": [ { "name": "complete", "description": "LLM completion via local Ollama", "input_schema": { "properties": { "prompt": { "type": "string" }, "model": { "type": "string", "default": "llama3" } }, "required": ["prompt"] }, "timeout_seconds": 120 }, { "name": "embed", "description": "Local embedding via Ollama", "input_schema": { "properties": { "text": { "type": "string" }, "model": { "type": "string", "default": "nomic-embed-text" } }, "required": ["text"] }, "timeout_seconds": 30 } ] } ``` `llm-bridge` can be configured (per-user setting or persona preference) to route completions through the user's local Ollama instead of the instance-level provider. BYOK taken to the extreme — bring your own hardware. ### Personal Automation A script that reacts to Armature events: ```python # Personal deploy watcher — connects to Armature, subscribes to # events in the #deploys conversation, runs kubectl locally ws = connect(f'{KERNEL_URL}/api/v1/sidecar/subscribe', token=TOKEN) for event in ws: if event['type'] == 'chat.message' and 'deploy' in event['content']: result = subprocess.run(['kubectl', 'get', 'pods', '-n', 'prod'], capture_output=True, text=True) # Post result back to chat requests.post(f'{KERNEL_URL}/api/v1/sidecar/lib/chat-core/send', json={'conversation_id': event['conversation_id'], 'content': f'```\n{result.stdout}\n```'}, headers={'Authorization': f'Bearer {TOKEN}'}) ``` Personal workflow glue. No instance-level deployment required. --- ## RBAC for User Sidecars Three-layer permission model: ### Layer 1: Admin Controls WHO New permission: `sidecar.connect_personal`. Admin grants to specific users, groups, or roles. Most users don't need it. Developers, power users, AI-heavy workflows — they get the permission. Default: not granted. Opt-in only. ### Layer 2: Admin Controls WHAT **Capability allowlist for user sidecars.** Admin setting: `SIDECAR_USER_ALLOWED_CAPABILITIES` Default: `["fs.search", "fs.read", "clipboard.get"]` — safe read-only operations. To enable terminal access: admin adds `"terminal.exec"` to the allowlist. This is a deliberate escalation requiring admin action. A user sidecar that attempts to register a capability not on the allowlist gets a clear error: "Capability 'terminal.exec' not permitted for user sidecars. Contact your administrator." ### Layer 3: User Controls VISIBILITY User sidecar capabilities are **private by default** — only the owning user's requests invoke them. Optional: user can share a specific capability with their team via the settings UI. Shared capabilities appear in team members' tool sets (with attribution: "via Jeff's local bridge"). User sidecars are never instance-wide. That's what instance sidecars are for. --- ## Security Considerations ### Instance Sidecars 1. **Compromised binary** → can read/write own package data, call exported functions. Mitigation: package-scoped access, audit logging, token revocation. 2. **Token theft** → impersonate sidecar. Mitigation: package-scoped tokens, mTLS eliminates theft, revocable. 3. **SSRF via sidecar.call** → Mitigation: endpoints validated at registration, no runtime changes. 4. **Resource exhaustion** → Mitigation: per-sidecar concurrency and rate limits, timeout enforcement, response size limits. 5. **Privilege escalation via lib.require** → Mitigation: calls attributed to sidecar identity, target permission checks apply. ### User Sidecars (Additional Concerns) 6. **User deploys malicious sidecar** → registers capabilities that exfiltrate data when called. Mitigation: capability allowlist (admin controls what capabilities can be declared), private-by-default (only the user's own requests trigger it — they're exfiltrating from themselves). 7. **User shares malicious capability with team** → team members invoke it, sidecar captures their request data. Mitigation: shared capabilities are clearly attributed ("via Jeff's local bridge"). Admin can disable capability sharing entirely. Shared capabilities run with the CALLING user's permissions, not the sidecar owner's. 8. **User sidecar as pivot** → user's laptop compromised, attacker uses connected sidecar to access Armature data. Mitigation: user sidecar can only access data the user could already access through the UI. No privilege escalation. User can revoke own tokens. Admin kill switch. 9. **Stale user sidecars** → user's laptop goes offline, sidecar becomes stale. Mitigation: same sweep mechanism as instance sidecars. 3× heartbeat interval = swept. User reconnects when laptop comes back online. ### Principle of Least Privilege - Instance sidecar: access own package data only, middleware-enforced. - User sidecar: access own package data scoped to owning user only. - Event emission scoped to package prefix. - Capability allowlist for user sidecars (admin-controlled). - Shared capabilities run with caller's permissions. --- ## Version Plan ### v0.14.0 — Sidecar Registry + Auth **Goal:** Sidecars can register, authenticate, and heartbeat. - Sidecar registry table with `scope` and `user_id` columns. - Registration endpoint with JWT token validation. - Heartbeat + sweep integration. - Token generation (instance tokens via admin, user tokens via settings). - `SIDECAR_AUTH_MODE` config: `token` or `mtls`. - mTLS auth: `sidecar:` and `sidecar-user:` CN prefixes. - Admin UI: sidecar health on packages page. **Instance sidecars only in this version.** User sidecar plumbing exists in the schema but user token generation and RBAC gating land in v0.14.5. ### v0.14.1 — Capability Registration + Execution **Goal:** Sidecars declare capabilities. Starlark extensions call them. - Capability manifest in registration payload. - `sidecar` Starlark module: `call()`, `available()`, `capabilities()`. - Kernel proxy: `sidecar.call()` → `POST {endpoint}/exec/{cap}`. - Input validation against declared schema. - Timeout, response size, concurrent call limits. - Error propagation. - `sidecar.call` permission. - Audit logging. ### v0.14.2 — Kernel API Access + Event Bus **Goal:** Sidecars read/write their own data and participate in events. - Sidecar middleware: authenticate, scope to own package routes. - `/api/v1/sidecar/lib/{pkg}/{fn}` — cross-package function calls. - `/api/v1/sidecar/emit` — event emission (package-scoped). - `/api/v1/sidecar/subscribe` — WebSocket event subscription. - Frontend `sw.sidecar` SDK module. ### v0.14.3 — Manifest Integration + Admin Polish **Goal:** Sidecar packages are first-class in install/admin flow. - `tier: "sidecar"` manifest support. - `sidecar` manifest field (image, required, scope, env_hints, download_url). - Admin UI: deployment instructions from manifest. - Admin UI: capability list with metrics. - Admin UI: per-sidecar rate limit configuration. - Token management UI. - Monitoring metrics. ### v0.14.4 — Reference Sidecar + Instance Quality Gate **Goal:** Ship one real instance sidecar. Prove the contract. **`armature-embed`** — minimal Python sidecar running sentence-transformers for local embedding generation. One capability: `embed(text) → float[]`. `vector-store` calls `sidecar.call('armature-embed', 'embed', {...})` for local embedding without an external API. Completes the local-first RAG story. **Instance sidecar quality gate:** - Registration, heartbeat, sweep lifecycle tested. - Token and mTLS auth both functional. - Capability execution with limits enforced. - Package-scoped API access enforced. - Event emission/subscription functional. - `sw.sidecar` frontend module functional. - Reference sidecar runs in Docker and K8s. - Admin UI complete. - Monitoring metrics in Grafana. ### v0.14.5 — User Sidecars **Goal:** Users connect personal processes to Armature. - `sidecar.connect_personal` permission (admin-granted). - User token generation in Settings → Sidecars. - Capability allowlist for user sidecars (admin setting: `SIDECAR_USER_ALLOWED_CAPABILITIES`). - User sidecar registration with `scope: "user"`, `user_id` binding. - Execution routing: `sidecar.call()` resolves user sidecars based on request context (calling user gets their own sidecar). - Capability sharing: user can share specific capabilities with team. Shared caps attributed ("via Jeff's local bridge"). - Separate resource limits for user sidecars (lower defaults). - User settings UI: connected sidecars, token management, sidecar instructions from package manifests. - Admin UI: user sidecar monitoring, kill switch. - Tool meta-tool integration: `llm-bridge` includes user's personal sidecar capabilities in tool set assembly. **Reference user sidecar: `user-bridge`** — thin agent binary with `fs.search`, `fs.read`, `clipboard.get` capabilities. Published as a package with `sidecar.scope: "user"`. Binary as a GitHub release. **User sidecar quality gate:** - RBAC: user without `sidecar.connect_personal` cannot register. - Capability allowlist enforced. - User A cannot invoke user B's sidecar. - Shared capabilities run with caller's permissions. - Admin can revoke any user sidecar. - Tool meta-tool assembles per-user tool sets correctly. - `user-bridge` reference sidecar runs on macOS, Linux, Windows. --- ## Schema Summary ### New tables - `sidecar_registry` — sidecar_id, package_id, scope, user_id, endpoint, heartbeat, capabilities (JSON), stats (JSON), registered_at. (v0.14.0) - `sidecar_tokens` — id, package_id, scope, user_id, token_hash, created_by, expires_at, revoked_at. (v0.14.0) --- ## Configuration | Setting | Default | Purpose | |---------|---------|---------| | `SIDECAR_AUTH_MODE` | `token` | `token` or `mtls` | | `SIDECAR_HEARTBEAT_INTERVAL` | `10s` | Expected heartbeat frequency | | `SIDECAR_STALE_THRESHOLD` | `30s` | 3× heartbeat = stale | | `SIDECAR_MAX_RESPONSE_SIZE` | `10485760` (10MB) | Instance default | | `SIDECAR_DEFAULT_CONCURRENCY` | `10` | Instance default | | `SIDECAR_DEFAULT_RATE_LIMIT` | `100/min` | Instance default | | `SIDECAR_USER_MAX_RESPONSE_SIZE` | `5242880` (5MB) | User default | | `SIDECAR_USER_CONCURRENCY` | `3` | User default | | `SIDECAR_USER_RATE_LIMIT` | `30/min` | User default | | `SIDECAR_USER_ALLOWED_CAPABILITIES` | `["fs.search","fs.read","clipboard.get"]` | Admin-controlled allowlist | --- ## Sidecar Author Contract ### Minimum Viable Sidecar (Python) ```python from flask import Flask, request, jsonify import requests, os, threading, time app = Flask(__name__) KERNEL = os.environ['ARMATURE_URL'] TOKEN = os.environ['ARMATURE_TOKEN'] def register(): requests.post(f'{KERNEL}/api/v1/sidecar/register', json={ 'package_id': 'my-sidecar', 'endpoint': f'http://localhost:8080', 'capabilities': [{ 'name': 'echo', 'description': 'Echo input back', 'input_schema': { 'type': 'object', 'properties': {'text': {'type': 'string'}}, 'required': ['text'] }, 'timeout_seconds': 10 }] }, headers={'Authorization': f'Bearer {TOKEN}'}) def heartbeat(): while True: time.sleep(10) r = requests.post(f'{KERNEL}/api/v1/sidecar/heartbeat', json={'stats': {}}, headers={'Authorization': f'Bearer {TOKEN}'}) if r.status_code == 404: register() @app.route('/api/v1/exec/echo', methods=['POST']) def exec_echo(): return jsonify({ 'output': {'text': request.json['input']['text']}, 'error': None }) @app.route('/api/v1/health') def health(): return jsonify({'status': 'ok'}) register() threading.Thread(target=heartbeat, daemon=True).start() app.run(port=8080) ``` Four things: register, heartbeat, `/exec/`, `/health`. That's the contract. --- ## Design Decisions | Decision | Rationale | |----------|-----------| | Connect-inward | Eliminates K8s RBAC, service mesh, DNS discovery. Works on any deployment target. | | Two auth modes | Token for simple. mTLS for production. Same internal identity. | | Cluster registry pattern | Proven heartbeat/sweep. No new coordination mechanism. | | HTTP/JSON, no gRPC | KISS. 4-endpoint contract. Every language has HTTP. | | Kernel proxies all calls | Auth, rate limits, audit, timeouts at single control point. | | Package-scoped access | Compromised sidecar can't read other packages. | | User sidecars as v0.14.5 | Prove instance contract first (v0.14.0–v0.14.4), then extend to users. Same mechanism, new scope. | | Three-layer user RBAC | Admin controls who (permission), what (capability allowlist), user controls visibility (private/shared). Defense in depth. | | Capability allowlist for user sidecars | Admin decides what user processes can expose. `terminal.exec` is a deliberate escalation. | | Private-by-default user capabilities | User sidecar can't affect other users unless explicitly shared. | | Shared caps run with caller's perms | Prevents privilege escalation through shared capabilities. | | Reference instance sidecar: embedding | Simpler than LLM. Proves contract. Completes local RAG story. | | Reference user sidecar: file bridge | Most compelling user sidecar demo. "Find a file on my laptop and save it to notes" is visceral. | | Lower resource limits for user sidecars | User sidecars are less trusted (running on personal devices). Lower defaults, admin can't be overridden by users. | | Separate from polish (v0.15.x) | Sidecar is security-critical infrastructure. Polish is quality-of-life. Don't mix. | --- ## Future Considerations - **Streaming responses.** LLM sidecar needs token streaming. SSE or chunked transfer. Deferred — batch covers embedding, classification, transcoding. - **SDK libraries.** Thin wrappers in Python, Go, Node, Rust for registration + heartbeat + parsing. Convenience, not requirement. - **Auto-scaling.** Multiple instances of same sidecar, kernel load-balances. No schema change needed — registry supports multiple rows per package. - **GPU detection.** Kernel capability for manifest negotiation. - **Sidecar-to-sidecar.** Direct calls for performance pipelines. Currently goes through kernel. - **User sidecar desktop app.** Electron/Tauri tray app that wraps the user-bridge agent with a GUI for capability management and connection status. Post-1.0.