@tyvm/knowhow 0.0.108 → 0.0.109-dev.05fe5a0

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 (210) hide show
  1. package/README.md +45 -0
  2. package/package.json +9 -4
  3. package/scripts/build-for-node.sh +10 -24
  4. package/scripts/publish.sh +86 -0
  5. package/src/agents/base/base.ts +10 -0
  6. package/src/agents/tools/execCommand.ts +49 -6
  7. package/src/agents/tools/index.ts +0 -1
  8. package/src/agents/tools/list.ts +0 -2
  9. package/src/chat/CliChatService.ts +10 -1
  10. package/src/chat/modules/AgentModule.ts +61 -31
  11. package/src/chat/modules/SessionsModule.ts +47 -3
  12. package/src/chat/renderer/CompactRenderer.ts +20 -0
  13. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  14. package/src/chat/renderer/FancyRenderer.ts +19 -0
  15. package/src/chat/renderer/types.ts +11 -0
  16. package/src/cli.ts +91 -659
  17. package/src/clients/anthropic.ts +17 -16
  18. package/src/clients/index.ts +6 -5
  19. package/src/clients/types.ts +19 -4
  20. package/src/cloudWorker.ts +175 -113
  21. package/src/commands/agent.ts +246 -0
  22. package/src/commands/misc.ts +174 -0
  23. package/src/commands/modules.ts +217 -0
  24. package/src/commands/services.ts +77 -0
  25. package/src/commands/workers.ts +168 -0
  26. package/src/config.ts +37 -0
  27. package/src/fileSync.ts +50 -17
  28. package/src/index.ts +18 -0
  29. package/src/logger.ts +197 -0
  30. package/src/plugins/embedding.ts +11 -6
  31. package/src/plugins/plugins.ts +0 -21
  32. package/src/plugins/vim.ts +5 -16
  33. package/src/processors/JsonCompressor.ts +6 -6
  34. package/src/services/EventService.ts +61 -1
  35. package/src/services/KnowhowClient.ts +34 -4
  36. package/src/services/modules/index.ts +95 -51
  37. package/src/services/modules/types.ts +6 -0
  38. package/src/tunnel.ts +216 -0
  39. package/src/types.ts +0 -1
  40. package/src/worker.ts +105 -312
  41. package/src/workers/auth/WsMiddleware.ts +99 -0
  42. package/src/workers/auth/authMiddleware.ts +104 -0
  43. package/src/workers/auth/types.ts +14 -2
  44. package/src/workers/tools/index.ts +2 -0
  45. package/src/workers/tools/reloadConfig.ts +84 -0
  46. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  47. package/tests/unit/commands/github-credentials.test.ts +211 -0
  48. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  49. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  50. package/ts_build/package.json +9 -4
  51. package/ts_build/src/agents/base/base.js +11 -0
  52. package/ts_build/src/agents/base/base.js.map +1 -1
  53. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  54. package/ts_build/src/agents/tools/execCommand.js +39 -5
  55. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  56. package/ts_build/src/agents/tools/index.d.ts +0 -1
  57. package/ts_build/src/agents/tools/index.js +0 -1
  58. package/ts_build/src/agents/tools/index.js.map +1 -1
  59. package/ts_build/src/agents/tools/list.js +0 -2
  60. package/ts_build/src/agents/tools/list.js.map +1 -1
  61. package/ts_build/src/chat/CliChatService.js +13 -1
  62. package/ts_build/src/chat/CliChatService.js.map +1 -1
  63. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  64. package/ts_build/src/chat/modules/AgentModule.js +43 -20
  65. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  66. package/ts_build/src/chat/modules/SessionsModule.js +37 -3
  67. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  68. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  69. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  70. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  71. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  72. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  73. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  74. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  75. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  76. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  77. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  78. package/ts_build/src/cli.js +47 -519
  79. package/ts_build/src/cli.js.map +1 -1
  80. package/ts_build/src/clients/anthropic.d.ts +5 -5
  81. package/ts_build/src/clients/anthropic.js +17 -16
  82. package/ts_build/src/clients/anthropic.js.map +1 -1
  83. package/ts_build/src/clients/index.js +2 -4
  84. package/ts_build/src/clients/index.js.map +1 -1
  85. package/ts_build/src/clients/types.d.ts +3 -2
  86. package/ts_build/src/cloudWorker.d.ts +14 -0
  87. package/ts_build/src/cloudWorker.js +105 -66
  88. package/ts_build/src/cloudWorker.js.map +1 -1
  89. package/ts_build/src/commands/agent.d.ts +6 -0
  90. package/ts_build/src/commands/agent.js +229 -0
  91. package/ts_build/src/commands/agent.js.map +1 -0
  92. package/ts_build/src/commands/misc.d.ts +10 -0
  93. package/ts_build/src/commands/misc.js +197 -0
  94. package/ts_build/src/commands/misc.js.map +1 -0
  95. package/ts_build/src/commands/modules.d.ts +3 -0
  96. package/ts_build/src/commands/modules.js +207 -0
  97. package/ts_build/src/commands/modules.js.map +1 -0
  98. package/ts_build/src/commands/services.d.ts +5 -0
  99. package/ts_build/src/commands/services.js +87 -0
  100. package/ts_build/src/commands/services.js.map +1 -0
  101. package/ts_build/src/commands/workers.d.ts +6 -0
  102. package/ts_build/src/commands/workers.js +168 -0
  103. package/ts_build/src/commands/workers.js.map +1 -0
  104. package/ts_build/src/config.d.ts +1 -0
  105. package/ts_build/src/config.js +32 -0
  106. package/ts_build/src/config.js.map +1 -1
  107. package/ts_build/src/fileSync.d.ts +6 -0
  108. package/ts_build/src/fileSync.js +37 -12
  109. package/ts_build/src/fileSync.js.map +1 -1
  110. package/ts_build/src/index.d.ts +1 -0
  111. package/ts_build/src/index.js +17 -1
  112. package/ts_build/src/index.js.map +1 -1
  113. package/ts_build/src/logger.d.ts +21 -0
  114. package/ts_build/src/logger.js +106 -0
  115. package/ts_build/src/logger.js.map +1 -0
  116. package/ts_build/src/plugins/embedding.js +4 -3
  117. package/ts_build/src/plugins/embedding.js.map +1 -1
  118. package/ts_build/src/plugins/plugins.d.ts +0 -2
  119. package/ts_build/src/plugins/plugins.js +0 -11
  120. package/ts_build/src/plugins/plugins.js.map +1 -1
  121. package/ts_build/src/plugins/vim.js +3 -9
  122. package/ts_build/src/plugins/vim.js.map +1 -1
  123. package/ts_build/src/processors/JsonCompressor.js +4 -4
  124. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  125. package/ts_build/src/services/EventService.d.ts +6 -1
  126. package/ts_build/src/services/EventService.js +29 -0
  127. package/ts_build/src/services/EventService.js.map +1 -1
  128. package/ts_build/src/services/KnowhowClient.d.ts +13 -1
  129. package/ts_build/src/services/KnowhowClient.js +19 -2
  130. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  131. package/ts_build/src/services/modules/index.d.ts +33 -0
  132. package/ts_build/src/services/modules/index.js +67 -47
  133. package/ts_build/src/services/modules/index.js.map +1 -1
  134. package/ts_build/src/services/modules/types.d.ts +6 -0
  135. package/ts_build/src/tunnel.d.ts +27 -0
  136. package/ts_build/src/tunnel.js +112 -0
  137. package/ts_build/src/tunnel.js.map +1 -0
  138. package/ts_build/src/types.d.ts +0 -1
  139. package/ts_build/src/types.js.map +1 -1
  140. package/ts_build/src/worker.d.ts +1 -4
  141. package/ts_build/src/worker.js +59 -227
  142. package/ts_build/src/worker.js.map +1 -1
  143. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  144. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  145. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  146. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  147. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  148. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  149. package/ts_build/src/workers/auth/types.d.ts +8 -1
  150. package/ts_build/src/workers/tools/index.d.ts +2 -0
  151. package/ts_build/src/workers/tools/index.js +4 -1
  152. package/ts_build/src/workers/tools/index.js.map +1 -1
  153. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  154. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  155. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  156. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  157. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  158. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
  159. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  160. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  161. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  162. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  163. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  164. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  165. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  166. package/src/agents/tools/executeScript/README.md +0 -94
  167. package/src/agents/tools/executeScript/definition.ts +0 -79
  168. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  169. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  170. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  171. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  172. package/src/agents/tools/executeScript/index.ts +0 -98
  173. package/src/services/script-execution/SandboxContext.ts +0 -282
  174. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  175. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  176. package/src/services/script-execution/ScriptTracer.ts +0 -249
  177. package/src/services/script-execution/types.ts +0 -134
  178. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  179. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  180. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  181. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  182. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  183. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  184. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  185. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  186. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  187. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  188. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  189. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  190. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  191. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  192. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  193. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  194. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  195. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  196. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  197. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  198. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  199. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  200. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  201. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  202. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  203. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  204. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  205. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  206. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  207. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  208. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  209. package/ts_build/src/services/script-execution/types.js +0 -3
  210. 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 } 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,6 +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";
