diff --git a/src/agents/tools/gateway.test.ts b/src/agents/tools/gateway.test.ts index fa31ca8fca9..7669261f7b6 100644 --- a/src/agents/tools/gateway.test.ts +++ b/src/agents/tools/gateway.test.ts @@ -109,6 +109,39 @@ describe("resolveGatewayTarget – env URL override classification", () => { expect(resolveGatewayTarget()).toBe("remote"); }); + it("classifies env-only remote URL (not matching gateway.remote.url) as 'remote'", () => { + // callGateway uses the env URL as-is even when validateGatewayUrlOverrideForAgentTools + // rejects it (different host than configured gateway.remote.url). Must not leak + // deliveryContext into a remote call by falling back to 'local'. + process.env.OPENCLAW_GATEWAY_URL = "wss://other-host.example.com"; + setConfig({ + gateway: { mode: "remote", remote: { url: "wss://remote.example.com" } }, + }); + expect(resolveGatewayTarget()).toBe("remote"); + }); + + it("classifies env-only remote URL with no configured gateway.remote.url as 'remote'", () => { + // callGateway picks up the env URL even when gateway.remote.url is absent. + process.env.OPENCLAW_GATEWAY_URL = "wss://remote.example.com"; + setConfig({}); + expect(resolveGatewayTarget()).toBe("remote"); + }); + + it("classifies env URL with /ws path (rejected by allowlist) as 'remote'", () => { + // URLs with non-root paths are rejected by validateGatewayUrlOverrideForAgentTools but + // callGateway/buildGatewayConnectionDetails still use them verbatim. Classify correctly. + process.env.OPENCLAW_GATEWAY_URL = "wss://remote.example.com/ws"; + setConfig({}); + expect(resolveGatewayTarget()).toBe("remote"); + }); + + it("classifies loopback env URL with /ws path (rejected by allowlist) as 'local'", () => { + // Even with a non-root path, loopback targets remain local. + process.env.OPENCLAW_GATEWAY_URL = "ws://127.0.0.1:18789/ws"; + setConfig({}); + expect(resolveGatewayTarget()).toBe("local"); + }); + it("OPENCLAW_GATEWAY_URL takes precedence over env CLAWDBOT_GATEWAY_URL", () => { process.env.OPENCLAW_GATEWAY_URL = "ws://127.0.0.1:18789"; process.env.CLAWDBOT_GATEWAY_URL = "wss://remote.example.com"; diff --git a/src/agents/tools/gateway.ts b/src/agents/tools/gateway.ts index c7acd4858b3..fab886252e3 100644 --- a/src/agents/tools/gateway.ts +++ b/src/agents/tools/gateway.ts @@ -144,7 +144,19 @@ export function resolveGatewayTarget(opts?: GatewayCallOptions): GatewayOverride urlOverride: envUrlOverride, }).target; } catch { - // Malformed or security-rejected env URL; fall through to config-based resolution. + // URL rejected by the agent-tools allowlist (e.g. non-loopback URL not matching + // gateway.remote.url, or URL with a non-root path like /ws). callGateway / + // buildGatewayConnectionDetails will still use this env URL as-is, so we must + // classify based on the actual target host — not silently fall back to local. + try { + const parsed = new URL(envUrlOverride.trim()); + // Normalize IPv6 brackets: "[::1]" → "::1" + const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, ""); + const isLoopback = host === "127.0.0.1" || host === "localhost" || host === "::1"; + return isLoopback ? "local" : "remote"; + } catch { + // Truly malformed URL; callGateway will also fail. Fall through to config-based resolution. + } } } // No env override. When mode=remote with a configured remote URL → truly remote.