fix(gateway): require explicit credentials for probe URL overrides (#118614)

This commit is contained in:
Peter Steinberger
2026-08-03 02:52:29 -07:00
committed by GitHub
parent 96b4258734
commit 188957a18a
2 changed files with 136 additions and 0 deletions
+129
View File
@@ -315,6 +315,8 @@ async function runGatewayStatus(
json?: boolean;
port?: unknown;
url?: string;
token?: string;
password?: string;
ssh?: string;
sshAuto?: boolean;
sshIdentity?: string;
@@ -447,6 +449,7 @@ describe("gateway-status command", () => {
timeout: "1000",
json: true,
url: "wss://remote.example:18789",
token: "explicit-remote-token",
});
expect(inspectWindowsGatewayFirewall).not.toHaveBeenCalled();
@@ -458,6 +461,132 @@ describe("gateway-status command", () => {
);
});
it.each([
{
source: "configured token",
auth: { mode: "token", token: "configured-local-token" },
env: {},
options: {},
},
{
source: "configured password",
auth: { mode: "password", password: "configured-local-password" },
env: {},
options: {},
},
{
source: "environment token",
auth: { mode: "token" },
env: { OPENCLAW_GATEWAY_TOKEN: "ambient-local-token" },
options: {},
},
{
source: "environment password",
auth: { mode: "password" },
env: { OPENCLAW_GATEWAY_PASSWORD: "ambient-local-password" },
options: {},
},
{
source: "whitespace token",
auth: { mode: "token", token: "configured-local-token" },
env: {},
options: { token: " " },
},
{
source: "whitespace password",
auth: { mode: "password", password: "configured-local-password" },
env: {},
options: { password: " " },
},
{
source: "explicit loopback URL",
auth: { mode: "token", token: "configured-local-token" },
env: {},
options: { url: "ws://127.0.0.1:18991" },
},
])(
"rejects a local $source before probing an explicit Gateway URL",
async ({ auth, env, options }) => {
const configuredGateway = { gateway: { mode: "local", auth } };
await withEnvAsync(
{
OPENCLAW_GATEWAY_TOKEN: undefined,
OPENCLAW_GATEWAY_PASSWORD: undefined,
...env,
},
async () => {
await readBestEffortConfig.withImplementation(
async () => configuredGateway as never,
async () => {
const { runtime } = createRuntimeCapture();
await expect(
runGatewayStatus(runtime, {
timeout: "1000",
json: true,
url: "wss://attacker.example:18789",
...options,
}),
).rejects.toMatchObject({
name: "GatewayExplicitAuthRequiredError",
message: expect.stringContaining(
"gateway url override requires explicit credentials",
),
});
expect(readBestEffortConfig).not.toHaveBeenCalled();
expect(discoverGatewayBeacons).not.toHaveBeenCalled();
expect(startSshPortForward).not.toHaveBeenCalled();
expect(probeGateway).not.toHaveBeenCalled();
},
);
},
);
},
);
it.each([
{
credential: "token",
options: { token: "explicit-remote-token" },
expectedAuth: { token: "explicit-remote-token", password: undefined },
},
{
credential: "password",
options: { password: "explicit-remote-password" },
expectedAuth: { token: undefined, password: "explicit-remote-password" },
},
])(
"honors an explicit $credential for an explicit Gateway URL",
async ({ options, expectedAuth }) => {
const explicitUrl = "wss://attacker.example:18789";
readBestEffortConfig.mockResolvedValueOnce({
gateway: {
mode: "local",
auth: { mode: "token", token: "configured-local-token" },
},
} as never);
await withEnvAsync(
{
OPENCLAW_GATEWAY_TOKEN: "ambient-local-token",
OPENCLAW_GATEWAY_PASSWORD: "ambient-local-password",
},
async () => {
const { runtime } = createRuntimeCapture();
await runGatewayStatus(runtime, {
timeout: "1000",
json: true,
url: explicitUrl,
...options,
});
expect(requireProbeCall(explicitUrl).auth).toEqual(expectedAuth);
},
);
},
);
it("includes diagnostic next steps when no gateway is reachable or discoverable", async () => {
const { runtime, runtimeLogs, runtimeErrors } = createRuntimeCapture();
const defaultProbeGateway = probeGateway.getMockImplementation();
+7
View File
@@ -3,6 +3,7 @@ import { isRich } from "../../packages/terminal-core/src/theme.js";
import { parseGatewayPortOption } from "../cli/gateway-port-option.js";
import { withProgress } from "../cli/progress.js";
import { readBestEffortConfig, resolveGatewayPort } from "../config/config.js";
import { ensureExplicitGatewayAuth, resolveExplicitGatewayAuth } from "../gateway/call.js";
import { resolveWideAreaDiscoveryDomain } from "../infra/widearea-dns.js";
import type { RuntimeEnv } from "../runtime.js";
import { createLazyImportLoader } from "../shared/lazy-promise.js";
@@ -52,6 +53,12 @@ export async function gatewayStatusCommand(
},
runtime: RuntimeEnv,
) {
ensureExplicitGatewayAuth({
urlOverride: opts.url?.trim(),
urlOverrideSource: "cli",
explicitAuth: resolveExplicitGatewayAuth(opts),
errorHint: "Fix: pass --token or --password with --url.",
});
const startedAt = Date.now();
const cfg = await readBestEffortConfig();
const rich = isRich() && opts.json !== true;