21
+ import {
22
+ extractTunnelDomain,
23
+ resolveTunnelConfig,
24
+ connectTunnelWebSocket,
25
+ } from "./tunnel";
17
26
 
18
27
  const API_URL = KNOWHOW_API_URL;
19
28
 
@@ -90,6 +99,7 @@ export async function worker(options?: {
90
99
  noSandbox?: boolean;
91
100
  passkey?: boolean;
92
101
  passkeyReset?: boolean;
102
+ allowedTools?: string[];
93
103
  }) {
94
104
  const config = await getConfig();
95
105
 
@@ -209,9 +219,9 @@ export async function worker(options?: {
209
219
  console.log(`🖥️ Using host mode (${sandboxSource})`);
210
220
  }
211
221
 
212
- // Use the config we already loaded above
213
-
214
- 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)) {
215
225
  console.log(
216
226
  "Worker tools configured! Update knowhow.json to adjust which tools are allowed by the worker."
217
227
  );
@@ -229,7 +239,8 @@ export async function worker(options?: {
229
239
  return;
230
240
  }
231
241
 
232
- let toolsToUse = Tools.getToolsByNames(config.worker.allowedTools);
242
+ const resolvedToolNames = options?.allowedTools ?? config.worker!.allowedTools;
243
+ let toolsToUse = Tools.getToolsByNames(resolvedToolNames);
233
244
 
234
245
  // If passkey auth is enabled, wrap all tool functions to check locked state
235
246
  // and register the unlock/lock auth tools
@@ -264,6 +275,22 @@ export async function worker(options?: {
264
275
  console.log("🔑 Auth tools registered: unlock, lock");
265
276
  }
266
277
 
278
+ // Register the reloadConfig tool so agents can hot-reload MCPs/config
279
+ // without restarting the worker process.
280
+ // Uses a closure over `toolsToUse` so the tool can update it in-place.
281
+ const { reloadConfig, reloadConfigDefinition } = makeReloadConfigTool(
282
+ Mcp,
283
+ Tools,
284
+ mcpServer,
285
+ (newTools) => {
286
+ toolsToUse = newTools;
287
+ }
288
+ );
289
+ Tools.addFunctions({ reloadConfig });
290
+ toolsToUse = [...toolsToUse, reloadConfigDefinition];
291
+
292
+ console.log("🔄 reloadConfig tool registered");
293
+
267
294
  mcpServer.createServer(clientName, clientVersion).withTools(toolsToUse);
268
295
 
269
296
  let connected = false;
@@ -272,31 +299,14 @@ export async function worker(options?: {
272
299
  let lastJwt: string | null = null;
273
300
  let unauthorizedJwt: string | null = null;
274
301
 
275
- // Check if tunnel is enabled
276
- 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);
277
308
 
278
- // Determine localHost based on environment
279
- let tunnelLocalHost = config.worker?.tunnel?.localHost;
280
- if (!tunnelLocalHost) {
281
- // Auto-detect based on Docker environment
282
- if (isInsideDocker) {
283
- tunnelLocalHost = "host.docker.internal";
284
- console.log(
285
- "🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
286
- );
287
- } else {
288
- tunnelLocalHost = "127.0.0.1";
289
- }
290
- }
291
-
292
- // Check for port mapping configuration
293
- const portMapping = config.worker?.tunnel?.portMapping || {};
294
- if (Object.keys(portMapping).length > 0) {
295
- console.log("🔀 Port mapping configured:");
296
- for (const [containerPort, hostPort] of Object.entries(portMapping)) {
297
- console.log(` Container port ${containerPort} → Host port ${hostPort}`);
298
- }
299
- }
309
+ const { tunnelLocalHost, portMapping } = resolveTunnelConfig(config, isInsideDocker);
300
310
 
301
311
  if (tunnelEnabled) {
302
312
  const tunnelPorts = config.worker?.tunnel?.allowedPorts || [];
@@ -313,31 +323,6 @@ export async function worker(options?: {
313
323
  );
314
324
  }
315
325
 
316
- // Extract tunnel domain from API_URL
317
- // e.g., "https://api.knowhow.tyvm.ai" -> "knowhow.tyvm.ai"
318
- // e.g., "http://localhost:4000" -> "localhost:4000"
319
- function extractTunnelDomain(apiUrl: string): {
320
- domain: string;
321
- useHttps: boolean;
322
- } {
323
- try {
324
- const url = new URL(apiUrl);
325
- const useHttps = url.protocol === "https:";
326
-
327
- // For localhost, include port; for production, just use hostname
328
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
329
- return {
330
- domain: `worker.${url.hostname}:${url.port || "80"}`,
331
- useHttps,
332
- };
333
- }
334
- return { domain: `worker.${url.hostname}`, useHttps };
335
- } catch (err) {
336
- console.error("Failed to parse API_URL for tunnel domain:", err);
337
- return { domain: "worker.localhost:4000", useHttps: false }; // fallback
338
- }
339
- }
340
-
341
326
  async function connectWebSocket() {
342
327
  const jwt = await loadJwt();
343
328
  console.log(`Connecting to ${API_URL}`);
@@ -403,6 +388,38 @@ export async function worker(options?: {
403
388
  console.log(`✅ Worker ID recorded: ${parsed.workerId}`);
404
389
  }
405
390
  }
391
+
392
+ // Hot-reload: re-read config, reconnect MCPs, and rebuild the tool list
393
+ // without restarting the worker process.
394
+ if (parsed?.type === "reloadConfig") {
395
+ console.log(
396
+ "🔄 Received reloadConfig — reloading MCPs, modules and tools..."
397
+ );
398
+ try {
399
+ // Re-read fresh config from disk
400
+ const freshConfig = await getConfig();
401
+
402
+ // Close all existing MCP connections
403
+ await Mcp.closeAll();
404
+
405
+ // Reconnect from fresh config and re-register tools
406
+ await Mcp.connectToConfigured(Tools);
407
+
408
+ // Rebuild the allowed tools list from fresh config
409
+ const allowedToolNames =
410
+ freshConfig.worker?.allowedTools ?? Tools.getToolNames();
411
+ toolsToUse = Tools.getToolsByNames(allowedToolNames);
412
+
413
+ // Update the MCP server with new tool list
414
+ mcpServer.withTools(toolsToUse);
415
+
416
+ console.log(
417
+ `✅ Config reloaded: ${toolsToUse.length} tools active`
418
+ );
419
+ } catch (err) {
420
+ console.error("❌ Failed to reload config:", err);
421
+ }
422
+ }
406
423
  } catch {
407
424
  // Not our message — ignore parse errors
408
425
  }
@@ -412,90 +429,40 @@ export async function worker(options?: {
412
429
  // Create separate WebSocket connection for tunnel if enabled
413
430
  let tunnelConnection: WebSocket | null = null;
414
431
  if (tunnelEnabled) {
415
- tunnelConnection = new WebSocket(`${API_URL}/ws/tunnel`, {
432
+ tunnelConnection = connectTunnelWebSocket({
433
+ tunnelDomain,
434
+ tunnelUseHttps,
435
+ tunnelLocalHost,
436
+ portMapping,
437
+ config,
416
438
  headers,
417
- });
418
-
419
- tunnelConnection.on("open", () => {
420
- console.log("Tunnel WebSocket connected");
421
-
422
- // Get the allowedPorts configuration
423
- const allowedPorts = config.worker?.tunnel?.allowedPorts || [];
424
-
425
- // Create URL rewriter callback that returns the hostname (without protocol)
426
- // The tunnel package will add the protocol based on the useHttps config
427
- // This receives port and metadata from the tunnel request
428
- const urlRewriter = (port: number, metadata?: any) => {
429
- const workerId = metadata?.workerId;
430
- const secret = metadata?.secret;
431
-
432
- // Build the hostname/domain (without protocol) based on metadata
433
- // The tunnel handler will add the protocol using the useHttps config
434
- // Examples:
435
- // - secret-p3000.worker.example.com
436
- // - workerId-p3000.worker.example.com
437
- const subdomain = secret
438
- ? `${secret}-p${port}`
439
- : `${workerId}-p${port}`;
440
-
441
- // Return just the hostname - the tunnel package should add the protocol
442
- // based on the useHttps configuration passed below
443
- const replacementUrl = `${subdomain}.${tunnelDomain}`;
444
- return replacementUrl;
445
- };
446
-
447
- const tunnelConfig = {
448
- allowedPorts,
449
- maxConcurrentStreams:
450
- config.worker?.tunnel?.maxConcurrentStreams || 50,
451
- tunnelUseHttps,
452
- localHost: tunnelLocalHost,
453
- urlRewriter,
454
- enableUrlRewriting:
455
- config.worker?.tunnel?.enableUrlRewriting !== false,
456
- portMapping,
457
- logLevel: "debug" as const,
458
- };
459
-
460
- // Initialize tunnel handler with the tunnel-specific WebSocket
461
- // Pass useHttps flag so the tunnel package can add the correct protocol
462
- tunnelHandler = createTunnelHandler(tunnelConnection!, tunnelConfig);
463
- console.log("🌐 Tunnel handler initialized");
464
- console.log(tunnelConfig);
465
- });
466
-
467
- tunnelConnection.on("close", (code, reason) => {
468
- console.log(
469
- `Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`
470
- );
471
- if (code === 1008) {
472
- unauthorizedJwt = lastJwt;
473
- console.error(
474
- "❌ Tunnel received Unauthorized (1008). The JWT may be expired."
475
- );
476
- console.error(" Pausing reconnection until JWT changes...");
477
- } else {
478
- console.log(
479
- "Tunnel connection will reconnect on next connection cycle..."
480
- );
481
- }
482
-
483
- // Cleanup tunnel handler
484
- if (tunnelHandler) {
485
- tunnelHandler.cleanup();
486
- tunnelHandler = null;
487
- }
488
- tunnelWs = null;
489
-
490
- // Mark as disconnected to trigger reconnection
491
- // The tunnel websocket is separate but we should reconnect both
492
- connected = false;
493
- });
494
-
495
- tunnelConnection.on("error", (error) => {
496
- console.error("Tunnel WebSocket error:", error);
497
- // Mark as disconnected on error to trigger reconnection
498
- 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
+ },
499
466
  });
500
467
 
501
468
  tunnelWs = tunnelConnection;
@@ -578,177 +545,3 @@ export async function worker(options?: {
578
545
  await wait(5000);
579
546
  }
580
547
  }
581
-
582
- /**
583
- * Run tunnel-only mode: connects to the Knowhow tunnel WebSocket without
584
- * registering any MCP tools. Useful for users who only want the web tunnel
585
- * feature to expose local ports to the cloud.
586
- */
587
- export async function tunnel(options?: {
588
- share?: boolean;
589
- unshare?: boolean;
590
- }) {
591
- const config = await getConfig();
592
-
593
- const isInsideDocker = process.env.KNOWHOW_DOCKER === "true";
594
-
595
- // Determine localHost based on environment
596
- let tunnelLocalHost = config.worker?.tunnel?.localHost;
597
- if (!tunnelLocalHost) {
598
- if (isInsideDocker) {
599
- tunnelLocalHost = "host.docker.internal";
600
- console.log(
601
- "🐳 Docker detected: tunnel will use host.docker.internal to reach host services"
602
- );
603
- } else {
604
- tunnelLocalHost = "127.0.0.1";
605
- }
606
- }
607
-
608
- // Check for port mapping configuration
609
- const portMapping = config.worker?.tunnel?.portMapping || {};
610
- if (Object.keys(portMapping).length > 0) {
611
- console.log("🔀 Port mapping configured:");
612
- for (const [containerPort, hostPort] of Object.entries(portMapping)) {
613
- console.log(` Container port ${containerPort} → Host port ${hostPort}`);
614
- }
615
- }
616
-
617
- const tunnelPorts = config.worker?.tunnel?.allowedPorts || [];
618
- if (tunnelPorts.length === 0) {
619
- console.warn(
620
- "⚠️ No allowedPorts configured. Add worker.tunnel.allowedPorts to knowhow.json"
621
- );
622
- } else {
623
- console.log(`🌐 Tunnel mode for ports: ${tunnelPorts.join(", ")}`);
624
- }
625
-
626
- // Extract tunnel domain from API_URL
627
- function extractTunnelDomain(apiUrl: string): {
628
- domain: string;
629
- useHttps: boolean;
630
- } {
631
- try {
632
- const url = new URL(apiUrl);
633
- const useHttps = url.protocol === "https:";
634
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
635
- return {
636
- domain: `worker.${url.hostname}:${url.port || "80"}`,
637
- useHttps,
638
- };
639
- }
640
- return { domain: `worker.${url.hostname}`, useHttps };
641
- } catch (err) {
642
- console.error("Failed to parse API_URL for tunnel domain:", err);
643
- return { domain: "worker.localhost:4000", useHttps: false };
644
- }
645
- }
646
-
647
- let connected = false;
648
- let tunnelHandler: TunnelHandler | null = null;
649
- let lastJwt: string | null = null;
650
- let unauthorizedJwt: string | null = null;
651
-
652
- async function connectTunnel() {
653
- const jwt = await loadJwt();
654
- lastJwt = jwt;
655
- console.log(`Connecting tunnel to ${API_URL}`);
656
-
657
- const dir = process.cwd();
658
- const homedir = os.homedir();
659
- const hostname = process.env.WORKER_HOSTNAME || os.hostname();
660
- const root =
661
- process.env.WORKER_ROOT ||
662
- (dir === homedir ? "~" : dir.replace(homedir, "~"));
663
-
664
- const headers: Record<string, string> = {
665
- Authorization: `Bearer ${jwt}`,
666
- "User-Agent": `knowhow-tunnel/1.0.0/${hostname}`,
667
- Root: root,
668
- };
669
-
670
- if (options?.share) {
671
- headers.Shared = "true";
672
- console.log("🔓 Tunnel shared with organization");
673
- } else if (options?.unshare) {
674
- headers.Shared = "false";
675
- console.log("🔒 Tunnel is now private (unshared)");
676
- } else {
677
- console.log("🔒 Tunnel is private (only you can use it)");
678
- }
679
-
680
- const { domain: tunnelDomain, useHttps: tunnelUseHttps } =
681
- extractTunnelDomain(API_URL);
682
-
683
- const tunnelConnection = new WebSocket(`${API_URL}/ws/tunnel`, { headers });
684
-
685
- tunnelConnection.on("open", () => {
686
- console.log("🌐 Tunnel WebSocket connected");
687
- connected = true;
688
-
689
- const allowedPorts = config.worker?.tunnel?.allowedPorts || [];
690
- const urlRewriter = (port: number, metadata?: any) => {
691
- const workerId = metadata?.workerId;
692
- const secret = metadata?.secret;
693
- const subdomain = secret ? `${secret}-p${port}` : `${workerId}-p${port}`;
694
- return `${subdomain}.${tunnelDomain}`;
695
- };
696
-
697
- const tunnelConfig = {
698
- allowedPorts,
699
- maxConcurrentStreams: config.worker?.tunnel?.maxConcurrentStreams || 50,
700
- tunnelUseHttps,
701
- localHost: tunnelLocalHost,
702
- urlRewriter,
703
- enableUrlRewriting: config.worker?.tunnel?.enableUrlRewriting !== false,
704
- portMapping,
705
- logLevel: "debug" as const,
706
- };
707
-
708
- tunnelHandler = createTunnelHandler(tunnelConnection, tunnelConfig);
709
- console.log("🌐 Tunnel handler initialized");
710
- console.log(tunnelConfig);
711
- });
712
-
713
- tunnelConnection.on("close", (code, reason) => {
714
- console.log(`Tunnel WebSocket closed. Code: ${code}, Reason: ${reason.toString()}`);
715
- if (code === 1008) {
716
- unauthorizedJwt = lastJwt;
717
- console.error("❌ Tunnel received Unauthorized (1008). The JWT may be expired.");
718
- console.error(" Run 'knowhow login' to refresh your token, then restart.");
719
- console.error(" Pausing reconnection until JWT changes...");
720
- } else {
721
- console.log("Tunnel connection will reconnect on next cycle...");
722
- }
723
- if (tunnelHandler) {
724
- tunnelHandler.cleanup();
725
- tunnelHandler = null;
726
- }
727
- connected = false;
728
- });
729
-
730
- tunnelConnection.on("error", (error) => {
731
- console.error("Tunnel WebSocket error:", error);
732
- connected = false;
733
- });
734
-
735
- return tunnelConnection;
736
- }
737
-
738
- while (true) {
739
- if (!connected) {
740
- if (unauthorizedJwt !== null) {
741
- const currentJwt = await loadJwt().catch(() => null);
742
- if (currentJwt === unauthorizedJwt) {
743
- await wait(5000);
744
- continue;
745
- }
746
- console.log("🔄 JWT has changed, attempting to reconnect tunnel...");
747
- unauthorizedJwt = null;
748
- }
749
- console.log("Attempting to connect tunnel...");
750
- await connectTunnel();
751
- }
752
- await wait(5000);
753
- }
754
- }
@@ -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
+ }