ANTHROPIC_PROXY_PORT is gone. It was 5051 hardcoded in four files: proxy.ts, which binds it, and three others that guessed the same constant to find it. It is the only sidecar that binds a fixed port, and that part is a real constraint rather than an oversight. Every other one binds `port: 0`, lets the kernel choose and reports back over the registration socket — which works because their consumer is the platform. The proxy's consumer is `claude`, spawned by a different pm2 process that needs ANTHROPIC_BASE_URL at spawn time and has no channel to ask what port the proxy landed on. Two processes with nothing between them have to agree in advance. So the number must be predictable, but it need not be 5051 — a value chosen against nothing, in the registered range, free to collide with anything the owner installs later. The symptom of that collision would have been chat failing while the rest of the platform looked healthy. PORT + 1 keeps the predictability and drops both the constant and the variable. Nothing to set, no second number to keep in agreement with the first, and the pair moves together when the install moves. Also corrects .env.example, which said the proxy "holds the API credential, which lives in the host env". It does not. The upstream credential is the OAuth token claude writes to ~/.claude/.credentials.json, and the ANTHROPIC_API_KEY the agent presents is the proxy's own generated secret. Verified the derivation at PORT=9000 and PORT=10000; all four consumers now import it; every edited file parses. Still not typechecked — empty node_modules, frozen installs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
3.8 KiB
JavaScript
76 lines
3.8 KiB
JavaScript
// 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}`;
|
|
|
|
// ── The Anthropic proxy: PORT + 1, derived, not configured ──
|
|
//
|
|
// This is the ONE sidecar that binds a fixed port. Every other one binds `port: 0`, lets the kernel
|
|
// choose, and reports what it got back over the registration socket — which works because their
|
|
// consumer is the platform.
|
|
//
|
|
// The proxy cannot do that. Its consumer is `claude`, spawned by a DIFFERENT pm2 process
|
|
// (officer-agent), which needs ANTHROPIC_BASE_URL at spawn time and has no channel to ask the proxy
|
|
// what port it landed on. Two independent processes with nothing between them have to agree in
|
|
// advance, so the number has to be predictable rather than discovered.
|
|
//
|
|
// It was ANTHROPIC_PROXY_PORT, defaulting to 5051 in four separate files: the one that binds it and
|
|
// three that guessed the same constant to find it. 5051 was chosen against nothing and could collide
|
|
// with anything the owner installs later, with the symptom being chat failing while the rest of the
|
|
// platform looked healthy.
|
|
//
|
|
// PORT + 1 keeps the predictability and removes both problems. There is no variable to set, no second
|
|
// number to keep in agreement with the first, and the pair moves together when the install moves.
|
|
export const ANTHROPIC_PROXY_PORT = PORT + 1;
|
|
export const ANTHROPIC_PROXY_URL = `http://127.0.0.1:${ANTHROPIC_PROXY_PORT}`;
|