mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(cli): honor gateway --port on the status leaf (#130847)
This commit is contained in:
committed by
GitHub
parent
58fbbe2e04
commit
d3aa4be83a
+9
-8
@@ -23,18 +23,19 @@ openclaw daemon uninstall
|
||||
|
||||
## Subcommands and options
|
||||
|
||||
| Subcommand | Options |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `status` | `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json` |
|
||||
| `install` | `--port`, `--runtime <node\|bun>`, `--token`, `--wrapper <path>`, `--force`, `--json` |
|
||||
| `uninstall` | `--json` |
|
||||
| `start` | `--json` |
|
||||
| `stop` | `--force`, `--json`, `--disable` (launchd only: suppress KeepAlive/RunAtLoad until next start) |
|
||||
| `restart` | `--force`, `--safe`, `--skip-deferral`, `--wait <duration>`, `--json` |
|
||||
| Subcommand | Options |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `status` | `--url`, `--port`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json` |
|
||||
| `install` | `--port`, `--runtime <node\|bun>`, `--token`, `--wrapper <path>`, `--force`, `--json` |
|
||||
| `uninstall` | `--json` |
|
||||
| `start` | `--json` |
|
||||
| `stop` | `--force`, `--json`, `--disable` (launchd only: suppress KeepAlive/RunAtLoad until next start) |
|
||||
| `restart` | `--force`, `--safe`, `--skip-deferral`, `--wait <duration>`, `--json` |
|
||||
|
||||
`--json` is accepted before or after every subcommand (for example, `daemon --json status` and `daemon status --json`).
|
||||
|
||||
- `status`: shows service install state (launchd/systemd/schtasks) and probes Gateway health.
|
||||
- `status --port <port>`: selects a local Gateway using the invoking CLI config for auth and TLS. Cannot combine with `--url`; native service details remain diagnostic-only.
|
||||
- `install`: installs the service; `--force` reinstalls/overwrites an existing install.
|
||||
- Node is the primary, default, and recommended service runtime. Bun 1.4+ with WAL-reset-safe `node:sqlite` is available as an explicit opt-in with `install --runtime bun`.
|
||||
- `restart --safe`: asks the running Gateway to preflight active work and schedule one coalesced restart after work drains, bounded to 5 minutes. When that budget expires, the restart is forced anyway. Plain `restart` uses the service manager directly; `--force` is the immediate override.
|
||||
|
||||
+6
-2
@@ -344,10 +344,14 @@ Shows the Gateway service (launchd/systemd/schtasks) plus an optional connectivi
|
||||
openclaw gateway status
|
||||
openclaw gateway status --json
|
||||
openclaw gateway status --require-rpc
|
||||
openclaw gateway status --port 19001
|
||||
```
|
||||
|
||||
<ParamField path="--url <url>" type="string">
|
||||
Add an explicit probe target. Configured remote + localhost are still probed.
|
||||
Probe this explicit WebSocket URL instead of the service-derived target. Cannot combine with `--port`.
|
||||
</ParamField>
|
||||
<ParamField path="--port <port>" type="number">
|
||||
Select a local Gateway port using the invoking CLI config for auth and TLS. Accepts `gateway --port 19001 status` and `gateway status --port 19001`; an explicit status port wins. Native service details remain visible as diagnostics but do not select the probe target.
|
||||
</ParamField>
|
||||
<ParamField path="--token <token>" type="string">
|
||||
Token auth for the probe.
|
||||
@@ -592,7 +596,7 @@ openclaw gateway restart
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Command options">
|
||||
- `gateway status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `gateway status`: `--url`, `--port`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `gateway install`: `--port`, `--runtime <node|bun>` (default: `node`), `--token`, `--wrapper <path>`, `--force`, `--json`
|
||||
- `gateway restart`: `--safe`, `--skip-deferral`, `--force`, `--wait <duration>`, `--json`
|
||||
- `gateway uninstall|start`: `--json`
|
||||
|
||||
@@ -274,58 +274,63 @@ describe("probeGatewayStatus", () => {
|
||||
expect(result.version).toBe("2026.5.6");
|
||||
});
|
||||
|
||||
it("uses a real status RPC when requireRpc is enabled", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
probeGatewayMock.mockReset();
|
||||
callGatewayMock.mockImplementationOnce(async (opts) => {
|
||||
opts.onHelloOk?.({
|
||||
server: { version: "2026.8.1", buildId: "build-1", connId: "conn-1" },
|
||||
auth: { role: "operator", scopes: ["operator.admin"] },
|
||||
it.each([undefined, 19191])(
|
||||
"uses a status RPC with local port override %s when requireRpc is enabled",
|
||||
async (localPortOverride) => {
|
||||
callGatewayMock.mockReset();
|
||||
probeGatewayMock.mockReset();
|
||||
callGatewayMock.mockImplementationOnce(async (opts) => {
|
||||
opts.onHelloOk?.({
|
||||
server: { version: "2026.8.1", buildId: "build-1", connId: "conn-1" },
|
||||
auth: { role: "operator", scopes: ["operator.admin"] },
|
||||
});
|
||||
return { runtimeVersion: "2026.8.1", status: "ok" };
|
||||
});
|
||||
return { runtimeVersion: "2026.8.1", status: "ok" };
|
||||
});
|
||||
|
||||
const result = await probeGatewayStatus({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
token: "temp-token",
|
||||
tlsFingerprint: "abc123",
|
||||
timeoutMs: 5_000,
|
||||
json: true,
|
||||
requireRpc: true,
|
||||
configPath: "/tmp/openclaw-daemon/openclaw.json",
|
||||
});
|
||||
const result = await probeGatewayStatus({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
token: "temp-token",
|
||||
tlsFingerprint: "abc123",
|
||||
timeoutMs: 5_000,
|
||||
json: true,
|
||||
requireRpc: true,
|
||||
localPortOverride,
|
||||
configPath: "/tmp/openclaw-daemon/openclaw.json",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
kind: "read",
|
||||
capability: "admin_capable",
|
||||
auth: {
|
||||
role: "operator",
|
||||
scopes: ["operator.admin"],
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
kind: "read",
|
||||
capability: "admin_capable",
|
||||
},
|
||||
server: {
|
||||
auth: {
|
||||
role: "operator",
|
||||
scopes: ["operator.admin"],
|
||||
capability: "admin_capable",
|
||||
},
|
||||
server: {
|
||||
version: "2026.8.1",
|
||||
buildId: "build-1",
|
||||
connId: "conn-1",
|
||||
},
|
||||
version: "2026.8.1",
|
||||
buildId: "build-1",
|
||||
connId: "conn-1",
|
||||
},
|
||||
version: "2026.8.1",
|
||||
});
|
||||
expect(probeGatewayMock).not.toHaveBeenCalled();
|
||||
expect(callGatewayMock).toHaveBeenCalledOnce();
|
||||
expect(callGatewayMock).toHaveBeenCalledWith({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
token: "temp-token",
|
||||
password: undefined,
|
||||
tlsFingerprint: "abc123",
|
||||
preauthHandshakeTimeoutMs: undefined,
|
||||
method: "status",
|
||||
timeoutMs: 5_000,
|
||||
sharedStateMode: "read-only",
|
||||
configPath: "/tmp/openclaw-daemon/openclaw.json",
|
||||
onHelloOk: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(probeGatewayMock).not.toHaveBeenCalled();
|
||||
expect(callGatewayMock).toHaveBeenCalledOnce();
|
||||
expect(callGatewayMock).toHaveBeenCalledWith({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
localPortOverride,
|
||||
token: "temp-token",
|
||||
password: undefined,
|
||||
tlsFingerprint: "abc123",
|
||||
preauthHandshakeTimeoutMs: undefined,
|
||||
method: "status",
|
||||
timeoutMs: 5_000,
|
||||
sharedStateMode: "read-only",
|
||||
configPath: "/tmp/openclaw-daemon/openclaw.json",
|
||||
onHelloOk: expect.any(Function),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps required status to one timeout-bound RPC", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
@@ -351,6 +356,7 @@ describe("probeGatewayStatus", () => {
|
||||
tlsFingerprint: undefined,
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
config,
|
||||
localPortOverride: undefined,
|
||||
method: "status",
|
||||
timeoutMs: 30_000,
|
||||
sharedStateMode: "read-only",
|
||||
@@ -403,6 +409,7 @@ describe("probeGatewayStatus", () => {
|
||||
timeoutMs: 5_000,
|
||||
sharedStateMode: "read-only",
|
||||
onHelloOk: expect.any(Function),
|
||||
localPortOverride: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ function projectGatewayConnectFailure(params: {
|
||||
/** Probe Gateway connectivity or read-capability status with optional RPC verification. */
|
||||
export async function probeGatewayStatus(opts: {
|
||||
url: string;
|
||||
localPortOverride?: number;
|
||||
token?: string;
|
||||
password?: string;
|
||||
config?: OpenClawConfig;
|
||||
@@ -85,6 +86,7 @@ export async function probeGatewayStatus(opts: {
|
||||
let server: GatewayProbeServerSummary | undefined;
|
||||
await callGateway({
|
||||
url: opts.url,
|
||||
localPortOverride: opts.localPortOverride,
|
||||
token: opts.token,
|
||||
password: opts.password,
|
||||
tlsFingerprint: opts.tlsFingerprint,
|
||||
|
||||
@@ -144,4 +144,30 @@ describe("addGatewayServiceCommands", () => {
|
||||
|
||||
expect(expectSingleDaemonCall(runner).json).toBe(true);
|
||||
});
|
||||
|
||||
it("inherits an explicit parent port instead of a status leaf default", async () => {
|
||||
const gateway = createGatewayParentLikeCommand().enablePositionalOptions();
|
||||
const status = gateway.commands.find((command) => command.name() === "status")!;
|
||||
status.setOptionValueWithSource("port", "19003", "default");
|
||||
|
||||
await gateway.parseAsync(["--port", "19002", "status"], { from: "user" });
|
||||
|
||||
expect(expectSingleDaemonCall(runDaemonStatus).rpc).toMatchObject({
|
||||
port: "19002",
|
||||
localPortOverride: 19002,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ argv: ["status", "--port", "0"], error: "--port must be an integer between 1 and 65535." },
|
||||
{
|
||||
argv: ["--port", "19002", "status", "--url", "ws://localhost:19002"],
|
||||
error: "Use either --url or --port, not both.",
|
||||
},
|
||||
])("rejects invalid status options $argv", async ({ argv, error }) => {
|
||||
const gateway = createGatewayParentLikeCommand().enablePositionalOptions().exitOverride();
|
||||
|
||||
await expect(gateway.parseAsync(argv, { from: "user" })).rejects.toThrow(error);
|
||||
expect(runDaemonStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,24 +2,13 @@
|
||||
import type { Command } from "commander";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { inheritOptionFromParent } from "../command-options.js";
|
||||
import type { DaemonInstallOptions, DaemonLifecycleOptions, GatewayRpcOpts } from "./types.js";
|
||||
import { resolveGatewayRpcOptionsWithLocalPort } from "../gateway-rpc.js";
|
||||
import type { DaemonInstallOptions, DaemonLifecycleOptions } from "./types.js";
|
||||
|
||||
const daemonInstallModuleLoader = createLazyImportLoader(() => import("./install.runtime.js"));
|
||||
const daemonLifecycleModuleLoader = createLazyImportLoader(() => import("./lifecycle.runtime.js"));
|
||||
const daemonStatusModuleLoader = createLazyImportLoader(() => import("./status.runtime.js"));
|
||||
|
||||
function loadDaemonInstallModule() {
|
||||
return daemonInstallModuleLoader.load();
|
||||
}
|
||||
|
||||
function loadDaemonLifecycleModule() {
|
||||
return daemonLifecycleModuleLoader.load();
|
||||
}
|
||||
|
||||
function loadDaemonStatusModule() {
|
||||
return daemonStatusModuleLoader.load();
|
||||
}
|
||||
|
||||
function resolveJsonOption(cmdOpts: { json?: boolean }, command?: Command): boolean {
|
||||
const parentJson = inheritOptionFromParent<boolean>(command, "json", "cli");
|
||||
return Boolean(cmdOpts.json || parentJson);
|
||||
@@ -41,16 +30,6 @@ function resolveInstallOptions(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRpcOptions(cmdOpts: GatewayRpcOpts, command?: Command): GatewayRpcOpts {
|
||||
const parentToken = inheritOptionFromParent<string>(command, "token");
|
||||
const parentPassword = inheritOptionFromParent<string>(command, "password");
|
||||
return {
|
||||
...cmdOpts,
|
||||
token: cmdOpts.token ?? parentToken,
|
||||
password: cmdOpts.password ?? parentPassword,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRestartOptions(cmdOpts: DaemonLifecycleOptions, command?: Command) {
|
||||
const parentForce = inheritOptionFromParent<boolean>(command, "force");
|
||||
return {
|
||||
@@ -78,6 +57,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
opts?.statusDescription ?? "Show gateway service status + probe connectivity/capability",
|
||||
)
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to config/remote/local)")
|
||||
.option("--port <port>", "Local Gateway port")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--password <password>", "Gateway password (password auth)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
@@ -86,9 +66,9 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
.option("--deep", "Scan system-level services", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (cmdOpts, command) => {
|
||||
const { runDaemonStatus } = await loadDaemonStatusModule();
|
||||
const { runDaemonStatus } = await daemonStatusModuleLoader.load();
|
||||
await runDaemonStatus({
|
||||
rpc: resolveRpcOptions(cmdOpts, command),
|
||||
rpc: resolveGatewayRpcOptionsWithLocalPort(cmdOpts, command),
|
||||
probe: Boolean(cmdOpts.probe),
|
||||
requireRpc: Boolean(cmdOpts.requireRpc),
|
||||
deep: Boolean(cmdOpts.deep),
|
||||
@@ -106,7 +86,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
.option("--force", "Reinstall/overwrite if already installed", false)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (cmdOpts, command) => {
|
||||
const { runDaemonInstall } = await loadDaemonInstallModule();
|
||||
const { runDaemonInstall } = await daemonInstallModuleLoader.load();
|
||||
await runDaemonInstall(resolveInstallOptions(cmdOpts, command));
|
||||
});
|
||||
|
||||
@@ -115,7 +95,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
.description("Uninstall the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (cmdOpts, command) => {
|
||||
const { runDaemonUninstall } = await loadDaemonLifecycleModule();
|
||||
const { runDaemonUninstall } = await daemonLifecycleModuleLoader.load();
|
||||
await runDaemonUninstall({ ...cmdOpts, json: resolveJsonOption(cmdOpts, command) });
|
||||
});
|
||||
|
||||
@@ -124,7 +104,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
.description("Start the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (cmdOpts, command) => {
|
||||
const { runDaemonStart } = await loadDaemonLifecycleModule();
|
||||
const { runDaemonStart } = await daemonLifecycleModuleLoader.load();
|
||||
await runDaemonStart({ ...cmdOpts, json: resolveJsonOption(cmdOpts, command) });
|
||||
});
|
||||
|
||||
@@ -139,7 +119,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
false,
|
||||
)
|
||||
.action(async (cmdOpts, command) => {
|
||||
const { runDaemonStop } = await loadDaemonLifecycleModule();
|
||||
const { runDaemonStop } = await daemonLifecycleModuleLoader.load();
|
||||
await runDaemonStop(resolveStopOptions(cmdOpts, command));
|
||||
});
|
||||
|
||||
@@ -165,7 +145,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
|
||||
)
|
||||
.option("--json", "Output JSON", false)
|
||||
.action(async (cmdOpts, command) => {
|
||||
const { runDaemonRestart } = await loadDaemonLifecycleModule();
|
||||
const { runDaemonRestart } = await daemonLifecycleModuleLoader.load();
|
||||
await runDaemonRestart(resolveRestartOptions(cmdOpts, command));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
|
||||
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
|
||||
@@ -10,6 +11,8 @@ import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import { registerGatewayCli } from "../gateway-cli/register.js";
|
||||
import { registerDaemonCli } from "./register.js";
|
||||
import type { GatewayRestartSnapshot } from "./restart-health.js";
|
||||
import { gatherDaemonStatus, renderPortDiagnosticsForCli } from "./status.gather.js";
|
||||
import { printDaemonStatus } from "./status.print.js";
|
||||
@@ -493,6 +496,95 @@ describe("gatherDaemonStatus", () => {
|
||||
expect(inspectWindowsGatewayFirewall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(
|
||||
[
|
||||
["gateway", "status", "--port", "19002"],
|
||||
["gateway", "--port", "19002", "status"],
|
||||
["gateway", "--port", "19003", "status", "--port", "19002"],
|
||||
["daemon", "status", "--port", "19002"],
|
||||
].map((argv) => ({ name: argv.join(" "), argv })),
|
||||
)("targets the selected local port for $name", async ({ argv }) => {
|
||||
const program = new Command().enablePositionalOptions().exitOverride();
|
||||
program.configureOutput({ writeErr: () => {} });
|
||||
registerGatewayCli(program);
|
||||
registerDaemonCli(program);
|
||||
const writeJson = vi.spyOn(defaultRuntime, "writeJson").mockImplementation(() => {});
|
||||
try {
|
||||
await program.parseAsync([...argv, "--json"], { from: "user" });
|
||||
|
||||
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "ws://127.0.0.1:19002",
|
||||
localPortOverride: 19002,
|
||||
config: cliLoadedConfig,
|
||||
configPath: "/tmp/openclaw-cli/openclaw.json",
|
||||
}),
|
||||
);
|
||||
expect(writeJson).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
port: 19002,
|
||||
portSource: "cli",
|
||||
probeUrl: "ws://127.0.0.1:19002",
|
||||
}),
|
||||
rpc: expect.objectContaining({ url: "ws://127.0.0.1:19002" }),
|
||||
service: expect.objectContaining({ targetRole: "diagnostic-only" }),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
writeJson.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"keeps an explicit local port in remote config with probe=%s",
|
||||
async (probe) => {
|
||||
cliLoadedConfig = {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
bind: "tailnet",
|
||||
tls: { enabled: true },
|
||||
auth: { token: "local-token" },
|
||||
remote: { url: "wss://gateway.example", token: "remote-token" },
|
||||
},
|
||||
};
|
||||
const status = await gatherStatus({ rpc: { localPortOverride: 19002 }, probe, deep: true });
|
||||
|
||||
expect(status.gateway).toMatchObject({
|
||||
port: 19002,
|
||||
portSource: "cli",
|
||||
probeUrl: "wss://127.0.0.1:19002",
|
||||
});
|
||||
expect(inspectPortConnections).toHaveBeenCalledWith(19002);
|
||||
expect(status.service.targetRole).toBe("diagnostic-only");
|
||||
expect(inspectGatewayRestart).not.toHaveBeenCalled();
|
||||
if (probe) {
|
||||
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "wss://127.0.0.1:19002",
|
||||
token: "local-token",
|
||||
tlsFingerprint: "sha256:11:22:33:44",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
expect(callGatewayStatusProbe).not.toHaveBeenCalled();
|
||||
expect(status.rpc).toBeUndefined();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ rpc: { port: "65536" }, message: "--port must be an integer between 1 and 65535." },
|
||||
{
|
||||
rpc: { port: "19002", url: "ws://localhost:19002" },
|
||||
message: "Use either --url or --port, not both.",
|
||||
},
|
||||
])("rejects invalid status target $rpc before service reads", async ({ rpc, message }) => {
|
||||
await expect(gatherStatus({ rpc })).rejects.toThrow(message);
|
||||
expect(serviceReadCommand).not.toHaveBeenCalled();
|
||||
expect(callGatewayStatusProbe).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("batches daemon and CLI port status inspection when ports differ", async () => {
|
||||
await gatherStatus();
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
} from "../../plugins/plugin-version-drift.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import { resolveGatewayLocalPortOverride } from "../gateway-port-option.js";
|
||||
import { parseTimeoutMsWithFallback } from "../parse-timeout.js";
|
||||
import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js";
|
||||
import type { GatewayRpcOpts } from "./types.js";
|
||||
@@ -87,7 +88,7 @@ type GatewayStatusSummary = {
|
||||
customBindHost?: string;
|
||||
tlsEnabled?: boolean;
|
||||
port: number;
|
||||
portSource: "service args" | "env/config";
|
||||
portSource: "cli" | "service args" | "env/config";
|
||||
probeUrl: string;
|
||||
controlUiLinks?: { httpUrl: string; wsUrl: string };
|
||||
probeNote?: string;
|
||||
@@ -433,12 +434,15 @@ async function resolveGatewayStatusSummary(params: {
|
||||
mergedDaemonEnv: Record<string, string | undefined>;
|
||||
commandProgramArguments?: string[];
|
||||
rpcUrlOverride?: string;
|
||||
localPortOverride?: number;
|
||||
}): Promise<ResolvedGatewayStatus> {
|
||||
const portFromArgs = parsePortFromArgs(params.commandProgramArguments);
|
||||
const daemonPort = portFromArgs ?? resolveGatewayPort(params.daemonCfg, params.mergedDaemonEnv);
|
||||
const portSource: GatewayStatusSummary["portSource"] = portFromArgs
|
||||
? "service args"
|
||||
: "env/config";
|
||||
const daemonPort =
|
||||
params.localPortOverride ??
|
||||
portFromArgs ??
|
||||
resolveGatewayPort(params.daemonCfg, params.mergedDaemonEnv);
|
||||
const portSource: GatewayStatusSummary["portSource"] =
|
||||
params.localPortOverride !== undefined ? "cli" : portFromArgs ? "service args" : "env/config";
|
||||
const bindMode: GatewayBindMode = params.daemonCfg.gateway?.bind ?? "loopback";
|
||||
const customBindHost = params.daemonCfg.gateway?.customBindHost;
|
||||
const { bindHost, warning: bindHostWarning } = await resolveBestEffortGatewayBindHostForDisplay({
|
||||
@@ -449,7 +453,10 @@ async function resolveGatewayStatusSummary(params: {
|
||||
const { tailnetIPv4, warning: tailnetWarning } = inspectBestEffortPrimaryTailnetIPv4({
|
||||
warningPrefix: "Status could not inspect tailnet addresses",
|
||||
});
|
||||
const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
|
||||
const probeHost =
|
||||
params.localPortOverride !== undefined
|
||||
? "127.0.0.1"
|
||||
: pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
|
||||
const probeUrlOverride = trimToUndefined(params.rpcUrlOverride) ?? null;
|
||||
const tlsEnabled = params.daemonCfg.gateway?.tls?.enabled === true;
|
||||
const scheme = tlsEnabled ? "wss" : "ws";
|
||||
@@ -595,6 +602,7 @@ export async function gatherDaemonStatus(
|
||||
allowExecSecretRefs?: boolean;
|
||||
} & FindExtraGatewayServicesOptions,
|
||||
): Promise<DaemonStatus> {
|
||||
const localPortOverride = resolveGatewayLocalPortOverride(opts.rpc);
|
||||
const timeoutMs = parseTimeoutMsWithFallback(opts.rpc.timeout, 10_000, {
|
||||
invalidType: "error",
|
||||
});
|
||||
@@ -605,10 +613,12 @@ export async function gatherDaemonStatus(
|
||||
});
|
||||
const { command, env: serviceEnv, loadState, runtime } = serviceState;
|
||||
const loaded = loadState.status === "loaded";
|
||||
// A non-default or externally supervised process does not own the host's
|
||||
// native service. Keep that service visible, but do not let it retarget probes.
|
||||
// An explicit local port or separate process context does not select the
|
||||
// native service. Keep that service visible without borrowing its target or auth.
|
||||
const useNativeServiceTargetContext =
|
||||
isDefaultInstallIdentity(process.env) && !isGatewayExternallySupervised(process.env);
|
||||
localPortOverride === undefined &&
|
||||
isDefaultInstallIdentity(process.env) &&
|
||||
!isGatewayExternallySupervised(process.env);
|
||||
const targetServiceCommand = useNativeServiceTargetContext ? command : null;
|
||||
const restartHandoff = opts.deep ? readGatewayRestartHandoffSync(serviceEnv) : null;
|
||||
const configAudit: ServiceConfigAudit = command
|
||||
@@ -635,9 +645,12 @@ export async function gatherDaemonStatus(
|
||||
mergedDaemonEnv,
|
||||
commandProgramArguments: targetServiceCommand?.programArguments,
|
||||
rpcUrlOverride: opts.rpc.url,
|
||||
localPortOverride,
|
||||
});
|
||||
const probeMode =
|
||||
localPortOverride === undefined && daemonCfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
const serviceTargetsProbe = useNativeServiceTargetContext && !probeUrlOverride;
|
||||
const shouldInspectLocalGateway = daemonCfg.gateway?.mode !== "remote" && !probeUrlOverride;
|
||||
const shouldInspectLocalGateway = probeMode === "local" && !probeUrlOverride;
|
||||
const windowsFirewall =
|
||||
opts.deep === true && shouldInspectLocalGateway
|
||||
? await inspectWindowsGatewayFirewall({
|
||||
@@ -655,7 +668,7 @@ export async function gatherDaemonStatus(
|
||||
const establishedClients = await inspectEstablishedGatewayClients({
|
||||
daemonPort,
|
||||
deep: opts.deep,
|
||||
gatewayMode: daemonCfg.gateway?.mode,
|
||||
gatewayMode: probeMode,
|
||||
});
|
||||
|
||||
const extraServices = opts.deep
|
||||
@@ -688,7 +701,6 @@ export async function gatherDaemonStatus(
|
||||
let allowRpcConfigCredentials = true;
|
||||
let skippedProbeAuthForDisabledExecSecretRef = false;
|
||||
if (opts.probe) {
|
||||
const probeMode = daemonCfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
const explicitAuth = {
|
||||
token: opts.rpc.token,
|
||||
password: opts.rpc.password,
|
||||
@@ -725,6 +737,7 @@ export async function gatherDaemonStatus(
|
||||
? await loadDaemonProbeModule().then(({ probeGatewayStatus }) =>
|
||||
probeGatewayStatus({
|
||||
url: probeUrl,
|
||||
localPortOverride,
|
||||
token: daemonProbeAuth?.token,
|
||||
password: daemonProbeAuth?.password,
|
||||
config: daemonCfg,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// Shared option types for Gateway service CLI commands.
|
||||
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
|
||||
import type { GatewayRpcOpts as SharedGatewayRpcOpts } from "../gateway-rpc.types.js";
|
||||
|
||||
/** RPC probe options accepted by Gateway service status commands. */
|
||||
export type GatewayRpcOpts = {
|
||||
url?: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
timeout?: string;
|
||||
json?: boolean;
|
||||
export type GatewayRpcOpts = Omit<SharedGatewayRpcOpts, "expectFinal"> & {
|
||||
localPortOverride?: number;
|
||||
};
|
||||
|
||||
/** Full option bag for Gateway service status. */
|
||||
|
||||
@@ -21,8 +21,12 @@ import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import { inheritOptionFromParent } from "../command-options.js";
|
||||
import { addGatewayServiceCommands } from "../daemon-cli/register-service-commands.js";
|
||||
import { formatCliJsonFailure, rethrowExpectedCliError } from "../failure-output.js";
|
||||
import { parseGatewayPortOption } from "../gateway-port-option.js";
|
||||
import { addGatewayClientOptions, callGatewayFromCliWithTransport } from "../gateway-rpc.js";
|
||||
import {
|
||||
addGatewayClientOptions,
|
||||
callGatewayFromCliWithTransport,
|
||||
resolveGatewayRpcOptions,
|
||||
resolveGatewayRpcOptionsWithLocalPort,
|
||||
} from "../gateway-rpc.js";
|
||||
import { formatHelpExamples } from "../help-format.js";
|
||||
import { parseTimeoutMsWithFallback } from "../parse-timeout.js";
|
||||
import { setCommandJsonMode } from "../program/json-mode.js";
|
||||
@@ -179,37 +183,6 @@ function parseDaysOption(raw: unknown, fallback = 30): number {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveGatewayRpcOptions<T extends { token?: string; password?: string }>(
|
||||
opts: T,
|
||||
command?: Command,
|
||||
): T {
|
||||
const parentToken = inheritOptionFromParent<string>(command, "token");
|
||||
const parentPassword = inheritOptionFromParent<string>(command, "password");
|
||||
return {
|
||||
...opts,
|
||||
token: opts.token ?? parentToken,
|
||||
password: opts.password ?? parentPassword,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGatewayRpcOptionsWithLocalPort(
|
||||
opts: GatewayRpcOpts & { port?: unknown },
|
||||
command?: Command,
|
||||
): GatewayRpcOpts {
|
||||
const rpcOpts = resolveGatewayRpcOptions(opts, command);
|
||||
const port = parseGatewayPortOption(opts.port ?? inheritOptionFromParent(command, "port"));
|
||||
if (port === undefined) {
|
||||
return rpcOpts;
|
||||
}
|
||||
if (typeof opts.url === "string" && opts.url.trim()) {
|
||||
throw new Error("Use either --url or --port, not both.");
|
||||
}
|
||||
return {
|
||||
...rpcOpts,
|
||||
localPortOverride: port,
|
||||
};
|
||||
}
|
||||
|
||||
async function renderCostUsageSummaryAsync(
|
||||
summary: CostUsageSummary,
|
||||
days: number,
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { OperatorScope } from "../gateway/operator-scopes.js";
|
||||
import type { DeviceIdentity } from "../infra/device-identity.js";
|
||||
import { createLazyImportLoader } from "../shared/lazy-promise.js";
|
||||
import { inheritOptionFromParent } from "./command-options.js";
|
||||
import { resolveGatewayLocalPortOverride } from "./gateway-port-option.js";
|
||||
import type { GatewayRpcOpts } from "./gateway-rpc.types.js";
|
||||
export type { GatewayRpcOpts } from "./gateway-rpc.types.js";
|
||||
|
||||
@@ -32,6 +34,34 @@ export function addGatewayClientOptions(cmd: Command, defaults?: { timeoutMs?: n
|
||||
.option("--expect-final", "Wait for final response (agent)", false);
|
||||
}
|
||||
|
||||
export function resolveGatewayRpcOptions<T extends { token?: string; password?: string }>(
|
||||
opts: T,
|
||||
command?: Command,
|
||||
): T {
|
||||
return {
|
||||
...opts,
|
||||
token: opts.token ?? inheritOptionFromParent<string>(command, "token"),
|
||||
password: opts.password ?? inheritOptionFromParent<string>(command, "password"),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGatewayRpcOptionsWithLocalPort<
|
||||
T extends Pick<GatewayRpcOpts, "url" | "port" | "token" | "password"> & {
|
||||
localPortOverride?: number;
|
||||
},
|
||||
>(opts: T, command?: Command) {
|
||||
// Leaf defaults must not hide an explicit port supplied before the subcommand.
|
||||
const port = command?.getOptionValueSource("port") === "default" ? undefined : opts.port;
|
||||
const rpcOpts = {
|
||||
...resolveGatewayRpcOptions(opts, command),
|
||||
port: port ?? inheritOptionFromParent<string>(command, "port"),
|
||||
};
|
||||
return {
|
||||
...rpcOpts,
|
||||
localPortOverride: resolveGatewayLocalPortOverride(rpcOpts),
|
||||
};
|
||||
}
|
||||
|
||||
export async function callGatewayFromCli(
|
||||
method: string,
|
||||
opts: GatewayRpcOpts,
|
||||
|
||||
Reference in New Issue
Block a user