twinclaw 1.1.1 → 1.1.2
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/README.md +3 -3
- package/dist/api/handlers/agents.js +82 -0
- package/dist/api/handlers/debug.js +69 -0
- package/dist/api/handlers/devices.js +79 -0
- package/dist/api/handlers/jobs.js +99 -0
- package/dist/api/handlers/status.js +149 -0
- package/dist/api/router.js +272 -2
- package/dist/api/runtime-event-producer.js +20 -0
- package/dist/api/shared.js +34 -0
- package/dist/api/websocket-hub.js +18 -7
- package/dist/config/json-config.js +393 -1
- package/dist/core/heartbeat.js +304 -8
- package/dist/core/lane-executor.js +18 -0
- package/dist/core/onboarding.js +2 -2
- package/dist/core/self-improve-cli.js +212 -0
- package/dist/core/simplified-onboarding.js +158 -16
- package/dist/index.js +61 -33
- package/dist/interfaces/dispatcher.js +4 -2
- package/dist/interfaces/whatsapp_handler.js +243 -2
- package/dist/services/auto-configurer.js +591 -0
- package/dist/services/device-pairing.js +378 -0
- package/dist/services/hooks.js +130 -0
- package/dist/services/job-scheduler.js +133 -1
- package/dist/services/learning-system.js +226 -0
- package/dist/services/self-healing.js +267 -0
- package/dist/services/skill-acquisition/intent-parser.js +115 -0
- package/dist/services/skill-acquisition/research-engine.js +319 -0
- package/dist/services/skill-builder.js +235 -0
- package/dist/services/sub-agent-service.js +215 -0
- package/dist/services/web-service.js +70 -0
- package/dist/services/webhook-service.js +127 -0
- package/dist/skills/builtin.js +827 -0
- package/dist/tools/agent-improvement.js +208 -0
- package/dist/types/skill-acquisition.js +4 -0
- package/package.json +3 -1
package/dist/api/router.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import { handleHealth, handleLiveness, handleReadiness } from './handlers/health.js';
|
|
4
|
+
import { handleStatus } from './handlers/status.js';
|
|
5
|
+
import { handleDebug } from './handlers/debug.js';
|
|
4
6
|
import { handleBrowserSnapshot, handleBrowserClick } from './handlers/browser.js';
|
|
5
7
|
import { handleWebhookCallback } from './handlers/callback.js';
|
|
6
8
|
import { handleSkillPackageDiagnostics, handleSkillPackageInstall, handleSkillPackageUpgrade, handleSkillPackageUninstall, } from './handlers/skill-packages.js';
|
|
7
9
|
import { handleLocalStateBackupDiagnostics, handleLocalStateCreateSnapshot, handleLocalStateRestoreSnapshot, } from './handlers/local-state-backup.js';
|
|
8
10
|
import { handlePersonaStateGet, handlePersonaStateUpdate, } from './handlers/persona-state.js';
|
|
9
11
|
import { handleConfigValidate } from './handlers/config-validate.js';
|
|
10
|
-
import { requestLogger,
|
|
12
|
+
import { requestLogger, requireAuth, sendError, sendOk, setRawRequestBody } from './shared.js';
|
|
11
13
|
import { BrowserService } from '../services/browser-service.js';
|
|
12
14
|
import { getPersonaStateService } from '../services/persona-state.js';
|
|
13
15
|
import { logThought } from '../utils/logger.js';
|
|
@@ -15,6 +17,17 @@ import { getCallbackOutcomeCounts } from '../services/db.js';
|
|
|
15
17
|
import { readFile } from 'node:fs/promises';
|
|
16
18
|
import path from 'node:path';
|
|
17
19
|
import { getConfigValue } from '../config/config-loader.js';
|
|
20
|
+
import { getLearningSystem } from '../services/learning-system.js';
|
|
21
|
+
import { getSkillBuilder } from '../services/skill-builder.js';
|
|
22
|
+
import { getAutoConfigurer } from '../services/auto-configurer.js';
|
|
23
|
+
import { getSelfHealingService } from '../services/self-healing.js';
|
|
24
|
+
import { getAgentImprovementTool } from '../tools/agent-improvement.js';
|
|
25
|
+
import { getIntentParser } from '../services/skill-acquisition/intent-parser.js';
|
|
26
|
+
import { getResearchEngine } from '../services/skill-acquisition/research-engine.js';
|
|
27
|
+
import { getDevicePairingService } from '../services/device-pairing.js';
|
|
28
|
+
import { handleJobsList, handleJobsGet, handleJobsRun, handleJobsStart, handleJobsStop, } from './handlers/jobs.js';
|
|
29
|
+
import { handleAgentsList, handleAgentsGet, handleAgentsCancel, } from './handlers/agents.js';
|
|
30
|
+
import { handleDevicesList, handleDevicesGet, handleDevicesCommand, } from './handlers/devices.js';
|
|
18
31
|
const DEFAULT_PORT = 18789;
|
|
19
32
|
const DEFAULT_BIND_HOST = '127.0.0.1';
|
|
20
33
|
/**
|
|
@@ -77,11 +90,37 @@ export function startApiServer(deps) {
|
|
|
77
90
|
const personaStateDeps = {
|
|
78
91
|
personaStateService: getPersonaStateService(),
|
|
79
92
|
};
|
|
93
|
+
const statusDeps = {
|
|
94
|
+
heartbeat: deps.heartbeat,
|
|
95
|
+
skillRegistry: deps.skillRegistry,
|
|
96
|
+
mcpManager: deps.mcpManager,
|
|
97
|
+
budgetGovernor: deps.budgetGovernor,
|
|
98
|
+
localStateBackup: deps.localStateBackup,
|
|
99
|
+
modelRouter: deps.modelRouter,
|
|
100
|
+
jobScheduler: deps.jobScheduler,
|
|
101
|
+
subAgentService: deps.subAgentService,
|
|
102
|
+
devicePairingService: getDevicePairingService(),
|
|
103
|
+
incidentManager: deps.incidentManager,
|
|
104
|
+
};
|
|
105
|
+
// Job Scheduler deps
|
|
106
|
+
const jobsDeps = {
|
|
107
|
+
scheduler: deps.jobScheduler,
|
|
108
|
+
};
|
|
109
|
+
// Sub-Agent deps
|
|
110
|
+
const agentsDeps = {
|
|
111
|
+
subAgentService: deps.subAgentService,
|
|
112
|
+
};
|
|
113
|
+
// Device Pairing deps
|
|
114
|
+
const devicesDeps = {
|
|
115
|
+
devicePairingService: getDevicePairingService(),
|
|
116
|
+
};
|
|
80
117
|
// ── Routes ──────────────────────────────────────────────────────────────────
|
|
81
118
|
app.get('/health', handleHealth(healthDeps));
|
|
82
119
|
app.get('/health/live', handleLiveness());
|
|
83
120
|
app.get('/health/ready', handleReadiness(healthDeps));
|
|
84
|
-
app.
|
|
121
|
+
app.get('/status', handleStatus(statusDeps));
|
|
122
|
+
app.get('/debug', handleDebug(statusDeps));
|
|
123
|
+
app.use(requireAuth);
|
|
85
124
|
app.get('/config/validate', handleConfigValidate());
|
|
86
125
|
app.get('/backup/diagnostics', handleLocalStateBackupDiagnostics(localStateBackupDeps));
|
|
87
126
|
app.post('/backup/snapshot', handleLocalStateCreateSnapshot(localStateBackupDeps));
|
|
@@ -272,6 +311,237 @@ export function startApiServer(deps) {
|
|
|
272
311
|
}
|
|
273
312
|
sendOk(res, deps.wsHub.getMetrics());
|
|
274
313
|
});
|
|
314
|
+
// ── Self-Improving Agent Routes ─────────────────────────────────────────────
|
|
315
|
+
app.get('/self/stats', (_req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const learning = getLearningSystem();
|
|
318
|
+
const stats = learning.getStats();
|
|
319
|
+
sendOk(res, stats);
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
sendError(res, `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
app.get('/self/health', async (_req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
const healer = getSelfHealingService();
|
|
328
|
+
const metrics = await healer.performHealthCheck();
|
|
329
|
+
sendOk(res, { metrics });
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
sendError(res, `Health check failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
app.get('/self/learn', (req, res) => {
|
|
336
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
337
|
+
if (!query) {
|
|
338
|
+
sendError(res, 'Query parameter "q" is required.', 400);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const learning = getLearningSystem();
|
|
343
|
+
const result = learning.query(query);
|
|
344
|
+
sendOk(res, result);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
sendError(res, `Query failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
app.post('/self/learn', async (req, res) => {
|
|
351
|
+
const { pattern, solution, type = 'fix' } = req.body;
|
|
352
|
+
if (!pattern || !solution) {
|
|
353
|
+
sendError(res, 'Required fields: pattern, solution', 400);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const learning = getLearningSystem();
|
|
358
|
+
await learning.learn(type, pattern, {}, solution, 'success');
|
|
359
|
+
sendOk(res, { message: `Learned: ${pattern} -> ${solution}` });
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
sendError(res, `Learn failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
app.get('/self/services', (_req, res) => {
|
|
366
|
+
try {
|
|
367
|
+
const configurer = getAutoConfigurer();
|
|
368
|
+
const services = configurer.listKnownServices();
|
|
369
|
+
sendOk(res, { services });
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
sendError(res, `Failed to list services: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
app.get('/self/configure/:service', async (req, res) => {
|
|
376
|
+
const service = req.params.service;
|
|
377
|
+
try {
|
|
378
|
+
const configurer = getAutoConfigurer();
|
|
379
|
+
const { config, steps } = await configurer.fetchDocsAndConfig(service);
|
|
380
|
+
sendOk(res, { service, config, steps });
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
sendError(res, `Configure failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
app.post('/self/configure/:service', async (req, res) => {
|
|
387
|
+
const service = req.params.service;
|
|
388
|
+
const config = req.body;
|
|
389
|
+
try {
|
|
390
|
+
const configurer = getAutoConfigurer();
|
|
391
|
+
const result = await configurer.saveServiceConfig(service, config);
|
|
392
|
+
sendOk(res, result);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
sendError(res, `Save config failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
app.post('/self/build-skill', async (req, res) => {
|
|
399
|
+
const { description, purpose } = req.body;
|
|
400
|
+
if (!description) {
|
|
401
|
+
sendError(res, 'Required field: description', 400);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const builder = getSkillBuilder();
|
|
406
|
+
const skill = await builder.buildFromRequest({ description, purpose });
|
|
407
|
+
sendOk(res, {
|
|
408
|
+
id: skill.id,
|
|
409
|
+
name: skill.name,
|
|
410
|
+
category: skill.category,
|
|
411
|
+
location: `${builder.getCustomSkillsDir()}/${skill.id}.ts`
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
sendError(res, `Build skill failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
app.get('/self/skills', (_req, res) => {
|
|
419
|
+
try {
|
|
420
|
+
const builder = getSkillBuilder();
|
|
421
|
+
const skills = builder.getCustomSkills();
|
|
422
|
+
sendOk(res, { skills });
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
sendError(res, `Failed to list skills: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
app.post('/self/improve', async (req, res) => {
|
|
429
|
+
const { goal, context } = req.body;
|
|
430
|
+
if (!goal) {
|
|
431
|
+
sendError(res, 'Required field: goal', 400);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const tool = getAgentImprovementTool();
|
|
436
|
+
const result = await tool.handleImprovementRequest({ goal, context });
|
|
437
|
+
sendOk(res, result);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
sendError(res, `Improve request failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
// ── Skill Acquisition Routes ───────────────────────────────────────────────
|
|
444
|
+
// POST /api/skills/acquire - Start acquiring a new skill
|
|
445
|
+
app.post('/skills/acquire', async (req, res) => {
|
|
446
|
+
const { request } = req.body;
|
|
447
|
+
if (!request) {
|
|
448
|
+
sendError(res, 'Required field: request (natural language description)', 400);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
// Step 1: Parse intent
|
|
453
|
+
const intentParser = getIntentParser();
|
|
454
|
+
const intent = await intentParser.parseIntent(request);
|
|
455
|
+
// Step 2: Research
|
|
456
|
+
const researchEngine = getResearchEngine();
|
|
457
|
+
const research = await researchEngine.research(intent);
|
|
458
|
+
// Step 3: Generate skill code
|
|
459
|
+
const skillBuilder = getSkillBuilder();
|
|
460
|
+
const skill = await skillBuilder.buildFromRequest({
|
|
461
|
+
description: request,
|
|
462
|
+
purpose: 'user requested'
|
|
463
|
+
});
|
|
464
|
+
sendOk(res, {
|
|
465
|
+
status: 'generated',
|
|
466
|
+
intent,
|
|
467
|
+
research,
|
|
468
|
+
skill: {
|
|
469
|
+
id: skill.id,
|
|
470
|
+
name: skill.name,
|
|
471
|
+
description: skill.description,
|
|
472
|
+
category: skill.category
|
|
473
|
+
},
|
|
474
|
+
nextSteps: [
|
|
475
|
+
'Review the generated skill',
|
|
476
|
+
'Provide credentials if needed',
|
|
477
|
+
'Approve to activate the skill'
|
|
478
|
+
]
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
sendError(res, `Skill acquisition failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// GET /api/skills - List all skills (built-in + custom)
|
|
486
|
+
app.get('/skills', (_req, res) => {
|
|
487
|
+
try {
|
|
488
|
+
const builder = getSkillBuilder();
|
|
489
|
+
const customSkills = builder.getCustomSkills();
|
|
490
|
+
sendOk(res, {
|
|
491
|
+
customSkills,
|
|
492
|
+
total: customSkills.length
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
sendError(res, `Failed to list skills: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
// GET /api/skills/:id - Get skill details
|
|
500
|
+
app.get('/skills/:id', (req, res) => {
|
|
501
|
+
const { id } = req.params;
|
|
502
|
+
try {
|
|
503
|
+
const builder = getSkillBuilder();
|
|
504
|
+
const skill = builder.getSkill(id);
|
|
505
|
+
if (!skill) {
|
|
506
|
+
sendError(res, 'Skill not found', 404);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
sendOk(res, skill);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
sendError(res, `Failed to get skill: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
// ── Job Scheduler Routes ─────────────────────────────────────────────────────
|
|
516
|
+
app.get('/jobs', handleJobsList(jobsDeps));
|
|
517
|
+
app.get('/jobs/:id', handleJobsGet(jobsDeps));
|
|
518
|
+
app.post('/jobs/:id/run', handleJobsRun(jobsDeps));
|
|
519
|
+
app.post('/jobs/:id/start', handleJobsStart(jobsDeps));
|
|
520
|
+
app.post('/jobs/:id/stop', handleJobsStop(jobsDeps));
|
|
521
|
+
// ── Sub-Agent Routes ─────────────────────────────────────────────────────
|
|
522
|
+
app.get('/agents', handleAgentsList(agentsDeps));
|
|
523
|
+
app.get('/agents/:id', handleAgentsGet(agentsDeps));
|
|
524
|
+
app.post('/agents/:id/cancel', handleAgentsCancel(agentsDeps));
|
|
525
|
+
// ── Device Pairing Routes ───────────────────────────────────────────────
|
|
526
|
+
app.get('/devices', handleDevicesList(devicesDeps));
|
|
527
|
+
app.get('/devices/:id', handleDevicesGet(devicesDeps));
|
|
528
|
+
app.post('/devices/:id/command', handleDevicesCommand(devicesDeps));
|
|
529
|
+
// DELETE /api/skills/:id - Delete a custom skill
|
|
530
|
+
app.delete('/skills/:id', async (req, res) => {
|
|
531
|
+
const { id } = req.params;
|
|
532
|
+
try {
|
|
533
|
+
const builder = getSkillBuilder();
|
|
534
|
+
const deleted = await builder.deleteSkill(id);
|
|
535
|
+
if (!deleted) {
|
|
536
|
+
sendError(res, 'Skill not found or could not be deleted', 404);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
sendOk(res, { message: 'Skill deleted successfully' });
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
sendError(res, `Failed to delete skill: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
275
545
|
// ── Catch-all 404 ──────────────────────────────────────────────────────────
|
|
276
546
|
app.use((_req, res) => {
|
|
277
547
|
sendError(res, 'Not found.', 404);
|
|
@@ -60,6 +60,16 @@ export class RuntimeEventProducer {
|
|
|
60
60
|
if (include('reliability') && this.#deps.dispatcher) {
|
|
61
61
|
snapshot.reliability = this.#buildReliabilityPayload();
|
|
62
62
|
}
|
|
63
|
+
if (include('jobs') && this.#deps.jobScheduler) {
|
|
64
|
+
snapshot.jobs = {
|
|
65
|
+
list: this.#deps.jobScheduler.listJobs(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (include('agents') && this.#deps.subAgentService) {
|
|
69
|
+
snapshot.agents = {
|
|
70
|
+
list: this.#deps.subAgentService.list(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
63
73
|
return snapshot;
|
|
64
74
|
}
|
|
65
75
|
// ── Private Helpers ────────────────────────────────────────────────────────
|
|
@@ -76,6 +86,16 @@ export class RuntimeEventProducer {
|
|
|
76
86
|
if (this.#deps.dispatcher) {
|
|
77
87
|
this.#hub.publish('reliability', this.#buildReliabilityPayload());
|
|
78
88
|
}
|
|
89
|
+
if (this.#deps.jobScheduler) {
|
|
90
|
+
this.#hub.publish('jobs', {
|
|
91
|
+
list: this.#deps.jobScheduler.listJobs(),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (this.#deps.subAgentService) {
|
|
95
|
+
this.#hub.publish('agents', {
|
|
96
|
+
list: this.#deps.subAgentService.list(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
79
99
|
}
|
|
80
100
|
#buildReliabilityPayload() {
|
|
81
101
|
const queueMetrics = this.#deps.dispatcher.queue.getStats() ?? null;
|
package/dist/api/shared.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual, randomUUID } from 'node:crypto';
|
|
2
2
|
import { logThought, scrubSensitiveText } from '../utils/logger.js';
|
|
3
3
|
import { getSecretVaultService } from '../services/secret-vault.js';
|
|
4
|
+
import { readConfig } from '../config/config-loader.js';
|
|
4
5
|
function stableStringify(value) {
|
|
5
6
|
if (value === null || typeof value !== 'object') {
|
|
6
7
|
const serialized = JSON.stringify(value);
|
|
@@ -112,6 +113,39 @@ export function requireSignature(req, res, next) {
|
|
|
112
113
|
}
|
|
113
114
|
next();
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate token-based auth for API requests.
|
|
118
|
+
* Supports both HMAC and token modes based on gateway config.
|
|
119
|
+
*/
|
|
120
|
+
export async function requireAuth(req, res, next) {
|
|
121
|
+
try {
|
|
122
|
+
const config = await readConfig();
|
|
123
|
+
const authMode = config.gateway?.auth?.mode ?? 'hmac';
|
|
124
|
+
if (authMode === 'token') {
|
|
125
|
+
const token = config.gateway?.auth?.token;
|
|
126
|
+
if (!token) {
|
|
127
|
+
sendError(res, 'Gateway token not configured.', 503);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const providedToken = req.headers['x-api-token'];
|
|
131
|
+
if (!providedToken || providedToken !== token) {
|
|
132
|
+
void logThought('[API] Token request rejected — invalid token.');
|
|
133
|
+
sendError(res, 'Invalid API token.', 401);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
requireSignature(req, res, next);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
next();
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const { status, message } = mapError(err);
|
|
145
|
+
void logThought(`[API] Auth error: ${message}`);
|
|
146
|
+
sendError(res, message, status);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
115
149
|
// ── Error Mapping ───────────────────────────────────────────────────────────
|
|
116
150
|
/** Map a caught error to a status code and message. */
|
|
117
151
|
export function mapError(err) {
|
|
@@ -3,12 +3,13 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
3
3
|
import { logThought } from '../utils/logger.js';
|
|
4
4
|
import { WsCloseCode } from '../types/websocket.js';
|
|
5
5
|
import { getSecretVaultService } from '../services/secret-vault.js';
|
|
6
|
+
import { readConfig } from '../config/config-loader.js';
|
|
6
7
|
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
7
8
|
const DEFAULT_AUTH_TIMEOUT_MS = 5_000;
|
|
8
9
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
9
10
|
/** Default backpressure threshold in kilobytes (see WsHubConfig.maxClientQueue). */
|
|
10
11
|
const DEFAULT_MAX_CLIENT_QUEUE = 200;
|
|
11
|
-
const VALID_TOPICS = new Set(['health', 'reliability', 'incidents', 'routing']);
|
|
12
|
+
const VALID_TOPICS = new Set(['health', 'reliability', 'incidents', 'routing', 'jobs', 'agents', 'messages']);
|
|
12
13
|
// ── WsHub ──────────────────────────────────────────────────────────────────────
|
|
13
14
|
/**
|
|
14
15
|
* Control-plane WebSocket hub.
|
|
@@ -155,7 +156,7 @@ export class WsHub {
|
|
|
155
156
|
client.isAlive = true;
|
|
156
157
|
});
|
|
157
158
|
ws.on('message', (data) => {
|
|
158
|
-
this.#handleMessage(client, data);
|
|
159
|
+
void this.#handleMessage(client, data);
|
|
159
160
|
});
|
|
160
161
|
ws.on('close', () => {
|
|
161
162
|
this.#cleanupClient(client);
|
|
@@ -166,7 +167,7 @@ export class WsHub {
|
|
|
166
167
|
});
|
|
167
168
|
void logThought(`[WsHub] New connection: ${clientId}.`);
|
|
168
169
|
}
|
|
169
|
-
#handleMessage(client, rawData) {
|
|
170
|
+
async #handleMessage(client, rawData) {
|
|
170
171
|
let msg;
|
|
171
172
|
try {
|
|
172
173
|
msg = JSON.parse(String(rawData));
|
|
@@ -181,7 +182,7 @@ export class WsHub {
|
|
|
181
182
|
}
|
|
182
183
|
switch (msg.type) {
|
|
183
184
|
case 'auth':
|
|
184
|
-
this.#handleAuth(client, msg);
|
|
185
|
+
await this.#handleAuth(client, msg);
|
|
185
186
|
break;
|
|
186
187
|
case 'subscribe':
|
|
187
188
|
this.#handleSubscribe(client, msg);
|
|
@@ -193,10 +194,20 @@ export class WsHub {
|
|
|
193
194
|
this.#sendError(client, 400, `Unknown message type: ${String(msg.type)}`);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
|
-
#handleAuth(client, msg) {
|
|
197
|
+
async #handleAuth(client, msg) {
|
|
197
198
|
const token = typeof msg.token === 'string' ? msg.token : '';
|
|
198
|
-
const
|
|
199
|
-
|
|
199
|
+
const config = await readConfig();
|
|
200
|
+
const authMode = config.gateway?.auth?.mode ?? 'hmac';
|
|
201
|
+
let authenticated = false;
|
|
202
|
+
if (authMode === 'token') {
|
|
203
|
+
const configuredToken = config.gateway?.auth?.token ?? '';
|
|
204
|
+
authenticated = !!configuredToken && token === configuredToken;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const apiSecret = getSecretVaultService().readSecret('API_SECRET') ?? '';
|
|
208
|
+
authenticated = !!apiSecret && token === apiSecret;
|
|
209
|
+
}
|
|
210
|
+
if (!authenticated) {
|
|
200
211
|
this.#metrics.authFailures++;
|
|
201
212
|
void logThought(`[WsHub] Client ${client.id} authentication failed (invalid token).`);
|
|
202
213
|
this.#sendError(client, WsCloseCode.AuthFailed, 'Authentication failed.');
|