@tyvm/knowhow 0.0.108-dev.126b29e → 0.0.108-dev.501f36f
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 -3
- package/src/agents/base/base.ts +9 -0
- package/src/agents/tools/index.ts +0 -1
- package/src/agents/tools/list.ts +0 -2
- package/src/chat/CliChatService.ts +10 -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 +91 -664
- package/src/clients/index.ts +6 -5
- package/src/clients/types.ts +12 -4
- package/src/cloudWorker.ts +110 -122
- package/src/commands/agent.ts +246 -0
- package/src/commands/misc.ts +174 -0
- package/src/commands/modules.ts +182 -0
- package/src/commands/services.ts +77 -0
- package/src/commands/workers.ts +168 -0
- package/src/config.ts +37 -0
- package/src/fileSync.ts +50 -17
- package/src/index.ts +1 -0
- package/src/logger.ts +197 -0
- package/src/plugins/plugins.ts +0 -21
- package/src/processors/JsonCompressor.ts +6 -6
- package/src/services/EventService.ts +61 -1
- package/src/services/KnowhowClient.ts +12 -2
- package/src/services/S3.ts +0 -10
- package/src/services/modules/index.ts +70 -50
- package/src/services/modules/types.ts +4 -0
- package/src/tunnel.ts +216 -0
- package/src/types.ts +0 -1
- 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/tests/unit/commands/github-credentials.test.ts +211 -0
- package/tests/unit/modules/moduleLoading.test.ts +39 -37
- package/tests/unit/plugins/pluginLoading.test.ts +0 -85
- package/ts_build/package.json +2 -3
- 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/agents/tools/index.d.ts +0 -1
- package/ts_build/src/agents/tools/index.js +0 -1
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +0 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +13 -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 +47 -525
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/index.js +2 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +2 -2
- package/ts_build/src/cloudWorker.d.ts +5 -0
- package/ts_build/src/cloudWorker.js +69 -66
- package/ts_build/src/cloudWorker.js.map +1 -1
- package/ts_build/src/commands/agent.d.ts +6 -0
- package/ts_build/src/commands/agent.js +229 -0
- package/ts_build/src/commands/agent.js.map +1 -0
- package/ts_build/src/commands/misc.d.ts +10 -0
- package/ts_build/src/commands/misc.js +197 -0
- package/ts_build/src/commands/misc.js.map +1 -0
- package/ts_build/src/commands/modules.d.ts +3 -0
- package/ts_build/src/commands/modules.js +160 -0
- package/ts_build/src/commands/modules.js.map +1 -0
- package/ts_build/src/commands/services.d.ts +5 -0
- package/ts_build/src/commands/services.js +87 -0
- package/ts_build/src/commands/services.js.map +1 -0
- package/ts_build/src/commands/workers.d.ts +6 -0
- package/ts_build/src/commands/workers.js +168 -0
- package/ts_build/src/commands/workers.js.map +1 -0
- package/ts_build/src/config.d.ts +1 -0
- package/ts_build/src/config.js +32 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/fileSync.d.ts +6 -0
- package/ts_build/src/fileSync.js +37 -12
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +3 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/logger.d.ts +21 -0
- package/ts_build/src/logger.js +106 -0
- package/ts_build/src/logger.js.map +1 -0
- package/ts_build/src/plugins/plugins.d.ts +0 -2
- package/ts_build/src/plugins/plugins.js +0 -11
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/processors/JsonCompressor.js +4 -4
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/services/EventService.d.ts +6 -1
- package/ts_build/src/services/EventService.js +29 -0
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +8 -2
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/S3.js +0 -7
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +33 -0
- package/ts_build/src/services/modules/index.js +46 -45
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +4 -0
- 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/types.d.ts +0 -1
- package/ts_build/src/types.js.map +1 -1
- 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/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
- package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
- package/src/agents/tools/executeScript/README.md +0 -94
- package/src/agents/tools/executeScript/definition.ts +0 -79
- package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
- package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
- package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
- package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
- package/src/agents/tools/executeScript/index.ts +0 -98
- package/src/services/script-execution/SandboxContext.ts +0 -282
- package/src/services/script-execution/ScriptExecutor.ts +0 -441
- package/src/services/script-execution/ScriptPolicy.ts +0 -194
- package/src/services/script-execution/ScriptTracer.ts +0 -249
- package/src/services/script-execution/types.ts +0 -134
- package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
- package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
- package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
- package/ts_build/src/agents/tools/executeScript/index.js +0 -72
- package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
- package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
- package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
- package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
- package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
- package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
- package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
- package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
- package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
- package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
- package/ts_build/src/services/script-execution/types.d.ts +0 -108
- package/ts_build/src/services/script-execution/types.js +0 -3
- package/ts_build/src/services/script-execution/types.js.map +0 -1
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
|
-
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import EventEmitter from "events";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A middleware function that intercepts raw WebSocket messages before
|
|
6
|
+
* they reach the transport/handler layer.
|
|
7
|
+
*
|
|
8
|
+
* - Call next() to pass the message through.
|
|
9
|
+
* - Call next(err) to abort (the socket will be closed with code 1008 + reason).
|
|
10
|
+
* - Sending a reply directly on ws is allowed (e.g. for auth challenges).
|
|
11
|
+
*/
|
|
12
|
+
export type WsMiddlewareFn = (
|
|
13
|
+
ws: WebSocket,
|
|
14
|
+
data: Buffer | string,
|
|
15
|
+
next: (err?: Error) => void
|
|
16
|
+
) => void | Promise<void>;
|
|
17
|
+
|
|
18
|
+
export class WsMiddlewareStack {
|
|
19
|
+
private fns: WsMiddlewareFn[] = [];
|
|
20
|
+
|
|
21
|
+
use(fn: WsMiddlewareFn): this {
|
|
22
|
+
this.fns.push(fn);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attach all middleware to a WebSocket, intercepting every "message" event.
|
|
28
|
+
* Once a message passes all middleware, onMessage is called.
|
|
29
|
+
* Use this for MCP transports where you control the message handler directly.
|
|
30
|
+
*/
|
|
31
|
+
attach(ws: WebSocket, onMessage: (data: Buffer | string) => void): void {
|
|
32
|
+
ws.on("message", async (data: Buffer | string) => {
|
|
33
|
+
let i = 0;
|
|
34
|
+
const next = async (err?: Error): Promise<void> => {
|
|
35
|
+
if (err) {
|
|
36
|
+
console.error("WS middleware rejected message:", err.message);
|
|
37
|
+
ws.close(1008, err.message);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const fn = this.fns[i++];
|
|
41
|
+
if (fn) {
|
|
42
|
+
await fn(ws, data, next);
|
|
43
|
+
} else {
|
|
44
|
+
onMessage(data);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
await next();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wrap a WebSocket so ALL incoming messages pass through this middleware
|
|
53
|
+
* before being dispatched to any subsequently-registered "message" listeners.
|
|
54
|
+
*
|
|
55
|
+
* Call this BEFORE any other code attaches "message" handlers (e.g. before
|
|
56
|
+
* initTunnelHandler). Uses Node.js EventEmitter ordering: our listener runs
|
|
57
|
+
* first because it was registered first.
|
|
58
|
+
*
|
|
59
|
+
* After wrapSocket(), any subsequent ws.on("message", handler) calls are
|
|
60
|
+
* redirected to an inner EventEmitter that only receives messages that have
|
|
61
|
+
* passed all middleware. This ensures the tunnel handler's listener is
|
|
62
|
+
* automatically gated by the middleware.
|
|
63
|
+
*/
|
|
64
|
+
wrapSocket(ws: WebSocket): void {
|
|
65
|
+
const innerEmitter = new EventEmitter();
|
|
66
|
+
|
|
67
|
+
// Our listener runs first (registered before tunnel handler's listener).
|
|
68
|
+
ws.on("message", async (data: Buffer | string) => {
|
|
69
|
+
let i = 0;
|
|
70
|
+
const next = async (err?: Error): Promise<void> => {
|
|
71
|
+
if (err) {
|
|
72
|
+
console.error("WS middleware rejected message:", err.message);
|
|
73
|
+
ws.close(1008, err.message);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const fn = this.fns[i++];
|
|
77
|
+
if (fn) {
|
|
78
|
+
await fn(ws, data, next);
|
|
79
|
+
} else {
|
|
80
|
+
// All middleware passed — dispatch to inner listeners
|
|
81
|
+
innerEmitter.emit("message", data);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
await next();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Redirect future ws.on("message", ...) calls to innerEmitter.
|
|
88
|
+
// This means createTunnelHandler()'s listener goes to innerEmitter,
|
|
89
|
+
// so it only receives messages that passed middleware.
|
|
90
|
+
const originalOn = ws.on.bind(ws);
|
|
91
|
+
(ws as any).on = (event: string, listener: (...args: any[]) => void) => {
|
|
92
|
+
if (event === "message") {
|
|
93
|
+
innerEmitter.on("message", listener);
|
|
94
|
+
return ws;
|
|
95
|
+
}
|
|
96
|
+
return originalOn(event, listener);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { WorkerPasskeyAuthService } from "./WorkerPasskeyAuth";
|
|
3
|
+
import { WsMiddlewareFn } from "./WsMiddleware";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WebSocket middleware that gates all messages behind passkey auth.
|
|
7
|
+
*
|
|
8
|
+
* This middleware is applied to the TUNNEL WebSocket only — NOT the worker
|
|
9
|
+
* MCP WebSocket. The MCP path keeps the existing unlock/lock tool-based approach.
|
|
10
|
+
*
|
|
11
|
+
* Auth protocol (out-of-band, not MCP):
|
|
12
|
+
*
|
|
13
|
+
* Client → Worker: { type: "auth:getChallenge" }
|
|
14
|
+
* Worker → Client: { type: "auth:challenge", challenge: "<base64url>", credentialId: "..." }
|
|
15
|
+
* Client → Worker: { type: "auth:response", challenge, signature, credentialId,
|
|
16
|
+
* authenticatorData, clientDataJSON }
|
|
17
|
+
* Worker → Client: { type: "auth:success", expiresAt: "<iso>" }
|
|
18
|
+
* or { type: "auth:failure", reason: "..." }
|
|
19
|
+
*
|
|
20
|
+
* While locked, all non-auth messages receive { type: "auth:locked" }.
|
|
21
|
+
* Once unlocked, isLocked() is re-checked on every message to enforce session expiry.
|
|
22
|
+
*
|
|
23
|
+
* The authService passed here is the SAME singleton used by the MCP unlock tool,
|
|
24
|
+
* so unlocking via either path opens both the tunnel and MCP tool access.
|
|
25
|
+
*/
|
|
26
|
+
export function makeAuthMiddleware(
|
|
27
|
+
authService: WorkerPasskeyAuthService
|
|
28
|
+
): WsMiddlewareFn {
|
|
29
|
+
return async (ws: WebSocket, data: Buffer | string, next) => {
|
|
30
|
+
// Re-check on every message to enforce session expiry
|
|
31
|
+
if (!authService.isLocked()) {
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse the raw message
|
|
36
|
+
let parsed: any;
|
|
37
|
+
try {
|
|
38
|
+
const raw = typeof data === "string" ? data : data.toString("utf-8");
|
|
39
|
+
parsed = JSON.parse(raw);
|
|
40
|
+
} catch {
|
|
41
|
+
ws.send(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
type: "auth:locked",
|
|
44
|
+
message: "Worker is locked. Send { type: 'auth:getChallenge' } first.",
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
return; // don't call next()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// auth:getChallenge — issue a challenge
|
|
51
|
+
if (parsed.type === "auth:getChallenge") {
|
|
52
|
+
const challenge = authService.generateChallenge();
|
|
53
|
+
ws.send(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
type: "auth:challenge",
|
|
56
|
+
challenge,
|
|
57
|
+
credentialId: authService.getCredentialId(),
|
|
58
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// auth:response — verify the assertion
|
|
65
|
+
if (parsed.type === "auth:response") {
|
|
66
|
+
const result = await authService.unlock({
|
|
67
|
+
signature: parsed.signature,
|
|
68
|
+
credentialId: parsed.credentialId,
|
|
69
|
+
authenticatorData: parsed.authenticatorData,
|
|
70
|
+
clientDataJSON: parsed.clientDataJSON,
|
|
71
|
+
challenge: parsed.challenge,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (result.success) {
|
|
75
|
+
const info = authService.getSessionInfo();
|
|
76
|
+
ws.send(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
type: "auth:success",
|
|
79
|
+
expiresAt: info.expiresAt,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
ws.send(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
type: "auth:failure",
|
|
86
|
+
reason: result.reason ?? "unknown",
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
// Auth protocol message — don't pass to tunnel handler
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Any other message while locked — block it
|
|
95
|
+
ws.send(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
type: "auth:locked",
|
|
98
|
+
message:
|
|
99
|
+
"Worker is locked. Send { type: 'auth:getChallenge' } to authenticate.",
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
// don't call next()
|
|
103
|
+
};
|
|
104
|
+
}
|