mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
fix(cli): preserve user-explicit-vs-default host signal through lifecycle
Guard review on cafcfa5 caught a real bug: the CLI daemon-start
lifecycle always exported `OPENRIG_HOST=127.0.0.1` to the daemon
process even when the operator never set it (config default
propagated through `startDaemon` → `buildDaemonEnv`). The daemon's
`index.ts` saw `OPENRIG_HOST` set and took the explicit-host branch,
single-binding loopback only — the multi-bind tailnet auto-detect
path never ran on a default product launch.
Fix: thread the user-explicit signal through the lifecycle.
- `packages/cli/src/commands/daemon.ts`: probe ConfigStore for the
host setting's source (`resolveWithSource("daemon.host")`). When the
source is `default` AND no `--host` flag, pass `host: undefined` to
`startDaemon`. When source is `env`, `file`, or a CLI flag is
present, pass the resolved host (operator opt-in path).
- `packages/cli/src/daemon-lifecycle.ts`:
* `StartOptions.host` was already optional; `startDaemon` now
threads `explicitHost = opts.host` (may be undefined) into
`buildDaemonEnv` and uses `probeHost = explicitHost ?? DEFAULT_HOST`
for the healthz probe + persisted state.host.
* `buildDaemonEnv` makes `opts.host` optional in its signature and
only sets `env["OPENRIG_HOST"]` when defined. If the operator's
shell already has `OPENRIG_HOST` in baseEnv, the env-passthrough
loop preserves that value (shell-level opt-in is honored).
After this fix, the three input paths each correctly signal the
daemon:
- `OPENRIG_HOST` set in shell → preserved through baseEnv →
daemon explicit path.
- `rig config set daemon.host X` → resolveWithSource source=`file`
→ CLI exports `OPENRIG_HOST=X` → daemon explicit path.
- No env, no config, no `--host` → source=`default` → CLI does NOT
export `OPENRIG_HOST` → daemon falls through to
multi-bind (loopback + tailscale auto-detect).
Tests:
- 3 new cases in daemon-lifecycle.test.ts asserting
`buildDaemonEnv` omits/sets/respects-shell-OPENRIG_HOST correctly.
- daemon-lifecycle.test.ts: 52/52 green (was 49).
- Full sweep (env vars cleaned): 4222/4223 — the one failure is
environmental (`packages/cli/dist/index.js` missing because the
worktree is unbuilt; unrelated to this change).
- daemon tsc + cli tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
cafcfa5e41
commit
f84161289c
3 files changed
+82
-9
No files matched your search
@@ -85,7 +85,14 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
|
||||
const configStore = new ConfigStore();
|
||||
const config = configStore.resolve();
|
||||
const effectivePort = opts.port ? parseInt(opts.port, 10) : config.daemon.port;
|
||||
// bug-fix slice auth-bearer-tailscale-trust: distinguish
|
||||
// user-explicit from default-fallback so the daemon can
|
||||
// multi-bind (loopback + tailscale auto-detect) when the operator
|
||||
// never opted in to a specific host.
|
||||
const hostResolution = configStore.resolveWithSource("daemon.host");
|
||||
const hostUserExplicit = opts.host !== undefined || hostResolution.source !== "default";
|
||||
const effectiveHost = opts.host ?? config.daemon.host;
|
||||
const hostForDaemon = hostUserExplicit ? effectiveHost : undefined;
|
||||
|
||||
// Run preflight before starting
|
||||
const preflight = new SystemPreflight({
|
||||
@@ -108,7 +115,7 @@ export function daemonCommand(depsOverride?: LifecycleDeps): Command {
|
||||
const state = await startDaemon(
|
||||
{
|
||||
port: effectivePort,
|
||||
host: effectiveHost,
|
||||
host: hostForDaemon,
|
||||
db: opts.db ?? config.db.path,
|
||||
transcriptsEnabled: config.transcripts.enabled,
|
||||
transcriptsPath: config.transcripts.path,
|
||||
|
||||
@@ -242,7 +242,17 @@ export function buildDaemonEnv(
|
||||
baseEnv: Record<string, string>,
|
||||
opts: {
|
||||
port: number;
|
||||
host: string;
|
||||
/**
|
||||
* Operator-explicit bind host. When undefined the CLI is signaling
|
||||
* "no operator opt-in" so the daemon's index.ts can fall through to
|
||||
* the default loopback+tailscale-auto multi-bind path (bug-fix slice
|
||||
* auth-bearer-tailscale-trust). When defined, the daemon takes the
|
||||
* explicit-host branch and applies the bearer invariant to it.
|
||||
* If undefined here AND the operator shell already has OPENRIG_HOST
|
||||
* set in baseEnv, the env-passthrough loop preserves that value
|
||||
* (their shell-level opt-in still wins).
|
||||
*/
|
||||
host?: string;
|
||||
db: string;
|
||||
transcriptsEnabled?: boolean;
|
||||
transcriptsPath?: string;
|
||||
@@ -263,7 +273,9 @@ export function buildDaemonEnv(
|
||||
|
||||
// Explicit OPENRIG_* overrides — always win over inherited values
|
||||
env["OPENRIG_PORT"] = String(opts.port);
|
||||
env["OPENRIG_HOST"] = opts.host;
|
||||
if (opts.host !== undefined) {
|
||||
env["OPENRIG_HOST"] = opts.host;
|
||||
}
|
||||
env["OPENRIG_DB"] = opts.db;
|
||||
if (opts.transcriptsEnabled !== undefined) {
|
||||
env["OPENRIG_TRANSCRIPTS_ENABLED"] = String(opts.transcriptsEnabled);
|
||||
@@ -283,8 +295,15 @@ export function buildDaemonEnv(
|
||||
|
||||
export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Promise<DaemonState> {
|
||||
const port = opts.port ?? DEFAULT_PORT;
|
||||
const host = opts.host ?? DEFAULT_HOST;
|
||||
const db = opts.db ?? DEFAULT_DB;
|
||||
// bug-fix slice auth-bearer-tailscale-trust: preserve the
|
||||
// user-explicit-vs-default distinction. `opts.host` is undefined when
|
||||
// the operator never opted in; in that case healthz probes still use
|
||||
// the loopback default, but buildDaemonEnv must NOT export
|
||||
// OPENRIG_HOST so the daemon falls through to the multi-bind default
|
||||
// (loopback + tailscale auto-detect).
|
||||
const explicitHost = opts.host;
|
||||
const probeHost = explicitHost ?? DEFAULT_HOST;
|
||||
|
||||
// Check if already running
|
||||
const existing = readState(deps);
|
||||
@@ -301,7 +320,7 @@ export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Prom
|
||||
} else {
|
||||
let recoveredRunning = false;
|
||||
try {
|
||||
await fetchDaemonProbe(deps, `http://${host}:${port}/healthz`);
|
||||
await fetchDaemonProbe(deps, `http://${probeHost}:${port}/healthz`);
|
||||
recoveredRunning = true;
|
||||
} catch (err) {
|
||||
if (err instanceof HealthProbeTimeoutError) {
|
||||
@@ -324,7 +343,7 @@ export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Prom
|
||||
const child = deps.spawn(process.execPath, [daemonEntry], {
|
||||
env: buildDaemonEnv(process.env as Record<string, string>, {
|
||||
port,
|
||||
host,
|
||||
host: explicitHost,
|
||||
db,
|
||||
transcriptsEnabled: opts.transcriptsEnabled,
|
||||
transcriptsPath: opts.transcriptsPath,
|
||||
@@ -339,8 +358,9 @@ export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Prom
|
||||
|
||||
const pid = child.pid!;
|
||||
|
||||
// Poll healthz
|
||||
const healthzUrl = `http://${host}:${port}/healthz`;
|
||||
// Poll healthz against the configured probe host (loopback default
|
||||
// when operator didn't opt in to a specific bind).
|
||||
const healthzUrl = `http://${probeHost}:${port}/healthz`;
|
||||
let healthy = false;
|
||||
for (let i = 0; i < HEALTHZ_RETRIES; i++) {
|
||||
try {
|
||||
@@ -364,7 +384,10 @@ export async function startDaemon(opts: StartOptions, deps: LifecycleDeps): Prom
|
||||
const state: DaemonState = {
|
||||
pid,
|
||||
port,
|
||||
host,
|
||||
// Persist the probe host (operator-explicit when set, loopback
|
||||
// default otherwise) so subsequent status reads can reach the
|
||||
// daemon regardless of whether tailscale was added at multi-bind.
|
||||
host: probeHost,
|
||||
db,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -938,6 +938,49 @@ describe("buildDaemonEnv", () => {
|
||||
expect(result["OPENRIG_DB"]).toBe("/new/path.db");
|
||||
});
|
||||
|
||||
// bug-fix slice auth-bearer-tailscale-trust forward-fix:
|
||||
// Default-product-launch must NOT export OPENRIG_HOST so daemon's
|
||||
// index.ts falls through to the loopback+tailscale multi-bind path.
|
||||
describe("auth-bearer-tailscale-trust: explicit-vs-default host signal", () => {
|
||||
it("omits OPENRIG_HOST when opts.host is undefined (default-product-launch)", () => {
|
||||
const baseEnv: Record<string, string> = { HOME: "/Users/tester", PATH: "/usr/bin" };
|
||||
const result = buildDaemonEnv(baseEnv, {
|
||||
port: 7433,
|
||||
db: "/tmp/test.db",
|
||||
// host intentionally omitted — operator never set it (default path)
|
||||
});
|
||||
expect(result["OPENRIG_HOST"]).toBeUndefined();
|
||||
// port + db still flow through
|
||||
expect(result["OPENRIG_PORT"]).toBe("7433");
|
||||
expect(result["OPENRIG_DB"]).toBe("/tmp/test.db");
|
||||
});
|
||||
|
||||
it("sets OPENRIG_HOST when opts.host is explicit (user opt-in)", () => {
|
||||
const baseEnv: Record<string, string> = { HOME: "/Users/tester", PATH: "/usr/bin" };
|
||||
const result = buildDaemonEnv(baseEnv, {
|
||||
port: 7433,
|
||||
host: "0.0.0.0",
|
||||
db: "/tmp/test.db",
|
||||
});
|
||||
expect(result["OPENRIG_HOST"]).toBe("0.0.0.0");
|
||||
});
|
||||
|
||||
it("does NOT scrub OPENRIG_HOST from baseEnv when opts.host is undefined (operator shell env wins)", () => {
|
||||
// If the operator has OPENRIG_HOST set in their shell, that's an
|
||||
// explicit signal — preserve it through the env-passthrough loop.
|
||||
const baseEnv: Record<string, string> = {
|
||||
HOME: "/Users/tester",
|
||||
PATH: "/usr/bin",
|
||||
OPENRIG_HOST: "0.0.0.0",
|
||||
};
|
||||
const result = buildDaemonEnv(baseEnv, {
|
||||
port: 7433,
|
||||
db: "/tmp/test.db",
|
||||
});
|
||||
expect(result["OPENRIG_HOST"]).toBe("0.0.0.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("V1 pre-release CLI/daemon Item 1 — transcript rotation tunables projection", () => {
|
||||
it("projects transcriptsLines + transcriptsPollIntervalSeconds into OPENRIG_* env vars when provided", () => {
|
||||
const baseEnv: Record<string, string> = { HOME: "/Users/tester", PATH: "/usr/bin" };
|
||||
|
||||
Reference in new issue
Block a user