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,338 @@
1
+ /**
2
+ * VerifierRunner — post-tool-call checks in the live ReAct loop.
3
+ *
4
+ * Verifier types:
5
+ * schema — JSON Schema validation on result (basic type + required checks)
6
+ * pattern — regex match/reject on result text
7
+ * custom — user-provided function loaded from verifiersDir
8
+ *
9
+ * Returns worst outcome: block > warn > pass.
10
+ * Block short-circuits (stops pipeline immediately).
11
+ */
12
+
13
+ import { insertVerifierResult } from './db.js';
14
+ import { resolve, isAbsolute } from 'path';
15
+
16
+ const OUTCOME_SEVERITY = { pass: 0, warn: 1, block: 2 };
17
+
18
+ export class VerifierRunner {
19
+ /**
20
+ * @param {import('better-sqlite3').Database} db
21
+ * @param {object} config — forge config (used for verifiersDir path + sandbox settings)
22
+ * @param {import('./verifier-worker-pool.js').VerifierWorkerPool} [workerPool] — optional pre-created pool
23
+ */
24
+ constructor(db, config = {}, workerPool = null) {
25
+ this._db = db;
26
+ this._config = config;
27
+ this._verifiers = new Map(); // toolName → verifier[]
28
+ this._workerPool = workerPool;
29
+ this._ownPool = false; // true if we created the pool internally
30
+ }
31
+
32
+ /**
33
+ * Tear down the worker pool (if we own it).
34
+ * Call this when the sidecar shuts down.
35
+ */
36
+ destroy() {
37
+ if (this._ownPool && this._workerPool) {
38
+ this._workerPool.destroy();
39
+ this._workerPool = null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Register verifiers for a tool (manual/programmatic registration).
45
+ * @param {string} toolName
46
+ * @param {Array<{ name: string, type: 'schema'|'pattern'|'custom', spec: object, order?: string }>} verifiers
47
+ */
48
+ registerVerifiers(toolName, verifiers) {
49
+ this._verifiers.set(toolName, verifiers);
50
+ }
51
+
52
+ /**
53
+ * Load all enabled verifiers and their bindings from the DB.
54
+ * Builds the internal _verifiers Map keyed by tool_name.
55
+ * Wildcard bindings stored under '*'.
56
+ *
57
+ * For custom type: spec_json = { filePath, exportName }. Runs through worker pool if sandbox enabled.
58
+ * If file missing → register a warn-only stub.
59
+ *
60
+ * @param {import('better-sqlite3').Database} db
61
+ */
62
+ async loadFromDb(db) {
63
+ const targetDb = db || this._db;
64
+ if (!targetDb) return;
65
+
66
+ // Lazily create worker pool for custom verifiers if sandbox is enabled
67
+ // and no pool was provided to the constructor.
68
+ const sandboxEnabled = this._config?.verification?.sandbox !== false;
69
+ if (sandboxEnabled && !this._workerPool) {
70
+ const { VerifierWorkerPool } = await import('./verifier-worker-pool.js');
71
+ this._workerPool = new VerifierWorkerPool({
72
+ size: this._config?.verification?.workerPoolSize ?? undefined,
73
+ timeoutMs: this._config?.verification?.customTimeout ?? 2000,
74
+ maxQueueDepth: this._config?.verification?.maxQueueDepth ?? 200
75
+ });
76
+ this._ownPool = true;
77
+ }
78
+
79
+ // Get all enabled verifiers with their bindings
80
+ const allVerifiers = targetDb.prepare(
81
+ 'SELECT * FROM verifier_registry WHERE enabled = 1 ORDER BY aciru_order ASC'
82
+ ).all();
83
+
84
+ const allBindings = targetDb.prepare(
85
+ 'SELECT * FROM verifier_tool_bindings WHERE enabled = 1'
86
+ ).all();
87
+
88
+ // Build a map: tool_name → sorted verifier specs
89
+ const toolMap = new Map();
90
+
91
+ for (const binding of allBindings) {
92
+ const verifier = allVerifiers.find(v => v.verifier_name === binding.verifier_name);
93
+ if (!verifier) continue;
94
+
95
+ let spec;
96
+ try {
97
+ spec = JSON.parse(verifier.spec_json);
98
+ } catch (err) {
99
+ process.stderr.write(`[verifier-runner] Skipping verifier "${verifier.verifier_name}": malformed spec_json: ${err.message}\n`);
100
+ continue;
101
+ }
102
+
103
+ const entry = {
104
+ name: verifier.verifier_name,
105
+ type: verifier.type,
106
+ order: verifier.aciru_order,
107
+ spec
108
+ };
109
+
110
+ // For custom verifiers, resolve the path (sandboxed to verifiersDir)
111
+ if (verifier.type === 'custom' && spec.filePath) {
112
+ const verifiersDir = this._config?.verification?.verifiersDir;
113
+ const resolvedPath = isAbsolute(spec.filePath) ? spec.filePath : resolve(spec.filePath);
114
+ if (!verifiersDir || !resolvedPath.startsWith(resolve(verifiersDir))) {
115
+ entry.spec = { fn: () => ({ outcome: 'warn', message: `Custom verifier "${verifier.verifier_name}": path outside verifiersDir` }) };
116
+ const toolName = binding.tool_name;
117
+ if (!toolMap.has(toolName)) toolMap.set(toolName, []);
118
+ toolMap.get(toolName).push(entry);
119
+ continue;
120
+ }
121
+ // If sandbox mode and worker pool available, store path for pool dispatch
122
+ if (this._workerPool) {
123
+ entry.spec = {
124
+ ...spec,
125
+ resolvedPath,
126
+ exportName: spec.exportName || 'verify',
127
+ usePool: true
128
+ };
129
+ } else {
130
+ // Sandbox disabled — import directly (dev mode)
131
+ try {
132
+ const mod = await import(resolvedPath);
133
+ const fn = mod[spec.exportName || 'verify'] || mod.default;
134
+ entry.spec = typeof fn === 'function'
135
+ ? { ...spec, fn }
136
+ : { fn: () => ({ outcome: 'warn', message: `Custom verifier "${verifier.verifier_name}": no verify function found` }) };
137
+ } catch {
138
+ entry.spec = { fn: () => ({ outcome: 'warn', message: `Custom verifier "${verifier.verifier_name}": file not found or import failed` }) };
139
+ }
140
+ }
141
+ // Store verifier role for pool timeout/crash outcome
142
+ entry.role = verifier.role ?? 'any';
143
+ }
144
+
145
+ const toolName = binding.tool_name;
146
+ if (!toolMap.has(toolName)) toolMap.set(toolName, []);
147
+ toolMap.get(toolName).push(entry);
148
+ }
149
+
150
+ // Sort each tool's verifiers by order
151
+ for (const [key, verifiers] of toolMap) {
152
+ verifiers.sort((a, b) => a.order.localeCompare(b.order));
153
+ this._verifiers.set(key, verifiers);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Run all registered verifiers for a tool against the call result.
159
+ * Merges tool-specific verifiers + wildcard ('*') verifiers.
160
+ * Deduplicates by verifier name. Sorts by ACIRU order.
161
+ * Block short-circuits (returns immediately).
162
+ *
163
+ * @param {string} toolName
164
+ * @param {object} args — tool call input
165
+ * @param {object} result — tool call result ({ status, body, error })
166
+ * @returns {{ outcome: 'pass'|'warn'|'block', message: string|null, verifierName: string|null }}
167
+ */
168
+ async verify(toolName, args, result) {
169
+ const toolSpecific = this._verifiers.get(toolName) || [];
170
+ const wildcards = this._verifiers.get('*') || [];
171
+
172
+ // Merge and deduplicate by name
173
+ const seen = new Set();
174
+ const merged = [];
175
+ for (const v of [...toolSpecific, ...wildcards]) {
176
+ if (!seen.has(v.name)) {
177
+ seen.add(v.name);
178
+ merged.push(v);
179
+ }
180
+ }
181
+
182
+ if (merged.length === 0) {
183
+ return { outcome: 'pass', message: null, verifierName: null };
184
+ }
185
+
186
+ // Sort by order field if present
187
+ merged.sort((a, b) => (a.order ?? 'Z-9999').localeCompare(b.order ?? 'Z-9999'));
188
+
189
+ let worst = { outcome: 'pass', message: null, verifierName: null };
190
+
191
+ for (const v of merged) {
192
+ let vResult;
193
+ try {
194
+ switch (v.type) {
195
+ case 'schema':
196
+ vResult = runSchemaVerifier(v.spec, result.body);
197
+ break;
198
+ case 'pattern':
199
+ vResult = runPatternVerifier(v.spec, result.body);
200
+ break;
201
+ case 'custom':
202
+ vResult = await runCustomVerifier(v.spec, toolName, args, result, this._workerPool, v.role ?? 'any');
203
+ break;
204
+ default:
205
+ vResult = { outcome: 'pass', message: `Unknown verifier type: ${v.type}` };
206
+ }
207
+ } catch (err) {
208
+ vResult = { outcome: 'warn', message: `Verifier "${v.name}" threw: ${err.message}` };
209
+ }
210
+
211
+ // Block → short-circuit immediately
212
+ if (vResult.outcome === 'block') {
213
+ return { ...vResult, verifierName: v.name };
214
+ }
215
+
216
+ if (!(vResult.outcome in OUTCOME_SEVERITY)) {
217
+ vResult = { outcome: 'warn', message: `Verifier "${v.name}" returned invalid outcome: "${vResult.outcome}"` };
218
+ }
219
+ if (OUTCOME_SEVERITY[vResult.outcome] > OUTCOME_SEVERITY[worst.outcome]) {
220
+ worst = { ...vResult, verifierName: v.name };
221
+ }
222
+ }
223
+
224
+ return worst;
225
+ }
226
+
227
+ /**
228
+ * Log a verifier result to the verifier_results table.
229
+ * @param {string} sessionId
230
+ * @param {string} toolName
231
+ * @param {{ outcome: string, message: string|null, verifierName: string|null }} result
232
+ */
233
+ logResult(sessionId, toolName, result) {
234
+ if (!this._db) return;
235
+ try {
236
+ insertVerifierResult(this._db, {
237
+ session_id: sessionId,
238
+ tool_name: toolName,
239
+ verifier_name: result.verifierName ?? 'unknown',
240
+ outcome: result.outcome,
241
+ message: result.message ?? null
242
+ });
243
+ } catch { /* log failure is non-fatal */ }
244
+ }
245
+ }
246
+
247
+ // ── Verifier implementations ─────────────────────────────────────────────
248
+
249
+ /**
250
+ * Schema verifier — basic type + required field checks.
251
+ * @param {object} schema — { required: string[], properties: { [key]: { type } } }
252
+ * @param {object} body — tool result body
253
+ * @returns {{ outcome: string, message: string|null }}
254
+ */
255
+ function runSchemaVerifier(schema, body) {
256
+ if (!body || typeof body !== 'object') {
257
+ return { outcome: 'block', message: 'Result is not an object' };
258
+ }
259
+
260
+ // Check required fields
261
+ if (schema.required) {
262
+ for (const field of schema.required) {
263
+ if (!(field in body)) {
264
+ return { outcome: 'block', message: `Missing required field: ${field}` };
265
+ }
266
+ }
267
+ }
268
+
269
+ // Check types
270
+ if (schema.properties) {
271
+ for (const [key, def] of Object.entries(schema.properties)) {
272
+ if (key in body && def.type) {
273
+ const actualType = Array.isArray(body[key]) ? 'array' : typeof body[key];
274
+ if (actualType !== def.type) {
275
+ return { outcome: 'block', message: `Field "${key}" expected type "${def.type}", got "${actualType}"` };
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ return { outcome: 'pass', message: null };
282
+ }
283
+
284
+ /**
285
+ * Pattern verifier — regex match/reject on stringified result.
286
+ * @param {{ match?: string, reject?: string, outcome?: string }} spec
287
+ * @param {object} body
288
+ * @returns {{ outcome: string, message: string|null }}
289
+ */
290
+ function runPatternVerifier(spec, body) {
291
+ const text = typeof body === 'string' ? body : JSON.stringify(body);
292
+ const outcome = spec.outcome ?? 'warn';
293
+
294
+ if (spec.reject) {
295
+ let regex;
296
+ try { regex = new RegExp(spec.reject); } catch (err) {
297
+ return { outcome: 'warn', message: `Invalid reject regex "${spec.reject}": ${err.message}` };
298
+ }
299
+ if (regex.test(text)) {
300
+ return { outcome, message: `Result matches reject pattern: ${spec.reject}` };
301
+ }
302
+ }
303
+
304
+ if (spec.match) {
305
+ let regex;
306
+ try { regex = new RegExp(spec.match); } catch (err) {
307
+ return { outcome: 'warn', message: `Invalid match regex "${spec.match}": ${err.message}` };
308
+ }
309
+ if (!regex.test(text)) {
310
+ return { outcome, message: `Result does not match required pattern: ${spec.match}` };
311
+ }
312
+ }
313
+
314
+ return { outcome: 'pass', message: null };
315
+ }
316
+
317
+ /**
318
+ * Custom verifier — routes through worker pool (sandboxed) or calls directly (dev mode).
319
+ * @param {{ fn?: Function, resolvedPath?: string, exportName?: string, usePool?: boolean }} spec
320
+ * @param {string} toolName
321
+ * @param {object} args
322
+ * @param {object} result
323
+ * @param {import('./verifier-worker-pool.js').VerifierWorkerPool|null} workerPool
324
+ * @param {string} role — 'read' | 'write' | 'any'
325
+ * @returns {Promise<{ outcome: string, message: string|null }>}
326
+ */
327
+ async function runCustomVerifier(spec, toolName, args, result, workerPool, role) {
328
+ // Sandboxed path: dispatch to worker pool
329
+ if (spec.usePool && workerPool) {
330
+ return workerPool.run(spec.resolvedPath, spec.exportName, toolName, args, result, role);
331
+ }
332
+
333
+ // Direct (non-sandboxed / dev mode)
334
+ if (typeof spec.fn !== 'function') {
335
+ return { outcome: 'warn', message: 'Custom verifier has no fn function' };
336
+ }
337
+ return await spec.fn(toolName, args, result);
338
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Verifier Scanner — Discovers existing verifiers from barrel.
3
+ * Used for gap detection: tools without verifier coverage.
4
+ */
5
+
6
+ import { readFileSync, existsSync, readdirSync } from 'fs';
7
+ import { resolve, join } from 'path';
8
+
9
+ /**
10
+ * Extract verifier names from barrel file.
11
+ * @param {string} barrelsPath
12
+ * @returns {string[]}
13
+ */
14
+ function scanBarrel(barrelsPath) {
15
+ const abs = resolve(process.cwd(), barrelsPath);
16
+ if (!existsSync(abs)) return [];
17
+ const content = readFileSync(abs, 'utf-8');
18
+ const names = [];
19
+ const re = /export\s+\{\s*(\w+Verifier)\s*\}\s+from\s+['"]([^'"]+)['"]/g;
20
+ let m;
21
+ const lines = content.split('\n').filter((l) => !l.trim().startsWith('//'));
22
+ const text = lines.join('\n');
23
+ while ((m = re.exec(text))) {
24
+ const exportName = m[1];
25
+ const snake = exportName
26
+ .replace(/Verifier$/, '')
27
+ .replace(/([A-Z])/g, (c) => '_' + c.toLowerCase())
28
+ .replace(/^_/, '');
29
+ names.push(snake);
30
+ }
31
+ return names;
32
+ }
33
+
34
+ /**
35
+ * Scan verifier files for `name = 'snake_case'` pattern.
36
+ * @param {string} verifiersDir
37
+ * @returns {string[]}
38
+ */
39
+ function scanVerifierFiles(verifiersDir) {
40
+ const abs = resolve(process.cwd(), verifiersDir);
41
+ if (!existsSync(abs)) return [];
42
+ const names = [];
43
+ const files = readdirSync(abs).filter(
44
+ (f) => f.endsWith('.verifier.ts') || f.endsWith('.verifier.js')
45
+ );
46
+ for (const file of files) {
47
+ const content = readFileSync(join(abs, file), 'utf-8');
48
+ const m = /name\s*=\s*['"]([^'"]+)['"]/.exec(content);
49
+ if (m) names.push(m[1]);
50
+ }
51
+ return names;
52
+ }
53
+
54
+ /**
55
+ * Get existing verifier names.
56
+ * @param {object} config - forge.config verification section
57
+ * @returns {string[]}
58
+ */
59
+ export function getExistingVerifiers(config) {
60
+ const verifiers = [];
61
+ if (config?.verifiersDir) {
62
+ const fromFiles = scanVerifierFiles(config.verifiersDir);
63
+ verifiers.push(...fromFiles);
64
+ }
65
+ if (config?.barrelsFile && verifiers.length === 0) {
66
+ const fromBarrel = scanBarrel(config.barrelsFile);
67
+ verifiers.push(...fromBarrel);
68
+ }
69
+ return [...new Set(verifiers)];
70
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * VerifierWorkerPool — fixed-size Worker thread pool for sandboxed custom verifier execution.
3
+ *
4
+ * Architecture (1000 verifiers × 1M sessions):
5
+ * - N workers (default: min(4, cpus), configurable via poolSize)
6
+ * - Module cache per worker (imported once, reused)
7
+ * - Dispatch to first available idle worker
8
+ * - Per-call timeout: 2000ms (configurable) — on expiry: terminate + replace worker
9
+ * - Queue: max 200 pending calls (configurable); rejects with warn/block based on role
10
+ * - Dead worker (crash/OOM): replaced immediately
11
+ * - Timeout/crash outcome: 'write' role → 'block', 'read'|'any' → 'warn'
12
+ */
13
+
14
+ import { Worker } from 'worker_threads';
15
+ import { cpus } from 'os';
16
+ import { fileURLToPath } from 'url';
17
+ import { dirname, resolve } from 'path';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const WORKER_PATH = resolve(__dirname, 'workers', 'verifier-worker.js');
21
+
22
+ export class VerifierWorkerPool {
23
+ /**
24
+ * @param {object} opts
25
+ * @param {number} [opts.size] — pool size (default: min(4, cpus().length))
26
+ * @param {number} [opts.timeoutMs=2000] — per-call timeout in ms
27
+ * @param {number} [opts.maxQueueDepth=200] — max pending calls before rejecting
28
+ */
29
+ constructor(opts = {}) {
30
+ this._size = opts.size ?? Math.min(4, cpus().length);
31
+ this._timeoutMs = opts.timeoutMs ?? 2000;
32
+ this._maxQueueDepth = opts.maxQueueDepth ?? 200;
33
+
34
+ this._workers = []; // { worker, busy, lastUsed }
35
+ this._callMap = new Map(); // callId → { resolve, timeoutHandle, role, entry }
36
+ this._queue = []; // { callArgs, role, resolve }
37
+ this._nextId = 1;
38
+ this._destroyed = false;
39
+
40
+ for (let i = 0; i < this._size; i++) {
41
+ this._workers.push(this._createWorkerEntry());
42
+ }
43
+ }
44
+
45
+ /** @private */
46
+ _createWorkerEntry() {
47
+ const worker = new Worker(WORKER_PATH, { type: 'module' });
48
+ const entry = { worker, busy: false, lastUsed: 0 };
49
+
50
+ worker.on('message', (msg) => {
51
+ this._handleMessage(msg, entry);
52
+ });
53
+ worker.on('error', (err) => {
54
+ this._handleWorkerError(entry, err);
55
+ });
56
+ worker.on('exit', (code) => {
57
+ if (code !== 0 && !this._destroyed) {
58
+ this._replaceWorker(entry);
59
+ }
60
+ });
61
+
62
+ return entry;
63
+ }
64
+
65
+ /** @private */
66
+ _handleMessage({ id, outcome, message }, entry) {
67
+ const pending = this._callMap.get(id);
68
+ if (!pending) return;
69
+ clearTimeout(pending.timeoutHandle);
70
+ this._callMap.delete(id);
71
+ entry.busy = false;
72
+ entry.lastUsed = Date.now();
73
+ pending.resolve({ outcome, message });
74
+ // Drain queue
75
+ this._drainQueue();
76
+ }
77
+
78
+ /** @private */
79
+ _handleWorkerError(crashedEntry, err) {
80
+ // Fail only the pending calls that were dispatched to the crashed worker
81
+ for (const [id, pending] of this._callMap) {
82
+ if (pending.entry !== crashedEntry) continue;
83
+ clearTimeout(pending.timeoutHandle);
84
+ this._callMap.delete(id);
85
+ const outcome = pending.role === 'write' ? 'block' : 'warn';
86
+ pending.resolve({ outcome, message: `Verifier worker crashed: ${err.message}` });
87
+ }
88
+ if (!this._destroyed) {
89
+ this._replaceWorker(crashedEntry);
90
+ }
91
+ }
92
+
93
+ /** @private */
94
+ _replaceWorker(entry) {
95
+ const idx = this._workers.indexOf(entry);
96
+ if (idx === -1) return;
97
+ try { entry.worker.terminate(); } catch { /* ignore */ }
98
+ this._workers[idx] = this._createWorkerEntry();
99
+ this._drainQueue();
100
+ }
101
+
102
+ /** @private */
103
+ _drainQueue() {
104
+ while (this._queue.length > 0) {
105
+ const freeEntry = this._workers.find(e => !e.busy);
106
+ if (!freeEntry) break;
107
+ const { callArgs, role, resolve } = this._queue.shift();
108
+ this._dispatch(freeEntry, callArgs, role, resolve);
109
+ }
110
+ }
111
+
112
+ /** @private */
113
+ _dispatch(entry, { id, verifierPath, exportName, toolName, args, result }, role, resolve) {
114
+ entry.busy = true;
115
+ entry.lastUsed = Date.now();
116
+
117
+ const timeoutHandle = setTimeout(() => {
118
+ this._callMap.delete(id);
119
+ const outcome = role === 'write' ? 'block' : 'warn';
120
+ resolve({ outcome, message: `Verifier timed out after ${this._timeoutMs}ms` });
121
+ // Replace the worker (stuck in user code)
122
+ this._replaceWorker(entry);
123
+ }, this._timeoutMs).unref();
124
+
125
+ this._callMap.set(id, { resolve, timeoutHandle, role, entry });
126
+ entry.worker.postMessage({ id, verifierPath, exportName, toolName, args, result });
127
+ }
128
+
129
+ /**
130
+ * Run a custom verifier in a worker thread.
131
+ *
132
+ * @param {string} verifierPath — absolute path to verifier module
133
+ * @param {string} exportName — exported function name (e.g. 'verify')
134
+ * @param {string} toolName
135
+ * @param {object} args — tool call input
136
+ * @param {object} result — tool call result
137
+ * @param {string} [role='any'] — 'read' | 'write' | 'any' (determines timeout outcome)
138
+ * @returns {Promise<{ outcome: 'pass'|'warn'|'block', message: string|null }>}
139
+ */
140
+ run(verifierPath, exportName, toolName, args, result, role = 'any') {
141
+ if (this._destroyed) {
142
+ const outcome = role === 'write' ? 'block' : 'warn';
143
+ return Promise.resolve({ outcome, message: 'Verifier pool is destroyed' });
144
+ }
145
+
146
+ const id = this._nextId++;
147
+
148
+ return new Promise((resolve) => {
149
+ const callArgs = { id, verifierPath, exportName, toolName, args, result };
150
+
151
+ // Try to dispatch immediately to a free worker
152
+ const freeEntry = this._workers.find(e => !e.busy);
153
+ if (freeEntry) {
154
+ this._dispatch(freeEntry, callArgs, role, resolve);
155
+ return;
156
+ }
157
+
158
+ // Queue if under limit
159
+ if (this._queue.length >= this._maxQueueDepth) {
160
+ const outcome = role === 'write' ? 'block' : 'warn';
161
+ resolve({ outcome, message: 'Verifier queue full — request dropped' });
162
+ return;
163
+ }
164
+
165
+ this._queue.push({ callArgs, role, resolve });
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Tear down all workers. Outstanding calls resolve with warn/block.
171
+ */
172
+ destroy() {
173
+ this._destroyed = true;
174
+
175
+ // Fail all pending calls
176
+ for (const [id, pending] of this._callMap) {
177
+ clearTimeout(pending.timeoutHandle);
178
+ this._callMap.delete(id);
179
+ const outcome = pending.role === 'write' ? 'block' : 'warn';
180
+ pending.resolve({ outcome, message: 'Verifier pool shutting down' });
181
+ }
182
+
183
+ // Drain queue
184
+ for (const { callArgs, role, resolve } of this._queue) {
185
+ const outcome = role === 'write' ? 'block' : 'warn';
186
+ resolve({ outcome, message: 'Verifier pool shutting down' });
187
+ }
188
+ this._queue.length = 0;
189
+
190
+ // Terminate workers
191
+ for (const entry of this._workers) {
192
+ try { entry.worker.terminate(); } catch { /* ignore */ }
193
+ }
194
+ this._workers.length = 0;
195
+ }
196
+ }