fix(cli): rig up respects daemon.host source (forward-fix #2)

Guard catch on f841612: the previous fix only covered `rig daemon
start`. `rig up` (auto-start path) also materialized
`resolvedConfig.daemon.host` into `startDaemon` regardless of whether
the value came from `env`/`file` (explicit) or `default` — so the
daemon's multi-bind path never ran on a default `rig up` launch
either. Same class of bug, second product launch path.

Applies the same `resolveWithSource("daemon.host")` discriminator to
`packages/cli/src/commands/up.ts`: when `source === "default"`, omit
`host` from the startDaemon call so `buildDaemonEnv` does not export
`OPENRIG_HOST` and the daemon falls through to loopback + tailscale
auto-detect. When `source === "env"` or `"file"`, pass the resolved
host through (operator opt-in path preserved).

Expanded forensic trace per banked feedback_forensic_trace and the
guard's expanded-audit directive: grep'd every `startDaemon` caller
in `packages/cli/src` — `daemon.ts` (fixed in f841612) and `up.ts`
(this commit). No other product launch paths call `startDaemon`.

Tests (TDD red→green with distinct-value discriminator per banked
feedback_poc_regression_must_discriminate):
- env-default scenario → `spawnedEnv["OPENRIG_HOST"]` undefined
- env-explicit scenario (`OPENRIG_HOST=100.64.55.66`) → preserved
  through spawn with the distinct tailscale-shape value, proving the
  preservation branch fired vs vacuous loopback-default match
- up.test.ts: 32/32 green (+2 new)
- daemon-lifecycle.test.ts: 52/52 green (unchanged)
- Full CLI+daemon sweep with env-var pollution cleaned: 4223/4225
  (same one environmental pre-existing fail: cli/dist/index.js
  missing in worktree)
- CLI tsc clean

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-11 03:54:44 -07:00
1 parent f84161289c
commit 0fb4e4e832
2 files changed
+107 -1

No files matched your search

+13 -1
View File
@@ -43,6 +43,14 @@ Examples:
transcripts: { enabled: boolean; path: string; lines: number; pollIntervalSeconds: number };
workspace: { root: string };
} | null = null;
// bug-fix slice auth-bearer-tailscale-trust: track whether
// daemon.host was operator-explicit (env or config file) vs
// default-fallback. The daemon's multi-bind path (loopback +
// tailscale auto-detect) only runs when OPENRIG_HOST is NOT
// exported to the child, so we omit it on the default path.
// Hoisted to function scope so the startDaemon block below can
// read it after the preflight try-catch.
let hostForDaemon: string | undefined;
try {
const { ConfigStore } = await import("../config-store.js");
const { SystemPreflight } = await import("../system-preflight.js");
@@ -50,6 +58,10 @@ Examples:
const { OPENRIG_DIR } = await import("../daemon-lifecycle.js");
const configStore = new ConfigStore();
resolvedConfig = configStore.resolve();
const hostResolution = configStore.resolveWithSource("daemon.host");
hostForDaemon = hostResolution.source === "default"
? undefined
: resolvedConfig.daemon.host;
const preflightExec = depsOverride?.preflightExec ?? (async (cmd: string) =>
execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }));
const preflight = new SystemPreflight({
@@ -77,7 +89,7 @@ Examples:
try {
await startDaemon({
port: resolvedConfig?.daemon.port,
host: resolvedConfig?.daemon.host,
host: hostForDaemon,
db: resolvedConfig?.db.path,
transcriptsEnabled: resolvedConfig?.transcripts.enabled,
transcriptsPath: resolvedConfig?.transcripts.path,
+94
View File
@@ -618,6 +618,100 @@ describe("Up CLI", () => {
expect(clientBaseUrl).toBe("http://127.0.0.1:7461");
});
// bug-fix slice auth-bearer-tailscale-trust forward-fix #2: `rig up`
// auto-start path was the second product launch path materializing
// the default daemon.host into OPENRIG_HOST. The discriminator below
// proves source=default → omit (so daemon multi-binds); source=env
// → preserve (operator opt-in).
describe("auth-bearer-tailscale-trust: up auto-start respects daemon.host source", () => {
function makeAutoStartDeps(captureSpawn: (env: Record<string, string>) => void): StatusDeps {
let daemonState: DaemonState | null = null;
let daemonStarted = false;
return {
lifecycleDeps: {
...mockLifecycleDeps(),
exists: vi.fn((p: string) => p === STATE_FILE ? daemonState !== null : false),
readFile: vi.fn((p: string) => p === STATE_FILE && daemonState ? JSON.stringify(daemonState) : null),
writeFile: vi.fn((p: string, content: string) => {
if (p === STATE_FILE) daemonState = JSON.parse(content) as DaemonState;
}),
openForAppend: vi.fn(() => 3),
mkdirp: vi.fn(),
spawn: vi.fn((cmd, args, opts) => {
daemonStarted = true;
captureSpawn(opts.env as Record<string, string>);
return { pid: 999, unref: vi.fn() } as never;
}),
fetch: vi.fn(async (url: string) => {
if (!daemonStarted) throw new Error(`refused:${url}`);
return { ok: url.includes("/healthz") };
}),
},
clientFactory: () => ({
post: vi.fn(async () => ({
status: 201,
data: { status: "completed", rigId: "rig-1", stages: [], errors: [], warnings: [] },
})),
} as unknown as DaemonClient),
};
}
it("default (no env, no config) does NOT export OPENRIG_HOST so daemon multi-binds", async () => {
const savedHost = process.env["OPENRIG_HOST"];
const savedPort = process.env["OPENRIG_PORT"];
delete process.env["OPENRIG_HOST"];
process.env["OPENRIG_PORT"] = "7471";
let spawnedEnv: Record<string, string> = {};
const deps = makeAutoStartDeps((env) => { spawnedEnv = env; });
const prog = new Command();
prog.exitOverride();
prog.addCommand(upCommand({ ...deps, preflightExec: healthyPreflightExec }));
await captureLogs(async () => {
await prog.parseAsync(["node", "rig", "up", "/tmp/test.yaml"]);
});
if (savedHost === undefined) delete process.env["OPENRIG_HOST"];
else process.env["OPENRIG_HOST"] = savedHost;
if (savedPort === undefined) delete process.env["OPENRIG_PORT"];
else process.env["OPENRIG_PORT"] = savedPort;
expect(spawnedEnv["OPENRIG_HOST"]).toBeUndefined();
expect(spawnedEnv["OPENRIG_PORT"]).toBe("7471");
});
it("env-explicit OPENRIG_HOST is preserved into spawn env (distinct value discriminator)", async () => {
const savedHost = process.env["OPENRIG_HOST"];
const savedPort = process.env["OPENRIG_PORT"];
// Use a distinct value so the discriminator can't pass vacuously:
// 100.64.55.66 is a tailscale CGNAT address — operator-explicit
// bind would normally be honored as-is.
process.env["OPENRIG_HOST"] = "100.64.55.66";
process.env["OPENRIG_PORT"] = "7472";
let spawnedEnv: Record<string, string> = {};
const deps = makeAutoStartDeps((env) => { spawnedEnv = env; });
const prog = new Command();
prog.exitOverride();
prog.addCommand(upCommand({ ...deps, preflightExec: healthyPreflightExec }));
await captureLogs(async () => {
await prog.parseAsync(["node", "rig", "up", "/tmp/test.yaml"]);
});
if (savedHost === undefined) delete process.env["OPENRIG_HOST"];
else process.env["OPENRIG_HOST"] = savedHost;
if (savedPort === undefined) delete process.env["OPENRIG_PORT"];
else process.env["OPENRIG_PORT"] = savedPort;
expect(spawnedEnv["OPENRIG_HOST"]).toBe("100.64.55.66");
expect(spawnedEnv["OPENRIG_PORT"]).toBe("7472");
});
});
it("up surfaces the real daemon auto-start failure instead of a generic hint", async () => {
const savedPort = process.env["OPENRIG_PORT"];
process.env["OPENRIG_PORT"] = "7463";