import { loadConfig, resolveGatewayPort } from "../../config/config.js"; import { callGateway } from "../../gateway/call.js"; import { resolveGatewayCredentialsFromConfig, trimToUndefined } from "../../gateway/credentials.js"; import { resolveLeastPrivilegeOperatorScopesForMethod, type OperatorScope, } from "../../gateway/method-scopes.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js"; import { readStringParam } from "./common.js"; export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789"; export type GatewayCallOptions = { gatewayUrl?: string; gatewayToken?: string; timeoutMs?: number; }; type GatewayOverrideTarget = "local" | "remote"; export function readGatewayCallOptions(params: Record): GatewayCallOptions { return { gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }), gatewayToken: readStringParam(params, "gatewayToken", { trim: false }), timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, }; } function canonicalizeToolGatewayWsUrl(raw: string): { origin: string; key: string } { const input = raw.trim(); let url: URL; try { url = new URL(input); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error }); } if (url.protocol !== "ws:" && url.protocol !== "wss:") { throw new Error(`invalid gatewayUrl protocol: ${url.protocol} (expected ws:// or wss://)`); } if (url.username || url.password) { throw new Error("invalid gatewayUrl: credentials are not allowed"); } if (url.search || url.hash) { throw new Error("invalid gatewayUrl: query/hash not allowed"); } // Agents/tools expect the gateway websocket on the origin, not arbitrary paths. if (url.pathname && url.pathname !== "/") { throw new Error("invalid gatewayUrl: path not allowed"); } const origin = url.origin; // Key: protocol + host only, lowercased. (host includes IPv6 brackets + port when present) const key = `${url.protocol}//${url.host.toLowerCase()}`; return { origin, key }; } function validateGatewayUrlOverrideForAgentTools(params: { cfg: ReturnType; urlOverride: string; }): { url: string; target: GatewayOverrideTarget } { const { cfg } = params; const port = resolveGatewayPort(cfg); const localAllowed = new Set([ `ws://127.0.0.1:${port}`, `wss://127.0.0.1:${port}`, `ws://localhost:${port}`, `wss://localhost:${port}`, `ws://[::1]:${port}`, `wss://[::1]:${port}`, ]); let remoteKey: string | undefined; const remoteUrl = typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : ""; if (remoteUrl) { try { const remote = canonicalizeToolGatewayWsUrl(remoteUrl); remoteKey = remote.key; } catch { // ignore: misconfigured remote url; tools should fall back to default resolution. } } const parsed = canonicalizeToolGatewayWsUrl(params.urlOverride); if (localAllowed.has(parsed.key)) { return { url: parsed.origin, target: "local" }; } if (remoteKey && parsed.key === remoteKey) { return { url: parsed.origin, target: "remote" }; } throw new Error( [ "gatewayUrl override rejected.", `Allowed: ws(s) loopback on port ${port} (127.0.0.1/localhost/[::1])`, "Or: configure gateway.remote.url and omit gatewayUrl to use the configured remote gateway.", ].join(" "), ); } function resolveGatewayOverrideToken(params: { cfg: ReturnType; target: GatewayOverrideTarget; explicitToken?: string; }): string | undefined { if (params.explicitToken) { return params.explicitToken; } return resolveGatewayCredentialsFromConfig({ cfg: params.cfg, env: process.env, modeOverride: params.target, remoteTokenFallback: params.target === "remote" ? "remote-only" : "remote-env-local", remotePasswordFallback: params.target === "remote" ? "remote-only" : "remote-env-local", }).token; } export function resolveGatewayOptions(opts?: GatewayCallOptions) { const cfg = loadConfig(); const validatedOverride = trimToUndefined(opts?.gatewayUrl) !== undefined ? validateGatewayUrlOverrideForAgentTools({ cfg, urlOverride: String(opts?.gatewayUrl), }) : undefined; const explicitToken = trimToUndefined(opts?.gatewayToken); const token = validatedOverride ? resolveGatewayOverrideToken({ cfg, target: validatedOverride.target, explicitToken, }) : explicitToken; const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.floor(opts.timeoutMs)) : 30_000; return { url: validatedOverride?.url, token, timeoutMs }; } export async function callGatewayTool>( method: string, opts: GatewayCallOptions, params?: unknown, extra?: { expectFinal?: boolean; scopes?: OperatorScope[] }, ) { const gateway = resolveGatewayOptions(opts); const scopes = Array.isArray(extra?.scopes) ? extra.scopes : resolveLeastPrivilegeOperatorScopesForMethod(method); return await callGateway({ url: gateway.url, token: gateway.token, method, params, timeoutMs: gateway.timeoutMs, expectFinal: extra?.expectFinal, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, clientDisplayName: "agent", mode: GATEWAY_CLIENT_MODES.BACKEND, scopes, }); }