PORT is read in one place, and it has no default

officer-url.mjs is now the only file in the tree that touches process.env.PORT.
Twenty-two others read it and supplied their own default; a value with
twenty-two sources is not configuration, it is twenty-two things to keep in sync,
and they had already drifted three ways.

It throws when PORT is unset rather than guessing. A default only covers the case
where .env was never loaded — which is not a machine anyone wants running,
because POSTGRES_URL is missing in the same breath. What the default bought was a
process that starts, binds somewhere unexpected, and fails later for a reason
that does not name the cause. Same posture as jwt.ts with JWT_SECRET.

It is .mjs, not .ts, and that is the whole reason this could be one file. pm2
launches officer-pty with node (ecosystem.config.cjs) and everything else with
bun; node cannot import TypeScript, so a .ts module would have left the pty
sidecar holding the only surviving copy of the default — precisely the thing
being removed. allowJs is already on, so the TS callers still get types. Verified
both runtimes import it, and that PUBLIC_URL-style overrides still work.

It also exports API_URL and OFFICER_API_URL, because nineteen sidecars were
independently building `ws://127.0.0.1:${PORT}` and two more were building the
http form. Those are one listener described in two protocols — no sidecar binds
anything — so they belong beside the port rather than being rediscovered per
file.

server.tsx now takes PORT as a number, so Number(PORT) at the serve site is gone.

