@tyvm/knowhow 0.0.108-dev.bd8f104-dev.bd8f104-dev.bd8f104 → 0.0.108-dev.c47492f
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/package.json +2 -2
- package/scripts/publish.sh +20 -4
- package/src/agents/base/base.ts +9 -0
- package/src/chat/CliChatService.ts +7 -1
- package/src/chat/renderer/CompactRenderer.ts +20 -0
- package/src/chat/renderer/ConsoleRenderer.ts +19 -0
- package/src/chat/renderer/FancyRenderer.ts +19 -0
- package/src/chat/renderer/types.ts +11 -0
- package/src/cli.ts +45 -21
- package/src/clients/types.ts +12 -4
- package/src/processors/JsonCompressor.ts +3 -3
- package/src/services/modules/index.ts +21 -17
- package/src/tunnel.ts +216 -0
- package/src/worker.ts +65 -336
- package/src/workers/auth/WsMiddleware.ts +99 -0
- package/src/workers/auth/authMiddleware.ts +104 -0
- package/src/workers/auth/types.ts +14 -2
- package/ts_build/package.json +2 -2
- package/ts_build/src/agents/base/base.js +10 -0
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +10 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/types.d.ts +2 -0
- package/ts_build/src/cli.js +17 -9
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +2 -2
- package/ts_build/src/processors/JsonCompressor.js +3 -3
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +30 -0
- package/ts_build/src/services/modules/index.js +9 -14
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/tunnel.d.ts +27 -0
- package/ts_build/src/tunnel.js +112 -0
- package/ts_build/src/tunnel.js.map +1 -0
- package/ts_build/src/worker.d.ts +1 -4
- package/ts_build/src/worker.js +38 -244
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
- package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
- package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
- package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
- package/ts_build/src/workers/auth/authMiddleware.js +60 -0
- package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
- package/ts_build/src/workers/auth/types.d.ts +8 -1
package/src/tunnel.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import { createTunnelHandler, TunnelHandler } from "@tyvm/knowhow-tunnel";
|
|
4
|
+
import { loadJwt } from "./login";
|
|
5
|
+
import { wait } from "./utils";
|
|
6
|
+
import { getConfig } from "./config";
|
|
7
|
+
import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
8
|
+
import { ModulesService } from "./services/modules";
|
|
9
|
+
import { WorkerPasskeyAuthService } from "./workers/auth/WorkerPasskeyAuth";
|
|
10
|
+
import { WsMiddlewareStack } from "./workers/auth/WsMiddleware";
|
|
11
|
+
import { makeAuthMiddleware } from "./workers/auth/authMiddleware";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract the tunnel domain and protocol from the API URL.
|
|
15
|
+
* e.g., "https://api.knowhow.tyvm.ai" -> { domain: "worker.knowhow.tyvm.ai", useHttps: true }
|
|
16
|
+
* e.g., "http://localhost:4000" -> { domain: "worker.localhost:4000", useHttps: false }
|
|
17
|
+
*/
|
|
18
|
+
export function extractTunnelDomain(apiUrl: string): {
|
|
19
|
+
domain: string;
|
|
20
|
+
useHttps: boolean;
|
|
21
|
+
} {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(apiUrl);
|
|
24
|
+
const useHttps = url.protocol === "https:";
|
|
25
|
+
|
|
26
|
+
// For localhost, include port; for production, just use hostname
|
|
27
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
28
|
+
return {
|
|
29
|
+
domain: `worker.${url.hostname}:${url.port || "80"}`,
|
|
30
|
+
useHttps,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { domain: `worker.${url.hostname}`, useHttps };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error("Failed to parse API_URL for tunnel domain:", err);
|
|
36
|
+
return { domain: "worker.localhost:4000", useHttps: false }; // fallback
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize a tunnel handler and load tunnel modules.
|
|
42
|
+
*/
|
|
43
|
+
export async function initTunnelHandler(
|
|
44
|
+
tunnelConnection: WebSocket,
|
|
45
|
+
tunnelConfig: Parameters<typeof createTunnelHandler>[1]
|
|
46
|
+
): Promise<TunnelHandler> {
|
|
47
|
+
const handler = createTunnelHandler(tunnelConnection, tunnelConfig);
|
|
48
|
+
console.log("🌐 Tunnel handler initialized");
|
|
49
|
+
console.log(tunnelConfig);
|
|
50
|
+
|
|
51
|
+
const tunnelModuleService = new ModulesService();
|
|
52
|
+
const tunnelContext = await tunnelModuleService.overrideDefaultContext({
|
|
53
|
+
Tunnel: handler,
|
|
54
|
+
});
|
|
55
|
+
tunnelModuleService.loadModulesFromConfig(tunnelContext).catch((err) => {
|
|
56
|
+
console.error("Failed to load tunnel modules:", err);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return handler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve tunnel local host, log port mapping, and return shared tunnel setup values.
|
|
64
|
+
* Extracted to avoid duplication between worker() and tunnel().
|
|
65
|
+
*/
|
|
66
|
+
export function resolveTunnelConfig(
|
|
67
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
68
|
+
isInsideDocker: boolean
|
|
69
|
+
): { tunnelLocalHost: string; portMapping: Record<string, number> } {
|
|
70
|
+
// Determine localHost based on environment
|
|
71
|
+
let tunnelLocalHost = config.worker?.tunnel?.localHost;
|
|
72
|
+
if (!tunnelLocalHost) {
|
|
73
|
+
if (isInsideDocker) {
|
|
74
|
+
tunnelLocalHost = "host.docker.internal";
|
|
75
|
+
console.log(
|
|
76
|
+
"🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
tunnelLocalHost = "127.0.0.1";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for port mapping configuration
|
|
84
|
+
const portMapping = (config.worker?.tunnel?.portMapping || {}) as Record<string, number>;
|
|
85
|
+
if (Object.keys(portMapping).length > 0) {
|
|
86
|
+
console.log("🔀 Port mapping configured:");
|
|
87
|
+
for (const [containerPort, hostPort] of Object.entries(portMapping)) {
|
|
88
|
+
console.log(` Container port ${containerPort} → Host port ${hostPort}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { tunnelLocalHost, portMapping };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Options for connectTunnelWebSocket helper.
|
|
97
|
+
*/
|
|
98
|
+
export interface TunnelWebSocketOptions {
|
|
99
|
+
/** Already-resolved tunnel domain (hostname only, no protocol) */
|
|
100
|
+
tunnelDomain: string;
|
|
101
|
+
/** Whether the tunnel should use HTTPS */
|
|
102
|
+
tunnelUseHttps: boolean;
|
|
103
|
+
/** Local host to forward tunnel traffic to */
|
|
104
|
+
tunnelLocalHost: string;
|
|
105
|
+
/** Port mapping configuration */
|
|
106
|
+
portMapping: Record<string, number>;
|
|
107
|
+
/** Worker config (for tunnel sub-config) */
|
|
108
|
+
config: Awaited<ReturnType<typeof getConfig>>;
|
|
109
|
+
/** HTTP headers to attach to the WebSocket upgrade request */
|
|
110
|
+
headers: Record<string, string>;
|
|
111
|
+
/** Callback invoked with the TunnelHandler once the connection opens */
|
|
112
|
+
onOpen?: (handler: TunnelHandler) => void;
|
|
113
|
+
/** Called when the connection closes; receives code + reason string */
|
|
114
|
+
onClose?: (code: number, reason: string) => void;
|
|
115
|
+
/** Called on error */
|
|
116
|
+
onError?: (error: Error) => void;
|
|
117
|
+
/** Optional passkey auth service — if provided, applies WS middleware to gate tunnel traffic */
|
|
118
|
+
authService?: WorkerPasskeyAuthService | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a tunnel WebSocket connection, build the tunnelConfig, and
|
|
123
|
+
* initialize the tunnel handler. Returns the WebSocket.
|
|
124
|
+
*
|
|
125
|
+
* The caller is responsible for storing a reference to the returned TunnelHandler
|
|
126
|
+
* (via onOpen) and performing any outer-state cleanup (via onClose / onError).
|
|
127
|
+
*/
|
|
128
|
+
export function connectTunnelWebSocket(
|
|
129
|
+
options: TunnelWebSocketOptions
|
|
130
|
+
): WebSocket {
|
|
131
|
+
const {
|
|
132
|
+
tunnelDomain,
|
|
133
|
+
tunnelUseHttps,
|
|
134
|
+
tunnelLocalHost,
|
|
135
|
+
portMapping,
|
|
136
|
+
config,
|
|
137
|
+
headers,
|
|
138
|
+
onOpen,
|
|
139
|
+
onClose,
|
|
140
|
+
onError,
|
|
141
|
+
authService,
|
|
142
|
+
} = options;
|
|
143
|
+
|
|
144
|
+
const tunnelConnection = new WebSocket(`${KNOWHOW_API_URL}/ws/tunnel`, { headers });
|
|
145
|
+
|
|
146
|
+
tunnelConnection.on("open", async () => {
|
|
147
|
+
console.log("Tunnel WebSocket connected");
|
|
148
|
+
|
|
149
|
+
// Apply passkey auth middleware FIRST, before tunnel handler registers its
|
|
150
|
+
// "message" listener. Node.js EventEmitter fires listeners in registration
|
|
151
|
+
// order, so our middleware runs first. wrapSocket() also redirects future
|
|
152
|
+
// ws.on("message", ...) calls to an inner emitter, ensuring the tunnel
|
|
153
|
+
// handler only receives messages that passed the middleware.
|
|
154
|
+
if (authService) {
|
|
155
|
+
const stack = new WsMiddlewareStack();
|
|
156
|
+
stack.use(makeAuthMiddleware(authService));
|
|
157
|
+
stack.wrapSocket(tunnelConnection);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const allowedPorts = config.worker?.tunnel?.allowedPorts || [];
|
|
161
|
+
|
|
162
|
+
// Create URL rewriter callback that returns the hostname (without protocol).
|
|
163
|
+
// The tunnel package will add the protocol based on the useHttps config.
|
|
164
|
+
const urlRewriter = (port: number, metadata?: any) => {
|
|
165
|
+
const workerId = metadata?.workerId;
|
|
166
|
+
const secret = metadata?.secret;
|
|
167
|
+
// Examples: secret-p3000.worker.example.com / workerId-p3000.worker.example.com
|
|
168
|
+
const subdomain = secret
|
|
169
|
+
? `${secret}-p${port}`
|
|
170
|
+
: `${workerId}-p${port}`;
|
|
171
|
+
return `${subdomain}.${tunnelDomain}`;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const tunnelConfig = {
|
|
175
|
+
allowedPorts,
|
|
176
|
+
maxConcurrentStreams: config.worker?.tunnel?.maxConcurrentStreams || 50,
|
|
177
|
+
tunnelUseHttps,
|
|
178
|
+
localHost: tunnelLocalHost,
|
|
179
|
+
urlRewriter,
|
|
180
|
+
enableUrlRewriting: config.worker?.tunnel?.enableUrlRewriting !== false,
|
|
181
|
+
portMapping,
|
|
182
|
+
logLevel: "debug" as const,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handler = await initTunnelHandler(tunnelConnection, tunnelConfig);
|
|
186
|
+
onOpen?.(handler);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
tunnelConnection.on("close", (code, reason) => {
|
|
190
|
+
console.log(
|
|
191
|
+
`Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`
|
|
192
|
+
);
|
|
193
|
+
onClose?.(code, reason.toString());
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
tunnelConnection.on("error", (error) => {
|
|
197
|
+
console.error("Tunnel WebSocket error:", error);
|
|
198
|
+
onError?.(error);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return tunnelConnection;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The minimal set of tool names that are always registered when running in
|
|
206
|
+
* tunnel mode. These are the tools the backend and frontend need to interact
|
|
207
|
+
* with the tunnel worker (port discovery, passkey auth).
|
|
208
|
+
*
|
|
209
|
+
* Additional tools can be added here in the future without changing the CLI.
|
|
210
|
+
*/
|
|
211
|
+
export const TUNNEL_MINIMAL_TOOLS = [
|
|
212
|
+
"listAllowedPorts",
|
|
213
|
+
"unlock",
|
|
214
|
+
"lock",
|
|
215
|
+
"reloadConfig",
|
|
216
|
+
];
|
package/src/worker.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
|
-
import {
|
|
3
|
+
import { TunnelHandler } from "@tyvm/knowhow-tunnel";
|
|
4
4
|
import { includedTools } from "./agents/tools/list";
|
|
5
5
|
import { loadJwt } from "./login";
|
|
6
6
|
import { services } from "./services";
|
|
7
7
|
import { PasskeySetupService } from "./workers/auth/PasskeySetup";
|
|
8
8
|
import { WorkerPasskeyAuthService } from "./workers/auth/WorkerPasskeyAuth";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
makeUnlockTool,
|
|
11
|
+
makeLockTool,
|
|
12
|
+
makeReloadConfigTool,
|
|
13
|
+
} from "./workers/tools";
|
|
10
14
|
import { McpServerService } from "./services/Mcp";
|
|
11
15
|
import * as allTools from "./agents/tools";
|
|
12
16
|
import workerTools from "./workers/tools";
|
|
@@ -14,7 +18,11 @@ import { wait } from "./utils";
|
|
|
14
18
|
import { getConfig, updateConfig } from "./config";
|
|
15
19
|
import { KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
16
20
|
import { registerWorkerPath } from "./workerRegistry";
|
|
17
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
extractTunnelDomain,
|
|
23
|
+
resolveTunnelConfig,
|
|
24
|
+
connectTunnelWebSocket,
|
|
25
|
+
} from "./tunnel";
|
|
18
26
|
|
|
19
27
|
const API_URL = KNOWHOW_API_URL;
|
|
20
28
|
|
|
@@ -91,6 +99,7 @@ export async function worker(options?: {
|
|
|
91
99
|
noSandbox?: boolean;
|
|
92
100
|
passkey?: boolean;
|
|
93
101
|
passkeyReset?: boolean;
|
|
102
|
+
allowedTools?: string[];
|
|
94
103
|
}) {
|
|
95
104
|
const config = await getConfig();
|
|
96
105
|
|
|
@@ -210,9 +219,9 @@ export async function worker(options?: {
|
|
|
210
219
|
console.log(`🖥️ Using host mode (${sandboxSource})`);
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
if (!config.worker || !config.worker.allowedTools) {
|
|
222
|
+
// If a tool list override was passed (e.g. from tunnel mode), skip the
|
|
223
|
+
// first-run config write and use it directly.
|
|
224
|
+
if (!options?.allowedTools && (!config.worker || !config.worker.allowedTools)) {
|
|
216
225
|
console.log(
|
|
217
226
|
"Worker tools configured! Update knowhow.json to adjust which tools are allowed by the worker."
|
|
218
227
|
);
|
|
@@ -230,7 +239,8 @@ export async function worker(options?: {
|
|
|
230
239
|
return;
|
|
231
240
|
}
|
|
232
241
|
|
|
233
|
-
|
|
242
|
+
const resolvedToolNames = options?.allowedTools ?? config.worker!.allowedTools;
|
|
243
|
+
let toolsToUse = Tools.getToolsByNames(resolvedToolNames);
|
|
234
244
|
|
|
235
245
|
// If passkey auth is enabled, wrap all tool functions to check locked state
|
|
236
246
|
// and register the unlock/lock auth tools
|
|
@@ -289,31 +299,14 @@ export async function worker(options?: {
|
|
|
289
299
|
let lastJwt: string | null = null;
|
|
290
300
|
let unauthorizedJwt: string | null = null;
|
|
291
301
|
|
|
292
|
-
// Check if tunnel is enabled
|
|
293
|
-
|
|
302
|
+
// Check if tunnel is enabled.
|
|
303
|
+
// When allowedTools is passed as an override (e.g. from `knowhow tunnel`),
|
|
304
|
+
// the tunnel is always forced on — that's the whole point of tunnel mode.
|
|
305
|
+
const tunnelEnabled = options?.allowedTools
|
|
306
|
+
? true
|
|
307
|
+
: (config.worker?.tunnel?.enabled ?? false);
|
|
294
308
|
|
|
295
|
-
|
|
296
|
-
let tunnelLocalHost = config.worker?.tunnel?.localHost;
|
|
297
|
-
if (!tunnelLocalHost) {
|
|
298
|
-
// Auto-detect based on Docker environment
|
|
299
|
-
if (isInsideDocker) {
|
|
300
|
-
tunnelLocalHost = "host.docker.internal";
|
|
301
|
-
console.log(
|
|
302
|
-
"🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
|
|
303
|
-
);
|
|
304
|
-
} else {
|
|
305
|
-
tunnelLocalHost = "127.0.0.1";
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Check for port mapping configuration
|
|
310
|
-
const portMapping = config.worker?.tunnel?.portMapping || {};
|
|
311
|
-
if (Object.keys(portMapping).length > 0) {
|
|
312
|
-
console.log("🔀 Port mapping configured:");
|
|
313
|
-
for (const [containerPort, hostPort] of Object.entries(portMapping)) {
|
|
314
|
-
console.log(` Container port ${containerPort} → Host port ${hostPort}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
309
|
+
const { tunnelLocalHost, portMapping } = resolveTunnelConfig(config, isInsideDocker);
|
|
317
310
|
|
|
318
311
|
if (tunnelEnabled) {
|
|
319
312
|
const tunnelPorts = config.worker?.tunnel?.allowedPorts || [];
|
|
@@ -330,31 +323,6 @@ export async function worker(options?: {
|
|
|
330
323
|
);
|
|
331
324
|
}
|
|
332
325
|
|
|
333
|
-
// Extract tunnel domain from API_URL
|
|
334
|
-
// e.g., "https://api.knowhow.tyvm.ai" -> "knowhow.tyvm.ai"
|
|
335
|
-
// e.g., "http://localhost:4000" -> "localhost:4000"
|
|
336
|
-
function extractTunnelDomain(apiUrl: string): {
|
|
337
|
-
domain: string;
|
|
338
|
-
useHttps: boolean;
|
|
339
|
-
} {
|
|
340
|
-
try {
|
|
341
|
-
const url = new URL(apiUrl);
|
|
342
|
-
const useHttps = url.protocol === "https:";
|
|
343
|
-
|
|
344
|
-
// For localhost, include port; for production, just use hostname
|
|
345
|
-
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
346
|
-
return {
|
|
347
|
-
domain: `worker.${url.hostname}:${url.port || "80"}`,
|
|
348
|
-
useHttps,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
return { domain: `worker.${url.hostname}`, useHttps };
|
|
352
|
-
} catch (err) {
|
|
353
|
-
console.error("Failed to parse API_URL for tunnel domain:", err);
|
|
354
|
-
return { domain: "worker.localhost:4000", useHttps: false }; // fallback
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
326
|
async function connectWebSocket() {
|
|
359
327
|
const jwt = await loadJwt();
|
|
360
328
|
console.log(`Connecting to ${API_URL}`);
|
|
@@ -424,7 +392,9 @@ export async function worker(options?: {
|
|
|
424
392
|
// Hot-reload: re-read config, reconnect MCPs, and rebuild the tool list
|
|
425
393
|
// without restarting the worker process.
|
|
426
394
|
if (parsed?.type === "reloadConfig") {
|
|
427
|
-
console.log(
|
|
395
|
+
console.log(
|
|
396
|
+
"🔄 Received reloadConfig — reloading MCPs, modules and tools..."
|
|
397
|
+
);
|
|
428
398
|
try {
|
|
429
399
|
// Re-read fresh config from disk
|
|
430
400
|
const freshConfig = await getConfig();
|
|
@@ -436,13 +406,16 @@ export async function worker(options?: {
|
|
|
436
406
|
await Mcp.connectToConfigured(Tools);
|
|
437
407
|
|
|
438
408
|
// Rebuild the allowed tools list from fresh config
|
|
439
|
-
const allowedToolNames =
|
|
409
|
+
const allowedToolNames =
|
|
410
|
+
freshConfig.worker?.allowedTools ?? Tools.getToolNames();
|
|
440
411
|
toolsToUse = Tools.getToolsByNames(allowedToolNames);
|
|
441
412
|
|
|
442
413
|
// Update the MCP server with new tool list
|
|
443
414
|
mcpServer.withTools(toolsToUse);
|
|
444
415
|
|
|
445
|
-
console.log(
|
|
416
|
+
console.log(
|
|
417
|
+
`✅ Config reloaded: ${toolsToUse.length} tools active`
|
|
418
|
+
);
|
|
446
419
|
} catch (err) {
|
|
447
420
|
console.error("❌ Failed to reload config:", err);
|
|
448
421
|
}
|
|
@@ -456,100 +429,40 @@ export async function worker(options?: {
|
|
|
456
429
|
// Create separate WebSocket connection for tunnel if enabled
|
|
457
430
|
let tunnelConnection: WebSocket | null = null;
|
|
458
431
|
if (tunnelEnabled) {
|
|
459
|
-
tunnelConnection =
|
|
432
|
+
tunnelConnection = connectTunnelWebSocket({
|
|
433
|
+
tunnelDomain,
|
|
434
|
+
tunnelUseHttps,
|
|
435
|
+
tunnelLocalHost,
|
|
436
|
+
portMapping,
|
|
437
|
+
config,
|
|
460
438
|
headers,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return replacementUrl;
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
const tunnelConfig = {
|
|
492
|
-
allowedPorts,
|
|
493
|
-
maxConcurrentStreams:
|
|
494
|
-
config.worker?.tunnel?.maxConcurrentStreams || 50,
|
|
495
|
-
tunnelUseHttps,
|
|
496
|
-
localHost: tunnelLocalHost,
|
|
497
|
-
urlRewriter,
|
|
498
|
-
enableUrlRewriting:
|
|
499
|
-
config.worker?.tunnel?.enableUrlRewriting !== false,
|
|
500
|
-
portMapping,
|
|
501
|
-
logLevel: "debug" as const,
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
// Initialize tunnel handler with the tunnel-specific WebSocket
|
|
505
|
-
// Pass useHttps flag so the tunnel package can add the correct protocol
|
|
506
|
-
tunnelHandler = createTunnelHandler(tunnelConnection!, tunnelConfig);
|
|
507
|
-
console.log("🌐 Tunnel handler initialized");
|
|
508
|
-
console.log(tunnelConfig);
|
|
509
|
-
|
|
510
|
-
// Let modules that need the tunnel handler register their addons now
|
|
511
|
-
const tunnelModulesService = new ModulesService();
|
|
512
|
-
const { Agents, Embeddings, Plugins, Clients, Tools, MediaProcessor } = services();
|
|
513
|
-
tunnelModulesService.loadModulesFromConfig({
|
|
514
|
-
Agents, Embeddings, Plugins, Clients, Tools, MediaProcessor,
|
|
515
|
-
Tunnel: tunnelHandler,
|
|
516
|
-
}).catch((err) => {
|
|
517
|
-
console.error("Failed to load tunnel modules:", err);
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
tunnelConnection.on("close", (code, reason) => {
|
|
522
|
-
console.log(
|
|
523
|
-
`Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`
|
|
524
|
-
);
|
|
525
|
-
if (code === 1008) {
|
|
526
|
-
unauthorizedJwt = lastJwt;
|
|
527
|
-
console.error(
|
|
528
|
-
"❌ Tunnel received Unauthorized (1008). The JWT may be expired."
|
|
529
|
-
);
|
|
530
|
-
console.error(" Pausing reconnection until JWT changes...");
|
|
531
|
-
} else {
|
|
532
|
-
console.log(
|
|
533
|
-
"Tunnel connection will reconnect on next connection cycle..."
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Cleanup tunnel handler
|
|
538
|
-
if (tunnelHandler) {
|
|
539
|
-
tunnelHandler.cleanup();
|
|
540
|
-
tunnelHandler = null;
|
|
541
|
-
}
|
|
542
|
-
tunnelWs = null;
|
|
543
|
-
|
|
544
|
-
// Mark as disconnected to trigger reconnection
|
|
545
|
-
// The tunnel websocket is separate but we should reconnect both
|
|
546
|
-
connected = false;
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
tunnelConnection.on("error", (error) => {
|
|
550
|
-
console.error("Tunnel WebSocket error:", error);
|
|
551
|
-
// Mark as disconnected on error to trigger reconnection
|
|
552
|
-
connected = false;
|
|
439
|
+
authService,
|
|
440
|
+
onOpen: (handler) => {
|
|
441
|
+
tunnelHandler = handler;
|
|
442
|
+
},
|
|
443
|
+
onClose: (code, _reason) => {
|
|
444
|
+
if (code === 1008) {
|
|
445
|
+
unauthorizedJwt = lastJwt;
|
|
446
|
+
console.error(
|
|
447
|
+
"❌ Tunnel received Unauthorized (1008). The JWT may be expired."
|
|
448
|
+
);
|
|
449
|
+
console.error(" Pausing reconnection until JWT changes...");
|
|
450
|
+
} else {
|
|
451
|
+
console.log(
|
|
452
|
+
"Tunnel connection will reconnect on next connection cycle..."
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (tunnelHandler) {
|
|
456
|
+
tunnelHandler.cleanup();
|
|
457
|
+
tunnelHandler = null;
|
|
458
|
+
}
|
|
459
|
+
tunnelWs = null;
|
|
460
|
+
// The tunnel websocket is separate but we should reconnect both
|
|
461
|
+
connected = false;
|
|
462
|
+
},
|
|
463
|
+
onError: (_error) => {
|
|
464
|
+
connected = false;
|
|
465
|
+
},
|
|
553
466
|
});
|
|
554
467
|
|
|
555
468
|
tunnelWs = tunnelConnection;
|
|
@@ -632,187 +545,3 @@ export async function worker(options?: {
|
|
|
632
545
|
await wait(5000);
|
|
633
546
|
}
|
|
634
547
|
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Run tunnel-only mode: connects to the Knowhow tunnel WebSocket without
|
|
638
|
-
* registering any MCP tools. Useful for users who only want the web tunnel
|
|
639
|
-
* feature to expose local ports to the cloud.
|
|
640
|
-
*/
|
|
641
|
-
export async function tunnel(options?: {
|
|
642
|
-
share?: boolean;
|
|
643
|
-
unshare?: boolean;
|
|
644
|
-
}) {
|
|
645
|
-
const config = await getConfig();
|
|
646
|
-
|
|
647
|
-
const isInsideDocker = process.env.KNOWHOW_DOCKER === "true";
|
|
648
|
-
|
|
649
|
-
// Determine localHost based on environment
|
|
650
|
-
let tunnelLocalHost = config.worker?.tunnel?.localHost;
|
|
651
|
-
if (!tunnelLocalHost) {
|
|
652
|
-
if (isInsideDocker) {
|
|
653
|
-
tunnelLocalHost = "host.docker.internal";
|
|
654
|
-
console.log(
|
|
655
|
-
"🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
|
|
656
|
-
);
|
|
657
|
-
} else {
|
|
658
|
-
tunnelLocalHost = "127.0.0.1";
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Check for port mapping configuration
|
|
663
|
-
const portMapping = config.worker?.tunnel?.portMapping || {};
|
|
664
|
-
if (Object.keys(portMapping).length > 0) {
|
|
665
|
-
console.log("🔀 Port mapping configured:");
|
|
666
|
-
for (const [containerPort, hostPort] of Object.entries(portMapping)) {
|
|
667
|
-
console.log(` Container port ${containerPort} → Host port ${hostPort}`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const tunnelPorts = config.worker?.tunnel?.allowedPorts || [];
|
|
672
|
-
if (tunnelPorts.length === 0) {
|
|
673
|
-
console.warn(
|
|
674
|
-
"⚠️ No allowedPorts configured. Add worker.tunnel.allowedPorts to knowhow.json"
|
|
675
|
-
);
|
|
676
|
-
} else {
|
|
677
|
-
console.log(`🌐 Tunnel mode for ports: ${tunnelPorts.join(", ")}`);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Extract tunnel domain from API_URL
|
|
681
|
-
function extractTunnelDomain(apiUrl: string): {
|
|
682
|
-
domain: string;
|
|
683
|
-
useHttps: boolean;
|
|
684
|
-
} {
|
|
685
|
-
try {
|
|
686
|
-
const url = new URL(apiUrl);
|
|
687
|
-
const useHttps = url.protocol === "https:";
|
|
688
|
-
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
689
|
-
return {
|
|
690
|
-
domain: `worker.${url.hostname}:${url.port || "80"}`,
|
|
691
|
-
useHttps,
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
return { domain: `worker.${url.hostname}`, useHttps };
|
|
695
|
-
} catch (err) {
|
|
696
|
-
console.error("Failed to parse API_URL for tunnel domain:", err);
|
|
697
|
-
return { domain: "worker.localhost:4000", useHttps: false };
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
let connected = false;
|
|
702
|
-
let tunnelHandler: TunnelHandler | null = null;
|
|
703
|
-
let lastJwt: string | null = null;
|
|
704
|
-
let unauthorizedJwt: string | null = null;
|
|
705
|
-
|
|
706
|
-
async function connectTunnel() {
|
|
707
|
-
const jwt = await loadJwt();
|
|
708
|
-
lastJwt = jwt;
|
|
709
|
-
console.log(`Connecting tunnel to ${API_URL}`);
|
|
710
|
-
|
|
711
|
-
const dir = process.cwd();
|
|
712
|
-
const homedir = os.homedir();
|
|
713
|
-
const hostname = process.env.WORKER_HOSTNAME || os.hostname();
|
|
714
|
-
const root =
|
|
715
|
-
process.env.WORKER_ROOT ||
|
|
716
|
-
(dir === homedir ? "~" : dir.replace(homedir, "~"));
|
|
717
|
-
|
|
718
|
-
const headers: Record<string, string> = {
|
|
719
|
-
Authorization: `Bearer ${jwt}`,
|
|
720
|
-
"User-Agent": `knowhow-tunnel/1.0.0/${hostname}`,
|
|
721
|
-
Root: root,
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
if (options?.share) {
|
|
725
|
-
headers.Shared = "true";
|
|
726
|
-
console.log("🔓 Tunnel shared with organization");
|
|
727
|
-
} else if (options?.unshare) {
|
|
728
|
-
headers.Shared = "false";
|
|
729
|
-
console.log("🔒 Tunnel is now private (unshared)");
|
|
730
|
-
} else {
|
|
731
|
-
console.log("🔒 Tunnel is private (only you can use it)");
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const { domain: tunnelDomain, useHttps: tunnelUseHttps } =
|
|
735
|
-
extractTunnelDomain(API_URL);
|
|
736
|
-
|
|
737
|
-
const tunnelConnection = new WebSocket(`${API_URL}/ws/tunnel`, { headers });
|
|
738
|
-
|
|
739
|
-
tunnelConnection.on("open", () => {
|
|
740
|
-
console.log("🌐 Tunnel WebSocket connected");
|
|
741
|
-
connected = true;
|
|
742
|
-
|
|
743
|
-
const allowedPorts = config.worker?.tunnel?.allowedPorts || [];
|
|
744
|
-
const urlRewriter = (port: number, metadata?: any) => {
|
|
745
|
-
const workerId = metadata?.workerId;
|
|
746
|
-
const secret = metadata?.secret;
|
|
747
|
-
const subdomain = secret ? `${secret}-p${port}` : `${workerId}-p${port}`;
|
|
748
|
-
return `${subdomain}.${tunnelDomain}`;
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
const tunnelConfig = {
|
|
752
|
-
allowedPorts,
|
|
753
|
-
maxConcurrentStreams: config.worker?.tunnel?.maxConcurrentStreams || 50,
|
|
754
|
-
tunnelUseHttps,
|
|
755
|
-
localHost: tunnelLocalHost,
|
|
756
|
-
urlRewriter,
|
|
757
|
-
enableUrlRewriting: config.worker?.tunnel?.enableUrlRewriting !== false,
|
|
758
|
-
portMapping,
|
|
759
|
-
logLevel: "debug" as const,
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
tunnelHandler = createTunnelHandler(tunnelConnection, tunnelConfig);
|
|
763
|
-
console.log("🌐 Tunnel handler initialized");
|
|
764
|
-
console.log(tunnelConfig);
|
|
765
|
-
|
|
766
|
-
// Let modules that need the tunnel handler register their addons now
|
|
767
|
-
const tunnelModulesService2 = new ModulesService();
|
|
768
|
-
const { Agents: A2, Embeddings: E2, Plugins: P2, Clients: C2, Tools: T2, MediaProcessor: MP2 } = services();
|
|
769
|
-
tunnelModulesService2.loadModulesFromConfig({
|
|
770
|
-
Agents: A2, Embeddings: E2, Plugins: P2, Clients: C2, Tools: T2, MediaProcessor: MP2,
|
|
771
|
-
Tunnel: tunnelHandler,
|
|
772
|
-
}).catch((err) => {
|
|
773
|
-
console.error("Failed to load tunnel modules:", err);
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
tunnelConnection.on("close", (code, reason) => {
|
|
778
|
-
console.log(`Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`);
|
|
779
|
-
if (code === 1008) {
|
|
780
|
-
unauthorizedJwt = lastJwt;
|
|
781
|
-
console.error("❌ Tunnel received Unauthorized (1008). The JWT may be expired.");
|
|
782
|
-
console.error(" Run 'knowhow login' to refresh your token, then restart.");
|
|
783
|
-
console.error(" Pausing reconnection until JWT changes...");
|
|
784
|
-
} else {
|
|
785
|
-
console.log("Tunnel connection will reconnect on next cycle...");
|
|
786
|
-
}
|
|
787
|
-
if (tunnelHandler) {
|
|
788
|
-
tunnelHandler.cleanup();
|
|
789
|
-
tunnelHandler = null;
|
|
790
|
-
}
|
|
791
|
-
connected = false;
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
tunnelConnection.on("error", (error) => {
|
|
795
|
-
console.error("Tunnel WebSocket error:", error);
|
|
796
|
-
connected = false;
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
return tunnelConnection;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
while (true) {
|
|
803
|
-
if (!connected) {
|
|
804
|
-
if (unauthorizedJwt !== null) {
|
|
805
|
-
const currentJwt = await loadJwt().catch(() => null);
|
|
806
|
-
if (currentJwt === unauthorizedJwt) {
|
|
807
|
-
await wait(5000);
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
console.log("🔄 JWT has changed, attempting to reconnect tunnel...");
|
|
811
|
-
unauthorizedJwt = null;
|
|
812
|
-
}
|
|
813
|
-
console.log("Attempting to connect tunnel...");
|
|
814
|
-
await connectTunnel();
|
|
815
|
-
}
|
|
816
|
-
await wait(5000);
|
|
817
|
-
}
|
|
818
|
-
}
|