diff --git a/docs/CLI.md b/docs/CLI.md index 14f948689..334a6216d 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -2294,13 +2294,14 @@ A daemon that refuses to boot because of its configuration — a either, `mode: "gateway"` with `http.enabled: false`, or, in worker mode, `gateway.url` without `gateway.token` or the reverse; see [CONFIGURATION.md](CONFIGURATION.md) for the full set — fails the start -rather than picking a value the operator did not write. That validation runs -before the socket is claimed, so a command that auto-starts the daemon -(`simlock lease`, the MCP server) never gets a daemon to talk to: the launch -times out and the command fails with exit 1 (`INTERNAL`). The error line says nothing -about the config, because nothing ever answered — the reason is in `simlock -daemon logs`, which reads the log file directly and so works even though the -daemon never came up. +rather than picking a value the operator did not write. A command that +would auto-start the daemon (`simlock daemon start`, `simlock lease`) loads +`config.json` first. When the config is invalid it starts no daemon, prints the +error line with the config message (for example `Invalid config value for +"capacity.config.limits.android.maxDevices": expected a positive integer`), and +exits 1 (`INTERNAL`). A daemon that is already running is not affected. The +daemon logs the same reason, and `simlock daemon logs` reads it even when no +daemon is up. The daemon writes one JSON line per record to `~/.simlock/daemon.log` (timestamp, level, module, message, and any fields). The log says what the diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index fbe677287..40504f3c2 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -1208,6 +1208,32 @@ describe("CLI: admin credential resolution (ADR 0003 §5)", () => { expect(output.stderr).not.toContain("notice"); }); + it("daemon start reports the config error and launches nothing when config.json is invalid", async () => { + const filesystem = new MemoryFilesystem(); + await filesystem.mkdirp("/simlock"); + await filesystem.writeFileAtomic( + "/simlock/config.json", + JSON.stringify({ capacity: { config: { limits: { android: { maxDevices: 0 } } } } }), + ); + const launcher = new FakeDaemonLauncher(); + const output = outputCapture({ + filesystem, + clock: new FakeClock(0), + systemStats: new FakeSystemStats({ + cpuCount: 8, + totalRamBytes: 32 * gibibyte, + }), + ipc: new MemoryIpcTransport(), + launcher, + dataDirectory: "/simlock", + }); + const exitCode = await runCli(["daemon", "start"], output.environmentWith()); + expect(exitCode).toBe(1); + expect(launcher.launches).toBe(0); + expect(output.stderr).toContain("limits.android.maxDevices"); + expect(output.stderr).not.toContain("Timed out"); + }); + it("B1: a stale admin.token degrades to an agent session instead of bricking the invocation", async () => { const filesystem = new MemoryFilesystem(); const ipcTransport = new MemoryIpcTransport(); diff --git a/src/cli/index.ts b/src/cli/index.ts index 8bde4dd2c..43007feca 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -316,6 +316,7 @@ class AutoLaunchIpcConnector implements IpcConnector { private readonly ipc: IpcConnector, private readonly clock: Clock, private readonly launcher: DaemonLauncher, + private readonly checkConfig: () => Promise, ) {} async connect(endpoint: string): Promise { @@ -324,6 +325,7 @@ class AutoLaunchIpcConnector implements IpcConnector { } catch (error: unknown) { if (!isUnavailable(error)) throw error; } + await this.checkConfig(); await this.launcher.launch(); const deadline = this.clock.now() + 5_000; let lastError: unknown; @@ -392,7 +394,11 @@ export function buildCliEnvironment( // per-invocation `--agent-id` flag on `lease` (parsed at that command's own boundary) takes // precedence over this default. const requesterId = resolveRequesterId(env, String(process.pid)); - const autoLaunchIpc = new AutoLaunchIpcConnector(ipc, clock, launcher); + // why: the daemon refuses to boot on a config `loadConfig` rejects and only its log says so; + // loading the same file before the launch puts the `ConfigError` on the CLI's stderr. + const autoLaunchIpc = new AutoLaunchIpcConnector(ipc, clock, launcher, async () => { + await loadConfig({ configPath, filesystem, systemStats }); + }); // ADR §5 / B2: the raw connection (and, for `connectAdmin`, any auto-launch it triggers) is // established *before* `resolveCredential` is ever called -- `admin.token` is written only