/** * WHICH TRANSPORT A SEAT ON THIS MACHINE IS CONFIGURED TO USE (Phase 5.4 Task 3.1–3.2). * * ⟨q-d404a6f6⟩ THIS IS MACHINE-SCOPED, NOT FLEET-WIDE — the file lives at * `$AGENT_COORD_DIR/config.json`, a path local to ONE box. A fleet now spans * machines: `tmux-push-remote` exists precisely for a seat on another host, and * that seat never reads THIS machine's config.json — it has (or lacks) its own. * Calling this "the fleet's statement" was false and load-bearing: it read as * an argument against ANY compiled default, when the argument that actually * holds (below) is narrower — against a default picked HERE being assumed * correct on a DIFFERENT machine. Per-process (read ONCE at startup, no * per-seat branching in `send_command` or `attach` for the seats ON THIS BOX) * is still exactly right; "whole-fleet" was the wrong word for it. * * ⛔ AN UNKNOWN VALUE REFUSES AT STARTUP. It does not fall back to anything. * * The reason is not tidiness. A SILENT FALLBACK AND A CORRECT DEFAULT PRODUCE * IDENTICAL EVIDENCE: both give you a seat on some transport with nothing in any * log, so a typo in the config reads exactly like a deliberate default, and the * person who typed `heardr` spends the afternoon asking why their transport * change did nothing. Refusing is louder than the bug it prevents. * * ⟨q-ec020f6a⟩ THERE IS NO BUILT-IN DEFAULT ANY MORE, for the identical reason. * `tmux-push` (0 of 13 live seats) used to be it; deleting that kind without * removing the fallback would have meant every unconfigured session refused * with "unknown transport 'tmux-push'" — a config-file bug wearing a runtime * bug's clothes. Picking a new implicit default (herdr, say) would be correct * on THIS machine today and wrong on the next one that doesn't run herdr — * the exact silent-guess failure ⟨q-e439e4ad⟩ spent a day fixing, moved one * layer up. So: zero configuration REFUSES, naming the kinds that exist. */ import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { ROOT } from "../store.js"; import { TRANSPORT_KINDS, type TransportKind } from "./types.js"; /** `$AGENT_COORD_DIR/config.json` — MACHINE-scoped, not fleet-wide: see the file header. */ export const TRANSPORT_CONFIG_FILE = path.join(ROOT, "config.json"); export const TRANSPORT_ENV_VAR = "AGENT_COORD_TRANSPORT"; /** * PRECEDENCE, documented here because 3.2 asks for a decision and not a * preference: **machine config file > env**. (⟨q-ec020f6a⟩: no default follows * either — see the file header.) * * The file wins because it is this MACHINE's statement and is reviewable — it * sits on disk where every seat ON THIS BOX reads the same bytes, and a wrong * value can be corrected in one place. An env var is per-process: it is the * right tool for one seat to deviate deliberately (a test, a bisect), and the * wrong tool for stating what this machine's seats do by default, because * nothing can see it from outside that process. So the narrower, less visible * source loses to the broader one, and `source` is reported so a surprising * answer can be traced to its origin rather than guessed at. */ export type ConfiguredTransport = { kind: TransportKind; source: "machine-config" | "env"; /** Where the value came from, for an error message a human can act on. */ origin: string; }; function refuse(value: string, origin: string): never { throw new Error( `[agent-coord-mcp] unknown transport ${JSON.stringify(value)} from ${origin}. ` + `Valid: ${TRANSPORT_KINDS.join(", ")}. ` + `REFUSING AT STARTUP rather than falling back to anything — a silent fallback and a correct ` + `default leave identical evidence, so a typo here would look exactly like a working value and the ` + `transport change would appear to do nothing. Fix the value or remove it (⟨q-ec020f6a⟩: there is no default to fall back to).`, ); } function asKind(value: unknown, origin: string): TransportKind { if (typeof value !== "string" || value.length === 0) refuse(String(value), origin); const match = TRANSPORT_KINDS.find((k) => k === value); if (!match) refuse(value, origin); return match; } let cached: ConfiguredTransport | undefined; /** * ⭐ ⟨q-9e0072b3⟩ THE PRECEDENCE DECISION, PULLED OUT SO IT CAN BE CALLED WITHOUT TOUCHING * DISK OR `process.env`. `configuredTransport()` below is the only caller that supplies real * values read from THIS machine; anything else — a test, or a capability probe that must prove * a property of the CODE rather than a fact about one machine's config.json — can hand this * CONSTRUCTED values instead. That is what makes it possible to probe "does the machine-config/ * env vocabulary exist and resolve correctly" without the probe itself becoming an undeclared * read of a mutable, machine-local baseline (`check-baseline-declared` correctly caught the * first version of that probe calling `configuredTransport()` directly for exactly this reason). */ export function resolveConfiguredTransport(input: { fileHasTransport: boolean; fileTransport: unknown; env: string | undefined }): ConfiguredTransport { if (input.fileHasTransport) { return { kind: asKind(input.fileTransport, `${TRANSPORT_CONFIG_FILE} ("transport")`), source: "machine-config", origin: TRANSPORT_CONFIG_FILE }; } if (input.env !== undefined && input.env !== "") { return { kind: asKind(input.env, `$${TRANSPORT_ENV_VAR}`), source: "env", origin: `$${TRANSPORT_ENV_VAR}` }; } throw new Error( `[agent-coord-mcp] no transport configured — set $${TRANSPORT_ENV_VAR} or ${TRANSPORT_CONFIG_FILE} ` + `("transport") to one of: ${TRANSPORT_KINDS.join(", ")}. ` + `REFUSING rather than picking one: a default that is right for this machine today is wrong for the ` + `next one that doesn't have it, and the two failures leave identical evidence.`, ); } /** * Resolve THIS MACHINE's configured transport. Throws on an unknown value — call it once at * startup so the refusal lands before any agent attaches. */ export function configuredTransport(): ConfiguredTransport { if (cached) return cached; let fileTransport: unknown; if (existsSync(TRANSPORT_CONFIG_FILE)) { let parsed: unknown; try { parsed = JSON.parse(readFileSync(TRANSPORT_CONFIG_FILE, "utf8")); } catch (e) { // A CONFIG FILE THAT CANNOT BE PARSED IS NOT AN ABSENT ONE. Treating it as // absent would silently use the default while a file sits there stating // otherwise — the same two-states-one-evidence defect as the fallback. throw new Error( `[agent-coord-mcp] ${TRANSPORT_CONFIG_FILE} is unreadable (${(e as Error).message}). ` + `REFUSING rather than treating it as absent: a file that exists and cannot be read is not the ` + `same as no file, and defaulting here would hide a stated intent behind a seat that looked healthy.`, ); } fileTransport = (parsed as { transport?: unknown } | null)?.transport; } cached = resolveConfiguredTransport({ fileHasTransport: fileTransport !== undefined, fileTransport, env: process.env[TRANSPORT_ENV_VAR] }); return cached; } /** * Drop the memo. FOR TESTS ONLY — the whole point of reading once is that * production code cannot do this. */ export function resetConfiguredTransportForTests(): void { cached = undefined; }