@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.
Files changed (177) hide show
  1. package/package.json +2 -3
  2. package/src/agents/base/base.ts +9 -0
  3. package/src/agents/tools/index.ts +0 -1
  4. package/src/agents/tools/list.ts +0 -2
  5. package/src/chat/CliChatService.ts +10 -1
  6. package/src/chat/renderer/CompactRenderer.ts +20 -0
  7. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  8. package/src/chat/renderer/FancyRenderer.ts +19 -0
  9. package/src/chat/renderer/types.ts +11 -0
  10. package/src/cli.ts +91 -664
  11. package/src/clients/index.ts +6 -5
  12. package/src/clients/types.ts +12 -4
  13. package/src/cloudWorker.ts +110 -122
  14. package/src/commands/agent.ts +246 -0
  15. package/src/commands/misc.ts +174 -0
  16. package/src/commands/modules.ts +182 -0
  17. package/src/commands/services.ts +77 -0
  18. package/src/commands/workers.ts +168 -0
  19. package/src/config.ts +37 -0
  20. package/src/fileSync.ts +50 -17
  21. package/src/index.ts +1 -0
  22. package/src/logger.ts +197 -0
  23. package/src/plugins/plugins.ts +0 -21
  24. package/src/processors/JsonCompressor.ts +6 -6
  25. package/src/services/EventService.ts +61 -1
  26. package/src/services/KnowhowClient.ts +12 -2
  27. package/src/services/S3.ts +0 -10
  28. package/src/services/modules/index.ts +70 -50
  29. package/src/services/modules/types.ts +4 -0
  30. package/src/tunnel.ts +216 -0
  31. package/src/types.ts +0 -1
  32. package/src/worker.ts +65 -336
  33. package/src/workers/auth/WsMiddleware.ts +99 -0
  34. package/src/workers/auth/authMiddleware.ts +104 -0
  35. package/src/workers/auth/types.ts +14 -2
  36. package/tests/unit/commands/github-credentials.test.ts +211 -0
  37. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  38. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  39. package/ts_build/package.json +2 -3
  40. package/ts_build/src/agents/base/base.js +10 -0
  41. package/ts_build/src/agents/base/base.js.map +1 -1
  42. package/ts_build/src/agents/tools/index.d.ts +0 -1
  43. package/ts_build/src/agents/tools/index.js +0 -1
  44. package/ts_build/src/agents/tools/index.js.map +1 -1
  45. package/ts_build/src/agents/tools/list.js +0 -2
  46. package/ts_build/src/agents/tools/list.js.map +1 -1
  47. package/ts_build/src/chat/CliChatService.js +13 -1
  48. package/ts_build/src/chat/CliChatService.js.map +1 -1
  49. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  50. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  51. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  52. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  53. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  54. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  55. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  56. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  57. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  58. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  59. package/ts_build/src/cli.js +47 -525
  60. package/ts_build/src/cli.js.map +1 -1
  61. package/ts_build/src/clients/index.js +2 -4
  62. package/ts_build/src/clients/index.js.map +1 -1
  63. package/ts_build/src/clients/types.d.ts +2 -2
  64. package/ts_build/src/cloudWorker.d.ts +5 -0
  65. package/ts_build/src/cloudWorker.js +69 -66
  66. package/ts_build/src/cloudWorker.js.map +1 -1
  67. package/ts_build/src/commands/agent.d.ts +6 -0
  68. package/ts_build/src/commands/agent.js +229 -0
  69. package/ts_build/src/commands/agent.js.map +1 -0
  70. package/ts_build/src/commands/misc.d.ts +10 -0
  71. package/ts_build/src/commands/misc.js +197 -0
  72. package/ts_build/src/commands/misc.js.map +1 -0
  73. package/ts_build/src/commands/modules.d.ts +3 -0
  74. package/ts_build/src/commands/modules.js +160 -0
  75. package/ts_build/src/commands/modules.js.map +1 -0
  76. package/ts_build/src/commands/services.d.ts +5 -0
  77. package/ts_build/src/commands/services.js +87 -0
  78. package/ts_build/src/commands/services.js.map +1 -0
  79. package/ts_build/src/commands/workers.d.ts +6 -0
  80. package/ts_build/src/commands/workers.js +168 -0
  81. package/ts_build/src/commands/workers.js.map +1 -0
  82. package/ts_build/src/config.d.ts +1 -0
  83. package/ts_build/src/config.js +32 -0
  84. package/ts_build/src/config.js.map +1 -1
  85. package/ts_build/src/fileSync.d.ts +6 -0
  86. package/ts_build/src/fileSync.js +37 -12
  87. package/ts_build/src/fileSync.js.map +1 -1
  88. package/ts_build/src/index.d.ts +1 -0
  89. package/ts_build/src/index.js +3 -1
  90. package/ts_build/src/index.js.map +1 -1
  91. package/ts_build/src/logger.d.ts +21 -0
  92. package/ts_build/src/logger.js +106 -0
  93. package/ts_build/src/logger.js.map +1 -0
  94. package/ts_build/src/plugins/plugins.d.ts +0 -2
  95. package/ts_build/src/plugins/plugins.js +0 -11
  96. package/ts_build/src/plugins/plugins.js.map +1 -1
  97. package/ts_build/src/processors/JsonCompressor.js +4 -4
  98. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  99. package/ts_build/src/services/EventService.d.ts +6 -1
  100. package/ts_build/src/services/EventService.js +29 -0
  101. package/ts_build/src/services/EventService.js.map +1 -1
  102. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  103. package/ts_build/src/services/KnowhowClient.js +8 -2
  104. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  105. package/ts_build/src/services/S3.js +0 -7
  106. package/ts_build/src/services/S3.js.map +1 -1
  107. package/ts_build/src/services/modules/index.d.ts +33 -0
  108. package/ts_build/src/services/modules/index.js +46 -45
  109. package/ts_build/src/services/modules/index.js.map +1 -1
  110. package/ts_build/src/services/modules/types.d.ts +4 -0
  111. package/ts_build/src/tunnel.d.ts +27 -0
  112. package/ts_build/src/tunnel.js +112 -0
  113. package/ts_build/src/tunnel.js.map +1 -0
  114. package/ts_build/src/types.d.ts +0 -1
  115. package/ts_build/src/types.js.map +1 -1
  116. package/ts_build/src/worker.d.ts +1 -4
  117. package/ts_build/src/worker.js +38 -244
  118. package/ts_build/src/worker.js.map +1 -1
  119. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  120. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  121. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  122. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  123. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  124. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  125. package/ts_build/src/workers/auth/types.d.ts +8 -1
  126. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  127. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  128. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  129. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  130. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  131. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  132. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  133. package/src/agents/tools/executeScript/README.md +0 -94
  134. package/src/agents/tools/executeScript/definition.ts +0 -79
  135. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  136. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  137. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  138. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  139. package/src/agents/tools/executeScript/index.ts +0 -98
  140. package/src/services/script-execution/SandboxContext.ts +0 -282
  141. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  142. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  143. package/src/services/script-execution/ScriptTracer.ts +0 -249
  144. package/src/services/script-execution/types.ts +0 -134
  145. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  146. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  147. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  148. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  149. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  150. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  151. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  152. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  153. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  154. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  155. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  156. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  157. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  158. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  159. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  160. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  161. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  162. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  163. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  164. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  165. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  166. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  167. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  168. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  169. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  170. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  171. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  172. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  173. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  174. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  175. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  176. package/ts_build/src/services/script-execution/types.js +0 -3
  177. 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 { createTunnelHandler, TunnelHandler } from "@tyvm/knowhow-tunnel";
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 { makeUnlockTool, makeLockTool, makeReloadConfigTool } from "./workers/tools";
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 { ModulesService } from "./services/modules";
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
- // Use the config we already loaded above
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
- let toolsToUse = Tools.getToolsByNames(config.worker.allowedTools);
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
- const tunnelEnabled = config.worker?.tunnel?.enabled ?? false;
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
- // Determine localHost based on environment
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("🔄 Received reloadConfig — reloading MCPs, modules and tools...");
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 = freshConfig.worker?.allowedTools ?? Tools.getToolNames();
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(`✅ Config reloaded: ${toolsToUse.length} tools active`);
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 = new WebSocket(`${API_URL}/ws/tunnel`, {
432
+ tunnelConnection = connectTunnelWebSocket({
433
+ tunnelDomain,
434
+ tunnelUseHttps,
435
+ tunnelLocalHost,
436
+ portMapping,
437
+ config,
460
438
  headers,
461
- });
462
-
463
- tunnelConnection.on("open", () => {
464
- console.log("Tunnel WebSocket connected");
465
-
466
- // Get the allowedPorts configuration
467
- const allowedPorts = config.worker?.tunnel?.allowedPorts || [];
468
-
469
- // Create URL rewriter callback that returns the hostname (without protocol)
470
- // The tunnel package will add the protocol based on the useHttps config
471
- // This receives port and metadata from the tunnel request
472
- const urlRewriter = (port: number, metadata?: any) => {
473
- const workerId = metadata?.workerId;
474
- const secret = metadata?.secret;
475
-
476
- // Build the hostname/domain (without protocol) based on metadata
477
- // The tunnel handler will add the protocol using the useHttps config
478
- // Examples:
479
- // - secret-p3000.worker.example.com
480
- // - workerId-p3000.worker.example.com
481
- const subdomain = secret
482
- ? `${secret}-p${port}`
483
- : `${workerId}-p${port}`;
484
-
485
- // Return just the hostname - the tunnel package should add the protocol
486
- // based on the useHttps configuration passed below
487
- const replacementUrl = `${subdomain}.${tunnelDomain}`;
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
+ }