Files
platform/docs/sidecar-topology.md
T
pastilhasandClaude Opus 5 829b034d13 docs: fold the workspace-root docs into the repo, with honest status
The root held four Markdown files that were not in any git repo and were being read as
current. CLAUDE_SIDECAR_ISOLATION.md is the one that prompted this: it describes, in the
present tense, an agent that dies whenever officer restarts. That was true when it was
written and has not been true for two days.

Rather than delete analysis that version control was not holding, the two substantial ones
moved into platform/docs/ with headers that say what has since happened:

- claude-sidecar-isolation.md — stages 0-2 are done and running (R1, R2, R4, R5 all
  satisfied); stages 3-5 are the only live part.
- sidecar-audit-2026-07.md — a snapshot audit, largely executed. Email, pty, music and vnc
  have been done since; claude, opencode and the cross-cutting notes are still open. The
  vault stays off-limits.

MUSIC_IMAGES_SPEC.md is deleted outright: the sidecar serves /image and /poster, so the
spec is the feature. The workspace root now holds one Markdown file, CLAUDE.md, which is
where cross-cutting operational reality belongs.

Also corrected, in the same pass:
- root CLAUDE.md listed email as "the big one, and untouched" and pty stage 4 as
  outstanding; both are done. It now names what actually remains (claude stages 3-5, the
  terminal orphan leak) and what landed.
- docs/sidecar-topology.md said "nothing built yet". Two sidecars now serve their own
  transport; what has NOT happened is the part the document is about — fixed ports, the
  shared table, .env toggles — so it says exactly that rather than implying the design is
  underway. Migration order updated: pty and email went first, not music.
- docs/navigation-audit.md is cited as authoritative but still planned work on Projects,
  a feature since deleted. A status header marks H3 void, H1/H2 done and H4 the one open
  item, so nobody follows it into a directory that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:45:26 +00:00

6.0 KiB
Raw Blame History

Sidecar topology — the target shape

Status: agreed design, 2026-07-30. Owner's design; this file is the record.

Progress. Points 5 and 6 are partly real already, ahead of the rest: pty and email now serve their own HTTP/WS listeners and the platform is a byte relay and a proxy onto them, with no command vocabulary left between them. Every HTTP sidecar shares one createSidecarProxy factory. What has NOT happened is the part this document is actually about — fixed ports, the platform reading a table instead of being told at runtime, and .env feature toggles. Ports are still ephemeral and still announced.

Not to be confused with sidecar-audit-2026-07.md, 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, pty:server, email:server, wallet:server, headscale:server, … and 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 order

The original plan was music-first. In practice pty and email went first, because both had platform code that had to move regardless, and both are now the worked examples of the end state: the sidecar owns its transport, the platform authenticates and forwards.

What remains is the topology work itself — fixed ports, the shared table, .env toggles — which touches every sidecar at once rather than one at a time. Vault last: off-limits by standing instruction, and 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 containerising this later 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.