Files
platform/ecosystem.config.cjs
T
pastilhasandClaude Opus 5 edf26323da memos sidecar
wraps the self-hosted memos instance, same shape as transmission and slskd. no
schema change was needed: service_connections already says `service` is text
because "adding a service should not be a schema change", and memos is the
one-instance-per-owner case that table was built for.

the sidecar holds the url and the personal access token; the platform side is
16 lines of createSidecarProxy and holds neither.

/_api/* is a pass-through onto the instance's own /api/v1 rather than a
hand-written wrapper per endpoint — memos generates its rest api from protobufs
and it moves between minor versions, so re-describing it here would be a second
thing to keep in sync. the allow-list is the one piece of policy, and it keeps
this from being a general ssrf hop. auth routes are excluded: signin/signout
would mint sessions on the instance, and this authenticates with a stored token.

probing is two calls on purpose. /healthz answers unauthenticated, so a bad url
is distinguishable from a bad token — memos returns 200 and an empty list for
unauthenticated reads rather than 401, so "the list came back" proves nothing.

verified against the live container: unconfigured reports not-connected, a bad
token is rejected WITH the reason and nothing is stored, and the platform mount
401s without a session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 03:24:15 +00:00

141 lines
5.1 KiB
JavaScript

module.exports = {
apps: [
{
name: 'officer',
script: 'bun',
args: 'start',
watch: false,
},
// The Anthropic credential proxy. Despite the old name (`officer-claude`) this process does NOT
// run agents — it holds the proxy secret and forwards to api.anthropic.com. The process that runs
// agents is `officer-agent` below.
{
name: 'officer-anthropic-proxy',
script: 'bun',
args: 'run src/servers/sidecar/claude/index.ts',
watch: false,
},
// The process that actually runs `claude`. It used to be spawned on demand by the main server,
// which made every agent session a grandchild of `officer` and killed it on every restart. As a PM2
// peer it survives them. It resolves the owner from the database and the proxy secret from the
// proxy's state file, so it needs nothing from `officer` in order to start.
{
name: 'officer-agent',
script: 'bun',
args: 'run src/servers/sidecar/claude/user-instance.ts',
watch: false,
},
{
name: 'officer-opencode',
script: 'bun',
args: 'run src/servers/sidecar/opencode/index.ts',
watch: false,
},
{
name: 'officer-email',
script: 'bun',
args: 'run src/servers/sidecar/email/index.ts',
watch: false,
},
// The only sidecar run by `node` rather than `bun`, and the only one that is not TypeScript: node-pty
// is a native addon. It also does not use sidecar/connect.ts, and carries its own copy of the
// reconnect loop.
{
name: 'officer-pty',
script: 'node',
args: 'src/servers/sidecar/pty/index.mjs',
watch: false,
},
{
name: 'officer-vnc',
script: 'bun',
args: 'run src/servers/sidecar/vnc/index.ts',
watch: false,
},
{
name: 'officer-music',
script: 'bun',
args: 'run src/servers/sidecar/music/index.ts',
watch: false,
},
{
name: 'officer-vault',
script: 'bun',
args: 'run src/servers/sidecar/vault/index.ts',
watch: false,
},
{
name: 'officer-slskd',
script: 'bun',
args: 'run src/servers/sidecar/slskd/index.ts',
watch: false,
},
{
name: 'officer-headscale',
script: 'bun',
args: 'run src/servers/sidecar/headscale/index.ts',
watch: false,
},
{
name: 'officer-transmission',
script: 'bun',
args: 'run src/servers/sidecar/transmission/index.ts',
watch: false,
},
// The books. Wraps a self-hosted InvoiceShelf. Instances, their Sanctum tokens and the company each one
// is pinned to are set by the owner from /invoices/settings and stored encrypted in
// `invoiceshelf_accounts` — read here, never from the environment, because Bun auto-loads `.env` into
// every process in this directory and `officer` would hold the token too.
{
name: 'officer-invoiceshelf',
script: 'bun',
args: 'run src/servers/sidecar/invoiceshelf/index.ts',
watch: false,
},
// Notes. Wraps a self-hosted Memos. The instance URL and its personal access token are set by the
// owner from the UI and stored in `service_connections` — read here, never from the environment.
{
name: 'officer-memos',
script: 'bun',
args: 'run src/servers/sidecar/memos/index.ts',
watch: false,
},
// Calendar and contacts. Supervises Radicale (CalDAV/CardDAV) on a loopback port and owns the
// collections under DATA_PATH/dav. Two doors: /dav for phones (DAVx5, iOS, Thunderbird — HTTP Basic
// against a scoped app password) and /api/caldav for Officer's own UI. The protocol is Radicale's;
// the platform authenticates and forwards. See docs/nextcloud-replacement.md.
{
name: 'officer-caldav',
script: 'bun',
args: 'run src/servers/sidecar/caldav/index.ts',
watch: false,
},
// The photo library. Wraps a self-hosted Immich. The instance and its key are set by the owner from
// /photos/settings and stored encrypted in `photos_config` — read here, never from the environment,
// because Bun auto-loads `.env` into every process in this directory and `officer` would hold it too.
{
name: 'officer-photos',
script: 'bun',
args: 'run src/servers/sidecar/photos/index.ts',
watch: false,
},
// The bitcoin wallet. Holds seed material (sealed under an owner passphrase) and node credentials, so
// it is the one sidecar whose restart has a security-relevant side effect: every wallet relocks.
// The one place anything leaves this machine to tell the owner something: push (APNs + FCM) and the
// Discord webhook, behind one interface. A sidecar rather than platform code because the producers
// are spread across sidecars, and a platform-owned notifier would make every one of them call back in.
{
name: 'officer-notify',
script: 'bun',
args: 'run src/servers/sidecar/notify/index.ts',
watch: false,
},
{
name: 'officer-wallet',
script: 'bun',
args: 'run src/servers/sidecar/wallet/index.ts',
watch: false,
},
],
};