agent-tool-forge 0.3.0

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -0
  3. package/lib/agent-registry.js +170 -0
  4. package/lib/api-client.js +792 -0
  5. package/lib/api-loader.js +260 -0
  6. package/lib/auth.d.ts +25 -0
  7. package/lib/auth.js +158 -0
  8. package/lib/checks/check-adapter.js +172 -0
  9. package/lib/checks/compose.js +42 -0
  10. package/lib/checks/content-match.js +14 -0
  11. package/lib/checks/cost-budget.js +11 -0
  12. package/lib/checks/index.js +18 -0
  13. package/lib/checks/json-valid.js +15 -0
  14. package/lib/checks/latency.js +11 -0
  15. package/lib/checks/length-bounds.js +17 -0
  16. package/lib/checks/negative-match.js +14 -0
  17. package/lib/checks/no-hallucinated-numbers.js +63 -0
  18. package/lib/checks/non-empty.js +34 -0
  19. package/lib/checks/regex-match.js +12 -0
  20. package/lib/checks/run-checks.js +84 -0
  21. package/lib/checks/schema-match.js +26 -0
  22. package/lib/checks/tool-call-count.js +16 -0
  23. package/lib/checks/tool-selection.js +34 -0
  24. package/lib/checks/types.js +45 -0
  25. package/lib/comparison/compare.js +86 -0
  26. package/lib/comparison/format.js +104 -0
  27. package/lib/comparison/index.js +6 -0
  28. package/lib/comparison/statistics.js +59 -0
  29. package/lib/comparison/types.js +41 -0
  30. package/lib/config-schema.js +200 -0
  31. package/lib/config.d.ts +66 -0
  32. package/lib/conversation-store.d.ts +77 -0
  33. package/lib/conversation-store.js +443 -0
  34. package/lib/db.d.ts +6 -0
  35. package/lib/db.js +1112 -0
  36. package/lib/dep-check.js +99 -0
  37. package/lib/drift-background.js +61 -0
  38. package/lib/drift-monitor.js +187 -0
  39. package/lib/eval-runner.js +566 -0
  40. package/lib/fixtures/fixture-store.js +161 -0
  41. package/lib/fixtures/index.js +11 -0
  42. package/lib/forge-engine.js +982 -0
  43. package/lib/forge-eval-generator.js +417 -0
  44. package/lib/forge-file-writer.js +386 -0
  45. package/lib/forge-service-client.js +190 -0
  46. package/lib/forge-service.d.ts +4 -0
  47. package/lib/forge-service.js +655 -0
  48. package/lib/forge-verifier-generator.js +271 -0
  49. package/lib/handlers/admin.js +151 -0
  50. package/lib/handlers/agents.js +229 -0
  51. package/lib/handlers/chat-resume.js +334 -0
  52. package/lib/handlers/chat-sync.js +320 -0
  53. package/lib/handlers/chat.js +320 -0
  54. package/lib/handlers/conversations.js +92 -0
  55. package/lib/handlers/preferences.js +88 -0
  56. package/lib/handlers/tools-list.js +58 -0
  57. package/lib/hitl-engine.d.ts +60 -0
  58. package/lib/hitl-engine.js +261 -0
  59. package/lib/http-utils.js +92 -0
  60. package/lib/index.d.ts +20 -0
  61. package/lib/index.js +141 -0
  62. package/lib/init.js +636 -0
  63. package/lib/manual-entry.js +59 -0
  64. package/lib/mcp-server.js +252 -0
  65. package/lib/output-groups.js +54 -0
  66. package/lib/postgres-store.d.ts +31 -0
  67. package/lib/postgres-store.js +465 -0
  68. package/lib/preference-store.d.ts +47 -0
  69. package/lib/preference-store.js +79 -0
  70. package/lib/prompt-store.d.ts +42 -0
  71. package/lib/prompt-store.js +60 -0
  72. package/lib/rate-limiter.d.ts +30 -0
  73. package/lib/rate-limiter.js +104 -0
  74. package/lib/react-engine.d.ts +110 -0
  75. package/lib/react-engine.js +337 -0
  76. package/lib/runner/cli.js +156 -0
  77. package/lib/runner/cost-estimator.js +71 -0
  78. package/lib/runner/gate.js +46 -0
  79. package/lib/runner/index.js +165 -0
  80. package/lib/sidecar.d.ts +83 -0
  81. package/lib/sidecar.js +161 -0
  82. package/lib/sse.d.ts +15 -0
  83. package/lib/sse.js +30 -0
  84. package/lib/tools-scanner.js +91 -0
  85. package/lib/tui.js +253 -0
  86. package/lib/verifier-report.js +78 -0
  87. package/lib/verifier-runner.js +338 -0
  88. package/lib/verifier-scanner.js +70 -0
  89. package/lib/verifier-worker-pool.js +196 -0
  90. package/lib/views/chat.js +340 -0
  91. package/lib/views/endpoints.js +203 -0
  92. package/lib/views/eval-run.js +206 -0
  93. package/lib/views/forge-agent.js +538 -0
  94. package/lib/views/forge.js +410 -0
  95. package/lib/views/main-menu.js +275 -0
  96. package/lib/views/mediation.js +381 -0
  97. package/lib/views/model-compare.js +430 -0
  98. package/lib/views/model-comparison.js +333 -0
  99. package/lib/views/onboarding.js +470 -0
  100. package/lib/views/performance.js +237 -0
  101. package/lib/views/run-evals.js +205 -0
  102. package/lib/views/settings.js +829 -0
  103. package/lib/views/tools-evals.js +514 -0
  104. package/lib/views/verifier-coverage.js +617 -0
  105. package/lib/workers/verifier-worker.js +52 -0
  106. package/package.json +123 -0
  107. package/widget/forge-chat.js +789 -0
