twinclaw 1.1.4 → 1.1.6
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/dist/api/handlers/agents.js +1 -1
- package/dist/api/handlers/browser.js +47 -7
- package/dist/api/handlers/callback.js +3 -3
- package/dist/api/handlers/debug.js +20 -15
- package/dist/api/handlers/devices.js +2 -1
- package/dist/api/handlers/health-data.js +87 -0
- package/dist/api/handlers/health.js +2 -84
- package/dist/api/handlers/self-routes.js +144 -0
- package/dist/api/handlers/skill-acquisition-routes.js +90 -0
- package/dist/api/handlers/status.js +10 -87
- package/dist/api/router.js +32 -229
- package/dist/api/runtime-event-producer.js +1 -1
- package/dist/api/shared.js +10 -5
- package/dist/api/websocket-hub.js +26 -10
- package/dist/bootstrap/owner-bootstrap.js +23 -0
- package/dist/bootstrap/startup-health-probe.js +24 -0
- package/dist/config/config-schema.js +54 -0
- package/dist/config/env-schema.js +73 -1
- package/dist/config/env-validator.js +21 -5
- package/dist/config/json-config.js +71 -6
- package/dist/config/model-catalog.js +336 -0
- package/dist/config/prestart-validation.js +37 -0
- package/dist/config/validation-rules.js +16 -0
- package/dist/config/workspace.js +18 -1
- package/dist/core/channels-cli.js +120 -3
- package/dist/core/cli.js +225 -99
- package/dist/core/context-assembly.js +7 -1
- package/dist/core/doctor.js +4 -3
- package/dist/core/gateway-cli.js +30 -12
- package/dist/core/gateway.js +57 -12
- package/dist/core/heartbeat.js +8 -41
- package/dist/core/help-formatter.js +41 -0
- package/dist/core/lane-executor.js +3 -2
- package/dist/core/logs-cli.js +24 -1
- package/dist/core/model-picker.js +148 -0
- package/dist/core/onboarding-v2.js +65 -0
- package/dist/core/onboarding.js +460 -79
- package/dist/core/pairing-cli.js +33 -7
- package/dist/core/queue-cli.js +125 -0
- package/dist/core/queue-metrics.js +49 -0
- package/dist/core/secret-vault-cli.js +24 -7
- package/dist/core/self-improve-cli.js +58 -48
- package/dist/core/simplified-onboarding.js +5 -3
- package/dist/core/status-cli.js +152 -0
- package/dist/index.js +380 -357
- package/dist/interfaces/dispatcher.js +39 -16
- package/dist/interfaces/whatsapp_handler.js +50 -10
- package/dist/release/twinclaw-config-schema.js +9 -2
- package/dist/services/auto-configurer.js +41 -17
- package/dist/services/block-chunker.js +20 -3
- package/dist/services/browser-service.js +67 -4
- package/dist/services/context-lifecycle.js +15 -8
- package/dist/services/db-delivery-callback.js +110 -0
- package/dist/services/db-incidents.js +97 -0
- package/dist/services/db-local-state.js +97 -0
- package/dist/services/db-mcp-audit.js +15 -0
- package/dist/services/db-model-routing.js +53 -0
- package/dist/services/db-orchestration.js +78 -0
- package/dist/services/db-reasoning.js +159 -0
- package/dist/services/db-runtime-budget.js +125 -0
- package/dist/services/db.js +69 -721
- package/dist/services/device-pairing.js +12 -22
- package/dist/services/embedding-service.js +59 -6
- package/dist/services/github-copilot-service.js +232 -0
- package/dist/services/hooks.js +44 -8
- package/dist/services/incident-manager.js +3 -3
- package/dist/services/learning-system.js +15 -5
- package/dist/services/local-state-backup.js +9 -13
- package/dist/services/mcp-client-adapter.js +53 -8
- package/dist/services/mcp-server-manager.js +2 -1
- package/dist/services/model-catalog-service.js +154 -0
- package/dist/services/model-router.js +226 -55
- package/dist/services/mvp-gate.js +5 -11
- package/dist/services/orchestration-service.js +26 -5
- package/dist/services/persona-state.js +48 -5
- package/dist/services/policy-engine.js +1 -1
- package/dist/services/proactive-notifier.js +35 -1
- package/dist/services/queue-service.js +25 -6
- package/dist/services/release-pipeline.js +3 -11
- package/dist/services/runtime-budget-governor.js +19 -7
- package/dist/services/secret-vault.js +27 -7
- package/dist/services/self-healing.js +59 -12
- package/dist/services/semantic-memory.js +19 -12
- package/dist/services/skill-builder.js +57 -59
- package/dist/services/skill-package-manager.js +10 -4
- package/dist/services/streaming-output.js +30 -0
- package/dist/services/sub-agent-service.js +17 -4
- package/dist/services/web-service.js +1 -1
- package/dist/skills/builtin.js +10 -11
- package/dist/skills/shell.js +5 -2
- package/dist/types/model-catalog.js +20 -0
- package/dist/types/model-routing.js +5 -1
- package/dist/utils/config-utils.js +33 -0
- package/dist/utils/fs-utils.js +14 -0
- package/dist/utils/logger.js +27 -8
- package/dist/utils/retry.js +7 -4
- package/dist/utils/secret-scan.js +1 -1
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ export function handleAgentsGet(deps) {
|
|
|
37
37
|
id: agent.id,
|
|
38
38
|
name: agent.name,
|
|
39
39
|
model: agent.model,
|
|
40
|
-
|
|
40
|
+
hasSystemPrompt: Boolean(agent.systemPrompt && agent.systemPrompt.trim().length > 0),
|
|
41
41
|
maxSteps: agent.maxSteps,
|
|
42
42
|
timeoutMs: agent.timeoutMs,
|
|
43
43
|
reportBackTo: agent.reportBackTo,
|
|
@@ -2,7 +2,18 @@ import { BrowserReferenceError } from '../../services/browser-service.js';
|
|
|
2
2
|
import { sendOk, sendError, mapError } from '../shared.js';
|
|
3
3
|
import { logThought } from '../../utils/logger.js';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { isIP } from 'node:net';
|
|
6
|
+
import { unlink } from 'node:fs/promises';
|
|
5
7
|
const DEFAULT_BROWSER_ALLOWED_HOSTS = ['example.com'];
|
|
8
|
+
const SNAPSHOT_CLEANUP_DELAY_MS = 5 * 60 * 1000;
|
|
9
|
+
const BLOCKED_METADATA_HOSTS = new Set([
|
|
10
|
+
'metadata.google.internal',
|
|
11
|
+
'metadata.azure.internal',
|
|
12
|
+
'metadata',
|
|
13
|
+
'169.254.169.254',
|
|
14
|
+
'169.254.170.2',
|
|
15
|
+
'100.100.100.200',
|
|
16
|
+
]);
|
|
6
17
|
function resolveAllowedBrowserHosts() {
|
|
7
18
|
const configured = (process.env.BROWSER_ALLOWED_HOSTS ?? '')
|
|
8
19
|
.split(',')
|
|
@@ -24,21 +35,37 @@ function isPrivateIpv4(hostname) {
|
|
|
24
35
|
a === 127 ||
|
|
25
36
|
a === 0 ||
|
|
26
37
|
(a === 169 && b === 254) ||
|
|
38
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
27
39
|
(a === 172 && b >= 16 && b <= 31) ||
|
|
28
40
|
(a === 192 && b === 168));
|
|
29
41
|
}
|
|
42
|
+
function isBlockedIpv6(hostname) {
|
|
43
|
+
const normalized = hostname.toLowerCase();
|
|
44
|
+
if (normalized === '::1' || normalized === '::') {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (normalized.startsWith('fe80:') || normalized.startsWith('fc') || normalized.startsWith('fd')) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (normalized.startsWith('::ffff:')) {
|
|
51
|
+
return isPrivateIpv4(normalized.slice('::ffff:'.length));
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
30
55
|
function isPrivateOrLocalHost(hostname) {
|
|
31
56
|
const normalized = hostname.toLowerCase();
|
|
57
|
+
if (BLOCKED_METADATA_HOSTS.has(normalized)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
32
60
|
if (normalized === 'localhost' ||
|
|
33
61
|
normalized.endsWith('.localhost') ||
|
|
34
|
-
normalized.endsWith('.local')
|
|
35
|
-
normalized === '::1' ||
|
|
36
|
-
normalized === '::' ||
|
|
37
|
-
normalized.startsWith('fe80:') ||
|
|
38
|
-
normalized.startsWith('fc') ||
|
|
39
|
-
normalized.startsWith('fd')) {
|
|
62
|
+
normalized.endsWith('.local')) {
|
|
40
63
|
return true;
|
|
41
64
|
}
|
|
65
|
+
const ipKind = isIP(normalized);
|
|
66
|
+
if (ipKind === 6) {
|
|
67
|
+
return isBlockedIpv6(normalized);
|
|
68
|
+
}
|
|
42
69
|
return isPrivateIpv4(normalized);
|
|
43
70
|
}
|
|
44
71
|
function hostMatchesAllowRule(hostname, rule) {
|
|
@@ -56,7 +83,7 @@ function validateNavigationUrl(inputUrl) {
|
|
|
56
83
|
try {
|
|
57
84
|
parsed = new URL(inputUrl);
|
|
58
85
|
}
|
|
59
|
-
catch {
|
|
86
|
+
catch (_error) {
|
|
60
87
|
return { ok: false, status: 400, error: 'Field "url" must be a valid absolute URL.' };
|
|
61
88
|
}
|
|
62
89
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
@@ -80,6 +107,18 @@ function validateNavigationUrl(inputUrl) {
|
|
|
80
107
|
}
|
|
81
108
|
return { ok: true, url: parsed.toString() };
|
|
82
109
|
}
|
|
110
|
+
function scheduleSnapshotCleanup(screenshotPath) {
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
void unlink(screenshotPath).catch((error) => {
|
|
113
|
+
const fsError = error;
|
|
114
|
+
if (fsError.code !== 'ENOENT') {
|
|
115
|
+
const message = fsError instanceof Error ? fsError.message : String(fsError);
|
|
116
|
+
void logThought(`[API] Snapshot cleanup failed for ${screenshotPath}: ${message}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}, SNAPSHOT_CLEANUP_DELAY_MS);
|
|
120
|
+
timeout.unref();
|
|
121
|
+
}
|
|
83
122
|
/**
|
|
84
123
|
* POST /browser/snapshot
|
|
85
124
|
*
|
|
@@ -116,6 +155,7 @@ export function handleBrowserSnapshot(deps) {
|
|
|
116
155
|
const result = await deps.browserService.takeScreenshotForVlm(screenshotPath, fullPage);
|
|
117
156
|
const tree = await deps.browserService.getAccessibilityTree();
|
|
118
157
|
const referenceContext = await deps.browserService.captureSnapshotReferenceContext();
|
|
158
|
+
scheduleSnapshotCleanup(result.path);
|
|
119
159
|
const data = {
|
|
120
160
|
snapshotId: referenceContext.snapshotId,
|
|
121
161
|
screenshotPath: result.path,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sendOk, sendError } from '../shared.js';
|
|
2
2
|
import { logThought } from '../../utils/logger.js';
|
|
3
|
-
import { recordCallbackReceipt, getCallbackReceipt, getDelivery, updateDeliveryState } from '../../services/db.js';
|
|
3
|
+
import { recordCallbackReceipt, getCallbackReceipt, getDelivery, updateDeliveryState } from '../../services/db-delivery-callback.js';
|
|
4
4
|
const MAX_SANITIZED_STRING_LENGTH = 512;
|
|
5
5
|
const MAX_SANITIZED_ARRAY_ITEMS = 25;
|
|
6
6
|
const MAX_SANITIZED_OBJECT_KEYS = 40;
|
|
@@ -107,8 +107,8 @@ export function handleWebhookCallback(deps) {
|
|
|
107
107
|
updateDeliveryState(body.taskId, newState, newState === 'sent' ? new Date().toISOString() : null);
|
|
108
108
|
await logThought(`[API] Webhook reconciled delivery queue item: ${body.taskId} -> ${newState}`);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
const gatewayResult = await deps.gateway.processText(sessionId, summaryText);
|
|
111
|
+
await logThought(`[API] Webhook payload processed by gateway for task ${body.taskId} (responseLength=${gatewayResult.length}).`);
|
|
112
112
|
recordCallbackReceipt(idempotencyKey, 202, 'accepted');
|
|
113
113
|
const data = {
|
|
114
114
|
accepted: true,
|
|
@@ -45,25 +45,30 @@ export function handleDebug(deps) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
async function getRecentLogs(limit) {
|
|
48
|
+
const dateIso = new Date().toISOString().slice(0, 10);
|
|
49
|
+
const logPath = path.resolve('memory', `${dateIso}.md`);
|
|
50
|
+
let content = '';
|
|
48
51
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
content = await readFile(logPath, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const fsError = error;
|
|
56
|
+
if (fsError.code === 'ENOENT') {
|
|
53
57
|
return [];
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
return sections.slice(-limit).map((s) => {
|
|
57
|
-
const [header, ...bodyLines] = s.split('\n');
|
|
58
|
-
const [type, timestamp] = header.split(' @ ');
|
|
59
|
-
return {
|
|
60
|
-
timestamp: timestamp || new Date().toISOString(),
|
|
61
|
-
level: type.toUpperCase(),
|
|
62
|
-
message: bodyLines.join('\n').trim(),
|
|
63
|
-
};
|
|
64
|
-
}).reverse();
|
|
59
|
+
throw new Error(`Failed to read debug logs from ${logPath}: ${fsError.message}`);
|
|
65
60
|
}
|
|
66
|
-
|
|
61
|
+
if (!content.trim()) {
|
|
67
62
|
return [];
|
|
68
63
|
}
|
|
64
|
+
const sections = content.split(/\n## /).filter(Boolean);
|
|
65
|
+
return sections.slice(-limit).map((s) => {
|
|
66
|
+
const [header, ...bodyLines] = s.split('\n');
|
|
67
|
+
const [type, timestamp] = header.split(' @ ');
|
|
68
|
+
return {
|
|
69
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
70
|
+
level: type.toUpperCase(),
|
|
71
|
+
message: bodyLines.join('\n').trim(),
|
|
72
|
+
};
|
|
73
|
+
}).reverse();
|
|
69
74
|
}
|
|
@@ -68,7 +68,8 @@ export function handleDevicesCommand(deps) {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
71
|
-
|
|
71
|
+
const status = result.error && /not implemented/i.test(result.error) ? 501 : 400;
|
|
72
|
+
sendError(res, result.error || 'Command execution failed', status);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
catch (err) {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getSecretVaultService } from '../../services/secret-vault.js';
|
|
2
|
+
export async function buildHealthData(deps, startTimeMs) {
|
|
3
|
+
const summary = deps.skillRegistry.summary();
|
|
4
|
+
const servers = deps.mcpManager.listServers();
|
|
5
|
+
const packageDiagnostics = await deps.mcpManager.getSkillPackageDiagnostics();
|
|
6
|
+
const secretDiagnostics = getSecretVaultService().getDiagnostics(['API_SECRET']);
|
|
7
|
+
const budgetSnapshot = deps.budgetGovernor?.getSnapshot('health');
|
|
8
|
+
const routingSnapshot = deps.modelRouter?.getHealthSnapshot();
|
|
9
|
+
const backupDiagnostics = deps.localStateBackup
|
|
10
|
+
? await deps.localStateBackup.getDiagnostics(5)
|
|
11
|
+
: null;
|
|
12
|
+
const heartbeatRunning = deps.heartbeat.scheduler
|
|
13
|
+
.listJobs()
|
|
14
|
+
.some((j) => j.status === 'running');
|
|
15
|
+
return {
|
|
16
|
+
status: servers.some((s) => s.state === 'error') ||
|
|
17
|
+
packageDiagnostics.blockedPackageCount > 0 ||
|
|
18
|
+
secretDiagnostics.health.hasIssues ||
|
|
19
|
+
budgetSnapshot?.directive.severity === 'hard_limit' ||
|
|
20
|
+
(routingSnapshot?.consecutiveFailures ?? 0) >= 3 ||
|
|
21
|
+
backupDiagnostics?.status === 'degraded'
|
|
22
|
+
? 'degraded'
|
|
23
|
+
: 'ok',
|
|
24
|
+
uptimeSec: Math.floor((Date.now() - startTimeMs) / 1000),
|
|
25
|
+
memoryUsageMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
26
|
+
heartbeat: { running: heartbeatRunning },
|
|
27
|
+
skills: {
|
|
28
|
+
builtin: summary.builtin ?? 0,
|
|
29
|
+
mcp: summary.mcp ?? 0,
|
|
30
|
+
total: deps.skillRegistry.size,
|
|
31
|
+
},
|
|
32
|
+
skillPackages: {
|
|
33
|
+
installed: packageDiagnostics.installed.length,
|
|
34
|
+
active: packageDiagnostics.activePackageCount,
|
|
35
|
+
blocked: packageDiagnostics.blockedPackageCount,
|
|
36
|
+
warnings: packageDiagnostics.warnings,
|
|
37
|
+
violations: packageDiagnostics.violations.map((violation) => ({
|
|
38
|
+
packageName: violation.packageName,
|
|
39
|
+
version: violation.version,
|
|
40
|
+
code: violation.code,
|
|
41
|
+
message: violation.message,
|
|
42
|
+
remediation: violation.remediation,
|
|
43
|
+
})),
|
|
44
|
+
},
|
|
45
|
+
secrets: {
|
|
46
|
+
status: secretDiagnostics.health.hasIssues ? 'degraded' : 'ok',
|
|
47
|
+
missingRequired: secretDiagnostics.health.missingRequired,
|
|
48
|
+
expired: secretDiagnostics.health.expired,
|
|
49
|
+
warnings: secretDiagnostics.health.warnings,
|
|
50
|
+
total: secretDiagnostics.total,
|
|
51
|
+
active: secretDiagnostics.active,
|
|
52
|
+
dueForRotation: secretDiagnostics.dueForRotation,
|
|
53
|
+
},
|
|
54
|
+
budget: budgetSnapshot
|
|
55
|
+
? {
|
|
56
|
+
severity: budgetSnapshot.directive.severity,
|
|
57
|
+
profile: budgetSnapshot.directive.profile,
|
|
58
|
+
pacingDelayMs: budgetSnapshot.directive.pacingDelayMs,
|
|
59
|
+
manualProfile: budgetSnapshot.manualProfile,
|
|
60
|
+
daily: budgetSnapshot.daily,
|
|
61
|
+
session: budgetSnapshot.session,
|
|
62
|
+
providers: budgetSnapshot.providers,
|
|
63
|
+
}
|
|
64
|
+
: undefined,
|
|
65
|
+
routing: routingSnapshot,
|
|
66
|
+
backups: backupDiagnostics
|
|
67
|
+
? {
|
|
68
|
+
status: backupDiagnostics.status,
|
|
69
|
+
lastSnapshotAt: backupDiagnostics.lastSnapshotAt,
|
|
70
|
+
lastRestoreAt: backupDiagnostics.lastRestoreAt,
|
|
71
|
+
validationFailureCount: backupDiagnostics.validationFailureCount,
|
|
72
|
+
recommendationCount: backupDiagnostics.recommendations.length,
|
|
73
|
+
}
|
|
74
|
+
: undefined,
|
|
75
|
+
mcpServers: servers.map((s) => ({
|
|
76
|
+
id: s.id,
|
|
77
|
+
name: s.name,
|
|
78
|
+
state: s.state,
|
|
79
|
+
toolCount: s.toolCount,
|
|
80
|
+
health: {
|
|
81
|
+
circuit: s.health.state,
|
|
82
|
+
failureCount: s.health.metrics.failureCount,
|
|
83
|
+
remainingCooldownMs: s.health.remainingCooldownMs,
|
|
84
|
+
},
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -1,93 +1,11 @@
|
|
|
1
1
|
import { getSecretVaultService } from '../../services/secret-vault.js';
|
|
2
2
|
import { sendOk } from '../shared.js';
|
|
3
|
+
import { buildHealthData } from './health-data.js';
|
|
3
4
|
const startTime = Date.now();
|
|
4
5
|
/** GET /health — Returns system health status and subsystem summaries. */
|
|
5
6
|
export function handleHealth(deps) {
|
|
6
7
|
return async (_req, res) => {
|
|
7
|
-
const
|
|
8
|
-
const servers = deps.mcpManager.listServers();
|
|
9
|
-
const packageDiagnostics = await deps.mcpManager.getSkillPackageDiagnostics();
|
|
10
|
-
const secretDiagnostics = getSecretVaultService().getDiagnostics(['API_SECRET']);
|
|
11
|
-
const budgetSnapshot = deps.budgetGovernor?.getSnapshot('health');
|
|
12
|
-
const routingSnapshot = deps.modelRouter?.getHealthSnapshot();
|
|
13
|
-
const backupDiagnostics = deps.localStateBackup
|
|
14
|
-
? await deps.localStateBackup.getDiagnostics(5)
|
|
15
|
-
: null;
|
|
16
|
-
const heartbeatRunning = deps.heartbeat.scheduler
|
|
17
|
-
.listJobs()
|
|
18
|
-
.some((j) => j.status === 'running');
|
|
19
|
-
const data = {
|
|
20
|
-
status: servers.some((s) => s.state === 'error') ||
|
|
21
|
-
packageDiagnostics.blockedPackageCount > 0 ||
|
|
22
|
-
secretDiagnostics.health.hasIssues ||
|
|
23
|
-
budgetSnapshot?.directive.severity === 'hard_limit' ||
|
|
24
|
-
(routingSnapshot?.consecutiveFailures ?? 0) >= 3 ||
|
|
25
|
-
backupDiagnostics?.status === 'degraded'
|
|
26
|
-
? 'degraded'
|
|
27
|
-
: 'ok',
|
|
28
|
-
uptimeSec: Math.floor((Date.now() - startTime) / 1000),
|
|
29
|
-
memoryUsageMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
30
|
-
heartbeat: { running: heartbeatRunning },
|
|
31
|
-
skills: {
|
|
32
|
-
builtin: summary.builtin ?? 0,
|
|
33
|
-
mcp: summary.mcp ?? 0,
|
|
34
|
-
total: deps.skillRegistry.size,
|
|
35
|
-
},
|
|
36
|
-
skillPackages: {
|
|
37
|
-
installed: packageDiagnostics.installed.length,
|
|
38
|
-
active: packageDiagnostics.activePackageCount,
|
|
39
|
-
blocked: packageDiagnostics.blockedPackageCount,
|
|
40
|
-
warnings: packageDiagnostics.warnings,
|
|
41
|
-
violations: packageDiagnostics.violations.map((violation) => ({
|
|
42
|
-
packageName: violation.packageName,
|
|
43
|
-
version: violation.version,
|
|
44
|
-
code: violation.code,
|
|
45
|
-
message: violation.message,
|
|
46
|
-
remediation: violation.remediation,
|
|
47
|
-
})),
|
|
48
|
-
},
|
|
49
|
-
secrets: {
|
|
50
|
-
status: secretDiagnostics.health.hasIssues ? 'degraded' : 'ok',
|
|
51
|
-
missingRequired: secretDiagnostics.health.missingRequired,
|
|
52
|
-
expired: secretDiagnostics.health.expired,
|
|
53
|
-
warnings: secretDiagnostics.health.warnings,
|
|
54
|
-
total: secretDiagnostics.total,
|
|
55
|
-
active: secretDiagnostics.active,
|
|
56
|
-
dueForRotation: secretDiagnostics.dueForRotation,
|
|
57
|
-
},
|
|
58
|
-
budget: budgetSnapshot
|
|
59
|
-
? {
|
|
60
|
-
severity: budgetSnapshot.directive.severity,
|
|
61
|
-
profile: budgetSnapshot.directive.profile,
|
|
62
|
-
pacingDelayMs: budgetSnapshot.directive.pacingDelayMs,
|
|
63
|
-
manualProfile: budgetSnapshot.manualProfile,
|
|
64
|
-
daily: budgetSnapshot.daily,
|
|
65
|
-
session: budgetSnapshot.session,
|
|
66
|
-
providers: budgetSnapshot.providers,
|
|
67
|
-
}
|
|
68
|
-
: undefined,
|
|
69
|
-
routing: routingSnapshot,
|
|
70
|
-
backups: backupDiagnostics
|
|
71
|
-
? {
|
|
72
|
-
status: backupDiagnostics.status,
|
|
73
|
-
lastSnapshotAt: backupDiagnostics.lastSnapshotAt,
|
|
74
|
-
lastRestoreAt: backupDiagnostics.lastRestoreAt,
|
|
75
|
-
validationFailureCount: backupDiagnostics.validationFailureCount,
|
|
76
|
-
recommendationCount: backupDiagnostics.recommendations.length,
|
|
77
|
-
}
|
|
78
|
-
: undefined,
|
|
79
|
-
mcpServers: servers.map((s) => ({
|
|
80
|
-
id: s.id,
|
|
81
|
-
name: s.name,
|
|
82
|
-
state: s.state,
|
|
83
|
-
toolCount: s.toolCount,
|
|
84
|
-
health: {
|
|
85
|
-
circuit: s.health.state,
|
|
86
|
-
failureCount: s.health.metrics.failureCount,
|
|
87
|
-
remainingCooldownMs: s.health.remainingCooldownMs,
|
|
88
|
-
},
|
|
89
|
-
})),
|
|
90
|
-
};
|
|
8
|
+
const data = await buildHealthData(deps, startTime);
|
|
91
9
|
sendOk(res, data);
|
|
92
10
|
};
|
|
93
11
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { sendError, sendOk } from '../shared.js';
|
|
2
|
+
import { getLearningSystem } from '../../services/learning-system.js';
|
|
3
|
+
import { getSkillBuilder } from '../../services/skill-builder.js';
|
|
4
|
+
import { getAutoConfigurer } from '../../services/auto-configurer.js';
|
|
5
|
+
import { getSelfHealingService } from '../../services/self-healing.js';
|
|
6
|
+
import { getAgentImprovementTool } from '../../tools/agent-improvement.js';
|
|
7
|
+
export function registerSelfRoutes(app) {
|
|
8
|
+
app.get('/self/stats', (_req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const learning = getLearningSystem();
|
|
11
|
+
const stats = learning.getStats();
|
|
12
|
+
sendOk(res, stats);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
sendError(res, `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
app.get('/self/health', async (_req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const healer = getSelfHealingService();
|
|
21
|
+
const metrics = await healer.performHealthCheck();
|
|
22
|
+
sendOk(res, { metrics });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
sendError(res, `Health check failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
app.get('/self/learn', (req, res) => {
|
|
29
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
30
|
+
if (!query) {
|
|
31
|
+
sendError(res, 'Query parameter "q" is required.', 400);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const learning = getLearningSystem();
|
|
36
|
+
const result = learning.query(query);
|
|
37
|
+
sendOk(res, result);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
sendError(res, `Query failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
app.post('/self/learn', async (req, res) => {
|
|
44
|
+
const { pattern, solution, type } = req.body ?? {};
|
|
45
|
+
if (!pattern || !solution) {
|
|
46
|
+
sendError(res, 'Required fields: pattern, solution', 400);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
let entryType = 'fix';
|
|
51
|
+
if (type === 'success' ||
|
|
52
|
+
type === 'failure' ||
|
|
53
|
+
type === 'pattern' ||
|
|
54
|
+
type === 'fix' ||
|
|
55
|
+
type === 'config') {
|
|
56
|
+
entryType = type;
|
|
57
|
+
}
|
|
58
|
+
const learning = getLearningSystem();
|
|
59
|
+
await learning.learn(entryType, pattern, {}, solution, 'success');
|
|
60
|
+
sendOk(res, { message: `Learned: ${pattern} -> ${solution}` });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
sendError(res, `Learn failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.get('/self/services', (_req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const configurer = getAutoConfigurer();
|
|
69
|
+
const services = configurer.listKnownServices();
|
|
70
|
+
sendOk(res, { services });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
sendError(res, `Failed to list services: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
app.get('/self/configure/:service', async (req, res) => {
|
|
77
|
+
const service = req.params.service;
|
|
78
|
+
try {
|
|
79
|
+
const configurer = getAutoConfigurer();
|
|
80
|
+
const { config, steps } = await configurer.fetchDocsAndConfig(service);
|
|
81
|
+
sendOk(res, { service, config, steps });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
sendError(res, `Configure failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
app.post('/self/configure/:service', async (req, res) => {
|
|
88
|
+
const service = req.params.service;
|
|
89
|
+
const config = req.body;
|
|
90
|
+
try {
|
|
91
|
+
const configurer = getAutoConfigurer();
|
|
92
|
+
const result = await configurer.saveServiceConfig(service, config);
|
|
93
|
+
sendOk(res, result);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
sendError(res, `Save config failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
app.post('/self/build-skill', async (req, res) => {
|
|
100
|
+
const { description, purpose } = req.body ?? {};
|
|
101
|
+
if (!description) {
|
|
102
|
+
sendError(res, 'Required field: description', 400);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const builder = getSkillBuilder();
|
|
107
|
+
const skill = await builder.buildFromRequest({ description, purpose });
|
|
108
|
+
sendOk(res, {
|
|
109
|
+
id: skill.id,
|
|
110
|
+
name: skill.name,
|
|
111
|
+
category: skill.category,
|
|
112
|
+
location: `${builder.getCustomSkillsDir()}/${skill.id}.ts`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
sendError(res, `Build skill failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
app.get('/self/skills', (_req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const builder = getSkillBuilder();
|
|
122
|
+
const skills = builder.getCustomSkills();
|
|
123
|
+
sendOk(res, { skills });
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
sendError(res, `Failed to list skills: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
app.post('/self/improve', async (req, res) => {
|
|
130
|
+
const { goal, context } = req.body ?? {};
|
|
131
|
+
if (!goal) {
|
|
132
|
+
sendError(res, 'Required field: goal', 400);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const tool = getAgentImprovementTool();
|
|
137
|
+
const result = await tool.handleImprovementRequest({ goal, context });
|
|
138
|
+
sendOk(res, result);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
sendError(res, `Improve request failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { sendError, sendOk } from '../shared.js';
|
|
2
|
+
import { getSkillBuilder } from '../../services/skill-builder.js';
|
|
3
|
+
import { getIntentParser } from '../../services/skill-acquisition/intent-parser.js';
|
|
4
|
+
import { getResearchEngine } from '../../services/skill-acquisition/research-engine.js';
|
|
5
|
+
export function registerSkillAcquisitionRoutes(app) {
|
|
6
|
+
// POST /api/skills/acquire - Start acquiring a new skill
|
|
7
|
+
app.post('/skills/acquire', async (req, res) => {
|
|
8
|
+
const { request } = req.body ?? {};
|
|
9
|
+
if (!request) {
|
|
10
|
+
sendError(res, 'Required field: request (natural language description)', 400);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const intentParser = getIntentParser();
|
|
15
|
+
const intent = await intentParser.parseIntent(request);
|
|
16
|
+
const researchEngine = getResearchEngine();
|
|
17
|
+
const research = await researchEngine.research(intent);
|
|
18
|
+
const skillBuilder = getSkillBuilder();
|
|
19
|
+
const skill = await skillBuilder.buildFromRequest({
|
|
20
|
+
description: request,
|
|
21
|
+
purpose: 'user requested',
|
|
22
|
+
});
|
|
23
|
+
sendOk(res, {
|
|
24
|
+
status: 'generated',
|
|
25
|
+
intent,
|
|
26
|
+
research,
|
|
27
|
+
skill: {
|
|
28
|
+
id: skill.id,
|
|
29
|
+
name: skill.name,
|
|
30
|
+
description: skill.description,
|
|
31
|
+
category: skill.category,
|
|
32
|
+
},
|
|
33
|
+
nextSteps: [
|
|
34
|
+
'Review the generated skill',
|
|
35
|
+
'Provide credentials if needed',
|
|
36
|
+
'Approve to activate the skill',
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
sendError(res, `Skill acquisition failed: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// GET /api/skills - List all custom skills
|
|
45
|
+
app.get('/skills', (_req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const builder = getSkillBuilder();
|
|
48
|
+
const customSkills = builder.getCustomSkills();
|
|
49
|
+
sendOk(res, {
|
|
50
|
+
customSkills,
|
|
51
|
+
total: customSkills.length,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
sendError(res, `Failed to list skills: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// GET /api/skills/:id - Get skill details
|
|
59
|
+
app.get('/skills/:id', (req, res) => {
|
|
60
|
+
const { id } = req.params;
|
|
61
|
+
try {
|
|
62
|
+
const builder = getSkillBuilder();
|
|
63
|
+
const skill = builder.getSkill(id);
|
|
64
|
+
if (!skill) {
|
|
65
|
+
sendError(res, 'Skill not found', 404);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
sendOk(res, skill);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
sendError(res, `Failed to get skill: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// DELETE /api/skills/:id - Delete a custom skill
|
|
75
|
+
app.delete('/skills/:id', async (req, res) => {
|
|
76
|
+
const { id } = req.params;
|
|
77
|
+
try {
|
|
78
|
+
const builder = getSkillBuilder();
|
|
79
|
+
const deleted = await builder.deleteSkill(id);
|
|
80
|
+
if (!deleted) {
|
|
81
|
+
sendError(res, 'Skill not found or could not be deleted', 404);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
sendOk(res, { message: 'Skill deleted successfully' });
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
sendError(res, `Failed to delete skill: ${err instanceof Error ? err.message : String(err)}`, 500);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|