Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 7 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ class AutoLaunchIpcConnector implements IpcConnector {
private readonly ipc: IpcConnector,
private readonly clock: Clock,
private readonly launcher: DaemonLauncher,
private readonly checkConfig: () => Promise<void>,
) {}

async connect(endpoint: string): Promise<IpcConnection> {
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading