fix: surface upstream gateway timeout for remote OpenClaw/Tailscale connections (#94)

* surface gateway timeout for tailscale

* talescale fix #2 - attempt 1

* luke findings fix#1

* add narrow log for clientId

* prod safe proxy log

* fix log visibility

* LAN connection & subagent SOUL|IDENTITY fixes

* Initialize missing files for subagent SOUL|IDENTITY

* surface missing files in UI

* capturing agent - runtime,identity,session

* plugin-install fix

* fix: recover agent workspace for marketplace installs

* fix: recover agent workspace and identity name from file provenance

* fix: tolerate webchat session patch blocks during permission updates
This commit is contained in:
gsknnft
2026-04-03 18:57:36 -04:00
committed by GitHub
parent 4be98d7080
commit a18c8c630c
28 changed files with 1174 additions and 76 deletions
+114 -1
View File
@@ -15,10 +15,16 @@ const setupAndImportHook = async (gatewayUrl: string | null) => {
vi.resetModules();
vi.spyOn(console, "info").mockImplementation(() => {});
const captured: { url: string | null; token: unknown; authScopeKey: unknown } = {
const captured: {
url: string | null;
token: unknown;
authScopeKey: unknown;
clientName: unknown;
} = {
url: null,
token: null,
authScopeKey: null,
clientName: null,
};
vi.doMock("../../src/lib/gateway/openclaw/GatewayBrowserClient", () => {
@@ -35,6 +41,7 @@ const setupAndImportHook = async (gatewayUrl: string | null) => {
captured.url = typeof opts.url === "string" ? opts.url : null;
captured.token = "token" in opts ? opts.token : null;
captured.authScopeKey = "authScopeKey" in opts ? opts.authScopeKey : null;
captured.clientName = "clientName" in opts ? opts.clientName : null;
this.opts = {
onHello: typeof opts.onHello === "function" ? (opts.onHello as (hello: unknown) => void) : undefined,
onEvent: typeof opts.onEvent === "function" ? (opts.onEvent as (event: unknown) => void) : undefined,
@@ -186,6 +193,99 @@ describe("useGatewayConnection", () => {
});
expect(captured.token).toBe("");
expect(captured.authScopeKey).toBe("wss://remote.example");
expect(captured.clientName).toBe("openclaw-control-ui");
});
it("uses_webchat_identity_for_remote_openclaw_connections", async () => {
const { useGatewayConnection, captured } = await setupAndImportHook(null);
const coordinator = {
loadSettings: async () => null,
loadSettingsEnvelope: async () => ({
settings: {
version: 1,
gateway: {
url: "wss://pi5.myth-coho.ts.net",
token: "shared-token",
adapterType: "openclaw",
lastKnownGood: {
url: "wss://pi5.myth-coho.ts.net",
token: "shared-token",
adapterType: "openclaw",
},
},
focused: {},
avatars: {},
analytics: {},
voiceReplies: {},
office: {},
deskAssignments: {},
standup: {},
taskBoard: {},
},
localGatewayDefaults: null,
}),
schedulePatch: () => {},
flushPending: async () => {},
};
const Probe = () => {
useGatewayConnection(coordinator);
return createElement("div", null, "ok");
};
render(createElement(Probe));
await waitFor(() => {
expect(captured.url).toBe("ws://localhost:3000/api/gateway/ws");
});
expect(captured.authScopeKey).toBe("wss://pi5.myth-coho.ts.net");
expect(captured.clientName).toBe("webchat-ui");
});
it("keeps_control_ui_identity_for_local_openclaw_connections", async () => {
const { useGatewayConnection, captured } = await setupAndImportHook(null);
const coordinator = {
loadSettings: async () => null,
loadSettingsEnvelope: async () => ({
settings: {
version: 1,
gateway: {
url: "ws://localhost:18789",
token: "shared-token",
adapterType: "openclaw",
lastKnownGood: {
url: "ws://localhost:18789",
token: "shared-token",
adapterType: "openclaw",
},
},
focused: {},
avatars: {},
analytics: {},
voiceReplies: {},
office: {},
deskAssignments: {},
standup: {},
taskBoard: {},
},
localGatewayDefaults: null,
}),
schedulePatch: () => {},
flushPending: async () => {},
};
const Probe = () => {
useGatewayConnection(coordinator);
return createElement("div", null, "ok");
};
render(createElement(Probe));
await waitFor(() => {
expect(captured.url).toBe("ws://localhost:3000/api/gateway/ws");
});
expect(captured.authScopeKey).toBe("ws://localhost:18789");
expect(captured.clientName).toBe("openclaw-control-ui");
});
it("does_not_auto_connect_without_a_last_known_good_state", async () => {
@@ -257,6 +357,19 @@ describe("useGatewayConnection", () => {
expect(mod.resolveInitialGatewayConnectAttemptCount("openclaw", true)).toBe(1);
});
it("uses_webchat_client_id_only_for_remote_openclaw", async () => {
const mod = await import("@/lib/gateway/GatewayClient");
expect(mod.resolveGatewayClientName("openclaw", "wss://pi5.myth-coho.ts.net")).toBe(
"webchat-ui"
);
expect(mod.resolveGatewayClientName("openclaw", "ws://localhost:18789")).toBe(
"openclaw-control-ui"
);
expect(mod.resolveGatewayClientName("hermes", "ws://localhost:18789")).toBe(
"openclaw-control-ui"
);
});
it("auto_applies_runtime_local_defaults_when_no_saved_gateway_and_build_time_empty", async () => {
// Simulates #57: NEXT_PUBLIC_GATEWAY_URL was never rebuilt, but
// CLAW3D_GATEWAY_URL is set on the server so localGatewayDefaults