diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbb28b8a4c..a677f771ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai - LINE/Inline directives auth: gate directive parsing (`/model`, `/think`, `/verbose`, `/reasoning`, `/queue`) on resolved authorization (`command.isAuthorizedSender`) so `commands.allowFrom`-authorized LINE senders are not silently stripped when raw `CommandAuthorized` is unset. Landed from contributor PR #27248 by @kevinWangSheng. (#27240) - Web tools/Proxy: route `web_search` provider HTTP calls (Brave, Perplexity, xAI, Gemini, Kimi), redirect resolution, and `web_fetch` through a shared proxy-aware SSRF guard path so gateway installs behind `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` no longer fail with transport `fetch failed` errors. (#27430) thanks @kevinWangSheng. - CLI/Gateway status: force local `gateway status` probe host to `127.0.0.1` for `bind=lan` so co-located probes do not trip non-loopback plaintext WebSocket checks. (#26997) thanks @chikko80. +- CLI/Daemon status TLS probe: use `wss://` and forward local TLS certificate fingerprint for TLS-enabled gateway daemon probes so `openclaw daemon status` works with `gateway.bind=lan` + `gateway.tls.enabled=true`. (#24234) thanks @liuy. - Gateway/Bind visibility: emit a startup warning when binding to non-loopback addresses so operators get explicit exposure guidance in runtime logs. (#25397) thanks @let5sne. - Podman/Default bind: change `run-openclaw-podman.sh` default gateway bind from `lan` to `loopback` and document explicit LAN opt-in with Control UI origin configuration. (#27491) thanks @robbyczgw-cla. - Daemon/macOS launchd: forward proxy env vars into supervised service environments, keep LaunchAgent `KeepAlive=true` semantics, and harden restart sequencing to `print -> bootout -> wait old pid exit -> bootstrap -> kickstart`. (#27276) thanks @frankekn. diff --git a/src/cli/daemon-cli/status.gather.test.ts b/src/cli/daemon-cli/status.gather.test.ts new file mode 100644 index 00000000000..826f80f6edf --- /dev/null +++ b/src/cli/daemon-cli/status.gather.test.ts @@ -0,0 +1,150 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { captureEnv } from "../../test-utils/env.js"; + +const callGatewayStatusProbe = vi.fn(async () => ({ ok: true as const })); +const loadGatewayTlsRuntime = vi.fn(async () => ({ + enabled: true, + required: true, + fingerprintSha256: "sha256:11:22:33:44", +})); +const findExtraGatewayServices = vi.fn(async () => []); +const inspectPortUsage = vi.fn(async (port: number) => ({ + port, + status: "free" as const, + listeners: [], + hints: [], +})); +const readLastGatewayErrorLine = vi.fn(async () => null); +const auditGatewayServiceConfig = vi.fn(async () => undefined); +const serviceIsLoaded = vi.fn(async () => true); +const serviceReadRuntime = vi.fn(async () => ({ status: "running" })); +const serviceReadCommand = vi.fn(async () => ({ + programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"], + environment: { + OPENCLAW_STATE_DIR: "/tmp/openclaw-daemon", + OPENCLAW_CONFIG_PATH: "/tmp/openclaw-daemon/openclaw.json", + }, +})); +const resolveGatewayBindHost = vi.fn(async () => "0.0.0.0"); +const pickPrimaryTailnetIPv4 = vi.fn(() => "100.64.0.9"); +const resolveGatewayPort = vi.fn((_cfg?: unknown) => 18789); +const resolveStateDir = vi.fn( + (env: NodeJS.ProcessEnv) => env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw-cli", +); +const resolveConfigPath = vi.fn((env: NodeJS.ProcessEnv, stateDir: string) => { + return env.OPENCLAW_CONFIG_PATH ?? `${stateDir}/openclaw.json`; +}); + +vi.mock("../../config/config.js", () => ({ + createConfigIO: ({ configPath }: { configPath: string }) => { + const isDaemon = configPath.includes("/openclaw-daemon/"); + return { + readConfigFileSnapshot: async () => ({ + path: configPath, + exists: true, + valid: true, + issues: [], + }), + loadConfig: () => + isDaemon + ? { + gateway: { + bind: "lan", + tls: { enabled: true }, + auth: { token: "daemon-token" }, + }, + } + : { + gateway: { + bind: "loopback", + }, + }, + }; + }, + resolveConfigPath: (env: NodeJS.ProcessEnv, stateDir: string) => resolveConfigPath(env, stateDir), + resolveGatewayPort: (cfg?: unknown, env?: unknown) => resolveGatewayPort(cfg, env), + resolveStateDir: (env: NodeJS.ProcessEnv) => resolveStateDir(env), +})); + +vi.mock("../../daemon/diagnostics.js", () => ({ + readLastGatewayErrorLine: (env: NodeJS.ProcessEnv) => readLastGatewayErrorLine(env), +})); + +vi.mock("../../daemon/inspect.js", () => ({ + findExtraGatewayServices: (env: unknown, opts?: unknown) => findExtraGatewayServices(env, opts), +})); + +vi.mock("../../daemon/service-audit.js", () => ({ + auditGatewayServiceConfig: (opts: unknown) => auditGatewayServiceConfig(opts), +})); + +vi.mock("../../daemon/service.js", () => ({ + resolveGatewayService: () => ({ + label: "LaunchAgent", + loadedText: "loaded", + notLoadedText: "not loaded", + isLoaded: serviceIsLoaded, + readCommand: serviceReadCommand, + readRuntime: serviceReadRuntime, + }), +})); + +vi.mock("../../gateway/net.js", () => ({ + resolveGatewayBindHost: (bindMode: string, customBindHost?: string) => + resolveGatewayBindHost(bindMode, customBindHost), +})); + +vi.mock("../../infra/ports.js", () => ({ + inspectPortUsage: (port: number) => inspectPortUsage(port), + formatPortDiagnostics: () => [], +})); + +vi.mock("../../infra/tailnet.js", () => ({ + pickPrimaryTailnetIPv4: () => pickPrimaryTailnetIPv4(), +})); + +vi.mock("../../infra/tls/gateway.js", () => ({ + loadGatewayTlsRuntime: (cfg: unknown) => loadGatewayTlsRuntime(cfg), +})); + +vi.mock("./probe.js", () => ({ + probeGatewayStatus: (opts: unknown) => callGatewayStatusProbe(opts), +})); + +const { gatherDaemonStatus } = await import("./status.gather.js"); + +describe("gatherDaemonStatus", () => { + let envSnapshot: ReturnType; + + beforeEach(() => { + envSnapshot = captureEnv(["OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH"]); + process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-cli"; + process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-cli/openclaw.json"; + callGatewayStatusProbe.mockClear(); + loadGatewayTlsRuntime.mockClear(); + }); + + afterEach(() => { + envSnapshot.restore(); + }); + + it("uses wss probe URL and forwards TLS fingerprint when daemon TLS is enabled", async () => { + const status = await gatherDaemonStatus({ + rpc: {}, + probe: true, + deep: false, + }); + + expect(loadGatewayTlsRuntime).toHaveBeenCalledTimes(1); + expect(callGatewayStatusProbe).toHaveBeenCalledWith( + expect.objectContaining({ + url: "wss://127.0.0.1:19001", + tlsFingerprint: "sha256:11:22:33:44", + token: "daemon-token", + }), + ); + expect(status.gateway?.probeUrl).toBe("wss://127.0.0.1:19001"); + expect(status.rpc?.url).toBe("wss://127.0.0.1:19001"); + expect(status.rpc?.ok).toBe(true); + }); +});