





















@@ -40,9 +40,17 @@ const writeDiagnosticStabilityBundleForFailureSync = vi.fn((_reason: string, _er
4040const controlUiState = vi.hoisted(() => ({
4141root: "/tmp/openclaw-control-ui" as string | null,
4242}));
43+const netState = vi.hoisted(() => ({
44+autoBindHost: "127.0.0.1",
45+container: false,
46+}));
4347const withoutSupervisorEnv = Object.fromEntries(
4448SUPERVISOR_HINT_ENV_VARS.map((key) => [key, undefined]),
4549) as Record<string, string | undefined>;
50+const withoutGatewayAuthEnv = {
51+OPENCLAW_GATEWAY_TOKEN: undefined,
52+OPENCLAW_GATEWAY_PASSWORD: undefined,
53+};
46544755const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
4856@@ -85,6 +93,35 @@ vi.mock("../../gateway/auth.js", () => ({
8593},
8694}));
879596+vi.mock("../../gateway/net.js", async (importOriginal) => {
97+const actual = await importOriginal<typeof import("../../gateway/net.js")>();
98+return {
99+ ...actual,
100+defaultGatewayBindMode: (tailscaleMode?: string) => {
101+if (tailscaleMode && tailscaleMode !== "off") {
102+return "loopback";
103+}
104+return netState.container ? "auto" : "loopback";
105+},
106+isContainerEnvironment: () => netState.container,
107+resolveGatewayBindHost: async (bind?: string, customHost?: string) => {
108+if (bind === "auto") {
109+return netState.autoBindHost;
110+}
111+if (bind === "lan") {
112+return "0.0.0.0";
113+}
114+if (bind === "custom") {
115+return customHost?.trim() || "0.0.0.0";
116+}
117+if (bind === "tailnet") {
118+return "100.64.0.1";
119+}
120+return "127.0.0.1";
121+},
122+};
123+});
124+88125vi.mock("../../gateway/server.js", () => ({
89126startGatewayServer: (port: number, opts?: unknown) => startGatewayServer(port, opts),
90127}));
@@ -177,6 +214,8 @@ describe("gateway run option collisions", () => {
177214resetRuntimeCapture();
178215configState.cfg = {};
179216configState.snapshot = { exists: false };
217+netState.autoBindHost = "127.0.0.1";
218+netState.container = false;
180219readBestEffortConfig.mockClear();
181220readConfigFileSnapshotWithPluginMetadata.mockClear();
182221controlUiState.root = "/tmp/openclaw-control-ui";
@@ -295,7 +334,9 @@ describe("gateway run option collisions", () => {
295334});
296335297336it("starts gateway when token mode has no configured token (startup bootstrap path)", async () => {
298-await runGatewayCli(["gateway", "run", "--allow-unconfigured"]);
337+await withEnvAsync(withoutGatewayAuthEnv, async () => {
338+await runGatewayCli(["gateway", "run", "--allow-unconfigured"]);
339+});
299340300341expect(readConfigFileSnapshotWithPluginMetadata).toHaveBeenCalledTimes(1);
301342expect(readBestEffortConfig).not.toHaveBeenCalled();
@@ -304,6 +345,56 @@ describe("gateway run option collisions", () => {
304345expect(options.startupConfigSnapshotRead).toEqual({ snapshot: configState.snapshot });
305346});
306347348+it("allows authless auto startup when it resolves to loopback", async () => {
349+await withEnvAsync(withoutGatewayAuthEnv, async () => {
350+await runGatewayCli(["gateway", "run", "--bind", "auto", "--allow-unconfigured"]);
351+});
352+353+const options = gatewayStartOptions();
354+expect(options.bind).toBe("auto");
355+});
356+357+it("blocks container auto startup without explicit gateway auth", async () => {
358+netState.autoBindHost = "0.0.0.0";
359+netState.container = true;
360+361+await withEnvAsync(withoutGatewayAuthEnv, async () => {
362+await expect(runGatewayCli(["gateway", "run", "--allow-unconfigured"])).rejects.toThrow(
363+"__exit__:78",
364+);
365+});
366+367+expect(runtimeErrors.join("\n")).toContain("Refusing to bind gateway to auto without auth.");
368+expect(startGatewayServer).not.toHaveBeenCalled();
369+});
370+371+it("blocks non-loopback startup without explicit gateway auth", async () => {
372+await withEnvAsync(withoutGatewayAuthEnv, async () => {
373+await expect(
374+runGatewayCli(["gateway", "run", "--bind", "lan", "--allow-unconfigured"]),
375+).rejects.toThrow("__exit__:78");
376+});
377+378+expect(runtimeErrors.join("\n")).toContain("Refusing to bind gateway to lan without auth.");
379+expect(startGatewayServer).not.toHaveBeenCalled();
380+});
381+382+it("allows non-loopback startup when token auth is explicit", async () => {
383+await runGatewayCli([
384+"gateway",
385+"run",
386+"--bind",
387+"lan",
388+"--token",
389+"tok_run",
390+"--allow-unconfigured",
391+]);
392+393+const options = gatewayStartOptions();
394+expect(options.bind).toBe("lan");
395+expect(options.auth?.token).toBe("tok_run");
396+});
397+307398it("uses the startup snapshot only for the first in-process gateway start", async () => {
308399runGatewayLoop.mockImplementationOnce(async ({ start }: { start: GatewayLoopStart }) => {
309400await start({ startupStartedAt: 1000 });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。