Docs expand v0.14.x sidecar tier + v0.15.x polish in roadmap
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

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>
This commit is contained in:
2026-04-03 17:51:18 +00:00
parent 446edb8333
commit 1113ddb65e
3 changed files with 1110 additions and 4 deletions

View File

@@ -0,0 +1,922 @@
# 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.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.

157
docs/ROADMAP-v010x-v100.md Normal file
View File

@@ -0,0 +1,157 @@
# ROADMAP — v0.10.x → v1.0.0 (Final)
## Full Roadmap
| Series | Title | Versions | Design Doc |
|--------|-------|----------|------------|
| v0.9.x | Multi-Surface + Workflow Redesign | 10 | — |
| **v0.10.x** | **Panels + Composable Layout** | 5 | `DESIGN-panels.md` |
| **v0.11.x** | **Notes Reference Extension** | 11 | `DESIGN-notes-v011x.md` |
| **v0.12.x** | **Chat Reference Extension** | 9 | `DESIGN-chat-v012x.md` |
| **v0.13.x** | **Reference Libraries + Extensions** | 9 | — |
| **v0.14.x** | **Sidecar Tier** | 6 | `DESIGN-sidecar-v014x.md` |
| **v0.15.x** | **Polish + Stability** | 3 | — |
| **v1.0.0** | **Stable Release** | — | — |
**Total: ~53 versions from v0.10.0 to v1.0.0**
---
## v0.10.x — Panels + Composable Layout
| Version | Title |
|---------|-------|
| v0.10.0 | Panel Manifest + Lifecycle |
| v0.10.1 | FloatingPanel Primitive |
| v0.10.2 | Docked Panels + Mode Transitions |
| v0.10.3 | Panel Communication Patterns |
| v0.10.4 | Reference Panel: Notes (basic) |
---
## v0.11.x — Notes Reference Extension
| Version | Title |
|---------|-------|
| v0.11.0 | UI/UX Foundation |
| v0.11.1 | Deep Folders + Navigation |
| v0.11.2 | Wikilinks + Backlinks |
| v0.11.3 | Live Preview + Rich Editing |
| v0.11.4 | Note Sharing + Permissions |
| v0.11.5 | Graph + Outline Hardening |
| v0.11.6 | Quick Switcher + Commands |
| v0.11.7 | Daily Notes + Templates |
| v0.11.8 | Transclusion + Embeds |
| v0.11.9 | Composability: Slots + Actions |
| v0.11.10 | Panel Enhancement + Quality Gate |
---
## v0.12.x — Chat Reference Extension
| Version | Title |
|---------|-------|
| v0.12.0 | UI/UX Foundation |
| v0.12.1 | Conversation Folders + Attributes |
| v0.12.2 | Reactions + Threads + Pins |
| v0.12.3 | Rich Compose + Attachments |
| v0.12.4 | @Mentions + Notifications |
| v0.12.5 | Link Previews + Message Formatting |
| v0.12.6 | Conversation Themes + Personality |
| v0.12.7 | Composability: Slots + Actions |
| v0.12.8 | Panels + Quality Gate |
---
## v0.13.x — Reference Libraries + Extensions
| Version | Title |
|---------|-------|
| v0.13.0 | `vector-store` Library |
| v0.13.1 | `llm-bridge` Core Library |
| v0.13.2 | `llm-bridge` → Chat: Multi-Persona + Tool Meta-Tool |
| v0.13.3 | `llm-bridge` → Notes: AI Toolbar Actions |
| v0.13.4 | `file-share` Extension |
| v0.13.5 | `code-workspace` Extension |
| v0.13.6 | `image-gen` + `image-edit` Extensions |
| v0.13.7 | Tool Meta-Tool Hardening + Scoping |
| v0.13.8 | Integration Quality Gate |
---
## v0.14.x — Sidecar Tier
Connect-inward model. Instance sidecars (shared infrastructure) and
user sidecars (personal local tools).
| Version | Title |
|---------|-------|
| v0.14.0 | Sidecar Registry + Auth |
| v0.14.1 | Capability Registration + Execution |
| v0.14.2 | Kernel API Access + Event Bus |
| v0.14.3 | Manifest Integration + Admin Polish |
| v0.14.4 | Reference Sidecar (`armature-embed`) + Instance Gate |
| v0.14.5 | User Sidecars + Reference (`user-bridge`) + User Gate |
---
## v0.15.x — Polish + Stability
| Version | Title |
|---------|-------|
| v0.15.0 | Native Dialog Audit |
| v0.15.1 | Versioned Migrations + `armature migrate` CLI |
| v0.15.2 | Pre-1.0 Schema Freeze + Upgrade Path Validation |
---
## v1.0.0 Gate Criteria
**Reference extensions:**
- Notes shipped (all v0.11.x) with UI quality review
- Chat shipped (all v0.12.x) with UI quality review
- At least 3 reference extensions beyond notes/chat
**Composability:**
- `llm-bridge` extends both notes and chat through slots/actions
- Tool meta-tool: AI uses 3+ extension actions in a single turn
- At least 2 panels consumed cross-package
- At least 2 slot contributions per host surface
**Sidecar:**
- Instance sidecar (`armature-embed`) deployed and functional
- User sidecar (`user-bridge`) functional on macOS/Linux/Windows
- Token + mTLS auth both tested
- User sidecar RBAC enforced (permission, capability allowlist, scoping)
- Tool meta-tool includes user sidecar capabilities per-user
**Infrastructure:**
- Admin safety rails validated
- Multi-persona context archetypes demonstrated
- Sharing functional (notes + conversations)
- Headless E2E green on PG + SQLite
- All kernel Starlark modules documented
- All API routes covered by OpenAPI spec
- `armature migrate` CLI functional
- Upgrade path tested v0.8.0 → v1.0.0
- Single-binary + Docker + K8s deployment paths documented
- Monitoring dashboard with kernel + sidecar metrics
---
## Design Decisions Log
| Decision | Rationale |
|----------|-----------|
| Panels as kernel primitive | Z-index, drag, layout are kernel concerns |
| UI/UX-first for reference extensions | Every feature builds on visual foundation |
| Notes → Chat → Libraries → Sidecar | Each builds on proven patterns from previous |
| Chat human-to-human first | AI is llm-bridge's job |
| Folder attributes as extension bridge | Zero coupling between chat and llm-bridge |
| Action registry as tool registry | Installed extensions = AI capabilities |
| 6-layer prompt architecture | Admin safety not overridable |
| Sidecar connect-inward | No K8s RBAC, no service mesh, no DNS discovery |
| Sidecar HTTP/JSON, not gRPC | KISS. 4-endpoint contract |
| User sidecars in v0.14.5 | Prove instance contract first, extend to users with same mechanism |
| Three-layer user sidecar RBAC | Admin controls who + what, user controls visibility |
| Separate polish (v0.15.x) | Security-critical infra and quality-of-life shouldn't share focus |