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,252 @@
1
+ /**
2
+ * MCP Server — Creates an MCP Server instance that proxies tool calls
3
+ * to internal API endpoints defined in tool_registry.
4
+ *
5
+ * Usage: const server = createMcpServer(db, config);
6
+ * then: const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
7
+ * await server.connect(transport);
8
+ * await transport.handleRequest(req, res, parsedBody);
9
+ */
10
+
11
+ import { Server } from '@modelcontextprotocol/sdk/server';
12
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
13
+ import { getAllToolRegistry, insertMcpCallLog } from './db.js';
14
+
15
+ /**
16
+ * Safe JSON.parse — returns null on failure.
17
+ * @param {string} str
18
+ * @returns {object|null}
19
+ */
20
+ function safeParseJson(str) {
21
+ try { return JSON.parse(str); } catch { return null; }
22
+ }
23
+
24
+ /**
25
+ * Call a tool's mcpRouting endpoint with the provided arguments.
26
+ * Builds URL from config.api.baseUrl + tool's mcpRouting.endpoint.
27
+ * Maps tool arguments to path params, query params, or body via paramMap.
28
+ *
29
+ * @param {object} spec - Parsed tool spec with mcpRouting
30
+ * @param {object} args - Tool call arguments
31
+ * @param {object} config - forge config with api.baseUrl
32
+ * @param {string|null} [userJwt] - User JWT to forward as Authorization header
33
+ * @returns {Promise<{ status: number; body: object; error: string|null }>}
34
+ */
35
+ async function callToolEndpoint(spec, args, config, userJwt = null) {
36
+ const baseUrl = (config.api?.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
37
+ const routing = spec.mcpRouting || {};
38
+ const path = routing.endpoint || '/';
39
+ const method = (routing.method || 'GET').toUpperCase();
40
+ const paramMap = routing.paramMap || {};
41
+
42
+ // Build URL with path params substituted; collect query and body params
43
+ let url = baseUrl + path;
44
+ const queryParams = new URLSearchParams();
45
+ const bodyObj = {};
46
+
47
+ // Validate property names before use to prevent prototype pollution
48
+ const SAFE_PROP_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
49
+
50
+ for (const [toolParam, mapping] of Object.entries(paramMap)) {
51
+ const val = args[toolParam];
52
+ if (val === undefined) continue;
53
+ if (mapping.path) {
54
+ // Validate path param name before substituting into URL
55
+ if (SAFE_PROP_NAME.test(mapping.path)) {
56
+ url = url.replace(`{${mapping.path}}`, encodeURIComponent(String(val)));
57
+ }
58
+ // silently skip invalid path param names
59
+ } else if (mapping.query) {
60
+ if (!SAFE_PROP_NAME.test(mapping.query)) {
61
+ throw new Error(`Unsafe mapping.query key: ${mapping.query}`);
62
+ }
63
+ queryParams.set(mapping.query, String(val));
64
+ } else if (mapping.body) {
65
+ // Validate body property name to prevent prototype pollution
66
+ if (SAFE_PROP_NAME.test(mapping.body)) {
67
+ bodyObj[mapping.body] = val;
68
+ }
69
+ // silently skip invalid property names
70
+ }
71
+ }
72
+
73
+ if ([...queryParams].length > 0) url += '?' + queryParams.toString();
74
+
75
+ const fetchOpts = {
76
+ method,
77
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
78
+ signal: AbortSignal.timeout(30_000)
79
+ };
80
+ // Forward user JWT if present — allows the app's internal API to validate the user
81
+ if (userJwt) {
82
+ fetchOpts.headers['Authorization'] = `Bearer ${userJwt}`;
83
+ }
84
+ if (['POST', 'PUT', 'PATCH'].includes(method) && Object.keys(bodyObj).length > 0) {
85
+ fetchOpts.body = JSON.stringify(bodyObj);
86
+ }
87
+
88
+ const res = await fetch(url, fetchOpts);
89
+ const text = await res.text();
90
+ let body;
91
+ try { body = JSON.parse(text); } catch { body = { text }; }
92
+ return {
93
+ status: res.status,
94
+ body,
95
+ error: res.ok ? null : `HTTP ${res.status}: ${text.slice(0, 200)}`
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Create an MCP Server that exposes promoted tools from tool_registry.
101
+ *
102
+ * @param {import('better-sqlite3').Database} db
103
+ * @param {object} config - forge.config.json contents
104
+ * @returns {import('@modelcontextprotocol/sdk/server').Server}
105
+ */
106
+ export function createMcpServer(db, config, sidecarCtx = null) {
107
+ const server = new Server(
108
+ { name: 'forge-mcp-server', version: '1.0.0' },
109
+ { capabilities: { tools: {} } }
110
+ );
111
+
112
+ // ── tools/list ──────────────────────────────────────────────────────────
113
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
114
+ const rows = getAllToolRegistry(db).filter(r => r.lifecycle_state === 'promoted');
115
+ const tools = [];
116
+ for (const row of rows) {
117
+ const spec = safeParseJson(row.spec_json);
118
+ if (!spec) {
119
+ console.error(`[mcp-server] Skipping tool "${row.tool_name}": malformed spec_json`);
120
+ continue;
121
+ }
122
+ const schema = spec.schema || {};
123
+ const properties = {};
124
+ const required = [];
125
+ for (const [k, v] of Object.entries(schema)) {
126
+ properties[k] = {
127
+ type: v.type || 'string',
128
+ description: v.description || k
129
+ };
130
+ if (!v.optional) required.push(k);
131
+ }
132
+ tools.push({
133
+ name: spec.name || row.tool_name,
134
+ description: spec.description || '',
135
+ inputSchema: {
136
+ type: 'object',
137
+ properties,
138
+ ...(required.length ? { required } : {})
139
+ }
140
+ });
141
+ }
142
+ return { tools };
143
+ });
144
+
145
+ // ── tools/call ───────────────────────────────────────────────────────────
146
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
147
+ const { name, arguments: args = {} } = request.params;
148
+ const rows = getAllToolRegistry(db).filter(r => r.lifecycle_state === 'promoted');
149
+ const row = rows.find(r => {
150
+ const s = safeParseJson(r.spec_json);
151
+ return (s?.name || r.tool_name) === name;
152
+ });
153
+
154
+ if (!row) {
155
+ return {
156
+ content: [{ type: 'text', text: `Tool "${name}" not found or not promoted` }],
157
+ isError: true
158
+ };
159
+ }
160
+
161
+ const spec = safeParseJson(row.spec_json);
162
+ if (!spec?.mcpRouting?.endpoint) {
163
+ return {
164
+ content: [{ type: 'text', text: `Tool "${name}" has no mcpRouting configured` }],
165
+ isError: true
166
+ };
167
+ }
168
+
169
+ const start = Date.now();
170
+ let result;
171
+ try {
172
+ // MCP protocol doesn't carry user JWTs; tool calls made via MCP are
173
+ // service-level. Pass null explicitly so it's clear this is intentional,
174
+ // not an accidental omission. A future transport that carries identity
175
+ // should extract it from sidecarCtx.session and pass it here.
176
+ const userJwt = null; // MCP protocol doesn't carry user JWT
177
+ result = await callToolEndpoint(spec, args, config, userJwt);
178
+ } catch (err) {
179
+ const latency_ms = Date.now() - start;
180
+ try {
181
+ insertMcpCallLog(db, {
182
+ tool_name: name,
183
+ input_json: JSON.stringify(args),
184
+ status_code: 0,
185
+ latency_ms,
186
+ error: err.message
187
+ });
188
+ } catch (logErr) {
189
+ console.error('[mcp-server] Failed to log call error:', logErr.message);
190
+ }
191
+ return {
192
+ content: [{ type: 'text', text: `Connection error: ${err.message}` }],
193
+ isError: true
194
+ };
195
+ }
196
+
197
+ const latency_ms = Date.now() - start;
198
+ try {
199
+ insertMcpCallLog(db, {
200
+ tool_name: name,
201
+ input_json: JSON.stringify(args),
202
+ output_json: JSON.stringify(result.body),
203
+ status_code: result.status,
204
+ latency_ms,
205
+ error: result.error || null
206
+ });
207
+ } catch (logErr) {
208
+ console.error('[mcp-server] Failed to log call:', logErr.message);
209
+ }
210
+
211
+ if (result.error) {
212
+ return {
213
+ content: [{ type: 'text', text: result.error }],
214
+ isError: true
215
+ };
216
+ }
217
+
218
+ // Run verifiers if sidecar context is available
219
+ if (sidecarCtx?.verifierRunner) {
220
+ try {
221
+ await sidecarCtx.verifierRunner.loadFromDb(db);
222
+ const vResult = await sidecarCtx.verifierRunner.verify(name, args, result);
223
+ if (vResult.outcome === 'block') {
224
+ sidecarCtx.verifierRunner.logResult('mcp', name, vResult);
225
+ return {
226
+ content: [{ type: 'text', text: `Tool blocked by verifier "${vResult.verifierName}": ${vResult.message}` }],
227
+ isError: true
228
+ };
229
+ }
230
+ if (vResult.outcome === 'warn') {
231
+ sidecarCtx.verifierRunner.logResult('mcp', name, vResult);
232
+ return {
233
+ content: [
234
+ { type: 'text', text: `Warning from verifier "${vResult.verifierName}": ${vResult.message}` },
235
+ { type: 'text', text: JSON.stringify(result.body, null, 2) }
236
+ ],
237
+ structuredContent: result.body,
238
+ isError: false
239
+ };
240
+ }
241
+ } catch { /* verifier failure is non-fatal */ }
242
+ }
243
+
244
+ return {
245
+ content: [{ type: 'text', text: JSON.stringify(result.body, null, 2) }],
246
+ structuredContent: result.body,
247
+ isError: false
248
+ };
249
+ });
250
+
251
+ return server;
252
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Output Groups — Infer which verifiers apply to which tools.
3
+ * Maps tool descriptions/tags to output groups, then to suggested verifiers.
4
+ */
5
+
6
+ const OUTPUT_GROUP_KEYWORDS = {
7
+ holdings: ['holdings', 'positions', 'allocation', 'portfolio'],
8
+ dividends: ['dividends', 'income', 'yield', 'dividend'],
9
+ performance: ['performance', 'p&l', 'returns', 'net worth'],
10
+ transactions: ['transactions', 'orders', 'trades'],
11
+ quotes: ['quotes', 'prices', 'market data', 'market_data'],
12
+ weather: ['weather', 'temperature', 'humidity', 'conditions'],
13
+ forecast: ['forecast', 'prediction', 'outlook', 'precipitation']
14
+ };
15
+
16
+ const VERIFIER_GROUPS = {
17
+ source_attribution: ['*'],
18
+ concentration_risk: ['holdings'],
19
+ stale_data: ['holdings', 'performance', 'quotes', 'dividends', 'weather', 'forecast']
20
+ };
21
+
22
+ /**
23
+ * Infer output groups for a tool from its description and tags.
24
+ * @param {{ description?: string; tags?: string[]; name?: string }} tool
25
+ * @returns {string[]}
26
+ */
27
+ export function inferOutputGroups(tool) {
28
+ const text = [
29
+ tool.description || '',
30
+ (tool.tags || []).join(' '),
31
+ tool.name || ''
32
+ ]
33
+ .join(' ')
34
+ .toLowerCase();
35
+ const groups = [];
36
+ for (const [group, keywords] of Object.entries(OUTPUT_GROUP_KEYWORDS)) {
37
+ if (keywords.some((k) => text.includes(k))) groups.push(group);
38
+ }
39
+ return groups.length > 0 ? groups : ['unknown'];
40
+ }
41
+
42
+ /**
43
+ * Get verifiers that would cover the given output groups.
44
+ * @param {string[]} outputGroups
45
+ * @returns {string[]}
46
+ */
47
+ export function getVerifiersForGroups(outputGroups) {
48
+ const verifiers = new Set();
49
+ for (const [verifier, groups] of Object.entries(VERIFIER_GROUPS)) {
50
+ if (groups.includes('*')) verifiers.add(verifier);
51
+ else if (groups.some((g) => outputGroups.includes(g))) verifiers.add(verifier);
52
+ }
53
+ return Array.from(verifiers);
54
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Postgres-backed storage adapter for horizontal scaling.
3
+ *
4
+ * Mirrors the SQLite query function signatures from `db.js` but uses a `pg` Pool.
5
+ * Only loaded when `conversation.store === 'postgres'` (or `database.type === 'postgres'`) in config.
6
+ *
7
+ * Requires: `npm install pg`
8
+ */
9
+ export class PostgresStore {
10
+ constructor(pgConfig: { connectionString: string });
11
+
12
+ /**
13
+ * Connect to Postgres and run schema migrations.
14
+ * Must be called before any other method.
15
+ */
16
+ connect(): Promise<this>;
17
+
18
+ /** Close the connection pool. */
19
+ close(): Promise<void>;
20
+
21
+ // ── Prompt versions ───────────────────────────────────────────────────────
22
+
23
+ getActivePrompt(): Promise<object | null>;
24
+ insertPromptVersion(row: { version: string; content: string; notes?: string | null }): Promise<number | null>;
25
+ activatePromptVersion(id: number): Promise<void>;
26
+
27
+ // ── User preferences ──────────────────────────────────────────────────────
28
+
29
+ getUserPreferences(userId: string): Promise<{ model: string | null; hitl_level: string | null } | null>;
30
+ upsertUserPreferences(userId: string, prefs: { model?: string; hitlLevel?: string }): Promise<void>;
31
+ }