Files
openrig/packages/cli/src/commands/host.ts
T
mvschwarz ac75777a3c Release 0.4.4
Multi-host + Living Notes.

Multi-host foundation: shareable whole-topology staged spin-up brings
a rig topology up across multiple hosts in stages; VPS product-factory
multi-host hardening ships with rig host verbs (add / list / doctor,
capped at three; one built-in product-factory-vps posture, three-
valued per item); the transport posture is documented (ssh for pane
ops, http-bearer for daemon REST, no cross-transport fallback); the
For-You feed aggregates activity across every registered host; and
rig file moves files across the topology's registered hosts.

Living Notes: durable INTENT / PLAN / DELIVERED signal layer at
mission and slice altitude with agent authorship + timestamps; cheap
composer surfaces make it the first-class place agents record
decisions, plans, and delivered work; the one-structure review
contract reads left-to-right as a single vertical stack (INTENT above,
PLAN + mockup in the middle, DELIVERED with paired proof at the
bottom); a plan change deletes the old plan and writes a new one -
never demotes or stacks multiple competing plans.

Operationalize the SDLC control plane: conventions SSOT ships in
source at docs/reference/sdlc-conventions.md (Living Notes UI section
names, proof-contract format + plannedRef mockup pairing, staged-
approval locks, C1 proof header + closed sets, three role contracts,
curation rule, elastic-middle doctrine, advisory fail-open audit
posture); rig scope slice create scaffolds the convention sections +
proof + PROOF.md + an IMPLEMENTATION-PRD.md skeleton for every
template kind; rig scope audit + rig workspace doctor gain sdlc-
convention advisories (records-and-advises, gates on HIGH only); the
mission-slice-sop skill ships in canonical source with byte-parity
guard; rig proof / rig scope slice create / rig scope slice approve
help + cli-reference teach the SDLC flow.

