@wingman-ai/gateway 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -111
- package/dist/agent/config/agentConfig.cjs +2 -0
- package/dist/agent/config/agentConfig.d.ts +6 -0
- package/dist/agent/config/agentConfig.js +2 -0
- package/dist/agent/config/agentLoader.cjs +21 -18
- package/dist/agent/config/agentLoader.js +22 -19
- package/dist/agent/config/mcpClientManager.cjs +48 -9
- package/dist/agent/config/mcpClientManager.d.ts +12 -0
- package/dist/agent/config/mcpClientManager.js +48 -9
- package/dist/agent/config/toolRegistry.cjs +19 -0
- package/dist/agent/config/toolRegistry.d.ts +4 -0
- package/dist/agent/config/toolRegistry.js +17 -1
- package/dist/agent/middleware/additional-messages.cjs +115 -11
- package/dist/agent/middleware/additional-messages.d.ts +9 -0
- package/dist/agent/middleware/additional-messages.js +115 -11
- package/dist/agent/tests/agentLoader.test.cjs +45 -0
- package/dist/agent/tests/agentLoader.test.js +45 -0
- package/dist/agent/tests/mcpClientManager.test.cjs +50 -0
- package/dist/agent/tests/mcpClientManager.test.js +50 -0
- package/dist/agent/tests/toolRegistry.test.cjs +2 -0
- package/dist/agent/tests/toolRegistry.test.js +2 -0
- package/dist/agent/tools/node_invoke.cjs +146 -0
- package/dist/agent/tools/node_invoke.d.ts +86 -0
- package/dist/agent/tools/node_invoke.js +109 -0
- package/dist/cli/commands/gateway.cjs +1 -1
- package/dist/cli/commands/gateway.js +1 -1
- package/dist/cli/commands/skill.cjs +12 -4
- package/dist/cli/commands/skill.js +12 -4
- package/dist/cli/config/jsonSchema.cjs +55 -0
- package/dist/cli/config/jsonSchema.d.ts +2 -0
- package/dist/cli/config/jsonSchema.js +18 -0
- package/dist/cli/config/loader.cjs +33 -1
- package/dist/cli/config/loader.js +33 -1
- package/dist/cli/config/schema.cjs +119 -2
- package/dist/cli/config/schema.d.ts +40 -0
- package/dist/cli/config/schema.js +119 -2
- package/dist/cli/core/agentInvoker.cjs +25 -4
- package/dist/cli/core/agentInvoker.d.ts +13 -0
- package/dist/cli/core/agentInvoker.js +25 -4
- package/dist/cli/services/skillRepository.cjs +138 -20
- package/dist/cli/services/skillRepository.d.ts +10 -2
- package/dist/cli/services/skillRepository.js +138 -20
- package/dist/cli/services/skillSecurityScanner.cjs +158 -0
- package/dist/cli/services/skillSecurityScanner.d.ts +28 -0
- package/dist/cli/services/skillSecurityScanner.js +121 -0
- package/dist/cli/services/skillService.cjs +44 -12
- package/dist/cli/services/skillService.d.ts +2 -0
- package/dist/cli/services/skillService.js +46 -14
- package/dist/cli/types/skill.d.ts +9 -0
- package/dist/gateway/http/nodes.cjs +247 -0
- package/dist/gateway/http/nodes.d.ts +20 -0
- package/dist/gateway/http/nodes.js +210 -0
- package/dist/gateway/node.cjs +10 -1
- package/dist/gateway/node.d.ts +10 -1
- package/dist/gateway/node.js +10 -1
- package/dist/gateway/server.cjs +418 -27
- package/dist/gateway/server.d.ts +34 -0
- package/dist/gateway/server.js +412 -27
- package/dist/gateway/types.d.ts +15 -1
- package/dist/gateway/validation.cjs +2 -0
- package/dist/gateway/validation.d.ts +4 -0
- package/dist/gateway/validation.js +2 -0
- package/dist/tests/additionalMessageMiddleware.test.cjs +92 -0
- package/dist/tests/additionalMessageMiddleware.test.js +92 -0
- package/dist/tests/cli-config-loader.test.cjs +33 -1
- package/dist/tests/cli-config-loader.test.js +33 -1
- package/dist/tests/config-json-schema.test.cjs +25 -0
- package/dist/tests/config-json-schema.test.d.ts +1 -0
- package/dist/tests/config-json-schema.test.js +19 -0
- package/dist/tests/gateway-http-security.test.cjs +277 -0
- package/dist/tests/gateway-http-security.test.d.ts +1 -0
- package/dist/tests/gateway-http-security.test.js +271 -0
- package/dist/tests/gateway-node-mode.test.cjs +174 -0
- package/dist/tests/gateway-node-mode.test.d.ts +1 -0
- package/dist/tests/gateway-node-mode.test.js +168 -0
- package/dist/tests/gateway-origin-policy.test.cjs +60 -0
- package/dist/tests/gateway-origin-policy.test.d.ts +1 -0
- package/dist/tests/gateway-origin-policy.test.js +54 -0
- package/dist/tests/gateway.test.cjs +1 -0
- package/dist/tests/gateway.test.js +1 -0
- package/dist/tests/node-tools.test.cjs +77 -0
- package/dist/tests/node-tools.test.d.ts +1 -0
- package/dist/tests/node-tools.test.js +71 -0
- package/dist/tests/nodes-api.test.cjs +86 -0
- package/dist/tests/nodes-api.test.d.ts +1 -0
- package/dist/tests/nodes-api.test.js +80 -0
- package/dist/tests/skill-repository.test.cjs +106 -0
- package/dist/tests/skill-repository.test.d.ts +1 -0
- package/dist/tests/skill-repository.test.js +100 -0
- package/dist/tests/skill-security-scanner.test.cjs +126 -0
- package/dist/tests/skill-security-scanner.test.d.ts +1 -0
- package/dist/tests/skill-security-scanner.test.js +120 -0
- package/dist/tests/uv.test.cjs +47 -0
- package/dist/tests/uv.test.d.ts +1 -0
- package/dist/tests/uv.test.js +41 -0
- package/dist/utils/uv.cjs +64 -0
- package/dist/utils/uv.d.ts +3 -0
- package/dist/utils/uv.js +24 -0
- package/dist/webui/assets/{index-DHbfLOUR.js → index-BMekSELC.js} +106 -106
- package/dist/webui/index.html +1 -1
- package/package.json +2 -1
- package/skills/gog/SKILL.md +36 -0
- package/skills/weather/SKILL.md +49 -0
package/dist/gateway/server.js
CHANGED
|
@@ -8,15 +8,17 @@ import { AgentInvoker } from "../cli/core/agentInvoker.js";
|
|
|
8
8
|
import { OutputManager } from "../cli/core/outputManager.js";
|
|
9
9
|
import { SessionManager } from "../cli/core/sessionManager.js";
|
|
10
10
|
import { createLogger } from "../logger.js";
|
|
11
|
+
import { ensureUvAvailableForFeature } from "../utils/uv.js";
|
|
11
12
|
import { DiscordGatewayAdapter } from "./adapters/discord.js";
|
|
12
13
|
import { GatewayAuth } from "./auth.js";
|
|
13
|
-
import { BrowserRelayServer } from "./browserRelayServer.js";
|
|
14
14
|
import { BroadcastGroupManager } from "./broadcast.js";
|
|
15
|
+
import { BrowserRelayServer } from "./browserRelayServer.js";
|
|
15
16
|
import { MDNSDiscoveryService, TailscaleDiscoveryService } from "./discovery/index.js";
|
|
16
17
|
import { getGatewayTokenFromEnv } from "./env.js";
|
|
17
18
|
import { InternalHookRegistry } from "./hooks/registry.js";
|
|
18
19
|
import { handleAgentsApi } from "./http/agents.js";
|
|
19
20
|
import { handleFsApi } from "./http/fs.js";
|
|
21
|
+
import { createNodeApprovalStore, handleNodesApi } from "./http/nodes.js";
|
|
20
22
|
import { handleProvidersApi } from "./http/providers.js";
|
|
21
23
|
import { createRoutineStore, handleRoutinesApi } from "./http/routines.js";
|
|
22
24
|
import { handleSessionsApi } from "./http/sessions.js";
|
|
@@ -36,19 +38,72 @@ function _define_property(obj, key, value) {
|
|
|
36
38
|
return obj;
|
|
37
39
|
}
|
|
38
40
|
const API_CORS_HEADERS = {
|
|
39
|
-
"Access-Control-Allow-Origin": "*",
|
|
40
41
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
41
42
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Wingman-Token, X-Wingman-Password",
|
|
42
43
|
"Access-Control-Max-Age": "600"
|
|
43
44
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
const LOOPBACK_HOSTNAMES = new Set([
|
|
46
|
+
"127.0.0.1",
|
|
47
|
+
"localhost",
|
|
48
|
+
"::1"
|
|
49
|
+
]);
|
|
50
|
+
const CLIENT_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/;
|
|
51
|
+
const CLIENT_TYPE_PATTERN = /^[a-zA-Z0-9._:-]{1,64}$/;
|
|
52
|
+
const NODE_EXECUTION_CAPABILITIES = new Set([
|
|
53
|
+
"system.notify",
|
|
54
|
+
"system.run"
|
|
55
|
+
]);
|
|
56
|
+
const DEFAULT_NODE_REQUEST_TIMEOUT_MS = 30000;
|
|
57
|
+
const MIN_NODE_REQUEST_TIMEOUT_MS = 1000;
|
|
58
|
+
const MAX_NODE_REQUEST_TIMEOUT_MS = 120000;
|
|
59
|
+
const MAX_PENDING_NODE_REQUESTS = 2000;
|
|
60
|
+
function normalizeHostname(hostname) {
|
|
61
|
+
const trimmed = hostname.trim().toLowerCase();
|
|
62
|
+
if (!trimmed) return "";
|
|
63
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1);
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
function defaultPortForProtocol(protocol) {
|
|
67
|
+
return "https:" === protocol ? "443" : "80";
|
|
68
|
+
}
|
|
69
|
+
function normalizeClientIdentifier(raw, pattern) {
|
|
70
|
+
const trimmed = raw.trim();
|
|
71
|
+
if (!trimmed) return null;
|
|
72
|
+
if (trimmed.length > 128) return null;
|
|
73
|
+
if (!pattern.test(trimmed)) return null;
|
|
74
|
+
return trimmed;
|
|
75
|
+
}
|
|
76
|
+
function includesNodeExecutionCapability(capabilities) {
|
|
77
|
+
if (!Array.isArray(capabilities)) return false;
|
|
78
|
+
for (const capability of capabilities)if ("string" == typeof capability && NODE_EXECUTION_CAPABILITIES.has(capability.trim())) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function isLoopbackHostname(hostname) {
|
|
82
|
+
return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
|
|
83
|
+
}
|
|
84
|
+
function isGatewayOriginAllowed(params) {
|
|
85
|
+
let originUrl;
|
|
86
|
+
let requestUrl;
|
|
87
|
+
try {
|
|
88
|
+
originUrl = new URL(params.origin);
|
|
89
|
+
requestUrl = "string" == typeof params.requestUrl ? new URL(params.requestUrl) : params.requestUrl;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if ("http:" !== originUrl.protocol && "https:" !== originUrl.protocol) return false;
|
|
94
|
+
const originHost = normalizeHostname(originUrl.hostname);
|
|
95
|
+
const requestHost = normalizeHostname(requestUrl.hostname);
|
|
96
|
+
const configHost = normalizeHostname(params.gatewayHost);
|
|
97
|
+
if (isLoopbackHostname(originHost) && isLoopbackHostname(requestHost)) return true;
|
|
98
|
+
if (!originHost || originHost !== requestHost) return false;
|
|
99
|
+
const originPort = originUrl.port || defaultPortForProtocol(originUrl.protocol);
|
|
100
|
+
const allowedPorts = new Set([
|
|
101
|
+
String(params.gatewayPort)
|
|
102
|
+
]);
|
|
103
|
+
if (params.controlUiEnabled) allowedPorts.add(String(params.controlUiPort));
|
|
104
|
+
if ("0.0.0.0" === configHost || "::" === configHost) return allowedPorts.has(originPort);
|
|
105
|
+
if (originHost !== configHost) return false;
|
|
106
|
+
return allowedPorts.has(originPort);
|
|
52
107
|
}
|
|
53
108
|
function resolveExecutionWorkspaceOverride(payload) {
|
|
54
109
|
const rawWorkspace = payload?.execution?.workspace;
|
|
@@ -66,9 +121,12 @@ function resolveExecutionConfigDirOverride(payload) {
|
|
|
66
121
|
class GatewayServer {
|
|
67
122
|
async start() {
|
|
68
123
|
if (void 0 === globalThis.Bun) throw new Error("Gateway server requires Bun runtime. Start with `bun ./bin/wingman gateway start`.");
|
|
124
|
+
const proxyConfig = this.wingmanConfig.gateway?.mcpProxy;
|
|
125
|
+
if (proxyConfig?.enabled) ensureUvAvailableForFeature(proxyConfig.command || "uvx", "gateway.mcpProxy.enabled");
|
|
69
126
|
this.startedAt = Date.now();
|
|
70
127
|
this.internalHooks = new InternalHookRegistry(this.getHttpContext(), this.wingmanConfig.hooks);
|
|
71
128
|
await this.internalHooks.load();
|
|
129
|
+
if (this.config.auth?.allowTailscale && !isLoopbackHostname(this.config.host)) this.log("warn", "Tailscale header-based auth bypass is disabled on non-loopback gateway hosts; use token/password auth instead.");
|
|
72
130
|
this.server = Bun.serve({
|
|
73
131
|
port: this.config.port,
|
|
74
132
|
hostname: this.config.host,
|
|
@@ -76,10 +134,21 @@ class GatewayServer {
|
|
|
76
134
|
const url = new URL(req.url);
|
|
77
135
|
if ("/health" === url.pathname) return this.handleHealthCheck();
|
|
78
136
|
if ("/stats" === url.pathname) return this.handleStats();
|
|
79
|
-
if ("/bridge/send" === url.pathname)
|
|
80
|
-
|
|
137
|
+
if ("/bridge/send" === url.pathname) {
|
|
138
|
+
const authFailure = this.requireHttpAuth(req);
|
|
139
|
+
if (authFailure) return authFailure;
|
|
140
|
+
return this.handleBridgeSend(req);
|
|
141
|
+
}
|
|
142
|
+
if ("/bridge/poll" === url.pathname) {
|
|
143
|
+
const authFailure = this.requireHttpAuth(req);
|
|
144
|
+
if (authFailure) return authFailure;
|
|
145
|
+
return this.handleBridgePoll(req);
|
|
146
|
+
}
|
|
81
147
|
if ("/ws" === url.pathname) {
|
|
82
|
-
|
|
148
|
+
if (!this.isRequestOriginAllowed(req, url)) return new Response("Forbidden origin", {
|
|
149
|
+
status: 403
|
|
150
|
+
});
|
|
151
|
+
const tailscaleUser = this.resolveTrustedTailscaleUser(req);
|
|
83
152
|
const upgraded = server.upgrade(req, {
|
|
84
153
|
data: {
|
|
85
154
|
nodeId: "",
|
|
@@ -269,6 +338,18 @@ class GatewayServer {
|
|
|
269
338
|
case "direct":
|
|
270
339
|
this.handleDirect(ws, msg);
|
|
271
340
|
break;
|
|
341
|
+
case "req:node":
|
|
342
|
+
this.handleNodeRequest(ws, msg);
|
|
343
|
+
break;
|
|
344
|
+
case "event:node":
|
|
345
|
+
this.handleNodeResponse(ws, msg);
|
|
346
|
+
break;
|
|
347
|
+
case "res":
|
|
348
|
+
this.handleNodeResponse(ws, msg);
|
|
349
|
+
break;
|
|
350
|
+
case "error":
|
|
351
|
+
this.handleNodeResponse(ws, msg);
|
|
352
|
+
break;
|
|
272
353
|
case "ping":
|
|
273
354
|
this.handlePing(ws, msg);
|
|
274
355
|
break;
|
|
@@ -290,6 +371,7 @@ class GatewayServer {
|
|
|
290
371
|
this.nodeManager.unregisterNode(nodeId);
|
|
291
372
|
this.log("info", `Node disconnected: ${nodeId}`);
|
|
292
373
|
}
|
|
374
|
+
this.cleanupPendingNodeRequestsForSocket(ws);
|
|
293
375
|
this.connectedClients.delete(ws);
|
|
294
376
|
this.clearSessionSubscriptions(ws);
|
|
295
377
|
this.cancelSocketAgentRequests(ws);
|
|
@@ -318,8 +400,21 @@ class GatewayServer {
|
|
|
318
400
|
ws.close();
|
|
319
401
|
return;
|
|
320
402
|
}
|
|
321
|
-
|
|
322
|
-
|
|
403
|
+
const clientId = normalizeClientIdentifier(msg.client.instanceId, CLIENT_ID_PATTERN);
|
|
404
|
+
const clientType = normalizeClientIdentifier(msg.client.clientType, CLIENT_TYPE_PATTERN);
|
|
405
|
+
if (!clientId || !clientType) {
|
|
406
|
+
this.sendMessage(ws, {
|
|
407
|
+
type: "res",
|
|
408
|
+
id: msg.id,
|
|
409
|
+
ok: false,
|
|
410
|
+
payload: "invalid client info",
|
|
411
|
+
timestamp: Date.now()
|
|
412
|
+
});
|
|
413
|
+
ws.close();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
ws.data.clientId = clientId;
|
|
417
|
+
ws.data.clientType = clientType;
|
|
323
418
|
ws.data.authenticated = true;
|
|
324
419
|
this.connectedClients.add(ws);
|
|
325
420
|
this.sendMessage(ws, {
|
|
@@ -513,7 +608,12 @@ class GatewayServer {
|
|
|
513
608
|
sessionManager,
|
|
514
609
|
terminalSessionManager: this.terminalSessionManager,
|
|
515
610
|
workdir,
|
|
516
|
-
defaultOutputDir
|
|
611
|
+
defaultOutputDir,
|
|
612
|
+
mcpProxyConfig: this.wingmanConfig.gateway?.mcpProxy,
|
|
613
|
+
nodeInvoker: (request)=>this.invokeNodeTool(ws, request),
|
|
614
|
+
nodeDefaultTargetClientId: "desktop" === ws.data.clientType ? ws.data.clientId : void 0,
|
|
615
|
+
nodeConnectedIdsProvider: ()=>this.listConnectedNodeIdsForRequester(ws),
|
|
616
|
+
nodeConnectedTargetsProvider: ()=>this.listConnectedNodeTargetsForRequester(ws)
|
|
517
617
|
});
|
|
518
618
|
const abortController = new AbortController();
|
|
519
619
|
this.activeAgentRequests.set(msg.id, {
|
|
@@ -669,14 +769,19 @@ class GatewayServer {
|
|
|
669
769
|
}
|
|
670
770
|
handleRegister(ws, msg) {
|
|
671
771
|
const payload = msg.payload;
|
|
672
|
-
|
|
772
|
+
const hasSessionAuth = true === ws.data.authenticated;
|
|
773
|
+
const hasPayloadAuth = this.auth.validate({
|
|
673
774
|
token: payload.token
|
|
674
|
-
}, ws.data.tailscaleUser)
|
|
775
|
+
}, ws.data.tailscaleUser);
|
|
776
|
+
if (!hasSessionAuth && !hasPayloadAuth) {
|
|
675
777
|
this.sendError(ws, "AUTH_FAILED", "Authentication failed");
|
|
676
778
|
ws.close();
|
|
677
779
|
return;
|
|
678
780
|
}
|
|
679
|
-
const
|
|
781
|
+
const clientId = ws.data.clientId?.trim();
|
|
782
|
+
const requiresNodeApproval = this.nodePairingRequired && includesNodeExecutionCapability(payload.capabilities);
|
|
783
|
+
if (requiresNodeApproval && (!clientId || !this.nodeApprovalStore.isEnabled(clientId))) return void this.sendError(ws, "NODE_NOT_ENABLED", "This client is not approved for node execution");
|
|
784
|
+
const node = this.nodeManager.registerNode(ws, payload.name, payload.capabilities, payload.sessionId, payload.agentName, clientId);
|
|
680
785
|
if (!node) {
|
|
681
786
|
this.sendError(ws, "MAX_NODES_REACHED", "Maximum nodes reached");
|
|
682
787
|
ws.close();
|
|
@@ -693,6 +798,7 @@ class GatewayServer {
|
|
|
693
798
|
},
|
|
694
799
|
timestamp: Date.now()
|
|
695
800
|
});
|
|
801
|
+
if (clientId) this.nodeApprovalStore.markSeen(clientId, payload.name);
|
|
696
802
|
const sessionInfo = node.sessionId ? ` (session: ${node.sessionId})` : "";
|
|
697
803
|
this.log("info", `Node registered: ${node.id} (${node.name})${sessionInfo}`);
|
|
698
804
|
}
|
|
@@ -733,6 +839,7 @@ class GatewayServer {
|
|
|
733
839
|
this.nodeManager.unregisterNode(nodeId);
|
|
734
840
|
this.log("info", `Node unregistered: ${nodeId}`);
|
|
735
841
|
}
|
|
842
|
+
this.cleanupPendingNodeRequestsForSocket(ws);
|
|
736
843
|
}
|
|
737
844
|
handleJoinGroup(ws, msg) {
|
|
738
845
|
const nodeId = ws.data.nodeId;
|
|
@@ -799,6 +906,77 @@ class GatewayServer {
|
|
|
799
906
|
const sent = this.nodeManager.sendToNode(payload.targetNodeId, directMsg);
|
|
800
907
|
if (!sent) this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
|
|
801
908
|
}
|
|
909
|
+
handleNodeRequest(ws, msg) {
|
|
910
|
+
if (!ws.data.authenticated) return void this.sendError(ws, "AUTH_REQUIRED", "Client is not authenticated");
|
|
911
|
+
if (!msg.id) return void this.sendError(ws, "INVALID_REQUEST", "Node request id is required");
|
|
912
|
+
const targetNodeId = "string" == typeof msg.targetNodeId ? msg.targetNodeId.trim() : "";
|
|
913
|
+
if (!targetNodeId) return void this.sendError(ws, "INVALID_REQUEST", "targetNodeId is required");
|
|
914
|
+
const target = this.nodeManager.getNode(targetNodeId);
|
|
915
|
+
if (!target) return void this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
|
|
916
|
+
if (!this.isNodeApprovedForExecution(target.clientId)) return void this.sendError(ws, "NODE_REVOKED", "Target node has been revoked");
|
|
917
|
+
if (this.pendingNodeRequests.has(msg.id)) return void this.sendError(ws, "DUPLICATE_REQUEST_ID", "Node request id is already in use");
|
|
918
|
+
if (this.pendingNodeRequests.size >= MAX_PENDING_NODE_REQUESTS) return void this.sendError(ws, "NODE_REQUEST_OVERLOADED", "Too many pending node requests");
|
|
919
|
+
const timeoutMs = this.resolveNodeRequestTimeout(msg.payload);
|
|
920
|
+
const pendingRequest = {
|
|
921
|
+
id: msg.id,
|
|
922
|
+
requester: ws,
|
|
923
|
+
targetNodeId,
|
|
924
|
+
createdAt: Date.now()
|
|
925
|
+
};
|
|
926
|
+
pendingRequest.timeoutHandle = setTimeout(()=>{
|
|
927
|
+
if (!this.pendingNodeRequests.has(msg.id)) return;
|
|
928
|
+
this.clearPendingNodeRequest(msg.id, new Error(`Node request timed out after ${timeoutMs}ms`));
|
|
929
|
+
this.sendMessageWithRetry(ws, {
|
|
930
|
+
type: "error",
|
|
931
|
+
id: msg.id,
|
|
932
|
+
payload: {
|
|
933
|
+
code: "NODE_TIMEOUT",
|
|
934
|
+
message: `Node request timed out after ${timeoutMs}ms`
|
|
935
|
+
},
|
|
936
|
+
timestamp: Date.now()
|
|
937
|
+
});
|
|
938
|
+
}, timeoutMs);
|
|
939
|
+
this.pendingNodeRequests.set(msg.id, pendingRequest);
|
|
940
|
+
const forwarded = {
|
|
941
|
+
type: "req:node",
|
|
942
|
+
id: msg.id,
|
|
943
|
+
clientId: ws.data.clientId || ws.data.nodeId || "gateway",
|
|
944
|
+
targetNodeId,
|
|
945
|
+
payload: msg.payload,
|
|
946
|
+
timestamp: Date.now()
|
|
947
|
+
};
|
|
948
|
+
const sent = this.nodeManager.sendToNode(targetNodeId, forwarded);
|
|
949
|
+
if (!sent) {
|
|
950
|
+
this.clearPendingNodeRequest(msg.id);
|
|
951
|
+
this.sendError(ws, "NODE_NOT_FOUND", "Target node not found");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
handleNodeResponse(ws, msg) {
|
|
955
|
+
if (!msg.id) return;
|
|
956
|
+
const pending = this.pendingNodeRequests.get(msg.id);
|
|
957
|
+
if (!pending) return;
|
|
958
|
+
const sourceNodeId = ws.data.nodeId;
|
|
959
|
+
if (!sourceNodeId || sourceNodeId !== pending.targetNodeId) return void this.sendError(ws, "NODE_RESPONSE_REJECTED", "Only the target node may respond to this request");
|
|
960
|
+
const forwarded = {
|
|
961
|
+
...msg,
|
|
962
|
+
nodeId: sourceNodeId,
|
|
963
|
+
timestamp: Date.now()
|
|
964
|
+
};
|
|
965
|
+
if (pending.requester) this.sendMessageWithRetry(pending.requester, forwarded);
|
|
966
|
+
if ("res" === msg.type) {
|
|
967
|
+
if (false === msg.ok) pending.reject?.(new Error(this.extractNodeErrorMessage(msg)));
|
|
968
|
+
else pending.resolve?.({
|
|
969
|
+
nodeId: sourceNodeId,
|
|
970
|
+
payload: msg.payload
|
|
971
|
+
});
|
|
972
|
+
this.clearPendingNodeRequest(msg.id);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if ("error" === msg.type) {
|
|
976
|
+
pending.reject?.(new Error(this.extractNodeErrorMessage(msg)));
|
|
977
|
+
this.clearPendingNodeRequest(msg.id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
802
980
|
handlePing(ws, msg) {
|
|
803
981
|
const nodeId = ws.data.nodeId;
|
|
804
982
|
if (nodeId) this.nodeManager.updatePing(nodeId);
|
|
@@ -811,6 +989,127 @@ class GatewayServer {
|
|
|
811
989
|
const nodeId = ws.data.nodeId;
|
|
812
990
|
if (nodeId) this.nodeManager.updatePing(nodeId);
|
|
813
991
|
}
|
|
992
|
+
cleanupPendingNodeRequestsForSocket(ws) {
|
|
993
|
+
for (const [requestId, pending] of this.pendingNodeRequests)if (pending.requester === ws || pending.targetNodeId === ws.data.nodeId) this.clearPendingNodeRequest(requestId, new Error("Node request cancelled due to disconnect"));
|
|
994
|
+
}
|
|
995
|
+
clearPendingNodeRequest(requestId, error) {
|
|
996
|
+
const pending = this.pendingNodeRequests.get(requestId);
|
|
997
|
+
if (!pending) return;
|
|
998
|
+
this.pendingNodeRequests.delete(requestId);
|
|
999
|
+
if (pending.timeoutHandle) clearTimeout(pending.timeoutHandle);
|
|
1000
|
+
if (error) pending.reject?.(error);
|
|
1001
|
+
}
|
|
1002
|
+
extractNodeErrorMessage(msg) {
|
|
1003
|
+
const payload = msg.payload;
|
|
1004
|
+
if (payload && "object" == typeof payload && !Array.isArray(payload) && "string" == typeof payload.message) return String(payload.message);
|
|
1005
|
+
if ("string" == typeof payload && payload.trim()) return payload;
|
|
1006
|
+
return "Node invocation failed";
|
|
1007
|
+
}
|
|
1008
|
+
isNodeApprovedForExecution(clientId) {
|
|
1009
|
+
if (!this.nodePairingRequired) return true;
|
|
1010
|
+
const trimmed = "string" == typeof clientId ? clientId.trim() : "";
|
|
1011
|
+
if (!trimmed) return false;
|
|
1012
|
+
return this.nodeApprovalStore.isEnabled(trimmed);
|
|
1013
|
+
}
|
|
1014
|
+
listConnectedNodeIdsForRequester(requester) {
|
|
1015
|
+
return this.listConnectedNodeTargetsForRequester(requester).map((node)=>node.nodeId);
|
|
1016
|
+
}
|
|
1017
|
+
listConnectedNodeTargetsForRequester(requester) {
|
|
1018
|
+
const requesterClientId = "desktop" === requester.data.clientType ? requester.data.clientId?.trim() || "" : "";
|
|
1019
|
+
const nodes = this.nodeManager.getAllNodes().filter((node)=>{
|
|
1020
|
+
if (!this.isNodeApprovedForExecution(node.clientId)) return false;
|
|
1021
|
+
if (!includesNodeExecutionCapability(node.capabilities)) return false;
|
|
1022
|
+
return true;
|
|
1023
|
+
}).sort((a, b)=>b.connectedAt - a.connectedAt);
|
|
1024
|
+
const preferred = requesterClientId ? nodes.filter((node)=>node.clientId === requesterClientId) : [];
|
|
1025
|
+
const others = requesterClientId ? nodes.filter((node)=>node.clientId !== requesterClientId) : nodes;
|
|
1026
|
+
return [
|
|
1027
|
+
...preferred,
|
|
1028
|
+
...others
|
|
1029
|
+
].map((node)=>({
|
|
1030
|
+
nodeId: node.id,
|
|
1031
|
+
clientId: node.clientId,
|
|
1032
|
+
name: node.name,
|
|
1033
|
+
capabilities: Array.isArray(node.capabilities) ? node.capabilities.filter((capability)=>"string" == typeof capability) : void 0
|
|
1034
|
+
}));
|
|
1035
|
+
}
|
|
1036
|
+
resolveNodeRequestTimeout(payload) {
|
|
1037
|
+
let requestedTimeout;
|
|
1038
|
+
if (payload && "object" == typeof payload && !Array.isArray(payload)) {
|
|
1039
|
+
const timeoutValue = payload.timeoutMs;
|
|
1040
|
+
if ("number" == typeof timeoutValue && Number.isFinite(timeoutValue)) requestedTimeout = Math.trunc(timeoutValue);
|
|
1041
|
+
}
|
|
1042
|
+
const timeout = requestedTimeout ?? DEFAULT_NODE_REQUEST_TIMEOUT_MS;
|
|
1043
|
+
return Math.min(Math.max(timeout, MIN_NODE_REQUEST_TIMEOUT_MS), MAX_NODE_REQUEST_TIMEOUT_MS);
|
|
1044
|
+
}
|
|
1045
|
+
resolveNodeTarget(request, defaultTargetClientId) {
|
|
1046
|
+
const requiredCapability = "string" == typeof request.capability ? request.capability.trim() : "";
|
|
1047
|
+
const targetNodeId = "string" == typeof request.targetNodeId ? request.targetNodeId.trim() : "";
|
|
1048
|
+
const targetClientId = ("string" == typeof request.targetClientId ? request.targetClientId.trim() : "") || defaultTargetClientId?.trim() || "";
|
|
1049
|
+
const canUseNode = (node)=>{
|
|
1050
|
+
if (!this.isNodeApprovedForExecution(node.clientId)) return false;
|
|
1051
|
+
if (!requiredCapability) return true;
|
|
1052
|
+
return Array.isArray(node.capabilities) && node.capabilities.includes(requiredCapability);
|
|
1053
|
+
};
|
|
1054
|
+
if (targetNodeId) {
|
|
1055
|
+
const node = this.nodeManager.getNode(targetNodeId);
|
|
1056
|
+
if (!node) throw new Error("Target node not found");
|
|
1057
|
+
if (!canUseNode(node)) throw new Error(requiredCapability ? `Target node does not support capability ${requiredCapability}` : "Target node is not available");
|
|
1058
|
+
return {
|
|
1059
|
+
nodeId: node.id
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
let candidates = this.nodeManager.getAllNodes().filter((node)=>canUseNode(node));
|
|
1063
|
+
if (targetClientId) {
|
|
1064
|
+
candidates = candidates.filter((node)=>node.clientId === targetClientId);
|
|
1065
|
+
if (0 === candidates.length) throw new Error(`No available node found for client ${targetClientId}`);
|
|
1066
|
+
}
|
|
1067
|
+
if (0 === candidates.length) throw new Error(requiredCapability ? `No available node supports capability ${requiredCapability}` : "No available node found");
|
|
1068
|
+
candidates.sort((a, b)=>b.connectedAt - a.connectedAt);
|
|
1069
|
+
const selected = candidates[0];
|
|
1070
|
+
return {
|
|
1071
|
+
nodeId: selected.id
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
async invokeNodeTool(requester, request) {
|
|
1075
|
+
if (!requester.data.authenticated) throw new Error("Client is not authenticated");
|
|
1076
|
+
const toolName = request.tool;
|
|
1077
|
+
if ("system.notify" !== toolName && "system.run" !== toolName) throw new Error(`Unsupported node tool: ${toolName}`);
|
|
1078
|
+
const timeoutMsRaw = "number" == typeof request.timeoutMs ? Math.trunc(request.timeoutMs) : DEFAULT_NODE_REQUEST_TIMEOUT_MS;
|
|
1079
|
+
const timeoutMs = Math.min(Math.max(timeoutMsRaw, MIN_NODE_REQUEST_TIMEOUT_MS), MAX_NODE_REQUEST_TIMEOUT_MS);
|
|
1080
|
+
if (this.pendingNodeRequests.size >= MAX_PENDING_NODE_REQUESTS) throw new Error("Too many pending node requests");
|
|
1081
|
+
const target = this.resolveNodeTarget(request, "desktop" === requester.data.clientType ? requester.data.clientId : void 0);
|
|
1082
|
+
let requestId = "";
|
|
1083
|
+
do requestId = `node-tool-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1084
|
+
while (this.pendingNodeRequests.has(requestId));
|
|
1085
|
+
return await new Promise((resolve, reject)=>{
|
|
1086
|
+
const pending = {
|
|
1087
|
+
id: requestId,
|
|
1088
|
+
targetNodeId: target.nodeId,
|
|
1089
|
+
createdAt: Date.now(),
|
|
1090
|
+
resolve,
|
|
1091
|
+
reject
|
|
1092
|
+
};
|
|
1093
|
+
pending.timeoutHandle = setTimeout(()=>{
|
|
1094
|
+
this.clearPendingNodeRequest(requestId, new Error(`Node invocation timed out after ${timeoutMs}ms`));
|
|
1095
|
+
}, timeoutMs);
|
|
1096
|
+
this.pendingNodeRequests.set(requestId, pending);
|
|
1097
|
+
const forwarded = {
|
|
1098
|
+
type: "req:node",
|
|
1099
|
+
id: requestId,
|
|
1100
|
+
clientId: requester.data.clientId || requester.data.nodeId || "gateway",
|
|
1101
|
+
targetNodeId: target.nodeId,
|
|
1102
|
+
payload: {
|
|
1103
|
+
tool: toolName,
|
|
1104
|
+
args: request.args || {},
|
|
1105
|
+
timeoutMs
|
|
1106
|
+
},
|
|
1107
|
+
timestamp: Date.now()
|
|
1108
|
+
};
|
|
1109
|
+
const sent = this.nodeManager.sendToNode(target.nodeId, forwarded);
|
|
1110
|
+
if (!sent) this.clearPendingNodeRequest(requestId, new Error("Target node not found"));
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
814
1113
|
sendMessage(ws, message) {
|
|
815
1114
|
try {
|
|
816
1115
|
const result = ws.send(JSON.stringify(message));
|
|
@@ -1175,15 +1474,87 @@ class GatewayServer {
|
|
|
1175
1474
|
break;
|
|
1176
1475
|
}
|
|
1177
1476
|
}
|
|
1477
|
+
isRequestOriginAllowed(req, url) {
|
|
1478
|
+
const origin = req.headers.get("origin");
|
|
1479
|
+
if (!origin) return true;
|
|
1480
|
+
return isGatewayOriginAllowed({
|
|
1481
|
+
origin,
|
|
1482
|
+
requestUrl: url,
|
|
1483
|
+
gatewayHost: this.config.host,
|
|
1484
|
+
gatewayPort: this.config.port,
|
|
1485
|
+
controlUiEnabled: this.controlUiEnabled,
|
|
1486
|
+
controlUiPort: this.controlUiPort
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
withApiCors(req, url, response) {
|
|
1490
|
+
const headers = new Headers(response.headers);
|
|
1491
|
+
for (const [key, value] of Object.entries(API_CORS_HEADERS))headers.set(key, value);
|
|
1492
|
+
const existingVary = headers.get("Vary");
|
|
1493
|
+
if (existingVary) {
|
|
1494
|
+
if (!existingVary.toLowerCase().includes("origin")) headers.set("Vary", `${existingVary}, Origin`);
|
|
1495
|
+
} else headers.set("Vary", "Origin");
|
|
1496
|
+
const origin = req.headers.get("origin");
|
|
1497
|
+
if (origin && this.isRequestOriginAllowed(req, url)) headers.set("Access-Control-Allow-Origin", origin);
|
|
1498
|
+
return new Response(response.body, {
|
|
1499
|
+
status: response.status,
|
|
1500
|
+
statusText: response.statusText,
|
|
1501
|
+
headers
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
resolveTrustedTailscaleUser(req) {
|
|
1505
|
+
if (!this.config.auth?.allowTailscale) return;
|
|
1506
|
+
if (!isLoopbackHostname(this.config.host)) return;
|
|
1507
|
+
const raw = req.headers.get("tailscale-user-login") || req.headers.get("ts-user-login") || "";
|
|
1508
|
+
const normalized = raw.trim();
|
|
1509
|
+
if (!normalized) return;
|
|
1510
|
+
if (normalized.length > 256) return;
|
|
1511
|
+
if (!/^[a-zA-Z0-9._:@+-]+$/.test(normalized)) return;
|
|
1512
|
+
return normalized;
|
|
1513
|
+
}
|
|
1514
|
+
resolveHttpAuthPayload(req) {
|
|
1515
|
+
const authorization = req.headers.get("authorization") || "";
|
|
1516
|
+
let bearerToken;
|
|
1517
|
+
if (authorization.toLowerCase().startsWith("bearer ")) {
|
|
1518
|
+
const value = authorization.slice(7).trim();
|
|
1519
|
+
bearerToken = value || void 0;
|
|
1520
|
+
}
|
|
1521
|
+
const headerToken = req.headers.get("x-wingman-token")?.trim();
|
|
1522
|
+
const password = req.headers.get("x-wingman-password")?.trim();
|
|
1523
|
+
return {
|
|
1524
|
+
token: headerToken || bearerToken,
|
|
1525
|
+
password: password || void 0
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
requireHttpAuth(req) {
|
|
1529
|
+
if (!this.auth.isAuthRequired()) return null;
|
|
1530
|
+
const tailscaleUser = this.resolveTrustedTailscaleUser(req);
|
|
1531
|
+
const authPayload = this.resolveHttpAuthPayload(req);
|
|
1532
|
+
const allowed = this.auth.validate(authPayload, tailscaleUser);
|
|
1533
|
+
if (allowed) return null;
|
|
1534
|
+
return new Response("Unauthorized", {
|
|
1535
|
+
status: 401,
|
|
1536
|
+
headers: {
|
|
1537
|
+
"WWW-Authenticate": 'Bearer realm="wingman-gateway"'
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1178
1541
|
async handleUiRequest(req) {
|
|
1179
1542
|
const url = new URL(req.url);
|
|
1180
1543
|
const ctx = this.getHttpContext();
|
|
1181
1544
|
const webhookResponse = await handleWebhookInvoke(ctx, this.webhookStore, req, url);
|
|
1182
1545
|
if (webhookResponse) return webhookResponse;
|
|
1183
1546
|
if (url.pathname.startsWith("/api/")) {
|
|
1184
|
-
if (
|
|
1547
|
+
if (!this.isRequestOriginAllowed(req, url)) return this.withApiCors(req, url, new Response("Forbidden origin", {
|
|
1548
|
+
status: 403
|
|
1549
|
+
}));
|
|
1550
|
+
if ("OPTIONS" === req.method) return this.withApiCors(req, url, new Response(null, {
|
|
1185
1551
|
status: 204
|
|
1186
1552
|
}));
|
|
1553
|
+
const publicApiRoute = "/api/config" === url.pathname || "/api/health" === url.pathname;
|
|
1554
|
+
if (!publicApiRoute) {
|
|
1555
|
+
const authFailure = this.requireHttpAuth(req);
|
|
1556
|
+
if (authFailure) return this.withApiCors(req, url, authFailure);
|
|
1557
|
+
}
|
|
1187
1558
|
if ("/api/config" === url.pathname) {
|
|
1188
1559
|
const agents = this.wingmanConfig.agents?.list?.map((agent)=>({
|
|
1189
1560
|
id: agent.id,
|
|
@@ -1191,7 +1562,7 @@ class GatewayServer {
|
|
|
1191
1562
|
default: agent.default
|
|
1192
1563
|
})) || [];
|
|
1193
1564
|
const defaultAgentId = this.router.selectAgent();
|
|
1194
|
-
return withApiCors(new Response(JSON.stringify({
|
|
1565
|
+
return this.withApiCors(req, url, new Response(JSON.stringify({
|
|
1195
1566
|
gatewayHost: this.config.host,
|
|
1196
1567
|
gatewayPort: this.config.port,
|
|
1197
1568
|
requireAuth: this.auth.isAuthRequired(),
|
|
@@ -1206,11 +1577,11 @@ class GatewayServer {
|
|
|
1206
1577
|
}
|
|
1207
1578
|
}));
|
|
1208
1579
|
}
|
|
1209
|
-
const apiResponse = await handleWebhooksApi(ctx, this.webhookStore, req, url) || await handleRoutinesApi(ctx, this.routineStore, req, url) || await handleAgentsApi(ctx, req, url) || await handleProvidersApi(ctx, req, url) || await handleVoiceApi(ctx, req, url) || await handleFsApi(ctx, req, url) || await handleSessionsApi(ctx, req, url);
|
|
1210
|
-
if (apiResponse) return withApiCors(apiResponse);
|
|
1211
|
-
if ("/api/health" === url.pathname) return withApiCors(this.handleHealthCheck());
|
|
1212
|
-
if ("/api/stats" === url.pathname) return withApiCors(this.handleStats());
|
|
1213
|
-
return withApiCors(new Response("Not Found", {
|
|
1580
|
+
const apiResponse = await handleWebhooksApi(ctx, this.webhookStore, req, url) || await handleRoutinesApi(ctx, this.routineStore, req, url) || await handleNodesApi(ctx, this.nodeManager, this.nodeApprovalStore, req, url) || await handleAgentsApi(ctx, req, url) || await handleProvidersApi(ctx, req, url) || await handleVoiceApi(ctx, req, url) || await handleFsApi(ctx, req, url) || await handleSessionsApi(ctx, req, url);
|
|
1581
|
+
if (apiResponse) return this.withApiCors(req, url, apiResponse);
|
|
1582
|
+
if ("/api/health" === url.pathname) return this.withApiCors(req, url, this.handleHealthCheck());
|
|
1583
|
+
if ("/api/stats" === url.pathname) return this.withApiCors(req, url, this.handleStats());
|
|
1584
|
+
return this.withApiCors(req, url, new Response("Not Found", {
|
|
1214
1585
|
status: 404
|
|
1215
1586
|
}));
|
|
1216
1587
|
}
|
|
@@ -1276,6 +1647,15 @@ class GatewayServer {
|
|
|
1276
1647
|
const validatedMessage = validation.data;
|
|
1277
1648
|
if ("register" === validatedMessage.type) {
|
|
1278
1649
|
const payload = validatedMessage.payload;
|
|
1650
|
+
const maxBridgeNodes = this.config.maxNodes || 1000;
|
|
1651
|
+
if (this.bridgeQueues.size >= maxBridgeNodes) return new Response(JSON.stringify({
|
|
1652
|
+
error: "Bridge capacity reached"
|
|
1653
|
+
}), {
|
|
1654
|
+
status: 429,
|
|
1655
|
+
headers: {
|
|
1656
|
+
"Content-Type": "application/json"
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1279
1659
|
const nodeId = this.generateNodeId();
|
|
1280
1660
|
this.bridgeQueues.set(nodeId, []);
|
|
1281
1661
|
const response = {
|
|
@@ -1407,6 +1787,7 @@ class GatewayServer {
|
|
|
1407
1787
|
_define_property(this, "browserRelayServer", null);
|
|
1408
1788
|
_define_property(this, "webhookStore", void 0);
|
|
1409
1789
|
_define_property(this, "routineStore", void 0);
|
|
1790
|
+
_define_property(this, "nodeApprovalStore", void 0);
|
|
1410
1791
|
_define_property(this, "internalHooks", null);
|
|
1411
1792
|
_define_property(this, "discordAdapter", null);
|
|
1412
1793
|
_define_property(this, "sessionSubscriptions", new Map());
|
|
@@ -1416,7 +1797,9 @@ class GatewayServer {
|
|
|
1416
1797
|
_define_property(this, "activeSessionRequests", new Map());
|
|
1417
1798
|
_define_property(this, "queuedSessionRequests", new Map());
|
|
1418
1799
|
_define_property(this, "requestSessionKeys", new Map());
|
|
1800
|
+
_define_property(this, "pendingNodeRequests", new Map());
|
|
1419
1801
|
_define_property(this, "terminalSessionManager", void 0);
|
|
1802
|
+
_define_property(this, "nodePairingRequired", void 0);
|
|
1420
1803
|
_define_property(this, "bridgeQueues", new Map());
|
|
1421
1804
|
_define_property(this, "bridgePollWaiters", new Map());
|
|
1422
1805
|
this.workspace = config.workspace || process.cwd();
|
|
@@ -1426,6 +1809,7 @@ class GatewayServer {
|
|
|
1426
1809
|
this.router = new GatewayRouter(this.wingmanConfig);
|
|
1427
1810
|
this.webhookStore = createWebhookStore(()=>this.resolveConfigDirPath());
|
|
1428
1811
|
this.routineStore = createRoutineStore(()=>this.resolveConfigDirPath());
|
|
1812
|
+
this.nodeApprovalStore = createNodeApprovalStore(()=>this.resolveConfigDirPath());
|
|
1429
1813
|
const gatewayDefaults = this.wingmanConfig.gateway;
|
|
1430
1814
|
const envToken = getGatewayTokenFromEnv();
|
|
1431
1815
|
const authFromConfig = config.auth?.mode === "token" ? {
|
|
@@ -1465,6 +1849,7 @@ class GatewayServer {
|
|
|
1465
1849
|
const controlUi = this.wingmanConfig.gateway?.controlUi;
|
|
1466
1850
|
this.controlUiEnabled = controlUi?.enabled ?? false;
|
|
1467
1851
|
this.controlUiPort = controlUi?.port || 18790;
|
|
1852
|
+
this.nodePairingRequired = controlUi?.pairingRequired ?? true;
|
|
1468
1853
|
this.controlUiSamePort = this.controlUiEnabled && this.controlUiPort === this.config.port;
|
|
1469
1854
|
this.uiDistDir = this.controlUiEnabled ? this.resolveControlUiDir() : null;
|
|
1470
1855
|
const relayConfig = this.wingmanConfig.browser?.relay;
|
|
@@ -1521,4 +1906,4 @@ function isFileAttachment(attachment) {
|
|
|
1521
1906
|
if ("file" === attachment.kind) return true;
|
|
1522
1907
|
return "string" == typeof attachment.textContent;
|
|
1523
1908
|
}
|
|
1524
|
-
export { GatewayServer, resolveExecutionConfigDirOverride, resolveExecutionWorkspaceOverride };
|
|
1909
|
+
export { GatewayServer, isGatewayOriginAllowed, isLoopbackHostname, resolveExecutionConfigDirOverride, resolveExecutionWorkspaceOverride };
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from "bun";
|
|
|
2
2
|
/**
|
|
3
3
|
* Message types for gateway communication
|
|
4
4
|
*/
|
|
5
|
-
export type MessageType = "connect" | "res" | "req:agent" | "req:agent:cancel" | "event:agent" | "session_subscribe" | "session_unsubscribe" | "register" | "registered" | "unregister" | "join_group" | "leave_group" | "broadcast" | "direct" | "ping" | "pong" | "error" | "ack" | "upgrade";
|
|
5
|
+
export type MessageType = "connect" | "res" | "req:agent" | "req:agent:cancel" | "event:agent" | "req:node" | "event:node" | "session_subscribe" | "session_unsubscribe" | "register" | "registered" | "unregister" | "join_group" | "leave_group" | "broadcast" | "direct" | "ping" | "pong" | "error" | "ack" | "upgrade";
|
|
6
6
|
/**
|
|
7
7
|
* Gateway message structure
|
|
8
8
|
*/
|
|
@@ -87,6 +87,7 @@ export interface RoutingInfo {
|
|
|
87
87
|
export interface NodeMetadata {
|
|
88
88
|
id: string;
|
|
89
89
|
name: string;
|
|
90
|
+
clientId?: string;
|
|
90
91
|
capabilities?: string[];
|
|
91
92
|
groups: Set<string>;
|
|
92
93
|
connectedAt: number;
|
|
@@ -102,6 +103,10 @@ export interface NodeMetadata {
|
|
|
102
103
|
export interface Node extends NodeMetadata {
|
|
103
104
|
ws: ServerWebSocket<{
|
|
104
105
|
nodeId: string;
|
|
106
|
+
clientId?: string;
|
|
107
|
+
clientType?: string;
|
|
108
|
+
authenticated?: boolean;
|
|
109
|
+
tailscaleUser?: string;
|
|
105
110
|
}>;
|
|
106
111
|
}
|
|
107
112
|
/**
|
|
@@ -143,6 +148,15 @@ export interface GatewayConfig {
|
|
|
143
148
|
method: "mdns" | "tailscale";
|
|
144
149
|
name: string;
|
|
145
150
|
};
|
|
151
|
+
mcpProxy?: {
|
|
152
|
+
enabled: boolean;
|
|
153
|
+
command?: string;
|
|
154
|
+
baseArgs?: string[];
|
|
155
|
+
projectName?: string;
|
|
156
|
+
pushExplorer?: boolean;
|
|
157
|
+
apiKey?: string;
|
|
158
|
+
apiUrl?: string;
|
|
159
|
+
};
|
|
146
160
|
}
|
|
147
161
|
export interface GatewayAuthConfig {
|
|
148
162
|
mode: "token" | "password" | "none";
|