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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
# 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 6–8. 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.
|
||||||
Reference in New Issue
Block a user