This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/DESIGN-sidecar-v014x.md
Jeffrey Smith 1113ddb65e
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m47s
CI/CD / test-sqlite (pull_request) Successful in 3m11s
CI/CD / build-and-deploy (pull_request) Successful in 1m26s
Docs expand v0.14.x sidecar tier + v0.15.x polish in roadmap
ROADMAP.md: v0.14.x expanded from 3→6 versions (sidecar registry,
capabilities, event bus, manifest, reference sidecar, user sidecars).
v0.15.x split out as polish+stability (dialog audit, migration CLI,
schema freeze). v1.0.0 gate adds sidecar criteria. Design decisions
log updated with sidecar rationale.

New docs: DESIGN-sidecar-v014x.md (full sidecar design), ROADMAP-v010x-v100.md
(detailed version plan through 1.0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:51:18 +00:00

33 KiB
Raw Blame History

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

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:

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.

# 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)

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:

{
  "input": { ... },
  "context": {
    "user_id": "...",
    "package_id": "...",
    "request_id": "..."
  }
}

Returns:

{
  "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:

{
  "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):

{
  "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:

{
  "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:

{
  "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:

# 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)

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

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

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

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

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