# 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: "<owner>"`.
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.
