Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
33 KiB
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
.pkgfile. 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).
- Admin generates an instance sidecar token:
armature sidecar token create --package ml-inferenceUser generates a personal sidecar token (from settings UI or CLI):armature sidecar token create --personal - 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>". - Sidecar starts with the token as env:
ARMATURE_TOKEN=ey... - Sidecar calls
POST /api/v1/sidecar/register. - Kernel validates JWT, creates registry entry with appropriate scope.
Mode B: mTLS
For production deployments with certificate infrastructure.
- Certificate CN encodes identity:
Instance:
sidecar:ml-inferenceUser:sidecar-user:jeff:local-bridge - Kernel's
MTLSNativeProviderparses 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/registerwith capability manifest. Kernel creates/updates registry row. - Heartbeat:
POST /api/v1/sidecar/heartbeatevery 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/deregisteron 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:
- Instance-level actions from
sw.actions.list()→ available to all. - Instance sidecar capabilities → available to all.
- 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
- Compromised binary → can read/write own package data, call exported functions. Mitigation: package-scoped access, audit logging, token revocation.
- Token theft → impersonate sidecar. Mitigation: package-scoped tokens, mTLS eliminates theft, revocable.
- SSRF via sidecar.call → Mitigation: endpoints validated at registration, no runtime changes.
- Resource exhaustion → Mitigation: per-sidecar concurrency and rate limits, timeout enforcement, response size limits.
- Privilege escalation via lib.require → Mitigation: calls attributed to sidecar identity, target permission checks apply.
User Sidecars (Additional Concerns)
-
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).
-
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.
-
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.
-
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
scopeanduser_idcolumns. - Registration endpoint with JWT token validation.
- Heartbeat + sweep integration.
- Token generation (instance tokens via admin, user tokens via settings).
SIDECAR_AUTH_MODEconfig:tokenormtls.- mTLS auth:
sidecar:andsidecar-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.
sidecarStarlark 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.callpermission.- 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.sidecarSDK module.
v0.14.3 — Manifest Integration + Admin Polish
Goal: Sidecar packages are first-class in install/admin flow.
tier: "sidecar"manifest support.sidecarmanifest 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.sidecarfrontend 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_personalpermission (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_idbinding. - 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-bridgeincludes 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_personalcannot 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-bridgereference 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.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.