Fix local gateway detail probes on loopback

This commit is contained in:
OpenClaw Assistant 2026-03-14 20:25:55 +00:00
parent b49e1386d0
commit 7bbaad2eea
6 changed files with 26 additions and 24 deletions

View File

@ -5,6 +5,7 @@ import {
isProbeReachable,
isScopeLimitedProbeFailure,
renderProbeSummaryLine,
resolveProbeBudgetMs,
resolveAuthForTarget,
} from "./helpers.js";
@ -273,3 +274,12 @@ describe("probe reachability classification", () => {
expect(renderProbeSummaryLine(probe, false)).toContain("RPC: failed");
});
});
describe("resolveProbeBudgetMs", () => {
it("gives local loopback probes enough time for detail RPCs", () => {
expect(resolveProbeBudgetMs(10_000, "localLoopback")).toBe(3000);
expect(resolveProbeBudgetMs(1200, "localLoopback")).toBe(1200);
expect(resolveProbeBudgetMs(10_000, "sshTunnel")).toBe(2000);
expect(resolveProbeBudgetMs(10_000, "explicit")).toBe(1500);
});
});

View File

@ -118,7 +118,9 @@ export function resolveTargets(cfg: OpenClawConfig, explicitUrl?: string): Gatew
export function resolveProbeBudgetMs(overallMs: number, kind: TargetKind): number {
if (kind === "localLoopback") {
return Math.min(800, overallMs);
// Full localhost detail probes can take longer than the old 800ms budget,
// especially when they exercise status + heartbeat + presence RPCs.
return Math.min(3000, overallMs);
}
if (kind === "sshTunnel") {
return Math.min(2000, overallMs);

View File

@ -28,6 +28,7 @@ let startMode: StartMode = "hello";
let closeCode = 1006;
let closeReason = "";
let helloMethods: string[] | undefined = ["health", "secrets.resolve"];
const deviceIdentityMarker = { id: "test-device-identity" };
vi.mock("./client.js", () => ({
describeGatewayCloseCode: (code: number) => {
@ -73,6 +74,10 @@ vi.mock("./client.js", () => ({
},
}));
vi.mock("../infra/device-identity.js", () => ({
loadOrCreateDeviceIdentity: () => deviceIdentityMarker,
}));
const { buildGatewayConnectionDetails, callGateway, callGatewayCli, callGatewayScoped } =
await import("./call.js");
@ -209,7 +214,7 @@ describe("callGateway url resolution", () => {
expect(lastClientOptions?.token).toBe("explicit-token");
});
it("does not attach device identity for local loopback shared-token auth", async () => {
it("attaches device identity for local loopback shared-token auth", async () => {
setLocalLoopbackGatewayConfig();
await callGateway({
@ -219,7 +224,7 @@ describe("callGateway url resolution", () => {
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18789");
expect(lastClientOptions?.token).toBe("explicit-token");
expect(lastClientOptions?.deviceIdentity).toBeUndefined();
expect(lastClientOptions?.deviceIdentity).toEqual(deviceIdentityMarker);
});
it("uses OPENCLAW_GATEWAY_URL env override in remote mode when remote URL is missing", async () => {

View File

@ -81,20 +81,15 @@ export type GatewayConnectionDetails = {
message: string;
};
function shouldAttachDeviceIdentityForGatewayCall(params: {
function shouldAttachDeviceIdentityForGatewayCall(_params: {
url: string;
token?: string;
password?: string;
}): boolean {
if (!(params.token || params.password)) {
return true;
}
try {
const parsed = new URL(params.url);
return !["127.0.0.1", "::1", "localhost"].includes(parsed.hostname);
} catch {
return true;
}
// Even when local CLI calls authenticate with a shared gateway token/password,
// we still want to attach device identity so paired operator scopes remain
// available for detail RPCs such as status / system-presence / last-heartbeat.
return true;
}
export type ExplicitGatewayAuth = {

View File

@ -51,7 +51,7 @@ describe("probeGateway", () => {
});
expect(gatewayClientState.options?.scopes).toEqual(["operator.read"]);
expect(gatewayClientState.options?.deviceIdentity).toBeNull();
expect(gatewayClientState.options?.deviceIdentity).toBeUndefined();
expect(gatewayClientState.requests).toEqual([
"health",
"status",

View File

@ -4,7 +4,6 @@ import type { SystemPresence } from "../infra/system-presence.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { GatewayClient } from "./client.js";
import { READ_SCOPE } from "./method-scopes.js";
import { isLoopbackHost } from "./net.js";
export type GatewayProbeAuth = {
token?: string;
@ -41,14 +40,6 @@ export async function probeGateway(opts: {
let connectError: string | null = null;
let close: GatewayProbeClose | null = null;
const disableDeviceIdentity = (() => {
try {
return isLoopbackHost(new URL(opts.url).hostname);
} catch {
return false;
}
})();
return await new Promise<GatewayProbeResult>((resolve) => {
let settled = false;
const settle = (result: Omit<GatewayProbeResult, "url">) => {
@ -70,7 +61,6 @@ export async function probeGateway(opts: {
clientVersion: "dev",
mode: GATEWAY_CLIENT_MODES.PROBE,
instanceId,
deviceIdentity: disableDeviceIdentity ? null : undefined,
onConnectError: (err) => {
connectError = formatErrorMessage(err);
},