BREAKING: rig ps default flips to consolidated all-active-rigs
compact projection (the v0.4.0 current-rig-only default is retired -
it hid running rigs from the operator's field of view). --json is
scope-not-shape (still a bare array with existing per-entry keys;
scope widens to all non-archived rigs including stopped ones; one-line
migration for the old fleet firehose: rig ps --nodes -A --full).
-A / --all-rigs keeps exactly ONE meaning (the --nodes fleet widener;
bare -A is a structured teaching error). --nodes names its scope
everywhere (session default local-only; --host <id> --nodes requires
--rig or -A; multi-host fan-out is rollup-only by default; the
explicit ladder --all-hosts --nodes -A --full fans out per-node with
hostId-stamped projected rows). --all-hosts / --hosts --json emits the
shared AggregatedPayload (items + hosts with closed-enum statuses:
ok | unreachable | unsupported-transport | auth-failed).

Operator UX: agent altitude coordination panel scopes multi-agent
state to the right altitude (workspace / mission / slice) so the
coordination surface reads without per-seat drowning; composes with
the workspace observability tabs from 0.4.1. Docs closeout catches
docs/as-built/architecture.md, docs/as-built/cli-reference.md, and
the codemaps up to shipped state.

Migrations: additive only. Existing v0.4.3 databases upgrade by
running rig daemon start.

Behavior change (rig ps default view): the compact all-active-rigs
projection replaces the current-rig-only default. If your call sites
relied on the v0.4.3 default shape, add --full for the rich per-node
projection or --rig <name> for the prior scope.

Public-safety scrub applied inline to packages/ui/twin/corrective/
fixtures-corrective.ts (one demo-fixture role identifier neutralized
to the demo-namespace baseline established at 0.4.1).

See CHANGELOG.md and docs/releases/v0.4.4.md.
2026-07-06 01:20:09 -07:00

473 lines
23 KiB
TypeScript

// OPR.0.4.4.13 — the host-registry verbs. CAPPED at exactly three
// (add / list / doctor — arch R13-2a): no edit/remove/tunnel/bootstrap
// verbs; hand-editing hosts.yaml remains the path for everything else,
// and the bootstrap ships as script + runbook.
//
// Secret hygiene (FR-1): every render carries bearer POINTERS (env var /
// file NAMES) only — no code path in this file resolves a bearer value
// for display.
import { Command } from "commander";
import { existsSync } from "node:fs";
import { connect } from "node:net";
import { addHostEntry, defaultHostRegistryPath, loadHostRegistry, hostDisplayTarget, resolveHost, resolveRemoteBearer, type HostEntry, type HttpHostEntry } from "../host-registry.js";
import { runCrossHostCommand, type CrossHostResult } from "../cross-host-executor.js";
// ---------------------------------------------------------------------------
// rig host doctor — stepwise, honest, three-valued (FR-1 + FR-2).
// ---------------------------------------------------------------------------
export type CheckStatus = "pass" | "fail" | "unknown";
export interface CheckRow {
step: string;
status: CheckStatus;
detail: string;
/** Actionable next step; REQUIRED for fail, encouraged for unknown. */
fix?: string;
}
export interface DoctorDeps {
run: (host: HostEntry, argv: readonly string[]) => Promise<CrossHostResult>;
httpGet: (url: string, headers?: Record<string, string>) => Promise<{ status: number; body: string }>;
/** TCP connect probe: "open" (connected) | "closed" (refused/filtered/timeout) | "unknown" (probe itself failed). */
tcpProbe: (target: string, port: number, timeoutMs: number) => Promise<"open" | "closed" | "unknown">;
}
function nestedBoolean(obj: unknown, path: readonly string[]): boolean | undefined {
let cur: unknown = obj;
for (const part of path) {
if (typeof cur !== "object" || cur === null) return undefined;
cur = (cur as Record<string, unknown>)[part];
}
return typeof cur === "boolean" ? cur : undefined;
}
function tailscaleRunSshFromPrefs(text: string): boolean | undefined {
if (!text.trim().startsWith("{")) return undefined;
const parsed = JSON.parse(text) as unknown;
return nestedBoolean(parsed, ["RunSSH"])
?? nestedBoolean(parsed, ["Prefs", "RunSSH"])
?? nestedBoolean(parsed, ["CurrentProfile", "RunSSH"]);
}
function defaultDoctorDeps(): DoctorDeps {
return {
run: (host, argv) => runCrossHostCommand(host, argv),
httpGet: async (url, headers) => {
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10_000) });
return { status: res.status, body: await res.text() };
},
tcpProbe: (target, port, timeoutMs) =>
new Promise((resolve) => {
const sock = connect({ host: target, port, timeout: timeoutMs });
sock.on("connect", () => { sock.destroy(); resolve("open"); });
sock.on("timeout", () => { sock.destroy(); resolve("closed"); });
sock.on("error", () => { sock.destroy(); resolve("closed"); });
}),
};
}
/** FR-1 doctor legs — each failing step maps to a DISTINCT actionable error:
* "SSH works but daemon is down" ≠ "daemon works but registry is wrong" ≠
* "remote rig binary missing/old". */
export async function doctorLegs(host: HostEntry, deps: DoctorDeps): Promise<CheckRow[]> {
const rows: CheckRow[] = [];
if (host.transport === "ssh") {
const reach = await deps.run(host, ["true"]);
if (!reach.ok && (reach.failedStep === "ssh-unreachable" || reach.failedStep === "permission-gate")) {
rows.push({
step: "transport-reachability",
status: "fail",
detail: `ssh to ${host.target} failed (${reach.failedStep}): ${reach.sshStderr.trim()}`,
fix: reach.failedStep === "permission-gate"
? "SSH auth/permission gate — check the key, agent, and remote authorized_keys"
: "verify the ssh target/alias, tailnet connectivity, and that sshd is running",
});
return rows; // later legs are meaningless without the shell
}
rows.push({ step: "transport-reachability", status: "pass", detail: `ssh to ${host.target} ok` });
const version = await deps.run(host, ["rig", "--version"]);
if (!version.ok) {
rows.push({
step: "remote-rig-binary",
status: "fail",
detail: "reached the remote shell, but `rig --version` failed — remote rig binary missing or broken",
fix: "install the published artifact on the host: npm install -g @openrig/cli (see the product-factory runbook)",
});
return rows;
}
rows.push({ step: "remote-rig-binary", status: "pass", detail: `remote rig ${version.stdout.trim() || "(version unreadable)"}` });
const daemon = await deps.run(host, ["rig", "daemon", "status"]);
const daemonUp = daemon.ok && /running/i.test(daemon.stdout);
rows.push(daemonUp
? { step: "remote-daemon-health", status: "pass", detail: "remote daemon reports running" }
: {
step: "remote-daemon-health",
status: "fail",
detail: "SSH works and rig is installed, but the remote daemon is not running",
fix: "on the host: rig daemon start (then re-run doctor)",
});
if (!daemonUp) return rows;
const whoami = await deps.run(host, ["rig", "ps", "--json", "--limit", "5"]);
let identityOk = false;
if (whoami.ok) {
try { identityOk = typeof JSON.parse(whoami.stdout) === "object"; } catch { identityOk = false; }
}
rows.push(identityOk
? { step: "remote-identity", status: "pass", detail: "remote daemon identity/list resolves (rig ps --json parses)" }
: {
step: "remote-identity",
status: "fail",
detail: "daemon is up but remote identity/list did not resolve",
fix: "on the host: check `rig ps --json` directly; the daemon API may be unhealthy even though status says running",
});
return rows;
}
// http transport
const bearer = resolveRemoteBearer(host as HttpHostEntry);
if (!bearer.ok) {
rows.push({ step: "transport-reachability", status: "fail", detail: bearer.error, fix: "set the bearer env var / file the registry entry points at (pointer names: rig host list)" });
return rows;
}
try {
const health = await deps.httpGet(`${(host as HttpHostEntry).url}/healthz`, { Authorization: `Bearer ${bearer.token}` });
if (health.status === 401 || health.status === 403) {
rows.push({ step: "transport-reachability", status: "fail", detail: `daemon reachable but rejected the bearer (HTTP ${health.status})`, fix: "registry is reachable but the token is wrong — rotate/set the bearer the entry points at" });
return rows;
}
if (health.status < 200 || health.status >= 300) {
rows.push({ step: "transport-reachability", status: "fail", detail: `healthz returned HTTP ${health.status}`, fix: "the daemon answered abnormally — check remote daemon logs" });
return rows;
}
rows.push({ step: "transport-reachability", status: "pass", detail: `healthz ok at ${(host as HttpHostEntry).url}` });
rows.push({ step: "remote-daemon-health", status: "pass", detail: "healthz IS the daemon health check on http transport" });
} catch (err) {
rows.push({ step: "transport-reachability", status: "fail", detail: `daemon unreachable at ${(host as HttpHostEntry).url}: ${(err as Error).message}`, fix: "check tailnet connectivity and that the remote daemon is running (rig daemon start on the host)" });
return rows;
}
rows.push({
step: "remote-rig-binary",
status: "unknown",
detail: "not determinable over http transport (no shell)",
fix: "verify via an ssh-transport entry for this host, or on the host directly: rig --version",
});
try {
const ps = await deps.httpGet(`${(host as HttpHostEntry).url}/api/ps`, { Authorization: `Bearer ${bearer.token}` });
rows.push(ps.status >= 200 && ps.status < 300
? { step: "remote-identity", status: "pass", detail: "authenticated daemon API answers (/api/ps)" }
: { step: "remote-identity", status: "fail", detail: `/api/ps returned HTTP ${ps.status}`, fix: "daemon healthz is up but the API is unhealthy — check remote daemon logs" });
} catch (err) {
rows.push({ step: "remote-identity", status: "fail", detail: `/api/ps unreachable: ${(err as Error).message}`, fix: "check remote daemon logs" });
}
return rows;
}
/** FR-2 — THE one built-in posture profile (arch R13-2b: a named constant
* table, no framework). Every item is three-valued; UNKNOWN is never
* smoothed to pass, and every non-pass carries its fix. */
export const POSTURE_PROFILE_ID = "product-factory-vps";
export async function postureCheck(
host: HostEntry,
deps: DoctorDeps,
opts: { publicAddr?: string } = {},
): Promise<CheckRow[]> {
if (host.transport !== "ssh") {
// Posture needs a shell. Report every item unknown — honestly.
return POSTURE_ITEM_IDS.map((id) => ({
step: id,
status: "unknown" as const,
detail: "posture checks need shell access; this host is registered with http transport",
fix: "register an ssh-transport entry for this host (or run the checks on the host per the runbook)",
}));
}
const rows: CheckRow[] = [];
const sh = async (cmd: string) => deps.run(host, ["sh", "-c", cmd]);
// 1. non-root openrig user
const idOut = await sh("id -u openrig 2>/dev/null");
if (idOut.ok && idOut.stdout.trim() !== "") {
rows.push(idOut.stdout.trim() === "0"
? { step: "nonroot-openrig-user", status: "fail", detail: "user 'openrig' resolves to uid 0", fix: "create a non-root openrig user per the runbook" }
: { step: "nonroot-openrig-user", status: "pass", detail: `user 'openrig' uid ${idOut.stdout.trim()}` });
} else {
rows.push({ step: "nonroot-openrig-user", status: "fail", detail: "user 'openrig' does not exist", fix: "adduser openrig + key-only SSH per the runbook" });
}
// 2+3. sshd effective config (root SSH off, password auth off)
const sshd = await sh("sudo -n sshd -T 2>/dev/null || sshd -T 2>/dev/null || true");
const sshdOut = sshd.ok ? sshd.stdout.toLowerCase() : "";
const sshdItem = (step: string, key: string, want: string): CheckRow => {
if (!sshdOut.includes(key)) {
return { step, status: "unknown", detail: "effective sshd config not readable from this vantage (sshd -T needs root)", fix: "on the host: sudo sshd -T | grep " + key };
}
return sshdOut.includes(`${key} ${want}`)
? { step, status: "pass", detail: `${key} ${want}` }
: { step, status: "fail", detail: `sshd -T reports ${key} != ${want}`, fix: `set ${key} ${want} in /etc/ssh/sshd_config.d/99-openrig-hardening.conf and reload sshd` };
};
rows.push(sshdItem("root-ssh-disabled", "permitrootlogin", "no"));
rows.push(sshdItem("key-only-ssh", "passwordauthentication", "no"));
// 4+5. UFW default-deny + tailnet ingress
const ufw = await sh("sudo -n ufw status verbose 2>/dev/null || true");
const ufwOut = ufw.ok ? ufw.stdout.toLowerCase() : "";
if (ufwOut.trim() === "") {
const unknownUfw = (step: string): CheckRow => ({ step, status: "unknown", detail: "ufw status not readable from this vantage (needs sudo)", fix: "on the host: sudo ufw status verbose" });
rows.push(unknownUfw("ufw-default-deny-incoming"));
rows.push(unknownUfw("tailnet-ingress-allowed"));
} else {
rows.push(/default:\s*deny\s*\(incoming/.test(ufwOut)
? { step: "ufw-default-deny-incoming", status: "pass", detail: "ufw default deny (incoming)" }
: { step: "ufw-default-deny-incoming", status: "fail", detail: "ufw incoming default is not deny", fix: "sudo ufw default deny incoming" });
// R2-B1: mentioning tailscale0 is NOT enough — a DENY on tailscale0
// must never pass. Pass requires an ALLOW IN rule on the interface.
const tailnetAllowIn = ufwOut.split("\n").some((line) => line.includes("tailscale0") && /allow\s+in/.test(line));
if (tailnetAllowIn) {
rows.push({ step: "tailnet-ingress-allowed", status: "pass", detail: "ufw has an ALLOW IN rule on tailscale0" });
} else if (ufwOut.includes("tailscale0")) {
rows.push({ step: "tailnet-ingress-allowed", status: "fail", detail: "tailscale0 appears in ufw rules but with no ALLOW IN rule (deny/other only)", fix: "sudo ufw allow in on tailscale0" });
} else {
rows.push({ step: "tailnet-ingress-allowed", status: "fail", detail: "no tailscale0 allow rule found", fix: "sudo ufw allow in on tailscale0" });
}
}
// 6. tailscale flags (no subnet routes / exit node / ts-SSH)
const ts = await sh("tailscale status --json 2>/dev/null || true");
const prefs = await sh("tailscale debug prefs --json 2>/dev/null || true");
let routes: number | undefined;
let exitNode: boolean | undefined;
let runSsh: boolean | undefined;
const unknownReasons: string[] = [];
try {
if (ts.ok && ts.stdout.trim().startsWith("{")) {
const parsed = JSON.parse(ts.stdout) as { Self?: { PrimaryRoutes?: unknown[]; ExitNodeOption?: boolean }; ExitNodeStatus?: unknown };
routes = parsed.Self?.PrimaryRoutes?.length ?? 0;
exitNode = (parsed.ExitNodeStatus !== undefined && parsed.ExitNodeStatus !== null) || parsed.Self?.ExitNodeOption === true;
} else {
unknownReasons.push("tailscale status unreadable");
}
} catch {
unknownReasons.push("tailscale status did not parse");
}
try {
runSsh = prefs.ok ? tailscaleRunSshFromPrefs(prefs.stdout) : undefined;
if (runSsh === undefined) unknownReasons.push("tailscale debug prefs unreadable");
} catch {
unknownReasons.push("tailscale debug prefs did not parse");
}
if ((routes ?? 0) > 0 || exitNode === true || runSsh === true) {
rows.push({
step: "tailscale-minimal-trust",
status: "fail",
detail: `subnet routes: ${routes ?? "unknown"}; exit node in use: ${exitNode ?? "unknown"}; Tailscale SSH enabled: ${runSsh ?? "unknown"}`,
fix: "remove advertised routes / exit-node use (tailscale set) and disable Tailscale SSH: tailscale set --ssh=false",
});
} else if (routes === 0 && exitNode === false && runSsh === false) {
rows.push({ step: "tailscale-minimal-trust", status: "pass", detail: "no advertised subnet routes; no exit-node use; Tailscale SSH disabled" });
} else {
rows.push({
step: "tailscale-minimal-trust",
status: "unknown",
detail: `tailscale posture not fully determined (${unknownReasons.join("; ")})`,
fix: "on the host: tailscale status --json && tailscale debug prefs --json; disable Tailscale SSH with tailscale set --ssh=false",
});
}
// 7. daemon bound loopback/tailnet — never public. R2-B1: a SPECIFIC
// public IP bind must fail exactly like a wildcard — pass is an
// ALLOWLIST (loopback or the tailnet CGNAT range 100.64.0.0/10), never
// "not wildcard".
const ss = await sh("ss -tln 2>/dev/null | grep ':7433' || true");
const ssOut = ss.ok ? ss.stdout : "";
const listenAddrs = ssOut
.split("\n")
.map((line) => line.match(/(\S+):7433\b/)?.[1])
.filter((a): a is string => a !== undefined);
if (listenAddrs.length === 0) {
rows.push({ step: "daemon-bind-not-public", status: "unknown", detail: "no listener on :7433 observed (daemon may be down)", fix: "start the daemon, then re-check: ss -tln | grep 7433" });
} else {
const isLoopbackOrTailnet = (addr: string): boolean => {
const a = addr.replace(/^\[|\]$/g, "");
if (a.startsWith("127.") || a === "::1") return true;
const m = a.match(/^100\.(\d+)\./);
return m !== null && Number(m[1]) >= 64 && Number(m[1]) <= 127;
};
const offending = listenAddrs.filter((a) => !isLoopbackOrTailnet(a));
rows.push(offending.length === 0
? { step: "daemon-bind-not-public", status: "pass", detail: `listener(s) bound loopback/tailnet only: ${listenAddrs.join(", ")}` }
: { step: "daemon-bind-not-public", status: "fail", detail: `daemon listens on a non-loopback/non-tailnet address: ${offending.join(", ")}`, fix: "bind the daemon to loopback/tailnet only (OPENRIG daemon host config); never expose :7433 publicly" });
}
// 8+9. public reachability probes — only meaningful with a known PUBLIC address.
if (opts.publicAddr) {
for (const [step, port] of [["public-daemon-port-unreachable", 7433], ["public-ssh-unreachable-or-accepted", 22]] as const) {
const probe = await deps.tcpProbe(opts.publicAddr, port, 5000);
rows.push(probe === "open"
? { step, status: "fail", detail: `${opts.publicAddr}:${port} is publicly reachable`, fix: "remove the public allow rule / firewall the port (the smoke-test hardening bar: public :22/:7433 time out)" }
: { step, status: "pass", detail: `${opts.publicAddr}:${port} not reachable (${probe})` });
}
} else {
const unknownProbe = (step: string, port: number): CheckRow => ({
step, status: "unknown",
detail: `public reachability of :${port} not probed (no public address known — the registry target may be a tailnet alias)`,
fix: `re-run with --public-addr <ip> to probe from this vantage`,
});
rows.push(unknownProbe("public-daemon-port-unreachable", 7433));
rows.push(unknownProbe("public-ssh-unreachable-or-accepted", 22));
}
return rows;
}
export const POSTURE_ITEM_IDS = [
"nonroot-openrig-user",
"root-ssh-disabled",
"key-only-ssh",
"ufw-default-deny-incoming",
"tailnet-ingress-allowed",
"tailscale-minimal-trust",
"daemon-bind-not-public",
"public-daemon-port-unreachable",
"public-ssh-unreachable-or-accepted",
] as const;
function renderRows(rows: CheckRow[], json: boolean | undefined): void {
if (json) {
console.log(JSON.stringify(rows));
return;
}
const glyph = { pass: "✓", fail: "✗", unknown: "?" } as const;
for (const r of rows) {
console.log(`${glyph[r.status]} ${r.step}: ${r.detail}${r.fix ? `\n fix: ${r.fix}` : ""}`);
}
}
function authPointer(h: HostEntry): string {
if (h.transport === "ssh") return "ssh-key";
return h.bearer_env ? `env:${h.bearer_env}` : h.bearer_file ? `file:${h.bearer_file}` : "—";
}
export function hostCommand(doctorDepsOverride?: DoctorDeps): Command {
const cmd = new Command("host").description("Manage the multi-host registry (~/.openrig/hosts.yaml)");
cmd
.command("add")
.description("Add a host entry (validated with the registry loader's own rules)")
.requiredOption("--id <id>", "Unique host id")
.requiredOption("--transport <transport>", "ssh or http")
.option("--target <target>", "SSH target (DNS name, ssh-config alias, or IP) — ssh transport")
.option("--user <user>", "SSH user — ssh transport")
.option("--url <url>", "Remote daemon base URL — http transport")
.option("--bearer-env <name>", "Env var NAME holding the bearer token — http transport (pointer, never a value)")
.option("--bearer-file <path>", "File PATH holding the bearer token — http transport (pointer, never a value)")
.option("--notes <text>", "Free-form operator note")
.option("--json", "JSON output")
.action((opts: { id: string; transport: string; target?: string; user?: string; url?: string; bearerEnv?: string; bearerFile?: string; notes?: string; json?: boolean }) => {
const rawEntry: Record<string, unknown> = { id: opts.id, transport: opts.transport };
if (opts.target !== undefined) rawEntry["target"] = opts.target;
if (opts.user !== undefined) rawEntry["user"] = opts.user;
if (opts.url !== undefined) rawEntry["url"] = opts.url;
if (opts.bearerEnv !== undefined) rawEntry["bearer_env"] = opts.bearerEnv;
if (opts.bearerFile !== undefined) rawEntry["bearer_file"] = opts.bearerFile;
if (opts.notes !== undefined) rawEntry["notes"] = opts.notes;
const res = addHostEntry(rawEntry);
if (!res.ok) {
console.error(res.error);
process.exitCode = 1;
return;
}
if (opts.json) {
console.log(JSON.stringify({ ok: true, path: res.path, entry: res.entry }));
return;
}
console.log(`Added host '${res.entry.id}' (${res.entry.transport}) to ${res.path}`);
console.log(`Verify it: rig host doctor ${res.entry.id}`);
});
cmd
.command("list")
.description("List registered hosts (config pointers only — never secret values)")
.option("--json", "JSON output")
.action((opts: { json?: boolean }) => {
const registryPath = defaultHostRegistryPath();
if (!existsSync(registryPath)) {
if (opts.json) {
console.log(JSON.stringify([]));
return;
}
console.log(`No hosts registered in ${registryPath}. Add one: rig host add --id <id> --transport <ssh|http> ...`);
return;
}
const loaded = loadHostRegistry(registryPath);
if (!loaded.ok) {
console.error(loaded.error);
process.exitCode = 1;
return;
}
if (opts.json) {
// Entries as validated: bearer fields are NAMES by construction.
console.log(JSON.stringify(loaded.registry.hosts));
return;
}
if (loaded.registry.hosts.length === 0) {
console.log(`No hosts registered in ${registryPath}. Add one: rig host add --id <id> --transport <ssh|http> ...`);
return;
}
const pad = (s: string, n: number) => (s.length > n ? s.slice(0, n - 1) + "…" : s.padEnd(n));
console.log(`${pad("ID", 20)} ${pad("TRANSPORT", 10)} ${pad("TARGET", 36)} ${pad("AUTH", 24)} NOTES`);
for (const h of loaded.registry.hosts) {
console.log(`${pad(h.id, 20)} ${pad(h.transport, 10)} ${pad(hostDisplayTarget(h), 36)} ${pad(authPointer(h), 24)} ${h.notes ?? "—"}`);
}
});
cmd
.command("doctor")
.description("Stepwise host verification (+ --posture for the product-factory-vps baseline)")
.argument("<id>", "Registered host id")
.option("--posture <profile>", `Run the posture baseline (only built-in profile: ${POSTURE_PROFILE_ID})`)
.option("--public-addr <ip>", "Public address for the outside-vantage reachability probes (posture items 8-9)")
.option("--json", "JSON output")
.action(async (id: string, opts: { posture?: string; publicAddr?: string; json?: boolean }) => {
const deps: DoctorDeps = doctorDepsOverride ?? defaultDoctorDeps();
const loaded = loadHostRegistry();
if (!loaded.ok) {
console.error(loaded.error);
process.exitCode = 1;
return;
}
const resolved = resolveHost(loaded.registry, id);
if (!resolved.ok) {
// The DISTINCT "registry is wrong" error class.
console.error(`registry: ${resolved.error}`);
process.exitCode = 1;
return;
}
if (opts.posture !== undefined && opts.posture !== POSTURE_PROFILE_ID) {
console.error(`unknown posture profile '${opts.posture}'. The only built-in profile is: ${POSTURE_PROFILE_ID}`);
process.exitCode = 1;
return;
}
const rows = await doctorLegs(resolved.host, deps);
if (opts.posture) {
rows.push(...await postureCheck(resolved.host, deps, { publicAddr: opts.publicAddr }));
}
renderRows(rows, opts.json);
if (rows.some((r) => r.status === "fail")) process.exitCode = 1;
const unknowns = rows.filter((r) => r.status === "unknown").length;
if (!opts.json && unknowns > 0) {
console.log(`${unknowns} item${unknowns === 1 ? "" : "s"} UNKNOWN — unknown is not pass; see the fix lines to verify.`);
}
});
return cmd;
}