Files
platform/docs/sidecar-topology.md
T
pastilhasandClaude Opus 5 1e6b01cd21 docs: the target sidecar topology
The owner's design, agreed in conversation and written down before it evaporates — this
session started by recovering a design that had been lost with its chat.

The premise is what makes it small: the tailnet becomes the perimeter and devices are
admitted by hand, so there is no authentication work inside it. Sidecars keep trusting
their caller; the boundary moves from loopback to the tailnet.

Six points: ecosystem is the source of truth and PM2 starts everything (peers, never
children — that was the tree-kill bug); one fixed port per sidecar; the platform reads the
table rather than being told at runtime; .env toggles features; NPM maps /api/<feature>
straight at the sidecar; the platform keeps auth and the frontend's own state and comes off
the data path.

What it buys is deletion: connect.ts's dial-and-register loop, every *:server port
announcement, and most of sidecar-registry.ts.

Also records what was considered and dropped — token distribution to every sidecar, a
public auth sidecar with JWKS, and the idea that Origin headers are a security boundary —
so none of it gets re-argued from scratch.

Nothing is built. Music is the first migration, end to end, with officer's proxy kept as a
fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:08:08 +00:00

84 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sidecar topology — the target shape
**Status:** agreed design, 2026-07-30. Nothing built yet. Owner's design; this file is the record.
Not to be confused with `SIDECAR_ARCHITECTURE.md` at the workspace root, which is the *audit* of the
current state (what's misplaced, and where). This is where it's going.
## The premise that makes it simple
**The tailnet is the perimeter.** Everything moves behind Tailscale and devices are admitted by hand —
friends and family included. Authentication *inside* that boundary is solving a problem we don't have, so
this design has no token work in it at all. Sidecars trust their caller exactly as they do today; the trust
boundary just moves from loopback to the tailnet.
Until that lands, things stay exposed as they are now. The security model is deliberately interim.
## The design
1. **`ecosystem.config.cjs` is the source of truth.** PM2 starts every sidecar. They stay *peers* of
`officer` — never children. This is not a style preference: officer used to spawn the agent itself,
which made it a grandchild, and PM2's tree-kill took the owner's chat session down on every restart.
That was the worst thing about working on the platform, and it is fixed. Don't reintroduce it.
2. **Fixed port per sidecar**, declared once in that table. No random ports.
3. **The platform reads the table** instead of being told at runtime.
4. **`.env` enables and disables** features, i.e. sidecars. Absent sidecar = feature 503s cleanly, which is
already how music, slskd and vault behave, so "disabled" is a tested state rather than a new one.
5. **NPM maps `/api/<feature>` straight to the sidecar** (`/api/music` → the music sidecar). Added by hand
for now; automating it via the NPM API is the last step, not the first — no point automating a config
that hasn't settled.
6. **The platform keeps auth, and the frontend's own concerns** — dashboards, persisted layouts, routing.
It comes off the data path entirely.
## What this deletes
The point of the exercise, and the reason it's worth doing:
- `sidecar/connect.ts` — the dial-out-and-register loop, plus its per-sidecar reconnect backoff copies
- every port announcement: `music:server`, `slskd:server`, `vault:server`, `opencode:server`, `vnc:started`
- most of `sidecar-registry.ts` — discovery, the pending-command map, capability lookup
- officer's proxying for anything that isn't auth or layout state
## Migration: music first, end to end
One sidecar all the way through before touching the other nine. Music because it's the highest-traffic,
the least dangerous if it breaks, it already listens on a port, and it already has an app of its own.
Keep officer's existing proxy in place as a fallback during the change, so a bad step is a revert rather
than an outage. Vault last — it's off-limits by standing instruction and is the least urgent anyway.
## Open questions
- **Ecosystem file, or a shared table?** The platform parsing `ecosystem.config.cjs` couples the app to
PM2 being the thing that started it, which matters for `DOCKERIZATION_PLAN.md` and for `bun dev`.
The alternative is one plain TypeScript table (name, script, port, capability, enabled) that
`ecosystem.config.cjs` generates its `apps:` array from and the platform imports directly — same single
source of truth, no supervisor coupling. **Recommended, not yet decided.**
- **Where do the things that are neither auth nor layout go?** The job/queue engine, the capabilities/items
store, the chat session list, the file browser. Each needs a named home or officer quietly stays fat.
- **Does the registration socket survive?** Not needed for discovery once ports are static. Possibly worth
keeping for liveness — or replace it with a health probe on the known port.
- **Capabilities.** Today a sidecar announces `capabilities: ['music']` and officer looks up by capability,
not by name — which is what let the agent's PM2 name change from `officer-claude` to `officer-agent`
without touching a caller. In a static table it collapses to a column. Keep it; it's cheap.
- **The non-owner account class may become dead weight.** `NON_OWNER_PATHS`, the music-only account
scoping and the super-admin backstop exist because music is public. If friends and family join the
tailnet instead, that whole class — and a chunk of `origin-validation.ts` — can go.
## Considered and dropped
Recorded so they aren't re-litigated:
- **Platform spawns the sidecars.** Rejected — that's the tree-kill bug again. PM2 starts them; the
platform only reads the topology.
- **Platform mints a token, tells every sidecar it's valid, apps then call sidecars directly.** This was
the original points 68. Dropped with the tailnet decision. Worth knowing *why* it was weak even on its
own terms: it replicates session state across ten processes, and breaks whenever one restarts, is down
at login, or has to be told about a logout.
- **A dedicated public auth sidecar** issuing short-lived asymmetric tokens, with sidecars verifying via
JWKS. Correct for a public deployment, unnecessary for a private tailnet. If the perimeter ever opens
up again, this is the design to come back to.
- **Origin headers as the security boundary.** They aren't. `Origin: officer://<hex>` is client-chosen —
trivially forged outside a browser, and the token is extractable from any shipped app binary. Origin
scoping stays useful as defence in depth; it was never authentication.