@@ -0,0 +1,617 @@
1
+ /**
2
+ * Verifier Coverage View — Interactive DB-backed verifier management.
3
+ *
4
+ * Shows promoted tools with their bound verifiers, supports attaching/detaching
5
+ * verifiers, creating schema/pattern verifiers inline, and registering
6
+ * discovered custom verifiers from disk.
7
+ */
8
+
9
+ import blessed from 'blessed';
10
+ import { existsSync } from 'fs';
11
+ import { resolve } from 'path';
12
+ import {
13
+ getDb, getAllToolRegistry, getAllVerifiers, getVerifiersForTool,
14
+ upsertVerifier, upsertVerifierBinding, removeVerifierBinding,
15
+ getBindingsForVerifier
16
+ } from '../db.js';
17
+ import { getExistingVerifiers } from '../verifier-scanner.js';
18
+
19
+ /**
20
+ * Load data from DB + filesystem.
21
+ * @param {object} config
22
+ * @param {import('better-sqlite3').Database} db
23
+ */
24
+ function loadData(config, db) {
25
+ const tools = getAllToolRegistry(db).filter(r => r.lifecycle_state === 'promoted');
26
+ const allVerifiers = getAllVerifiers(db);
27
+ const rows = tools.map(tool => {
28
+ const bound = getVerifiersForTool(db, tool.tool_name);
29
+ return {
30
+ tool: tool.tool_name,
31
+ verifiers: bound,
32
+ verifierDisplay: bound.length > 0
33
+ ? bound.map(v => `${v.aciru_order} ${v.verifier_name}`).join(', ')
34
+ : '—',
35
+ hasVerifiers: bound.length > 0
36
+ };
37
+ });
38
+
39
+ // Discover unregistered verifiers from filesystem
40
+ let unregistered = [];
41
+ const verification = config?.verification || {};
42
+ if (verification?.verifiersDir) {
43
+ const onDisk = getExistingVerifiers(verification);
44
+ const inDb = new Set(allVerifiers.map(v => v.verifier_name));
45
+ unregistered = onDisk.filter(name => !inDb.has(name));
46
+ }
47
+
48
+ return { rows, allVerifiers, unregistered };
49
+ }
50
+
51
+ /**
52
+ * Compute next ACIRU sequence number for a category.
53
+ * @param {object[]} allVerifiers
54
+ * @param {string} category
55
+ * @returns {string}
56
+ */
57
+ function nextAciruOrder(allVerifiers, category) {
58
+ const prefix = category + '-';
59
+ const existing = allVerifiers
60
+ .filter(v => v.aciru_order.startsWith(prefix))
61
+ .map(v => parseInt(v.aciru_order.slice(prefix.length), 10))
62
+ .filter(n => !isNaN(n));
63
+ const next = existing.length > 0 ? Math.max(...existing) + 1 : 1;
64
+ return `${category}-${String(next).padStart(4, '0')}`;
65
+ }
66
+
67
+ export function createView({ screen, content, config, navigate, setFooter, screenKey, openPopup, closePopup }) {
68
+ // Resolve DB
69
+ let db;
70
+ try {
71
+ const dbPath = resolve(process.cwd(), config?.dbPath || 'forge.db');
72
+ if (existsSync(dbPath)) {
73
+ db = getDb(dbPath);
74
+ }
75
+ } catch { /* ignore */ }
76
+
77
+ const container = blessed.box({ top: 0, left: 0, width: '100%', height: '100%', tags: true });
78
+
79
+ const table = blessed.listtable({
80
+ parent: container,
81
+ top: 0, left: 0, width: '100%', height: '100%-2',
82
+ tags: true, keys: true, vi: true, mouse: true,
83
+ border: { type: 'line' }, align: 'left',
84
+ style: {
85
+ header: { bold: true, fg: 'cyan' },
86
+ cell: { selected: { bg: '#1a3a5c', fg: 'white' } },
87
+ border: { fg: '#333333' }
88
+ },
89
+ pad: 1
90
+ });
91
+
92
+ const summaryBar = blessed.box({
93
+ parent: container,
94
+ bottom: 0, left: 0, width: '100%', height: 2, tags: true
95
+ });
96
+
97
+ setFooter(
98
+ ' {cyan-fg}Enter{/cyan-fg} actions {cyan-fg}a{/cyan-fg} attach {cyan-fg}d{/cyan-fg} detach ' +
99
+ '{cyan-fg}n{/cyan-fg} new {cyan-fg}r{/cyan-fg} refresh {cyan-fg}b{/cyan-fg} back'
100
+ );
101
+
102
+ let currentData = { rows: [], allVerifiers: [], unregistered: [] };
103
+
104
+ function refreshView() {
105
+ if (!db) {
106
+ table.setData([
107
+ ['Tool', 'ACIRU Order', 'Verifiers', 'Status'],
108
+ ['No database available', '', '', '']
109
+ ]);
110
+ summaryBar.setContent('');
111
+ screen.render();
112
+ return;
113
+ }
114
+
115
+ try {
116
+ currentData = loadData(config, db);
117
+ } catch (err) {
118
+ table.setData([
119
+ ['Tool', 'ACIRU Order', 'Verifiers', 'Status'],
120
+ [`Error: ${String(err.message).replace(/\{/g, '\\{')}`, '', '', '']
121
+ ]);
122
+ screen.render();
123
+ return;
124
+ }
125
+
126
+ const { rows, allVerifiers, unregistered } = currentData;
127
+
128
+ if (rows.length === 0) {
129
+ table.setData([
130
+ ['Tool', 'ACIRU Order', 'Verifiers', 'Status'],
131
+ ['No promoted tools found', '', '', '']
132
+ ]);
133
+ summaryBar.setContent('');
134
+ screen.render();
135
+ return;
136
+ }
137
+
138
+ table.setData([
139
+ ['Tool', 'ACIRU Order', 'Verifiers', 'Status'],
140
+ ...rows.map(r => {
141
+ const orders = r.verifiers.map(v => v.aciru_order).join(', ') || '—';
142
+ const names = r.verifiers.map(v => {
143
+ const bindings = getBindingsForVerifier(db, v.verifier_name);
144
+ const isWild = bindings.some(b => b.tool_name === '*');
145
+ return isWild ? `${v.verifier_name} [*]` : v.verifier_name;
146
+ }).join(', ') || '—';
147
+ const status = r.hasVerifiers
148
+ ? '{green-fg}✓{/green-fg}'
149
+ : '{yellow-fg}⚠ none{/yellow-fg}';
150
+ return [r.tool, orders, names, status];
151
+ })
152
+ ]);
153
+
154
+ const unverifiedCount = rows.filter(r => !r.hasVerifiers).length;
155
+ summaryBar.setContent(
156
+ ` {white-fg}${rows.length}{/white-fg} tools | ` +
157
+ `{white-fg}${allVerifiers.length}{/white-fg} verifiers | ` +
158
+ (unverifiedCount > 0
159
+ ? `{yellow-fg}${unverifiedCount} unverified{/yellow-fg}`
160
+ : '{green-fg}all verified{/green-fg}') +
161
+ (unregistered.length > 0
162
+ ? ` | {cyan-fg}${unregistered.length} discovered on disk{/cyan-fg}`
163
+ : '')
164
+ );
165
+
166
+ screen.render();
167
+ }
168
+
169
+ // ── Action Popup ──────────────────────────────────────────────────────────
170
+
171
+ function getSelectedRow() {
172
+ const sel = table.selected;
173
+ if (sel < 1 || sel > currentData.rows.length) return null;
174
+ return currentData.rows[sel - 1];
175
+ }
176
+
177
+ function showActionMenu(row) {
178
+ const items = ['Attach verifier', 'Detach verifier', 'Create schema verifier', 'Create pattern verifier'];
179
+ if (currentData.unregistered.length > 0) items.push('Register discovered');
180
+ items.push('Cancel');
181
+
182
+ const popup = blessed.list({
183
+ parent: screen,
184
+ top: 'center', left: 'center',
185
+ width: 40, height: items.length + 2,
186
+ tags: true, keys: true, vi: true, mouse: true,
187
+ border: { type: 'line' },
188
+ label: ` {cyan-fg}${row.tool}{/cyan-fg} `,
189
+ style: {
190
+ selected: { bg: '#1a3a5c', fg: 'white' },
191
+ border: { fg: 'cyan' }
192
+ },
193
+ items
194
+ });
195
+
196
+ openPopup && openPopup();
197
+ popup.focus();
198
+ screen.render();
199
+
200
+ popup.on('select', (item, idx) => {
201
+ popup.detach();
202
+ closePopup && closePopup();
203
+ screen.render();
204
+
205
+ switch (idx) {
206
+ case 0: showAttachMenu(row); break;
207
+ case 1: showDetachMenu(row); break;
208
+ case 2: showSchemaForm(row); break;
209
+ case 3: showPatternForm(row); break;
210
+ case 4:
211
+ if (currentData.unregistered.length > 0) showRegisterMenu(row);
212
+ break;
213
+ }
214
+ });
215
+
216
+ popup.key(['escape', 'q'], () => {
217
+ popup.detach();
218
+ closePopup && closePopup();
219
+ table.focus();
220
+ screen.render();
221
+ });
222
+ }
223
+
224
+ // ── Attach ────────────────────────────────────────────────────────────────
225
+
226
+ function showAttachMenu(row) {
227
+ const boundNames = new Set(row.verifiers.map(v => v.verifier_name));
228
+ const available = currentData.allVerifiers.filter(v => !boundNames.has(v.verifier_name));
229
+
230
+ if (available.length === 0) {
231
+ showMessage('No unbound verifiers available');
232
+ return;
233
+ }
234
+
235
+ const items = available.map(v => `${v.aciru_order} ${v.verifier_name} (${v.type})`);
236
+ items.push('Cancel');
237
+
238
+ const popup = blessed.list({
239
+ parent: screen,
240
+ top: 'center', left: 'center',
241
+ width: 50, height: Math.min(items.length + 2, 15),
242
+ tags: true, keys: true, vi: true, mouse: true,
243
+ border: { type: 'line' },
244
+ label: ' Attach verifier ',
245
+ style: { selected: { bg: '#1a3a5c', fg: 'white' }, border: { fg: 'green' } },
246
+ items
247
+ });
248
+
249
+ openPopup && openPopup();
250
+ popup.focus();
251
+ screen.render();
252
+
253
+ popup.on('select', (item, idx) => {
254
+ popup.detach();
255
+ closePopup && closePopup();
256
+ if (idx < available.length) {
257
+ upsertVerifierBinding(db, { verifier_name: available[idx].verifier_name, tool_name: row.tool });
258
+ refreshView();
259
+ }
260
+ table.focus();
261
+ screen.render();
262
+ });
263
+
264
+ popup.key(['escape'], () => {
265
+ popup.detach();
266
+ closePopup && closePopup();
267
+ table.focus();
268
+ screen.render();
269
+ });
270
+ }
271
+
272
+ // ── Detach ────────────────────────────────────────────────────────────────
273
+
274
+ function showDetachMenu(row) {
275
+ if (row.verifiers.length === 0) {
276
+ showMessage('No verifiers to detach');
277
+ return;
278
+ }
279
+
280
+ const items = row.verifiers.map(v => `${v.aciru_order} ${v.verifier_name}`);
281
+ items.push('Cancel');
282
+
283
+ const popup = blessed.list({
284
+ parent: screen,
285
+ top: 'center', left: 'center',
286
+ width: 50, height: Math.min(items.length + 2, 15),
287
+ tags: true, keys: true, vi: true, mouse: true,
288
+ border: { type: 'line' },
289
+ label: ' Detach verifier ',
290
+ style: { selected: { bg: '#1a3a5c', fg: 'white' }, border: { fg: 'yellow' } },
291
+ items
292
+ });
293
+
294
+ openPopup && openPopup();
295
+ popup.focus();
296
+ screen.render();
297
+
298
+ popup.on('select', (item, idx) => {
299
+ popup.detach();
300
+ closePopup && closePopup();
301
+ if (idx < row.verifiers.length) {
302
+ removeVerifierBinding(db, row.verifiers[idx].verifier_name, row.tool);
303
+ refreshView();
304
+ }
305
+ table.focus();
306
+ screen.render();
307
+ });
308
+
309
+ popup.key(['escape'], () => {
310
+ popup.detach();
311
+ closePopup && closePopup();
312
+ table.focus();
313
+ screen.render();
314
+ });
315
+ }
316
+
317
+ // ── Schema Verifier Form ──────────────────────────────────────────────────
318
+
319
+ function showSchemaForm(row) {
320
+ const form = blessed.form({
321
+ parent: screen,
322
+ top: 'center', left: 'center',
323
+ width: 60, height: 14,
324
+ tags: true, keys: true,
325
+ border: { type: 'line' },
326
+ label: ' Create Schema Verifier ',
327
+ style: { border: { fg: 'green' } }
328
+ });
329
+
330
+ blessed.text({ parent: form, top: 1, left: 2, content: 'Name:', tags: true });
331
+ const nameInput = blessed.textbox({
332
+ parent: form, top: 1, left: 10, width: 44, height: 1,
333
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }
334
+ });
335
+
336
+ blessed.text({ parent: form, top: 3, left: 2, content: 'Required fields (comma-sep):', tags: true });
337
+ const reqInput = blessed.textbox({
338
+ parent: form, top: 4, left: 2, width: 52, height: 1,
339
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }
340
+ });
341
+
342
+ blessed.text({ parent: form, top: 6, left: 2, content: 'Property types (key:type, key:type):', tags: true });
343
+ const propsInput = blessed.textbox({
344
+ parent: form, top: 7, left: 2, width: 52, height: 1,
345
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }
346
+ });
347
+
348
+ blessed.text({ parent: form, top: 9, left: 2, content: 'ACIRU category:', tags: true });
349
+ const catInput = blessed.textbox({
350
+ parent: form, top: 9, left: 20, width: 5, height: 1,
351
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }, value: 'I'
352
+ });
353
+
354
+ const submitBtn = blessed.button({
355
+ parent: form, top: 11, left: 2, width: 12, height: 1,
356
+ content: ' Create ', tags: true, mouse: true,
357
+ style: { fg: 'white', bg: 'green', focus: { bg: 'cyan' } }
358
+ });
359
+
360
+ const cancelBtn = blessed.button({
361
+ parent: form, top: 11, left: 16, width: 12, height: 1,
362
+ content: ' Cancel ', tags: true, mouse: true,
363
+ style: { fg: 'white', bg: '#555', focus: { bg: '#777' } }
364
+ });
365
+
366
+ openPopup && openPopup();
367
+ nameInput.focus();
368
+ screen.render();
369
+
370
+ submitBtn.on('press', () => {
371
+ const name = nameInput.getValue().trim();
372
+ if (!name) { showMessage('Name is required'); return; }
373
+
374
+ const category = (catInput.getValue().trim() || 'I').toUpperCase();
375
+ const order = nextAciruOrder(currentData.allVerifiers, category);
376
+ const required = reqInput.getValue().trim()
377
+ ? reqInput.getValue().trim().split(',').map(s => s.trim()).filter(Boolean)
378
+ : [];
379
+ const properties = {};
380
+ const propsStr = propsInput.getValue().trim();
381
+ if (propsStr) {
382
+ for (const pair of propsStr.split(',')) {
383
+ const [k, t] = pair.split(':').map(s => s.trim());
384
+ if (k && t) properties[k] = { type: t };
385
+ }
386
+ }
387
+
388
+ upsertVerifier(db, {
389
+ verifier_name: name,
390
+ display_name: name,
391
+ type: 'schema',
392
+ aciru_category: category,
393
+ aciru_order: order,
394
+ spec_json: JSON.stringify({ required, properties }),
395
+ description: `Schema verifier for ${row.tool}`
396
+ });
397
+ upsertVerifierBinding(db, { verifier_name: name, tool_name: row.tool });
398
+
399
+ form.detach();
400
+ closePopup && closePopup();
401
+ refreshView();
402
+ table.focus();
403
+ });
404
+
405
+ cancelBtn.on('press', () => {
406
+ form.detach();
407
+ closePopup && closePopup();
408
+ table.focus();
409
+ screen.render();
410
+ });
411
+
412
+ form.key(['escape'], () => {
413
+ form.detach();
414
+ closePopup && closePopup();
415
+ table.focus();
416
+ screen.render();
417
+ });
418
+ }
419
+
420
+ // ── Pattern Verifier Form ─────────────────────────────────────────────────
421
+
422
+ function showPatternForm(row) {
423
+ const form = blessed.form({
424
+ parent: screen,
425
+ top: 'center', left: 'center',
426
+ width: 60, height: 14,
427
+ tags: true, keys: true,
428
+ border: { type: 'line' },
429
+ label: ' Create Pattern Verifier ',
430
+ style: { border: { fg: 'green' } }
431
+ });
432
+
433
+ blessed.text({ parent: form, top: 1, left: 2, content: 'Name:', tags: true });
434
+ const nameInput = blessed.textbox({
435
+ parent: form, top: 1, left: 10, width: 44, height: 1,
436
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }
437
+ });
438
+
439
+ blessed.text({ parent: form, top: 3, left: 2, content: 'Match pattern (regex, optional):', tags: true });
440
+ const matchInput = blessed.textbox({
441
+ parent: form, top: 4, left: 2, width: 52, height: 1,
442
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }
443
+ });
444
+
445
+ blessed.text({ parent: form, top: 6, left: 2, content: 'Reject pattern (regex, optional):', tags: true });
446
+ const rejectInput = blessed.textbox({
447
+ parent: form, top: 7, left: 2, width: 52, height: 1,
448
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }
449
+ });
450
+
451
+ blessed.text({ parent: form, top: 9, left: 2, content: 'Outcome:', tags: true });
452
+ const outcomeInput = blessed.textbox({
453
+ parent: form, top: 9, left: 12, width: 10, height: 1,
454
+ inputOnFocus: true, style: { fg: 'white', bg: '#333' }, value: 'warn'
455
+ });
456
+
457
+ const submitBtn = blessed.button({
458
+ parent: form, top: 11, left: 2, width: 12, height: 1,
459
+ content: ' Create ', tags: true, mouse: true,
460
+ style: { fg: 'white', bg: 'green', focus: { bg: 'cyan' } }
461
+ });
462
+
463
+ const cancelBtn = blessed.button({
464
+ parent: form, top: 11, left: 16, width: 12, height: 1,
465
+ content: ' Cancel ', tags: true, mouse: true,
466
+ style: { fg: 'white', bg: '#555', focus: { bg: '#777' } }
467
+ });
468
+
469
+ openPopup && openPopup();
470
+ nameInput.focus();
471
+ screen.render();
472
+
473
+ submitBtn.on('press', () => {
474
+ const name = nameInput.getValue().trim();
475
+ if (!name) { showMessage('Name is required'); return; }
476
+
477
+ const order = nextAciruOrder(currentData.allVerifiers, 'I');
478
+ const spec = {};
479
+ const match = matchInput.getValue().trim();
480
+ const reject = rejectInput.getValue().trim();
481
+ const outcome = outcomeInput.getValue().trim() || 'warn';
482
+ if (match) spec.match = match;
483
+ if (reject) spec.reject = reject;
484
+ spec.outcome = outcome;
485
+
486
+ upsertVerifier(db, {
487
+ verifier_name: name,
488
+ display_name: name,
489
+ type: 'pattern',
490
+ aciru_category: 'I',
491
+ aciru_order: order,
492
+ spec_json: JSON.stringify(spec),
493
+ description: `Pattern verifier for ${row.tool}`
494
+ });
495
+ upsertVerifierBinding(db, { verifier_name: name, tool_name: row.tool });
496
+
497
+ form.detach();
498
+ closePopup && closePopup();
499
+ refreshView();
500
+ table.focus();
501
+ });
502
+
503
+ cancelBtn.on('press', () => {
504
+ form.detach();
505
+ closePopup && closePopup();
506
+ table.focus();
507
+ screen.render();
508
+ });
509
+
510
+ form.key(['escape'], () => {
511
+ form.detach();
512
+ closePopup && closePopup();
513
+ table.focus();
514
+ screen.render();
515
+ });
516
+ }
517
+
518
+ // ── Register Discovered ───────────────────────────────────────────────────
519
+
520
+ function showRegisterMenu(row) {
521
+ const items = [...currentData.unregistered, 'Cancel'];
522
+
523
+ const popup = blessed.list({
524
+ parent: screen,
525
+ top: 'center', left: 'center',
526
+ width: 50, height: Math.min(items.length + 2, 15),
527
+ tags: true, keys: true, vi: true, mouse: true,
528
+ border: { type: 'line' },
529
+ label: ' Register discovered verifier ',
530
+ style: { selected: { bg: '#1a3a5c', fg: 'white' }, border: { fg: 'cyan' } },
531
+ items
532
+ });
533
+
534
+ openPopup && openPopup();
535
+ popup.focus();
536
+ screen.render();
537
+
538
+ popup.on('select', (item, idx) => {
539
+ popup.detach();
540
+ closePopup && closePopup();
541
+ if (idx < currentData.unregistered.length) {
542
+ const name = currentData.unregistered[idx];
543
+ const verifiersDir = config?.verification?.verifiersDir || '';
544
+ const filePath = resolve(process.cwd(), verifiersDir, `${name}.verifier.js`);
545
+ const order = nextAciruOrder(currentData.allVerifiers, 'R');
546
+
547
+ upsertVerifier(db, {
548
+ verifier_name: name,
549
+ display_name: name,
550
+ type: 'custom',
551
+ aciru_category: 'R',
552
+ aciru_order: order,
553
+ spec_json: JSON.stringify({ filePath, exportName: 'verify' }),
554
+ description: `Custom verifier discovered on disk`
555
+ });
556
+ upsertVerifierBinding(db, { verifier_name: name, tool_name: row.tool });
557
+ refreshView();
558
+ }
559
+ table.focus();
560
+ screen.render();
561
+ });
562
+
563
+ popup.key(['escape'], () => {
564
+ popup.detach();
565
+ closePopup && closePopup();
566
+ table.focus();
567
+ screen.render();
568
+ });
569
+ }
570
+
571
+ // ── Message popup ─────────────────────────────────────────────────────────
572
+
573
+ function showMessage(msg) {
574
+ const box = blessed.message({
575
+ parent: screen,
576
+ top: 'center', left: 'center',
577
+ width: msg.length + 6, height: 5,
578
+ tags: true, border: { type: 'line' },
579
+ style: { border: { fg: 'yellow' } }
580
+ });
581
+ openPopup && openPopup();
582
+ box.display(msg, 2, () => {
583
+ box.detach();
584
+ closePopup && closePopup();
585
+ table.focus();
586
+ screen.render();
587
+ });
588
+ }
589
+
590
+ // ── Key bindings ──────────────────────────────────────────────────────────
591
+
592
+ table.on('select', () => {
593
+ const row = getSelectedRow();
594
+ if (row) showActionMenu(row);
595
+ });
596
+
597
+ screenKey('a', () => {
598
+ const row = getSelectedRow();
599
+ if (row) showAttachMenu(row);
600
+ });
601
+
602
+ screenKey('d', () => {
603
+ const row = getSelectedRow();
604
+ if (row) showDetachMenu(row);
605
+ });
606
+
607
+ screenKey('n', () => {
608
+ const row = getSelectedRow();
609
+ if (row) showSchemaForm(row);
610
+ });
611
+
612
+ container.refresh = () => refreshView();
613
+
614
+ refreshView();
615
+ table.focus();
616
+ return container;
617
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Verifier Worker — ESM Worker thread for sandboxed custom verifier execution.
3
+ *
4
+ * Receives: { id, verifierPath, exportName, toolName, args, result }
5
+ * Replies: { id, outcome, message }
6
+ *
7
+ * Outcomes: 'pass' | 'warn' | 'block'
8
+ * On unhandled error: posts { id, outcome: 'warn', message: err.message }
9
+ *
10
+ * Module cache: each worker caches imported verifier modules after first load,
11
+ * so repeated calls to the same file are cheap.
12
+ */
13
+
14
+ import { parentPort } from 'worker_threads';
15
+ import path, { isAbsolute } from 'path';
16
+
17
+ if (!parentPort) throw new Error('[verifier-worker] Must be run as a Worker thread');
18
+
19
+ // Module cache: verifierPath → exported function
20
+ const moduleCache = new Map();
21
+
22
+ async function loadFn(resolved, safeExport) {
23
+ const cacheKey = `${resolved}::${safeExport}`;
24
+ if (moduleCache.has(cacheKey)) return moduleCache.get(cacheKey);
25
+
26
+ const mod = await import(resolved);
27
+ const fn = mod[safeExport] || mod.default;
28
+ moduleCache.set(cacheKey, fn ?? null);
29
+ return fn ?? null;
30
+ }
31
+
32
+ parentPort.on('message', async ({ id, verifierPath, exportName, toolName, args, result }) => {
33
+ const resolved = typeof verifierPath === 'string' ? path.resolve(verifierPath) : null;
34
+ if (!resolved || !resolved.endsWith('.js') || resolved.startsWith('data:')) {
35
+ parentPort.postMessage({ id, outcome: 'warn', message: 'invalid verifier path' });
36
+ return;
37
+ }
38
+ const safeExport = exportName && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportName) ? exportName : 'verify';
39
+ try {
40
+ const fn = await loadFn(resolved, safeExport);
41
+ if (typeof fn !== 'function') {
42
+ parentPort.postMessage({ id, outcome: 'warn', message: `Custom verifier "${verifierPath}": no verify function found` });
43
+ return;
44
+ }
45
+ const vResult = await fn(toolName, args, result);
46
+ const outcome = vResult?.outcome ?? 'pass';
47
+ const message = vResult?.message ?? null;
48
+ parentPort.postMessage({ id, outcome, message });
49
+ } catch (err) {
50
+ parentPort.postMessage({ id, outcome: 'warn', message: err?.message ?? String(err) });
51
+ }
52
+ });