diff --git a/CHANGELOG.md b/CHANGELOG.md index 9193d8853..2c0f7a465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438. - Doctor/Daemon: surface gateway runtime state + port collision diagnostics; warn on legacy workspace dirs. - Gateway/CLI: include gateway target/source details in close/timeout errors and verbose health/status output. +- Gateway/CLI: honor `gateway.auth.password` for local CLI calls when env is unset. Thanks @jeffersonwarrior for PR #301. - Discord: format slow listener logs in seconds to match shared duration style. - CLI: show colored table output for `clawdbot cron list` (JSON behind `--json`). - CLI: add cron `create`/`remove`/`delete` aliases for job management. diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index 389d12002..628f474bf 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -6,6 +6,8 @@ const pickPrimaryTailnetIPv4 = vi.fn(); let lastClientOptions: { url?: string; + token?: string; + password?: string; onHelloOk?: () => void | Promise; onClose?: (code: number, reason: string) => void; } | null = null; @@ -36,6 +38,8 @@ vi.mock("./client.js", () => ({ GatewayClient: class { constructor(opts: { url?: string; + token?: string; + password?: string; onHelloOk?: () => void | Promise; onClose?: (code: number, reason: string) => void; }) { @@ -162,3 +166,86 @@ describe("callGateway error details", () => { expect(err?.message).toContain("Bind: loopback"); }); }); + +describe("callGateway password resolution", () => { + const originalEnvPassword = process.env.CLAWDBOT_GATEWAY_PASSWORD; + + beforeEach(() => { + loadConfig.mockReset(); + resolveGatewayPort.mockReset(); + pickPrimaryTailnetIPv4.mockReset(); + lastClientOptions = null; + startMode = "hello"; + closeCode = 1006; + closeReason = ""; + delete process.env.CLAWDBOT_GATEWAY_PASSWORD; + resolveGatewayPort.mockReturnValue(18789); + pickPrimaryTailnetIPv4.mockReturnValue(undefined); + }); + + afterEach(() => { + if (originalEnvPassword == null) { + delete process.env.CLAWDBOT_GATEWAY_PASSWORD; + } else { + process.env.CLAWDBOT_GATEWAY_PASSWORD = originalEnvPassword; + } + }); + + it("uses local config password when env is unset", async () => { + loadConfig.mockReturnValue({ + gateway: { + mode: "local", + bind: "loopback", + auth: { password: "secret" }, + }, + }); + + await callGateway({ method: "health" }); + + expect(lastClientOptions?.password).toBe("secret"); + }); + + it("prefers env password over local config password", async () => { + process.env.CLAWDBOT_GATEWAY_PASSWORD = "from-env"; + loadConfig.mockReturnValue({ + gateway: { + mode: "local", + bind: "loopback", + auth: { password: "from-config" }, + }, + }); + + await callGateway({ method: "health" }); + + expect(lastClientOptions?.password).toBe("from-env"); + }); + + it("uses remote password in remote mode when env is unset", async () => { + loadConfig.mockReturnValue({ + gateway: { + mode: "remote", + remote: { url: "ws://remote.example:18789", password: "remote-secret" }, + auth: { password: "from-config" }, + }, + }); + + await callGateway({ method: "health" }); + + expect(lastClientOptions?.password).toBe("remote-secret"); + }); + + it("prefers env password over remote password in remote mode", async () => { + process.env.CLAWDBOT_GATEWAY_PASSWORD = "from-env"; + loadConfig.mockReturnValue({ + gateway: { + mode: "remote", + remote: { url: "ws://remote.example:18789", password: "remote-secret" }, + auth: { password: "from-config" }, + }, + }); + + await callGateway({ method: "health" }); + + expect(lastClientOptions?.password).toBe("from-env"); + }); +}); diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 5da72cdc9..391096d0f 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -29,6 +29,7 @@ export async function callGateway( const isRemoteMode = config.gateway?.mode === "remote"; const remote = isRemoteMode ? config.gateway?.remote : undefined; const authToken = config.gateway?.auth?.token; + const authPassword = config.gateway?.auth?.password; const localPort = resolveGatewayPort(config); const tailnetIPv4 = pickPrimaryTailnetIPv4(); const bindMode = config.gateway?.bind ?? "loopback"; @@ -64,9 +65,13 @@ export async function callGateway( ? opts.password.trim() : undefined) || process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || - (typeof remote?.password === "string" && remote.password.trim().length > 0 - ? remote.password.trim() - : undefined); + (isRemoteMode + ? typeof remote?.password === "string" && remote.password.trim().length > 0 + ? remote.password.trim() + : undefined + : typeof authPassword === "string" && authPassword.trim().length > 0 + ? authPassword.trim() + : undefined); const urlSource = urlOverride ? "cli --url" : remoteUrl