@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.
- package/package.json +4 -2
- package/src/agents/base/base.ts +72 -62
- package/src/agents/index.ts +30 -14
- package/src/agents/tools/startAgentTask.ts +3 -1
- package/src/chat/CliChatService.ts +20 -4
- package/src/chat/modules/AgentModule.ts +399 -357
- package/src/chat/modules/CustomCommandsModule.ts +0 -1
- package/src/chat/modules/InternalChatModule.ts +18 -2
- package/src/chat/modules/RendererModule.ts +109 -0
- package/src/chat/modules/SessionsModule.ts +854 -0
- package/src/chat/modules/SetupModule.ts +6 -8
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/renderer/CompactRenderer.ts +209 -0
- package/src/chat/renderer/ConsoleRenderer.ts +141 -0
- package/src/chat/renderer/FancyRenderer.ts +421 -0
- package/src/chat/renderer/index.ts +5 -0
- package/src/chat/renderer/loadRenderer.ts +314 -0
- package/src/chat/renderer/messagesToRenderEvents.ts +96 -0
- package/src/chat/renderer/types.ts +88 -0
- package/src/chat/types.ts +5 -0
- package/src/chat.ts +69 -5
- package/src/cli.ts +24 -5
- package/src/config.ts +15 -0
- package/src/plugins/AgentsMdPlugin.ts +1 -1
- package/src/plugins/GitPlugin.ts +20 -20
- package/src/plugins/PluginBase.ts +11 -0
- package/src/plugins/SkillsPlugin.ts +150 -0
- package/src/plugins/asana.ts +4 -4
- package/src/plugins/embedding.ts +3 -5
- package/src/plugins/exec.ts +3 -3
- package/src/plugins/figma.ts +3 -7
- package/src/plugins/github.ts +18 -29
- package/src/plugins/jira.ts +2 -2
- package/src/plugins/language.ts +4 -4
- package/src/plugins/linear.ts +4 -4
- package/src/plugins/notion.ts +6 -8
- package/src/plugins/plugins.ts +29 -3
- package/src/plugins/url.ts +2 -2
- package/src/plugins/vim.ts +4 -3
- package/src/services/AgentService.ts +17 -0
- package/src/services/AgentSyncFs.ts +3 -0
- package/src/services/EventService.ts +168 -27
- package/src/services/KnowhowClient.ts +1 -0
- package/src/services/SessionManager.ts +51 -1
- package/src/services/SyncedAgentWatcher.ts +397 -0
- package/src/services/SyncerService.ts +147 -0
- package/src/services/index.ts +2 -0
- package/src/services/modules/index.ts +14 -3
- package/src/types.ts +25 -0
- package/src/worker.ts +80 -2
- package/src/workers/auth/PasskeySetup.ts +185 -0
- package/src/workers/auth/WorkerPasskeyAuth.ts +190 -0
- package/src/workers/auth/types.ts +58 -0
- package/src/workers/tools/getChallenge.ts +33 -0
- package/src/workers/tools/index.ts +8 -0
- package/src/workers/tools/lock.ts +31 -0
- package/src/workers/tools/unlock.ts +116 -0
- package/tests/unit/modules/moduleLoading.test.ts +226 -0
- package/tests/unit/plugins/pluginLoading.test.ts +151 -0
- package/ts_build/package.json +4 -2
- package/ts_build/src/agents/base/base.d.ts +4 -3
- package/ts_build/src/agents/base/base.js +54 -30
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/index.d.ts +3 -0
- package/ts_build/src/agents/index.js +21 -11
- package/ts_build/src/agents/index.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -1
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +16 -5
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +34 -17
- package/ts_build/src/chat/modules/AgentModule.js +248 -258
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +3 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +16 -1
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/RendererModule.d.ts +16 -0
- package/ts_build/src/chat/modules/RendererModule.js +76 -0
- package/ts_build/src/chat/modules/RendererModule.js.map +1 -0
- package/ts_build/src/chat/modules/SessionsModule.d.ts +33 -0
- package/ts_build/src/chat/modules/SessionsModule.js +582 -0
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -0
- package/ts_build/src/chat/modules/SetupModule.d.ts +3 -3
- package/ts_build/src/chat/modules/SetupModule.js +4 -6
- package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/renderer/CompactRenderer.d.ts +23 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js +167 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +22 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js +110 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/FancyRenderer.d.ts +23 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js +328 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/index.d.ts +5 -0
- package/ts_build/src/chat/renderer/index.js +29 -0
- package/ts_build/src/chat/renderer/index.js.map +1 -0
- package/ts_build/src/chat/renderer/loadRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/loadRenderer.js +246 -0
- package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/messagesToRenderEvents.d.ts +15 -0
- package/ts_build/src/chat/renderer/messagesToRenderEvents.js +72 -0
- package/ts_build/src/chat/renderer/messagesToRenderEvents.js.map +1 -0
- package/ts_build/src/chat/renderer/types.d.ts +75 -0
- package/ts_build/src/chat/renderer/types.js +3 -0
- package/ts_build/src/chat/renderer/types.js.map +1 -0
- package/ts_build/src/chat/types.d.ts +5 -0
- package/ts_build/src/chat.js +46 -4
- package/ts_build/src/chat.js.map +1 -1
- package/ts_build/src/cli.js +18 -5
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/config.d.ts +1 -0
- package/ts_build/src/config.js +17 -1
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/plugins/AgentsMdPlugin.js +1 -1
- package/ts_build/src/plugins/AgentsMdPlugin.js.map +1 -1
- package/ts_build/src/plugins/GitPlugin.js +20 -20
- package/ts_build/src/plugins/GitPlugin.js.map +1 -1
- package/ts_build/src/plugins/PluginBase.d.ts +1 -0
- package/ts_build/src/plugins/PluginBase.js +13 -0
- package/ts_build/src/plugins/PluginBase.js.map +1 -1
- package/ts_build/src/plugins/SkillsPlugin.d.ts +13 -0
- package/ts_build/src/plugins/SkillsPlugin.js +149 -0
- package/ts_build/src/plugins/SkillsPlugin.js.map +1 -0
- package/ts_build/src/plugins/asana.js +4 -4
- package/ts_build/src/plugins/asana.js.map +1 -1
- package/ts_build/src/plugins/embedding.js +3 -3
- package/ts_build/src/plugins/embedding.js.map +1 -1
- package/ts_build/src/plugins/exec.js +3 -3
- package/ts_build/src/plugins/exec.js.map +1 -1
- package/ts_build/src/plugins/figma.js +3 -3
- package/ts_build/src/plugins/figma.js.map +1 -1
- package/ts_build/src/plugins/github.js +18 -18
- package/ts_build/src/plugins/github.js.map +1 -1
- package/ts_build/src/plugins/jira.js +2 -2
- package/ts_build/src/plugins/jira.js.map +1 -1
- package/ts_build/src/plugins/language.js +4 -4
- package/ts_build/src/plugins/language.js.map +1 -1
- package/ts_build/src/plugins/linear.js +4 -4
- package/ts_build/src/plugins/linear.js.map +1 -1
- package/ts_build/src/plugins/notion.js +6 -6
- package/ts_build/src/plugins/notion.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +3 -0
- package/ts_build/src/plugins/plugins.js +18 -3
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/plugins/url.js +2 -2
- package/ts_build/src/plugins/url.js.map +1 -1
- package/ts_build/src/plugins/vim.js +2 -2
- package/ts_build/src/plugins/vim.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +3 -0
- package/ts_build/src/services/AgentService.js +7 -0
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
- package/ts_build/src/services/AgentSyncFs.js +2 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/EventService.d.ts +25 -2
- package/ts_build/src/services/EventService.js +92 -14
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/SessionManager.d.ts +6 -0
- package/ts_build/src/services/SessionManager.js +39 -1
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +101 -0
- package/ts_build/src/services/SyncedAgentWatcher.js +312 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -0
- package/ts_build/src/services/SyncerService.d.ts +30 -0
- package/ts_build/src/services/SyncerService.js +72 -0
- package/ts_build/src/services/SyncerService.js.map +1 -0
- package/ts_build/src/services/index.d.ts +2 -0
- package/ts_build/src/services/index.js +2 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.js +10 -2
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +19 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker.d.ts +2 -0
- package/ts_build/src/worker.js +59 -4
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/auth/PasskeySetup.d.ts +10 -0
- package/ts_build/src/workers/auth/PasskeySetup.js +131 -0
- package/ts_build/src/workers/auth/PasskeySetup.js.map +1 -0
- package/ts_build/src/workers/auth/WorkerPasskeyAuth.d.ts +35 -0
- package/ts_build/src/workers/auth/WorkerPasskeyAuth.js +129 -0
- package/ts_build/src/workers/auth/WorkerPasskeyAuth.js.map +1 -0
- package/ts_build/src/workers/auth/types.d.ts +36 -0
- package/ts_build/src/workers/auth/types.js +3 -0
- package/ts_build/src/workers/auth/types.js.map +1 -0
- package/ts_build/src/workers/tools/getChallenge.d.ts +9 -0
- package/ts_build/src/workers/tools/getChallenge.js +27 -0
- package/ts_build/src/workers/tools/getChallenge.js.map +1 -0
- package/ts_build/src/workers/tools/index.d.ts +6 -0
- package/ts_build/src/workers/tools/index.js +10 -0
- package/ts_build/src/workers/tools/index.js.map +1 -1
- package/ts_build/src/workers/tools/lock.d.ts +9 -0
- package/ts_build/src/workers/tools/lock.js +27 -0
- package/ts_build/src/workers/tools/lock.js.map +1 -0
- package/ts_build/src/workers/tools/unlock.d.ts +18 -0
- package/ts_build/src/workers/tools/unlock.js +78 -0
- package/ts_build/src/workers/tools/unlock.js.map +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.d.ts +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js +187 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.d.ts +1 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +123 -0
- 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
|
-
|
|
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
|
+
}
|