@promptev/context-engine 0.0.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.
- package/LICENSE.md +202 -0
- package/NOTICE +17 -0
- package/README.md +112 -0
- package/dist/cli.js +11998 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-Bl9U789m.d.cts +174 -0
- package/dist/config-Bt9bUQqU.d.ts +174 -0
- package/dist/embeddings-B-jZ42mk.d.cts +67 -0
- package/dist/embeddings-DaSdAZN3.d.ts +67 -0
- package/dist/express.cjs +3173 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +24 -0
- package/dist/express.d.ts +24 -0
- package/dist/express.js +3170 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.cjs +3184 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +16 -0
- package/dist/fastify.d.ts +16 -0
- package/dist/fastify.js +3181 -0
- package/dist/fastify.js.map +1 -0
- package/dist/governance-BDkcv4qZ.d.cts +79 -0
- package/dist/governance-XIScatRO.d.ts +79 -0
- package/dist/graph/index.cjs +1428 -0
- package/dist/graph/index.cjs.map +1 -0
- package/dist/graph/index.d.cts +104 -0
- package/dist/graph/index.d.ts +104 -0
- package/dist/graph/index.js +1413 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/hono.cjs +3183 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +39 -0
- package/dist/hono.d.ts +39 -0
- package/dist/hono.js +3179 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +11731 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +851 -0
- package/dist/index.d.ts +851 -0
- package/dist/index.js +11676 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.cjs +181 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +26 -0
- package/dist/mcp.d.ts +26 -0
- package/dist/mcp.js +179 -0
- package/dist/mcp.js.map +1 -0
- package/dist/migrations/sql/0001.sql +119 -0
- package/dist/migrations/sql/0002_graph.sql +48 -0
- package/dist/migrations/sql/0003_tools.sql +61 -0
- package/dist/migrations/sql/0004_acl_indexes.sql +4 -0
- package/dist/redaction-BmDSWJ7h.d.cts +98 -0
- package/dist/redaction-BmDSWJ7h.d.ts +98 -0
- package/dist/redaction-presidio.cjs +79 -0
- package/dist/redaction-presidio.cjs.map +1 -0
- package/dist/redaction-presidio.d.cts +22 -0
- package/dist/redaction-presidio.d.ts +22 -0
- package/dist/redaction-presidio.js +73 -0
- package/dist/redaction-presidio.js.map +1 -0
- package/dist/router-CrxZ2y_Z.d.ts +82 -0
- package/dist/router-OPgSoYAB.d.cts +82 -0
- package/dist/skills/context-engine/SKILL.md +160 -0
- package/package.json +184 -0
- package/src/migrations/sql/0001.sql +119 -0
- package/src/migrations/sql/0002_graph.sql +48 -0
- package/src/migrations/sql/0003_tools.sql +61 -0
- package/src/migrations/sql/0004_acl_indexes.sql +4 -0
- package/src/skills/context-engine/SKILL.md +160 -0
package/dist/hono.cjs
ADDED
|
@@ -0,0 +1,3183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var module$1 = require('module');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/errors.ts
|
|
19
|
+
var EngineActionError, ApprovalNotPending, ApprovalExpired, ExtraMissingError;
|
|
20
|
+
var init_errors = __esm({
|
|
21
|
+
"src/errors.ts"() {
|
|
22
|
+
EngineActionError = class extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "EngineActionError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
ApprovalNotPending = class extends Error {
|
|
29
|
+
constructor(message = "approval is not pending") {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ApprovalNotPending";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
ApprovalExpired = class extends Error {
|
|
35
|
+
constructor(message = "approval has expired") {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "ApprovalExpired";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
ExtraMissingError = class extends Error {
|
|
41
|
+
constructor(extra, pkg, what) {
|
|
42
|
+
super(`${what} requires the '${pkg}' package (optional extra: ${extra}): npm install ${pkg}`);
|
|
43
|
+
this.name = "ExtraMissingError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/tools/executors/db.ts
|
|
50
|
+
var db_exports = {};
|
|
51
|
+
__export(db_exports, {
|
|
52
|
+
MAX_ROWS: () => MAX_ROWS,
|
|
53
|
+
buildConnectionUrl: () => buildConnectionUrl,
|
|
54
|
+
executeDb: () => executeDb,
|
|
55
|
+
executeQuery: () => executeQuery,
|
|
56
|
+
fetchSchema: () => fetchSchema,
|
|
57
|
+
getSchemaText: () => getSchemaText,
|
|
58
|
+
stripSqlNoise: () => stripSqlNoise,
|
|
59
|
+
testConnection: () => testConnection
|
|
60
|
+
});
|
|
61
|
+
function stripSqlNoise(sql) {
|
|
62
|
+
return sql.replace(SQL_NOISE_RE, (m) => " ".repeat(m.length));
|
|
63
|
+
}
|
|
64
|
+
function hasKeyword(sqlUpper, keyword) {
|
|
65
|
+
return new RegExp(`\\b${keyword}\\b`).test(sqlUpper);
|
|
66
|
+
}
|
|
67
|
+
function buildConnectionUrl(config) {
|
|
68
|
+
const engine = String(config.engine ?? "postgresql");
|
|
69
|
+
if (engine === "sqlite") return `sqlite:///${config.database ?? ""}`;
|
|
70
|
+
const host = String(config.host ?? "localhost");
|
|
71
|
+
const port = Number(config.port ?? 5432);
|
|
72
|
+
const database = String(config.database ?? "");
|
|
73
|
+
const username = String(config.username ?? "");
|
|
74
|
+
const password = encodeURIComponent(String(config.password ?? ""));
|
|
75
|
+
if (engine === "oracle") {
|
|
76
|
+
const serviceName = String(config.service_name ?? database);
|
|
77
|
+
return `oracle://${username}:${password}@${host}:${port}/?service_name=${serviceName}`;
|
|
78
|
+
}
|
|
79
|
+
if (engine === "snowflake") {
|
|
80
|
+
const schemaName = String(config.schema_name ?? "PUBLIC");
|
|
81
|
+
const warehouse = String(config.warehouse ?? "");
|
|
82
|
+
let url = `snowflake://${username}:${password}@${host}/${database}/${schemaName}`;
|
|
83
|
+
if (warehouse) url += `?warehouse=${warehouse}`;
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
if (engine === "clickhouse") {
|
|
87
|
+
const secure = [9440, 8443, 443].includes(port);
|
|
88
|
+
return `clickhouse://${username}:${password}@${host}:${port}/${database}${secure ? "?secure=True" : ""}`;
|
|
89
|
+
}
|
|
90
|
+
if (engine === "mysql" || engine === "mariadb") {
|
|
91
|
+
return `mysql://${username}:${password}@${host}:${port}/${database}`;
|
|
92
|
+
}
|
|
93
|
+
if (engine === "mssql") {
|
|
94
|
+
return `mssql://${username}:${password}@${host}:${port}/${database}`;
|
|
95
|
+
}
|
|
96
|
+
return `postgresql://${username}:${password}@${host}:${port}/${database}`;
|
|
97
|
+
}
|
|
98
|
+
function rowsToText(rows) {
|
|
99
|
+
if (!rows.length) return "";
|
|
100
|
+
const columns = Object.keys(rows[0]);
|
|
101
|
+
const lines = [columns.join(" ")];
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
lines.push(columns.map((col) => String(row[col] ?? "")).join(" "));
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
async function runOnPostgres(config, sql, maxRows, readOnly = false) {
|
|
108
|
+
let pg;
|
|
109
|
+
try {
|
|
110
|
+
pg = require2("pg");
|
|
111
|
+
} catch {
|
|
112
|
+
throw new ExtraMissingError("postgres", "pg", "database tool executor");
|
|
113
|
+
}
|
|
114
|
+
const url = buildConnectionUrl({ ...config, engine: "postgresql" });
|
|
115
|
+
const client = new pg.Client({
|
|
116
|
+
connectionString: url,
|
|
117
|
+
connectionTimeoutMillis: CONNECT_TIMEOUT * 1e3,
|
|
118
|
+
statement_timeout: QUERY_TIMEOUT_MS
|
|
119
|
+
});
|
|
120
|
+
await client.connect();
|
|
121
|
+
try {
|
|
122
|
+
await client.query("BEGIN");
|
|
123
|
+
if (readOnly) {
|
|
124
|
+
try {
|
|
125
|
+
await client.query("SET TRANSACTION READ ONLY");
|
|
126
|
+
} catch (exc) {
|
|
127
|
+
await client.query("ROLLBACK");
|
|
128
|
+
console.warn(
|
|
129
|
+
"[context-engine] db executor: engine rejected SET TRANSACTION READ ONLY; read-only mode is enforced by statement inspection alone for this query",
|
|
130
|
+
exc
|
|
131
|
+
);
|
|
132
|
+
await client.query("BEGIN");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const result = await client.query(sql);
|
|
136
|
+
await client.query("COMMIT");
|
|
137
|
+
const columns = result.fields?.map((f) => f.name) ?? (result.rows[0] ? Object.keys(result.rows[0]) : []);
|
|
138
|
+
const rows = result.rows.slice(0, maxRows).map((r) => ({ ...r }));
|
|
139
|
+
return { columns, rows, rowCount: result.rowCount ?? rows.length };
|
|
140
|
+
} catch (e) {
|
|
141
|
+
try {
|
|
142
|
+
await client.query("ROLLBACK");
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
throw e;
|
|
146
|
+
} finally {
|
|
147
|
+
await client.end();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function runOnMysql(config, sql, maxRows) {
|
|
151
|
+
let mysql;
|
|
152
|
+
try {
|
|
153
|
+
mysql = require2("mysql2/promise");
|
|
154
|
+
} catch {
|
|
155
|
+
throw new ExtraMissingError("mysql", "mysql2", "MySQL/MariaDB tool executor");
|
|
156
|
+
}
|
|
157
|
+
const conn = await mysql.createConnection({
|
|
158
|
+
host: config.host ?? "localhost",
|
|
159
|
+
port: Number(config.port ?? 3306),
|
|
160
|
+
user: config.username ?? "",
|
|
161
|
+
password: config.password ?? "",
|
|
162
|
+
database: config.database ?? "",
|
|
163
|
+
connectTimeout: CONNECT_TIMEOUT * 1e3
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
const [rows, fields] = await conn.query(sql);
|
|
167
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
168
|
+
const columns = (fields ?? []).map((f) => f.name);
|
|
169
|
+
return { columns, rows: list.slice(0, maxRows), rowCount: list.length };
|
|
170
|
+
} finally {
|
|
171
|
+
await conn.end();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function executeRaw(config, sql, maxRows, readOnly = false) {
|
|
175
|
+
const engine = String(config.engine ?? "postgresql");
|
|
176
|
+
if (engine === "postgresql") return runOnPostgres(config, sql, maxRows, readOnly);
|
|
177
|
+
if (engine === "mysql" || engine === "mariadb") return runOnMysql(config, sql, maxRows);
|
|
178
|
+
throw new Error(
|
|
179
|
+
`db executor: engine ${JSON.stringify(engine)} is not supported in the JS port (supported: postgresql, mysql, mariadb)`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function executeQuery(config, sql, maxRows = MAX_ROWS, accessMode = "readonly") {
|
|
183
|
+
return executeQueryAsync(config, sql, maxRows, accessMode);
|
|
184
|
+
}
|
|
185
|
+
async function executeQueryAsync(config, sql, maxRows, accessModeRaw) {
|
|
186
|
+
const accessMode = accessModeRaw || "readonly";
|
|
187
|
+
if (!ALLOWED_ACCESS_MODES.includes(accessMode)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Invalid access_mode ${JSON.stringify(accessModeRaw)}; must be one of ${ALLOWED_ACCESS_MODES}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const sqlUpper = stripSqlNoise(sql).trim().toUpperCase();
|
|
193
|
+
for (const keyword of ALWAYS_BLOCKED) {
|
|
194
|
+
if (hasKeyword(sqlUpper, keyword)) {
|
|
195
|
+
return { success: false, error: `${keyword} queries are not allowed` };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (accessMode === "readonly") {
|
|
199
|
+
if (!sqlUpper.startsWith("SELECT") && !sqlUpper.startsWith("WITH")) {
|
|
200
|
+
return { success: false, error: "Only SELECT queries are allowed in read-only mode" };
|
|
201
|
+
}
|
|
202
|
+
for (const keyword of READONLY_BLOCKED) {
|
|
203
|
+
if (hasKeyword(sqlUpper, keyword)) {
|
|
204
|
+
return { success: false, error: `${keyword} queries are not allowed in read-only mode` };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (accessMode === "readwrite") {
|
|
208
|
+
for (const keyword of READWRITE_BLOCKED) {
|
|
209
|
+
if (hasKeyword(sqlUpper, keyword)) {
|
|
210
|
+
return { success: false, error: `${keyword} queries are not allowed in read-write mode` };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const result = await executeRaw(config, sql, maxRows, accessMode === "readonly");
|
|
216
|
+
if (!sqlUpper.startsWith("SELECT") && !sqlUpper.startsWith("WITH")) {
|
|
217
|
+
return {
|
|
218
|
+
success: true,
|
|
219
|
+
columns: [],
|
|
220
|
+
rows: [],
|
|
221
|
+
row_count: result.rowCount,
|
|
222
|
+
affected_rows: result.rowCount,
|
|
223
|
+
truncated: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
columns: result.columns,
|
|
229
|
+
rows: result.rows,
|
|
230
|
+
row_count: result.rows.length,
|
|
231
|
+
truncated: result.rows.length >= maxRows,
|
|
232
|
+
text: rowsToText(result.rows)
|
|
233
|
+
};
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return { success: false, error: String(e) };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function executeDb(config, sql) {
|
|
239
|
+
const maxRows = Number(config.max_rows ?? MAX_ROWS);
|
|
240
|
+
const accessMode = String(config.access_mode ?? "readonly");
|
|
241
|
+
return executeQueryAsync(config, sql, maxRows, accessMode);
|
|
242
|
+
}
|
|
243
|
+
async function testConnection(config) {
|
|
244
|
+
const engineType = String(config.engine ?? "postgresql");
|
|
245
|
+
const versionQueries = {
|
|
246
|
+
postgresql: "SELECT version()",
|
|
247
|
+
mysql: "SELECT version()",
|
|
248
|
+
mariadb: "SELECT version()",
|
|
249
|
+
mssql: "SELECT @@VERSION",
|
|
250
|
+
sqlite: "SELECT sqlite_version()",
|
|
251
|
+
oracle: "SELECT banner FROM v$version WHERE ROWNUM = 1",
|
|
252
|
+
clickhouse: "SELECT version()",
|
|
253
|
+
snowflake: "SELECT CURRENT_VERSION()"
|
|
254
|
+
};
|
|
255
|
+
const q = versionQueries[engineType] ?? "SELECT version()";
|
|
256
|
+
const result = await executeQueryAsync(config, q, 1, "readonly");
|
|
257
|
+
if (!result.success) return { success: false, error: result.error };
|
|
258
|
+
const version = result.rows?.[0] ? Object.values(result.rows[0])[0] : null;
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
version: String(version),
|
|
262
|
+
host: config.host,
|
|
263
|
+
database: config.database,
|
|
264
|
+
engine: engineType
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function fetchSchema(config, selectedTables) {
|
|
268
|
+
const engine = String(config.engine ?? "postgresql");
|
|
269
|
+
if (engine !== "postgresql") {
|
|
270
|
+
return { success: false, error: `schema introspection is implemented for postgresql (got ${engine})` };
|
|
271
|
+
}
|
|
272
|
+
let pg;
|
|
273
|
+
try {
|
|
274
|
+
pg = require2("pg");
|
|
275
|
+
} catch {
|
|
276
|
+
throw new ExtraMissingError("postgres", "pg", "database tool executor");
|
|
277
|
+
}
|
|
278
|
+
const client = new pg.Client({ connectionString: buildConnectionUrl(config) });
|
|
279
|
+
await client.connect();
|
|
280
|
+
try {
|
|
281
|
+
const schemaName = String(config.schema_name ?? "public");
|
|
282
|
+
const tablesRes = await client.query(
|
|
283
|
+
`SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = $1`,
|
|
284
|
+
[schemaName]
|
|
285
|
+
);
|
|
286
|
+
let items = tablesRes.rows.map((r) => ({
|
|
287
|
+
name: String(r.table_name),
|
|
288
|
+
type: String(r.table_type).toLowerCase().includes("view") ? "view" : "table"
|
|
289
|
+
}));
|
|
290
|
+
if (selectedTables?.length) {
|
|
291
|
+
const selected = new Set(selectedTables);
|
|
292
|
+
items = items.filter((i) => selected.has(i.name));
|
|
293
|
+
}
|
|
294
|
+
const tables = [];
|
|
295
|
+
for (const item of items) {
|
|
296
|
+
const cols = await client.query(
|
|
297
|
+
`SELECT column_name, data_type, is_nullable FROM information_schema.columns
|
|
298
|
+
WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position`,
|
|
299
|
+
[schemaName, item.name]
|
|
300
|
+
);
|
|
301
|
+
tables.push({
|
|
302
|
+
name: item.name,
|
|
303
|
+
type: item.type,
|
|
304
|
+
columns: cols.rows.map((c) => ({
|
|
305
|
+
name: c.column_name,
|
|
306
|
+
type: c.data_type,
|
|
307
|
+
nullable: c.is_nullable === "YES"
|
|
308
|
+
})),
|
|
309
|
+
row_count: 0
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
success: true,
|
|
314
|
+
database: config.database,
|
|
315
|
+
schema: schemaName,
|
|
316
|
+
tables,
|
|
317
|
+
table_count: tables.length,
|
|
318
|
+
all_tables: items.map((i) => i.name)
|
|
319
|
+
};
|
|
320
|
+
} finally {
|
|
321
|
+
await client.end();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function getSchemaText(config, selectedTables) {
|
|
325
|
+
const schema = await fetchSchema(config, selectedTables);
|
|
326
|
+
const schemaRows = [];
|
|
327
|
+
for (const table of schema.tables ?? []) {
|
|
328
|
+
const kind = table.type ?? "table";
|
|
329
|
+
for (const c of table.columns ?? []) {
|
|
330
|
+
schemaRows.push({
|
|
331
|
+
table: table.name,
|
|
332
|
+
table_type: kind,
|
|
333
|
+
column: c.name,
|
|
334
|
+
type: String(c.type).toUpperCase(),
|
|
335
|
+
pk: c.pk ?? false,
|
|
336
|
+
fk: c.fk ?? "",
|
|
337
|
+
rows: table.row_count
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return rowsToText(schemaRows);
|
|
342
|
+
}
|
|
343
|
+
var require2, MAX_ROWS, CONNECT_TIMEOUT, QUERY_TIMEOUT_MS, ALWAYS_BLOCKED, READONLY_BLOCKED, READWRITE_BLOCKED, ALLOWED_ACCESS_MODES, SQL_NOISE_RE;
|
|
344
|
+
var init_db = __esm({
|
|
345
|
+
"src/tools/executors/db.ts"() {
|
|
346
|
+
init_errors();
|
|
347
|
+
require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('hono.cjs', document.baseURI).href)));
|
|
348
|
+
MAX_ROWS = 1e3;
|
|
349
|
+
CONNECT_TIMEOUT = 10;
|
|
350
|
+
QUERY_TIMEOUT_MS = 3e4;
|
|
351
|
+
ALWAYS_BLOCKED = ["GRANT", "REVOKE"];
|
|
352
|
+
READONLY_BLOCKED = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "TRUNCATE"];
|
|
353
|
+
READWRITE_BLOCKED = ["DELETE", "DROP", "TRUNCATE"];
|
|
354
|
+
ALLOWED_ACCESS_MODES = ["readonly", "readwrite", "full"];
|
|
355
|
+
SQL_NOISE_RE = /'(?:[^']|'')*'|\$([A-Za-z_]\w*)?\$.*?\$\1?\$|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// src/hono.ts
|
|
360
|
+
init_errors();
|
|
361
|
+
|
|
362
|
+
// src/sentinels.ts
|
|
363
|
+
var TrustedSentinel = class {
|
|
364
|
+
[Symbol.toStringTag] = "TRUSTED";
|
|
365
|
+
toString() {
|
|
366
|
+
return "TRUSTED";
|
|
367
|
+
}
|
|
368
|
+
valueOf() {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var TRUSTED = Object.freeze(new TrustedSentinel());
|
|
373
|
+
|
|
374
|
+
// src/routing-core.ts
|
|
375
|
+
var HandlerError = class extends Error {
|
|
376
|
+
status;
|
|
377
|
+
detail;
|
|
378
|
+
constructor(status, detail) {
|
|
379
|
+
super(typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
380
|
+
this.name = "HandlerError";
|
|
381
|
+
this.status = status;
|
|
382
|
+
this.detail = detail;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var ingestJsonRequestSchema = zod.z.object({
|
|
386
|
+
text: zod.z.string(),
|
|
387
|
+
name: zod.z.string(),
|
|
388
|
+
source_id: zod.z.string().nullable().optional(),
|
|
389
|
+
external_id: zod.z.string().nullable().optional(),
|
|
390
|
+
description: zod.z.string().nullable().optional(),
|
|
391
|
+
meta_data: zod.z.record(zod.z.unknown()).nullable().optional(),
|
|
392
|
+
acl: zod.z.array(zod.z.string()).nullable().optional(),
|
|
393
|
+
mode: zod.z.enum(["hybrid", "graph"]).nullable().optional(),
|
|
394
|
+
extract_structured: zod.z.boolean().optional().default(false),
|
|
395
|
+
batch: zod.z.boolean().optional().default(false)
|
|
396
|
+
}).strip();
|
|
397
|
+
var documentPatchSchema = zod.z.object({
|
|
398
|
+
acl: zod.z.array(zod.z.string()).nullable().optional(),
|
|
399
|
+
name: zod.z.string().nullable().optional(),
|
|
400
|
+
description: zod.z.string().nullable().optional(),
|
|
401
|
+
meta_data: zod.z.record(zod.z.unknown()).nullable().optional()
|
|
402
|
+
}).strict();
|
|
403
|
+
var searchRequestSchema = zod.z.object({
|
|
404
|
+
query: zod.z.string(),
|
|
405
|
+
source_ids: zod.z.array(zod.z.string()).nullable().optional(),
|
|
406
|
+
top_k: zod.z.number().int().optional().default(10),
|
|
407
|
+
mode: zod.z.enum(["hybrid", "graph"]).optional().default("hybrid"),
|
|
408
|
+
compress_to_tokens: zod.z.number().int().nullable().optional()
|
|
409
|
+
}).strip();
|
|
410
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
411
|
+
function requireValidUuid(documentId) {
|
|
412
|
+
if (!UUID_RE.test(String(documentId))) {
|
|
413
|
+
throw new HandlerError(400, `invalid document id: ${JSON.stringify(documentId)}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function formBool(value) {
|
|
417
|
+
if (typeof value === "boolean") return value;
|
|
418
|
+
if (value == null) return false;
|
|
419
|
+
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
|
|
420
|
+
}
|
|
421
|
+
function formJson(value) {
|
|
422
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
423
|
+
if (typeof value === "object") return value;
|
|
424
|
+
try {
|
|
425
|
+
return JSON.parse(String(value));
|
|
426
|
+
} catch {
|
|
427
|
+
throw new HandlerError(400, `invalid JSON in field: ${JSON.stringify(value)}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function formStr(value) {
|
|
431
|
+
return value === void 0 || value === null || value === "" ? null : String(value);
|
|
432
|
+
}
|
|
433
|
+
function resolveRequestPrincipals(value, opts) {
|
|
434
|
+
if (value === TRUSTED) return null;
|
|
435
|
+
if (value == null) {
|
|
436
|
+
throw new HandlerError(
|
|
437
|
+
500,
|
|
438
|
+
`${opts.surface}: the \`principals\` dependency returned null, which means TRUSTED CALLER \u2014 it disables ACL filtering and skips the check that stops a caller filing documents under groups it does not hold. This mount is misconfigured: return [] for an unauthenticated caller, or pass TRUSTED explicitly (from @promptev/context-engine) if the surface really is trusted.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
function authorizeAcl(requested, principals) {
|
|
444
|
+
if (requested == null || principals == null) return;
|
|
445
|
+
if (!Array.isArray(requested)) {
|
|
446
|
+
throw new HandlerError(422, "acl must be a list of principal strings");
|
|
447
|
+
}
|
|
448
|
+
const held = new Set(principals);
|
|
449
|
+
const ungranted = [...new Set(requested.map(String))].filter((p) => !held.has(p)).sort();
|
|
450
|
+
if (ungranted.length) {
|
|
451
|
+
throw new HandlerError(403, `cannot grant access to principals you do not hold: ${ungranted}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function asDict(obj) {
|
|
455
|
+
if (obj == null) return obj;
|
|
456
|
+
if (typeof obj === "object" && typeof obj.toJSON === "function") {
|
|
457
|
+
return obj.toJSON();
|
|
458
|
+
}
|
|
459
|
+
return obj;
|
|
460
|
+
}
|
|
461
|
+
function zodError(err) {
|
|
462
|
+
return new HandlerError(
|
|
463
|
+
422,
|
|
464
|
+
err.issues.map((i) => ({ loc: i.path, msg: i.message, type: i.code }))
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
async function runIngest(engine, kwargs) {
|
|
468
|
+
try {
|
|
469
|
+
return asDict(await engine.ingest(kwargs));
|
|
470
|
+
} catch (exc) {
|
|
471
|
+
if (exc instanceof TypeError || exc instanceof Error && exc.name === "TypeError") {
|
|
472
|
+
throw new HandlerError(400, String(exc));
|
|
473
|
+
}
|
|
474
|
+
if (exc instanceof Error && exc.name === "Error" && /invalid|required/i.test(exc.message)) {
|
|
475
|
+
throw new HandlerError(400, String(exc));
|
|
476
|
+
}
|
|
477
|
+
throw exc;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function handleIngestJson(engine, payload, principals) {
|
|
481
|
+
const resolved = resolveRequestPrincipals(principals, { surface: "handle_ingest_json" });
|
|
482
|
+
const parsed = ingestJsonRequestSchema.safeParse(payload);
|
|
483
|
+
if (!parsed.success) throw zodError(parsed.error);
|
|
484
|
+
const body = parsed.data;
|
|
485
|
+
authorizeAcl(body.acl, resolved);
|
|
486
|
+
return runIngest(engine, {
|
|
487
|
+
text: body.text,
|
|
488
|
+
name: body.name,
|
|
489
|
+
description: body.description,
|
|
490
|
+
sourceId: body.source_id,
|
|
491
|
+
externalId: body.external_id,
|
|
492
|
+
metaData: body.meta_data,
|
|
493
|
+
acl: body.acl,
|
|
494
|
+
mode: body.mode,
|
|
495
|
+
extractStructured: body.extract_structured,
|
|
496
|
+
batch: body.batch
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function handleIngestFile(engine, opts) {
|
|
500
|
+
const resolved = resolveRequestPrincipals(opts.principals, { surface: "handle_ingest_file" });
|
|
501
|
+
const form = opts.form instanceof Map ? Object.fromEntries(opts.form) : opts.form;
|
|
502
|
+
const acl = formJson(form.acl);
|
|
503
|
+
authorizeAcl(acl, resolved);
|
|
504
|
+
return runIngest(engine, {
|
|
505
|
+
content: opts.content,
|
|
506
|
+
filename: opts.filename,
|
|
507
|
+
name: formStr(form.name) || opts.filename,
|
|
508
|
+
description: formStr(form.description),
|
|
509
|
+
sourceId: formStr(form.source_id),
|
|
510
|
+
externalId: formStr(form.external_id),
|
|
511
|
+
metaData: formJson(form.meta_data),
|
|
512
|
+
acl,
|
|
513
|
+
mode: formStr(form.mode),
|
|
514
|
+
extractStructured: formBool(form.extract_structured),
|
|
515
|
+
batch: formBool(form.batch)
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async function handleListDocuments(engine, opts) {
|
|
519
|
+
const resolved = resolveRequestPrincipals(opts.principals, { surface: "handle_list_documents" });
|
|
520
|
+
let parsedCursor = null;
|
|
521
|
+
if (opts.cursor) {
|
|
522
|
+
try {
|
|
523
|
+
parsedCursor = JSON.parse(opts.cursor);
|
|
524
|
+
} catch {
|
|
525
|
+
throw new HandlerError(400, `invalid cursor: ${JSON.stringify(opts.cursor)}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return engine.listDocuments({
|
|
529
|
+
sourceId: opts.sourceId ?? null,
|
|
530
|
+
principals: resolved,
|
|
531
|
+
cursor: parsedCursor,
|
|
532
|
+
limit: opts.limit
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async function handleGetDocument(engine, documentId, principals) {
|
|
536
|
+
const resolved = resolveRequestPrincipals(principals, { surface: "handle_get_document" });
|
|
537
|
+
requireValidUuid(documentId);
|
|
538
|
+
try {
|
|
539
|
+
return await engine.getDocument(documentId, { principals: resolved });
|
|
540
|
+
} catch (exc) {
|
|
541
|
+
if (exc instanceof Error && (exc.name === "DocumentNotFoundError" || exc instanceof TypeError === false)) {
|
|
542
|
+
const msg = String(exc);
|
|
543
|
+
if (/not found/i.test(msg) || exc.name === "DocumentNotFoundError") {
|
|
544
|
+
throw new HandlerError(404, msg.replace(/^Error:\s*/, ""));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
throw exc;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function handleDeleteDocument(engine, documentId, principals) {
|
|
551
|
+
const resolved = resolveRequestPrincipals(principals, { surface: "handle_delete_document" });
|
|
552
|
+
requireValidUuid(documentId);
|
|
553
|
+
try {
|
|
554
|
+
await engine.deleteDocument(documentId, { principals: resolved });
|
|
555
|
+
} catch (exc) {
|
|
556
|
+
const msg = String(exc);
|
|
557
|
+
if (/not found/i.test(msg) || exc instanceof Error && exc.name === "DocumentNotFoundError") {
|
|
558
|
+
throw new HandlerError(404, msg.replace(/^Error:\s*/, ""));
|
|
559
|
+
}
|
|
560
|
+
throw exc;
|
|
561
|
+
}
|
|
562
|
+
return { deleted: documentId };
|
|
563
|
+
}
|
|
564
|
+
async function handleUpdateDocument(engine, documentId, payload, principals) {
|
|
565
|
+
const resolved = resolveRequestPrincipals(principals, { surface: "handle_update_document" });
|
|
566
|
+
requireValidUuid(documentId);
|
|
567
|
+
const parsed = documentPatchSchema.safeParse(payload);
|
|
568
|
+
if (!parsed.success) throw zodError(parsed.error);
|
|
569
|
+
const provided = {};
|
|
570
|
+
const raw = payload;
|
|
571
|
+
for (const key of ["acl", "name", "description", "meta_data"]) {
|
|
572
|
+
if (Object.hasOwn(raw, key)) {
|
|
573
|
+
provided[key === "meta_data" ? "metaData" : key] = raw[key];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if ("acl" in provided) authorizeAcl(provided.acl, resolved);
|
|
577
|
+
try {
|
|
578
|
+
const changed = await engine.updateDocument(documentId, { principals: resolved, ...provided });
|
|
579
|
+
return { changed };
|
|
580
|
+
} catch (exc) {
|
|
581
|
+
const msg = String(exc);
|
|
582
|
+
if (/not found/i.test(msg) || exc instanceof Error && exc.name === "DocumentNotFoundError") {
|
|
583
|
+
throw new HandlerError(404, msg.replace(/^Error:\s*/, ""));
|
|
584
|
+
}
|
|
585
|
+
throw exc;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async function handleSearch(engine, payload, principals) {
|
|
589
|
+
const resolved = resolveRequestPrincipals(principals, { surface: "handle_search" });
|
|
590
|
+
const parsed = searchRequestSchema.safeParse(payload);
|
|
591
|
+
if (!parsed.success) throw zodError(parsed.error);
|
|
592
|
+
const body = parsed.data;
|
|
593
|
+
try {
|
|
594
|
+
const result = await engine.search(body.query, {
|
|
595
|
+
sourceIds: body.source_ids,
|
|
596
|
+
principals: resolved,
|
|
597
|
+
topK: body.top_k,
|
|
598
|
+
mode: body.mode,
|
|
599
|
+
compressToTokens: body.compress_to_tokens
|
|
600
|
+
});
|
|
601
|
+
const hits = (result.hits ?? []).map((hit) => asDict(hit));
|
|
602
|
+
return { hits, usage: result.usage };
|
|
603
|
+
} catch (exc) {
|
|
604
|
+
if (exc instanceof Error && /invalid|required|graph/i.test(exc.message)) {
|
|
605
|
+
throw new HandlerError(400, String(exc));
|
|
606
|
+
}
|
|
607
|
+
throw exc;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function handleStats(engine, sourceId) {
|
|
611
|
+
return engine.stats(sourceId ?? null);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/tools/router.ts
|
|
615
|
+
init_errors();
|
|
616
|
+
|
|
617
|
+
// src/tools/acl.ts
|
|
618
|
+
function aclVisible(acl, principals) {
|
|
619
|
+
if (principals === null) return true;
|
|
620
|
+
if (acl == null) return true;
|
|
621
|
+
if (!acl.length) return false;
|
|
622
|
+
const held = new Set(principals ?? []);
|
|
623
|
+
return acl.some((p) => held.has(p));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/tools/approval.ts
|
|
627
|
+
init_errors();
|
|
628
|
+
var DEFAULT_TIMEOUT_MINUTES = 60;
|
|
629
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
630
|
+
var MAX_LIST_LIMIT = 200;
|
|
631
|
+
var CONDITION_RE = /^\s*(?<field>[A-Za-z_][A-Za-z0-9_.]*)\s*(?<op>>=|<=|==|!=|>|<)\s*(?<value>.+?)\s*$/;
|
|
632
|
+
function coerceValue(raw) {
|
|
633
|
+
const trimmed = raw.trim();
|
|
634
|
+
if (trimmed.length >= 2 && trimmed[0] === trimmed[trimmed.length - 1] && (trimmed[0] === "'" || trimmed[0] === '"')) {
|
|
635
|
+
return trimmed.slice(1, -1);
|
|
636
|
+
}
|
|
637
|
+
if (/^-?\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);
|
|
638
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) return Number.parseFloat(trimmed);
|
|
639
|
+
return trimmed;
|
|
640
|
+
}
|
|
641
|
+
function evaluateCondition(condition, args) {
|
|
642
|
+
if (typeof condition !== "string") return true;
|
|
643
|
+
const match = CONDITION_RE.exec(condition);
|
|
644
|
+
if (!match?.groups) return true;
|
|
645
|
+
const fieldName = match.groups.field;
|
|
646
|
+
const op = match.groups.op;
|
|
647
|
+
if (!(fieldName in args)) return true;
|
|
648
|
+
const left = args[fieldName];
|
|
649
|
+
const right = coerceValue(match.groups.value);
|
|
650
|
+
try {
|
|
651
|
+
switch (op) {
|
|
652
|
+
case ">":
|
|
653
|
+
return left > right;
|
|
654
|
+
case ">=":
|
|
655
|
+
return left >= right;
|
|
656
|
+
case "<":
|
|
657
|
+
return left < right;
|
|
658
|
+
case "<=":
|
|
659
|
+
return left <= right;
|
|
660
|
+
case "==":
|
|
661
|
+
return left === right;
|
|
662
|
+
case "!=":
|
|
663
|
+
return left !== right;
|
|
664
|
+
default:
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function shouldRequireApproval(ct, args) {
|
|
672
|
+
if (ct.requiresApproval) return true;
|
|
673
|
+
const policy = ct.approvalPolicy ?? {};
|
|
674
|
+
const condition = policy.condition;
|
|
675
|
+
if (!condition) return false;
|
|
676
|
+
return evaluateCondition(condition, args ?? {});
|
|
677
|
+
}
|
|
678
|
+
function rowToRecord(row) {
|
|
679
|
+
return {
|
|
680
|
+
id: String(row.id),
|
|
681
|
+
toolName: String(row.tool_name),
|
|
682
|
+
toolArgsFrozen: row.tool_args_frozen ?? {},
|
|
683
|
+
sourceId: row.source_id ?? null,
|
|
684
|
+
principals: row.principals,
|
|
685
|
+
status: String(row.status),
|
|
686
|
+
approver: row.approver ?? null,
|
|
687
|
+
approverMeta: row.approver_meta ?? null,
|
|
688
|
+
expiresAt: row.expires_at ? new Date(String(row.expires_at)) : null,
|
|
689
|
+
resolvedAt: row.resolved_at ? new Date(String(row.resolved_at)) : null,
|
|
690
|
+
createdAt: row.created_at ? new Date(String(row.created_at)) : null
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function visibilitySql(principals, paramIndex) {
|
|
694
|
+
if (principals === null) return { sql: "TRUE", params: [] };
|
|
695
|
+
if (!principals.length) {
|
|
696
|
+
return { sql: `(principals IS NULL OR principals = 'null'::jsonb)`, params: [] };
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
sql: `(principals IS NULL OR principals = 'null'::jsonb OR principals ?| $${paramIndex}::text[])`,
|
|
700
|
+
params: [principals]
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
async function createPending(engine, opts) {
|
|
704
|
+
const policy = opts.policy ?? {};
|
|
705
|
+
const frozen = structuredClone(opts.args);
|
|
706
|
+
const createdAt = opts.now ?? /* @__PURE__ */ new Date();
|
|
707
|
+
const timeout = Number(policy.timeout_minutes ?? DEFAULT_TIMEOUT_MINUTES);
|
|
708
|
+
const expiresAt = new Date(createdAt.getTime() + timeout * 6e4);
|
|
709
|
+
const id = crypto.randomUUID();
|
|
710
|
+
await engine.pool.query(
|
|
711
|
+
`INSERT INTO context_engine_tool_approvals
|
|
712
|
+
(id, tool_name, tool_args_frozen, source_id, principals, status, expires_at, created_at)
|
|
713
|
+
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,'pending',$6,$7)`,
|
|
714
|
+
[
|
|
715
|
+
id,
|
|
716
|
+
opts.toolName,
|
|
717
|
+
JSON.stringify(frozen),
|
|
718
|
+
opts.sourceId ?? null,
|
|
719
|
+
opts.principals === void 0 ? null : JSON.stringify(opts.principals),
|
|
720
|
+
expiresAt,
|
|
721
|
+
createdAt
|
|
722
|
+
]
|
|
723
|
+
);
|
|
724
|
+
return {
|
|
725
|
+
id,
|
|
726
|
+
toolName: opts.toolName,
|
|
727
|
+
toolArgsFrozen: frozen,
|
|
728
|
+
sourceId: opts.sourceId ?? null,
|
|
729
|
+
principals: opts.principals ?? null,
|
|
730
|
+
status: "pending",
|
|
731
|
+
approver: null,
|
|
732
|
+
approverMeta: null,
|
|
733
|
+
expiresAt,
|
|
734
|
+
resolvedAt: null,
|
|
735
|
+
createdAt
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
async function resolveApproval(engine, approvalId, decision, approver, meta = null, opts = {}) {
|
|
739
|
+
if (decision !== "approved" && decision !== "rejected") {
|
|
740
|
+
throw new Error(`invalid decision: ${JSON.stringify(decision)} (expected 'approved' or 'rejected')`);
|
|
741
|
+
}
|
|
742
|
+
const principals = opts.principals === void 0 ? null : opts.principals;
|
|
743
|
+
const vis = visibilitySql(principals, 5);
|
|
744
|
+
const result = await engine.pool.query(
|
|
745
|
+
`UPDATE context_engine_tool_approvals
|
|
746
|
+
SET status = $1, approver = $2, approver_meta = $3::jsonb, resolved_at = now()
|
|
747
|
+
WHERE id = $4 AND status = 'pending'
|
|
748
|
+
AND (expires_at IS NULL OR expires_at > now())
|
|
749
|
+
AND ${vis.sql}
|
|
750
|
+
RETURNING *`,
|
|
751
|
+
[decision, approver, JSON.stringify(meta ?? {}), approvalId, ...vis.params]
|
|
752
|
+
);
|
|
753
|
+
if (result.rowCount === 1) return rowToRecord(result.rows[0]);
|
|
754
|
+
const existing = await engine.pool.query(`SELECT * FROM context_engine_tool_approvals WHERE id = $1`, [
|
|
755
|
+
approvalId
|
|
756
|
+
]);
|
|
757
|
+
const row = existing.rows[0];
|
|
758
|
+
const rowPrincipals = row?.principals == null || row.principals === null ? null : row.principals;
|
|
759
|
+
if (!row || !aclVisible(Array.isArray(rowPrincipals) ? rowPrincipals : null, principals)) {
|
|
760
|
+
throw new ApprovalNotPending("not_found");
|
|
761
|
+
}
|
|
762
|
+
if (row.status === "pending") {
|
|
763
|
+
await engine.pool.query(
|
|
764
|
+
`UPDATE context_engine_tool_approvals SET status = 'expired' WHERE id = $1 AND status = 'pending'`,
|
|
765
|
+
[approvalId]
|
|
766
|
+
);
|
|
767
|
+
throw new ApprovalExpired();
|
|
768
|
+
}
|
|
769
|
+
throw new ApprovalNotPending(String(row.status));
|
|
770
|
+
}
|
|
771
|
+
async function listApprovals(engine, opts = {}) {
|
|
772
|
+
const limit = Math.max(1, Math.min(Number(opts.limit || DEFAULT_LIST_LIMIT), MAX_LIST_LIMIT));
|
|
773
|
+
const principals = opts.principals === void 0 ? null : opts.principals;
|
|
774
|
+
const params = [];
|
|
775
|
+
const clauses = [];
|
|
776
|
+
if (opts.status != null) {
|
|
777
|
+
params.push(opts.status);
|
|
778
|
+
clauses.push(`status = $${params.length}`);
|
|
779
|
+
}
|
|
780
|
+
if (opts.sourceId != null) {
|
|
781
|
+
params.push(opts.sourceId);
|
|
782
|
+
clauses.push(`source_id = $${params.length}`);
|
|
783
|
+
}
|
|
784
|
+
const vis = visibilitySql(principals, params.length + 1);
|
|
785
|
+
clauses.push(vis.sql);
|
|
786
|
+
params.push(...vis.params);
|
|
787
|
+
params.push(limit);
|
|
788
|
+
const result = await engine.pool.query(
|
|
789
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
790
|
+
WHERE ${clauses.join(" AND ")}
|
|
791
|
+
ORDER BY created_at DESC
|
|
792
|
+
LIMIT $${params.length}`,
|
|
793
|
+
params
|
|
794
|
+
);
|
|
795
|
+
return result.rows.map(rowToRecord);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/tools/config.ts
|
|
799
|
+
var KINDS = ["http", "db", "mcp", "function"];
|
|
800
|
+
var ToolConfig = class _ToolConfig {
|
|
801
|
+
id;
|
|
802
|
+
name;
|
|
803
|
+
kind;
|
|
804
|
+
description;
|
|
805
|
+
config;
|
|
806
|
+
sourceId;
|
|
807
|
+
acl;
|
|
808
|
+
requiresApproval;
|
|
809
|
+
approvalPolicy;
|
|
810
|
+
enabled;
|
|
811
|
+
constructor(init) {
|
|
812
|
+
this.id = init.id ?? null;
|
|
813
|
+
this.name = init.name;
|
|
814
|
+
this.kind = init.kind;
|
|
815
|
+
this.description = init.description ?? "";
|
|
816
|
+
this.config = init.config ?? {};
|
|
817
|
+
this.sourceId = init.sourceId ?? init.source_id ?? null;
|
|
818
|
+
this.acl = init.acl ?? null;
|
|
819
|
+
this.requiresApproval = init.requiresApproval ?? init.requires_approval ?? false;
|
|
820
|
+
this.approvalPolicy = init.approvalPolicy ?? init.approval_policy ?? {};
|
|
821
|
+
this.enabled = init.enabled ?? true;
|
|
822
|
+
}
|
|
823
|
+
static fromUnknown(body) {
|
|
824
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
825
|
+
throw new Error("tool config must be an object");
|
|
826
|
+
}
|
|
827
|
+
const b = body;
|
|
828
|
+
const kind = String(b.kind ?? "");
|
|
829
|
+
if (!KINDS.includes(kind)) {
|
|
830
|
+
throw new Error(`canonicalFromConfig does not support kind=${JSON.stringify(b.kind)}`);
|
|
831
|
+
}
|
|
832
|
+
const name = String(b.name ?? "").trim();
|
|
833
|
+
if (!name) throw new Error("name is required");
|
|
834
|
+
return new _ToolConfig({
|
|
835
|
+
id: b.id != null ? String(b.id) : null,
|
|
836
|
+
name,
|
|
837
|
+
kind,
|
|
838
|
+
description: b.description != null ? String(b.description) : "",
|
|
839
|
+
config: b.config ?? {},
|
|
840
|
+
sourceId: b.sourceId ?? b.source_id,
|
|
841
|
+
acl: b.acl ?? null,
|
|
842
|
+
requiresApproval: Boolean(b.requiresApproval ?? b.requires_approval ?? false),
|
|
843
|
+
approvalPolicy: b.approvalPolicy ?? b.approval_policy ?? {},
|
|
844
|
+
enabled: b.enabled === void 0 ? true : Boolean(b.enabled)
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
var SCHEMAS = {
|
|
849
|
+
http: {
|
|
850
|
+
type: "object",
|
|
851
|
+
properties: {
|
|
852
|
+
url: { type: "string", description: "Endpoint URL, may contain {path} params" },
|
|
853
|
+
method: {
|
|
854
|
+
type: "string",
|
|
855
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
856
|
+
default: "GET"
|
|
857
|
+
},
|
|
858
|
+
headers: {
|
|
859
|
+
type: "object",
|
|
860
|
+
additionalProperties: { type: "string" },
|
|
861
|
+
writeOnly: true
|
|
862
|
+
},
|
|
863
|
+
parameters: {
|
|
864
|
+
type: "object",
|
|
865
|
+
description: "Static/user-fixed parameters sent on every call"
|
|
866
|
+
},
|
|
867
|
+
llmParameters: {
|
|
868
|
+
type: "object",
|
|
869
|
+
description: "JSON-Schema-shaped parameters the LLM fills in per call"
|
|
870
|
+
},
|
|
871
|
+
llmQueryParameters: {
|
|
872
|
+
type: "object",
|
|
873
|
+
description: "LLM-filled parameters sent as the query string"
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
required: ["url", "method"]
|
|
877
|
+
},
|
|
878
|
+
db: {
|
|
879
|
+
type: "object",
|
|
880
|
+
properties: {
|
|
881
|
+
engine: {
|
|
882
|
+
type: "string",
|
|
883
|
+
enum: ["postgresql", "mysql", "mariadb", "mssql", "sqlite", "oracle", "clickhouse", "snowflake"]
|
|
884
|
+
},
|
|
885
|
+
host: { type: "string" },
|
|
886
|
+
port: { type: "integer" },
|
|
887
|
+
database: { type: "string" },
|
|
888
|
+
username: { type: "string" },
|
|
889
|
+
password: { type: "string", writeOnly: true },
|
|
890
|
+
access_mode: {
|
|
891
|
+
type: "string",
|
|
892
|
+
enum: ["readonly", "readwrite", "full"],
|
|
893
|
+
default: "readonly"
|
|
894
|
+
},
|
|
895
|
+
max_rows: { type: "integer", default: 1e3 },
|
|
896
|
+
selected_tables: { type: "array", items: { type: "string" } }
|
|
897
|
+
},
|
|
898
|
+
required: ["engine", "database"]
|
|
899
|
+
},
|
|
900
|
+
mcp: {
|
|
901
|
+
type: "object",
|
|
902
|
+
properties: {
|
|
903
|
+
url: { type: "string" },
|
|
904
|
+
transport: { type: "string", enum: ["sse", "http", "websocket"] },
|
|
905
|
+
oauth: {
|
|
906
|
+
type: "object",
|
|
907
|
+
description: "Discovered OAuth config, if any",
|
|
908
|
+
writeOnly: true
|
|
909
|
+
},
|
|
910
|
+
headers: {
|
|
911
|
+
type: "object",
|
|
912
|
+
additionalProperties: { type: "string" },
|
|
913
|
+
writeOnly: true
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
required: ["url", "transport"]
|
|
917
|
+
},
|
|
918
|
+
function: {
|
|
919
|
+
type: "object",
|
|
920
|
+
properties: {}
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
function configSchema(kind) {
|
|
924
|
+
const schema = SCHEMAS[kind];
|
|
925
|
+
if (!schema) throw new Error(`unknown tool kind: ${JSON.stringify(kind)}`);
|
|
926
|
+
return schema;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/version.ts
|
|
930
|
+
var __version__ = "0.0.0";
|
|
931
|
+
|
|
932
|
+
// src/tools/executors/mcp-client.ts
|
|
933
|
+
var MCPError = class _MCPError extends Error {
|
|
934
|
+
code;
|
|
935
|
+
data;
|
|
936
|
+
constructor(code, message, data = null) {
|
|
937
|
+
super(`[${code}] ${message}`);
|
|
938
|
+
this.name = "MCPError";
|
|
939
|
+
this.code = code;
|
|
940
|
+
this.data = data;
|
|
941
|
+
}
|
|
942
|
+
static fromJsonrpc(err) {
|
|
943
|
+
return new _MCPError(Number(err.code ?? -32e3), String(err.message ?? "MCP error"), err.data);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
function parseSseBuffer(raw) {
|
|
947
|
+
const dataLines = raw.split("\n").filter((ln) => ln.startsWith("data:")).map((ln) => ln.slice(5).trimStart());
|
|
948
|
+
if (!dataLines.length) return null;
|
|
949
|
+
try {
|
|
950
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
951
|
+
if (payload && typeof payload === "object" && ("result" in payload || "error" in payload)) {
|
|
952
|
+
return payload;
|
|
953
|
+
}
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
var HttpTransport = class {
|
|
959
|
+
kind = "http";
|
|
960
|
+
url;
|
|
961
|
+
baseHeaders;
|
|
962
|
+
sessionId = null;
|
|
963
|
+
constructor(url, headers) {
|
|
964
|
+
this.url = url;
|
|
965
|
+
this.baseHeaders = {
|
|
966
|
+
...headers,
|
|
967
|
+
"Content-Type": "application/json",
|
|
968
|
+
Accept: "application/json, text/event-stream"
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
async connect() {
|
|
972
|
+
}
|
|
973
|
+
async close() {
|
|
974
|
+
this.sessionId = null;
|
|
975
|
+
}
|
|
976
|
+
buildHeaders() {
|
|
977
|
+
const hdrs = { ...this.baseHeaders };
|
|
978
|
+
if (this.sessionId) hdrs["Mcp-Session-Id"] = this.sessionId;
|
|
979
|
+
return hdrs;
|
|
980
|
+
}
|
|
981
|
+
async request(msg) {
|
|
982
|
+
const resp = await fetch(this.url, {
|
|
983
|
+
method: "POST",
|
|
984
|
+
headers: this.buildHeaders(),
|
|
985
|
+
body: JSON.stringify(msg)
|
|
986
|
+
});
|
|
987
|
+
const sid = resp.headers.get("Mcp-Session-Id");
|
|
988
|
+
if (sid) this.sessionId = sid;
|
|
989
|
+
if (resp.status >= 400) {
|
|
990
|
+
const body = await resp.text();
|
|
991
|
+
console.error(`[HTTP Transport] ${resp.status} from ${this.url}: ${body.slice(0, 300)}`);
|
|
992
|
+
throw new Error(`HTTP ${resp.status}: ${body.slice(0, 300)}`);
|
|
993
|
+
}
|
|
994
|
+
const contentType = resp.headers.get("Content-Type") ?? "";
|
|
995
|
+
if (contentType.includes("text/event-stream")) {
|
|
996
|
+
const text = await resp.text();
|
|
997
|
+
let result = {};
|
|
998
|
+
for (const frame of text.split(/\r?\n\r?\n/)) {
|
|
999
|
+
const parsed = parseSseBuffer(frame);
|
|
1000
|
+
if (parsed) result = parsed;
|
|
1001
|
+
}
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
return await resp.json();
|
|
1005
|
+
}
|
|
1006
|
+
async send(msg) {
|
|
1007
|
+
await this.request(msg);
|
|
1008
|
+
}
|
|
1009
|
+
async recv() {
|
|
1010
|
+
return {};
|
|
1011
|
+
}
|
|
1012
|
+
getBaseHeaders() {
|
|
1013
|
+
return { ...this.baseHeaders };
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
var SseTransport = class {
|
|
1017
|
+
kind = "sse";
|
|
1018
|
+
eventsUrl;
|
|
1019
|
+
baseUrl;
|
|
1020
|
+
headers;
|
|
1021
|
+
postUrl = null;
|
|
1022
|
+
queue = [];
|
|
1023
|
+
waiters = [];
|
|
1024
|
+
abort = null;
|
|
1025
|
+
constructor(url, headers) {
|
|
1026
|
+
this.eventsUrl = url.replace(/\/$/, "");
|
|
1027
|
+
if (url.endsWith("/events")) this.baseUrl = url.slice(0, -7);
|
|
1028
|
+
else if (url.endsWith("/sse")) this.baseUrl = url.slice(0, -4);
|
|
1029
|
+
else this.baseUrl = url.replace(/\/$/, "");
|
|
1030
|
+
this.headers = { ...headers };
|
|
1031
|
+
}
|
|
1032
|
+
enqueue(msg) {
|
|
1033
|
+
const waiter = this.waiters.shift();
|
|
1034
|
+
if (waiter) waiter(msg);
|
|
1035
|
+
else this.queue.push(msg);
|
|
1036
|
+
}
|
|
1037
|
+
async connect() {
|
|
1038
|
+
this.abort = new AbortController();
|
|
1039
|
+
void this.listen();
|
|
1040
|
+
}
|
|
1041
|
+
async listen() {
|
|
1042
|
+
const resp = await fetch(this.eventsUrl, {
|
|
1043
|
+
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
1044
|
+
signal: this.abort?.signal
|
|
1045
|
+
});
|
|
1046
|
+
if (!resp.ok) throw new Error(`SSE ${resp.status}`);
|
|
1047
|
+
if (!resp.body) throw new Error("SSE response has no body");
|
|
1048
|
+
const reader = resp.body.getReader();
|
|
1049
|
+
const decoder = new TextDecoder();
|
|
1050
|
+
let buf = "";
|
|
1051
|
+
let eventType = null;
|
|
1052
|
+
const frame = [];
|
|
1053
|
+
while (true) {
|
|
1054
|
+
const { done, value } = await reader.read();
|
|
1055
|
+
if (done) break;
|
|
1056
|
+
buf += decoder.decode(value, { stream: true });
|
|
1057
|
+
const lines = buf.split(/\n/);
|
|
1058
|
+
buf = lines.pop() ?? "";
|
|
1059
|
+
for (const line of lines) {
|
|
1060
|
+
if (line === "" || line === "\r") {
|
|
1061
|
+
this.dispatchFrame(frame, eventType);
|
|
1062
|
+
frame.length = 0;
|
|
1063
|
+
eventType = null;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const stripped = line.replace(/\r$/, "");
|
|
1067
|
+
if (stripped.startsWith("event:")) eventType = stripped.slice(6).trim();
|
|
1068
|
+
frame.push(stripped);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
dispatchFrame(buf, eventType) {
|
|
1073
|
+
const dataLines = buf.filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart());
|
|
1074
|
+
if (!dataLines.length) return;
|
|
1075
|
+
const payload = dataLines.join("\n");
|
|
1076
|
+
if (eventType === "endpoint") {
|
|
1077
|
+
const endpoint = payload.trim();
|
|
1078
|
+
this.postUrl = endpoint.startsWith("/") ? `${this.baseUrl}${endpoint}` : endpoint;
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
this.enqueue(JSON.parse(payload));
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async close() {
|
|
1087
|
+
this.abort?.abort();
|
|
1088
|
+
}
|
|
1089
|
+
async send(msg) {
|
|
1090
|
+
if (!this.postUrl) {
|
|
1091
|
+
for (let i = 0; i < 30; i++) {
|
|
1092
|
+
if (this.postUrl) break;
|
|
1093
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const postUrl = this.postUrl || this.baseUrl;
|
|
1097
|
+
const resp = await fetch(postUrl, {
|
|
1098
|
+
method: "POST",
|
|
1099
|
+
headers: { ...this.headers, "Content-Type": "application/json" },
|
|
1100
|
+
body: JSON.stringify(msg)
|
|
1101
|
+
});
|
|
1102
|
+
if (!resp.ok) throw new Error(`SSE POST ${resp.status}`);
|
|
1103
|
+
try {
|
|
1104
|
+
const data = await resp.json();
|
|
1105
|
+
if (data && typeof data === "object" && ("result" in data || "error" in data)) {
|
|
1106
|
+
this.enqueue(data);
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async recv() {
|
|
1112
|
+
if (this.queue.length) return this.queue.shift();
|
|
1113
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
var WsTransport = class {
|
|
1117
|
+
kind = "websocket";
|
|
1118
|
+
url;
|
|
1119
|
+
headers;
|
|
1120
|
+
ws = null;
|
|
1121
|
+
queue = [];
|
|
1122
|
+
waiters = [];
|
|
1123
|
+
constructor(url, headers) {
|
|
1124
|
+
this.url = url;
|
|
1125
|
+
this.headers = headers;
|
|
1126
|
+
}
|
|
1127
|
+
async connect() {
|
|
1128
|
+
const WS = globalThis.WebSocket;
|
|
1129
|
+
if (!WS) throw new Error("WebSocket is not available in this runtime");
|
|
1130
|
+
this.ws = new WS(this.url);
|
|
1131
|
+
this.ws.addEventListener("message", (ev) => {
|
|
1132
|
+
try {
|
|
1133
|
+
const data = JSON.parse(String(ev.data));
|
|
1134
|
+
const waiter = this.waiters.shift();
|
|
1135
|
+
if (waiter) waiter(data);
|
|
1136
|
+
else this.queue.push(data);
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
await new Promise((resolve, reject) => {
|
|
1141
|
+
this.ws.addEventListener("open", () => resolve());
|
|
1142
|
+
this.ws.addEventListener("error", () => reject(new Error("WebSocket error")));
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
async close() {
|
|
1146
|
+
this.ws?.close();
|
|
1147
|
+
this.ws = null;
|
|
1148
|
+
}
|
|
1149
|
+
async send(msg) {
|
|
1150
|
+
if (!this.ws) throw new Error("Not connected");
|
|
1151
|
+
this.ws.send(JSON.stringify(msg));
|
|
1152
|
+
}
|
|
1153
|
+
async recv() {
|
|
1154
|
+
if (this.queue.length) return this.queue.shift();
|
|
1155
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
var MCPClient = class _MCPClient {
|
|
1159
|
+
url;
|
|
1160
|
+
transport;
|
|
1161
|
+
connected = false;
|
|
1162
|
+
id = 0;
|
|
1163
|
+
pending = /* @__PURE__ */ new Map();
|
|
1164
|
+
capabilities = {};
|
|
1165
|
+
tools = [];
|
|
1166
|
+
resources = [];
|
|
1167
|
+
headers;
|
|
1168
|
+
constructor(url, headers = null) {
|
|
1169
|
+
this.url = url;
|
|
1170
|
+
this.headers = headers ?? {};
|
|
1171
|
+
this.transport = _MCPClient.createTransport(url, this.headers);
|
|
1172
|
+
}
|
|
1173
|
+
static createTransport(url, headers) {
|
|
1174
|
+
if (url.startsWith("stdio:") || url === "stdio") {
|
|
1175
|
+
throw new Error("stdio MCP transport is not supported (cloud deployments use SSE/HTTP/WebSocket)");
|
|
1176
|
+
}
|
|
1177
|
+
if (url.startsWith("ws://") || url.startsWith("wss://")) return new WsTransport(url, headers);
|
|
1178
|
+
if (url.startsWith("sse+http://") || url.startsWith("sse+https://") || url.endsWith("/events") || url.endsWith("/sse")) {
|
|
1179
|
+
const clean = url.replace("sse+http://", "http://").replace("sse+https://", "https://");
|
|
1180
|
+
return new SseTransport(clean, headers);
|
|
1181
|
+
}
|
|
1182
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return new HttpTransport(url, headers);
|
|
1183
|
+
throw new Error(`Unsupported URL: ${url} (use http(s)://, ws(s)://, or .../sse)`);
|
|
1184
|
+
}
|
|
1185
|
+
nextId() {
|
|
1186
|
+
this.id += 1;
|
|
1187
|
+
return String(this.id);
|
|
1188
|
+
}
|
|
1189
|
+
async recvLoop() {
|
|
1190
|
+
while (this.connected) {
|
|
1191
|
+
const msg = await this.transport.recv();
|
|
1192
|
+
if (!msg) continue;
|
|
1193
|
+
const mid = msg.id != null ? String(msg.id) : null;
|
|
1194
|
+
if (mid && ("result" in msg || "error" in msg)) {
|
|
1195
|
+
const fut = this.pending.get(mid);
|
|
1196
|
+
this.pending.delete(mid);
|
|
1197
|
+
fut?.resolve(msg);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async rpc(method, params = null, timeout = 6e4) {
|
|
1202
|
+
const mid = this.nextId();
|
|
1203
|
+
const payload = { jsonrpc: "2.0", id: mid, method, params: params ?? {} };
|
|
1204
|
+
if (this.transport instanceof HttpTransport) {
|
|
1205
|
+
const msg2 = await this.transport.request(payload);
|
|
1206
|
+
if (msg2.error) throw MCPError.fromJsonrpc(msg2.error);
|
|
1207
|
+
return msg2.result ?? {};
|
|
1208
|
+
}
|
|
1209
|
+
const msg = await new Promise((resolve, reject) => {
|
|
1210
|
+
const timer = setTimeout(() => {
|
|
1211
|
+
this.pending.delete(mid);
|
|
1212
|
+
reject(new Error(`MCP RPC '${method}' timed out after ${timeout}ms`));
|
|
1213
|
+
}, timeout);
|
|
1214
|
+
this.pending.set(mid, {
|
|
1215
|
+
resolve: (v) => {
|
|
1216
|
+
clearTimeout(timer);
|
|
1217
|
+
resolve(v);
|
|
1218
|
+
},
|
|
1219
|
+
reject
|
|
1220
|
+
});
|
|
1221
|
+
void this.transport.send(payload).catch((e) => {
|
|
1222
|
+
clearTimeout(timer);
|
|
1223
|
+
this.pending.delete(mid);
|
|
1224
|
+
reject(e);
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
if (msg.error) throw MCPError.fromJsonrpc(msg.error);
|
|
1228
|
+
return msg.result ?? {};
|
|
1229
|
+
}
|
|
1230
|
+
async notify(method, params = null) {
|
|
1231
|
+
await this.transport.send({ jsonrpc: "2.0", method, params: params ?? {} });
|
|
1232
|
+
}
|
|
1233
|
+
async connect() {
|
|
1234
|
+
await this.transport.connect();
|
|
1235
|
+
if (!(this.transport instanceof HttpTransport)) {
|
|
1236
|
+
this.connected = true;
|
|
1237
|
+
void this.recvLoop();
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const init = await this.rpc("initialize", {
|
|
1241
|
+
protocolVersion: "2024-11-05",
|
|
1242
|
+
capabilities: { roots: { listChanged: true }, sampling: {} },
|
|
1243
|
+
clientInfo: { name: "context-engine", version: __version__ }
|
|
1244
|
+
});
|
|
1245
|
+
this.capabilities = init.capabilities ?? {};
|
|
1246
|
+
await this.notify("notifications/initialized");
|
|
1247
|
+
this.connected = true;
|
|
1248
|
+
} catch (e) {
|
|
1249
|
+
await this.close();
|
|
1250
|
+
throw e;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async close() {
|
|
1254
|
+
this.connected = false;
|
|
1255
|
+
for (const [, fut] of this.pending) fut.reject(new Error("closed"));
|
|
1256
|
+
this.pending.clear();
|
|
1257
|
+
try {
|
|
1258
|
+
await this.transport.close();
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async listTools() {
|
|
1263
|
+
const res = await this.rpc("tools/list");
|
|
1264
|
+
this.tools = res.tools ?? [];
|
|
1265
|
+
return this.tools;
|
|
1266
|
+
}
|
|
1267
|
+
async listResources() {
|
|
1268
|
+
try {
|
|
1269
|
+
const res = await this.rpc("resources/list", null, 1e4);
|
|
1270
|
+
this.resources = res.resources ?? [];
|
|
1271
|
+
} catch {
|
|
1272
|
+
this.resources = [];
|
|
1273
|
+
}
|
|
1274
|
+
return this.resources;
|
|
1275
|
+
}
|
|
1276
|
+
async callTool(toolName, arguments_, timeout = 12e4) {
|
|
1277
|
+
return this.rpc("tools/call", { name: toolName, arguments: arguments_ }, timeout);
|
|
1278
|
+
}
|
|
1279
|
+
async ping() {
|
|
1280
|
+
try {
|
|
1281
|
+
await this.rpc("ping", {}, 1e4);
|
|
1282
|
+
return true;
|
|
1283
|
+
} catch {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async callToolWithReconnect(toolName, arguments_, timeout = 12e4) {
|
|
1288
|
+
try {
|
|
1289
|
+
if (!this.connected) throw new Error("Not connected");
|
|
1290
|
+
return await this.callTool(toolName, arguments_, timeout);
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
console.warn(`[MCP] Connection issue during call_tool(${toolName}), reconnecting:`, e);
|
|
1293
|
+
try {
|
|
1294
|
+
await this.reconnect();
|
|
1295
|
+
return await this.callTool(toolName, arguments_, timeout);
|
|
1296
|
+
} catch (retryErr) {
|
|
1297
|
+
throw new Error(`Reconnect failed: ${retryErr}`, { cause: e });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async reconnect() {
|
|
1302
|
+
const url = this.url;
|
|
1303
|
+
let headers = {};
|
|
1304
|
+
if (this.transport instanceof HttpTransport) {
|
|
1305
|
+
headers = this.transport.getBaseHeaders();
|
|
1306
|
+
delete headers["Content-Type"];
|
|
1307
|
+
delete headers.Accept;
|
|
1308
|
+
} else if (this.transport instanceof SseTransport || this.transport instanceof WsTransport) {
|
|
1309
|
+
headers = { ...this.transport.headers };
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1312
|
+
await this.close();
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
this.id = 0;
|
|
1316
|
+
this.pending.clear();
|
|
1317
|
+
this.transport = _MCPClient.createTransport(url, headers);
|
|
1318
|
+
await this.connect();
|
|
1319
|
+
await this.listTools();
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
var MCPRegistry = class {
|
|
1323
|
+
clients = /* @__PURE__ */ new Map();
|
|
1324
|
+
healthTask = null;
|
|
1325
|
+
async connect(name, url, headers = null, token = null) {
|
|
1326
|
+
this.clients.delete(name);
|
|
1327
|
+
const hdrs = { ...headers ?? {} };
|
|
1328
|
+
if (token) hdrs.Authorization = hdrs.Authorization ?? `Bearer ${token}`;
|
|
1329
|
+
const client = new MCPClient(url, hdrs);
|
|
1330
|
+
await client.connect();
|
|
1331
|
+
await client.listTools();
|
|
1332
|
+
this.clients.set(name, client);
|
|
1333
|
+
if (!this.healthTask) {
|
|
1334
|
+
this.healthTask = setInterval(() => void this.healthLoopOnce(), 6e4);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async healthLoopOnce() {
|
|
1338
|
+
for (const [name, client] of [...this.clients.entries()]) {
|
|
1339
|
+
if (!client.connected) continue;
|
|
1340
|
+
try {
|
|
1341
|
+
const alive = await client.ping();
|
|
1342
|
+
if (!alive) await client.reconnect();
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
console.warn(`[MCP Health] ${name} health check error:`, e);
|
|
1345
|
+
try {
|
|
1346
|
+
await client.reconnect();
|
|
1347
|
+
} catch {
|
|
1348
|
+
this.clients.delete(name);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
get(name) {
|
|
1354
|
+
const c = this.clients.get(name);
|
|
1355
|
+
if (!c) throw new Error(`Unknown MCP server: '${name}'`);
|
|
1356
|
+
return c;
|
|
1357
|
+
}
|
|
1358
|
+
has(name) {
|
|
1359
|
+
return this.clients.has(name);
|
|
1360
|
+
}
|
|
1361
|
+
async disconnect(name) {
|
|
1362
|
+
const c = this.clients.get(name);
|
|
1363
|
+
this.clients.delete(name);
|
|
1364
|
+
if (c) {
|
|
1365
|
+
try {
|
|
1366
|
+
await c.close();
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
async shutdown() {
|
|
1372
|
+
if (this.healthTask) {
|
|
1373
|
+
clearInterval(this.healthTask);
|
|
1374
|
+
this.healthTask = null;
|
|
1375
|
+
}
|
|
1376
|
+
for (const name of [...this.clients.keys()]) await this.disconnect(name);
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
var PromptevMCP = class {
|
|
1380
|
+
clients = new MCPRegistry();
|
|
1381
|
+
async addServer(name, url, opts = {}) {
|
|
1382
|
+
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null);
|
|
1383
|
+
}
|
|
1384
|
+
async shutdown() {
|
|
1385
|
+
await this.clients.shutdown();
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
// src/tools/governance.ts
|
|
1390
|
+
init_errors();
|
|
1391
|
+
|
|
1392
|
+
// src/hooks.ts
|
|
1393
|
+
var log = {
|
|
1394
|
+
warn: (...args) => console.warn("[context-engine]", ...args),
|
|
1395
|
+
error: (...args) => console.error("[context-engine]", ...args)
|
|
1396
|
+
};
|
|
1397
|
+
function emitUsage(hooks, event) {
|
|
1398
|
+
if (!hooks?.onUsage) return;
|
|
1399
|
+
try {
|
|
1400
|
+
hooks.onUsage(event);
|
|
1401
|
+
} catch {
|
|
1402
|
+
log.warn("onUsage callback raised; swallowing");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function emitError(hooks, exc, ctx) {
|
|
1406
|
+
log.error("context_engine error:", exc, "| ctx=", ctx);
|
|
1407
|
+
if (!hooks?.onError) return;
|
|
1408
|
+
try {
|
|
1409
|
+
hooks.onError(exc, ctx);
|
|
1410
|
+
} catch {
|
|
1411
|
+
log.warn("onError callback raised; swallowing");
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function emitToolCall(hooks, event) {
|
|
1415
|
+
if (!hooks?.onToolCall) return;
|
|
1416
|
+
try {
|
|
1417
|
+
hooks.onToolCall(event);
|
|
1418
|
+
} catch {
|
|
1419
|
+
log.warn("onToolCall callback raised; swallowing");
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function encryptDict(data, key) {
|
|
1423
|
+
const nonce = crypto.randomBytes(12);
|
|
1424
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
1425
|
+
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
1426
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1427
|
+
const tag = cipher.getAuthTag();
|
|
1428
|
+
return Buffer.concat([nonce, ciphertext, tag]).toString("base64");
|
|
1429
|
+
}
|
|
1430
|
+
function decryptDict(token, key) {
|
|
1431
|
+
const raw = Buffer.from(token, "base64");
|
|
1432
|
+
const nonce = raw.subarray(0, 12);
|
|
1433
|
+
const tag = raw.subarray(raw.length - 16);
|
|
1434
|
+
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
1435
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
1436
|
+
decipher.setAuthTag(tag);
|
|
1437
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1438
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
1439
|
+
}
|
|
1440
|
+
function getSecretKey(config) {
|
|
1441
|
+
const secretKey = config.secretKey;
|
|
1442
|
+
if (!secretKey) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
"ContextEngineConfig.secretKey (env CE_SECRET_KEY) is not configured \u2014 a base64url-encoded 32-byte AES key is required to encrypt/decrypt tool configs that hold secrets."
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
return Buffer.from(secretKey, "base64url");
|
|
1448
|
+
}
|
|
1449
|
+
function hmacSha256Hex(key, value) {
|
|
1450
|
+
const k = typeof key === "string" ? Buffer.from(key) : key;
|
|
1451
|
+
return crypto.createHmac("sha256", k).update(value, "utf8").digest("hex");
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/redaction.ts
|
|
1455
|
+
var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
1456
|
+
var PHONE_RE = /\+\d[\d\s-]{7,17}\d/g;
|
|
1457
|
+
var SSN_RE = /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g;
|
|
1458
|
+
var CARD_RE = /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g;
|
|
1459
|
+
var IBAN_RE = /(?<![A-Za-z0-9])[A-Z]{2}\d{2}[A-Z0-9]{10,30}(?![A-Za-z0-9])/g;
|
|
1460
|
+
var API_KEY_ALNUM = "A-Za-z0-9_\\-+/=";
|
|
1461
|
+
var API_KEY_PREFIX_RE = new RegExp(
|
|
1462
|
+
`(?<![${API_KEY_ALNUM}])(?:(?:AKIA|ASIA)[0-9A-Z]{16}|sk-[A-Za-z0-9]{20,}|gh[opsu]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9\\-]{10,}|AIza[0-9A-Za-z_\\-]{35})(?![${API_KEY_ALNUM}])`,
|
|
1463
|
+
"g"
|
|
1464
|
+
);
|
|
1465
|
+
var API_KEY_GENERIC_RE = new RegExp(
|
|
1466
|
+
`(?<![${API_KEY_ALNUM}])[${API_KEY_ALNUM}]{24,}(?![${API_KEY_ALNUM}])`,
|
|
1467
|
+
"g"
|
|
1468
|
+
);
|
|
1469
|
+
var UUID_RE2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1470
|
+
function spansFrom(re, text) {
|
|
1471
|
+
const out = [];
|
|
1472
|
+
re.lastIndex = 0;
|
|
1473
|
+
let m = re.exec(text);
|
|
1474
|
+
while (m !== null) {
|
|
1475
|
+
out.push([m.index, m.index + m[0].length]);
|
|
1476
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
1477
|
+
m = re.exec(text);
|
|
1478
|
+
}
|
|
1479
|
+
return out;
|
|
1480
|
+
}
|
|
1481
|
+
function luhnOk(digits) {
|
|
1482
|
+
let total = 0;
|
|
1483
|
+
const rev = [...digits].reverse();
|
|
1484
|
+
for (let i = 0; i < rev.length; i++) {
|
|
1485
|
+
let d = Number(rev[i]);
|
|
1486
|
+
if (i % 2 === 1) {
|
|
1487
|
+
d *= 2;
|
|
1488
|
+
if (d > 9) d -= 9;
|
|
1489
|
+
}
|
|
1490
|
+
total += d;
|
|
1491
|
+
}
|
|
1492
|
+
return total % 10 === 0;
|
|
1493
|
+
}
|
|
1494
|
+
function detectCreditCard(text) {
|
|
1495
|
+
const spans = [];
|
|
1496
|
+
for (const [start, end] of spansFrom(CARD_RE, text)) {
|
|
1497
|
+
const digits = text.slice(start, end).replace(/[ -]/g, "");
|
|
1498
|
+
if (digits.length >= 13 && digits.length <= 19 && luhnOk(digits)) {
|
|
1499
|
+
spans.push([start, end]);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return spans;
|
|
1503
|
+
}
|
|
1504
|
+
function looksLikeGenericSecret(token) {
|
|
1505
|
+
if (UUID_RE2.test(token)) return false;
|
|
1506
|
+
let hasUpper = false;
|
|
1507
|
+
let hasLower = false;
|
|
1508
|
+
let hasDigit = false;
|
|
1509
|
+
for (const c of token) {
|
|
1510
|
+
if (c >= "A" && c <= "Z") hasUpper = true;
|
|
1511
|
+
else if (c >= "a" && c <= "z") hasLower = true;
|
|
1512
|
+
else if (c >= "0" && c <= "9") hasDigit = true;
|
|
1513
|
+
}
|
|
1514
|
+
return hasUpper && hasLower && hasDigit;
|
|
1515
|
+
}
|
|
1516
|
+
function detectApiKey(text) {
|
|
1517
|
+
const spans = spansFrom(API_KEY_PREFIX_RE, text);
|
|
1518
|
+
for (const [start, end] of spansFrom(API_KEY_GENERIC_RE, text)) {
|
|
1519
|
+
if (spans.some(([s, e]) => start < e && s < end)) continue;
|
|
1520
|
+
if (looksLikeGenericSecret(text.slice(start, end))) spans.push([start, end]);
|
|
1521
|
+
}
|
|
1522
|
+
return spans;
|
|
1523
|
+
}
|
|
1524
|
+
var BUILTIN = {
|
|
1525
|
+
email: (t) => spansFrom(EMAIL_RE, t),
|
|
1526
|
+
phone: (t) => spansFrom(PHONE_RE, t),
|
|
1527
|
+
ssn: (t) => spansFrom(SSN_RE, t),
|
|
1528
|
+
credit_card: detectCreditCard,
|
|
1529
|
+
iban: (t) => spansFrom(IBAN_RE, t),
|
|
1530
|
+
api_key: detectApiKey
|
|
1531
|
+
};
|
|
1532
|
+
function detectBuiltin(name, text) {
|
|
1533
|
+
const detector = BUILTIN[name];
|
|
1534
|
+
if (!detector) throw new Error(`unknown built-in detector: ${name}`);
|
|
1535
|
+
if (!text) return [];
|
|
1536
|
+
return detector(text).sort((a, b) => a[0] - b[0]);
|
|
1537
|
+
}
|
|
1538
|
+
function ruleApplies(rule, opts) {
|
|
1539
|
+
if (rule.applyAt !== "both" && rule.applyAt !== opts.phase) return false;
|
|
1540
|
+
if (!rule.unless.length) return true;
|
|
1541
|
+
if (opts.principals === null) return false;
|
|
1542
|
+
const held = new Set(opts.principals);
|
|
1543
|
+
return !rule.unless.some((p) => held.has(p));
|
|
1544
|
+
}
|
|
1545
|
+
function spansForRule(rule, text, policy) {
|
|
1546
|
+
if (rule.pattern !== null) {
|
|
1547
|
+
const re = rule.patternRe() ?? new RegExp(rule.pattern, "g");
|
|
1548
|
+
return spansFrom(re, text);
|
|
1549
|
+
}
|
|
1550
|
+
if (rule.detector !== null) {
|
|
1551
|
+
const custom = policy.customDetectors[rule.detector];
|
|
1552
|
+
if (custom) return [...custom(text)];
|
|
1553
|
+
return detectBuiltin(rule.detector, text);
|
|
1554
|
+
}
|
|
1555
|
+
return [];
|
|
1556
|
+
}
|
|
1557
|
+
var HASH_TOKEN_CHARS = 16;
|
|
1558
|
+
function hashToken(value, secretKey) {
|
|
1559
|
+
const key = Buffer.isBuffer(secretKey) ? secretKey : Buffer.from(String(secretKey ?? ""));
|
|
1560
|
+
return hmacSha256Hex(key, value).slice(0, HASH_TOKEN_CHARS);
|
|
1561
|
+
}
|
|
1562
|
+
function applyRedaction(text, policy, opts) {
|
|
1563
|
+
if (!text || policy.isEmpty()) return [text, {}];
|
|
1564
|
+
const principals = opts.principals ?? null;
|
|
1565
|
+
const collected = [];
|
|
1566
|
+
const fired = [];
|
|
1567
|
+
const failed = [];
|
|
1568
|
+
for (const rule of policy.rules) {
|
|
1569
|
+
if (!ruleApplies(rule, { phase: opts.phase, principals })) continue;
|
|
1570
|
+
if (rule.action === "hash" && !opts.secretKey) {
|
|
1571
|
+
throw new Error(
|
|
1572
|
+
`rule '${rule.name}': action='hash' requires a non-empty secretKey (an unkeyed HMAC is a reversible pseudonym, not a redaction)`
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
const raw = spansForRule(rule, text, policy);
|
|
1577
|
+
const ruleSpans = [];
|
|
1578
|
+
let invalid = false;
|
|
1579
|
+
for (const item of raw) {
|
|
1580
|
+
const start = item?.[0];
|
|
1581
|
+
const end = item?.[1];
|
|
1582
|
+
if (typeof start !== "number" || typeof end !== "number") {
|
|
1583
|
+
invalid = true;
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if (!(start >= 0 && start < end && end <= text.length)) {
|
|
1587
|
+
invalid = true;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
ruleSpans.push([start, end]);
|
|
1591
|
+
}
|
|
1592
|
+
if (invalid) failed.push(rule.name);
|
|
1593
|
+
for (const [s, e] of ruleSpans) collected.push([s, e, rule]);
|
|
1594
|
+
} catch (exc) {
|
|
1595
|
+
failed.push(rule.name);
|
|
1596
|
+
if (opts.hooks) emitError(opts.hooks, exc, { stage: "redaction", rule: rule.name });
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (!collected.length) {
|
|
1600
|
+
if (failed.length) return [text, { rules_fired: [], spans: 0, rules_failed: failed }];
|
|
1601
|
+
return [text, {}];
|
|
1602
|
+
}
|
|
1603
|
+
collected.sort((a, b) => a[0] - b[0] || b[1] - b[0] - (a[1] - a[0]));
|
|
1604
|
+
const merged = [];
|
|
1605
|
+
for (const [start, end, rule] of collected) {
|
|
1606
|
+
const last = merged[merged.length - 1];
|
|
1607
|
+
if (last && start < last[1]) {
|
|
1608
|
+
if (end > last[1]) last[1] = end;
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
merged.push([start, end, rule]);
|
|
1612
|
+
}
|
|
1613
|
+
const out = [];
|
|
1614
|
+
let cursor = 0;
|
|
1615
|
+
for (const [start, end, rule] of merged) {
|
|
1616
|
+
out.push(text.slice(cursor, start));
|
|
1617
|
+
const original = text.slice(start, end);
|
|
1618
|
+
if (rule.action === "mask") out.push(rule.effectivePlaceholder());
|
|
1619
|
+
else if (rule.action === "hash") {
|
|
1620
|
+
out.push(`[${rule.name.toUpperCase()}:${hashToken(original, opts.secretKey)}]`);
|
|
1621
|
+
}
|
|
1622
|
+
if (!fired.includes(rule.name)) fired.push(rule.name);
|
|
1623
|
+
cursor = end;
|
|
1624
|
+
}
|
|
1625
|
+
out.push(text.slice(cursor));
|
|
1626
|
+
const note = { rules_fired: fired, spans: merged.length };
|
|
1627
|
+
if (failed.length) note.rules_failed = failed;
|
|
1628
|
+
return [out.join(""), note];
|
|
1629
|
+
}
|
|
1630
|
+
var MAX_OUTPUT_SIZE = 5e4;
|
|
1631
|
+
var MAX_ERROR_MESSAGE_SIZE = 2e3;
|
|
1632
|
+
var ToolCallTimer = class {
|
|
1633
|
+
started = null;
|
|
1634
|
+
ended = null;
|
|
1635
|
+
start() {
|
|
1636
|
+
this.started = Date.now();
|
|
1637
|
+
return this;
|
|
1638
|
+
}
|
|
1639
|
+
stop() {
|
|
1640
|
+
this.ended = Date.now();
|
|
1641
|
+
return this;
|
|
1642
|
+
}
|
|
1643
|
+
get durationMs() {
|
|
1644
|
+
if (this.started == null || this.ended == null) return null;
|
|
1645
|
+
return this.ended - this.started;
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
function sanitize(obj) {
|
|
1649
|
+
if (obj == null) return obj;
|
|
1650
|
+
try {
|
|
1651
|
+
return JSON.parse(JSON.stringify(obj, (_k, v) => typeof v === "bigint" ? String(v) : v));
|
|
1652
|
+
} catch {
|
|
1653
|
+
return { _raw: String(obj) };
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function truncateOutput(outputResult) {
|
|
1657
|
+
if (outputResult == null) return [null, false];
|
|
1658
|
+
let serialized;
|
|
1659
|
+
try {
|
|
1660
|
+
serialized = JSON.stringify(outputResult);
|
|
1661
|
+
} catch {
|
|
1662
|
+
serialized = JSON.stringify(String(outputResult));
|
|
1663
|
+
}
|
|
1664
|
+
if (serialized.length <= MAX_OUTPUT_SIZE) {
|
|
1665
|
+
try {
|
|
1666
|
+
return [JSON.parse(serialized), false];
|
|
1667
|
+
} catch {
|
|
1668
|
+
return [outputResult, false];
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return [{ _truncated: serialized.slice(0, MAX_OUTPUT_SIZE), _original_size: serialized.length }, true];
|
|
1672
|
+
}
|
|
1673
|
+
function errorMessage(error) {
|
|
1674
|
+
if (error == null) return null;
|
|
1675
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1676
|
+
return text.slice(0, MAX_ERROR_MESSAGE_SIZE);
|
|
1677
|
+
}
|
|
1678
|
+
async function logToolCall(engine, row) {
|
|
1679
|
+
const event = {
|
|
1680
|
+
tool_id: row.toolId != null ? String(row.toolId) : null,
|
|
1681
|
+
tool_name: row.toolName,
|
|
1682
|
+
kind: row.kind,
|
|
1683
|
+
actor_type: row.actorType ?? null,
|
|
1684
|
+
actor_id: row.actorId != null ? String(row.actorId) : null,
|
|
1685
|
+
source: row.source ?? null,
|
|
1686
|
+
input_args: row.inputArgs ?? {},
|
|
1687
|
+
output_result: row.outputResult,
|
|
1688
|
+
success: row.success ?? null,
|
|
1689
|
+
error: row.error != null ? String(row.error) : null,
|
|
1690
|
+
duration_ms: row.durationMs ?? null,
|
|
1691
|
+
units: row.units ?? null,
|
|
1692
|
+
approval_id: row.approvalId ?? null
|
|
1693
|
+
};
|
|
1694
|
+
try {
|
|
1695
|
+
emitToolCall(engine.hooks, event);
|
|
1696
|
+
} catch {
|
|
1697
|
+
}
|
|
1698
|
+
try {
|
|
1699
|
+
const [stored, truncated] = truncateOutput(row.outputResult);
|
|
1700
|
+
const args = sanitize(row.inputArgs ?? {}) ?? {};
|
|
1701
|
+
await engine.pool.query(
|
|
1702
|
+
`INSERT INTO context_engine_tool_calls
|
|
1703
|
+
(id, tool_id, tool_name, kind, actor_type, actor_id, source, input_args, output_result,
|
|
1704
|
+
output_truncated, success, error_message, duration_ms, units, approval_id)
|
|
1705
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9::jsonb,$10,$11,$12,$13,$14,$15)`,
|
|
1706
|
+
[
|
|
1707
|
+
crypto.randomUUID(),
|
|
1708
|
+
row.toolId ?? null,
|
|
1709
|
+
row.toolName,
|
|
1710
|
+
row.kind,
|
|
1711
|
+
row.actorType ?? null,
|
|
1712
|
+
row.actorId != null ? String(row.actorId) : null,
|
|
1713
|
+
row.source ?? "api",
|
|
1714
|
+
JSON.stringify(args),
|
|
1715
|
+
JSON.stringify(stored),
|
|
1716
|
+
truncated,
|
|
1717
|
+
row.success ?? null,
|
|
1718
|
+
errorMessage(row.error),
|
|
1719
|
+
row.durationMs ?? null,
|
|
1720
|
+
row.units ?? 1,
|
|
1721
|
+
row.approvalId ?? null
|
|
1722
|
+
]
|
|
1723
|
+
);
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/tools/governance.ts
|
|
1729
|
+
init_db();
|
|
1730
|
+
|
|
1731
|
+
// src/tools/executors/http.ts
|
|
1732
|
+
var TIMEOUT_MS = 3e4;
|
|
1733
|
+
var SENSITIVE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
1734
|
+
"set-cookie",
|
|
1735
|
+
"set-cookie2",
|
|
1736
|
+
"authorization",
|
|
1737
|
+
"proxy-authenticate",
|
|
1738
|
+
"www-authenticate"
|
|
1739
|
+
]);
|
|
1740
|
+
function percentEncode(value) {
|
|
1741
|
+
return encodeURIComponent(value).replace(
|
|
1742
|
+
/[!'()*]/g,
|
|
1743
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
function extract(schema, args) {
|
|
1747
|
+
if (!schema) return {};
|
|
1748
|
+
const properties = schema.properties ?? {};
|
|
1749
|
+
const required = schema.required ?? [];
|
|
1750
|
+
const values = {};
|
|
1751
|
+
for (const key of Object.keys(properties)) {
|
|
1752
|
+
const value = args[key];
|
|
1753
|
+
if (required.includes(key) && (value === void 0 || value === null || value === "")) {
|
|
1754
|
+
throw new Error(`Missing required LLM parameter: ${key}`);
|
|
1755
|
+
}
|
|
1756
|
+
if (value !== void 0 && value !== null) values[key] = value;
|
|
1757
|
+
}
|
|
1758
|
+
return values;
|
|
1759
|
+
}
|
|
1760
|
+
function withQuery(url, params) {
|
|
1761
|
+
const usp = new URLSearchParams();
|
|
1762
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1763
|
+
if (v === void 0 || v === null) continue;
|
|
1764
|
+
usp.set(k, String(v));
|
|
1765
|
+
}
|
|
1766
|
+
const q = usp.toString();
|
|
1767
|
+
if (!q) return url;
|
|
1768
|
+
return url.includes("?") ? `${url}&${q}` : `${url}?${q}`;
|
|
1769
|
+
}
|
|
1770
|
+
async function defaultRequest(init) {
|
|
1771
|
+
let url = init.url;
|
|
1772
|
+
if (init.params) url = withQuery(url, init.params);
|
|
1773
|
+
const headers = { ...init.headers ?? {} };
|
|
1774
|
+
let body;
|
|
1775
|
+
if (init.json !== void 0) {
|
|
1776
|
+
headers["Content-Type"] = headers["Content-Type"] ?? headers["content-type"] ?? "application/json";
|
|
1777
|
+
body = JSON.stringify(init.json);
|
|
1778
|
+
} else if (init.content !== void 0) {
|
|
1779
|
+
body = init.content;
|
|
1780
|
+
}
|
|
1781
|
+
const resp = await fetch(url, {
|
|
1782
|
+
method: init.method,
|
|
1783
|
+
headers,
|
|
1784
|
+
body,
|
|
1785
|
+
redirect: "follow",
|
|
1786
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
1787
|
+
});
|
|
1788
|
+
const respHeaders = {};
|
|
1789
|
+
resp.headers.forEach((v, k) => {
|
|
1790
|
+
respHeaders[k] = v;
|
|
1791
|
+
});
|
|
1792
|
+
const text = await resp.text();
|
|
1793
|
+
return {
|
|
1794
|
+
status: resp.status,
|
|
1795
|
+
headers: respHeaders,
|
|
1796
|
+
async json() {
|
|
1797
|
+
return JSON.parse(text);
|
|
1798
|
+
},
|
|
1799
|
+
async text() {
|
|
1800
|
+
return text;
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
async function executeHttp(config, args, opts = {}) {
|
|
1805
|
+
const method = String(config.method ?? "GET").toUpperCase();
|
|
1806
|
+
let url = config.url;
|
|
1807
|
+
const headers = { ...config.headers ?? {} };
|
|
1808
|
+
const staticParams = { ...config.parameters ?? {} };
|
|
1809
|
+
const body = config.body;
|
|
1810
|
+
const runtimeArgs = args ?? {};
|
|
1811
|
+
const queryValues = extract(config.llmQueryParameters, runtimeArgs);
|
|
1812
|
+
const bodyValues = extract(config.llmParameters, runtimeArgs);
|
|
1813
|
+
if (url?.includes("{{")) {
|
|
1814
|
+
for (const source of [queryValues, bodyValues]) {
|
|
1815
|
+
for (const key of Object.keys(source)) {
|
|
1816
|
+
const placeholder = `{{${key}}}`;
|
|
1817
|
+
if (url.includes(placeholder)) {
|
|
1818
|
+
url = url.replaceAll(placeholder, percentEncode(String(source[key])));
|
|
1819
|
+
delete source[key];
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
const requestInit = {
|
|
1825
|
+
method,
|
|
1826
|
+
url: url ?? "",
|
|
1827
|
+
headers
|
|
1828
|
+
};
|
|
1829
|
+
if (method === "GET" || method === "DELETE") {
|
|
1830
|
+
const query = { ...staticParams, ...queryValues, ...bodyValues };
|
|
1831
|
+
if (Object.keys(query).length) requestInit.params = query;
|
|
1832
|
+
} else {
|
|
1833
|
+
const query = { ...staticParams, ...queryValues };
|
|
1834
|
+
if (Object.keys(query).length) requestInit.params = query;
|
|
1835
|
+
let jsonBody = {};
|
|
1836
|
+
if (body !== void 0 && body !== null) {
|
|
1837
|
+
if (typeof body === "object" && !Array.isArray(body)) {
|
|
1838
|
+
jsonBody = { ...body };
|
|
1839
|
+
} else if (typeof body === "string") {
|
|
1840
|
+
try {
|
|
1841
|
+
const parsed = JSON.parse(body);
|
|
1842
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1843
|
+
jsonBody = parsed;
|
|
1844
|
+
}
|
|
1845
|
+
} catch {
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
Object.assign(jsonBody, bodyValues);
|
|
1850
|
+
if (Object.keys(jsonBody).length) {
|
|
1851
|
+
requestInit.json = jsonBody;
|
|
1852
|
+
} else if (body !== void 0 && body !== null) {
|
|
1853
|
+
const contentType2 = headers["Content-Type"] ?? headers["content-type"] ?? "";
|
|
1854
|
+
if (typeof body === "string" && !contentType2.includes("application/json")) {
|
|
1855
|
+
requestInit.content = body;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const client = opts.client;
|
|
1860
|
+
const resp = client ? await client.request(requestInit) : await defaultRequest(requestInit);
|
|
1861
|
+
const contentType = resp.headers["content-type"] ?? resp.headers["Content-Type"] ?? "";
|
|
1862
|
+
let data;
|
|
1863
|
+
try {
|
|
1864
|
+
data = contentType.includes("application/json") ? await resp.json() : await resp.text();
|
|
1865
|
+
} catch {
|
|
1866
|
+
data = await resp.text();
|
|
1867
|
+
}
|
|
1868
|
+
const safeHeaders = {};
|
|
1869
|
+
for (const [k, v] of Object.entries(resp.headers)) {
|
|
1870
|
+
if (!SENSITIVE_RESPONSE_HEADERS.has(k.toLowerCase())) safeHeaders[k] = v;
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
status_code: resp.status,
|
|
1874
|
+
headers: safeHeaders,
|
|
1875
|
+
data
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/tools/registry.ts
|
|
1880
|
+
function httpParamsSchema(config) {
|
|
1881
|
+
const properties = {};
|
|
1882
|
+
const required = [];
|
|
1883
|
+
for (const key of ["llmParameters", "llmQueryParameters"]) {
|
|
1884
|
+
const src = config[key] ?? {};
|
|
1885
|
+
if (!src || typeof src !== "object") continue;
|
|
1886
|
+
Object.assign(properties, src.properties ?? {});
|
|
1887
|
+
const req = src.required;
|
|
1888
|
+
if (Array.isArray(req)) required.push(...req.map(String));
|
|
1889
|
+
}
|
|
1890
|
+
return { type: "object", properties, required };
|
|
1891
|
+
}
|
|
1892
|
+
function expandMcpTools(tc) {
|
|
1893
|
+
const config = tc.config ?? {};
|
|
1894
|
+
const discovered = config.discovered_tools ?? [];
|
|
1895
|
+
if (!discovered.length) {
|
|
1896
|
+
return [
|
|
1897
|
+
{
|
|
1898
|
+
id: tc.id ?? null,
|
|
1899
|
+
kind: "mcp",
|
|
1900
|
+
callName: `mcp_${tc.name}`,
|
|
1901
|
+
displayName: tc.name,
|
|
1902
|
+
description: tc.description || "MCP tool",
|
|
1903
|
+
paramsSchema: { type: "object", properties: {}, required: [] },
|
|
1904
|
+
requiresApproval: tc.requiresApproval,
|
|
1905
|
+
approvalPolicy: tc.approvalPolicy,
|
|
1906
|
+
sourceId: tc.sourceId,
|
|
1907
|
+
acl: tc.acl,
|
|
1908
|
+
raw: config
|
|
1909
|
+
}
|
|
1910
|
+
];
|
|
1911
|
+
}
|
|
1912
|
+
const tools = [];
|
|
1913
|
+
for (const dt of discovered) {
|
|
1914
|
+
if (dt.enabled === false) continue;
|
|
1915
|
+
const name = String(dt.name ?? "unknown");
|
|
1916
|
+
const schema = dt.inputSchema ?? dt.input_schema ?? {
|
|
1917
|
+
type: "object",
|
|
1918
|
+
properties: {},
|
|
1919
|
+
required: []
|
|
1920
|
+
};
|
|
1921
|
+
const toolApproval = dt.requires_approval;
|
|
1922
|
+
const toolApprovalPolicy = dt.approval_policy;
|
|
1923
|
+
tools.push({
|
|
1924
|
+
id: tc.id ?? null,
|
|
1925
|
+
kind: "mcp",
|
|
1926
|
+
callName: `mcp_${name}`,
|
|
1927
|
+
displayName: name,
|
|
1928
|
+
description: String(dt.description ?? `MCP tool: ${name}`),
|
|
1929
|
+
paramsSchema: schema,
|
|
1930
|
+
requiresApproval: toolApproval !== void 0 ? Boolean(toolApproval) : tc.requiresApproval,
|
|
1931
|
+
approvalPolicy: toolApprovalPolicy ?? tc.approvalPolicy,
|
|
1932
|
+
sourceId: tc.sourceId,
|
|
1933
|
+
acl: tc.acl,
|
|
1934
|
+
raw: { ...config, tool_name: name }
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
return tools;
|
|
1938
|
+
}
|
|
1939
|
+
function canonicalFromConfig(tc) {
|
|
1940
|
+
if (tc.kind === "http") {
|
|
1941
|
+
return [
|
|
1942
|
+
{
|
|
1943
|
+
id: tc.id ?? null,
|
|
1944
|
+
kind: "http",
|
|
1945
|
+
callName: `http_${tc.name}`,
|
|
1946
|
+
displayName: tc.name,
|
|
1947
|
+
description: tc.description || `Execute HTTP request: ${tc.name}`,
|
|
1948
|
+
paramsSchema: httpParamsSchema(tc.config ?? {}),
|
|
1949
|
+
requiresApproval: tc.requiresApproval,
|
|
1950
|
+
approvalPolicy: tc.approvalPolicy,
|
|
1951
|
+
sourceId: tc.sourceId,
|
|
1952
|
+
acl: tc.acl,
|
|
1953
|
+
raw: tc.config ?? {}
|
|
1954
|
+
}
|
|
1955
|
+
];
|
|
1956
|
+
}
|
|
1957
|
+
if (tc.kind === "db") {
|
|
1958
|
+
return [
|
|
1959
|
+
{
|
|
1960
|
+
id: tc.id ?? null,
|
|
1961
|
+
kind: "db",
|
|
1962
|
+
callName: `db_${tc.name}`,
|
|
1963
|
+
displayName: tc.name,
|
|
1964
|
+
description: tc.description || `Query database: ${tc.name}`,
|
|
1965
|
+
paramsSchema: {
|
|
1966
|
+
type: "object",
|
|
1967
|
+
properties: {
|
|
1968
|
+
query: {
|
|
1969
|
+
type: "string",
|
|
1970
|
+
description: "Either 'describe:table1,table2' to get column details, or a SQL query to execute"
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
required: ["query"]
|
|
1974
|
+
},
|
|
1975
|
+
requiresApproval: tc.requiresApproval,
|
|
1976
|
+
approvalPolicy: tc.approvalPolicy,
|
|
1977
|
+
sourceId: tc.sourceId,
|
|
1978
|
+
acl: tc.acl,
|
|
1979
|
+
raw: tc.config ?? {}
|
|
1980
|
+
}
|
|
1981
|
+
];
|
|
1982
|
+
}
|
|
1983
|
+
if (tc.kind === "mcp") return expandMcpTools(tc);
|
|
1984
|
+
throw new Error(
|
|
1985
|
+
`canonicalFromConfig does not support kind=${JSON.stringify(tc.kind)}; function tools have no serializable config \u2014 build them with functionTool(fn) instead`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
function findTool(tools, callName) {
|
|
1989
|
+
return tools.find((t) => t.callName === callName);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// src/tools/governance.ts
|
|
1993
|
+
var RESULT_MAX_CHARS = 8e3;
|
|
1994
|
+
var RESULT_MAX_ROWS = 100;
|
|
1995
|
+
var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
1996
|
+
"name",
|
|
1997
|
+
"kind",
|
|
1998
|
+
"description",
|
|
1999
|
+
"sourceId",
|
|
2000
|
+
"source_id",
|
|
2001
|
+
"acl",
|
|
2002
|
+
"requiresApproval",
|
|
2003
|
+
"requires_approval",
|
|
2004
|
+
"approvalPolicy",
|
|
2005
|
+
"approval_policy",
|
|
2006
|
+
"enabled"
|
|
2007
|
+
]);
|
|
2008
|
+
function toolVisible(ct, principals) {
|
|
2009
|
+
return aclVisible(ct.acl, principals);
|
|
2010
|
+
}
|
|
2011
|
+
function stripUnderscoreArgs(args) {
|
|
2012
|
+
const out = {};
|
|
2013
|
+
for (const [k, v] of Object.entries(args ?? {})) {
|
|
2014
|
+
if (!String(k).startsWith("_")) out[k] = v;
|
|
2015
|
+
}
|
|
2016
|
+
return out;
|
|
2017
|
+
}
|
|
2018
|
+
function shapeResult(result, opts = {}) {
|
|
2019
|
+
const maxChars = opts.maxChars ?? RESULT_MAX_CHARS;
|
|
2020
|
+
const maxRows = opts.maxRows ?? RESULT_MAX_ROWS;
|
|
2021
|
+
let truncated = false;
|
|
2022
|
+
let shaped = result;
|
|
2023
|
+
if (result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
|
|
2024
|
+
const rows = result.rows;
|
|
2025
|
+
const kept = rows.slice(0, maxRows);
|
|
2026
|
+
shaped = {
|
|
2027
|
+
...result,
|
|
2028
|
+
rows: kept,
|
|
2029
|
+
_result_shaping: { rows_returned: kept.length, rows_omitted: rows.length - kept.length }
|
|
2030
|
+
};
|
|
2031
|
+
truncated = true;
|
|
2032
|
+
}
|
|
2033
|
+
let serialized;
|
|
2034
|
+
try {
|
|
2035
|
+
serialized = JSON.stringify(shaped);
|
|
2036
|
+
} catch {
|
|
2037
|
+
serialized = JSON.stringify(String(shaped));
|
|
2038
|
+
}
|
|
2039
|
+
if (serialized.length > maxChars) {
|
|
2040
|
+
return [
|
|
2041
|
+
{
|
|
2042
|
+
_truncated: serialized.slice(0, maxChars),
|
|
2043
|
+
_original_size: serialized.length,
|
|
2044
|
+
_note: "tool result exceeded the context budget and was truncated"
|
|
2045
|
+
},
|
|
2046
|
+
true
|
|
2047
|
+
];
|
|
2048
|
+
}
|
|
2049
|
+
return [shaped, truncated];
|
|
2050
|
+
}
|
|
2051
|
+
function redactToolResult(result, policy, opts) {
|
|
2052
|
+
if (policy.isEmpty()) return [result, {}];
|
|
2053
|
+
const fired = [];
|
|
2054
|
+
const failed = [];
|
|
2055
|
+
let total = 0;
|
|
2056
|
+
const walk = (node) => {
|
|
2057
|
+
if (typeof node === "string") {
|
|
2058
|
+
const [text, note2] = applyRedaction(node, policy, {
|
|
2059
|
+
phase: "output",
|
|
2060
|
+
principals: opts.principals ?? null,
|
|
2061
|
+
secretKey: opts.secretKey,
|
|
2062
|
+
hooks: opts.hooks
|
|
2063
|
+
});
|
|
2064
|
+
if (note2 && Object.keys(note2).length) {
|
|
2065
|
+
total += note2.spans ?? 0;
|
|
2066
|
+
for (const name of note2.rules_fired ?? []) {
|
|
2067
|
+
if (!fired.includes(name)) fired.push(name);
|
|
2068
|
+
}
|
|
2069
|
+
for (const name of note2.rules_failed ?? []) {
|
|
2070
|
+
if (!failed.includes(name)) failed.push(name);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return text;
|
|
2074
|
+
}
|
|
2075
|
+
if (node && typeof node === "object") {
|
|
2076
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
2077
|
+
const out = {};
|
|
2078
|
+
for (const [k, v] of Object.entries(node)) out[k] = walk(v);
|
|
2079
|
+
return out;
|
|
2080
|
+
}
|
|
2081
|
+
return node;
|
|
2082
|
+
};
|
|
2083
|
+
const redacted = walk(result);
|
|
2084
|
+
if (!fired.length && !failed.length) return [redacted, {}];
|
|
2085
|
+
const note = { rules_fired: fired, spans: total };
|
|
2086
|
+
if (failed.length) note.rules_failed = failed;
|
|
2087
|
+
return [redacted, note];
|
|
2088
|
+
}
|
|
2089
|
+
function rowToToolConfig(row, engine) {
|
|
2090
|
+
let config = {};
|
|
2091
|
+
if (row.config_encrypted) {
|
|
2092
|
+
config = decryptDict(String(row.config_encrypted), getSecretKey(engine.config));
|
|
2093
|
+
}
|
|
2094
|
+
return new ToolConfig({
|
|
2095
|
+
id: String(row.id),
|
|
2096
|
+
name: String(row.name),
|
|
2097
|
+
kind: row.kind,
|
|
2098
|
+
description: String(row.description ?? ""),
|
|
2099
|
+
config,
|
|
2100
|
+
sourceId: row.source_id ?? null,
|
|
2101
|
+
acl: row.acl != null ? [...row.acl] : null,
|
|
2102
|
+
requiresApproval: Boolean(row.requires_approval),
|
|
2103
|
+
approvalPolicy: row.approval_policy ?? {},
|
|
2104
|
+
enabled: Boolean(row.enabled)
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
async function loadPersistedCanonicals(engine, sourceId) {
|
|
2108
|
+
const params = [];
|
|
2109
|
+
let sql = `SELECT * FROM context_engine_tools WHERE enabled IS TRUE`;
|
|
2110
|
+
if (sourceId != null) {
|
|
2111
|
+
params.push(sourceId);
|
|
2112
|
+
sql += ` AND source_id = $1`;
|
|
2113
|
+
}
|
|
2114
|
+
const result = await engine.pool.query(sql, params);
|
|
2115
|
+
const canonicals = [];
|
|
2116
|
+
for (const row of result.rows) {
|
|
2117
|
+
canonicals.push(...canonicalFromConfig(rowToToolConfig(row, engine)));
|
|
2118
|
+
}
|
|
2119
|
+
return canonicals;
|
|
2120
|
+
}
|
|
2121
|
+
async function mergedTools(engine, sourceId) {
|
|
2122
|
+
const persisted = await loadPersistedCanonicals(engine, sourceId);
|
|
2123
|
+
const functions = Object.values(engine._functionTools ?? {});
|
|
2124
|
+
return [...persisted, ...functions];
|
|
2125
|
+
}
|
|
2126
|
+
async function registerTool(engine, tc) {
|
|
2127
|
+
canonicalFromConfig(tc);
|
|
2128
|
+
const config = tc.config ?? {};
|
|
2129
|
+
const configEncrypted = Object.keys(config).length ? encryptDict(config, getSecretKey(engine.config)) : null;
|
|
2130
|
+
const id = crypto.randomUUID();
|
|
2131
|
+
await engine.pool.query(
|
|
2132
|
+
`INSERT INTO context_engine_tools
|
|
2133
|
+
(id, name, kind, description, source_id, acl, config_encrypted,
|
|
2134
|
+
requires_approval, approval_policy, enabled)
|
|
2135
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
|
|
2136
|
+
[
|
|
2137
|
+
id,
|
|
2138
|
+
tc.name,
|
|
2139
|
+
tc.kind,
|
|
2140
|
+
tc.description || "",
|
|
2141
|
+
tc.sourceId,
|
|
2142
|
+
tc.acl,
|
|
2143
|
+
configEncrypted,
|
|
2144
|
+
tc.requiresApproval,
|
|
2145
|
+
JSON.stringify(tc.approvalPolicy ?? {}),
|
|
2146
|
+
tc.enabled
|
|
2147
|
+
]
|
|
2148
|
+
);
|
|
2149
|
+
return id;
|
|
2150
|
+
}
|
|
2151
|
+
async function updateTool(engine, id, opts) {
|
|
2152
|
+
const { principals = null, ...fields } = opts;
|
|
2153
|
+
const existing = await engine.pool.query(`SELECT * FROM context_engine_tools WHERE id = $1`, [id]);
|
|
2154
|
+
const row = existing.rows[0];
|
|
2155
|
+
if (!row || !aclVisible(row.acl != null ? [...row.acl] : null, principals)) {
|
|
2156
|
+
throw new EngineActionError(`tool not found: ${id}`);
|
|
2157
|
+
}
|
|
2158
|
+
const sets = [];
|
|
2159
|
+
const params = [];
|
|
2160
|
+
let i = 1;
|
|
2161
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
2162
|
+
if (key === "config") {
|
|
2163
|
+
sets.push(`config_encrypted = $${i++}`);
|
|
2164
|
+
params.push(value ? encryptDict(value, getSecretKey(engine.config)) : null);
|
|
2165
|
+
} else if (UPDATABLE_COLUMNS.has(key)) {
|
|
2166
|
+
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key;
|
|
2167
|
+
sets.push(`${col} = $${i++}`);
|
|
2168
|
+
params.push(col === "approval_policy" ? JSON.stringify(value ?? {}) : value);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
sets.push(`updated_at = now()`);
|
|
2172
|
+
params.push(id);
|
|
2173
|
+
const updated = await engine.pool.query(
|
|
2174
|
+
`UPDATE context_engine_tools SET ${sets.join(", ")} WHERE id = $${i} RETURNING *`,
|
|
2175
|
+
params
|
|
2176
|
+
);
|
|
2177
|
+
return rowToToolConfig(updated.rows[0], engine);
|
|
2178
|
+
}
|
|
2179
|
+
async function deleteTool(engine, id, opts = {}) {
|
|
2180
|
+
const existing = await engine.pool.query(`SELECT * FROM context_engine_tools WHERE id = $1`, [id]);
|
|
2181
|
+
const row = existing.rows[0];
|
|
2182
|
+
if (!row || !aclVisible(row.acl != null ? [...row.acl] : null, opts.principals ?? null)) {
|
|
2183
|
+
throw new EngineActionError(`tool not found: ${id}`);
|
|
2184
|
+
}
|
|
2185
|
+
await engine.pool.query(`DELETE FROM context_engine_tools WHERE id = $1`, [id]);
|
|
2186
|
+
}
|
|
2187
|
+
var PROBE_AUTH = [
|
|
2188
|
+
"authentication failed",
|
|
2189
|
+
"pg_hba",
|
|
2190
|
+
"permission denied",
|
|
2191
|
+
"access denied",
|
|
2192
|
+
"password",
|
|
2193
|
+
"unauthorized",
|
|
2194
|
+
"forbidden",
|
|
2195
|
+
"401",
|
|
2196
|
+
"403",
|
|
2197
|
+
"invalid credentials"
|
|
2198
|
+
];
|
|
2199
|
+
var PROBE_UNREACHABLE = [
|
|
2200
|
+
"connection refused",
|
|
2201
|
+
"could not connect",
|
|
2202
|
+
"name or service not known",
|
|
2203
|
+
"nodename nor servname",
|
|
2204
|
+
"no route to host",
|
|
2205
|
+
"network is unreachable",
|
|
2206
|
+
"temporary failure in name resolution",
|
|
2207
|
+
"getaddrinfo",
|
|
2208
|
+
"connection reset",
|
|
2209
|
+
"fetch failed",
|
|
2210
|
+
"econnrefused",
|
|
2211
|
+
"enotfound"
|
|
2212
|
+
];
|
|
2213
|
+
var PROBE_TIMEOUT = ["timed out", "timeout", "aborted"];
|
|
2214
|
+
var PROBE_MISCONFIGURED = [
|
|
2215
|
+
"does not exist",
|
|
2216
|
+
"invalid",
|
|
2217
|
+
"unknown database",
|
|
2218
|
+
"syntax error",
|
|
2219
|
+
"no such",
|
|
2220
|
+
"missing",
|
|
2221
|
+
"not supported",
|
|
2222
|
+
"unsupported"
|
|
2223
|
+
];
|
|
2224
|
+
function classifyProbeError(exc) {
|
|
2225
|
+
if (exc instanceof DOMException && exc.name === "TimeoutError") return "timeout";
|
|
2226
|
+
const name = exc instanceof Error ? exc.name : "";
|
|
2227
|
+
if (name.includes("Timeout")) return "timeout";
|
|
2228
|
+
if (["ConnectError", "ConnectionError", "NetworkError", "TypeError"].some((t) => name.includes(t))) {
|
|
2229
|
+
if (name === "TypeError" && !String(exc).toLowerCase().includes("fetch")) ; else if (name !== "TypeError") return "unreachable";
|
|
2230
|
+
}
|
|
2231
|
+
if (["Auth", "Permission", "Forbidden", "Unauthorized"].some((t) => name.includes(t))) return "auth_failed";
|
|
2232
|
+
const text = String(exc).toLowerCase();
|
|
2233
|
+
if (PROBE_TIMEOUT.some((t) => text.includes(t))) return "timeout";
|
|
2234
|
+
if (PROBE_AUTH.some((t) => text.includes(t))) return "auth_failed";
|
|
2235
|
+
if (PROBE_UNREACHABLE.some((t) => text.includes(t))) return "unreachable";
|
|
2236
|
+
if (PROBE_MISCONFIGURED.some((t) => text.includes(t))) return "misconfigured";
|
|
2237
|
+
return "error";
|
|
2238
|
+
}
|
|
2239
|
+
function probeFailure(exc, kind) {
|
|
2240
|
+
const category = classifyProbeError(exc);
|
|
2241
|
+
console.warn(`test_tool(kind=${kind}) failed:`, exc, "->", category);
|
|
2242
|
+
return { ok: false, error: category };
|
|
2243
|
+
}
|
|
2244
|
+
async function testTool(_engine, tc) {
|
|
2245
|
+
const kind = tc.kind;
|
|
2246
|
+
const config = tc.config ?? {};
|
|
2247
|
+
if (kind === "http") {
|
|
2248
|
+
const url = config.url;
|
|
2249
|
+
if (!url) return { ok: false, error: "http config missing 'url'" };
|
|
2250
|
+
try {
|
|
2251
|
+
const resp = await fetch(url, {
|
|
2252
|
+
method: "HEAD",
|
|
2253
|
+
headers: config.headers ?? {},
|
|
2254
|
+
redirect: "follow",
|
|
2255
|
+
signal: AbortSignal.timeout(1e4)
|
|
2256
|
+
});
|
|
2257
|
+
return { ok: resp.status < 500, status_code: resp.status };
|
|
2258
|
+
} catch (exc) {
|
|
2259
|
+
return probeFailure(exc, "http");
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (kind === "db") {
|
|
2263
|
+
try {
|
|
2264
|
+
const { testConnection: testConnection2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
2265
|
+
const result = await testConnection2(config);
|
|
2266
|
+
return { ok: Boolean(result.success ?? result.ok), ...result };
|
|
2267
|
+
} catch (exc) {
|
|
2268
|
+
return probeFailure(exc, "db");
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (kind === "mcp") {
|
|
2272
|
+
const url = config.url;
|
|
2273
|
+
if (!url) return { ok: false, error: "mcp config missing 'url'" };
|
|
2274
|
+
const token = config.oauth_token ?? config.bearer ?? config.access_token;
|
|
2275
|
+
const mcp = new PromptevMCP();
|
|
2276
|
+
try {
|
|
2277
|
+
await mcp.addServer(tc.name, url, { token: token ?? null });
|
|
2278
|
+
const client = mcp.clients.get(tc.name);
|
|
2279
|
+
return { ok: true, tools: client.tools.map((t) => t.name) };
|
|
2280
|
+
} catch (exc) {
|
|
2281
|
+
return probeFailure(exc, "mcp");
|
|
2282
|
+
} finally {
|
|
2283
|
+
await mcp.shutdown();
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return { ok: false, error: `test_tool does not support kind=${JSON.stringify(kind)}` };
|
|
2287
|
+
}
|
|
2288
|
+
async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
2289
|
+
const claimedAt = /* @__PURE__ */ new Date();
|
|
2290
|
+
const candidates = await engine.pool.query(
|
|
2291
|
+
`SELECT * FROM context_engine_tool_approvals WHERE tool_name = $1 AND status = 'approved'`,
|
|
2292
|
+
[toolName]
|
|
2293
|
+
);
|
|
2294
|
+
for (const row of candidates.rows) {
|
|
2295
|
+
if ((row.source_id ?? null) !== (sourceId ?? null)) continue;
|
|
2296
|
+
const frozen = row.tool_args_frozen ?? {};
|
|
2297
|
+
if (JSON.stringify(frozen) !== JSON.stringify(args)) continue;
|
|
2298
|
+
const result = await engine.pool.query(
|
|
2299
|
+
`UPDATE context_engine_tool_approvals
|
|
2300
|
+
SET status = 'executed', resolved_at = $1
|
|
2301
|
+
WHERE id = $2 AND status = 'approved'
|
|
2302
|
+
RETURNING id`,
|
|
2303
|
+
[claimedAt, row.id]
|
|
2304
|
+
);
|
|
2305
|
+
if (result.rowCount === 1) return String(row.id);
|
|
2306
|
+
}
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
async function dispatchMcp(ct, config, args) {
|
|
2310
|
+
const url = config.url;
|
|
2311
|
+
if (!url) throw new EngineActionError(`mcp tool ${ct.callName} config missing 'url'`);
|
|
2312
|
+
const headers = { ...config.headers ?? {} };
|
|
2313
|
+
const token = config.oauth_token ?? config.bearer ?? config.access_token;
|
|
2314
|
+
const apiKey = config.api_key;
|
|
2315
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2316
|
+
else if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
2317
|
+
const toolName = String(ct.raw?.tool_name ?? ct.displayName);
|
|
2318
|
+
const mcp = new PromptevMCP();
|
|
2319
|
+
try {
|
|
2320
|
+
await mcp.addServer(ct.callName, url, { headers: Object.keys(headers).length ? headers : null });
|
|
2321
|
+
const client = mcp.clients.get(ct.callName);
|
|
2322
|
+
return await client.callToolWithReconnect(toolName, args);
|
|
2323
|
+
} finally {
|
|
2324
|
+
await mcp.shutdown();
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
async function dispatch(engine, ct, config, args) {
|
|
2328
|
+
if (ct.kind === "http") {
|
|
2329
|
+
return executeHttp(config, args, { client: engine._toolHttpClient ?? null });
|
|
2330
|
+
}
|
|
2331
|
+
if (ct.kind === "db") {
|
|
2332
|
+
const query = String(args.query ?? "");
|
|
2333
|
+
if (query.trim().toLowerCase().startsWith("describe:")) {
|
|
2334
|
+
const tables = query.slice(query.indexOf(":") + 1).split(",").map((t) => t.trim()).filter(Boolean);
|
|
2335
|
+
const schemaText = await getSchemaText(config, tables.length ? tables : null);
|
|
2336
|
+
return { success: true, schema: schemaText };
|
|
2337
|
+
}
|
|
2338
|
+
const result = await executeDb(config, query);
|
|
2339
|
+
if (result && typeof result === "object" && result.success === false) {
|
|
2340
|
+
throw new EngineActionError(String(result.error || "database query failed"));
|
|
2341
|
+
}
|
|
2342
|
+
return result;
|
|
2343
|
+
}
|
|
2344
|
+
if (ct.kind === "mcp") return dispatchMcp(ct, config, args);
|
|
2345
|
+
if (ct.kind === "function") {
|
|
2346
|
+
const fn = ct.raw?.callable;
|
|
2347
|
+
if (!fn) throw new EngineActionError(`function tool ${ct.callName} has no callable`);
|
|
2348
|
+
return await fn(...namedArgs(fn, args));
|
|
2349
|
+
}
|
|
2350
|
+
throw new EngineActionError(`unsupported tool kind: ${JSON.stringify(ct.kind)}`);
|
|
2351
|
+
}
|
|
2352
|
+
function namedArgs(fn, args) {
|
|
2353
|
+
const src = Function.prototype.toString.call(fn);
|
|
2354
|
+
const match = src.match(/\(([^)]*)\)/);
|
|
2355
|
+
const names = (match?.[1] ?? "").split(",").map((p) => p.trim().split(/\s*=/)[0]?.trim()).filter(Boolean);
|
|
2356
|
+
if (!names.length) return Object.values(args);
|
|
2357
|
+
return names.map((n) => args[n]);
|
|
2358
|
+
}
|
|
2359
|
+
async function decryptCtConfig(engine, toolId) {
|
|
2360
|
+
const result = await engine.pool.query(`SELECT config_encrypted FROM context_engine_tools WHERE id = $1`, [
|
|
2361
|
+
toolId
|
|
2362
|
+
]);
|
|
2363
|
+
const row = result.rows[0];
|
|
2364
|
+
if (!row?.config_encrypted) return {};
|
|
2365
|
+
return decryptDict(String(row.config_encrypted), getSecretKey(engine.config));
|
|
2366
|
+
}
|
|
2367
|
+
async function executeTool(engine, callName, args, opts = {}) {
|
|
2368
|
+
const runtimeArgs = args ?? {};
|
|
2369
|
+
const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
|
|
2370
|
+
if (!ct) throw new EngineActionError(`tool not found: ${callName}`);
|
|
2371
|
+
if (!toolVisible(ct, opts.principals ?? null)) {
|
|
2372
|
+
throw new EngineActionError(`tool not found: ${callName}`);
|
|
2373
|
+
}
|
|
2374
|
+
const publicArgs = stripUnderscoreArgs(runtimeArgs);
|
|
2375
|
+
let approvalId = null;
|
|
2376
|
+
if (shouldRequireApproval(ct, publicArgs)) {
|
|
2377
|
+
approvalId = await findAndClaimApproved(engine, ct.callName, publicArgs, opts.sourceId ?? null);
|
|
2378
|
+
if (approvalId == null) {
|
|
2379
|
+
const record = await createPending(engine, {
|
|
2380
|
+
toolName: ct.callName,
|
|
2381
|
+
args: publicArgs,
|
|
2382
|
+
sourceId: opts.sourceId ?? null,
|
|
2383
|
+
principals: opts.principals ?? null,
|
|
2384
|
+
policy: ct.approvalPolicy ?? {}
|
|
2385
|
+
});
|
|
2386
|
+
const reason = ct.requiresApproval ? "tool requires approval" : `approval policy condition met: ${ct.approvalPolicy?.condition}`;
|
|
2387
|
+
return {
|
|
2388
|
+
approval_required: {
|
|
2389
|
+
approval_id: String(record.id),
|
|
2390
|
+
tool_name: ct.callName,
|
|
2391
|
+
args: publicArgs,
|
|
2392
|
+
reason
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
let config = {};
|
|
2398
|
+
if (ct.kind !== "function" && ct.id != null) {
|
|
2399
|
+
config = await decryptCtConfig(engine, ct.id);
|
|
2400
|
+
}
|
|
2401
|
+
const actorType = opts.actor?.type ?? null;
|
|
2402
|
+
const actorId = opts.actor?.id ?? null;
|
|
2403
|
+
let rawResult = null;
|
|
2404
|
+
let exc = null;
|
|
2405
|
+
const timer = new ToolCallTimer().start();
|
|
2406
|
+
try {
|
|
2407
|
+
rawResult = await dispatch(engine, ct, config, publicArgs);
|
|
2408
|
+
} catch (e) {
|
|
2409
|
+
exc = e;
|
|
2410
|
+
}
|
|
2411
|
+
timer.stop();
|
|
2412
|
+
let success = exc == null;
|
|
2413
|
+
let shaped = null;
|
|
2414
|
+
let truncated = false;
|
|
2415
|
+
if (success) {
|
|
2416
|
+
try {
|
|
2417
|
+
const [redacted] = redactToolResult(rawResult, engine.config.redaction, {
|
|
2418
|
+
principals: opts.principals ?? null,
|
|
2419
|
+
secretKey: engine.config.secretKey,
|
|
2420
|
+
hooks: engine.hooks
|
|
2421
|
+
});
|
|
2422
|
+
rawResult = redacted;
|
|
2423
|
+
[shaped, truncated] = shapeResult(rawResult);
|
|
2424
|
+
} catch (e) {
|
|
2425
|
+
exc = e;
|
|
2426
|
+
success = false;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
await logToolCall(engine, {
|
|
2430
|
+
toolId: ct.id,
|
|
2431
|
+
toolName: ct.callName,
|
|
2432
|
+
kind: ct.kind,
|
|
2433
|
+
actorType,
|
|
2434
|
+
actorId,
|
|
2435
|
+
source: opts.source ?? "api",
|
|
2436
|
+
inputArgs: publicArgs,
|
|
2437
|
+
outputResult: success ? rawResult : null,
|
|
2438
|
+
success,
|
|
2439
|
+
error: exc,
|
|
2440
|
+
durationMs: timer.durationMs,
|
|
2441
|
+
units: success ? 1 : 0,
|
|
2442
|
+
approvalId
|
|
2443
|
+
});
|
|
2444
|
+
if (!success) {
|
|
2445
|
+
throw new EngineActionError(`tool execution failed: ${ct.callName}: ${exc}`);
|
|
2446
|
+
}
|
|
2447
|
+
const usage = {
|
|
2448
|
+
kind: "tool",
|
|
2449
|
+
units: 1,
|
|
2450
|
+
detail: { tool_name: ct.callName, kind: ct.kind, truncated }
|
|
2451
|
+
};
|
|
2452
|
+
emitUsage(engine.hooks, usage);
|
|
2453
|
+
return {
|
|
2454
|
+
result: shaped,
|
|
2455
|
+
usage: { units: 1, kind: "tool", tool_name: ct.callName, truncated }
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
var STATE_TTL_SECONDS = 600;
|
|
2459
|
+
var DISCOVERY_TIMEOUT_MS = 1e4;
|
|
2460
|
+
var TOKEN_TIMEOUT_MS = 15e3;
|
|
2461
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
2462
|
+
var REGISTER_TIMEOUT_MS = 1e4;
|
|
2463
|
+
function report(hooks, exc, ctx) {
|
|
2464
|
+
if (hooks) emitError(hooks, exc, ctx);
|
|
2465
|
+
else console.error("[context-engine] mcp_oauth error:", exc, "| ctx=", ctx);
|
|
2466
|
+
}
|
|
2467
|
+
var InMemoryPendingStore = class {
|
|
2468
|
+
store = /* @__PURE__ */ new Map();
|
|
2469
|
+
cleanupExpired() {
|
|
2470
|
+
const now = Date.now() / 1e3;
|
|
2471
|
+
for (const [k, v] of this.store) {
|
|
2472
|
+
if (v.expiresAt <= now) this.store.delete(k);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
put(state, data, ttlSeconds = STATE_TTL_SECONDS) {
|
|
2476
|
+
this.cleanupExpired();
|
|
2477
|
+
this.store.set(state, { data, expiresAt: Date.now() / 1e3 + ttlSeconds });
|
|
2478
|
+
}
|
|
2479
|
+
pop(state) {
|
|
2480
|
+
this.cleanupExpired();
|
|
2481
|
+
const entry = this.store.get(state);
|
|
2482
|
+
this.store.delete(state);
|
|
2483
|
+
if (!entry) return null;
|
|
2484
|
+
if (entry.expiresAt <= Date.now() / 1e3) return null;
|
|
2485
|
+
return entry.data;
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
async function fetchJson(url, opts = {}) {
|
|
2489
|
+
const resp = await fetch(url, {
|
|
2490
|
+
method: opts.method ?? "GET",
|
|
2491
|
+
headers: opts.headers,
|
|
2492
|
+
body: opts.body,
|
|
2493
|
+
redirect: opts.redirect ?? "follow",
|
|
2494
|
+
signal: AbortSignal.timeout(opts.timeout ?? DISCOVERY_TIMEOUT_MS)
|
|
2495
|
+
});
|
|
2496
|
+
return {
|
|
2497
|
+
status: resp.status,
|
|
2498
|
+
json: () => resp.json(),
|
|
2499
|
+
text: () => resp.text()
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
async function discoverOauthMetadata(serverUrl, opts = {}) {
|
|
2503
|
+
const parsed = new URL(serverUrl);
|
|
2504
|
+
const base = `${parsed.protocol}//${parsed.host}`;
|
|
2505
|
+
const wellKnownUrl = `${base}/.well-known/oauth-authorization-server`;
|
|
2506
|
+
try {
|
|
2507
|
+
const resp = await fetchJson(wellKnownUrl, { timeout: DISCOVERY_TIMEOUT_MS });
|
|
2508
|
+
if (resp.status === 200) {
|
|
2509
|
+
const data = await resp.json();
|
|
2510
|
+
if (data.authorization_endpoint && data.token_endpoint) {
|
|
2511
|
+
return {
|
|
2512
|
+
authorization_endpoint: data.authorization_endpoint,
|
|
2513
|
+
token_endpoint: data.token_endpoint,
|
|
2514
|
+
registration_endpoint: data.registration_endpoint,
|
|
2515
|
+
scopes_supported: data.scopes_supported ?? [],
|
|
2516
|
+
response_types_supported: data.response_types_supported ?? [],
|
|
2517
|
+
code_challenge_methods_supported: data.code_challenge_methods_supported ?? ["S256"],
|
|
2518
|
+
issuer: data.issuer ?? base
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
} catch (e) {
|
|
2523
|
+
if (!(e instanceof TypeError) && !(e instanceof DOMException)) {
|
|
2524
|
+
report(opts.hooks, e, { stage: "discover_oauth_metadata_8414", url: wellKnownUrl });
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
const oidcUrl = `${base}/.well-known/openid-configuration`;
|
|
2528
|
+
try {
|
|
2529
|
+
const resp = await fetchJson(oidcUrl, { timeout: DISCOVERY_TIMEOUT_MS });
|
|
2530
|
+
if (resp.status === 200) {
|
|
2531
|
+
const data = await resp.json();
|
|
2532
|
+
if (data.authorization_endpoint && data.token_endpoint) {
|
|
2533
|
+
return {
|
|
2534
|
+
authorization_endpoint: data.authorization_endpoint,
|
|
2535
|
+
token_endpoint: data.token_endpoint,
|
|
2536
|
+
registration_endpoint: data.registration_endpoint,
|
|
2537
|
+
scopes_supported: data.scopes_supported ?? [],
|
|
2538
|
+
code_challenge_methods_supported: data.code_challenge_methods_supported ?? ["S256"],
|
|
2539
|
+
issuer: data.issuer ?? base
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
} catch {
|
|
2544
|
+
}
|
|
2545
|
+
for (const [authPath, tokenPath] of [
|
|
2546
|
+
["/oauth/authorize", "/oauth/token"],
|
|
2547
|
+
["/authorize", "/token"],
|
|
2548
|
+
["/oauth2/authorize", "/oauth2/token"]
|
|
2549
|
+
]) {
|
|
2550
|
+
try {
|
|
2551
|
+
const resp = await fetchJson(`${base}${authPath}`, {
|
|
2552
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
2553
|
+
redirect: "manual"
|
|
2554
|
+
});
|
|
2555
|
+
if ([200, 302, 303, 400].includes(resp.status)) {
|
|
2556
|
+
return {
|
|
2557
|
+
authorization_endpoint: `${base}${authPath}`,
|
|
2558
|
+
token_endpoint: `${base}${tokenPath}`,
|
|
2559
|
+
issuer: base,
|
|
2560
|
+
scopes_supported: [],
|
|
2561
|
+
code_challenge_methods_supported: ["S256"]
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
async function registerOauthClient(registrationEndpoint, redirectUri, clientName = "Promptev Context Engine", opts = {}) {
|
|
2570
|
+
const payload = {
|
|
2571
|
+
client_name: clientName,
|
|
2572
|
+
redirect_uris: [redirectUri],
|
|
2573
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
2574
|
+
response_types: ["code"],
|
|
2575
|
+
token_endpoint_auth_method: "none"
|
|
2576
|
+
};
|
|
2577
|
+
let resp;
|
|
2578
|
+
try {
|
|
2579
|
+
resp = await fetchJson(registrationEndpoint, {
|
|
2580
|
+
method: "POST",
|
|
2581
|
+
timeout: REGISTER_TIMEOUT_MS,
|
|
2582
|
+
headers: { "Content-Type": "application/json" },
|
|
2583
|
+
body: JSON.stringify(payload)
|
|
2584
|
+
});
|
|
2585
|
+
} catch (e) {
|
|
2586
|
+
report(opts.hooks, e, { stage: "register_oauth_client", registration_endpoint: registrationEndpoint });
|
|
2587
|
+
throw new Error(`Cannot reach registration endpoint: ${e}`);
|
|
2588
|
+
}
|
|
2589
|
+
if (resp.status !== 200 && resp.status !== 201) {
|
|
2590
|
+
const error = new Error(`Client registration failed: ${resp.status}`);
|
|
2591
|
+
report(opts.hooks, error, {
|
|
2592
|
+
stage: "register_oauth_client",
|
|
2593
|
+
registration_endpoint: registrationEndpoint,
|
|
2594
|
+
status_code: resp.status
|
|
2595
|
+
});
|
|
2596
|
+
throw error;
|
|
2597
|
+
}
|
|
2598
|
+
return await resp.json();
|
|
2599
|
+
}
|
|
2600
|
+
async function buildAuthorizeUrl(metadata, serverUrl, toolId, redirectUri, store, opts = {}) {
|
|
2601
|
+
const authEndpoint = metadata.authorization_endpoint;
|
|
2602
|
+
const tokenEndpoint = metadata.token_endpoint;
|
|
2603
|
+
if (!authEndpoint) throw new Error("OAuth metadata missing 'authorization_endpoint'");
|
|
2604
|
+
if (!tokenEndpoint) throw new Error("OAuth metadata missing 'token_endpoint'");
|
|
2605
|
+
const codeVerifier = crypto.randomBytes(64).toString("base64url").slice(0, 128);
|
|
2606
|
+
const digest = crypto.createHash("sha256").update(codeVerifier, "ascii").digest();
|
|
2607
|
+
const codeChallenge = digest.toString("base64url");
|
|
2608
|
+
const state = crypto.randomBytes(32).toString("base64url");
|
|
2609
|
+
await Promise.resolve(
|
|
2610
|
+
store.put(
|
|
2611
|
+
state,
|
|
2612
|
+
{
|
|
2613
|
+
server_url: serverUrl,
|
|
2614
|
+
code_verifier: codeVerifier,
|
|
2615
|
+
tool_id: toolId,
|
|
2616
|
+
redirect_uri: redirectUri,
|
|
2617
|
+
token_endpoint: tokenEndpoint,
|
|
2618
|
+
client_id: opts.clientId ?? null,
|
|
2619
|
+
client_secret: opts.clientSecret ?? null
|
|
2620
|
+
},
|
|
2621
|
+
opts.ttlSeconds ?? STATE_TTL_SECONDS
|
|
2622
|
+
)
|
|
2623
|
+
);
|
|
2624
|
+
const params = new URLSearchParams({
|
|
2625
|
+
response_type: "code",
|
|
2626
|
+
state,
|
|
2627
|
+
redirect_uri: redirectUri,
|
|
2628
|
+
code_challenge: codeChallenge,
|
|
2629
|
+
code_challenge_method: "S256"
|
|
2630
|
+
});
|
|
2631
|
+
if (opts.clientId) params.set("client_id", opts.clientId);
|
|
2632
|
+
if (opts.scopes) params.set("scope", opts.scopes);
|
|
2633
|
+
return { authorize_url: `${authEndpoint}?${params.toString()}`, state };
|
|
2634
|
+
}
|
|
2635
|
+
async function exchangeCodeForToken(state, code, store, opts = {}) {
|
|
2636
|
+
const pending = await Promise.resolve(store.pop(state));
|
|
2637
|
+
if (!pending) throw new Error("Invalid or expired OAuth state \u2014 please try connecting again");
|
|
2638
|
+
const tokenEndpoint = pending.token_endpoint;
|
|
2639
|
+
if (!tokenEndpoint) throw new Error("No token_endpoint in stored OAuth state");
|
|
2640
|
+
const body = new URLSearchParams({
|
|
2641
|
+
grant_type: "authorization_code",
|
|
2642
|
+
code,
|
|
2643
|
+
redirect_uri: String(pending.redirect_uri),
|
|
2644
|
+
code_verifier: String(pending.code_verifier)
|
|
2645
|
+
});
|
|
2646
|
+
if (pending.client_id) body.set("client_id", String(pending.client_id));
|
|
2647
|
+
const secret = opts.clientSecret || pending.client_secret;
|
|
2648
|
+
if (secret) body.set("client_secret", String(secret));
|
|
2649
|
+
let resp;
|
|
2650
|
+
try {
|
|
2651
|
+
resp = await fetchJson(tokenEndpoint, {
|
|
2652
|
+
method: "POST",
|
|
2653
|
+
timeout: TOKEN_TIMEOUT_MS,
|
|
2654
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2655
|
+
body
|
|
2656
|
+
});
|
|
2657
|
+
} catch (e) {
|
|
2658
|
+
report(opts.hooks, e, { stage: "exchange_code_for_token", token_endpoint: tokenEndpoint });
|
|
2659
|
+
throw new Error(`Could not reach token endpoint: ${e}`);
|
|
2660
|
+
}
|
|
2661
|
+
if (resp.status !== 200) {
|
|
2662
|
+
const error = new Error(`Token exchange failed: ${resp.status}`);
|
|
2663
|
+
report(opts.hooks, error, {
|
|
2664
|
+
stage: "exchange_code_for_token",
|
|
2665
|
+
token_endpoint: tokenEndpoint,
|
|
2666
|
+
status_code: resp.status
|
|
2667
|
+
});
|
|
2668
|
+
throw error;
|
|
2669
|
+
}
|
|
2670
|
+
let tokenData;
|
|
2671
|
+
try {
|
|
2672
|
+
tokenData = await resp.json();
|
|
2673
|
+
} catch {
|
|
2674
|
+
const text = await resp.text();
|
|
2675
|
+
tokenData = Object.fromEntries(new URLSearchParams(text));
|
|
2676
|
+
}
|
|
2677
|
+
const accessToken = tokenData.access_token;
|
|
2678
|
+
if (!accessToken) {
|
|
2679
|
+
const error = new Error("Token response missing access_token");
|
|
2680
|
+
report(opts.hooks, error, { stage: "exchange_code_for_token", token_endpoint: tokenEndpoint });
|
|
2681
|
+
throw error;
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
access_token: accessToken,
|
|
2685
|
+
refresh_token: tokenData.refresh_token,
|
|
2686
|
+
token_type: tokenData.token_type ?? "Bearer",
|
|
2687
|
+
expires_in: tokenData.expires_in,
|
|
2688
|
+
scope: tokenData.scope,
|
|
2689
|
+
tool_id: pending.tool_id,
|
|
2690
|
+
server_url: pending.server_url
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/tools/router.ts
|
|
2695
|
+
var MCP_OAUTH_CALLBACK_PATH = "/mcp/oauth/callback";
|
|
2696
|
+
var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2697
|
+
function coerceUuid(value) {
|
|
2698
|
+
if (!UUID_RE3.test(String(value))) {
|
|
2699
|
+
throw new HandlerError(400, `invalid id: ${JSON.stringify(value)}`);
|
|
2700
|
+
}
|
|
2701
|
+
return value;
|
|
2702
|
+
}
|
|
2703
|
+
function toolRowPublic(row) {
|
|
2704
|
+
return {
|
|
2705
|
+
id: String(row.id),
|
|
2706
|
+
name: row.name,
|
|
2707
|
+
kind: row.kind,
|
|
2708
|
+
description: row.description || "",
|
|
2709
|
+
source_id: row.source_id,
|
|
2710
|
+
acl: row.acl != null ? [...row.acl] : null,
|
|
2711
|
+
requires_approval: row.requires_approval,
|
|
2712
|
+
approval_policy: row.approval_policy ?? {},
|
|
2713
|
+
enabled: row.enabled
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function resolveCaller(value, surface) {
|
|
2717
|
+
return resolveRequestPrincipals(value, { surface });
|
|
2718
|
+
}
|
|
2719
|
+
function recordToDict(record) {
|
|
2720
|
+
return {
|
|
2721
|
+
id: record.id,
|
|
2722
|
+
tool_name: record.toolName,
|
|
2723
|
+
tool_args_frozen: record.toolArgsFrozen,
|
|
2724
|
+
source_id: record.sourceId,
|
|
2725
|
+
principals: record.principals,
|
|
2726
|
+
status: record.status,
|
|
2727
|
+
approver: record.approver,
|
|
2728
|
+
approver_meta: record.approverMeta,
|
|
2729
|
+
expires_at: record.expiresAt?.toISOString() ?? null,
|
|
2730
|
+
resolved_at: record.resolvedAt?.toISOString() ?? null,
|
|
2731
|
+
created_at: record.createdAt?.toISOString() ?? null
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
function popupCloseHtml(success, message, targetOrigin, tokenData = null) {
|
|
2735
|
+
const status = success ? "success" : "error";
|
|
2736
|
+
const payload = { type: `mcp-oauth-${status}`, message };
|
|
2737
|
+
if (tokenData && success) payload.token = tokenData;
|
|
2738
|
+
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
2739
|
+
const title = success ? "Connected" : "Failed";
|
|
2740
|
+
return `<!DOCTYPE html><html><head><title>Context Engine MCP OAuth</title></head><body style="font-family:system-ui;text-align:center;padding:40px;"><h2>${title}</h2><p>${message}</p><p style="font-size:12px;">This window will close automatically…</p><script>try { var d = JSON.parse(atob("${encoded}")); if (window.opener) window.opener.postMessage(d, "${targetOrigin}");} catch (e) { console.error('postMessage error', e); }setTimeout(function () { window.close(); }, 1500);</script></body></html>`;
|
|
2741
|
+
}
|
|
2742
|
+
function createToolsHandlers(engine, opts = {}) {
|
|
2743
|
+
const store = opts.pendingStore ?? new InMemoryPendingStore();
|
|
2744
|
+
const redirectBaseUrl = (opts.redirectBaseUrl ?? "").replace(/\/$/, "");
|
|
2745
|
+
let targetOrigin = "";
|
|
2746
|
+
try {
|
|
2747
|
+
if (redirectBaseUrl) targetOrigin = new URL(redirectBaseUrl).origin;
|
|
2748
|
+
} catch {
|
|
2749
|
+
targetOrigin = "";
|
|
2750
|
+
}
|
|
2751
|
+
const redirectUri = redirectBaseUrl ? `${redirectBaseUrl}${MCP_OAUTH_CALLBACK_PATH}` : "";
|
|
2752
|
+
return {
|
|
2753
|
+
async createTool(body, principals) {
|
|
2754
|
+
const caller = resolveCaller(principals, "POST /tools");
|
|
2755
|
+
const tc = ToolConfig.fromUnknown(body);
|
|
2756
|
+
authorizeAcl(tc.acl, caller);
|
|
2757
|
+
try {
|
|
2758
|
+
const toolId = engine.registerTool ? await engine.registerTool(tc) : await registerTool(engine, tc);
|
|
2759
|
+
return { id: toolId };
|
|
2760
|
+
} catch (exc) {
|
|
2761
|
+
if (exc instanceof Error && /canonicalFromConfig|kind=/.test(exc.message)) {
|
|
2762
|
+
throw new HandlerError(400, String(exc));
|
|
2763
|
+
}
|
|
2764
|
+
throw exc;
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
async listToolsAdmin(query, principals) {
|
|
2768
|
+
const caller = resolveCaller(principals, "GET /tools");
|
|
2769
|
+
const params = [];
|
|
2770
|
+
let sql = `SELECT * FROM context_engine_tools`;
|
|
2771
|
+
if (query.source_id != null) {
|
|
2772
|
+
params.push(query.source_id);
|
|
2773
|
+
sql += ` WHERE source_id = $1`;
|
|
2774
|
+
}
|
|
2775
|
+
const result = await engine.pool.query(sql, params);
|
|
2776
|
+
const tools = result.rows.filter((row) => aclVisible(row.acl != null ? [...row.acl] : null, caller)).map(toolRowPublic);
|
|
2777
|
+
return { tools };
|
|
2778
|
+
},
|
|
2779
|
+
async getTool(toolId, principals) {
|
|
2780
|
+
const caller = resolveCaller(principals, "GET /tools/{tool_id}");
|
|
2781
|
+
const parsedId = coerceUuid(toolId);
|
|
2782
|
+
const result = await engine.pool.query(`SELECT * FROM context_engine_tools WHERE id = $1`, [parsedId]);
|
|
2783
|
+
const row = result.rows[0];
|
|
2784
|
+
if (!row || !aclVisible(row.acl != null ? [...row.acl] : null, caller)) {
|
|
2785
|
+
throw new HandlerError(404, `tool not found: ${toolId}`);
|
|
2786
|
+
}
|
|
2787
|
+
return toolRowPublic(row);
|
|
2788
|
+
},
|
|
2789
|
+
async updateToolRoute(toolId, body, principals) {
|
|
2790
|
+
const caller = resolveCaller(principals, "PATCH /tools/{tool_id}");
|
|
2791
|
+
coerceUuid(toolId);
|
|
2792
|
+
const fields = {};
|
|
2793
|
+
const map = {
|
|
2794
|
+
name: "name",
|
|
2795
|
+
kind: "kind",
|
|
2796
|
+
description: "description",
|
|
2797
|
+
config: "config",
|
|
2798
|
+
source_id: "sourceId",
|
|
2799
|
+
acl: "acl",
|
|
2800
|
+
requires_approval: "requiresApproval",
|
|
2801
|
+
approval_policy: "approvalPolicy",
|
|
2802
|
+
enabled: "enabled"
|
|
2803
|
+
};
|
|
2804
|
+
for (const [k, dest] of Object.entries(map)) {
|
|
2805
|
+
if (Object.hasOwn(body, k)) fields[dest] = body[k];
|
|
2806
|
+
}
|
|
2807
|
+
if ("acl" in fields) authorizeAcl(fields.acl, caller);
|
|
2808
|
+
try {
|
|
2809
|
+
const updated = engine.updateTool ? await engine.updateTool(toolId, { principals: caller, ...fields }) : await updateTool(engine, toolId, { principals: caller, ...fields });
|
|
2810
|
+
return {
|
|
2811
|
+
id: updated.id,
|
|
2812
|
+
name: updated.name,
|
|
2813
|
+
kind: updated.kind,
|
|
2814
|
+
description: updated.description,
|
|
2815
|
+
source_id: updated.sourceId,
|
|
2816
|
+
acl: updated.acl,
|
|
2817
|
+
requires_approval: updated.requiresApproval,
|
|
2818
|
+
approval_policy: updated.approvalPolicy,
|
|
2819
|
+
enabled: updated.enabled
|
|
2820
|
+
};
|
|
2821
|
+
} catch (exc) {
|
|
2822
|
+
if (exc instanceof EngineActionError) throw new HandlerError(404, String(exc.message));
|
|
2823
|
+
throw exc;
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2826
|
+
async deleteToolRoute(toolId, principals) {
|
|
2827
|
+
const caller = resolveCaller(principals, "DELETE /tools/{tool_id}");
|
|
2828
|
+
coerceUuid(toolId);
|
|
2829
|
+
try {
|
|
2830
|
+
if (engine.deleteTool) await engine.deleteTool(toolId, { principals: caller });
|
|
2831
|
+
else await deleteTool(engine, toolId, { principals: caller });
|
|
2832
|
+
} catch (exc) {
|
|
2833
|
+
if (exc instanceof EngineActionError) throw new HandlerError(404, String(exc.message));
|
|
2834
|
+
throw exc;
|
|
2835
|
+
}
|
|
2836
|
+
return { deleted: toolId };
|
|
2837
|
+
},
|
|
2838
|
+
async testToolRoute(body) {
|
|
2839
|
+
const tc = ToolConfig.fromUnknown(body);
|
|
2840
|
+
return engine.testTool ? engine.testTool(tc) : testTool(engine, tc);
|
|
2841
|
+
},
|
|
2842
|
+
toolSchemaRoute(kind) {
|
|
2843
|
+
try {
|
|
2844
|
+
return configSchema(kind);
|
|
2845
|
+
} catch {
|
|
2846
|
+
throw new HandlerError(404, `unknown tool kind: ${JSON.stringify(kind)}`);
|
|
2847
|
+
}
|
|
2848
|
+
},
|
|
2849
|
+
async executeToolRoute(body, principals) {
|
|
2850
|
+
const caller = resolveCaller(principals, "POST /tools/execute");
|
|
2851
|
+
if (!body.call_name) throw new HandlerError(422, "call_name is required");
|
|
2852
|
+
try {
|
|
2853
|
+
const exec = engine.executeTool ?? ((n, a, o) => executeTool(engine, n, a, o));
|
|
2854
|
+
return await exec(body.call_name, body.args ?? null, {
|
|
2855
|
+
sourceId: body.source_id,
|
|
2856
|
+
principals: caller
|
|
2857
|
+
});
|
|
2858
|
+
} catch (exc) {
|
|
2859
|
+
if (exc instanceof EngineActionError) throw new HandlerError(404, String(exc.message));
|
|
2860
|
+
throw exc;
|
|
2861
|
+
}
|
|
2862
|
+
},
|
|
2863
|
+
async mcpConnectAndList(body) {
|
|
2864
|
+
const mcp = new PromptevMCP();
|
|
2865
|
+
try {
|
|
2866
|
+
await mcp.addServer(body.server_name, body.url, { token: body.token, headers: body.headers });
|
|
2867
|
+
const client = mcp.clients.get(body.server_name);
|
|
2868
|
+
const tools = await client.listTools();
|
|
2869
|
+
const resources = body.include_resources ? await client.listResources() : null;
|
|
2870
|
+
return {
|
|
2871
|
+
server: body.server_name,
|
|
2872
|
+
url: client.url,
|
|
2873
|
+
transport: client.transport.kind,
|
|
2874
|
+
tools,
|
|
2875
|
+
resources,
|
|
2876
|
+
requires_oauth: false
|
|
2877
|
+
};
|
|
2878
|
+
} catch (exc) {
|
|
2879
|
+
const msg = String(exc);
|
|
2880
|
+
const statusMatch = /HTTP (401|403)/.exec(msg);
|
|
2881
|
+
if (statusMatch || /401|403/.test(msg)) {
|
|
2882
|
+
const metadata = await discoverOauthMetadata(body.url, { hooks: engine.hooks });
|
|
2883
|
+
if (metadata) {
|
|
2884
|
+
return {
|
|
2885
|
+
server: body.server_name,
|
|
2886
|
+
url: body.url,
|
|
2887
|
+
transport: "pending_oauth",
|
|
2888
|
+
tools: [],
|
|
2889
|
+
resources: null,
|
|
2890
|
+
requires_oauth: true
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
throw new HandlerError(
|
|
2894
|
+
502,
|
|
2895
|
+
`MCP server returned ${statusMatch?.[1] ?? "401/403"}. No OAuth endpoint found \u2014 provide a Bearer token instead.`
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
if (exc instanceof MCPError) throw new HandlerError(502, `MCP protocol error: ${exc}`);
|
|
2899
|
+
throw new HandlerError(502, `Cannot reach MCP server: ${exc}`);
|
|
2900
|
+
} finally {
|
|
2901
|
+
await mcp.shutdown();
|
|
2902
|
+
}
|
|
2903
|
+
},
|
|
2904
|
+
async mcpOauthStart(body) {
|
|
2905
|
+
if (!redirectUri) {
|
|
2906
|
+
throw new HandlerError(500, "redirectBaseUrl is required to start MCP OAuth");
|
|
2907
|
+
}
|
|
2908
|
+
const metadata = await discoverOauthMetadata(body.server_url, { hooks: engine.hooks });
|
|
2909
|
+
if (!metadata) {
|
|
2910
|
+
throw new HandlerError(502, "MCP server does not support OAuth \u2014 no authorization endpoint found");
|
|
2911
|
+
}
|
|
2912
|
+
let clientId = body.client_id ?? null;
|
|
2913
|
+
let clientSecret = body.client_secret ?? null;
|
|
2914
|
+
if (!clientId && metadata.registration_endpoint) {
|
|
2915
|
+
try {
|
|
2916
|
+
const reg = await registerOauthClient(
|
|
2917
|
+
String(metadata.registration_endpoint),
|
|
2918
|
+
redirectUri,
|
|
2919
|
+
"Promptev Context Engine",
|
|
2920
|
+
{ hooks: engine.hooks }
|
|
2921
|
+
);
|
|
2922
|
+
clientId = reg.client_id ?? null;
|
|
2923
|
+
clientSecret = reg.client_secret || clientSecret;
|
|
2924
|
+
} catch {
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
try {
|
|
2928
|
+
return await buildAuthorizeUrl(metadata, body.server_url, body.tool_id || "", redirectUri, store, {
|
|
2929
|
+
clientId,
|
|
2930
|
+
clientSecret,
|
|
2931
|
+
scopes: body.scopes
|
|
2932
|
+
});
|
|
2933
|
+
} catch (exc) {
|
|
2934
|
+
throw new HandlerError(400, String(exc));
|
|
2935
|
+
}
|
|
2936
|
+
},
|
|
2937
|
+
async mcpOauthCallback(query) {
|
|
2938
|
+
const html = (success, message, token = null) => popupCloseHtml(success, message, targetOrigin, token);
|
|
2939
|
+
if (query.error) {
|
|
2940
|
+
return { html: html(false, query.error_description || query.error), contentType: "text/html" };
|
|
2941
|
+
}
|
|
2942
|
+
if (!query.code || !query.state) {
|
|
2943
|
+
return { html: html(false, "Missing code or state parameter"), contentType: "text/html" };
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
const tokenResult = await exchangeCodeForToken(query.state, query.code, store, {
|
|
2947
|
+
hooks: engine.hooks
|
|
2948
|
+
});
|
|
2949
|
+
return {
|
|
2950
|
+
html: html(true, "Connected successfully", {
|
|
2951
|
+
access_token: tokenResult.access_token,
|
|
2952
|
+
refresh_token: tokenResult.refresh_token,
|
|
2953
|
+
token_type: tokenResult.token_type ?? "Bearer",
|
|
2954
|
+
expires_in: tokenResult.expires_in,
|
|
2955
|
+
scope: tokenResult.scope,
|
|
2956
|
+
tool_id: tokenResult.tool_id
|
|
2957
|
+
}),
|
|
2958
|
+
contentType: "text/html"
|
|
2959
|
+
};
|
|
2960
|
+
} catch (exc) {
|
|
2961
|
+
return { html: html(false, String(exc)), contentType: "text/html" };
|
|
2962
|
+
}
|
|
2963
|
+
},
|
|
2964
|
+
async listApprovalsRoute(query, principals) {
|
|
2965
|
+
const caller = resolveCaller(principals, "GET /approvals");
|
|
2966
|
+
const records = await listApprovals(engine, {
|
|
2967
|
+
status: query.status,
|
|
2968
|
+
sourceId: query.source_id,
|
|
2969
|
+
principals: caller
|
|
2970
|
+
});
|
|
2971
|
+
return { approvals: records.map(recordToDict) };
|
|
2972
|
+
},
|
|
2973
|
+
async resolveApprovalRoute(approvalId, body, principals) {
|
|
2974
|
+
const caller = resolveCaller(principals, "POST /approvals/{approval_id}/resolve");
|
|
2975
|
+
const parsedId = coerceUuid(approvalId);
|
|
2976
|
+
try {
|
|
2977
|
+
const record = await resolveApproval(
|
|
2978
|
+
engine,
|
|
2979
|
+
parsedId,
|
|
2980
|
+
body.decision,
|
|
2981
|
+
body.approver,
|
|
2982
|
+
body.meta ?? null,
|
|
2983
|
+
{
|
|
2984
|
+
principals: caller
|
|
2985
|
+
}
|
|
2986
|
+
);
|
|
2987
|
+
return recordToDict(record);
|
|
2988
|
+
} catch (exc) {
|
|
2989
|
+
if (exc instanceof ApprovalNotPending) {
|
|
2990
|
+
throw new HandlerError(404, `approval not pending: ${exc.message}`);
|
|
2991
|
+
}
|
|
2992
|
+
if (exc instanceof ApprovalExpired) {
|
|
2993
|
+
throw new HandlerError(409, "approval expired");
|
|
2994
|
+
}
|
|
2995
|
+
throw exc;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/hono.ts
|
|
3002
|
+
var require3 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('hono.cjs', document.baseURI).href)));
|
|
3003
|
+
function requireHono() {
|
|
3004
|
+
try {
|
|
3005
|
+
return require3("hono");
|
|
3006
|
+
} catch {
|
|
3007
|
+
throw new ExtraMissingError("hono", "hono", "createHonoRouter");
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
function jsonError(c, exc) {
|
|
3011
|
+
return c.json({ detail: exc.detail }, exc.status);
|
|
3012
|
+
}
|
|
3013
|
+
async function readForm(c) {
|
|
3014
|
+
const body = await c.req.parseBody({ all: true });
|
|
3015
|
+
const file = body.file;
|
|
3016
|
+
const fields = {};
|
|
3017
|
+
for (const [k, v] of Object.entries(body)) {
|
|
3018
|
+
if (k !== "file") fields[k] = v;
|
|
3019
|
+
}
|
|
3020
|
+
return { file, fields };
|
|
3021
|
+
}
|
|
3022
|
+
function createHonoRouter(engine, opts) {
|
|
3023
|
+
if (!opts?.auth || !opts?.principals) {
|
|
3024
|
+
throw new TypeError("createHonoRouter requires auth and principals");
|
|
3025
|
+
}
|
|
3026
|
+
const { Hono } = requireHono();
|
|
3027
|
+
const app = new Hono();
|
|
3028
|
+
const tools = createToolsHandlers(engine, { redirectBaseUrl: opts.redirectBaseUrl });
|
|
3029
|
+
app.use("*", async (c, next) => {
|
|
3030
|
+
if (c.req.path === MCP_OAUTH_CALLBACK_PATH) return next();
|
|
3031
|
+
await opts.auth(c);
|
|
3032
|
+
await next();
|
|
3033
|
+
});
|
|
3034
|
+
const wrap = (fn) => async (c) => {
|
|
3035
|
+
try {
|
|
3036
|
+
const result = await fn(c);
|
|
3037
|
+
if (result && typeof result === "object" && "html" in result) {
|
|
3038
|
+
const html = result;
|
|
3039
|
+
return c.html(html.html);
|
|
3040
|
+
}
|
|
3041
|
+
return c.json(result);
|
|
3042
|
+
} catch (exc) {
|
|
3043
|
+
if (exc instanceof HandlerError) return jsonError(c, exc);
|
|
3044
|
+
throw exc;
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
app.post(
|
|
3048
|
+
"/documents",
|
|
3049
|
+
wrap(async (c) => {
|
|
3050
|
+
const ct = c.req.header("content-type") ?? "";
|
|
3051
|
+
const principals = await opts.principals(c);
|
|
3052
|
+
if (ct.startsWith("multipart/form-data")) {
|
|
3053
|
+
const { file, fields } = await readForm(c);
|
|
3054
|
+
if (!file) throw new HandlerError(400, "multipart upload requires a 'file' field");
|
|
3055
|
+
const content = file.data ?? Buffer.from([]);
|
|
3056
|
+
return handleIngestFile(engine, {
|
|
3057
|
+
content,
|
|
3058
|
+
filename: file.filename ?? null,
|
|
3059
|
+
form: fields,
|
|
3060
|
+
principals
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
if (ct.startsWith("application/json")) {
|
|
3064
|
+
const payload = await c.req.json();
|
|
3065
|
+
return handleIngestJson(engine, payload, principals);
|
|
3066
|
+
}
|
|
3067
|
+
throw new HandlerError(415, "Content-Type must be multipart/form-data or application/json");
|
|
3068
|
+
})
|
|
3069
|
+
);
|
|
3070
|
+
app.get(
|
|
3071
|
+
"/documents",
|
|
3072
|
+
wrap(
|
|
3073
|
+
async (c) => handleListDocuments(engine, {
|
|
3074
|
+
sourceId: c.req.query("source_id") ?? null,
|
|
3075
|
+
limit: Number(c.req.query("limit") ?? 50),
|
|
3076
|
+
cursor: c.req.query("cursor") ?? null,
|
|
3077
|
+
principals: await opts.principals(c)
|
|
3078
|
+
})
|
|
3079
|
+
)
|
|
3080
|
+
);
|
|
3081
|
+
app.get(
|
|
3082
|
+
"/documents/:document_id",
|
|
3083
|
+
wrap(async (c) => handleGetDocument(engine, c.req.param("document_id"), await opts.principals(c)))
|
|
3084
|
+
);
|
|
3085
|
+
app.delete(
|
|
3086
|
+
"/documents/:document_id",
|
|
3087
|
+
wrap(async (c) => handleDeleteDocument(engine, c.req.param("document_id"), await opts.principals(c)))
|
|
3088
|
+
);
|
|
3089
|
+
app.patch(
|
|
3090
|
+
"/documents/:document_id",
|
|
3091
|
+
wrap(
|
|
3092
|
+
async (c) => handleUpdateDocument(engine, c.req.param("document_id"), await c.req.json(), await opts.principals(c))
|
|
3093
|
+
)
|
|
3094
|
+
);
|
|
3095
|
+
app.post(
|
|
3096
|
+
"/search",
|
|
3097
|
+
wrap(async (c) => handleSearch(engine, await c.req.json(), await opts.principals(c)))
|
|
3098
|
+
);
|
|
3099
|
+
app.get(
|
|
3100
|
+
"/stats",
|
|
3101
|
+
wrap(async (c) => handleStats(engine, c.req.query("source_id") ?? null))
|
|
3102
|
+
);
|
|
3103
|
+
app.post(
|
|
3104
|
+
"/tools",
|
|
3105
|
+
wrap(async (c) => tools.createTool(await c.req.json(), await opts.principals(c)))
|
|
3106
|
+
);
|
|
3107
|
+
app.get(
|
|
3108
|
+
"/tools",
|
|
3109
|
+
wrap(
|
|
3110
|
+
async (c) => tools.listToolsAdmin({ source_id: c.req.query("source_id") }, await opts.principals(c))
|
|
3111
|
+
)
|
|
3112
|
+
);
|
|
3113
|
+
app.get(
|
|
3114
|
+
"/tools/schema/:kind",
|
|
3115
|
+
wrap(async (c) => tools.toolSchemaRoute(c.req.param("kind")))
|
|
3116
|
+
);
|
|
3117
|
+
app.post(
|
|
3118
|
+
"/tools/test",
|
|
3119
|
+
wrap(async (c) => tools.testToolRoute(await c.req.json()))
|
|
3120
|
+
);
|
|
3121
|
+
app.post(
|
|
3122
|
+
"/tools/execute",
|
|
3123
|
+
wrap(
|
|
3124
|
+
async (c) => tools.executeToolRoute(await c.req.json(), await opts.principals(c))
|
|
3125
|
+
)
|
|
3126
|
+
);
|
|
3127
|
+
app.get(
|
|
3128
|
+
"/tools/:tool_id",
|
|
3129
|
+
wrap(async (c) => tools.getTool(c.req.param("tool_id"), await opts.principals(c)))
|
|
3130
|
+
);
|
|
3131
|
+
app.patch(
|
|
3132
|
+
"/tools/:tool_id",
|
|
3133
|
+
wrap(
|
|
3134
|
+
async (c) => tools.updateToolRoute(
|
|
3135
|
+
c.req.param("tool_id"),
|
|
3136
|
+
await c.req.json(),
|
|
3137
|
+
await opts.principals(c)
|
|
3138
|
+
)
|
|
3139
|
+
)
|
|
3140
|
+
);
|
|
3141
|
+
app.delete(
|
|
3142
|
+
"/tools/:tool_id",
|
|
3143
|
+
wrap(async (c) => tools.deleteToolRoute(c.req.param("tool_id"), await opts.principals(c)))
|
|
3144
|
+
);
|
|
3145
|
+
app.post(
|
|
3146
|
+
"/mcp/connect-and-list",
|
|
3147
|
+
wrap(async (c) => tools.mcpConnectAndList(await c.req.json()))
|
|
3148
|
+
);
|
|
3149
|
+
app.post(
|
|
3150
|
+
"/mcp/oauth/start",
|
|
3151
|
+
wrap(async (c) => tools.mcpOauthStart(await c.req.json()))
|
|
3152
|
+
);
|
|
3153
|
+
app.get(
|
|
3154
|
+
MCP_OAUTH_CALLBACK_PATH,
|
|
3155
|
+
wrap(async (c) => tools.mcpOauthCallback(c.req.query()))
|
|
3156
|
+
);
|
|
3157
|
+
app.get(
|
|
3158
|
+
"/approvals",
|
|
3159
|
+
wrap(
|
|
3160
|
+
async (c) => tools.listApprovalsRoute(
|
|
3161
|
+
{ status: c.req.query("status"), source_id: c.req.query("source_id") },
|
|
3162
|
+
await opts.principals(c)
|
|
3163
|
+
)
|
|
3164
|
+
)
|
|
3165
|
+
);
|
|
3166
|
+
app.post(
|
|
3167
|
+
"/approvals/:approval_id/resolve",
|
|
3168
|
+
wrap(
|
|
3169
|
+
async (c) => tools.resolveApprovalRoute(
|
|
3170
|
+
c.req.param("approval_id"),
|
|
3171
|
+
await c.req.json(),
|
|
3172
|
+
await opts.principals(c)
|
|
3173
|
+
)
|
|
3174
|
+
)
|
|
3175
|
+
);
|
|
3176
|
+
return app;
|
|
3177
|
+
}
|
|
3178
|
+
var createHonoApp = createHonoRouter;
|
|
3179
|
+
|
|
3180
|
+
exports.createHonoApp = createHonoApp;
|
|
3181
|
+
exports.createHonoRouter = createHonoRouter;
|
|
3182
|
+
//# sourceMappingURL=hono.cjs.map
|
|
3183
|
+
//# sourceMappingURL=hono.cjs.map
|