fix: prefer tailnet IP for local gateway calls
This commit is contained in:
@@ -18,9 +18,11 @@
|
|||||||
- Control UI: chat view uses page scroll with sticky header/sidebar and fixed composer (no inner scroll frame).
|
- Control UI: chat view uses page scroll with sticky header/sidebar and fixed composer (no inner scroll frame).
|
||||||
- macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639
|
- macOS: treat location permission as always-only to avoid iOS-only enums. (#165) — thanks @Nachx639
|
||||||
- macOS: make generated gateway protocol models `Sendable` for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan
|
- macOS: make generated gateway protocol models `Sendable` for Swift 6 strict concurrency. (#195) — thanks @andranik-sahakyan
|
||||||
|
- macOS: bundle QR code renderer modules so DMG gateway boot doesn't crash on missing qrcode-terminal vendor files.
|
||||||
- WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj
|
- WhatsApp: suppress typing indicator during heartbeat background tasks. (#190) — thanks @mcinteerj
|
||||||
- WhatsApp: mark offline history sync messages as read without auto-reply. (#193) — thanks @mcinteerj
|
- WhatsApp: mark offline history sync messages as read without auto-reply. (#193) — thanks @mcinteerj
|
||||||
- Discord: avoid duplicate replies when a provider emits late streaming `text_end` events (OpenAI/GPT).
|
- Discord: avoid duplicate replies when a provider emits late streaming `text_end` events (OpenAI/GPT).
|
||||||
|
- CLI: use tailnet IP for local gateway calls when bind is tailnet/auto (fixes #176).
|
||||||
- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`.
|
- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`.
|
||||||
- Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).
|
- Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).
|
||||||
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
|
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
|
||||||
|
|||||||
78
src/gateway/call.test.ts
Normal file
78
src/gateway/call.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const loadConfig = vi.fn();
|
||||||
|
const resolveGatewayPort = vi.fn();
|
||||||
|
const pickPrimaryTailnetIPv4 = vi.fn();
|
||||||
|
|
||||||
|
let lastClientOptions: {
|
||||||
|
url?: string;
|
||||||
|
onHelloOk?: () => void | Promise<void>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
vi.mock("../config/config.js", () => ({
|
||||||
|
loadConfig,
|
||||||
|
resolveGatewayPort,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../infra/tailnet.js", () => ({
|
||||||
|
pickPrimaryTailnetIPv4,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./client.js", () => ({
|
||||||
|
GatewayClient: class {
|
||||||
|
constructor(opts: {
|
||||||
|
url?: string;
|
||||||
|
onHelloOk?: () => void | Promise<void>;
|
||||||
|
}) {
|
||||||
|
lastClientOptions = opts;
|
||||||
|
}
|
||||||
|
async request() {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
void lastClientOptions?.onHelloOk?.();
|
||||||
|
}
|
||||||
|
stop() {}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { callGateway } = await import("./call.js");
|
||||||
|
|
||||||
|
describe("callGateway url resolution", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
loadConfig.mockReset();
|
||||||
|
resolveGatewayPort.mockReset();
|
||||||
|
pickPrimaryTailnetIPv4.mockReset();
|
||||||
|
lastClientOptions = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses tailnet IP when local bind is tailnet", async () => {
|
||||||
|
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "tailnet" } });
|
||||||
|
resolveGatewayPort.mockReturnValue(18800);
|
||||||
|
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.1");
|
||||||
|
|
||||||
|
await callGateway({ method: "health" });
|
||||||
|
|
||||||
|
expect(lastClientOptions?.url).toBe("ws://100.64.0.1:18800");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses tailnet IP when local bind is auto and tailnet is present", async () => {
|
||||||
|
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "auto" } });
|
||||||
|
resolveGatewayPort.mockReturnValue(18800);
|
||||||
|
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.2");
|
||||||
|
|
||||||
|
await callGateway({ method: "health" });
|
||||||
|
|
||||||
|
expect(lastClientOptions?.url).toBe("ws://100.64.0.2:18800");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to loopback when local bind is auto without tailnet IP", async () => {
|
||||||
|
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "auto" } });
|
||||||
|
resolveGatewayPort.mockReturnValue(18800);
|
||||||
|
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
await callGateway({ method: "health" });
|
||||||
|
|
||||||
|
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18800");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
||||||
|
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||||
import { GatewayClient } from "./client.js";
|
import { GatewayClient } from "./client.js";
|
||||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||||
|
|
||||||
@@ -29,6 +30,14 @@ export async function callGateway<T = unknown>(
|
|||||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||||
const authToken = config.gateway?.auth?.token;
|
const authToken = config.gateway?.auth?.token;
|
||||||
const localPort = resolveGatewayPort(config);
|
const localPort = resolveGatewayPort(config);
|
||||||
|
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
||||||
|
const bindMode = config.gateway?.bind ?? "loopback";
|
||||||
|
const preferTailnet =
|
||||||
|
bindMode === "tailnet" || (bindMode === "auto" && !!tailnetIPv4);
|
||||||
|
const localUrl =
|
||||||
|
preferTailnet && tailnetIPv4
|
||||||
|
? `ws://${tailnetIPv4}:${localPort}`
|
||||||
|
: `ws://127.0.0.1:${localPort}`;
|
||||||
const url =
|
const url =
|
||||||
(typeof opts.url === "string" && opts.url.trim().length > 0
|
(typeof opts.url === "string" && opts.url.trim().length > 0
|
||||||
? opts.url.trim()
|
? opts.url.trim()
|
||||||
@@ -36,7 +45,7 @@ export async function callGateway<T = unknown>(
|
|||||||
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
||||||
? remote.url.trim()
|
? remote.url.trim()
|
||||||
: undefined) ||
|
: undefined) ||
|
||||||
`ws://127.0.0.1:${localPort}`;
|
localUrl;
|
||||||
const token =
|
const token =
|
||||||
(typeof opts.token === "string" && opts.token.trim().length > 0
|
(typeof opts.token === "string" && opts.token.trim().length > 0
|
||||||
? opts.token.trim()
|
? opts.token.trim()
|
||||||
|
|||||||
Reference in New Issue
Block a user