fix: stabilize gateway ws + iOS

This commit is contained in:
Peter Steinberger
2026-01-19 06:22:01 +00:00
parent 73afbc9193
commit 3776de906f
14 changed files with 105 additions and 46 deletions

View File

@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import { WebSocket } from "ws";
import { WebSocket, type ClientOptions, type CertMeta } from "ws";
import { rawDataToString } from "../infra/ws.js";
import { logDebug, logError } from "../logger.js";
import type { DeviceIdentity } from "../infra/device-identity.js";
@@ -85,18 +85,21 @@ export class GatewayClient {
if (this.closed) return;
const url = this.opts.url ?? "ws://127.0.0.1:18789";
// Allow node screen snapshots and other large responses.
const wsOptions: ConstructorParameters<typeof WebSocket>[1] = {
const wsOptions: ClientOptions = {
maxPayload: 25 * 1024 * 1024,
};
if (url.startsWith("wss://") && this.opts.tlsFingerprint) {
wsOptions.rejectUnauthorized = false;
wsOptions.checkServerIdentity = (_host, cert) => {
wsOptions.checkServerIdentity = (_host: string, cert: CertMeta) => {
const fingerprintValue =
typeof cert === "object" && cert && "fingerprint256" in cert
? (cert as { fingerprint256?: string }).fingerprint256 ?? ""
: "";
const fingerprint = normalizeFingerprint(
typeof cert?.fingerprint256 === "string" ? cert.fingerprint256 : "",
typeof fingerprintValue === "string" ? fingerprintValue : "",
);
const expected = normalizeFingerprint(this.opts.tlsFingerprint ?? "");
if (fingerprint && fingerprint === expected) return undefined;
return new Error("gateway tls fingerprint mismatch");
return Boolean(fingerprint && fingerprint === expected);
};
}
this.ws = new WebSocket(url, wsOptions);

View File

@@ -119,7 +119,7 @@ export class NodeRegistry {
timeoutMs: params.timeoutMs,
idempotencyKey: params.idempotencyKey,
};
const ok = this.sendEvent(node, "node.invoke.request", payload);
const ok = this.sendEventToSession(node, "node.invoke.request", payload);
if (!ok) {
return {
ok: false,
@@ -172,7 +172,7 @@ export class NodeRegistry {
return this.sendEventToSession(node, event, payload);
}
private sendEvent(node: NodeSession, event: string, payload: unknown): boolean {
private sendEventInternal(node: NodeSession, event: string, payload: unknown): boolean {
try {
node.client.socket.send(
JSON.stringify({
@@ -188,6 +188,6 @@ export class NodeRegistry {
}
private sendEventToSession(node: NodeSession, event: string, payload: unknown): boolean {
return this.sendEvent(node, event, payload);
return this.sendEventInternal(node, event, payload);
}
}

View File

@@ -451,7 +451,6 @@ export const nodeHandlers: GatewayRequestHandlers = {
nodeContext,
"node",
{
type: "event",
event: p.event,
payloadJSON,
},

View File

@@ -356,13 +356,15 @@ export async function startGatewayServer(
const execApprovalManager = new ExecApprovalManager();
const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager);
const canvasHostServerPort = (canvasHostServer as CanvasHostServer | null)?.port;
attachGatewayWsHandlers({
wss,
clients,
port,
gatewayHost: bindHost ?? undefined,
canvasHostEnabled: Boolean(canvasHost),
canvasHostServerPort: canvasHostServer?.port ?? undefined,
canvasHostServerPort,
resolvedAuth,
gatewayMethods,
events: GATEWAY_EVENTS,

View File

@@ -11,7 +11,7 @@ import {
rpcReq,
startServerWithClient,
} from "./test-helpers.js";
import { GATEWAY_CLIENT_MODES } from "../utils/message-channel.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
installGatewayTestHooks();
@@ -127,7 +127,7 @@ describe("gateway server models + voicewake", () => {
await connectOk(nodeWs, {
role: "node",
client: {
id: "n1",
id: GATEWAY_CLIENT_NAMES.NODE_HOST,
version: "1.0.0",
platform: "ios",
mode: GATEWAY_CLIENT_MODES.NODE,

View File

@@ -32,6 +32,7 @@ describe("sessions_send gateway loopback", () => {
it("returns reply when lifecycle ends before agent.wait", async () => {
const port = await getFreePort();
vi.stubEnv("CLAWDBOT_GATEWAY_PORT", String(port));
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "test-token");
const server = await startGatewayServer(port);
const spy = vi.mocked(agentCommand);
@@ -105,6 +106,7 @@ describe("sessions_send label lookup", () => {
it("finds session by label and sends message", { timeout: 60_000 }, async () => {
const port = await getFreePort();
vi.stubEnv("CLAWDBOT_GATEWAY_PORT", String(port));
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "test-token");
const server = await startGatewayServer(port);
servers.push(server);
@@ -171,6 +173,7 @@ describe("sessions_send label lookup", () => {
it("returns error when label not found", { timeout: 60_000 }, async () => {
const port = await getFreePort();
vi.stubEnv("CLAWDBOT_GATEWAY_PORT", String(port));
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "test-token");
const server = await startGatewayServer(port);
servers.push(server);
@@ -191,6 +194,7 @@ describe("sessions_send label lookup", () => {
it("returns error when neither sessionKey nor label provided", { timeout: 60_000 }, async () => {
const port = await getFreePort();
vi.stubEnv("CLAWDBOT_GATEWAY_PORT", String(port));
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "test-token");
const server = await startGatewayServer(port);
servers.push(server);

View File

@@ -4,6 +4,8 @@ import {
loadGatewayTlsRuntime as loadGatewayTlsRuntimeConfig,
} from "../../infra/tls/gateway.js";
export type { GatewayTlsRuntime } from "../../infra/tls/gateway.js";
export async function loadGatewayTlsRuntime(
cfg: GatewayTlsConfig | undefined,
log?: { info?: (msg: string) => void; warn?: (msg: string) => void },