Not typechecked (empty node_modules, frozen installs). Every edited file parses
under `bun build --no-bundle`; node and bun both load the new module; the unset
and non-numeric paths were exercised; the pm2 profile still loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 23:43:39 +00:00
co-authored by Claude Opus 5
parent e72cae4830
commit 3c7f52ab77
23 changed files with 80 additions and 26 deletions
+2 -2
View File
@@ -4,6 +4,7 @@ import { serve } from 'bun';
import { honoServer, PROTECTED_API_PREFIXES, UNPROTECTED_API_PREFIXES } from './servers/hono'; import { honoServer, PROTECTED_API_PREFIXES, UNPROTECTED_API_PREFIXES } from './servers/hono';
import { assertCapabilityTotality } from './servers/capabilities/totality'; import { assertCapabilityTotality } from './servers/capabilities/totality';
import { assertInstallLayout } from './servers/data-path'; import { assertInstallLayout } from './servers/data-path';
import { PORT } from './servers/officer-url.mjs';
import { assertSecretsClosed } from './servers/os-user'; import { assertSecretsClosed } from './servers/os-user';
import { resolveHomeDir } from './servers/user-home'; import { resolveHomeDir } from './servers/user-home';
import { resolveAuthToken } from './servers/auth-token'; import { resolveAuthToken } from './servers/auth-token';
@@ -23,7 +24,6 @@ import './servers/api/chat/opencode/sidecar-server'; // subscribe to the opencod
import type { SidecarRegistration } from './servers/sidecar/registration-protocol'; import type { SidecarRegistration } from './servers/sidecar/registration-protocol';
import { toShellUsername } from './servers/data-path'; import { toShellUsername } from './servers/data-path';
const { PORT = '9000' } = process.env;
// Build static file routes from public/ // Build static file routes from public/
const publicRoutes: Record<string, (req: Request) => Response> = {}; const publicRoutes: Record<string, (req: Request) => Response> = {};
@@ -250,7 +250,7 @@ async function upgradeWs(
} }
const server = serve({ const server = serve({
port: Number(PORT), port: PORT,
idleTimeout: 60, idleTimeout: 60,
maxRequestBodySize: 1024 * 1024 * 1024 * 50, // 50 GB maxRequestBodySize: 1024 * 1024 * 1024 * 50, // 50 GB
routes: { routes: {
+2 -1
View File
@@ -9,6 +9,7 @@ import {
} from 'officerdb'; } from 'officerdb';
import { introduceAgentPanel } from '../agent-handoff/deliver'; import { introduceAgentPanel } from '../agent-handoff/deliver';
import { logger } from './logger'; import { logger } from './logger';
import { OFFICER_API_URL } from '../../officer-url.mjs';
/** /**
* The browser's half of the address book: name a panel, look up what a panel is, rename, remove. * The browser's half of the address book: name a panel, look up what a panel is, rename, remove.
@@ -23,7 +24,7 @@ import { logger } from './logger';
/** A name has to survive being typed into a prompt and into a shell, so keep it boring. */ /** A name has to survive being typed into a prompt and into a shell, so keep it boring. */
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$|^[a-z0-9]$/; const NAME_RE = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$|^[a-z0-9]$/;
const API_ORIGIN = `http://127.0.0.1:${process.env.PORT ?? '9000'}`; const API_ORIGIN = OFFICER_API_URL;
export function registerAgentPanelRoutes(router: Hono<any>): void { export function registerAgentPanelRoutes(router: Hono<any>): void {
// GET /chat/agent-panels?dashboardId=… — the address book for one dashboard. // GET /chat/agent-panels?dashboardId=… — the address book for one dashboard.
+3 -2
View File
@@ -1,6 +1,7 @@
import { sign } from '@@/jwt'; import { sign } from '@@/jwt';
import { PORT, OFFICER_API_URL } from '../../officer-url.mjs';
const { PORT = '9000', PUBLIC_URL } = process.env; const { PUBLIC_URL } = process.env;
const PUBLIC_HOST = (() => { const PUBLIC_HOST = (() => {
try { try {
@@ -24,7 +25,7 @@ type TaskApiUser = { id: number; email: string; username?: string | null };
export async function buildTaskApiEnv(user: TaskApiUser): Promise<Record<string, string>> { export async function buildTaskApiEnv(user: TaskApiUser): Promise<Record<string, string>> {
const token = await sign({ id: user.id, email: user.email, username: user.username ?? '' }, '12h'); const token = await sign({ id: user.id, email: user.email, username: user.username ?? '' }, '12h');
return { return {
OFFICER_API_URL: `http://127.0.0.1:${PORT}`, OFFICER_API_URL,
OFFICER_API_HOST: PUBLIC_HOST ?? `127.0.0.1:${PORT}`, OFFICER_API_HOST: PUBLIC_HOST ?? `127.0.0.1:${PORT}`,
OFFICER_AUTH_TOKEN: token, OFFICER_AUTH_TOKEN: token,
}; };
+54
View File
@@ -0,0 +1,54 @@
// Where the app is, and the only place that knows.
//
// ── Why this file is .mjs ──
//
// Every process is bun except `officer-pty`, which pm2 launches with node (see
// ecosystem.config.cjs). Node cannot import TypeScript, so a .ts module here would have left the pty
// sidecar with its own copy — which is exactly the thing this file exists to end. Plain JS is
// importable by both, and `allowJs` in tsconfig.json means the TS callers still get types.
//
// ── Why there is no default ──
//
// There were twenty-two, and they disagreed: 5000 in the app and nineteen sidecars, 9010 in
// user-instance.ts, 9000 in .env.example. Each was defensible where it was written and none was
// visible from the others.
//
// A default is a guess at a value that .env always supplies. The one case it covers — PORT genuinely
// unset — is not a machine anyone wants running: it means .env was not loaded, so POSTGRES_URL is
// missing too and nothing works anyway. What a default buys there is a process that starts, binds
// somewhere unexpected and fails later for a reason that does not name the cause.
//
// So: no default anywhere, and this throws. Same posture as jwt.ts with JWT_SECRET.
const raw = process.env.PORT;
if (!raw) {
throw new Error(
[
'PORT is not set.',
'',
'Officer reads it from .env, which bun auto-loads from the working directory. If this is a pm2',
'process, the `cwd` pin in ecosystem.profile.cjs is what puts it there — a PORT this empty',
'usually means .env was never loaded at all, and POSTGRES_URL is missing too.',
'',
'Set PORT in .env (officer-setup writes 9000), or start from the repo root.',
].join('\n'),
);
}
const parsed = Number(raw);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
throw new Error(`PORT must be an integer between 1 and 65535, but it is ${JSON.stringify(raw)}`);
}
/** The port the app binds. Everything else addresses it; nothing else binds. */
export const PORT = parsed;
// The app serves HTTP and WebSocket on ONE listener, so these are the same port in two protocols.
// Sidecars append their own path — `/api/sidecar/register` for the registration socket.
//
// The env overrides are kept because they are explicit escape hatches rather than defaults: nothing
// sets either today, and a value that is present is a deliberate act rather than a guess.
export const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${PORT}`;
export const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${PORT}`;
+1 -1
View File
@@ -3,6 +3,7 @@ import { createSidecarConnector } from '../connect';
import { startRadicale, davPaths } from './radicale'; import { startRadicale, davPaths } from './radicale';
import { listCollections, listEvents, listContacts } from './collections'; import { listCollections, listEvents, listContacts } from './collections';
import { DATA_PATH } from '../../data-path'; import { DATA_PATH } from '../../data-path';
import { API_URL } from '../../officer-url.mjs';
// The officer-caldav sidecar. Owns the whole CalDAV/CardDAV contract: it supervises Radicale, owns the // The officer-caldav sidecar. Owns the whole CalDAV/CardDAV contract: it supervises Radicale, owns the
// collection storage under DATA_PATH/dav, and exposes two very different doors. // collection storage under DATA_PATH/dav, and exposes two very different doors.
@@ -23,7 +24,6 @@ import { DATA_PATH } from '../../data-path';
// machine-facing interface. Same split officer-email already uses. // machine-facing interface. Same split officer-email already uses.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -2,8 +2,8 @@ import type { SidecarCommand, SidecarEvent } from '../protocol';
import { loadState, flushAndSave, acquireLock, releaseLock, getState } from './state'; import { loadState, flushAndSave, acquireLock, releaseLock, getState } from './state';
import { startAnthropicProxy, getProxySecret, ensureProxySecret } from './proxy'; import { startAnthropicProxy, getProxySecret, ensureProxySecret } from './proxy';
import { createSidecarConnector } from '../connect'; import { createSidecarConnector } from '../connect';
import { API_URL } from '../../officer-url.mjs';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// ── Startup ── // ── Startup ──
+1 -3
View File
@@ -18,6 +18,7 @@ import { createSidecarConnector } from '../connect';
import { sign } from '../../jwt'; import { sign } from '../../jwt';
import { getUserByEmail, getOwnerUser, getEmailAccounts } from 'officerdb'; import { getUserByEmail, getOwnerUser, getEmailAccounts } from 'officerdb';
import { DATA_PATH } from '../../data-path'; import { DATA_PATH } from '../../data-path';
import { API_URL, OFFICER_API_URL } from '../../officer-url.mjs';
// PM2 starts this sidecar with no user in its env, so resolve the owner from the database rather than // PM2 starts this sidecar with no user in its env, so resolve the owner from the database rather than
// being told who to run as by the main server — one less thing that has to come from `officer` before // being told who to run as by the main server — one less thing that has to come from `officer` before
@@ -70,9 +71,6 @@ const email = dbUser.email;
// 9000 everywhere now, matching server.tsx and .env.example. 9010 was the old installer's default // 9000 everywhere now, matching server.tsx and .env.example. 9010 was the old installer's default
// (scripts/setup-old/setup.sh), which is why it was the only one of the three that ever matched a real // (scripts/setup-old/setup.sh), which is why it was the only one of the three that ever matched a real
// machine. // machine.
const OFFICER_PORT = process.env.PORT ?? '9000';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${OFFICER_PORT}`;
const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${OFFICER_PORT}`;
const MCP_SERVER_SCRIPT = resolve(import.meta.dir, '../../mcp-tool-server.ts'); const MCP_SERVER_SCRIPT = resolve(import.meta.dir, '../../mcp-tool-server.ts');
// Mint a long-lived JWT for this user so tools (e.g. gmail) can call back to dev-platform as them // Mint a long-lived JWT for this user so tools (e.g. gmail) can call back to dev-platform as them
+1 -1
View File
@@ -4,8 +4,8 @@ import { initEmailIdle, stopEmailIdle } from './email-idle';
import { broadcastEmailNew } from './routes'; import { broadcastEmailNew } from './routes';
import { startEmailServer } from './http'; import { startEmailServer } from './http';
import { createSidecarConnector } from '../connect'; import { createSidecarConnector } from '../connect';
import { API_URL } from '../../officer-url.mjs';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// The sidecar used to reach BACK into the platform's queue over this socket to get a sync run — // The sidecar used to reach BACK into the platform's queue over this socket to get a sync run —
// enqueueViaWs / listJobsViaWs and a pending-response map. Syncs run in this process now // enqueueViaWs / listJobsViaWs and a pending-response map. Syncs run in this process now
+1 -1
View File
@@ -16,6 +16,7 @@ import {
normalizeBase, normalizeBase,
probe, probe,
} from './upstream'; } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-gitea sidecar. Owns the whole Gitea contract: the instance URL and the personal access // The officer-gitea sidecar. Owns the whole Gitea contract: the instance URL and the personal access
// token. The platform side is a thin auth-gated forwarder holding no Gitea credentials. // token. The platform side is a thin auth-gated forwarder holding no Gitea credentials.
@@ -42,7 +43,6 @@ import {
// sidecar from being a general-purpose SSRF hop into whatever else is on that host. // sidecar from being a general-purpose SSRF hop into whatever else is on that host.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// Everything under /api/v1 the UI legitimately needs. // Everything under /api/v1 the UI legitimately needs.
// //
+1 -1
View File
@@ -2,6 +2,7 @@ import type { SidecarCommand, SidecarEvent } from '../protocol';
import { createSidecarConnector } from '../connect'; import { createSidecarConnector } from '../connect';
import { handleOfficerRoute } from './routes'; import { handleOfficerRoute } from './routes';
import { MIN_VERSION_LABEL } from './version'; import { MIN_VERSION_LABEL } from './version';
import { API_URL } from '../../officer-url.mjs';
// The officer-headscale sidecar. Owns the whole Headscale contract for Officer: the registered servers and // The officer-headscale sidecar. Owns the whole Headscale contract for Officer: the registered servers and
// their admin API keys, the >=0.29 version floor, and every multi-call composition the UI needs. The platform // their admin API keys, the >=0.29 version floor, and every multi-call composition the UI needs. The platform
@@ -54,7 +55,6 @@ import { MIN_VERSION_LABEL } from './version';
// — the mistake the Soulseek panels made with 37 raw upstream calls. Every quirk is absorbed here. // — the mistake the Soulseek panels made with 37 raw upstream calls. Every quirk is absorbed here.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -3,6 +3,7 @@ import { createSidecarConnector } from '../connect';
import { handleConfigRoute, noteProbe, probe } from './config'; import { handleConfigRoute, noteProbe, probe } from './config';
import { handleOfficerRoute } from './routes'; import { handleOfficerRoute } from './routes';
import { getConfig } from './upstream'; import { getConfig } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-invoiceshelf sidecar. Owns the whole InvoiceShelf contract for Officer: the instance URL, the // The officer-invoiceshelf sidecar. Owns the whole InvoiceShelf contract for Officer: the instance URL, the
// Sanctum API token, and the `company` header that scopes every request. The platform API is a thin // Sanctum API token, and the `company` header that scopes every request. The platform API is a thin
@@ -51,7 +52,6 @@ import { getConfig } from './upstream';
// update/*, installation/*, mail config, settings writes, ownership transfer — is deliberately unreachable. // update/*, installation/*, mail config, settings writes, ownership transfer — is deliberately unreachable.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -3,6 +3,7 @@ import { createSidecarConnector } from '../connect';
import { handleConfigRoute, noteProbe } from './config'; import { handleConfigRoute, noteProbe } from './config';
import { handleBytesRoute, handleOfficerRoute } from './routes'; import { handleBytesRoute, handleOfficerRoute } from './routes';
import { getConfig, probe } from './upstream'; import { getConfig, probe } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-jellyfin sidecar. Owns the whole Jellyfin contract for Officer: the instance URL, the access // The officer-jellyfin sidecar. Owns the whole Jellyfin contract for Officer: the instance URL, the access
// token, the Jellyfin user it belongs to and the DeviceId its sessions are keyed by. The platform API is a // token, the Jellyfin user it belongs to and the DeviceId its sessions are keyed by. The platform API is a
@@ -43,7 +44,6 @@ import { getConfig, probe } from './upstream';
// general proxy, and why HLS forces it to keep Jellyfin's own paths. // general proxy, and why HLS forces it to keep Jellyfin's own paths.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -2,6 +2,7 @@ import type { SidecarCommand, SidecarEvent } from '../protocol';
import { createSidecarConnector } from '../connect'; import { createSidecarConnector } from '../connect';
import { getServiceConnection, saveServiceConnection, deleteServiceConnection, recordServiceProbe } from 'officerdb'; import { getServiceConnection, saveServiceConnection, deleteServiceConnection, recordServiceProbe } from 'officerdb';
import { callMemos, getMemosConfig, invalidateMemosConfig, normalizeBase, probe } from './upstream'; import { callMemos, getMemosConfig, invalidateMemosConfig, normalizeBase, probe } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-memos sidecar. Owns the whole Memos contract: the instance URL and the personal access // The officer-memos sidecar. Owns the whole Memos contract: the instance URL and the personal access
// token. The platform side is a thin auth-gated forwarder holding no Memos credentials. // token. The platform side is a thin auth-gated forwarder holding no Memos credentials.
@@ -22,7 +23,6 @@ import { callMemos, getMemosConfig, invalidateMemosConfig, normalizeBase, probe
// SSRF hop into whatever else is on that host. // SSRF hop into whatever else is on that host.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// Everything under /api/v1 the UI legitimately needs. Auth routes are excluded on purpose: signin and // Everything under /api/v1 the UI legitimately needs. Auth routes are excluded on purpose: signin and
// signout would mint or destroy sessions on the instance, and this sidecar authenticates with a stored // signout would mint or destroy sessions on the instance, and this sidecar authenticates with a stored
+1 -1
View File
@@ -39,6 +39,7 @@ import {
type FavoriteKind, type FavoriteKind,
} from 'officerdb'; } from 'officerdb';
import { DATA_PATH } from '../../data-path'; import { DATA_PATH } from '../../data-path';
import { API_URL } from '../../officer-url.mjs';
// ── Per-user state validation ── // ── Per-user state validation ──
@@ -114,7 +115,6 @@ const asKeys = (v: unknown): string[] | null =>
// `v` = per-album version stamp; unchanged `v` ⇒ nothing changed ⇒ the phone can skip re-downloading. // `v` = per-album version stamp; unchanged `v` ⇒ nothing changed ⇒ the phone can skip re-downloading.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// ── Audio-streaming HTTP server ── // ── Audio-streaming HTTP server ──
+1 -1
View File
@@ -5,6 +5,7 @@ import { handleDeviceRoute } from './devices';
import { resolveNotifyUser } from './resolve-user'; import { resolveNotifyUser } from './resolve-user';
import { closeApnsSessions } from './apns'; import { closeApnsSessions } from './apns';
import type { Notification, NotifyType } from './types'; import type { Notification, NotifyType } from './types';
import { API_URL } from '../../officer-url.mjs';
// The officer-notify sidecar. The one place anything leaves this machine to tell the owner something. // The officer-notify sidecar. The one place anything leaves this machine to tell the owner something.
// //
@@ -26,7 +27,6 @@ import type { Notification, NotifyType } from './types';
// the visible string is composed here rather than sent by the producer. // the visible string is composed here rather than sent by the producer.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
const VALID_TYPES: NotifyType[] = ['job', 'mail', 'agent', 'download', 'test']; const VALID_TYPES: NotifyType[] = ['job', 'mail', 'agent', 'download', 'test'];
+1 -1
View File
@@ -9,6 +9,7 @@ import { createSessionLogStore } from '../claude/session-log';
import type { SidecarCommand, SidecarEvent } from '../protocol'; import type { SidecarCommand, SidecarEvent } from '../protocol';
import type { RunnerMessage } from './serve-runner'; import type { RunnerMessage } from './serve-runner';
import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllServeTurns } from './serve-runner'; import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllServeTurns } from './serve-runner';
import { API_URL } from '../../officer-url.mjs';
// The OpenCode sidecar (officer-opencode). Same philosophy as officer-claude: a singleton process that // The OpenCode sidecar (officer-opencode). Same philosophy as officer-claude: a singleton process that
// OWNS its runtime — here, an `opencode serve` — registers with the API server, and answers commands. It // OWNS its runtime — here, an `opencode serve` — registers with the API server, and answers commands. It
@@ -18,7 +19,6 @@ import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllSe
// CRUD only, with turns spawned as `opencode run --dir <cwd>` subprocesses — that path was deleted on // CRUD only, with turns spawned as `opencode run --dir <cwd>` subprocesses — that path was deleted on
// 2026-08-10 once the serve had streaming, mid-turn injection and interrupt working end to end. // 2026-08-10 once the serve had streaming, mid-turn injection and interrupt working end to end.
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
const OPENCODE_BIN = process.env.OPENCODE_BIN || join(homedir(), '.opencode', 'bin', 'opencode'); const OPENCODE_BIN = process.env.OPENCODE_BIN || join(homedir(), '.opencode', 'bin', 'opencode');
const SERVE_CWD = join(DATA_PATH, 'opencode_server'); const SERVE_CWD = join(DATA_PATH, 'opencode_server');
const HEALTH_TIMEOUT_MS = 20_000; const HEALTH_TIMEOUT_MS = 20_000;
+1 -1
View File
@@ -4,6 +4,7 @@ import { handleConfigRoute, noteProbe, probe } from './config';
import { handleLockedRoute } from './locked'; import { handleLockedRoute } from './locked';
import { handleOfficerRoute } from './routes'; import { handleOfficerRoute } from './routes';
import { getConfig } from './upstream'; import { getConfig } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-photos sidecar. Owns the whole Immich contract for Officer: the instance URL and the API key. // The officer-photos sidecar. Owns the whole Immich contract for Officer: the instance URL and the API key.
// The platform API is a thin auth-gated forwarder (src/servers/api/photos/router.ts) holding no Immich // The platform API is a thin auth-gated forwarder (src/servers/api/photos/router.ts) holding no Immich
@@ -46,7 +47,6 @@ import { getConfig } from './upstream';
// ETag included. The administrative half of Immich's API is unreachable — see routes.ts for the list. // ETag included. The administrative half of Immich's API is unreachable — see routes.ts for the list.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -18,8 +18,8 @@ import * as store from './sessions.mjs';
import { startServer } from './server.mjs'; import { startServer } from './server.mjs';
import 'dotenv/config'; import 'dotenv/config';
import { API_URL } from '../../officer-url.mjs';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
const REGISTER_URL = `${API_URL}/api/sidecar/register`; const REGISTER_URL = `${API_URL}/api/sidecar/register`;
const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000]; const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000];
+1 -1
View File
@@ -4,6 +4,7 @@ import { resetStaleSoulseekBrowses } from 'officerdb';
import { getSlskdConfig, stripHopByHop } from './upstream'; import { getSlskdConfig, stripHopByHop } from './upstream';
import { handleConfigRoute, probe } from './config'; import { handleConfigRoute, probe } from './config';
import { handleOfficerRoute } from './officer'; import { handleOfficerRoute } from './officer';
import { API_URL } from '../../officer-url.mjs';
// The officer-slskd sidecar. Same philosophy as officer-vault / officer-music: a singleton process that // The officer-slskd sidecar. Same philosophy as officer-vault / officer-music: a singleton process that
// registers with the API server and OWNS a contract — here, a reverse-proxy to a self-hosted slskd // registers with the API server and OWNS a contract — here, a reverse-proxy to a self-hosted slskd
@@ -37,7 +38,6 @@ import { handleOfficerRoute } from './officer';
// (src/servers/sidecar/vault/index.ts) once the client needs real-time updates. SignalR carries its // (src/servers/sidecar/vault/index.ts) once the client needs real-time updates. SignalR carries its
// credential as an `?access_token=` query param on the socket, so the key injection differs from HTTP. // credential as an `?access_token=` query param on the socket, so the key injection differs from HTTP.
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -4,6 +4,7 @@ import { handleConfigRoute } from './config';
import { handleOfficerRoute } from './routes'; import { handleOfficerRoute } from './routes';
import { probe } from './rpc'; import { probe } from './rpc';
import { getTransmissionConfig } from './upstream'; import { getTransmissionConfig } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-transmission sidecar. Owns the whole Transmission contract for Officer: the daemon URL and // The officer-transmission sidecar. Owns the whole Transmission contract for Officer: the daemon URL and
// credentials, the X-Transmission-Session-Id CSRF handshake, and the translation from Transmission's // credentials, the X-Transmission-Session-Id CSRF handshake, and the translation from Transmission's
@@ -45,7 +46,6 @@ import { getTransmissionConfig } from './upstream';
// platform, and which daemon to talk to is per-owner data. // platform, and which daemon to talk to is per-owner data.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -2,6 +2,7 @@ import type { ServerWebSocket } from 'bun';
import type { SidecarCommand, SidecarEvent } from '../protocol'; import type { SidecarCommand, SidecarEvent } from '../protocol';
import { createSidecarConnector } from '../connect'; import { createSidecarConnector } from '../connect';
import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstream'; import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstream';
import { API_URL } from '../../officer-url.mjs';
// The officer-vault sidecar. Same philosophy as the other officer-* sidecars: a singleton process that // The officer-vault sidecar. Same philosophy as the other officer-* sidecars: a singleton process that
// registers with the API server and OWNS a contract — here, a transparent reverse-proxy to a self-hosted // registers with the API server and OWNS a contract — here, a transparent reverse-proxy to a self-hosted
@@ -21,7 +22,6 @@ import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstr
// The server listens on a random loopback port, reported to the API on connect so it can route here. // The server listens on a random loopback port, reported to the API on connect so it can route here.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {
+1 -1
View File
@@ -2,8 +2,8 @@ import type { SidecarCommand, SidecarEvent } from '../protocol';
import * as vncManager from './vnc-manager'; import * as vncManager from './vnc-manager';
import { createSidecarConnector } from '../connect'; import { createSidecarConnector } from '../connect';
import { getOwnerHomeDir } from '@@/data-path'; import { getOwnerHomeDir } from '@@/data-path';
import { API_URL } from '../../officer-url.mjs';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// ── Command handlers ── // ── Command handlers ──
+1 -1
View File
@@ -6,6 +6,7 @@ import { handleOfficerRoute } from './routes';
import { getChainSource, getConfig, hasStoreKey } from './upstream'; import { getChainSource, getConfig, hasStoreKey } from './upstream';
import { lockAll } from './keys'; import { lockAll } from './keys';
import { invalidateAll } from './resolve'; import { invalidateAll } from './resolve';
import { API_URL } from '../../officer-url.mjs';
// The officer-wallet sidecar. A bitcoin wallet in the shape Zeus models one — several interchangeable // The officer-wallet sidecar. A bitcoin wallet in the shape Zeus models one — several interchangeable
// backends behind one interface — but server-side, with the key material held here and nowhere else. // backends behind one interface — but server-side, with the key material held here and nowhere else.
@@ -81,7 +82,6 @@ import { invalidateAll } from './resolve';
// WALLET_LOCKED from signing paths only; every read above keeps working. // WALLET_LOCKED from signing paths only; every read above keeps working.
// ───────────────────────────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */ /** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number { function getFreePort(): number {