@tyvm/knowhow 0.0.83 → 0.0.84

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/package.json +4 -2
  2. package/src/agents/base/base.ts +72 -62
  3. package/src/agents/index.ts +30 -14
  4. package/src/agents/tools/startAgentTask.ts +3 -1
  5. package/src/chat/CliChatService.ts +20 -4
  6. package/src/chat/modules/AgentModule.ts +399 -357
  7. package/src/chat/modules/CustomCommandsModule.ts +0 -1
  8. package/src/chat/modules/InternalChatModule.ts +18 -2
  9. package/src/chat/modules/RendererModule.ts +109 -0
  10. package/src/chat/modules/SessionsModule.ts +854 -0
  11. package/src/chat/modules/SetupModule.ts +6 -8
  12. package/src/chat/modules/index.ts +1 -0
  13. package/src/chat/renderer/CompactRenderer.ts +209 -0
  14. package/src/chat/renderer/ConsoleRenderer.ts +141 -0
  15. package/src/chat/renderer/FancyRenderer.ts +421 -0
  16. package/src/chat/renderer/index.ts +5 -0
  17. package/src/chat/renderer/loadRenderer.ts +314 -0
  18. package/src/chat/renderer/messagesToRenderEvents.ts +96 -0
  19. package/src/chat/renderer/types.ts +88 -0
  20. package/src/chat/types.ts +5 -0
  21. package/src/chat.ts +69 -5
  22. package/src/cli.ts +24 -5
  23. package/src/config.ts +15 -0
  24. package/src/plugins/AgentsMdPlugin.ts +1 -1
  25. package/src/plugins/GitPlugin.ts +20 -20
  26. package/src/plugins/PluginBase.ts +11 -0
  27. package/src/plugins/SkillsPlugin.ts +150 -0
  28. package/src/plugins/asana.ts +4 -4
  29. package/src/plugins/embedding.ts +3 -5
  30. package/src/plugins/exec.ts +3 -3
  31. package/src/plugins/figma.ts +3 -7
  32. package/src/plugins/github.ts +18 -29
  33. package/src/plugins/jira.ts +2 -2
  34. package/src/plugins/language.ts +4 -4
  35. package/src/plugins/linear.ts +4 -4
  36. package/src/plugins/notion.ts +6 -8
  37. package/src/plugins/plugins.ts +29 -3
  38. package/src/plugins/url.ts +2 -2
  39. package/src/plugins/vim.ts +4 -3
  40. package/src/services/AgentService.ts +17 -0
  41. package/src/services/AgentSyncFs.ts +3 -0
  42. package/src/services/EventService.ts +168 -27
  43. package/src/services/KnowhowClient.ts +1 -0
  44. package/src/services/SessionManager.ts +51 -1
  45. package/src/services/SyncedAgentWatcher.ts +397 -0
  46. package/src/services/SyncerService.ts +147 -0
  47. package/src/services/index.ts +2 -0
  48. package/src/services/modules/index.ts +14 -3
  49. package/src/types.ts +25 -0
  50. package/src/worker.ts +80 -2
  51. package/src/workers/auth/PasskeySetup.ts +185 -0
  52. package/src/workers/auth/WorkerPasskeyAuth.ts +190 -0
  53. package/src/workers/auth/types.ts +58 -0
  54. package/src/workers/tools/getChallenge.ts +33 -0
  55. package/src/workers/tools/index.ts +8 -0
  56. package/src/workers/tools/lock.ts +31 -0
  57. package/src/workers/tools/unlock.ts +116 -0
  58. package/tests/unit/modules/moduleLoading.test.ts +226 -0
  59. package/tests/unit/plugins/pluginLoading.test.ts +151 -0
  60. package/ts_build/package.json +4 -2
  61. package/ts_build/src/agents/base/base.d.ts +4 -3
  62. package/ts_build/src/agents/base/base.js +54 -30
  63. package/ts_build/src/agents/base/base.js.map +1 -1
  64. package/ts_build/src/agents/index.d.ts +3 -0
  65. package/ts_build/src/agents/index.js +21 -11
  66. package/ts_build/src/agents/index.js.map +1 -1
  67. package/ts_build/src/agents/tools/startAgentTask.js +2 -1
  68. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  69. package/ts_build/src/chat/CliChatService.js +16 -5
  70. package/ts_build/src/chat/CliChatService.js.map +1 -1
  71. package/ts_build/src/chat/modules/AgentModule.d.ts +34 -17
  72. package/ts_build/src/chat/modules/AgentModule.js +248 -258
  73. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  74. package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
  75. package/ts_build/src/chat/modules/InternalChatModule.d.ts +3 -0
  76. package/ts_build/src/chat/modules/InternalChatModule.js +16 -1
  77. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  78. package/ts_build/src/chat/modules/RendererModule.d.ts +16 -0
  79. package/ts_build/src/chat/modules/RendererModule.js +76 -0
  80. package/ts_build/src/chat/modules/RendererModule.js.map +1 -0
  81. package/ts_build/src/chat/modules/SessionsModule.d.ts +33 -0
  82. package/ts_build/src/chat/modules/SessionsModule.js +582 -0
  83. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -0
  84. package/ts_build/src/chat/modules/SetupModule.d.ts +3 -3
  85. package/ts_build/src/chat/modules/SetupModule.js +4 -6
  86. package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
  87. package/ts_build/src/chat/modules/index.d.ts +1 -0
  88. package/ts_build/src/chat/modules/index.js +3 -1
  89. package/ts_build/src/chat/modules/index.js.map +1 -1
  90. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +23 -0
  91. package/ts_build/src/chat/renderer/CompactRenderer.js +167 -0
  92. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -0
  93. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +22 -0
  94. package/ts_build/src/chat/renderer/ConsoleRenderer.js +110 -0
  95. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -0
  96. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +23 -0
  97. package/ts_build/src/chat/renderer/FancyRenderer.js +328 -0
  98. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -0
  99. package/ts_build/src/chat/renderer/index.d.ts +5 -0
  100. package/ts_build/src/chat/renderer/index.js +29 -0
  101. package/ts_build/src/chat/renderer/index.js.map +1 -0
  102. package/ts_build/src/chat/renderer/loadRenderer.d.ts +4 -0
  103. package/ts_build/src/chat/renderer/loadRenderer.js +246 -0
  104. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -0
  105. package/ts_build/src/chat/renderer/messagesToRenderEvents.d.ts +15 -0
  106. package/ts_build/src/chat/renderer/messagesToRenderEvents.js +72 -0
  107. package/ts_build/src/chat/renderer/messagesToRenderEvents.js.map +1 -0
  108. package/ts_build/src/chat/renderer/types.d.ts +75 -0
  109. package/ts_build/src/chat/renderer/types.js +3 -0
  110. package/ts_build/src/chat/renderer/types.js.map +1 -0
  111. package/ts_build/src/chat/types.d.ts +5 -0
  112. package/ts_build/src/chat.js +46 -4
  113. package/ts_build/src/chat.js.map +1 -1
  114. package/ts_build/src/cli.js +18 -5
  115. package/ts_build/src/cli.js.map +1 -1
  116. package/ts_build/src/config.d.ts +1 -0
  117. package/ts_build/src/config.js +17 -1
  118. package/ts_build/src/config.js.map +1 -1
  119. package/ts_build/src/plugins/AgentsMdPlugin.js +1 -1
  120. package/ts_build/src/plugins/AgentsMdPlugin.js.map +1 -1
  121. package/ts_build/src/plugins/GitPlugin.js +20 -20
  122. package/ts_build/src/plugins/GitPlugin.js.map +1 -1
  123. package/ts_build/src/plugins/PluginBase.d.ts +1 -0
  124. package/ts_build/src/plugins/PluginBase.js +13 -0
  125. package/ts_build/src/plugins/PluginBase.js.map +1 -1
  126. package/ts_build/src/plugins/SkillsPlugin.d.ts +13 -0
  127. package/ts_build/src/plugins/SkillsPlugin.js +149 -0
  128. package/ts_build/src/plugins/SkillsPlugin.js.map +1 -0
  129. package/ts_build/src/plugins/asana.js +4 -4
  130. package/ts_build/src/plugins/asana.js.map +1 -1
  131. package/ts_build/src/plugins/embedding.js +3 -3
  132. package/ts_build/src/plugins/embedding.js.map +1 -1
  133. package/ts_build/src/plugins/exec.js +3 -3
  134. package/ts_build/src/plugins/exec.js.map +1 -1
  135. package/ts_build/src/plugins/figma.js +3 -3
  136. package/ts_build/src/plugins/figma.js.map +1 -1
  137. package/ts_build/src/plugins/github.js +18 -18
  138. package/ts_build/src/plugins/github.js.map +1 -1
  139. package/ts_build/src/plugins/jira.js +2 -2
  140. package/ts_build/src/plugins/jira.js.map +1 -1
  141. package/ts_build/src/plugins/language.js +4 -4
  142. package/ts_build/src/plugins/language.js.map +1 -1
  143. package/ts_build/src/plugins/linear.js +4 -4
  144. package/ts_build/src/plugins/linear.js.map +1 -1
  145. package/ts_build/src/plugins/notion.js +6 -6
  146. package/ts_build/src/plugins/notion.js.map +1 -1
  147. package/ts_build/src/plugins/plugins.d.ts +3 -0
  148. package/ts_build/src/plugins/plugins.js +18 -3
  149. package/ts_build/src/plugins/plugins.js.map +1 -1
  150. package/ts_build/src/plugins/url.js +2 -2
  151. package/ts_build/src/plugins/url.js.map +1 -1
  152. package/ts_build/src/plugins/vim.js +2 -2
  153. package/ts_build/src/plugins/vim.js.map +1 -1
  154. package/ts_build/src/services/AgentService.d.ts +3 -0
  155. package/ts_build/src/services/AgentService.js +7 -0
  156. package/ts_build/src/services/AgentService.js.map +1 -1
  157. package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
  158. package/ts_build/src/services/AgentSyncFs.js +2 -0
  159. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  160. package/ts_build/src/services/EventService.d.ts +25 -2
  161. package/ts_build/src/services/EventService.js +92 -14
  162. package/ts_build/src/services/EventService.js.map +1 -1
  163. package/ts_build/src/services/KnowhowClient.d.ts +1 -0
  164. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  165. package/ts_build/src/services/SessionManager.d.ts +6 -0
  166. package/ts_build/src/services/SessionManager.js +39 -1
  167. package/ts_build/src/services/SessionManager.js.map +1 -1
  168. package/ts_build/src/services/SyncedAgentWatcher.d.ts +101 -0
  169. package/ts_build/src/services/SyncedAgentWatcher.js +312 -0
  170. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -0
  171. package/ts_build/src/services/SyncerService.d.ts +30 -0
  172. package/ts_build/src/services/SyncerService.js +72 -0
  173. package/ts_build/src/services/SyncerService.js.map +1 -0
  174. package/ts_build/src/services/index.d.ts +2 -0
  175. package/ts_build/src/services/index.js +2 -0
  176. package/ts_build/src/services/index.js.map +1 -1
  177. package/ts_build/src/services/modules/index.js +10 -2
  178. package/ts_build/src/services/modules/index.js.map +1 -1
  179. package/ts_build/src/types.d.ts +19 -0
  180. package/ts_build/src/types.js.map +1 -1
  181. package/ts_build/src/worker.d.ts +2 -0
  182. package/ts_build/src/worker.js +59 -4
  183. package/ts_build/src/worker.js.map +1 -1
  184. package/ts_build/src/workers/auth/PasskeySetup.d.ts +10 -0
  185. package/ts_build/src/workers/auth/PasskeySetup.js +131 -0
  186. package/ts_build/src/workers/auth/PasskeySetup.js.map +1 -0
  187. package/ts_build/src/workers/auth/WorkerPasskeyAuth.d.ts +35 -0
  188. package/ts_build/src/workers/auth/WorkerPasskeyAuth.js +129 -0
  189. package/ts_build/src/workers/auth/WorkerPasskeyAuth.js.map +1 -0
  190. package/ts_build/src/workers/auth/types.d.ts +36 -0
  191. package/ts_build/src/workers/auth/types.js +3 -0
  192. package/ts_build/src/workers/auth/types.js.map +1 -0
  193. package/ts_build/src/workers/tools/getChallenge.d.ts +9 -0
  194. package/ts_build/src/workers/tools/getChallenge.js +27 -0
  195. package/ts_build/src/workers/tools/getChallenge.js.map +1 -0
  196. package/ts_build/src/workers/tools/index.d.ts +6 -0
  197. package/ts_build/src/workers/tools/index.js +10 -0
  198. package/ts_build/src/workers/tools/index.js.map +1 -1
  199. package/ts_build/src/workers/tools/lock.d.ts +9 -0
  200. package/ts_build/src/workers/tools/lock.js +27 -0
  201. package/ts_build/src/workers/tools/lock.js.map +1 -0
  202. package/ts_build/src/workers/tools/unlock.d.ts +18 -0
  203. package/ts_build/src/workers/tools/unlock.js +78 -0
  204. package/ts_build/src/workers/tools/unlock.js.map +1 -0
  205. package/ts_build/tests/unit/modules/moduleLoading.test.d.ts +1 -0
  206. package/ts_build/tests/unit/modules/moduleLoading.test.js +187 -0
  207. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -0
  208. package/ts_build/tests/unit/plugins/pluginLoading.test.d.ts +1 -0
  209. package/ts_build/tests/unit/plugins/pluginLoading.test.js +123 -0
  210. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -0
