Files
clawdbot/src/gateway/server-node-bridge.ts
2026-01-14 09:11:21 +00:00

172 lines
5.1 KiB
TypeScript

import type { NodeBridgeServer } from "../infra/bridge/server.js";
import { startNodeBridgeServer } from "../infra/bridge/server.js";
import {
listSystemPresence,
upsertPresence,
} from "../infra/system-presence.js";
import { loadVoiceWakeConfig } from "../infra/voicewake.js";
import { isLoopbackAddress } from "./net.js";
import {
getHealthVersion,
getPresenceVersion,
incrementPresenceVersion,
} from "./server/health-state.js";
import type {
BridgeEvent,
BridgeRequest,
BridgeResponse,
} from "./server-bridge-types.js";
export type GatewayNodeBridgeRuntime = {
bridge: NodeBridgeServer | null;
nodePresenceTimers: Map<string, ReturnType<typeof setInterval>>;
};
export async function startGatewayNodeBridge(params: {
bridgeEnabled: boolean;
bridgePort: number;
bridgeHost: string | null;
machineDisplayName: string;
canvasHostPort?: number;
canvasHostHost?: string;
broadcast: (
event: string,
payload: unknown,
opts?: {
dropIfSlow?: boolean;
stateVersion?: { presence?: number; health?: number };
},
) => void;
bridgeUnsubscribeAll: (nodeId: string) => void;
handleBridgeRequest: (
nodeId: string,
req: BridgeRequest,
) => Promise<BridgeResponse>;
handleBridgeEvent: (nodeId: string, evt: BridgeEvent) => Promise<void> | void;
logBridge: { info: (msg: string) => void; warn: (msg: string) => void };
}): Promise<GatewayNodeBridgeRuntime> {
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>();
const stopNodePresenceTimer = (nodeId: string) => {
const timer = nodePresenceTimers.get(nodeId);
if (timer) {
clearInterval(timer);
}
nodePresenceTimers.delete(nodeId);
};
const beaconNodePresence = (
node: {
nodeId: string;
displayName?: string;
remoteIp?: string;
version?: string;
platform?: string;
deviceFamily?: string;
modelIdentifier?: string;
},
reason: string,
) => {
const host = node.displayName?.trim() || node.nodeId;
const rawIp = node.remoteIp?.trim();
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
const version = node.version?.trim() || "unknown";
const platform = node.platform?.trim() || undefined;
const deviceFamily = node.deviceFamily?.trim() || undefined;
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason ${reason}`;
upsertPresence(node.nodeId, {
host,
ip,
version,
platform,
deviceFamily,
modelIdentifier,
mode: "remote",
reason,
lastInputSeconds: 0,
instanceId: node.nodeId,
text,
});
incrementPresenceVersion();
params.broadcast(
"presence",
{ presence: listSystemPresence() },
{
dropIfSlow: true,
stateVersion: {
presence: getPresenceVersion(),
health: getHealthVersion(),
},
},
);
};
const startNodePresenceTimer = (node: { nodeId: string }) => {
stopNodePresenceTimer(node.nodeId);
nodePresenceTimers.set(
node.nodeId,
setInterval(() => {
beaconNodePresence(node, "periodic");
}, 180_000),
);
};
if (params.bridgeEnabled && params.bridgePort > 0 && params.bridgeHost) {
try {
const started = await startNodeBridgeServer({
host: params.bridgeHost,
port: params.bridgePort,
serverName: params.machineDisplayName,
canvasHostPort: params.canvasHostPort,
canvasHostHost: params.canvasHostHost,
onRequest: (nodeId, req) => params.handleBridgeRequest(nodeId, req),
onAuthenticated: async (node) => {
beaconNodePresence(node, "node-connected");
startNodePresenceTimer(node);
try {
const cfg = await loadVoiceWakeConfig();
started.sendEvent({
nodeId: node.nodeId,
event: "voicewake.changed",
payloadJSON: JSON.stringify({ triggers: cfg.triggers }),
});
} catch {
// Best-effort only.
}
},
onDisconnected: (node) => {
params.bridgeUnsubscribeAll(node.nodeId);
stopNodePresenceTimer(node.nodeId);
beaconNodePresence(node, "node-disconnected");
},
onEvent: params.handleBridgeEvent,
onPairRequested: (request) => {
params.broadcast("node.pair.requested", request, {
dropIfSlow: true,
});
},
});
if (started.port > 0) {
params.logBridge.info(
`listening on tcp://${params.bridgeHost}:${started.port} (node)`,
);
return { bridge: started, nodePresenceTimers };
}
} catch (err) {
params.logBridge.warn(`failed to start: ${String(err)}`);
}
} else if (
params.bridgeEnabled &&
params.bridgePort > 0 &&
!params.bridgeHost
) {
params.logBridge.warn(
"bind policy requested tailnet IP, but no tailnet interface was found; refusing to start bridge",
);
}
return { bridge: null, nodePresenceTimers };
}