package/src/worker.ts CHANGED
@@ -4,6 +4,9 @@ import { createTunnelHandler, 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
+ import { PasskeySetupService } from "./workers/auth/PasskeySetup";
8
+ import { WorkerPasskeyAuthService } from "./workers/auth/WorkerPasskeyAuth";
9
+ import { makeUnlockTool, makeLockTool } from "./workers/tools";
7
10
  import { McpServerService } from "./services/Mcp";
8
11
  import * as allTools from "./agents/tools";
9
12
  import workerTools from "./workers/tools";
@@ -85,9 +88,33 @@ export async function worker(options?: {
85
88
  unshare?: boolean;
86
89
  sandbox?: boolean;
87
90
  noSandbox?: boolean;
91
+ passkey?: boolean;
92
+ passkeyReset?: boolean;
88
93
  }) {
89
94
  const config = await getConfig();
90
95
 
96
+ // Handle --passkey-reset: remove passkey from config
97
+ if (options?.passkeyReset) {
98
+ const passkeySetup = new PasskeySetupService();
99
+ await passkeySetup.reset();
100
+ return;
101
+ }
102
+
103
+ // Handle --passkey: run browser-based passkey registration flow
104
+ if (options?.passkey) {
105
+ let jwt: string;
106
+ try {
107
+ jwt = await loadJwt();
108
+ } catch {
109
+ console.error("❌ You must be logged in to set up a passkey.");
110
+ console.error(" Run 'knowhow login' first.");
111
+ process.exit(1);
112
+ }
113
+ const passkeySetup = new PasskeySetupService();
114
+ await passkeySetup.setup(jwt);
115
+ return;
116
+ }
117
+
91
118
  // Check if we're already running inside a Docker container
92
119
  const isInsideDocker = process.env.KNOWHOW_DOCKER === "true";
93
120
 
@@ -158,6 +185,24 @@ export async function worker(options?: {
158
185
  const clientName = "knowhow-worker";
159
186
  const clientVersion = "1.1.1";
160
187
 
188
+ // ---------------------------------------------------------------------------
189
+ // Passkey auth gating
190
+ // ---------------------------------------------------------------------------
191
+ let authService: WorkerPasskeyAuthService | null = null;
192
+ const passkeyConfig = config.worker?.auth?.passkey;
193
+
194
+ if (passkeyConfig?.publicKey && passkeyConfig?.credentialId) {
195
+ authService = new WorkerPasskeyAuthService(
196
+ {
197
+ publicKey: passkeyConfig.publicKey,
198
+ credentialId: passkeyConfig.credentialId,
199
+ algorithm: -7, // ES256
200
+ },
201
+ config.worker?.auth?.sessionDurationHours ?? 3
202
+ );
203
+ console.log("🔒 Passkey auth enabled — worker starts locked");
204
+ }
205
+
161
206
  if (!shouldUseSandbox) {
162
207
  console.log(`🖥️ Using host mode (${sandboxSource})`);
163
208
  }
@@ -172,7 +217,6 @@ export async function worker(options?: {
172
217
  ...config.worker,
173
218
  allowedTools: Tools.getToolNames(),
174
219
  };
175
-
176
220
  await updateConfig(config);
177
221
  return;
178
222
  }
@@ -183,7 +227,41 @@ export async function worker(options?: {
183
227
  return;
184
228
  }
185
229
 
186
- const toolsToUse = Tools.getToolsByNames(config.worker.allowedTools);
230
+ let toolsToUse = Tools.getToolsByNames(config.worker.allowedTools);
231
+
232
+ // If passkey auth is enabled, wrap all tool functions to check locked state
233
+ // and register the unlock/lock auth tools
234
+ if (authService) {
235
+ const _authService = authService;
236
+
237
+ // Wrap every configured tool to gate on locked state
238
+ for (const tool of toolsToUse) {
239
+ const toolName = tool.function.name;
240
+ const originalFn = Tools.getFunction(toolName);
241
+ Tools.addFunctions({
242
+ [toolName]: async (...args: any[]) => {
243
+ if (_authService.isLocked()) {
244
+ return {
245
+ error: "WORKER_LOCKED",
246
+ message:
247
+ "Worker is locked. Call the `unlock` tool with your passkey assertion to unlock it first.",
248
+ };
249
+ }
250
+ return originalFn(...args);
251
+ },
252
+ });
253
+ }
254
+
255
+ // Build and register the auth tools
256
+ const { unlock, unlockDefinition } = makeUnlockTool(_authService);
257
+ const { lock, lockDefinition } = makeLockTool(_authService);
258
+
259
+ Tools.addFunctions({ unlock, lock });
260
+ toolsToUse = [unlockDefinition, lockDefinition, ...toolsToUse];
261
+
262
+ console.log("🔑 Auth tools registered: unlock, lock");
263
+ }
264
+
187
265
  mcpServer.createServer(clientName, clientVersion).withTools(toolsToUse);
188
266
 
189
267
  let connected = false;
@@ -0,0 +1,185 @@
1
+ import axios from "axios";
2
+ import { KNOWHOW_API_URL } from "../../services/KnowhowClient";
3
+ import { openBrowser } from "../../auth/browserLogin";
4
+ import { Spinner } from "../../auth/spinner";
5
+ import { getConfig, updateConfig } from "../../config";
6
+ import { PasskeySetupSession, PasskeySetupStatus, PasskeyCredential } from "./types";
7
+
8
+ /**
9
+ * Service that handles the CLI-side passkey setup flow.
10
+ *
11
+ * Flow:
12
+ * 1. POST /api/worker/passkey/setup/session → get sessionId + browserUrl
13
+ * 2. Open browser to browserUrl
14
+ * 3. Poll /api/worker/passkey/setup/status/:sessionId until status === 'complete'
15
+ * 4. Save the returned credential to local config
16
+ */
17
+ export class PasskeySetupService {
18
+ private baseUrl: string;
19
+
20
+ constructor(baseUrl: string = KNOWHOW_API_URL) {
21
+ if (!baseUrl) {
22
+ throw new Error("KNOWHOW_API_URL environment variable not set");
23
+ }
24
+ this.baseUrl = baseUrl;
25
+ }
26
+
27
+ /**
28
+ * Run the full passkey setup flow.
29
+ */
30
+ async setup(jwt: string): Promise<void> {
31
+ const spinner = new Spinner();
32
+
33
+ try {
34
+ spinner.start("Creating passkey setup session");
35
+ const session = await this.createSetupSession(jwt);
36
+ spinner.stop();
37
+
38
+ await openBrowser(session.browserUrl);
39
+ console.log(
40
+ `\nIf the browser didn't open automatically, please visit:\n ${session.browserUrl}\n`
41
+ );
42
+
43
+ spinner.start("Waiting for passkey registration in browser…");
44
+
45
+ const credential = await this.pollForCompletion(session.sessionId, spinner);
46
+
47
+ spinner.stop();
48
+ spinner.start("Saving passkey credential to config");
49
+
50
+ await this.saveCredential(credential);
51
+
52
+ spinner.stop();
53
+ console.log("✅ Passkey registered successfully!");
54
+ console.log(
55
+ " Worker will now require passkey authentication for new connections."
56
+ );
57
+ } catch (error) {
58
+ spinner.stop();
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Remove passkey requirement from config.
65
+ */
66
+ async reset(): Promise<void> {
67
+ const config = await getConfig();
68
+
69
+ if (!config.worker?.auth?.passkey) {
70
+ console.log("ℹ️ No passkey configured for this worker.");
71
+ return;
72
+ }
73
+
74
+ const updatedConfig = {
75
+ ...config,
76
+ worker: {
77
+ ...config.worker,
78
+ auth: {
79
+ ...config.worker.auth,
80
+ required: false,
81
+ passkey: undefined,
82
+ },
83
+ },
84
+ };
85
+
86
+ await updateConfig(updatedConfig);
87
+ console.log("✅ Passkey requirement removed from config.");
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Private helpers
92
+ // ---------------------------------------------------------------------------
93
+
94
+ private async createSetupSession(jwt: string): Promise<PasskeySetupSession> {
95
+ try {
96
+ const response = await axios.post<PasskeySetupSession>(
97
+ `${this.baseUrl}/api/worker/passkey/setup/session`,
98
+ {},
99
+ {
100
+ headers: { Authorization: `Bearer ${jwt}` },
101
+ timeout: 10000,
102
+ }
103
+ );
104
+ return response.data;
105
+ } catch (error) {
106
+ if (axios.isAxiosError(error)) {
107
+ throw new Error(
108
+ `Failed to create passkey setup session: ${
109
+ error.response?.data?.message || error.message
110
+ }`
111
+ );
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ private async pollForCompletion(
118
+ sessionId: string,
119
+ spinner: Spinner
120
+ ): Promise<PasskeyCredential> {
121
+ const maxAttempts = 60; // 5 minutes at 5-second intervals
122
+ let attempt = 0;
123
+
124
+ while (attempt < maxAttempts) {
125
+ attempt++;
126
+
127
+ try {
128
+ const response = await axios.get<PasskeySetupStatus>(
129
+ `${this.baseUrl}/api/worker/passkey/setup/status/${sessionId}`,
130
+ { timeout: 10000 }
131
+ );
132
+
133
+ const { status, credential } = response.data;
134
+
135
+ if (status === "complete" && credential) {
136
+ return credential;
137
+ } else if (status === "expired") {
138
+ throw new Error(
139
+ "Passkey setup session expired. Please run 'knowhow worker --passkey' again."
140
+ );
141
+ }
142
+ } catch (error) {
143
+ if (axios.isAxiosError(error) && error.code === "ECONNABORTED") {
144
+ // Timeout — keep polling
145
+ } else if (!(error instanceof Error && error.message.includes("expired"))) {
146
+ // Re-throw non-timeout, non-expected errors
147
+ throw error;
148
+ } else {
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ await this.sleep(5000);
154
+ }
155
+
156
+ throw new Error("Passkey setup timed out. Please try again.");
157
+ }
158
+
159
+ private async saveCredential(credential: PasskeyCredential): Promise<void> {
160
+ const config = await getConfig();
161
+
162
+ const updatedConfig = {
163
+ ...config,
164
+ worker: {
165
+ ...config.worker,
166
+ auth: {
167
+ ...config.worker?.auth,
168
+ required: true,
169
+ passkey: {
170
+ publicKey: credential.publicKey,
171
+ credentialId: credential.credentialId,
172
+ algorithm: credential.algorithm,
173
+ },
174
+ sessionDurationHours: config.worker?.auth?.sessionDurationHours ?? 3,
175
+ },
176
+ },
177
+ };
178
+
179
+ await updateConfig(updatedConfig);
180
+ }
181
+
182
+ private async sleep(ms: number): Promise<void> {
183
+ return new Promise((resolve) => setTimeout(resolve, ms));
184
+ }
185
+ }
@@ -0,0 +1,190 @@
1
+ import crypto from "crypto";
2
+ import { verifyAuthenticationResponse } from "@simplewebauthn/server";
3
+
4
+ /**
5
+ * Manages the locked/unlocked state of the worker's passkey auth.
6
+ * When the worker has passkey configured, it starts locked.
7
+ * The user must call `unlock` with a valid WebAuthn assertion to unlock.
8
+ * The worker verifies the signature locally using the stored public key.
9
+ */
10
+
11
+ export interface PasskeyConfig {
12
+ publicKey: string; // base64url-encoded COSE public key
13
+ credentialId: string; // base64url-encoded credential ID
14
+ algorithm: number; // e.g. -7 for ES256
15
+ }
16
+
17
+ export interface UnlockParams {
18
+ /** base64url-encoded signature from WebAuthn assertion */
19
+ signature: string;
20
+ /** base64url-encoded credential ID */
21
+ credentialId: string;
22
+ /** base64url-encoded authenticatorData */
23
+ authenticatorData: string;
24
+ /** base64url-encoded clientDataJSON */
25
+ clientDataJSON: string;
26
+ /** The challenge that was signed (base64url) */
27
+ challenge: string;
28
+ }
29
+
30
+ export class WorkerPasskeyAuthService {
31
+ private locked = true;
32
+ private sessionExpiry: number | null = null;
33
+ private sessionDurationMs: number;
34
+ // Pending challenge: base64url
35
+ private pendingChallenge: string | null = null;
36
+ private pendingChallengeExpiry: number | null = null;
37
+ private readonly CHALLENGE_TTL_MS = 5 * 60 * 1000; // 5 minutes
38
+
39
+ constructor(
40
+ private passkeyConfig: PasskeyConfig,
41
+ sessionDurationHours: number = 3
42
+ ) {
43
+ this.sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
44
+ }
45
+
46
+ isLocked(): boolean {
47
+ if (!this.locked) {
48
+ // Check if session has expired
49
+ if (this.sessionExpiry && Date.now() > this.sessionExpiry) {
50
+ console.log("🔒 Passkey session expired, locking worker");
51
+ this.locked = true;
52
+ this.sessionExpiry = null;
53
+ }
54
+ }
55
+ return this.locked;
56
+ }
57
+
58
+ /**
59
+ * Generate a new challenge for the client to sign.
60
+ * Returns base64url-encoded challenge bytes.
61
+ */
62
+ generateChallenge(): string {
63
+ const challengeBytes = crypto.randomBytes(32);
64
+ const challenge = challengeBytes.toString("base64url");
65
+ this.pendingChallenge = challenge;
66
+ this.pendingChallengeExpiry = Date.now() + this.CHALLENGE_TTL_MS;
67
+ return challenge;
68
+ }
69
+
70
+ /**
71
+ * Attempt to unlock the worker by verifying a WebAuthn assertion.
72
+ * Returns true if the signature is valid and the worker is now unlocked.
73
+ */
74
+ async unlock(params: UnlockParams): Promise<{ success: boolean; reason?: string }> {
75
+ // Verify the challenge matches what we issued
76
+ if (!this.pendingChallenge) {
77
+ return { success: false, reason: "No pending challenge. Call getChallenge first." };
78
+ }
79
+ if (this.pendingChallengeExpiry && Date.now() > this.pendingChallengeExpiry) {
80
+ this.pendingChallenge = null;
81
+ this.pendingChallengeExpiry = null;
82
+ return { success: false, reason: "Challenge expired. Please request a new challenge." };
83
+ }
84
+
85
+ // Verify the challenge in clientDataJSON matches our pending challenge
86
+ let clientData: { type: string; challenge: string; origin: string };
87
+ try {
88
+ const clientDataBytes = Buffer.from(params.clientDataJSON, "base64url");
89
+ clientData = JSON.parse(clientDataBytes.toString("utf8"));
90
+ } catch {
91
+ return { success: false, reason: "Invalid clientDataJSON" };
92
+ }
93
+
94
+ // The challenge in clientDataJSON is base64url-encoded
95
+ if (clientData.challenge !== this.pendingChallenge) {
96
+ return { success: false, reason: "Challenge mismatch" };
97
+ }
98
+
99
+ if (clientData.type !== "webauthn.get") {
100
+ return { success: false, reason: "Invalid clientData type" };
101
+ }
102
+
103
+ // Verify credential ID matches our stored credential
104
+ if (params.credentialId !== this.passkeyConfig.credentialId) {
105
+ return { success: false, reason: "Unknown credential" };
106
+ }
107
+
108
+ // Verify the signature using @simplewebauthn/server
109
+ const valid = await this.verifySignature(params, clientData.origin);
110
+ if (!valid) {
111
+ return { success: false, reason: "Invalid signature" };
112
+ }
113
+
114
+ // Unlock!
115
+ this.locked = false;
116
+ this.sessionExpiry = Date.now() + this.sessionDurationMs;
117
+ this.pendingChallenge = null;
118
+ this.pendingChallengeExpiry = null;
119
+
120
+ const expiresAt = new Date(this.sessionExpiry).toISOString();
121
+ console.log(`🔓 Worker unlocked! Session expires at ${expiresAt}`);
122
+ return { success: true };
123
+ }
124
+
125
+ lock(): void {
126
+ this.locked = true;
127
+ this.sessionExpiry = null;
128
+ this.pendingChallenge = null;
129
+ this.pendingChallengeExpiry = null;
130
+ console.log("🔒 Worker locked");
131
+ }
132
+
133
+ getSessionInfo() {
134
+ if (this.locked || !this.sessionExpiry) {
135
+ return { locked: true, expiresAt: null };
136
+ }
137
+ return {
138
+ locked: false,
139
+ expiresAt: new Date(this.sessionExpiry).toISOString(),
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Returns the credential ID stored in the passkey config.
145
+ */
146
+ getCredentialId(): string {
147
+ return this.passkeyConfig.credentialId;
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Signature verification via @simplewebauthn/server
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Verify a WebAuthn assertion using @simplewebauthn/server's verifyAuthenticationResponse.
156
+ * This handles all COSE key format complexity automatically.
157
+ */
158
+ private async verifySignature(params: UnlockParams, origin: string): Promise<boolean> {
159
+ try {
160
+ const { verified } = await verifyAuthenticationResponse({
161
+ response: {
162
+ id: params.credentialId,
163
+ rawId: params.credentialId,
164
+ response: {
165
+ authenticatorData: params.authenticatorData,
166
+ clientDataJSON: params.clientDataJSON,
167
+ signature: params.signature,
168
+ },
169
+ type: "public-key",
170
+ clientExtensionResults: {},
171
+ },
172
+ expectedChallenge: this.pendingChallenge!,
173
+ expectedOrigin: origin,
174
+ expectedRPID: new URL(origin).hostname,
175
+ credential: {
176
+ id: this.passkeyConfig.credentialId,
177
+ publicKey: Buffer.from(this.passkeyConfig.publicKey, "base64url"),
178
+ counter: 0,
179
+ transports: undefined,
180
+ },
181
+ requireUserVerification: false,
182
+ });
183
+
184
+ return verified;
185
+ } catch (err) {
186
+ console.error("Signature verification error:", err);
187
+ return false;
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Auth message types for the worker WebSocket authentication protocol.
3
+ */
4
+
5
+ // Worker → Client: challenge
6
+ export interface AuthChallengeMessage {
7
+ type: "auth:challenge";
8
+ challenge: string; // base64-encoded 32 random bytes
9
+ timestamp: number; // epoch seconds
10
+ }
11
+
12
+ // Client → Worker: response
13
+ export interface AuthResponseMessage {
14
+ type: "auth:response";
15
+ challenge: string; // echo back
16
+ signature: string; // base64-encoded WebAuthn assertion signature
17
+ credentialId: string; // base64-encoded credential ID
18
+ authenticatorData: string; // base64-encoded authenticator data
19
+ clientDataJSON: string; // base64-encoded client data JSON
20
+ }
21
+
22
+ // Worker → Client: success
23
+ export interface AuthSuccessMessage {
24
+ type: "auth:success";
25
+ token: string; // opaque session token
26
+ expiresAt: number; // epoch seconds
27
+ }
28
+
29
+ // Worker → Client: failure
30
+ export interface AuthFailureMessage {
31
+ type: "auth:failure";
32
+ reason: "invalid_signature" | "expired" | "unknown_credential";
33
+ }
34
+
35
+ export type AuthMessage =
36
+ | AuthChallengeMessage
37
+ | AuthResponseMessage
38
+ | AuthSuccessMessage
39
+ | AuthFailureMessage;
40
+
41
+ // Passkey credential stored in config
42
+ export interface PasskeyCredential {
43
+ publicKey: string; // base64-encoded public key
44
+ credentialId: string; // base64-encoded credential ID
45
+ algorithm: string; // e.g. "ES256"
46
+ }
47
+
48
+ // Setup session returned by knowhow-web
49
+ export interface PasskeySetupSession {
50
+ sessionId: string;
51
+ browserUrl: string;
52
+ }
53
+
54
+ // Status of a passkey setup session
55
+ export interface PasskeySetupStatus {
56
+ status: "pending" | "complete" | "expired";
57
+ credential?: PasskeyCredential;
58
+ }
@@ -0,0 +1,33 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { WorkerPasskeyAuthService } from "../auth/WorkerPasskeyAuth";
3
+
4
+ /**
5
+ * Returns a challenge for the client to sign with their passkey.
6
+ * The challenge must be signed and passed to the `unlock` tool.
7
+ */
8
+ export function makeGetChallengeTool(authService: WorkerPasskeyAuthService) {
9
+ async function getChallenge(): Promise<{ challenge: string; message: string }> {
10
+ const challenge = authService.generateChallenge();
11
+ return {
12
+ challenge,
13
+ message:
14
+ "Sign this challenge with your passkey and call the `unlock` tool with the assertion data.",
15
+ };
16
+ }
17
+
18
+ const getChallengeDefinition: Tool = {
19
+ type: "function" as const,
20
+ function: {
21
+ name: "getChallenge",
22
+ description:
23
+ "Get a challenge string to sign with your passkey. Required before calling `unlock`. Returns a base64url-encoded challenge.",
24
+ parameters: {
25
+ type: "object",
26
+ properties: {},
27
+ required: [],
28
+ },
29
+ },
30
+ };
31
+
32
+ return { getChallenge, getChallengeDefinition };
33
+ }
@@ -1,9 +1,17 @@
1
1
  export * from "./listAllowedPorts";
2
+ export * from "./getChallenge";
3
+ export * from "./unlock";
4
+ export * from "./lock";
5
+
2
6
  import {
3
7
  listAllowedPorts,
4
8
  listAllowedPortsDefinition,
5
9
  } from "./listAllowedPorts";
6
10
 
11
+ export { makeGetChallengeTool } from "./getChallenge";
12
+ export { makeUnlockTool } from "./unlock";
13
+ export { makeLockTool } from "./lock";
14
+
7
15
  export default {
8
16
  tools: { listAllowedPorts },
9
17
  definitions: [listAllowedPortsDefinition],
@@ -0,0 +1,31 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { WorkerPasskeyAuthService } from "../auth/WorkerPasskeyAuth";
3
+
4
+ /**
5
+ * Re-locks the worker, requiring passkey authentication again for further tool access.
6
+ */
7
+ export function makeLockTool(authService: WorkerPasskeyAuthService) {
8
+ async function lock(): Promise<{ success: boolean; message: string }> {
9
+ authService.lock();
10
+ return {
11
+ success: true,
12
+ message: "Worker locked. Call getChallenge and unlock to regain access.",
13
+ };
14
+ }
15
+
16
+ const lockDefinition: Tool = {
17
+ type: "function" as const,
18
+ function: {
19
+ name: "lock",
20
+ description:
21
+ "Lock the worker. After locking, only getChallenge, unlock, and lock tools will be accessible until the worker is unlocked again with a valid passkey assertion.",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {},
25
+ required: [],
26
+ },
27
+ },
28
+ };
29
+
30
+ return { lock, lockDefinition };
31
+ }