@promptev/context-engine 0.0.0 → 0.0.1
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/README.md +93 -5
- package/dist/cli.js +1761 -501
- package/dist/cli.js.map +1 -1
- package/dist/{config-Bt9bUQqU.d.ts → config-CNnASw5X.d.cts} +42 -5
- package/dist/{config-Bl9U789m.d.cts → config-CdlSkKgV.d.ts} +42 -5
- package/dist/express.cjs +925 -151
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +11 -4
- package/dist/express.d.ts +11 -4
- package/dist/express.js +926 -152
- package/dist/express.js.map +1 -1
- package/dist/fastify.cjs +923 -151
- package/dist/fastify.cjs.map +1 -1
- package/dist/fastify.d.cts +8 -4
- package/dist/fastify.d.ts +8 -4
- package/dist/fastify.js +924 -152
- package/dist/fastify.js.map +1 -1
- package/dist/{governance-BDkcv4qZ.d.cts → governance-D8g6Wyvb.d.cts} +8 -2
- package/dist/{governance-XIScatRO.d.ts → governance-XFVgtEdV.d.ts} +8 -2
- package/dist/graph/index.cjs +122 -43
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +5 -3
- package/dist/graph/index.d.ts +5 -3
- package/dist/graph/index.js +122 -43
- package/dist/graph/index.js.map +1 -1
- package/dist/hono.cjs +923 -151
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +8 -4
- package/dist/hono.d.ts +8 -4
- package/dist/hono.js +924 -152
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +2186 -910
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -107
- package/dist/index.d.ts +70 -107
- package/dist/index.js +2186 -908
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +100 -14
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +5 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.js +100 -14
- package/dist/mcp.js.map +1 -1
- package/dist/migrations/sql/0003_tools.sql +2 -0
- package/dist/migrations/sql/0004_acl_indexes.sql +23 -2
- package/dist/{redaction-BmDSWJ7h.d.cts → redaction-BqD_DEUQ.d.cts} +22 -1
- package/dist/{redaction-BmDSWJ7h.d.ts → redaction-BqD_DEUQ.d.ts} +22 -1
- package/dist/redaction-presidio.d.cts +1 -1
- package/dist/redaction-presidio.d.ts +1 -1
- package/dist/{router-CrxZ2y_Z.d.ts → router-B_DTkQgU.d.ts} +17 -2
- package/dist/{router-OPgSoYAB.d.cts → router-Dsv3fv0R.d.cts} +17 -2
- package/dist/storage-DU1JRno5.d.cts +164 -0
- package/dist/storage-Dvt2ZxsV.d.ts +164 -0
- package/package.json +61 -23
- package/src/migrations/sql/0003_tools.sql +2 -0
- package/src/migrations/sql/0004_acl_indexes.sql +23 -2
- package/dist/embeddings-B-jZ42mk.d.cts +0 -67
- package/dist/embeddings-DaSdAZN3.d.ts +0 -67
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID, createHash, randomBytes, createCipheriv, createDecipheriv, createHmac } from 'crypto';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { readFileSync, existsSync, mkdirSync, writeFileSync, openSync, readSync, closeSync } from 'fs';
|
|
4
5
|
import { join, dirname, basename } from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -13,11 +14,12 @@ import mammoth from 'mammoth';
|
|
|
13
14
|
import PostalMime from 'postal-mime';
|
|
14
15
|
import { getDocumentProxy, extractText, renderPageAsImage, extractImages } from 'unpdf';
|
|
15
16
|
import { getEncoding } from 'js-tiktoken';
|
|
17
|
+
import { promises } from 'dns';
|
|
18
|
+
import { isIP } from 'net';
|
|
16
19
|
import { createRequire } from 'module';
|
|
17
20
|
import { createServer } from 'http';
|
|
18
21
|
import { homedir } from 'os';
|
|
19
22
|
import { Command } from 'commander';
|
|
20
|
-
import { z } from 'zod';
|
|
21
23
|
|
|
22
24
|
var __defProp = Object.defineProperty;
|
|
23
25
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -82,6 +84,14 @@ function emitError(hooks, exc, ctx) {
|
|
|
82
84
|
log.warn("onError callback raised; swallowing");
|
|
83
85
|
}
|
|
84
86
|
}
|
|
87
|
+
function emitProgress(hooks, event) {
|
|
88
|
+
if (!hooks?.onProgress) return;
|
|
89
|
+
try {
|
|
90
|
+
hooks.onProgress(event);
|
|
91
|
+
} catch {
|
|
92
|
+
log.warn("onProgress callback raised; swallowing");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
85
95
|
function emitToolCall(hooks, event) {
|
|
86
96
|
if (!hooks?.onToolCall) return;
|
|
87
97
|
try {
|
|
@@ -360,6 +370,184 @@ var init_redaction = __esm({
|
|
|
360
370
|
HASH_TOKEN_CHARS = 16;
|
|
361
371
|
}
|
|
362
372
|
});
|
|
373
|
+
function camelize(key) {
|
|
374
|
+
return key.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
375
|
+
}
|
|
376
|
+
function loadCeEnv() {
|
|
377
|
+
const root = {};
|
|
378
|
+
for (const [raw, value] of Object.entries(process.env)) {
|
|
379
|
+
if (!raw.startsWith("CE_") || value === void 0) continue;
|
|
380
|
+
const path = raw.slice(3).split("__").map(camelize);
|
|
381
|
+
let cur = root;
|
|
382
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
383
|
+
const k = path[i];
|
|
384
|
+
const next = cur[k];
|
|
385
|
+
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
386
|
+
cur = cur[k];
|
|
387
|
+
}
|
|
388
|
+
cur[path[path.length - 1]] = coerceEnv(value);
|
|
389
|
+
}
|
|
390
|
+
return root;
|
|
391
|
+
}
|
|
392
|
+
function coerceEnv(value) {
|
|
393
|
+
if (value === "true") return true;
|
|
394
|
+
if (value === "false") return false;
|
|
395
|
+
if (/^-?\d+$/.test(value)) return Number(value);
|
|
396
|
+
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
function deepMerge(a, b) {
|
|
400
|
+
const out = { ...a };
|
|
401
|
+
for (const [k, v] of Object.entries(b)) {
|
|
402
|
+
if (v === void 0) continue;
|
|
403
|
+
const existing = out[k];
|
|
404
|
+
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
405
|
+
out[k] = deepMerge(existing, v);
|
|
406
|
+
} else {
|
|
407
|
+
out[k] = v;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
var embeddingSchema, llmSchema, graphSchema, rerankerSchema, fusionSchema, storageSchema, extractionSchema, ContextEngineConfig;
|
|
413
|
+
var init_config = __esm({
|
|
414
|
+
"src/config.ts"() {
|
|
415
|
+
init_redaction();
|
|
416
|
+
embeddingSchema = z.object({
|
|
417
|
+
provider: z.enum(["openai", "azure_openai", "gemini", "voyage", "cohere", "custom"]),
|
|
418
|
+
model: z.string(),
|
|
419
|
+
dim: z.number().int().positive().nullable().optional().default(null),
|
|
420
|
+
apiKey: z.string().nullable().optional().default(null),
|
|
421
|
+
baseUrl: z.string().nullable().optional().default(null)
|
|
422
|
+
});
|
|
423
|
+
llmSchema = z.object({
|
|
424
|
+
provider: z.enum(["anthropic", "openai", "azure_openai", "gemini", "bedrock", "custom"]),
|
|
425
|
+
model: z.string(),
|
|
426
|
+
apiKey: z.string().nullable().optional().default(null),
|
|
427
|
+
baseUrl: z.string().nullable().optional().default(null)
|
|
428
|
+
});
|
|
429
|
+
graphSchema = z.object({
|
|
430
|
+
enabled: z.boolean().default(false),
|
|
431
|
+
neo4jUri: z.string().nullable().optional().default(null),
|
|
432
|
+
neo4jUser: z.string().default("neo4j"),
|
|
433
|
+
neo4jPassword: z.string().nullable().optional().default(null),
|
|
434
|
+
neo4jDatabase: z.string().default("neo4j"),
|
|
435
|
+
extractionLlm: llmSchema.nullable().optional().default(null)
|
|
436
|
+
});
|
|
437
|
+
rerankerSchema = z.object({
|
|
438
|
+
enabled: z.boolean().default(false),
|
|
439
|
+
provider: z.enum(["cohere", "voyage", "jina", "custom"]).nullable().optional().default(null),
|
|
440
|
+
model: z.string().nullable().optional().default(null),
|
|
441
|
+
apiKey: z.string().nullable().optional().default(null),
|
|
442
|
+
baseUrl: z.string().nullable().optional().default(null),
|
|
443
|
+
candidates: z.number().int().positive().default(50)
|
|
444
|
+
});
|
|
445
|
+
fusionSchema = z.object({
|
|
446
|
+
method: z.literal("rrf").default("rrf"),
|
|
447
|
+
k: z.number().int().positive().default(60),
|
|
448
|
+
weights: z.record(z.string(), z.number()).default({ fts: 1, trgm: 0.8, ann: 1, graph: 1 })
|
|
449
|
+
});
|
|
450
|
+
storageSchema = z.object({
|
|
451
|
+
backend: z.literal("postgres").default("postgres"),
|
|
452
|
+
annExactThreshold: z.number().int().positive().default(5e4),
|
|
453
|
+
// Connection pool, passed straight through to `pg.Pool`. Sizing is a
|
|
454
|
+
// deployment decision (how many workers times how many concurrent searches
|
|
455
|
+
// each runs, against what the database allows), so these are pg's own
|
|
456
|
+
// defaults rather than a guess at your topology. (`pg` has no pre-ping.)
|
|
457
|
+
//
|
|
458
|
+
// Connections kept open in the pool.
|
|
459
|
+
poolMax: z.number().int().positive().default(10),
|
|
460
|
+
// Milliseconds an idle connection is kept before it is closed.
|
|
461
|
+
poolIdleTimeoutMs: z.number().int().nonnegative().default(1e4),
|
|
462
|
+
// Milliseconds a caller waits for a connection before the attempt fails.
|
|
463
|
+
poolConnectionTimeoutMs: z.number().int().nonnegative().default(3e4)
|
|
464
|
+
});
|
|
465
|
+
extractionSchema = z.object({
|
|
466
|
+
// Ceiling on an image's LONG EDGE in pixels, for rendered PDF pages and
|
|
467
|
+
// for images sent to the vision LLM.
|
|
468
|
+
maxRenderPx: z.number().int().positive().default(2e3),
|
|
469
|
+
// Ceiling on ONE vision batch's transcription, in output tokens — a reply
|
|
470
|
+
// far past a batch's honest bound is a repetition loop. Raising it is the
|
|
471
|
+
// fix if very dense pages come back truncated.
|
|
472
|
+
visionMaxOutputTokens: z.number().int().positive().default(16e3),
|
|
473
|
+
// Pages per vision-LLM call. Smaller batches mean more calls but more
|
|
474
|
+
// concurrency and less risk of hitting the output ceiling or a provider
|
|
475
|
+
// deadline; larger batches the reverse. Measure on your own corpus.
|
|
476
|
+
pagesPerVisionBatch: z.number().int().positive().default(5),
|
|
477
|
+
// Language hint for the Tesseract OCR fallback ('en', 'ur', 'mixed', any
|
|
478
|
+
// Tesseract code, or 'auto' for script detection). null (the default)
|
|
479
|
+
// detects the document's language from its usable text-layer pages and
|
|
480
|
+
// falls back to 'auto' when there is nothing to sample.
|
|
481
|
+
ocrLanguage: z.string().nullable().default(null)
|
|
482
|
+
});
|
|
483
|
+
ContextEngineConfig = class _ContextEngineConfig {
|
|
484
|
+
databaseUrl;
|
|
485
|
+
storage;
|
|
486
|
+
defaultMode;
|
|
487
|
+
embedding;
|
|
488
|
+
llm;
|
|
489
|
+
visionLlm;
|
|
490
|
+
graph;
|
|
491
|
+
reranker;
|
|
492
|
+
fusion;
|
|
493
|
+
extraction;
|
|
494
|
+
enableCodeExecution;
|
|
495
|
+
secretKey;
|
|
496
|
+
/**
|
|
497
|
+
* Lets http tools and probes reach loopback/private/link-local/metadata
|
|
498
|
+
* addresses. Off by default — an http tool is registered by a caller, and a
|
|
499
|
+
* private destination is server-side request forgery. See `tools/egress.ts`.
|
|
500
|
+
*/
|
|
501
|
+
allowPrivateEgress;
|
|
502
|
+
redaction;
|
|
503
|
+
constructor(init) {
|
|
504
|
+
this.databaseUrl = init.databaseUrl;
|
|
505
|
+
this.storage = storageSchema.parse(init.storage ?? {});
|
|
506
|
+
this.defaultMode = init.defaultMode ?? "hybrid";
|
|
507
|
+
this.embedding = embeddingSchema.parse(init.embedding);
|
|
508
|
+
this.llm = init.llm ? llmSchema.parse(init.llm) : null;
|
|
509
|
+
this.visionLlm = init.visionLlm ? llmSchema.parse(init.visionLlm) : null;
|
|
510
|
+
this.graph = graphSchema.parse(init.graph ?? {});
|
|
511
|
+
this.reranker = rerankerSchema.parse(init.reranker ?? {});
|
|
512
|
+
this.fusion = fusionSchema.parse(init.fusion ?? {});
|
|
513
|
+
this.extraction = extractionSchema.parse(init.extraction ?? {});
|
|
514
|
+
this.enableCodeExecution = init.enableCodeExecution ?? false;
|
|
515
|
+
this.secretKey = init.secretKey ?? null;
|
|
516
|
+
this.allowPrivateEgress = init.allowPrivateEgress ?? false;
|
|
517
|
+
this.redaction = init.redaction instanceof RedactionPolicy ? init.redaction : new RedactionPolicy(init.redaction ?? {});
|
|
518
|
+
this.validate();
|
|
519
|
+
}
|
|
520
|
+
validate() {
|
|
521
|
+
if (this.graph.enabled && !(this.graph.neo4jUri && this.graph.neo4jPassword && this.graph.extractionLlm)) {
|
|
522
|
+
throw new Error("graph enabled but neo4jUri/neo4jPassword/extractionLlm missing");
|
|
523
|
+
}
|
|
524
|
+
if (this.reranker.enabled && !(this.reranker.provider && this.reranker.apiKey)) {
|
|
525
|
+
throw new Error("reranker enabled but provider/apiKey missing");
|
|
526
|
+
}
|
|
527
|
+
if (this.defaultMode === "graph" && !this.graph.enabled) {
|
|
528
|
+
throw new Error("defaultMode is 'graph' but graph enabled is false");
|
|
529
|
+
}
|
|
530
|
+
for (const rule of this.redaction.rules) {
|
|
531
|
+
if (rule.action === "hash" && !this.secretKey) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`redaction rule '${rule.name}': action='hash' requires ContextEngineConfig.secretKey to be set`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
static fromEnv(overrides = {}) {
|
|
539
|
+
const env = loadCeEnv();
|
|
540
|
+
const merged = deepMerge(env, overrides);
|
|
541
|
+
if (!merged.databaseUrl || !merged.embedding) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
"ContextEngineConfig.fromEnv requires CE_DATABASE_URL and CE_EMBEDDING__PROVIDER/MODEL (or explicit overrides)"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return new _ContextEngineConfig(merged);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
});
|
|
363
551
|
|
|
364
552
|
// src/errors.ts
|
|
365
553
|
var EngineActionError, DocumentNotFoundError, GraphLegUnavailable, ExtraMissingError;
|
|
@@ -462,6 +650,7 @@ async function runMigrate(databaseUrl, opts = {}) {
|
|
|
462
650
|
recorded = rev;
|
|
463
651
|
done.add(rev);
|
|
464
652
|
}
|
|
653
|
+
await client.query(loadSql("0004_acl_indexes", dim));
|
|
465
654
|
const meta = await client.query("SELECT embedding_dim FROM context_engine_meta WHERE id = 1");
|
|
466
655
|
if (!meta.rows.length) {
|
|
467
656
|
await client.query("INSERT INTO context_engine_meta (id, embedding_dim) VALUES (1, $1)", [dim]);
|
|
@@ -517,16 +706,116 @@ var init_extras = __esm({
|
|
|
517
706
|
}
|
|
518
707
|
});
|
|
519
708
|
|
|
520
|
-
// src/
|
|
709
|
+
// src/sentinels.ts
|
|
710
|
+
function resolvePrincipals(value, method) {
|
|
711
|
+
if (value === TRUSTED) return null;
|
|
712
|
+
if (value === void 0 || value === null) {
|
|
713
|
+
if (!warnedMethods.has(method)) {
|
|
714
|
+
warnedMethods.add(method);
|
|
715
|
+
process.emitWarning(
|
|
716
|
+
`${method}(principals=${value === null ? "null" : "undefined"}) means TRUSTED CALLER \u2014 access control is disabled and every document is returned. If that is what you want, pass principals=TRUSTED (from @promptev/context-engine) to say so explicitly. If you meant 'no authenticated user', pass principals=[] instead. Passing null/omitting will raise in 1.0.`,
|
|
717
|
+
{ type: "DeprecationWarning", code: "CE_PRINCIPALS_NULL" }
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
if (!Array.isArray(value)) {
|
|
723
|
+
throw new TypeError(
|
|
724
|
+
`${method}(principals=...) must be an array of principal strings, [] for an anonymous caller, or TRUSTED \u2014 got ${typeof value}. A non-array value must never silently disable ACL filtering.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return value;
|
|
728
|
+
}
|
|
729
|
+
var UNSET, TRUSTED, warnedMethods;
|
|
730
|
+
var init_sentinels = __esm({
|
|
731
|
+
"src/sentinels.ts"() {
|
|
732
|
+
UNSET = /* @__PURE__ */ Symbol.for("context_engine.UNSET");
|
|
733
|
+
TRUSTED = /* @__PURE__ */ Symbol.for("context_engine.TRUSTED");
|
|
734
|
+
warnedMethods = /* @__PURE__ */ new Set();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
function requireValidUuid(documentId) {
|
|
738
|
+
if (!UUID_RE2.test(String(documentId))) {
|
|
739
|
+
throw new HandlerError(400, `invalid document id: ${JSON.stringify(documentId)}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function resolveRequestPrincipals(value, opts) {
|
|
743
|
+
if (value === TRUSTED) return TRUSTED;
|
|
744
|
+
if (value == null) {
|
|
745
|
+
throw new HandlerError(
|
|
746
|
+
500,
|
|
747
|
+
`${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.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (!Array.isArray(value) || value.some((p) => typeof p !== "string")) {
|
|
751
|
+
throw new HandlerError(
|
|
752
|
+
500,
|
|
753
|
+
`${opts.surface}: the \`principals\` dependency must return a list of principal strings, [] for an anonymous caller, or TRUSTED \u2014 got ${typeof value}.`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return value;
|
|
757
|
+
}
|
|
758
|
+
var HandlerError, UUID_RE2;
|
|
759
|
+
var init_routing_core = __esm({
|
|
760
|
+
"src/routing-core.ts"() {
|
|
761
|
+
init_sentinels();
|
|
762
|
+
HandlerError = class extends Error {
|
|
763
|
+
status;
|
|
764
|
+
detail;
|
|
765
|
+
constructor(status, detail) {
|
|
766
|
+
super(typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
767
|
+
this.name = "HandlerError";
|
|
768
|
+
this.status = status;
|
|
769
|
+
this.detail = detail;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
z.object({
|
|
773
|
+
text: z.string(),
|
|
774
|
+
name: z.string(),
|
|
775
|
+
source_id: z.string().nullable().optional(),
|
|
776
|
+
external_id: z.string().nullable().optional(),
|
|
777
|
+
description: z.string().nullable().optional(),
|
|
778
|
+
meta_data: z.record(z.unknown()).nullable().optional(),
|
|
779
|
+
acl: z.array(z.string()).nullable().optional(),
|
|
780
|
+
mode: z.enum(["hybrid", "graph"]).nullable().optional(),
|
|
781
|
+
extract_structured: z.boolean().optional().default(false),
|
|
782
|
+
batch: z.boolean().optional().default(false)
|
|
783
|
+
}).strip();
|
|
784
|
+
z.object({
|
|
785
|
+
acl: z.array(z.string()).nullable().optional(),
|
|
786
|
+
name: z.string().nullable().optional(),
|
|
787
|
+
description: z.string().nullable().optional(),
|
|
788
|
+
meta_data: z.record(z.unknown()).nullable().optional()
|
|
789
|
+
}).strict();
|
|
790
|
+
z.object({
|
|
791
|
+
query: z.string(),
|
|
792
|
+
source_ids: z.array(z.string()).nullable().optional(),
|
|
793
|
+
// Narrows WITHIN a source and INTERSECTS with source_ids — it can only
|
|
794
|
+
// shrink the result set (the ACL predicate still applies in the same SQL
|
|
795
|
+
// conjunction), so exposing it needs no authorizeAcl-style grant check.
|
|
796
|
+
document_ids: z.array(z.string()).nullable().optional(),
|
|
797
|
+
top_k: z.number().int().optional().default(10),
|
|
798
|
+
mode: z.enum(["hybrid", "graph"]).optional().default("hybrid"),
|
|
799
|
+
compress_to_tokens: z.number().int().nullable().optional()
|
|
800
|
+
}).strip();
|
|
801
|
+
UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
802
|
+
}
|
|
803
|
+
});
|
|
521
804
|
async function resolvePrincipalsFn(principals) {
|
|
522
805
|
const result = await Promise.resolve(principals());
|
|
523
|
-
|
|
806
|
+
try {
|
|
807
|
+
return resolveRequestPrincipals(result, { surface: "the MCP `principals` dependency" });
|
|
808
|
+
} catch (exc) {
|
|
809
|
+
if (exc instanceof HandlerError) throw new Error(exc.message, { cause: exc });
|
|
810
|
+
throw exc;
|
|
811
|
+
}
|
|
524
812
|
}
|
|
525
|
-
function registerToolGateway(mcp, engine, principals) {
|
|
813
|
+
function registerToolGateway(mcp, engine, principals, approvalScope = null) {
|
|
526
814
|
mcp.tool(
|
|
527
815
|
"search_tools",
|
|
528
816
|
"Keyword search over the tools this deployment has registered (http/db/mcp/function) \u2014 the ACL-visible name, kind, description, and JSON Schema params of each match, so a caller can discover what it can then invoke with `execute_tool`." + SCOPE_NOTE,
|
|
529
|
-
|
|
817
|
+
// Zod raw shape — see the note on the search tool in mcp.ts.
|
|
818
|
+
{ query: z.string(), limit: z.number().int().optional() },
|
|
530
819
|
async (...raw) => {
|
|
531
820
|
const args = raw[0] ?? {};
|
|
532
821
|
const callerPrincipals = await resolvePrincipalsFn(principals);
|
|
@@ -540,13 +829,15 @@ function registerToolGateway(mcp, engine, principals) {
|
|
|
540
829
|
mcp.tool(
|
|
541
830
|
"execute_tool",
|
|
542
831
|
"Governed execution of a tool previously discovered via `search_tools` (ACL check, approval gate, audit). Returns `{result, usage}` on success, or an `approval_required` payload when the call needs a human approval that isn't already granted \u2014 the caller must not retry until that approval resolves." + SCOPE_NOTE,
|
|
543
|
-
{ name:
|
|
832
|
+
{ name: z.string(), args: z.record(z.unknown()).optional() },
|
|
544
833
|
async (...raw) => {
|
|
545
834
|
const body = raw[0] ?? {};
|
|
546
835
|
const callerPrincipals = await resolvePrincipalsFn(principals);
|
|
836
|
+
const scope = approvalScope ? await Promise.resolve(approvalScope()) : null;
|
|
547
837
|
return engine.executeTool(String(body.name), body.args ?? null, {
|
|
548
838
|
principals: callerPrincipals,
|
|
549
|
-
source: "mcp"
|
|
839
|
+
source: "mcp",
|
|
840
|
+
approvalScope: scope
|
|
550
841
|
});
|
|
551
842
|
}
|
|
552
843
|
);
|
|
@@ -554,6 +845,7 @@ function registerToolGateway(mcp, engine, principals) {
|
|
|
554
845
|
var SCOPE_NOTE;
|
|
555
846
|
var init_mcp_tools = __esm({
|
|
556
847
|
"src/tools/mcp-tools.ts"() {
|
|
848
|
+
init_routing_core();
|
|
557
849
|
SCOPE_NOTE = " Results are scoped to the caller's permissions (resolved by the server's injected principals dependency \u2014 never a caller-supplied argument) and to the given source_ids, if any.";
|
|
558
850
|
}
|
|
559
851
|
});
|
|
@@ -606,15 +898,24 @@ async function createMcpApp(engine, opts) {
|
|
|
606
898
|
"search_knowledge_base",
|
|
607
899
|
`Hybrid search (full-text + trigram + vector, RRF-fused) over the ingested corpus. Returns the top-matching chunks with their source document.${SCOPE_NOTE}`,
|
|
608
900
|
{
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
901
|
+
// Zod RAW SHAPES, not JSON schema: the SDK's isZodRawShape test
|
|
902
|
+
// rejects a plain schema object — on current SDK versions that made
|
|
903
|
+
// registration THROW at startup, and on 1.12.0 the object was consumed
|
|
904
|
+
// as annotations and every handler ran with NO arguments.
|
|
905
|
+
query: z.string(),
|
|
906
|
+
source_ids: z.array(z.string()).optional(),
|
|
907
|
+
document_ids: z.array(z.string()).optional(),
|
|
908
|
+
top_k: z.number().int().optional(),
|
|
909
|
+
mode: z.enum(["hybrid", "graph"]).optional()
|
|
613
910
|
},
|
|
614
911
|
async (args) => {
|
|
912
|
+
for (const did of args.document_ids ?? []) {
|
|
913
|
+
requireValidUuid(did);
|
|
914
|
+
}
|
|
615
915
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
616
916
|
const result = await engine.search(String(args.query ?? ""), {
|
|
617
917
|
sourceIds: args.source_ids,
|
|
918
|
+
documentIds: args.document_ids,
|
|
618
919
|
principals: callerPrincipals,
|
|
619
920
|
topK: args.top_k ?? 10,
|
|
620
921
|
mode: args.mode ?? "hybrid"
|
|
@@ -636,7 +937,7 @@ async function createMcpApp(engine, opts) {
|
|
|
636
937
|
tool(
|
|
637
938
|
"get_document",
|
|
638
939
|
`Fetch one document's full text and metadata by id.${SCOPE_NOTE}`,
|
|
639
|
-
{ document_id:
|
|
940
|
+
{ document_id: z.string() },
|
|
640
941
|
async (args) => {
|
|
641
942
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
642
943
|
return engine.getDocument(String(args.document_id), { principals: callerPrincipals });
|
|
@@ -646,7 +947,7 @@ async function createMcpApp(engine, opts) {
|
|
|
646
947
|
tool(
|
|
647
948
|
"query_structured",
|
|
648
949
|
`Answer a question against documents' extracted structured data (only documents with structured data are candidates).${SCOPE_NOTE}`,
|
|
649
|
-
{ question:
|
|
950
|
+
{ question: z.string(), source_ids: z.array(z.string()).optional() },
|
|
650
951
|
async (args) => {
|
|
651
952
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
652
953
|
if (!engine.queryStructured) throw new Error("queryStructured is not available on this engine");
|
|
@@ -660,7 +961,7 @@ async function createMcpApp(engine, opts) {
|
|
|
660
961
|
tool(
|
|
661
962
|
"compute",
|
|
662
963
|
`Run LLM-authored JavaScript over in-scope spreadsheet documents and return the computed result.${SCOPE_NOTE}`,
|
|
663
|
-
{ instruction:
|
|
964
|
+
{ instruction: z.string(), source_ids: z.array(z.string()).optional() },
|
|
664
965
|
async (args) => {
|
|
665
966
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
666
967
|
if (!engine.compute) throw new Error("compute is not available on this engine");
|
|
@@ -679,7 +980,8 @@ async function createMcpApp(engine, opts) {
|
|
|
679
980
|
}
|
|
680
981
|
},
|
|
681
982
|
engine,
|
|
682
|
-
opts.principals
|
|
983
|
+
opts.principals,
|
|
984
|
+
opts.approvalScope ?? null
|
|
683
985
|
);
|
|
684
986
|
const handler = (async (req, res) => {
|
|
685
987
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
@@ -693,6 +995,7 @@ var init_mcp = __esm({
|
|
|
693
995
|
"src/mcp.ts"() {
|
|
694
996
|
init_errors();
|
|
695
997
|
init_extras();
|
|
998
|
+
init_routing_core();
|
|
696
999
|
init_mcp_tools();
|
|
697
1000
|
init_version();
|
|
698
1001
|
}
|
|
@@ -1967,6 +2270,8 @@ var init_chunkers = __esm({
|
|
|
1967
2270
|
// src/providers/llm.ts
|
|
1968
2271
|
var llm_exports = {};
|
|
1969
2272
|
__export(llm_exports, {
|
|
2273
|
+
CALL_TIMEOUT_MS: () => CALL_TIMEOUT_MS,
|
|
2274
|
+
GEMINI_CALL_TIMEOUT_MS: () => GEMINI_CALL_TIMEOUT_MS,
|
|
1970
2275
|
LLMClient: () => LLMClient,
|
|
1971
2276
|
TIMEOUT_MS: () => TIMEOUT_MS,
|
|
1972
2277
|
buildLlmClient: () => buildLlmClient,
|
|
@@ -2009,7 +2314,7 @@ async function loadGeminiChatClient(apiKey) {
|
|
|
2009
2314
|
if (!Ctor) {
|
|
2010
2315
|
throw new ExtraMissingError("gemini", specifier, "gemini llm");
|
|
2011
2316
|
}
|
|
2012
|
-
return new Ctor({ apiKey: apiKey ?? null });
|
|
2317
|
+
return new Ctor({ apiKey: apiKey ?? null, httpOptions: { timeout: GEMINI_CALL_TIMEOUT_MS } });
|
|
2013
2318
|
}
|
|
2014
2319
|
async function loadBedrockSdk() {
|
|
2015
2320
|
const specifier = "@aws-sdk/client-bedrock-runtime";
|
|
@@ -2046,11 +2351,13 @@ async function callLlm(cfg2, opts) {
|
|
|
2046
2351
|
await owned.aclose();
|
|
2047
2352
|
}
|
|
2048
2353
|
}
|
|
2049
|
-
var TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY, LLMClient;
|
|
2354
|
+
var CALL_TIMEOUT_MS, TIMEOUT_MS, GEMINI_CALL_TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY, LLMClient;
|
|
2050
2355
|
var init_llm = __esm({
|
|
2051
2356
|
"src/providers/llm.ts"() {
|
|
2052
2357
|
init_errors();
|
|
2053
|
-
|
|
2358
|
+
CALL_TIMEOUT_MS = 24e4;
|
|
2359
|
+
TIMEOUT_MS = CALL_TIMEOUT_MS;
|
|
2360
|
+
GEMINI_CALL_TIMEOUT_MS = CALL_TIMEOUT_MS;
|
|
2054
2361
|
ANTHROPIC_VERSION = "2023-06-01";
|
|
2055
2362
|
ANTHROPIC_MAX_TOKENS = 4096;
|
|
2056
2363
|
OPENAI_FAMILY = /* @__PURE__ */ new Set(["openai", "azure_openai", "custom"]);
|
|
@@ -2078,22 +2385,30 @@ var init_llm = __esm({
|
|
|
2078
2385
|
await this.aclose();
|
|
2079
2386
|
}
|
|
2080
2387
|
async call(opts) {
|
|
2081
|
-
const {
|
|
2388
|
+
const {
|
|
2389
|
+
system,
|
|
2390
|
+
user,
|
|
2391
|
+
jsonMode = false,
|
|
2392
|
+
images = null,
|
|
2393
|
+
maxTokens = null,
|
|
2394
|
+
thinkingBudget = null,
|
|
2395
|
+
temperature = null
|
|
2396
|
+
} = opts;
|
|
2082
2397
|
if (this.provider === "anthropic") {
|
|
2083
|
-
return this.callAnthropic(system, user, jsonMode, images);
|
|
2398
|
+
return this.callAnthropic(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
2084
2399
|
}
|
|
2085
2400
|
if (OPENAI_FAMILY.has(this.provider)) {
|
|
2086
|
-
return this.callOpenAI(system, user, jsonMode, images);
|
|
2401
|
+
return this.callOpenAI(system, user, jsonMode, images, maxTokens, temperature);
|
|
2087
2402
|
}
|
|
2088
2403
|
if (this.provider === "gemini") {
|
|
2089
|
-
return this.callGemini(system, user, jsonMode, images);
|
|
2404
|
+
return this.callGemini(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
2090
2405
|
}
|
|
2091
2406
|
if (this.provider === "bedrock") {
|
|
2092
|
-
return this.callBedrock(system, user, jsonMode, images);
|
|
2407
|
+
return this.callBedrock(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
2093
2408
|
}
|
|
2094
2409
|
throw new Error(`unknown llm provider: ${JSON.stringify(this.provider)}`);
|
|
2095
2410
|
}
|
|
2096
|
-
async callAnthropic(system, user, jsonMode, images) {
|
|
2411
|
+
async callAnthropic(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
2097
2412
|
if (!this.fetchImpl) {
|
|
2098
2413
|
throw new Error("anthropic llm client has no fetch implementation");
|
|
2099
2414
|
}
|
|
@@ -2114,26 +2429,27 @@ Respond with valid JSON only.`;
|
|
|
2114
2429
|
});
|
|
2115
2430
|
}
|
|
2116
2431
|
content.push({ type: "text", text: user });
|
|
2117
|
-
const
|
|
2118
|
-
this.
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2432
|
+
const body = {
|
|
2433
|
+
model: this.model,
|
|
2434
|
+
max_tokens: maxTokens ?? ANTHROPIC_MAX_TOKENS,
|
|
2435
|
+
system,
|
|
2436
|
+
messages: [{ role: "user", content }]
|
|
2437
|
+
};
|
|
2438
|
+
if (thinkingBudget) {
|
|
2439
|
+
body.thinking = { type: "enabled", budget_tokens: thinkingBudget };
|
|
2440
|
+
} else if (temperature !== null) {
|
|
2441
|
+
body.temperature = temperature;
|
|
2442
|
+
}
|
|
2443
|
+
const data = await postJson(this.fetchImpl, "https://api.anthropic.com/v1/messages", body, {
|
|
2444
|
+
"x-api-key": this.cfg.apiKey ?? "",
|
|
2445
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
2446
|
+
"content-type": "application/json"
|
|
2447
|
+
});
|
|
2132
2448
|
const text = data.content[0].text;
|
|
2133
2449
|
const usage = data.usage ?? {};
|
|
2134
2450
|
return [text, { input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 }];
|
|
2135
2451
|
}
|
|
2136
|
-
async callOpenAI(system, user, jsonMode, images) {
|
|
2452
|
+
async callOpenAI(system, user, jsonMode, images, maxTokens, temperature = null) {
|
|
2137
2453
|
if (!this.client) {
|
|
2138
2454
|
throw new Error("openai-family llm client has no client");
|
|
2139
2455
|
}
|
|
@@ -2154,12 +2470,18 @@ Respond with valid JSON only.`;
|
|
|
2154
2470
|
if (jsonMode) {
|
|
2155
2471
|
body.response_format = { type: "json_object" };
|
|
2156
2472
|
}
|
|
2473
|
+
if (maxTokens) {
|
|
2474
|
+
body.max_completion_tokens = maxTokens;
|
|
2475
|
+
}
|
|
2476
|
+
if (temperature !== null) {
|
|
2477
|
+
body.temperature = temperature;
|
|
2478
|
+
}
|
|
2157
2479
|
const resp = await this.client.chat.completions.create(body);
|
|
2158
2480
|
const text = resp.choices[0]?.message?.content ?? "";
|
|
2159
2481
|
const usage = resp.usage;
|
|
2160
2482
|
return [text, { input: usage?.prompt_tokens ?? 0, output: usage?.completion_tokens ?? 0 }];
|
|
2161
2483
|
}
|
|
2162
|
-
async callGemini(system, user, jsonMode, images) {
|
|
2484
|
+
async callGemini(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
2163
2485
|
if (!this.genaiClient) {
|
|
2164
2486
|
this.genaiClient = await loadGeminiChatClient(this.cfg.apiKey);
|
|
2165
2487
|
}
|
|
@@ -2168,10 +2490,30 @@ Respond with valid JSON only.`;
|
|
|
2168
2490
|
parts.push({ inlineData: { mimeType: "image/png", data: toBase64(img) } });
|
|
2169
2491
|
}
|
|
2170
2492
|
parts.push({ text: user });
|
|
2171
|
-
const config = {
|
|
2493
|
+
const config = {
|
|
2494
|
+
systemInstruction: system,
|
|
2495
|
+
// ALWAYS off, and not as a preference. Automatic function calling means
|
|
2496
|
+
// the SDK itself EXECUTES a callable it was handed as a tool and loops
|
|
2497
|
+
// on the result — up to ten round trips — before returning anything.
|
|
2498
|
+
// This package passes declarations only, so today nothing is executable;
|
|
2499
|
+
// but that depends on every future caller continuing to do the same, and
|
|
2500
|
+
// an application that gates tool execution behind human approval would
|
|
2501
|
+
// have that gate bypassed silently, by a library, with the loop already
|
|
2502
|
+
// run before it could object.
|
|
2503
|
+
automaticFunctionCalling: { disable: true }
|
|
2504
|
+
};
|
|
2172
2505
|
if (jsonMode) {
|
|
2173
2506
|
config.responseMimeType = "application/json";
|
|
2174
2507
|
}
|
|
2508
|
+
if (maxTokens) {
|
|
2509
|
+
config.maxOutputTokens = maxTokens;
|
|
2510
|
+
}
|
|
2511
|
+
if (temperature !== null) {
|
|
2512
|
+
config.temperature = temperature;
|
|
2513
|
+
}
|
|
2514
|
+
if (thinkingBudget !== null) {
|
|
2515
|
+
config.thinkingConfig = { thinkingBudget };
|
|
2516
|
+
}
|
|
2175
2517
|
const resp = await this.genaiClient.models.generateContent({
|
|
2176
2518
|
model: this.model,
|
|
2177
2519
|
contents: parts,
|
|
@@ -2183,7 +2525,7 @@ Respond with valid JSON only.`;
|
|
|
2183
2525
|
const outputTokens = usageMeta?.candidatesTokenCount ?? usageMeta?.candidates_token_count ?? 0;
|
|
2184
2526
|
return [text, { input: inputTokens || 0, output: outputTokens || 0 }];
|
|
2185
2527
|
}
|
|
2186
|
-
async callBedrock(system, user, jsonMode, images) {
|
|
2528
|
+
async callBedrock(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
2187
2529
|
const { BedrockRuntimeClient, ConverseCommand } = await loadBedrockSdk();
|
|
2188
2530
|
if (jsonMode) {
|
|
2189
2531
|
system = `${system}
|
|
@@ -2207,7 +2549,21 @@ Respond with valid JSON only.`;
|
|
|
2207
2549
|
new ConverseCommand({
|
|
2208
2550
|
modelId: this.model,
|
|
2209
2551
|
system: [{ text: system }],
|
|
2210
|
-
messages: [{ role: "user", content }]
|
|
2552
|
+
messages: [{ role: "user", content }],
|
|
2553
|
+
...maxTokens || temperature !== null ? {
|
|
2554
|
+
inferenceConfig: {
|
|
2555
|
+
...maxTokens ? { maxTokens } : {},
|
|
2556
|
+
...temperature !== null ? { temperature } : {}
|
|
2557
|
+
}
|
|
2558
|
+
} : {},
|
|
2559
|
+
// Anthropic-style passthrough — Converse forwards it to the model.
|
|
2560
|
+
// Zero (the vision transcription contract) sends nothing: thinking
|
|
2561
|
+
// is opt-in for the anthropic models bedrock hosts.
|
|
2562
|
+
...thinkingBudget ? {
|
|
2563
|
+
additionalModelRequestFields: {
|
|
2564
|
+
thinking: { type: "enabled", budget_tokens: thinkingBudget }
|
|
2565
|
+
}
|
|
2566
|
+
} : {}
|
|
2211
2567
|
})
|
|
2212
2568
|
);
|
|
2213
2569
|
const text = result.output?.message?.content?.[0]?.text ?? "";
|
|
@@ -2802,6 +3158,20 @@ RETURN JSON SCHEMA (EXACT):
|
|
|
2802
3158
|
}`;
|
|
2803
3159
|
}
|
|
2804
3160
|
});
|
|
3161
|
+
|
|
3162
|
+
// src/tools/acl.ts
|
|
3163
|
+
function aclVisible(acl, principals) {
|
|
3164
|
+
if (principals === null || principals === TRUSTED) return true;
|
|
3165
|
+
if (acl == null) return true;
|
|
3166
|
+
if (!acl.length) return false;
|
|
3167
|
+
const held = new Set(principals ?? []);
|
|
3168
|
+
return acl.some((p) => held.has(p));
|
|
3169
|
+
}
|
|
3170
|
+
var init_acl = __esm({
|
|
3171
|
+
"src/tools/acl.ts"() {
|
|
3172
|
+
init_sentinels();
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
2805
3175
|
function getFileExtension(filename) {
|
|
2806
3176
|
if (!filename?.includes(".")) return "";
|
|
2807
3177
|
return filename.slice(filename.lastIndexOf(".")).toLowerCase();
|
|
@@ -3618,8 +3988,26 @@ __export(pdf_exports, {
|
|
|
3618
3988
|
embeddedTextPerPage: () => embeddedTextPerPage,
|
|
3619
3989
|
extract: () => extract,
|
|
3620
3990
|
extractPdf: () => extract,
|
|
3991
|
+
ocrLanguageHint: () => ocrLanguageHint,
|
|
3621
3992
|
pageCount: () => pageCount
|
|
3622
3993
|
});
|
|
3994
|
+
function ocrLanguageHint(texts, override) {
|
|
3995
|
+
if (override) return override;
|
|
3996
|
+
let sample = "";
|
|
3997
|
+
const indices = [...texts.keys()].sort((a, b) => a - b).slice(0, 3);
|
|
3998
|
+
for (const idx of indices) {
|
|
3999
|
+
sample += texts.get(idx) ?? "";
|
|
4000
|
+
if (sample.length >= OCR_LANG_SAMPLE_MIN) break;
|
|
4001
|
+
}
|
|
4002
|
+
if (sample.length >= OCR_LANG_SAMPLE_MIN) {
|
|
4003
|
+
try {
|
|
4004
|
+
const detected = detectLanguage(sample, 30);
|
|
4005
|
+
if (detected) return detected;
|
|
4006
|
+
} catch {
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
return "auto";
|
|
4010
|
+
}
|
|
3623
4011
|
function isCcControl(ch) {
|
|
3624
4012
|
if (ch === "\n" || ch === "\r" || ch === " ") return false;
|
|
3625
4013
|
const cp = ch.codePointAt(0);
|
|
@@ -3672,16 +4060,27 @@ async function extract(content, opts) {
|
|
|
3672
4060
|
const { pages, structured: structuredMarkdown } = await classifyPages(content);
|
|
3673
4061
|
const providerTokens = {};
|
|
3674
4062
|
let visionMarkdown = false;
|
|
4063
|
+
let benignBlank = /* @__PURE__ */ new Set();
|
|
3675
4064
|
if (pages.needsOcr.length) {
|
|
3676
4065
|
if (opts.visionLlm) {
|
|
3677
4066
|
try {
|
|
3678
4067
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
3679
|
-
const [visionTexts, tokens] = await vision.extractPdfPages(content, pages.needsOcr, {
|
|
3680
|
-
visionLlm: opts.visionLlm
|
|
4068
|
+
const [visionTexts, tokens, outcomes] = await vision.extractPdfPages(content, pages.needsOcr, {
|
|
4069
|
+
visionLlm: opts.visionLlm,
|
|
4070
|
+
extraction: opts.extraction
|
|
3681
4071
|
});
|
|
3682
4072
|
for (const [idx, text] of Object.entries(visionTexts)) {
|
|
3683
4073
|
pages.texts.set(Number(idx), text);
|
|
3684
4074
|
}
|
|
4075
|
+
benignBlank = new Set(
|
|
4076
|
+
Object.entries(outcomes).filter(([, o]) => o.status === "blank").map(([idx]) => Number(idx))
|
|
4077
|
+
);
|
|
4078
|
+
const failed = Object.entries(outcomes).filter(([, o]) => o.status === "failed");
|
|
4079
|
+
if (failed.length) {
|
|
4080
|
+
console.warn(
|
|
4081
|
+
`[pdf] vision failed on ${failed.length} of ${pages.needsOcr.length} pages: ` + failed.map(([idx, o]) => `page ${Number(idx) + 1}: ${o.error}`).join("; ")
|
|
4082
|
+
);
|
|
4083
|
+
}
|
|
3685
4084
|
visionMarkdown = Object.keys(visionTexts).length > 0;
|
|
3686
4085
|
for (const [key, val] of Object.entries(tokens)) {
|
|
3687
4086
|
providerTokens[key] = (providerTokens[key] ?? 0) + val;
|
|
@@ -3693,9 +4092,17 @@ async function extract(content, opts) {
|
|
|
3693
4092
|
try {
|
|
3694
4093
|
const ocr = await Promise.resolve().then(() => (init_ocr(), ocr_exports));
|
|
3695
4094
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
3696
|
-
const rendered = await vision.renderPdfPagesAsImages(
|
|
4095
|
+
const rendered = await vision.renderPdfPagesAsImages(
|
|
4096
|
+
content,
|
|
4097
|
+
pages.needsOcr,
|
|
4098
|
+
150,
|
|
4099
|
+
opts.extraction?.maxRenderPx ?? vision.MAX_RENDER_PX
|
|
4100
|
+
);
|
|
4101
|
+
const ocrConfig = {
|
|
4102
|
+
language: ocrLanguageHint(pages.texts, opts.extraction?.ocrLanguage)
|
|
4103
|
+
};
|
|
3697
4104
|
for (const [idx, pngBytes] of rendered) {
|
|
3698
|
-
const result = await ocr.extractImageTextOcr(pngBytes);
|
|
4105
|
+
const result = await ocr.extractImageTextOcr(pngBytes, ocrConfig);
|
|
3699
4106
|
if (result.text.trim()) pages.texts.set(idx, result.text.trim());
|
|
3700
4107
|
}
|
|
3701
4108
|
} catch (exc) {
|
|
@@ -3713,31 +4120,38 @@ async function extract(content, opts) {
|
|
|
3713
4120
|
if (t) textParts.push(`--- Page ${i + 1} ---
|
|
3714
4121
|
${t}`);
|
|
3715
4122
|
}
|
|
4123
|
+
const unread = pages.needsOcr.filter((i) => !pages.texts.get(i) && !benignBlank.has(i));
|
|
3716
4124
|
return new Extracted2({
|
|
3717
4125
|
text: textParts.join("\n\n"),
|
|
3718
4126
|
pages: pages.total,
|
|
3719
4127
|
slides: null,
|
|
3720
4128
|
mediaOnly: false,
|
|
3721
4129
|
providerTokens,
|
|
3722
|
-
isMarkdown: visionMarkdown || structuredMarkdown
|
|
4130
|
+
isMarkdown: visionMarkdown || structuredMarkdown,
|
|
4131
|
+
unreadableReason: unread.length ? opts.visionLlm ? "vision_failed" : "needs_vision" : null,
|
|
4132
|
+
unreadablePages: unread.length
|
|
3723
4133
|
});
|
|
3724
4134
|
}
|
|
3725
|
-
var GARBAGE_SCAN_LIMIT, MIN_USABLE_TEXT_LEN;
|
|
4135
|
+
var GARBAGE_SCAN_LIMIT, MIN_USABLE_TEXT_LEN, OCR_LANG_SAMPLE_MIN;
|
|
3726
4136
|
var init_pdf = __esm({
|
|
3727
4137
|
"src/extraction/pdf.ts"() {
|
|
3728
4138
|
init_errors();
|
|
3729
4139
|
init_hooks();
|
|
4140
|
+
init_text();
|
|
3730
4141
|
init_pdf_structure();
|
|
3731
4142
|
GARBAGE_SCAN_LIMIT = 2e4;
|
|
3732
4143
|
MIN_USABLE_TEXT_LEN = 50;
|
|
4144
|
+
OCR_LANG_SAMPLE_MIN = 100;
|
|
3733
4145
|
}
|
|
3734
4146
|
});
|
|
3735
4147
|
|
|
3736
4148
|
// src/extraction/vision.ts
|
|
3737
4149
|
var vision_exports = {};
|
|
3738
4150
|
__export(vision_exports, {
|
|
4151
|
+
MAX_RENDER_PX: () => MAX_RENDER_PX,
|
|
3739
4152
|
PAGES_PER_VISION_BATCH: () => PAGES_PER_VISION_BATCH,
|
|
3740
4153
|
VISION_BATCH_ATTEMPTS: () => VISION_BATCH_ATTEMPTS,
|
|
4154
|
+
VISION_MAX_OUTPUT_TOKENS: () => VISION_MAX_OUTPUT_TOKENS,
|
|
3741
4155
|
assignBatchPages: () => assignBatchPages,
|
|
3742
4156
|
coercePagesResult: () => coercePagesResult,
|
|
3743
4157
|
detectPdfVisionPages: () => detectPdfVisionPages,
|
|
@@ -3754,6 +4168,17 @@ function pagesPrompt(nImages) {
|
|
|
3754
4168
|
Return JSON: {"pages": [{"page": 1, "text": "<markdown for this page>"}]}
|
|
3755
4169
|
Return exactly ${nImages} page entries numbered 1 to ${nImages} in the order the images are given \u2014 IGNORE any page numbers printed on the pages.`;
|
|
3756
4170
|
}
|
|
4171
|
+
function asText(value) {
|
|
4172
|
+
if (typeof value === "string") return value;
|
|
4173
|
+
if (value == null || typeof value === "boolean") return "";
|
|
4174
|
+
if (Array.isArray(value)) {
|
|
4175
|
+
return value.filter((v) => v != null && v !== "").map(asText).join("\n");
|
|
4176
|
+
}
|
|
4177
|
+
if (typeof value === "object") {
|
|
4178
|
+
return Object.values(value).filter((v) => v != null && v !== "").map(asText).join("\n");
|
|
4179
|
+
}
|
|
4180
|
+
return String(value);
|
|
4181
|
+
}
|
|
3757
4182
|
function coercePagesResult(parsed) {
|
|
3758
4183
|
let base;
|
|
3759
4184
|
let rawPages;
|
|
@@ -3773,7 +4198,7 @@ function coercePagesResult(parsed) {
|
|
|
3773
4198
|
const rec = entry;
|
|
3774
4199
|
pages.push({
|
|
3775
4200
|
page: typeof rec.page === "number" ? rec.page : i + 1,
|
|
3776
|
-
text:
|
|
4201
|
+
text: asText(rec.text)
|
|
3777
4202
|
});
|
|
3778
4203
|
} else if (typeof entry === "string") {
|
|
3779
4204
|
pages.push({ page: i + 1, text: entry });
|
|
@@ -3804,16 +4229,28 @@ function parsePagesResponse(rawText) {
|
|
|
3804
4229
|
function sleep(ms) {
|
|
3805
4230
|
return new Promise((r) => setTimeout(r, ms));
|
|
3806
4231
|
}
|
|
3807
|
-
async function
|
|
3808
|
-
if (!
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4232
|
+
async function capped(image, canvas, maxPx) {
|
|
4233
|
+
if (!canvas) return image;
|
|
4234
|
+
try {
|
|
4235
|
+
const img = await canvas.loadImage(image);
|
|
4236
|
+
const longEdge = Math.max(img.width, img.height);
|
|
4237
|
+
if (longEdge <= maxPx) return image;
|
|
4238
|
+
const ratio = maxPx / longEdge;
|
|
4239
|
+
const w = Math.max(Math.round(img.width * ratio), 1);
|
|
4240
|
+
const h = Math.max(Math.round(img.height * ratio), 1);
|
|
4241
|
+
const out = canvas.createCanvas(w, h);
|
|
4242
|
+
out.getContext("2d").drawImage(img, 0, 0, w, h);
|
|
4243
|
+
return out.toBuffer("image/png");
|
|
4244
|
+
} catch {
|
|
4245
|
+
return image;
|
|
3813
4246
|
}
|
|
3814
|
-
|
|
4247
|
+
}
|
|
4248
|
+
async function runBatches(batchSource, opts) {
|
|
4249
|
+
const started = Date.now();
|
|
4250
|
+
const results = /* @__PURE__ */ new Map();
|
|
4251
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
3815
4252
|
const tokensTotal = { input: 0, output: 0 };
|
|
3816
|
-
const
|
|
4253
|
+
const maxConcurrent = opts.maxConcurrent ?? 8;
|
|
3817
4254
|
let lock = Promise.resolve();
|
|
3818
4255
|
const withLock = async (fn) => {
|
|
3819
4256
|
const prev = lock;
|
|
@@ -3829,66 +4266,183 @@ async function extractTextFromImages(images, opts) {
|
|
|
3829
4266
|
}
|
|
3830
4267
|
};
|
|
3831
4268
|
const client = buildLlmClient(opts.visionLlm);
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
4269
|
+
let total = 0;
|
|
4270
|
+
let nBatches = 0;
|
|
4271
|
+
const queue = new BoundedQueue(maxConcurrent);
|
|
4272
|
+
const processBatch = async (batchNum, baseIdx, batchImages) => {
|
|
4273
|
+
const batchStarted = Date.now();
|
|
4274
|
+
let pendingLocal = batchImages.map((_, i) => i);
|
|
4275
|
+
for (let attempt = 1; attempt <= VISION_BATCH_ATTEMPTS; attempt++) {
|
|
4276
|
+
const ask = pendingLocal.map((i) => batchImages[i]);
|
|
4277
|
+
try {
|
|
4278
|
+
const [rawText, usage] = await callLlm(opts.visionLlm, {
|
|
4279
|
+
system: VISION_SYSTEM_PROMPT,
|
|
4280
|
+
user: pagesPrompt(ask.length),
|
|
4281
|
+
jsonMode: true,
|
|
4282
|
+
images: ask,
|
|
4283
|
+
maxTokens: opts.extCfg.visionMaxOutputTokens,
|
|
4284
|
+
// Transcription is not reasoning, and on a thinking model the two
|
|
4285
|
+
// compete for the SAME budget — see `LlmCallOpts.thinkingBudget`.
|
|
4286
|
+
// Zero, not small: there is nothing here to reason ABOUT, the model
|
|
4287
|
+
// is being asked what is on the page. `temperature: 0` for the same
|
|
4288
|
+
// reason — one scan must transcribe to the same text twice.
|
|
4289
|
+
thinkingBudget: 0,
|
|
4290
|
+
temperature: 0,
|
|
4291
|
+
client
|
|
4292
|
+
});
|
|
4293
|
+
if (!rawText?.trim()) throw new Error("empty vision response");
|
|
4294
|
+
const [, pages] = parsePagesResponse(rawText);
|
|
4295
|
+
const assigned = assignBatchPages(pages, ask.length);
|
|
4296
|
+
const batchSeconds = (Date.now() - batchStarted) / 1e3;
|
|
4297
|
+
const stillMissing = [];
|
|
4298
|
+
await withLock(() => {
|
|
4299
|
+
tokensTotal.input += usage.input ?? 0;
|
|
4300
|
+
tokensTotal.output += usage.output ?? 0;
|
|
4301
|
+
pendingLocal.forEach((localIdx, position) => {
|
|
4302
|
+
const pageText = assigned[position];
|
|
4303
|
+
if (pageText === void 0) {
|
|
4304
|
+
stillMissing.push(localIdx);
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
results.set(baseIdx + localIdx, pageText);
|
|
4308
|
+
outcomes.set(baseIdx + localIdx, {
|
|
4309
|
+
status: pageText.trim() ? "read" : "blank",
|
|
4310
|
+
attempts: attempt,
|
|
4311
|
+
error: null,
|
|
4312
|
+
seconds: batchSeconds
|
|
4313
|
+
});
|
|
3845
4314
|
});
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4315
|
+
});
|
|
4316
|
+
pendingLocal = stillMissing;
|
|
4317
|
+
if (pendingLocal.length === 0) return;
|
|
4318
|
+
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
4319
|
+
console.warn(
|
|
4320
|
+
`[vision] batch ${batchNum} attempt ${attempt} answered ${ask.length - pendingLocal.length} of ${ask.length} pages \u2014 asking again for ${pendingLocal.length}`
|
|
4321
|
+
);
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
console.error(
|
|
4325
|
+
`[vision] batch ${batchNum}: ${pendingLocal.length} page(s) never came back after ${attempt} attempts`
|
|
4326
|
+
);
|
|
4327
|
+
await withLock(() => {
|
|
4328
|
+
for (const localIdx of pendingLocal) {
|
|
4329
|
+
outcomes.set(baseIdx + localIdx, {
|
|
4330
|
+
status: "failed",
|
|
4331
|
+
attempts: attempt,
|
|
4332
|
+
error: "page missing from batch response",
|
|
4333
|
+
seconds: batchSeconds
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
});
|
|
4337
|
+
return;
|
|
4338
|
+
} catch (exc) {
|
|
4339
|
+
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
4340
|
+
console.warn(`[vision] batch ${batchNum} attempt ${attempt} failed: ${exc} \u2014 retrying`);
|
|
4341
|
+
await sleep(2e3 * attempt);
|
|
4342
|
+
} else {
|
|
4343
|
+
console.error(`[vision] batch ${batchNum} failed after ${VISION_BATCH_ATTEMPTS} attempts: ${exc}`);
|
|
4344
|
+
const batchSeconds = (Date.now() - batchStarted) / 1e3;
|
|
3849
4345
|
await withLock(() => {
|
|
3850
|
-
for (const
|
|
3851
|
-
|
|
3852
|
-
|
|
4346
|
+
for (const localIdx of pendingLocal) {
|
|
4347
|
+
outcomes.set(baseIdx + localIdx, {
|
|
4348
|
+
status: "failed",
|
|
4349
|
+
attempts: attempt,
|
|
4350
|
+
error: String(exc),
|
|
4351
|
+
seconds: batchSeconds
|
|
4352
|
+
});
|
|
3853
4353
|
}
|
|
3854
|
-
tokensTotal.input += usage.input ?? 0;
|
|
3855
|
-
tokensTotal.output += usage.output ?? 0;
|
|
3856
4354
|
});
|
|
3857
|
-
return;
|
|
3858
|
-
} catch (exc) {
|
|
3859
|
-
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
3860
|
-
console.warn(`[vision] batch ${batchIdx} attempt ${attempt} failed: ${exc} \u2014 retrying`);
|
|
3861
|
-
await sleep(2e3 * attempt);
|
|
3862
|
-
} else {
|
|
3863
|
-
console.error(
|
|
3864
|
-
`[vision] batch ${batchIdx} failed after ${VISION_BATCH_ATTEMPTS} attempts: ${exc}`
|
|
3865
|
-
);
|
|
3866
|
-
}
|
|
3867
4355
|
}
|
|
3868
4356
|
}
|
|
3869
|
-
} finally {
|
|
3870
|
-
semaphore.release();
|
|
3871
4357
|
}
|
|
3872
4358
|
};
|
|
4359
|
+
const worker = async () => {
|
|
4360
|
+
for (; ; ) {
|
|
4361
|
+
const item = await queue.get();
|
|
4362
|
+
if (item === null) return;
|
|
4363
|
+
await processBatch(...item);
|
|
4364
|
+
}
|
|
4365
|
+
};
|
|
4366
|
+
const workers = Array.from({ length: maxConcurrent }, () => worker());
|
|
3873
4367
|
try {
|
|
3874
|
-
await
|
|
4368
|
+
for await (const [baseIdx, batchImages] of batchSource) {
|
|
4369
|
+
if (!batchImages.length) continue;
|
|
4370
|
+
for (let j = 0; j < batchImages.length; j++) {
|
|
4371
|
+
outcomes.set(baseIdx + j, { status: "failed", attempts: 0, error: "never attempted", seconds: 0 });
|
|
4372
|
+
}
|
|
4373
|
+
total = Math.max(total, baseIdx + batchImages.length);
|
|
4374
|
+
await queue.put([nBatches, baseIdx, batchImages]);
|
|
4375
|
+
nBatches += 1;
|
|
4376
|
+
}
|
|
4377
|
+
queue.close();
|
|
4378
|
+
await Promise.all(workers);
|
|
4379
|
+
} catch (exc) {
|
|
4380
|
+
queue.abort();
|
|
4381
|
+
await Promise.allSettled(workers);
|
|
4382
|
+
throw exc;
|
|
3875
4383
|
} finally {
|
|
3876
4384
|
await client.aclose();
|
|
3877
4385
|
}
|
|
3878
|
-
|
|
4386
|
+
const texts = Array.from({ length: total }, (_, i) => results.get(i) ?? "");
|
|
4387
|
+
const outcomeList = Array.from(
|
|
4388
|
+
{ length: total },
|
|
4389
|
+
(_, i) => outcomes.get(i) ?? { status: "failed", attempts: 0, error: "never attempted", seconds: 0 }
|
|
4390
|
+
);
|
|
4391
|
+
const counts = { read: 0, blank: 0, failed: 0 };
|
|
4392
|
+
for (const o of outcomeList) counts[o.status] += 1;
|
|
4393
|
+
console.info(
|
|
4394
|
+
`[vision] ${total} pages, ${nBatches} batches, ${((Date.now() - started) / 1e3).toFixed(1)}s, ${tokensTotal.input} in / ${tokensTotal.output} out tokens, ${counts.read} read / ${counts.blank} blank / ${counts.failed} failed`
|
|
4395
|
+
);
|
|
4396
|
+
return [texts, tokensTotal, outcomeList];
|
|
4397
|
+
}
|
|
4398
|
+
async function extractTextFromImages(images, opts) {
|
|
4399
|
+
if (!images.length) return [[], {}, []];
|
|
4400
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
4401
|
+
const canvasModule = await tryImport("@napi-rs/canvas");
|
|
4402
|
+
const sendable = await Promise.all(images.map((img) => capped(img, canvasModule, extCfg.maxRenderPx)));
|
|
4403
|
+
const batchSize = extCfg.pagesPerVisionBatch ?? PAGES_PER_VISION_BATCH;
|
|
4404
|
+
async function* batches() {
|
|
4405
|
+
for (let i = 0; i < sendable.length; i += batchSize) {
|
|
4406
|
+
yield [i, sendable.slice(i, i + batchSize)];
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
return runBatches(batches(), {
|
|
4410
|
+
visionLlm: opts.visionLlm,
|
|
4411
|
+
extCfg,
|
|
4412
|
+
maxConcurrent: opts.maxConcurrent
|
|
4413
|
+
});
|
|
3879
4414
|
}
|
|
3880
4415
|
async function extractPdfPages(content, pageIndices, opts) {
|
|
3881
|
-
const
|
|
3882
|
-
const
|
|
3883
|
-
const
|
|
4416
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
4417
|
+
const batchSize = extCfg.pagesPerVisionBatch ?? PAGES_PER_VISION_BATCH;
|
|
4418
|
+
const pageOrder = [];
|
|
4419
|
+
async function* renderedBatches() {
|
|
4420
|
+
let pos = 0;
|
|
4421
|
+
for (let start = 0; start < pageIndices.length; start += batchSize) {
|
|
4422
|
+
const group = pageIndices.slice(start, start + batchSize);
|
|
4423
|
+
const rendered = await renderPdfPagesAsImages(content, group, 150, extCfg.maxRenderPx);
|
|
4424
|
+
if (!rendered.length) continue;
|
|
4425
|
+
pageOrder.push(...rendered.map(([idx]) => idx));
|
|
4426
|
+
yield [pos, rendered.map(([, png]) => png)];
|
|
4427
|
+
pos += rendered.length;
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
const [texts, tokens, outcomes] = await runBatches(renderedBatches(), {
|
|
4431
|
+
visionLlm: opts.visionLlm,
|
|
4432
|
+
extCfg
|
|
4433
|
+
});
|
|
3884
4434
|
const pageTexts = {};
|
|
3885
|
-
|
|
4435
|
+
const pageOutcomes = {};
|
|
4436
|
+
for (let i = 0; i < pageOrder.length; i++) {
|
|
3886
4437
|
const text = texts[i];
|
|
3887
|
-
if (text?.trim()) pageTexts[
|
|
4438
|
+
if (text?.trim()) pageTexts[pageOrder[i]] = text;
|
|
4439
|
+
const outcome = outcomes[i];
|
|
4440
|
+
if (outcome) pageOutcomes[pageOrder[i]] = outcome;
|
|
3888
4441
|
}
|
|
3889
|
-
return [pageTexts, tokens];
|
|
4442
|
+
return [pageTexts, tokens, pageOutcomes];
|
|
3890
4443
|
}
|
|
3891
|
-
async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
4444
|
+
async function renderPdfPagesAsImages(content, pageIndices, dpi = 150, maxPx = MAX_RENDER_PX) {
|
|
4445
|
+
const started = Date.now();
|
|
3892
4446
|
const scale = dpi / 72;
|
|
3893
4447
|
const data = new Uint8Array(content);
|
|
3894
4448
|
const pdf = await getDocumentProxy(data);
|
|
@@ -3897,9 +4451,12 @@ async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
|
3897
4451
|
for (const idx of pageIndices) {
|
|
3898
4452
|
if (idx < 0 || idx >= pdf.numPages) continue;
|
|
3899
4453
|
try {
|
|
4454
|
+
const page = await pdf.getPage(idx + 1);
|
|
4455
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
4456
|
+
const pageScale = Math.min(scale, maxPx / Math.max(viewport.width, viewport.height, 1));
|
|
3900
4457
|
const buf = await renderPageAsImage(pdf, idx + 1, {
|
|
3901
4458
|
canvasImport: canvasImport ? async () => canvasImport : void 0,
|
|
3902
|
-
scale
|
|
4459
|
+
scale: pageScale
|
|
3903
4460
|
});
|
|
3904
4461
|
const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf);
|
|
3905
4462
|
results.push([idx, Buffer.from(bytes)]);
|
|
@@ -3907,6 +4464,7 @@ async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
|
3907
4464
|
console.warn(`[vision] failed to render PDF page ${idx}:`, exc);
|
|
3908
4465
|
}
|
|
3909
4466
|
}
|
|
4467
|
+
console.info(`[pdf] rastered ${results.length} pages in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
|
|
3910
4468
|
return results;
|
|
3911
4469
|
}
|
|
3912
4470
|
async function detectPdfVisionPages(content) {
|
|
@@ -3928,7 +4486,13 @@ async function detectPdfVisionPages(content) {
|
|
|
3928
4486
|
return visionPages;
|
|
3929
4487
|
}
|
|
3930
4488
|
async function extractDocxImages(content) {
|
|
3931
|
-
|
|
4489
|
+
let zip;
|
|
4490
|
+
try {
|
|
4491
|
+
zip = await JSZip.loadAsync(content);
|
|
4492
|
+
} catch (exc) {
|
|
4493
|
+
console.warn(`[vision] cannot open DOCX for images: ${exc}`);
|
|
4494
|
+
return [];
|
|
4495
|
+
}
|
|
3932
4496
|
const rels = zip.file("word/_rels/document.xml.rels");
|
|
3933
4497
|
if (!rels) return [];
|
|
3934
4498
|
const xml = await rels.async("string");
|
|
@@ -3954,7 +4518,13 @@ async function extractDocxImages(content) {
|
|
|
3954
4518
|
return images;
|
|
3955
4519
|
}
|
|
3956
4520
|
async function extractPptxImages(content) {
|
|
3957
|
-
|
|
4521
|
+
let zip;
|
|
4522
|
+
try {
|
|
4523
|
+
zip = await JSZip.loadAsync(content);
|
|
4524
|
+
} catch (exc) {
|
|
4525
|
+
console.warn(`[vision] cannot open PPTX for images: ${exc}`);
|
|
4526
|
+
return [];
|
|
4527
|
+
}
|
|
3958
4528
|
const slideNames = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)).sort((a, b) => {
|
|
3959
4529
|
const na = Number(/slide(\d+)/.exec(a)?.[1] ?? 0);
|
|
3960
4530
|
const nb = Number(/slide(\d+)/.exec(b)?.[1] ?? 0);
|
|
@@ -3987,12 +4557,22 @@ async function extractPptxImages(content) {
|
|
|
3987
4557
|
}
|
|
3988
4558
|
async function extractImage(content, opts) {
|
|
3989
4559
|
const { Extracted: Extracted2 } = await Promise.resolve().then(() => (init_extraction(), extraction_exports));
|
|
4560
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
4561
|
+
const canvasModule = await tryImport("@napi-rs/canvas");
|
|
4562
|
+
const image = await capped(content, canvasModule, extCfg.maxRenderPx);
|
|
3990
4563
|
const [rawText, usage] = await callLlm(opts.visionLlm, {
|
|
3991
4564
|
system: VISION_SYSTEM_PROMPT,
|
|
3992
4565
|
user: `${MARKDOWN_RULES}
|
|
3993
4566
|
|
|
3994
4567
|
Transcribe this single image. Return the Markdown only.`,
|
|
3995
|
-
images: [
|
|
4568
|
+
images: [image],
|
|
4569
|
+
maxTokens: extCfg.visionMaxOutputTokens,
|
|
4570
|
+
// Same contract as the batched path (see processBatch): with the output
|
|
4571
|
+
// cap in place, default-on thinking bills against it — an all-thinking
|
|
4572
|
+
// truncated-empty reply lands as vision_failed. Transcription is not
|
|
4573
|
+
// reasoning, and it must be deterministic.
|
|
4574
|
+
thinkingBudget: 0,
|
|
4575
|
+
temperature: 0
|
|
3996
4576
|
});
|
|
3997
4577
|
const text = (rawText || "").trim();
|
|
3998
4578
|
return new Extracted2({
|
|
@@ -4001,37 +4581,75 @@ Transcribe this single image. Return the Markdown only.`,
|
|
|
4001
4581
|
slides: null,
|
|
4002
4582
|
mediaOnly: !text,
|
|
4003
4583
|
providerTokens: usage ?? {},
|
|
4004
|
-
isMarkdown: Boolean(text)
|
|
4584
|
+
isMarkdown: Boolean(text),
|
|
4585
|
+
// A configured reader that returns nothing (refusal, filter, truncation)
|
|
4586
|
+
// is the same "reader read nothing" state the PDF path labels
|
|
4587
|
+
// vision_failed — not a blank image with no explanation.
|
|
4588
|
+
unreadableReason: text ? null : "vision_failed",
|
|
4589
|
+
unreadablePages: text ? 0 : 1
|
|
4005
4590
|
});
|
|
4006
4591
|
}
|
|
4007
|
-
var
|
|
4592
|
+
var VISION_BATCH_ATTEMPTS, EXTRACTION_DEFAULTS, MAX_RENDER_PX, VISION_MAX_OUTPUT_TOKENS, PAGES_PER_VISION_BATCH, VISION_SYSTEM_PROMPT, MARKDOWN_RULES, BoundedQueue;
|
|
4008
4593
|
var init_vision = __esm({
|
|
4009
4594
|
"src/extraction/vision.ts"() {
|
|
4595
|
+
init_config();
|
|
4010
4596
|
init_extras();
|
|
4011
4597
|
init_llm();
|
|
4012
4598
|
init_json();
|
|
4013
4599
|
init_pdf();
|
|
4014
|
-
PAGES_PER_VISION_BATCH = 5;
|
|
4015
4600
|
VISION_BATCH_ATTEMPTS = 3;
|
|
4601
|
+
EXTRACTION_DEFAULTS = extractionSchema.parse({});
|
|
4602
|
+
MAX_RENDER_PX = EXTRACTION_DEFAULTS.maxRenderPx;
|
|
4603
|
+
VISION_MAX_OUTPUT_TOKENS = EXTRACTION_DEFAULTS.visionMaxOutputTokens;
|
|
4604
|
+
PAGES_PER_VISION_BATCH = EXTRACTION_DEFAULTS.pagesPerVisionBatch;
|
|
4016
4605
|
VISION_SYSTEM_PROMPT = "You are a precise document transcription engine. You transcribe page images into clean GitHub-flavored Markdown that preserves tables, headings, and reading order \u2014 verbatim, never summarizing, translating, or inventing content.";
|
|
4017
4606
|
MARKDOWN_RULES = "Transcribe into clean Markdown that PRESERVES structure:\n- TABLES: reproduce as GitHub-flavored Markdown tables \u2014 one row per line, real column separators, keep EVERY cell (empty cell for blanks; put a spanning cell's value in its top-left position). NEVER flatten a table into a sentence.\n- HEADINGS/TITLES: mark with #/##/### by visual hierarchy.\n- LISTS: use - or 1. as printed.\n- FIGURES/CHARTS: add a short italic caption line, e.g. *Figure: bar chart of ...*.\n- Follow natural reading order (multi-column pages: finish the left column, then the right).\n- Transcribe VERBATIM \u2014 every word, number, label, caption. Do not summarize, translate, or invent.";
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4607
|
+
BoundedQueue = class {
|
|
4608
|
+
constructor(maxsize) {
|
|
4609
|
+
this.maxsize = maxsize;
|
|
4610
|
+
}
|
|
4611
|
+
maxsize;
|
|
4612
|
+
items = [];
|
|
4613
|
+
putters = [];
|
|
4614
|
+
getters = [];
|
|
4615
|
+
closed = false;
|
|
4616
|
+
async put(item) {
|
|
4617
|
+
while (!this.closed && this.items.length >= this.maxsize) {
|
|
4618
|
+
await new Promise((resolve) => this.putters.push(resolve));
|
|
4619
|
+
}
|
|
4620
|
+
if (this.closed) return;
|
|
4621
|
+
this.items.push(item);
|
|
4622
|
+
this.getters.shift()?.();
|
|
4623
|
+
}
|
|
4624
|
+
/** `null` once the queue is closed AND drained — the worker's exit signal. */
|
|
4625
|
+
async get() {
|
|
4626
|
+
for (; ; ) {
|
|
4627
|
+
if (this.items.length) {
|
|
4628
|
+
const item = this.items.shift();
|
|
4629
|
+
this.putters.shift()?.();
|
|
4630
|
+
return item;
|
|
4631
|
+
}
|
|
4632
|
+
if (this.closed) return null;
|
|
4633
|
+
await new Promise((resolve) => this.getters.push(resolve));
|
|
4028
4634
|
}
|
|
4029
|
-
await new Promise((resolve) => this.waiters.push(resolve));
|
|
4030
4635
|
}
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4636
|
+
close() {
|
|
4637
|
+
this.closed = true;
|
|
4638
|
+
for (const wake of this.getters.splice(0)) wake();
|
|
4639
|
+
for (const wake of this.putters.splice(0)) wake();
|
|
4640
|
+
}
|
|
4641
|
+
/**
|
|
4642
|
+
* Like `close()`, but for the error path: DISCARDS whatever is still
|
|
4643
|
+
* queued rather than letting it drain. A batch already in a worker's hands
|
|
4644
|
+
* keeps running (there is no cancelling that), but one that only ever sat
|
|
4645
|
+
* in the queue must never start — the source has already failed, so
|
|
4646
|
+
* spending a full vision-LLM retry budget on it buys nothing.
|
|
4647
|
+
*/
|
|
4648
|
+
abort() {
|
|
4649
|
+
this.closed = true;
|
|
4650
|
+
this.items.length = 0;
|
|
4651
|
+
for (const wake of this.getters.splice(0)) wake();
|
|
4652
|
+
for (const wake of this.putters.splice(0)) wake();
|
|
4035
4653
|
}
|
|
4036
4654
|
};
|
|
4037
4655
|
}
|
|
@@ -4041,7 +4659,8 @@ var init_vision = __esm({
|
|
|
4041
4659
|
var extraction_exports = {};
|
|
4042
4660
|
__export(extraction_exports, {
|
|
4043
4661
|
Extracted: () => Extracted,
|
|
4044
|
-
extract: () => extract2
|
|
4662
|
+
extract: () => extract2,
|
|
4663
|
+
extractEmbeddedImagesText: () => extractEmbeddedImagesText
|
|
4045
4664
|
});
|
|
4046
4665
|
function isPdf(ext, mime) {
|
|
4047
4666
|
return ext === ".pdf" || mime === "application/pdf";
|
|
@@ -4049,14 +4668,50 @@ function isPdf(ext, mime) {
|
|
|
4049
4668
|
function isImage(ext, mime) {
|
|
4050
4669
|
return mime.startsWith("image/") || IMAGE_EXTS.has(ext);
|
|
4051
4670
|
}
|
|
4052
|
-
async function
|
|
4671
|
+
async function officeUnreadable(text, images, visionLlm) {
|
|
4672
|
+
if (text.trim()) return { reason: null, unread: 0 };
|
|
4673
|
+
let readable = 0;
|
|
4674
|
+
for (const img of images) {
|
|
4675
|
+
if (!await embeddedImageIsNegligible(img.imageBytes)) readable += 1;
|
|
4676
|
+
}
|
|
4677
|
+
if (!readable) return { reason: null, unread: 0 };
|
|
4678
|
+
return { reason: visionLlm ? "vision_failed" : "needs_vision", unread: readable };
|
|
4679
|
+
}
|
|
4680
|
+
async function embeddedImageIsNegligible(data) {
|
|
4681
|
+
if (data.length < MIN_EMBEDDED_IMAGE_BYTES) return true;
|
|
4682
|
+
const canvas = await tryImport("@napi-rs/canvas");
|
|
4683
|
+
if (!canvas) return false;
|
|
4684
|
+
try {
|
|
4685
|
+
const img = await canvas.loadImage(data);
|
|
4686
|
+
return Math.max(img.width, img.height) < MIN_EMBEDDED_IMAGE_PX;
|
|
4687
|
+
} catch {
|
|
4688
|
+
return false;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
async function extractEmbeddedImagesText(images, visionLlm, hooks, extraction) {
|
|
4053
4692
|
if (!images.length) return [[], {}];
|
|
4054
4693
|
try {
|
|
4055
4694
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4695
|
+
const { createHash: createHash3 } = await import('crypto');
|
|
4696
|
+
const keys = [];
|
|
4697
|
+
const firstAt = /* @__PURE__ */ new Map();
|
|
4698
|
+
const send = [];
|
|
4699
|
+
for (const img of images) {
|
|
4700
|
+
const blob = img.imageBytes;
|
|
4701
|
+
if (await embeddedImageIsNegligible(blob)) {
|
|
4702
|
+
keys.push(null);
|
|
4703
|
+
continue;
|
|
4704
|
+
}
|
|
4705
|
+
const digest = createHash3("sha256").update(blob).digest("hex");
|
|
4706
|
+
if (!firstAt.has(digest)) {
|
|
4707
|
+
firstAt.set(digest, send.length);
|
|
4708
|
+
send.push(blob);
|
|
4709
|
+
}
|
|
4710
|
+
keys.push(digest);
|
|
4711
|
+
}
|
|
4712
|
+
if (!send.length) return [images.map(() => ""), {}];
|
|
4713
|
+
const [texts, tokens] = await vision.extractTextFromImages(send, { visionLlm, extraction });
|
|
4714
|
+
return [keys.map((k) => k == null ? "" : texts[firstAt.get(k)] ?? ""), tokens];
|
|
4060
4715
|
} catch (exc) {
|
|
4061
4716
|
emitError(hooks, exc, { stage: "embedded_image_vision" });
|
|
4062
4717
|
return [[], {}];
|
|
@@ -4067,22 +4722,35 @@ async function extract2(content, filename, mime, opts) {
|
|
|
4067
4722
|
const ext = getFileExtension(filename);
|
|
4068
4723
|
const visionLlm = opts.visionLlm ?? null;
|
|
4069
4724
|
const hooks = opts.hooks;
|
|
4725
|
+
const extraction = opts.extraction ?? null;
|
|
4070
4726
|
try {
|
|
4071
4727
|
if (isNonIngestibleMedia(filename, m)) {
|
|
4072
4728
|
return new Extracted({ text: "", mediaOnly: true });
|
|
4073
4729
|
}
|
|
4074
4730
|
if (isPdf(ext, m)) {
|
|
4075
4731
|
const pdf = await Promise.resolve().then(() => (init_pdf(), pdf_exports));
|
|
4076
|
-
return await pdf.extract(content, { visionLlm, hooks });
|
|
4732
|
+
return await pdf.extract(content, { visionLlm, hooks, extraction });
|
|
4077
4733
|
}
|
|
4078
4734
|
if (isImage(ext, m)) {
|
|
4079
|
-
if (!visionLlm)
|
|
4735
|
+
if (!visionLlm) {
|
|
4736
|
+
return new Extracted({
|
|
4737
|
+
text: "",
|
|
4738
|
+
mediaOnly: true,
|
|
4739
|
+
unreadableReason: "needs_vision",
|
|
4740
|
+
unreadablePages: 1
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4080
4743
|
try {
|
|
4081
4744
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
4082
|
-
return await vision.extractImage(content, { visionLlm });
|
|
4745
|
+
return await vision.extractImage(content, { visionLlm, extraction });
|
|
4083
4746
|
} catch (exc) {
|
|
4084
4747
|
emitError(hooks, exc, { stage: "image_vision", filename });
|
|
4085
|
-
return new Extracted({
|
|
4748
|
+
return new Extracted({
|
|
4749
|
+
text: "",
|
|
4750
|
+
mediaOnly: true,
|
|
4751
|
+
unreadableReason: "vision_failed",
|
|
4752
|
+
unreadablePages: 1
|
|
4753
|
+
});
|
|
4086
4754
|
}
|
|
4087
4755
|
}
|
|
4088
4756
|
if (ext === ".docx" || m === DOCX_MIME) {
|
|
@@ -4090,10 +4758,11 @@ async function extract2(content, filename, mime, opts) {
|
|
|
4090
4758
|
let providerTokens = {};
|
|
4091
4759
|
let llmPictures = 0;
|
|
4092
4760
|
let combined = text;
|
|
4761
|
+
let images = [];
|
|
4093
4762
|
if (visionLlm) {
|
|
4094
4763
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
4095
|
-
|
|
4096
|
-
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks);
|
|
4764
|
+
images = await vision.extractDocxImages(content);
|
|
4765
|
+
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks, extraction);
|
|
4097
4766
|
for (let i = 0; i < texts.length; i++) {
|
|
4098
4767
|
const imgText = texts[i];
|
|
4099
4768
|
if (imgText) {
|
|
@@ -4104,12 +4773,18 @@ ${imgText}`;
|
|
|
4104
4773
|
}
|
|
4105
4774
|
}
|
|
4106
4775
|
providerTokens = tokens;
|
|
4776
|
+
} else if (!text.trim()) {
|
|
4777
|
+
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
4778
|
+
images = await vision.extractDocxImages(content);
|
|
4107
4779
|
}
|
|
4780
|
+
const { reason, unread } = await officeUnreadable(combined, images, visionLlm);
|
|
4108
4781
|
return new Extracted({
|
|
4109
4782
|
text: combined,
|
|
4110
4783
|
pages: await countDocxPages(content),
|
|
4111
4784
|
providerTokens,
|
|
4112
|
-
llmPictures
|
|
4785
|
+
llmPictures,
|
|
4786
|
+
unreadableReason: reason,
|
|
4787
|
+
unreadablePages: unread
|
|
4113
4788
|
});
|
|
4114
4789
|
}
|
|
4115
4790
|
if (ext === ".pptx" || m === PPTX_MIME) {
|
|
@@ -4117,10 +4792,11 @@ ${imgText}`;
|
|
|
4117
4792
|
let providerTokens = {};
|
|
4118
4793
|
let llmPictures = 0;
|
|
4119
4794
|
let combined = text;
|
|
4795
|
+
let images = [];
|
|
4120
4796
|
if (visionLlm) {
|
|
4121
4797
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
4122
|
-
|
|
4123
|
-
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks);
|
|
4798
|
+
images = await vision.extractPptxImages(content);
|
|
4799
|
+
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks, extraction);
|
|
4124
4800
|
for (let i = 0; i < texts.length; i++) {
|
|
4125
4801
|
const imgText = texts[i];
|
|
4126
4802
|
if (imgText) {
|
|
@@ -4132,12 +4808,18 @@ ${imgText}`;
|
|
|
4132
4808
|
}
|
|
4133
4809
|
}
|
|
4134
4810
|
providerTokens = tokens;
|
|
4811
|
+
} else if (!text.trim()) {
|
|
4812
|
+
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
4813
|
+
images = await vision.extractPptxImages(content);
|
|
4135
4814
|
}
|
|
4815
|
+
const { reason, unread } = await officeUnreadable(combined, images, visionLlm);
|
|
4136
4816
|
return new Extracted({
|
|
4137
4817
|
text: combined,
|
|
4138
4818
|
slides: await countPptxSlides(content),
|
|
4139
4819
|
providerTokens,
|
|
4140
|
-
llmPictures
|
|
4820
|
+
llmPictures,
|
|
4821
|
+
unreadableReason: reason,
|
|
4822
|
+
unreadablePages: unread
|
|
4141
4823
|
});
|
|
4142
4824
|
}
|
|
4143
4825
|
if (ext === ".xlsx" || m === XLSX_MIME) {
|
|
@@ -4161,9 +4843,10 @@ ${imgText}`;
|
|
|
4161
4843
|
return new Extracted({ text: content.toString("utf8") });
|
|
4162
4844
|
}
|
|
4163
4845
|
}
|
|
4164
|
-
var IMAGE_EXTS, Extracted;
|
|
4846
|
+
var IMAGE_EXTS, Extracted, MIN_EMBEDDED_IMAGE_BYTES, MIN_EMBEDDED_IMAGE_PX;
|
|
4165
4847
|
var init_extraction = __esm({
|
|
4166
4848
|
"src/extraction/index.ts"() {
|
|
4849
|
+
init_extras();
|
|
4167
4850
|
init_hooks();
|
|
4168
4851
|
init_files();
|
|
4169
4852
|
IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp"]);
|
|
@@ -4175,6 +4858,10 @@ var init_extraction = __esm({
|
|
|
4175
4858
|
providerTokens;
|
|
4176
4859
|
llmPictures;
|
|
4177
4860
|
isMarkdown;
|
|
4861
|
+
/** See `UnreadableReason`. `mediaOnly` is the same idea for whole files and
|
|
4862
|
+
* stays set alongside this, so existing callers keep working. */
|
|
4863
|
+
unreadableReason;
|
|
4864
|
+
unreadablePages;
|
|
4178
4865
|
constructor(init) {
|
|
4179
4866
|
this.text = init.text;
|
|
4180
4867
|
this.pages = init.pages ?? null;
|
|
@@ -4183,8 +4870,12 @@ var init_extraction = __esm({
|
|
|
4183
4870
|
this.providerTokens = init.providerTokens ?? {};
|
|
4184
4871
|
this.llmPictures = init.llmPictures ?? 0;
|
|
4185
4872
|
this.isMarkdown = init.isMarkdown ?? false;
|
|
4873
|
+
this.unreadableReason = init.unreadableReason ?? null;
|
|
4874
|
+
this.unreadablePages = init.unreadablePages ?? 0;
|
|
4186
4875
|
}
|
|
4187
4876
|
};
|
|
4877
|
+
MIN_EMBEDDED_IMAGE_BYTES = 3 * 1024;
|
|
4878
|
+
MIN_EMBEDDED_IMAGE_PX = 100;
|
|
4188
4879
|
}
|
|
4189
4880
|
});
|
|
4190
4881
|
function checkProvider(cfg2) {
|
|
@@ -4362,7 +5053,9 @@ async function prepare(opts) {
|
|
|
4362
5053
|
mediaOnly: false,
|
|
4363
5054
|
providerTokens: {},
|
|
4364
5055
|
contentHash: calculateContentHash({ fullText: clean }),
|
|
4365
|
-
isMarkdown: false
|
|
5056
|
+
isMarkdown: false,
|
|
5057
|
+
unreadableReason: null,
|
|
5058
|
+
unreadablePages: 0
|
|
4366
5059
|
};
|
|
4367
5060
|
}
|
|
4368
5061
|
const content = opts.content;
|
|
@@ -4370,11 +5063,12 @@ async function prepare(opts) {
|
|
|
4370
5063
|
const mime = mimeForFilename(fname) || "";
|
|
4371
5064
|
const extracted = await extract2(content, fname, mime || null, {
|
|
4372
5065
|
visionLlm: opts.config.visionLlm,
|
|
5066
|
+
extraction: opts.config.extraction,
|
|
4373
5067
|
hooks: opts.hooks
|
|
4374
5068
|
});
|
|
4375
5069
|
let body = sanitizeText(extracted.text || "") || "";
|
|
4376
5070
|
const effectiveMime = mime || inferMime(body, fname) || DEFAULT_MIME;
|
|
4377
|
-
if (effectiveMime === "application/pdf" && body) body = dedupLines(body);
|
|
5071
|
+
if (effectiveMime === "application/pdf" && body && !extracted.isMarkdown) body = dedupLines(body);
|
|
4378
5072
|
return {
|
|
4379
5073
|
text: body,
|
|
4380
5074
|
mime: effectiveMime,
|
|
@@ -4385,7 +5079,9 @@ async function prepare(opts) {
|
|
|
4385
5079
|
mediaOnly: Boolean(extracted.mediaOnly),
|
|
4386
5080
|
providerTokens: { ...extracted.providerTokens ?? {} },
|
|
4387
5081
|
contentHash: calculateContentHash({ docBytes: content }),
|
|
4388
|
-
isMarkdown: Boolean(extracted.isMarkdown)
|
|
5082
|
+
isMarkdown: Boolean(extracted.isMarkdown),
|
|
5083
|
+
unreadableReason: extracted.unreadableReason ?? null,
|
|
5084
|
+
unreadablePages: extracted.unreadablePages ?? 0
|
|
4389
5085
|
};
|
|
4390
5086
|
}
|
|
4391
5087
|
function computeUnits(prepared) {
|
|
@@ -4563,7 +5259,11 @@ async function decide(pool, opts) {
|
|
|
4563
5259
|
}
|
|
4564
5260
|
const same = stored != null && incoming != null && (Buffer.isBuffer(stored) && Buffer.isBuffer(incoming) ? Buffer.compare(stored, incoming) === 0 : stored === incoming);
|
|
4565
5261
|
if (!same) return { action: "process", documentId: existingId };
|
|
4566
|
-
if (existing.status === "skipped")
|
|
5262
|
+
if (existing.status === "skipped") {
|
|
5263
|
+
const metaData = existing.meta_data;
|
|
5264
|
+
if (metaData?.unreadable_reason) return { action: "process", documentId: existingId };
|
|
5265
|
+
return { action: "skip", documentId: existingId };
|
|
5266
|
+
}
|
|
4567
5267
|
const reqMode = opts.request.mode ?? "hybrid";
|
|
4568
5268
|
if ((MODE_RANK[reqMode] ?? 0) <= (MODE_RANK[existing.mode || "hybrid"] ?? 0)) {
|
|
4569
5269
|
return { action: "skip", documentId: existingId };
|
|
@@ -4854,6 +5554,22 @@ function singleReport(doc) {
|
|
|
4854
5554
|
async function maybeAwait(value) {
|
|
4855
5555
|
return value;
|
|
4856
5556
|
}
|
|
5557
|
+
function progress(hooks, request, displayName, documentId, stage, state, detail = {}) {
|
|
5558
|
+
emitProgress(hooks, {
|
|
5559
|
+
sourceId: request.sourceId ?? null,
|
|
5560
|
+
externalId: request.externalId ?? null,
|
|
5561
|
+
documentId: documentId == null ? null : String(documentId),
|
|
5562
|
+
name: displayName,
|
|
5563
|
+
stage,
|
|
5564
|
+
state,
|
|
5565
|
+
detail: { ...detail }
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5568
|
+
function extractMethod(prepared, text) {
|
|
5569
|
+
if (text != null) return "text";
|
|
5570
|
+
if (Object.keys(prepared.providerTokens).length || prepared.isMarkdown) return "vision";
|
|
5571
|
+
return "parser";
|
|
5572
|
+
}
|
|
4857
5573
|
async function runModeUpgrade(request, opts) {
|
|
4858
5574
|
await refreshOnSkip(request, {
|
|
4859
5575
|
pool: opts.pool,
|
|
@@ -4867,6 +5583,7 @@ async function runModeUpgrade(request, opts) {
|
|
|
4867
5583
|
let chunkCount = 0;
|
|
4868
5584
|
try {
|
|
4869
5585
|
if (request.mode === "graph" && opts.graphStage) {
|
|
5586
|
+
progress(opts.hooks, request, opts.displayName, opts.documentId, "graph", "started");
|
|
4870
5587
|
graphUnits2 = Number(
|
|
4871
5588
|
await maybeAwait(
|
|
4872
5589
|
opts.graphStage({
|
|
@@ -4877,6 +5594,9 @@ async function runModeUpgrade(request, opts) {
|
|
|
4877
5594
|
})
|
|
4878
5595
|
) || 0
|
|
4879
5596
|
);
|
|
5597
|
+
progress(opts.hooks, request, opts.displayName, opts.documentId, "graph", "done", {
|
|
5598
|
+
graph_units: graphUnits2
|
|
5599
|
+
});
|
|
4880
5600
|
}
|
|
4881
5601
|
chunkCount = await upgradeMode(opts.pool, opts.documentId, request.mode ?? "hybrid");
|
|
4882
5602
|
} catch (exc) {
|
|
@@ -4966,6 +5686,7 @@ async function runIngest(request, opts) {
|
|
|
4966
5686
|
);
|
|
4967
5687
|
}
|
|
4968
5688
|
}
|
|
5689
|
+
progress(opts.hooks, request, displayName, null, "extract", "started");
|
|
4969
5690
|
const prepared = await prepare({
|
|
4970
5691
|
content,
|
|
4971
5692
|
filename,
|
|
@@ -4974,6 +5695,11 @@ async function runIngest(request, opts) {
|
|
|
4974
5695
|
config: opts.config,
|
|
4975
5696
|
hooks: opts.hooks
|
|
4976
5697
|
});
|
|
5698
|
+
progress(opts.hooks, request, displayName, null, "extract", "done", {
|
|
5699
|
+
pages: prepared.pages,
|
|
5700
|
+
method: extractMethod(prepared, text),
|
|
5701
|
+
mime: prepared.mime
|
|
5702
|
+
});
|
|
4977
5703
|
const ingestHash = sha256Bytes(prepared.text || "");
|
|
4978
5704
|
const decision = await decide(opts.pool, {
|
|
4979
5705
|
request: { ...request, mode },
|
|
@@ -5060,8 +5786,11 @@ async function runIngest(request, opts) {
|
|
|
5060
5786
|
const metaUpdates = { ...request.metaData ?? {} };
|
|
5061
5787
|
metaUpdates.mime_type = prepared.mime;
|
|
5062
5788
|
if (prepared.contentHash) metaUpdates.content_hash = prepared.contentHash;
|
|
5789
|
+
metaUpdates.unreadable_reason = prepared.unreadableReason;
|
|
5790
|
+
metaUpdates.unreadable_pages = prepared.unreadablePages;
|
|
5063
5791
|
const redactionFailed = [];
|
|
5064
5792
|
let documentText;
|
|
5793
|
+
progress(opts.hooks, request, displayName, documentId, "redact", "started");
|
|
5065
5794
|
try {
|
|
5066
5795
|
documentText = redactDocumentText(
|
|
5067
5796
|
prepared.text,
|
|
@@ -5074,6 +5803,9 @@ async function runIngest(request, opts) {
|
|
|
5074
5803
|
await finalizeDocument(opts.pool, documentId, { status: "failed", error: String(exc) });
|
|
5075
5804
|
throw exc;
|
|
5076
5805
|
}
|
|
5806
|
+
progress(opts.hooks, request, displayName, documentId, "redact", "done", {
|
|
5807
|
+
rules_failed: redactionFailed.length
|
|
5808
|
+
});
|
|
5077
5809
|
if (!prepared.text.trim() || prepared.mediaOnly || looksMostlyBoilerplate(prepared.text)) {
|
|
5078
5810
|
const reason = prepared.mediaOnly ? "media-only (no extractable text)" : "no extractable text";
|
|
5079
5811
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, []);
|
|
@@ -5099,7 +5831,13 @@ async function runIngest(request, opts) {
|
|
|
5099
5831
|
graphUnits: 0,
|
|
5100
5832
|
providerTokens: {},
|
|
5101
5833
|
error: reason,
|
|
5102
|
-
redactionFailed
|
|
5834
|
+
redactionFailed,
|
|
5835
|
+
// Extraction RAN here — an unreadable scan is exactly what lands in
|
|
5836
|
+
// this branch, so this is the report that has to say why. The
|
|
5837
|
+
// dedup-skip/upgrade sites stay at the defaults: they never extracted
|
|
5838
|
+
// and would be claiming blind.
|
|
5839
|
+
unreadableReason: prepared.unreadableReason,
|
|
5840
|
+
unreadablePages: prepared.unreadablePages
|
|
5103
5841
|
});
|
|
5104
5842
|
}
|
|
5105
5843
|
const units = computeUnits(prepared);
|
|
@@ -5109,18 +5847,26 @@ async function runIngest(request, opts) {
|
|
|
5109
5847
|
let rows = [];
|
|
5110
5848
|
let effectiveMime = prepared.mime;
|
|
5111
5849
|
try {
|
|
5850
|
+
progress(opts.hooks, request, displayName, documentId, "chunk", "started");
|
|
5112
5851
|
[rows, effectiveMime] = buildChunks(prepared, request.name, {
|
|
5113
5852
|
policy: opts.config.redaction,
|
|
5114
5853
|
secretKey: opts.config.secretKey,
|
|
5115
5854
|
hooks: opts.hooks,
|
|
5116
5855
|
failed: redactionFailed
|
|
5117
5856
|
});
|
|
5857
|
+
progress(opts.hooks, request, displayName, documentId, "chunk", "done", { chunks: rows.length });
|
|
5858
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "started");
|
|
5118
5859
|
if (request.batch) {
|
|
5119
5860
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, rows);
|
|
5120
5861
|
const batchId = await submitEmbeddingBatch(
|
|
5121
5862
|
opts.config.embedding,
|
|
5122
5863
|
rows.map((r) => r.text)
|
|
5123
5864
|
);
|
|
5865
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "done", {
|
|
5866
|
+
batch: true,
|
|
5867
|
+
batch_id: batchId,
|
|
5868
|
+
chunks: rows.length
|
|
5869
|
+
});
|
|
5124
5870
|
metaUpdates.mime_type = effectiveMime;
|
|
5125
5871
|
metaUpdates.batch = {
|
|
5126
5872
|
id: batchId,
|
|
@@ -5150,15 +5896,26 @@ async function runIngest(request, opts) {
|
|
|
5150
5896
|
graphUnits: 0,
|
|
5151
5897
|
providerTokens,
|
|
5152
5898
|
error: null,
|
|
5153
|
-
redactionFailed
|
|
5899
|
+
redactionFailed,
|
|
5900
|
+
// Extraction RAN here just like the skipped/failed/completed sites —
|
|
5901
|
+
// this report has to say why pages were unreadable too, not leave
|
|
5902
|
+
// the caller to re-derive it later from listDocuments.
|
|
5903
|
+
unreadableReason: prepared.unreadableReason,
|
|
5904
|
+
unreadablePages: prepared.unreadablePages
|
|
5154
5905
|
});
|
|
5155
5906
|
}
|
|
5156
5907
|
const embeddingTokens = await embedChunks(opts.embedder, rows);
|
|
5157
5908
|
if (embeddingTokens) providerTokens.embedding_tokens = embeddingTokens;
|
|
5158
5909
|
await recordEmbeddingDim(opts.pool, opts.embedder.dim);
|
|
5159
5910
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, rows);
|
|
5911
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "done", {
|
|
5912
|
+
batch: false,
|
|
5913
|
+
chunks: rows.length,
|
|
5914
|
+
tokens: embeddingTokens
|
|
5915
|
+
});
|
|
5160
5916
|
const structuredKw = {};
|
|
5161
5917
|
if (request.extractStructured && opts.config.llm) {
|
|
5918
|
+
progress(opts.hooks, request, displayName, documentId, "structured", "started");
|
|
5162
5919
|
const extraction = await extractStructuredData(documentText, {
|
|
5163
5920
|
llmCfg: opts.config.llm,
|
|
5164
5921
|
fieldHints: request.fieldHints
|
|
@@ -5199,9 +5956,15 @@ async function runIngest(request, opts) {
|
|
|
5199
5956
|
document: displayName
|
|
5200
5957
|
});
|
|
5201
5958
|
}
|
|
5959
|
+
progress(opts.hooks, request, displayName, documentId, "structured", "done", {
|
|
5960
|
+
document_type: extraction.documentType,
|
|
5961
|
+
keys: extraction.keysNormalized.length,
|
|
5962
|
+
quality: extraction.quality
|
|
5963
|
+
});
|
|
5202
5964
|
}
|
|
5203
5965
|
let graphUnits2 = 0;
|
|
5204
5966
|
if (mode === "graph" && opts.graphStage) {
|
|
5967
|
+
progress(opts.hooks, request, displayName, documentId, "graph", "started");
|
|
5205
5968
|
graphUnits2 = Number(
|
|
5206
5969
|
await maybeAwait(
|
|
5207
5970
|
opts.graphStage({
|
|
@@ -5212,6 +5975,7 @@ async function runIngest(request, opts) {
|
|
|
5212
5975
|
})
|
|
5213
5976
|
) || 0
|
|
5214
5977
|
);
|
|
5978
|
+
progress(opts.hooks, request, displayName, documentId, "graph", "done", { graph_units: graphUnits2 });
|
|
5215
5979
|
}
|
|
5216
5980
|
metaUpdates.mime_type = effectiveMime;
|
|
5217
5981
|
await finalizeDocument(opts.pool, documentId, {
|
|
@@ -5251,7 +6015,9 @@ async function runIngest(request, opts) {
|
|
|
5251
6015
|
graphUnits: graphUnits2,
|
|
5252
6016
|
providerTokens,
|
|
5253
6017
|
error: null,
|
|
5254
|
-
redactionFailed
|
|
6018
|
+
redactionFailed,
|
|
6019
|
+
unreadableReason: prepared.unreadableReason,
|
|
6020
|
+
unreadablePages: prepared.unreadablePages
|
|
5255
6021
|
});
|
|
5256
6022
|
} catch (exc) {
|
|
5257
6023
|
emitError(opts.hooks, exc, { stage: "ingest", document: displayName });
|
|
@@ -5266,7 +6032,9 @@ async function runIngest(request, opts) {
|
|
|
5266
6032
|
graphUnits: 0,
|
|
5267
6033
|
providerTokens,
|
|
5268
6034
|
error: String(exc),
|
|
5269
|
-
redactionFailed
|
|
6035
|
+
redactionFailed,
|
|
6036
|
+
unreadableReason: prepared.unreadableReason,
|
|
6037
|
+
unreadablePages: prepared.unreadablePages
|
|
5270
6038
|
});
|
|
5271
6039
|
}
|
|
5272
6040
|
}
|
|
@@ -5310,17 +6078,11 @@ var init_ingest = __esm({
|
|
|
5310
6078
|
// src/actions.ts
|
|
5311
6079
|
function parseDocumentId(documentId) {
|
|
5312
6080
|
const raw = String(documentId ?? "").replace(/[ \n\t]/g, "").trim();
|
|
5313
|
-
if (!
|
|
6081
|
+
if (!UUID_RE3.test(raw)) {
|
|
5314
6082
|
throw new EngineActionError(`invalid document id: ${JSON.stringify(documentId)}`);
|
|
5315
6083
|
}
|
|
5316
6084
|
return raw;
|
|
5317
6085
|
}
|
|
5318
|
-
function visible(acl, principals) {
|
|
5319
|
-
if (principals === null) return true;
|
|
5320
|
-
if (acl == null) return true;
|
|
5321
|
-
const held = new Set(principals);
|
|
5322
|
-
return acl.some((p) => held.has(p));
|
|
5323
|
-
}
|
|
5324
6086
|
function scopeSql(sourceIds, principals, params) {
|
|
5325
6087
|
const where = [];
|
|
5326
6088
|
if (sourceIds != null) {
|
|
@@ -5409,7 +6171,9 @@ async function getDocumentRow(pool, documentId, principals) {
|
|
|
5409
6171
|
createdAt: doc.created_at,
|
|
5410
6172
|
updatedAt: doc.updated_at,
|
|
5411
6173
|
startedAt: doc.started_at,
|
|
5412
|
-
completedAt: doc.completed_at
|
|
6174
|
+
completedAt: doc.completed_at,
|
|
6175
|
+
unreadableReason: doc.meta_data?.unreadable_reason ?? null,
|
|
6176
|
+
unreadablePages: doc.meta_data?.unreadable_pages ?? 0
|
|
5413
6177
|
};
|
|
5414
6178
|
}
|
|
5415
6179
|
async function stats(pool, sourceId) {
|
|
@@ -5458,7 +6222,7 @@ async function listDocuments(opts) {
|
|
|
5458
6222
|
let where = scopeSql(sourceIds, principals, params);
|
|
5459
6223
|
const cursorTime = parsedCursor?.time ?? parsedCursor?.created_at;
|
|
5460
6224
|
const cursorId = parsedCursor?.id;
|
|
5461
|
-
if (cursorTime && cursorId &&
|
|
6225
|
+
if (cursorTime && cursorId && UUID_RE3.test(String(cursorId))) {
|
|
5462
6226
|
params.push(cursorTime, cursorId);
|
|
5463
6227
|
const extra = `(created_at < $${params.length - 1} OR (created_at = $${params.length - 1} AND id < $${params.length}::uuid))`;
|
|
5464
6228
|
where = where ? `${where} AND ${extra}` : `WHERE ${extra}`;
|
|
@@ -5466,7 +6230,7 @@ async function listDocuments(opts) {
|
|
|
5466
6230
|
params.push(limit + 1);
|
|
5467
6231
|
const { rows } = await opts.pool.query(
|
|
5468
6232
|
`SELECT id, source_id, external_id, name, description, mode, mime_type, lang, status,
|
|
5469
|
-
document_type, created_at, updated_at, acl
|
|
6233
|
+
document_type, created_at, updated_at, acl, meta_data
|
|
5470
6234
|
FROM context_engine_documents
|
|
5471
6235
|
${where}
|
|
5472
6236
|
ORDER BY created_at DESC, id DESC
|
|
@@ -5491,7 +6255,15 @@ async function listDocuments(opts) {
|
|
|
5491
6255
|
hooks: opts.hooks
|
|
5492
6256
|
}),
|
|
5493
6257
|
createdAt: doc.created_at instanceof Date ? doc.created_at.toISOString() : doc.created_at,
|
|
5494
|
-
updatedAt: doc.updated_at instanceof Date ? doc.updated_at.toISOString() : doc.updated_at
|
|
6258
|
+
updatedAt: doc.updated_at instanceof Date ? doc.updated_at.toISOString() : doc.updated_at,
|
|
6259
|
+
unreadableReason: doc.meta_data?.unreadable_reason ?? null,
|
|
6260
|
+
unreadablePages: doc.meta_data?.unreadable_pages ?? 0,
|
|
6261
|
+
// WHO can see this. Stored, enforced on every query and editable through
|
|
6262
|
+
// updateDocument — and, until this line, invisible to every caller,
|
|
6263
|
+
// because this serializer builds a FIXED object. `null` is UNRESTRICTED
|
|
6264
|
+
// and must stay null; an empty array would read as "nobody", which is the
|
|
6265
|
+
// opposite claim.
|
|
6266
|
+
acl: doc.acl ?? null
|
|
5495
6267
|
}));
|
|
5496
6268
|
const result = {
|
|
5497
6269
|
documents,
|
|
@@ -5649,9 +6421,9 @@ async function compute(instruction, opts) {
|
|
|
5649
6421
|
return `mime_type ILIKE $${params.length}`;
|
|
5650
6422
|
}).join(" OR ");
|
|
5651
6423
|
where = where ? `${where} AND (${mimeClause})` : `WHERE (${mimeClause})`;
|
|
5652
|
-
if (opts.
|
|
6424
|
+
if (opts.documentIds?.length) {
|
|
5653
6425
|
const parsedIds = [];
|
|
5654
|
-
for (const did of opts.
|
|
6426
|
+
for (const did of opts.documentIds) {
|
|
5655
6427
|
try {
|
|
5656
6428
|
parsedIds.push(parseDocumentId(did));
|
|
5657
6429
|
} catch {
|
|
@@ -5679,13 +6451,13 @@ async function compute(instruction, opts) {
|
|
|
5679
6451
|
}
|
|
5680
6452
|
if (tabular.length > MAX_COMPUTE_DOCUMENTS) {
|
|
5681
6453
|
throw new EngineActionError(
|
|
5682
|
-
`more than ${MAX_COMPUTE_DOCUMENTS} tabular documents are in scope for compute() \u2014 narrow the request with
|
|
6454
|
+
`more than ${MAX_COMPUTE_DOCUMENTS} tabular documents are in scope for compute() \u2014 narrow the request with documentIds or sourceIds`
|
|
5683
6455
|
);
|
|
5684
6456
|
}
|
|
5685
6457
|
const totalChars = tabular.reduce((n, d) => n + String(d.text ?? "").length, 0);
|
|
5686
6458
|
if (totalChars > MAX_COMPUTE_TEXT_CHARS) {
|
|
5687
6459
|
throw new EngineActionError(
|
|
5688
|
-
`in-scope spreadsheet text too large to load (${totalChars} chars > ${MAX_COMPUTE_TEXT_CHARS} cap) \u2014 narrow the request with
|
|
6460
|
+
`in-scope spreadsheet text too large to load (${totalChars} chars > ${MAX_COMPUTE_TEXT_CHARS} cap) \u2014 narrow the request with documentIds or sourceIds`
|
|
5689
6461
|
);
|
|
5690
6462
|
}
|
|
5691
6463
|
const dfs = {};
|
|
@@ -5766,7 +6538,7 @@ ${schemaLines.join("\n")}`;
|
|
|
5766
6538
|
hooks: opts.hooks
|
|
5767
6539
|
});
|
|
5768
6540
|
}
|
|
5769
|
-
var MAX_LIST_LIMIT, MAX_COMPUTE_TEXT_CHARS, DEFAULT_COMPUTE_TIMEOUT, MAX_COMPUTE_DOCUMENTS, TABULAR_MIME_PATTERNS,
|
|
6541
|
+
var MAX_LIST_LIMIT, MAX_COMPUTE_TEXT_CHARS, DEFAULT_COMPUTE_TIMEOUT, MAX_COMPUTE_DOCUMENTS, TABULAR_MIME_PATTERNS, UUID_RE3, WORD_RE, STOPWORDS, SHEET_MARKER_RE2, CODE_FENCE_RE, COMPUTE_SYSTEM_PROMPT, visible;
|
|
5770
6542
|
var init_actions = __esm({
|
|
5771
6543
|
"src/actions.ts"() {
|
|
5772
6544
|
init_chunkers();
|
|
@@ -5776,13 +6548,14 @@ var init_actions = __esm({
|
|
|
5776
6548
|
init_redaction();
|
|
5777
6549
|
init_sandbox();
|
|
5778
6550
|
init_structured();
|
|
6551
|
+
init_acl();
|
|
5779
6552
|
init_ingest();
|
|
5780
6553
|
MAX_LIST_LIMIT = 200;
|
|
5781
6554
|
MAX_COMPUTE_TEXT_CHARS = 2e6;
|
|
5782
6555
|
DEFAULT_COMPUTE_TIMEOUT = 30;
|
|
5783
6556
|
MAX_COMPUTE_DOCUMENTS = 50;
|
|
5784
6557
|
TABULAR_MIME_PATTERNS = ["%csv%", "%sheet%", "%excel%", "%spreadsheetml%", "%tab-separated%"];
|
|
5785
|
-
|
|
6558
|
+
UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5786
6559
|
WORD_RE = /[^\W\d_]+/gu;
|
|
5787
6560
|
STOPWORDS = /* @__PURE__ */ new Set([
|
|
5788
6561
|
"the",
|
|
@@ -5861,6 +6634,7 @@ Rules:
|
|
|
5861
6634
|
1. Set a variable named \`result\` to the final answer (a number, string, array, or object).
|
|
5862
6635
|
2. No file I/O, no network calls, no imports, no require, no process.
|
|
5863
6636
|
3. Return ONLY the JavaScript code \u2014 no markdown fences, no explanation, no commentary.`;
|
|
6637
|
+
visible = aclVisible;
|
|
5864
6638
|
}
|
|
5865
6639
|
});
|
|
5866
6640
|
function estimatedTokens(texts) {
|
|
@@ -5899,7 +6673,7 @@ async function loadGeminiEmbedClient(apiKey) {
|
|
|
5899
6673
|
if (!Ctor) {
|
|
5900
6674
|
throw new ExtraMissingError("gemini", specifier, "gemini embeddings");
|
|
5901
6675
|
}
|
|
5902
|
-
return new Ctor({ apiKey: apiKey ?? null });
|
|
6676
|
+
return new Ctor({ apiKey: apiKey ?? null, httpOptions: { timeout: TIMEOUT_MS3 } });
|
|
5903
6677
|
}
|
|
5904
6678
|
function buildEmbedder(cfg2, opts) {
|
|
5905
6679
|
if (OPENAI_FAMILY2.has(cfg2.provider)) {
|
|
@@ -7447,10 +8221,10 @@ async function runLegs(query, opts) {
|
|
|
7447
8221
|
return ranked;
|
|
7448
8222
|
}
|
|
7449
8223
|
async function hydrate(pool, chunkIds) {
|
|
7450
|
-
const parsed = chunkIds.filter((id) =>
|
|
8224
|
+
const parsed = chunkIds.filter((id) => UUID_RE4.test(id));
|
|
7451
8225
|
if (parsed.length !== chunkIds.length) {
|
|
7452
8226
|
for (const cid of chunkIds) {
|
|
7453
|
-
if (!
|
|
8227
|
+
if (!UUID_RE4.test(cid)) {
|
|
7454
8228
|
console.warn("search: skipping unhydratable chunk id %s", cid);
|
|
7455
8229
|
}
|
|
7456
8230
|
}
|
|
@@ -7549,6 +8323,7 @@ async function runSearch(query, opts) {
|
|
|
7549
8323
|
const topK = Math.max(1, Math.trunc(opts.topK ?? 10));
|
|
7550
8324
|
const scope = {
|
|
7551
8325
|
sourceIds: opts.sourceIds != null ? [...opts.sourceIds] : null,
|
|
8326
|
+
documentIds: opts.documentIds != null ? [...opts.documentIds] : null,
|
|
7552
8327
|
principals: opts.principals != null ? [...opts.principals] : null,
|
|
7553
8328
|
limit: legLimit(topK, opts.config)
|
|
7554
8329
|
};
|
|
@@ -7569,13 +8344,8 @@ async function runSearch(query, opts) {
|
|
|
7569
8344
|
let degraded = null;
|
|
7570
8345
|
if (mode === "graph" && !graphLeg) {
|
|
7571
8346
|
degraded = "graph_leg_unavailable";
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
new GraphLegUnavailable(
|
|
7575
|
-
"search(mode='graph') ran the hybrid legs only: no graph ranked list was supplied (the graph retrieval leg is not implemented yet). Results are hybrid and billed as hybrid."
|
|
7576
|
-
),
|
|
7577
|
-
{ stage: "graph_leg", mode, degraded }
|
|
7578
|
-
);
|
|
8347
|
+
const reason = opts.graphRanked?.length ? "search(mode='graph') ran the hybrid legs only: the supplied graph ranked list was filtered to nothing by the search scope (source/document/ACL) \u2014 every candidate lies outside it. Results are hybrid and billed as hybrid." : "search(mode='graph') ran the hybrid legs only: graph retrieval supplied no candidates. Results are hybrid and billed as hybrid.";
|
|
8348
|
+
emitError(opts.hooks, new GraphLegUnavailable(reason), { stage: "graph_leg", mode, degraded });
|
|
7579
8349
|
}
|
|
7580
8350
|
const fused = rrfFuse(ranked, { k: opts.config.fusion.k, weights: opts.config.fusion.weights });
|
|
7581
8351
|
const window = opts.config.reranker.enabled ? Math.max(topK, opts.config.reranker.candidates) : topK;
|
|
@@ -7649,7 +8419,7 @@ async function runSearch(query, opts) {
|
|
|
7649
8419
|
});
|
|
7650
8420
|
return { hits, usage };
|
|
7651
8421
|
}
|
|
7652
|
-
var MIN_LEG_CANDIDATES, MAX_LEG_CANDIDATES, UNITS_PER_SCOPE_HYBRID, UNITS_PER_SCOPE_GRAPH,
|
|
8422
|
+
var MIN_LEG_CANDIDATES, MAX_LEG_CANDIDATES, UNITS_PER_SCOPE_HYBRID, UNITS_PER_SCOPE_GRAPH, UUID_RE4;
|
|
7653
8423
|
var init_search = __esm({
|
|
7654
8424
|
"src/search.ts"() {
|
|
7655
8425
|
init_compression();
|
|
@@ -7662,37 +8432,7 @@ var init_search = __esm({
|
|
|
7662
8432
|
MAX_LEG_CANDIDATES = 500;
|
|
7663
8433
|
UNITS_PER_SCOPE_HYBRID = 1;
|
|
7664
8434
|
UNITS_PER_SCOPE_GRAPH = 5;
|
|
7665
|
-
|
|
7666
|
-
}
|
|
7667
|
-
});
|
|
7668
|
-
|
|
7669
|
-
// src/sentinels.ts
|
|
7670
|
-
function resolvePrincipals(value, method) {
|
|
7671
|
-
if (value === TRUSTED) return null;
|
|
7672
|
-
if (value === void 0) return null;
|
|
7673
|
-
if (value === null) {
|
|
7674
|
-
process.emitWarning(
|
|
7675
|
-
`${method}(principals=null) means TRUSTED CALLER \u2014 access control is disabled and every document is returned. If that is what you want, pass principals=TRUSTED (from @promptev/context-engine) to say so explicitly. If you meant 'no authenticated user', pass principals=[] instead; null returns the entire corpus. Passing null will raise in 1.0.`,
|
|
7676
|
-
"DeprecationWarning"
|
|
7677
|
-
);
|
|
7678
|
-
return null;
|
|
7679
|
-
}
|
|
7680
|
-
return Array.isArray(value) ? value : null;
|
|
7681
|
-
}
|
|
7682
|
-
var UNSET, TrustedSentinel, TRUSTED;
|
|
7683
|
-
var init_sentinels = __esm({
|
|
7684
|
-
"src/sentinels.ts"() {
|
|
7685
|
-
UNSET = /* @__PURE__ */ Symbol.for("context_engine.UNSET");
|
|
7686
|
-
TrustedSentinel = class {
|
|
7687
|
-
[Symbol.toStringTag] = "TRUSTED";
|
|
7688
|
-
toString() {
|
|
7689
|
-
return "TRUSTED";
|
|
7690
|
-
}
|
|
7691
|
-
valueOf() {
|
|
7692
|
-
return true;
|
|
7693
|
-
}
|
|
7694
|
-
};
|
|
7695
|
-
TRUSTED = Object.freeze(new TrustedSentinel());
|
|
8435
|
+
UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7696
8436
|
}
|
|
7697
8437
|
});
|
|
7698
8438
|
function isSpacelessQuery(query) {
|
|
@@ -7735,7 +8475,7 @@ FROM context_engine_chunks c
|
|
|
7735
8475
|
WHERE c.embedding IS NOT NULL
|
|
7736
8476
|
${SCOPE}
|
|
7737
8477
|
ORDER BY c.embedding <=> ${vec}
|
|
7738
|
-
LIMIT $
|
|
8478
|
+
LIMIT $4
|
|
7739
8479
|
`;
|
|
7740
8480
|
}
|
|
7741
8481
|
function sqlAnnExact(vec) {
|
|
@@ -7748,13 +8488,13 @@ WITH eligible AS MATERIALIZED (
|
|
|
7748
8488
|
)
|
|
7749
8489
|
SELECT id::text FROM eligible
|
|
7750
8490
|
ORDER BY embedding <=> ${vec}
|
|
7751
|
-
LIMIT $
|
|
8491
|
+
LIMIT $4
|
|
7752
8492
|
`;
|
|
7753
8493
|
}
|
|
7754
8494
|
function idsOf(rows) {
|
|
7755
8495
|
return rows.map((r) => String(r.id));
|
|
7756
8496
|
}
|
|
7757
|
-
var MIN_ITERATIVE_SCAN_VERSION, ANN_MIN_EF_SEARCH, ANN_MAX_EF_SEARCH, ANN_SCAN_FRACTION, ANN_SCAN_PER_ROW, ANN_MIN_SCAN_TUPLES, ANN_MAX_SCAN_TUPLES, ANN_SCAN_MEM_MULTIPLIER, ANN_EXACT_THRESHOLD, SCOPE, SQL_FTS, SQL_TRGM, SQL_COUNT_ELIGIBLE2, SQL_FILTER_IDS,
|
|
8497
|
+
var MIN_ITERATIVE_SCAN_VERSION, ANN_MIN_EF_SEARCH, ANN_MAX_EF_SEARCH, ANN_SCAN_FRACTION, ANN_SCAN_PER_ROW, ANN_MIN_SCAN_TUPLES, ANN_MAX_SCAN_TUPLES, ANN_SCAN_MEM_MULTIPLIER, ANN_EXACT_THRESHOLD, SCOPE, SQL_FTS, SQL_TRGM, SQL_COUNT_ELIGIBLE2, SQL_FILTER_IDS, UUID_RE5, PostgresBackend;
|
|
7758
8498
|
var init_storage = __esm({
|
|
7759
8499
|
"src/storage.ts"() {
|
|
7760
8500
|
init_text();
|
|
@@ -7769,25 +8509,26 @@ var init_storage = __esm({
|
|
|
7769
8509
|
ANN_EXACT_THRESHOLD = 5e4;
|
|
7770
8510
|
SCOPE = `
|
|
7771
8511
|
AND ($1::text[] IS NULL OR c.source_id = ANY($1))
|
|
7772
|
-
AND ($2::
|
|
8512
|
+
AND ($2::uuid[] IS NULL OR c.document_id = ANY($2::uuid[]))
|
|
8513
|
+
AND ($3::text[] IS NULL OR c.acl IS NULL OR c.acl && $3)
|
|
7773
8514
|
`;
|
|
7774
8515
|
SQL_FTS = `
|
|
7775
8516
|
SELECT c.id::text
|
|
7776
8517
|
FROM context_engine_chunks c
|
|
7777
8518
|
WHERE c.text IS NOT NULL
|
|
7778
|
-
AND c.text_search @@ websearch_to_tsquery('simple'::regconfig, $
|
|
8519
|
+
AND c.text_search @@ websearch_to_tsquery('simple'::regconfig, $4)
|
|
7779
8520
|
${SCOPE}
|
|
7780
|
-
ORDER BY ts_rank_cd(c.text_search, websearch_to_tsquery('simple'::regconfig, $
|
|
7781
|
-
LIMIT $
|
|
8521
|
+
ORDER BY ts_rank_cd(c.text_search, websearch_to_tsquery('simple'::regconfig, $4)) DESC, c.id
|
|
8522
|
+
LIMIT $5
|
|
7782
8523
|
`;
|
|
7783
8524
|
SQL_TRGM = `
|
|
7784
8525
|
SELECT c.id::text
|
|
7785
8526
|
FROM context_engine_chunks c
|
|
7786
8527
|
WHERE c.text IS NOT NULL
|
|
7787
|
-
AND c.text_trgm_norm % $
|
|
8528
|
+
AND c.text_trgm_norm % $4
|
|
7788
8529
|
${SCOPE}
|
|
7789
|
-
ORDER BY similarity(c.text_trgm_norm, $
|
|
7790
|
-
LIMIT $
|
|
8530
|
+
ORDER BY similarity(c.text_trgm_norm, $4) DESC, c.id
|
|
8531
|
+
LIMIT $5
|
|
7791
8532
|
`;
|
|
7792
8533
|
SQL_COUNT_ELIGIBLE2 = `
|
|
7793
8534
|
SELECT count(*)::int AS count FROM (
|
|
@@ -7795,16 +8536,16 @@ SELECT count(*)::int AS count FROM (
|
|
|
7795
8536
|
FROM context_engine_chunks c
|
|
7796
8537
|
WHERE c.embedding IS NOT NULL
|
|
7797
8538
|
${SCOPE}
|
|
7798
|
-
LIMIT $
|
|
8539
|
+
LIMIT $4
|
|
7799
8540
|
) probe
|
|
7800
8541
|
`;
|
|
7801
8542
|
SQL_FILTER_IDS = `
|
|
7802
8543
|
SELECT c.id::text
|
|
7803
8544
|
FROM context_engine_chunks c
|
|
7804
|
-
WHERE c.id = ANY($
|
|
8545
|
+
WHERE c.id = ANY($4::uuid[])
|
|
7805
8546
|
${SCOPE}
|
|
7806
8547
|
`;
|
|
7807
|
-
|
|
8548
|
+
UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7808
8549
|
PostgresBackend = class {
|
|
7809
8550
|
supportsFts = true;
|
|
7810
8551
|
supportsTrgm = true;
|
|
@@ -7826,24 +8567,26 @@ WHERE c.id = ANY($3::uuid[])
|
|
|
7826
8567
|
* mean different things.
|
|
7827
8568
|
*/
|
|
7828
8569
|
scopeParams(scope) {
|
|
7829
|
-
return
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
8570
|
+
return {
|
|
8571
|
+
binds: [
|
|
8572
|
+
scope.sourceIds != null ? [...scope.sourceIds] : null,
|
|
8573
|
+
scope.documentIds != null ? [...scope.documentIds] : null,
|
|
8574
|
+
scope.principals != null ? [...scope.principals] : null
|
|
8575
|
+
],
|
|
8576
|
+
lim: Math.max(1, Math.trunc(scope.limit ?? 50))
|
|
8577
|
+
};
|
|
7834
8578
|
}
|
|
7835
8579
|
async upsertChunks(documentId, sourceId, acl, chunks) {
|
|
7836
8580
|
const client = await this.pool.connect();
|
|
7837
8581
|
try {
|
|
7838
8582
|
await client.query("BEGIN");
|
|
7839
8583
|
await client.query("DELETE FROM context_engine_chunks WHERE document_id = $1", [documentId]);
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
[
|
|
8584
|
+
const BATCH = 500;
|
|
8585
|
+
for (let start = 0; start < chunks.length; start += BATCH) {
|
|
8586
|
+
const batch = chunks.slice(start, start + BATCH);
|
|
8587
|
+
const params = [];
|
|
8588
|
+
const rows = batch.map((c) => {
|
|
8589
|
+
params.push(
|
|
7847
8590
|
randomUUID(),
|
|
7848
8591
|
documentId,
|
|
7849
8592
|
sourceId,
|
|
@@ -7852,7 +8595,16 @@ WHERE c.id = ANY($3::uuid[])
|
|
|
7852
8595
|
c.text,
|
|
7853
8596
|
c.lang ?? null,
|
|
7854
8597
|
JSON.stringify(c.meta ?? {})
|
|
7855
|
-
|
|
8598
|
+
);
|
|
8599
|
+
const p = params.length;
|
|
8600
|
+
const emb = c.embedding != null ? vectorLiteral2(c.embedding) : "NULL";
|
|
8601
|
+
return `($${p - 7}, $${p - 6}, $${p - 5}, $${p - 4}, $${p - 3}, $${p - 2}, $${p - 1}, ${emb}, $${p}::jsonb)`;
|
|
8602
|
+
});
|
|
8603
|
+
await client.query(
|
|
8604
|
+
`INSERT INTO context_engine_chunks
|
|
8605
|
+
(id, document_id, source_id, acl, idx, text, lang, embedding, meta_data)
|
|
8606
|
+
VALUES ${rows.join(", ")}`,
|
|
8607
|
+
params
|
|
7856
8608
|
);
|
|
7857
8609
|
}
|
|
7858
8610
|
await client.query("COMMIT");
|
|
@@ -7912,24 +8664,24 @@ WHERE c.id = ANY($3::uuid[])
|
|
|
7912
8664
|
const parsed = [];
|
|
7913
8665
|
for (const cid of chunkIds) {
|
|
7914
8666
|
try {
|
|
7915
|
-
if (
|
|
8667
|
+
if (UUID_RE5.test(String(cid))) parsed.push(String(cid));
|
|
7916
8668
|
} catch {
|
|
7917
8669
|
}
|
|
7918
8670
|
}
|
|
7919
8671
|
if (!parsed.length) return [];
|
|
7920
|
-
const
|
|
7921
|
-
const { rows } = await this.pool.query(SQL_FILTER_IDS, [
|
|
8672
|
+
const { binds } = this.scopeParams(scope);
|
|
8673
|
+
const { rows } = await this.pool.query(SQL_FILTER_IDS, [...binds, parsed]);
|
|
7922
8674
|
const visible2 = new Set(idsOf(rows));
|
|
7923
8675
|
return chunkIds.filter((cid) => visible2.has(String(cid))).map(String);
|
|
7924
8676
|
}
|
|
7925
8677
|
async ftsSearch(query, scope) {
|
|
7926
|
-
const
|
|
7927
|
-
const { rows } = await this.pool.query(SQL_FTS, [
|
|
8678
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
8679
|
+
const { rows } = await this.pool.query(SQL_FTS, [...binds, query, lim]);
|
|
7928
8680
|
return idsOf(rows);
|
|
7929
8681
|
}
|
|
7930
8682
|
async trgmSearch(query, scope) {
|
|
7931
8683
|
const threshold = trigramThreshold(query);
|
|
7932
|
-
const
|
|
8684
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
7933
8685
|
const client = await this.pool.connect();
|
|
7934
8686
|
let previous = null;
|
|
7935
8687
|
let usedSetLimit = false;
|
|
@@ -7938,7 +8690,7 @@ WHERE c.id = ANY($3::uuid[])
|
|
|
7938
8690
|
const applied = await this.applyTrgmThreshold(client, threshold);
|
|
7939
8691
|
previous = applied.previous;
|
|
7940
8692
|
usedSetLimit = applied.usedSetLimit;
|
|
7941
|
-
const { rows } = await client.query(SQL_TRGM, [
|
|
8693
|
+
const { rows } = await client.query(SQL_TRGM, [...binds, query, lim]);
|
|
7942
8694
|
if (usedSetLimit) await this.restoreTrgmLimit(client, previous);
|
|
7943
8695
|
await client.query("COMMIT");
|
|
7944
8696
|
return idsOf(rows);
|
|
@@ -7984,18 +8736,18 @@ WHERE c.id = ANY($3::uuid[])
|
|
|
7984
8736
|
}
|
|
7985
8737
|
async annSearch(vector, scope) {
|
|
7986
8738
|
const literal = vectorLiteral2(vector);
|
|
7987
|
-
const
|
|
7988
|
-
const scoped =
|
|
8739
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
8740
|
+
const scoped = binds.some((v) => v !== null);
|
|
7989
8741
|
const client = await this.pool.connect();
|
|
7990
8742
|
try {
|
|
7991
8743
|
await client.query("BEGIN");
|
|
7992
|
-
if (scoped && await this.eligibleIsSmall(client,
|
|
7993
|
-
const { rows: rows2 } = await client.query(sqlAnnExact(literal), [
|
|
8744
|
+
if (scoped && await this.eligibleIsSmall(client, binds)) {
|
|
8745
|
+
const { rows: rows2 } = await client.query(sqlAnnExact(literal), [...binds, lim]);
|
|
7994
8746
|
await client.query("COMMIT");
|
|
7995
8747
|
return idsOf(rows2);
|
|
7996
8748
|
}
|
|
7997
8749
|
await this.tuneAnnScan(client, lim, scoped);
|
|
7998
|
-
const { rows } = await client.query(sqlAnn(literal), [
|
|
8750
|
+
const { rows } = await client.query(sqlAnn(literal), [...binds, lim]);
|
|
7999
8751
|
await client.query("COMMIT");
|
|
8000
8752
|
return idsOf(rows);
|
|
8001
8753
|
} catch (err) {
|
|
@@ -8008,11 +8760,17 @@ WHERE c.id = ANY($3::uuid[])
|
|
|
8008
8760
|
client.release();
|
|
8009
8761
|
}
|
|
8010
8762
|
}
|
|
8011
|
-
async eligibleIsSmall(client,
|
|
8763
|
+
async eligibleIsSmall(client, binds) {
|
|
8012
8764
|
const cap = this.exactThreshold + 1;
|
|
8013
|
-
const { rows } = await client.query(SQL_COUNT_ELIGIBLE2, [
|
|
8765
|
+
const { rows } = await client.query(SQL_COUNT_ELIGIBLE2, [...binds, cap]);
|
|
8014
8766
|
return Number(rows[0]?.count ?? 0) <= this.exactThreshold;
|
|
8015
8767
|
}
|
|
8768
|
+
/** Size the HNSW candidate budget, and make it iterative when filtered.
|
|
8769
|
+
*
|
|
8770
|
+
* Public because the graph leg's seed query (`graph/retrieval.ts`) is the
|
|
8771
|
+
* same shape — an ANN walk with the scope predicate as a POST-filter — and
|
|
8772
|
+
* must not carry a second copy of this. Call it inside a transaction on the
|
|
8773
|
+
* client the query itself will run on: every setting here is `SET LOCAL`. */
|
|
8016
8774
|
async tuneAnnScan(client, limit, scoped) {
|
|
8017
8775
|
const efSearch = Math.min(Math.max(Math.trunc(limit) * 4, ANN_MIN_EF_SEARCH), ANN_MAX_EF_SEARCH);
|
|
8018
8776
|
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
@@ -8107,19 +8865,6 @@ var init_function = __esm({
|
|
|
8107
8865
|
"src/tools/executors/function.ts"() {
|
|
8108
8866
|
}
|
|
8109
8867
|
});
|
|
8110
|
-
|
|
8111
|
-
// src/tools/acl.ts
|
|
8112
|
-
function aclVisible(acl, principals) {
|
|
8113
|
-
if (principals === null) return true;
|
|
8114
|
-
if (acl == null) return true;
|
|
8115
|
-
if (!acl.length) return false;
|
|
8116
|
-
const held = new Set(principals ?? []);
|
|
8117
|
-
return acl.some((p) => held.has(p));
|
|
8118
|
-
}
|
|
8119
|
-
var init_acl = __esm({
|
|
8120
|
-
"src/tools/acl.ts"() {
|
|
8121
|
-
}
|
|
8122
|
-
});
|
|
8123
8868
|
function coerceValue(raw) {
|
|
8124
8869
|
const trimmed = raw.trim();
|
|
8125
8870
|
if (trimmed.length >= 2 && trimmed[0] === trimmed[trimmed.length - 1] && (trimmed[0] === "'" || trimmed[0] === '"')) {
|
|
@@ -8166,45 +8911,121 @@ function shouldRequireApproval(ct, args) {
|
|
|
8166
8911
|
if (!condition) return false;
|
|
8167
8912
|
return evaluateCondition(condition, args ?? {});
|
|
8168
8913
|
}
|
|
8169
|
-
|
|
8914
|
+
function rowToRecord(row) {
|
|
8915
|
+
return {
|
|
8916
|
+
id: String(row.id),
|
|
8917
|
+
toolName: String(row.tool_name),
|
|
8918
|
+
toolArgsFrozen: row.tool_args_frozen ?? {},
|
|
8919
|
+
sourceId: row.source_id ?? null,
|
|
8920
|
+
approvalScope: row.approval_scope ?? null,
|
|
8921
|
+
principals: row.principals,
|
|
8922
|
+
status: String(row.status),
|
|
8923
|
+
approver: row.approver ?? null,
|
|
8924
|
+
approverMeta: row.approver_meta ?? null,
|
|
8925
|
+
expiresAt: toDate(row.expires_at),
|
|
8926
|
+
resolvedAt: toDate(row.resolved_at),
|
|
8927
|
+
createdAt: toDate(row.created_at)
|
|
8928
|
+
};
|
|
8929
|
+
}
|
|
8930
|
+
function toDate(value) {
|
|
8931
|
+
if (value == null) return null;
|
|
8932
|
+
if (value instanceof Date) return value;
|
|
8933
|
+
return new Date(String(value));
|
|
8934
|
+
}
|
|
8935
|
+
function claimVisibilitySql(principals, paramIndex) {
|
|
8936
|
+
if (principals === null || principals === TRUSTED) return { sql: "TRUE", params: [] };
|
|
8937
|
+
const noWall = `principals IS NULL OR principals = 'null'::jsonb OR principals = '[]'::jsonb`;
|
|
8938
|
+
principals = principals;
|
|
8939
|
+
if (!principals.length) return { sql: `(${noWall})`, params: [] };
|
|
8940
|
+
return { sql: `(${noWall} OR principals ?| $${paramIndex}::text[])`, params: [principals] };
|
|
8941
|
+
}
|
|
8942
|
+
function validateApprovalScope(approvalScope) {
|
|
8943
|
+
if (approvalScope === null || approvalScope === void 0) return null;
|
|
8944
|
+
if (typeof approvalScope !== "string") {
|
|
8945
|
+
throw new TypeError(`approvalScope must be a non-empty string or null, got ${typeof approvalScope}`);
|
|
8946
|
+
}
|
|
8947
|
+
if (!approvalScope) throw new Error("approvalScope must be a non-empty string or null, got ''");
|
|
8948
|
+
return approvalScope;
|
|
8949
|
+
}
|
|
8950
|
+
function pendingRow(opts, approvalScope) {
|
|
8170
8951
|
const policy = opts.policy ?? {};
|
|
8171
8952
|
const frozen = structuredClone(opts.args);
|
|
8172
8953
|
const createdAt = opts.now ?? /* @__PURE__ */ new Date();
|
|
8173
8954
|
const timeout = Number(policy.timeout_minutes ?? DEFAULT_TIMEOUT_MINUTES);
|
|
8174
8955
|
const expiresAt = new Date(createdAt.getTime() + timeout * 6e4);
|
|
8956
|
+
const principals = opts.principals === void 0 || opts.principals === TRUSTED ? null : opts.principals;
|
|
8957
|
+
return { frozen, createdAt, expiresAt, principals, approvalScope };
|
|
8958
|
+
}
|
|
8959
|
+
async function insertPending(engine, opts, row) {
|
|
8175
8960
|
const id = randomUUID();
|
|
8176
8961
|
await engine.pool.query(
|
|
8177
8962
|
`INSERT INTO context_engine_tool_approvals
|
|
8178
|
-
(id, tool_name, tool_args_frozen, source_id, principals, status, expires_at, created_at)
|
|
8179
|
-
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,'pending',$
|
|
8963
|
+
(id, tool_name, tool_args_frozen, source_id, approval_scope, principals, status, expires_at, created_at)
|
|
8964
|
+
VALUES ($1,$2,$3::jsonb,$4,$5,$6::jsonb,'pending',$7,$8)`,
|
|
8180
8965
|
[
|
|
8181
8966
|
id,
|
|
8182
8967
|
opts.toolName,
|
|
8183
|
-
JSON.stringify(frozen),
|
|
8968
|
+
JSON.stringify(row.frozen),
|
|
8184
8969
|
opts.sourceId ?? null,
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8970
|
+
row.approvalScope,
|
|
8971
|
+
row.principals === null ? null : JSON.stringify(row.principals),
|
|
8972
|
+
row.expiresAt,
|
|
8973
|
+
row.createdAt
|
|
8188
8974
|
]
|
|
8189
8975
|
);
|
|
8190
8976
|
return {
|
|
8191
8977
|
id,
|
|
8192
8978
|
toolName: opts.toolName,
|
|
8193
|
-
toolArgsFrozen: frozen,
|
|
8979
|
+
toolArgsFrozen: row.frozen,
|
|
8194
8980
|
sourceId: opts.sourceId ?? null,
|
|
8195
|
-
|
|
8981
|
+
approvalScope: row.approvalScope,
|
|
8982
|
+
principals: row.principals,
|
|
8196
8983
|
status: "pending",
|
|
8197
8984
|
approver: null,
|
|
8198
8985
|
approverMeta: null,
|
|
8199
|
-
expiresAt,
|
|
8986
|
+
expiresAt: row.expiresAt,
|
|
8200
8987
|
resolvedAt: null,
|
|
8201
|
-
createdAt
|
|
8988
|
+
createdAt: row.createdAt
|
|
8202
8989
|
};
|
|
8203
8990
|
}
|
|
8991
|
+
async function createPending(engine, opts) {
|
|
8992
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
8993
|
+
return insertPending(engine, opts, pendingRow(opts, approvalScope));
|
|
8994
|
+
}
|
|
8995
|
+
async function findOrCreatePending(engine, opts) {
|
|
8996
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
8997
|
+
if (approvalScope === null)
|
|
8998
|
+
throw new Error("findOrCreatePending requires an approvalScope; use createPending");
|
|
8999
|
+
const row = pendingRow(opts, approvalScope);
|
|
9000
|
+
const frozenJson = JSON.stringify(row.frozen);
|
|
9001
|
+
const sameCall = `tool_name = $1 AND approval_scope = $2 AND tool_args_frozen = $3::jsonb`;
|
|
9002
|
+
const params = [opts.toolName, approvalScope, frozenJson, row.createdAt];
|
|
9003
|
+
await engine.pool.query(
|
|
9004
|
+
`UPDATE context_engine_tool_approvals SET status = 'expired'
|
|
9005
|
+
WHERE ${sameCall} AND status = 'pending' AND expires_at <= $4`,
|
|
9006
|
+
params
|
|
9007
|
+
);
|
|
9008
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
9009
|
+
const found = await engine.pool.query(
|
|
9010
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
9011
|
+
WHERE ${sameCall} AND status = 'pending' AND (expires_at IS NULL OR expires_at > $4)
|
|
9012
|
+
ORDER BY created_at ASC LIMIT 1`,
|
|
9013
|
+
params
|
|
9014
|
+
);
|
|
9015
|
+
if (found.rows[0]) return rowToRecord(found.rows[0]);
|
|
9016
|
+
try {
|
|
9017
|
+
return await insertPending(engine, opts, row);
|
|
9018
|
+
} catch (exc) {
|
|
9019
|
+
if (exc?.code !== "23505") throw exc;
|
|
9020
|
+
}
|
|
9021
|
+
}
|
|
9022
|
+
throw new Error("findOrCreatePending: lost the insert race twice and found no live pending row");
|
|
9023
|
+
}
|
|
8204
9024
|
var DEFAULT_TIMEOUT_MINUTES, CONDITION_RE;
|
|
8205
9025
|
var init_approval = __esm({
|
|
8206
9026
|
"src/tools/approval.ts"() {
|
|
8207
9027
|
init_errors();
|
|
9028
|
+
init_sentinels();
|
|
8208
9029
|
init_acl();
|
|
8209
9030
|
DEFAULT_TIMEOUT_MINUTES = 60;
|
|
8210
9031
|
CONDITION_RE = /^\s*(?<field>[A-Za-z_][A-Za-z0-9_.]*)\s*(?<op>>=|<=|==|!=|>|<)\s*(?<value>.+?)\s*$/;
|
|
@@ -8315,8 +9136,48 @@ var init_audit = __esm({
|
|
|
8315
9136
|
});
|
|
8316
9137
|
|
|
8317
9138
|
// src/tools/config.ts
|
|
8318
|
-
|
|
8319
|
-
|
|
9139
|
+
function isPlainObject(value) {
|
|
9140
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9141
|
+
}
|
|
9142
|
+
function containsSentinel(value) {
|
|
9143
|
+
if (typeof value === "string") return value === REDACTED_SENTINEL;
|
|
9144
|
+
if (Array.isArray(value)) return value.some(containsSentinel);
|
|
9145
|
+
if (isPlainObject(value)) return Object.values(value).some(containsSentinel);
|
|
9146
|
+
return false;
|
|
9147
|
+
}
|
|
9148
|
+
function mergeRedacted(incoming, stored) {
|
|
9149
|
+
const resolve = (value, kept, keptPresent, path) => {
|
|
9150
|
+
if (isPlainObject(value)) {
|
|
9151
|
+
const keptDict = isPlainObject(kept) ? kept : {};
|
|
9152
|
+
const out2 = {};
|
|
9153
|
+
for (const [k, v] of Object.entries(value)) {
|
|
9154
|
+
out2[k] = resolve(v, keptDict[k], k in keptDict, path ? `${path}.${k}` : k);
|
|
9155
|
+
}
|
|
9156
|
+
return out2;
|
|
9157
|
+
}
|
|
9158
|
+
if (Array.isArray(value)) {
|
|
9159
|
+
const keptList = Array.isArray(kept) ? kept : [];
|
|
9160
|
+
return value.map((v, i) => resolve(v, keptList[i], i < keptList.length, `${path}[${i}]`));
|
|
9161
|
+
}
|
|
9162
|
+
if (value === REDACTED_SENTINEL) {
|
|
9163
|
+
if (!keptPresent) {
|
|
9164
|
+
throw new ConfigTemplateError(
|
|
9165
|
+
`config.${path} is ${JSON.stringify(REDACTED_SENTINEL)} but there is no stored value to keep \u2014 re-enter the secret or omit the field`
|
|
9166
|
+
);
|
|
9167
|
+
}
|
|
9168
|
+
return kept;
|
|
9169
|
+
}
|
|
9170
|
+
return value;
|
|
9171
|
+
};
|
|
9172
|
+
const keptRoot = stored ?? {};
|
|
9173
|
+
const out = {};
|
|
9174
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
9175
|
+
out[k] = resolve(v, keptRoot[k], k in keptRoot, k);
|
|
9176
|
+
}
|
|
9177
|
+
return out;
|
|
9178
|
+
}
|
|
9179
|
+
var KINDS, ToolConfig, REDACTED_SENTINEL, ConfigTemplateError;
|
|
9180
|
+
var init_config2 = __esm({
|
|
8320
9181
|
"src/tools/config.ts"() {
|
|
8321
9182
|
KINDS = ["http", "db", "mcp", "function"];
|
|
8322
9183
|
ToolConfig = class _ToolConfig {
|
|
@@ -8330,6 +9191,10 @@ var init_config = __esm({
|
|
|
8330
9191
|
requiresApproval;
|
|
8331
9192
|
approvalPolicy;
|
|
8332
9193
|
enabled;
|
|
9194
|
+
/** Engine-opaque user metadata (documents have the same column). Stored
|
|
9195
|
+
* and returned in CLEAR on the admin surface only — secrets go in
|
|
9196
|
+
* `config`, which is encrypted. */
|
|
9197
|
+
metaData;
|
|
8333
9198
|
constructor(init) {
|
|
8334
9199
|
this.id = init.id ?? null;
|
|
8335
9200
|
this.name = init.name;
|
|
@@ -8341,6 +9206,7 @@ var init_config = __esm({
|
|
|
8341
9206
|
this.requiresApproval = init.requiresApproval ?? init.requires_approval ?? false;
|
|
8342
9207
|
this.approvalPolicy = init.approvalPolicy ?? init.approval_policy ?? {};
|
|
8343
9208
|
this.enabled = init.enabled ?? true;
|
|
9209
|
+
this.metaData = init.metaData ?? init.meta_data ?? {};
|
|
8344
9210
|
}
|
|
8345
9211
|
static fromUnknown(body) {
|
|
8346
9212
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -8363,10 +9229,14 @@ var init_config = __esm({
|
|
|
8363
9229
|
acl: b.acl ?? null,
|
|
8364
9230
|
requiresApproval: Boolean(b.requiresApproval ?? b.requires_approval ?? false),
|
|
8365
9231
|
approvalPolicy: b.approvalPolicy ?? b.approval_policy ?? {},
|
|
8366
|
-
enabled: b.enabled === void 0 ? true : Boolean(b.enabled)
|
|
9232
|
+
enabled: b.enabled === void 0 ? true : Boolean(b.enabled),
|
|
9233
|
+
metaData: b.metaData ?? b.meta_data ?? {}
|
|
8367
9234
|
});
|
|
8368
9235
|
}
|
|
8369
9236
|
};
|
|
9237
|
+
REDACTED_SENTINEL = "__redacted__";
|
|
9238
|
+
ConfigTemplateError = class extends Error {
|
|
9239
|
+
};
|
|
8370
9240
|
}
|
|
8371
9241
|
});
|
|
8372
9242
|
|
|
@@ -8376,6 +9246,198 @@ var init_crypto2 = __esm({
|
|
|
8376
9246
|
init_crypto();
|
|
8377
9247
|
}
|
|
8378
9248
|
});
|
|
9249
|
+
function isRedirect(resp) {
|
|
9250
|
+
return resp.status >= 300 && resp.status < 400 || resp.type === "opaqueredirect";
|
|
9251
|
+
}
|
|
9252
|
+
function parseV4(text) {
|
|
9253
|
+
const parts = text.split(".");
|
|
9254
|
+
if (parts.length !== 4) return null;
|
|
9255
|
+
const out = new Uint8Array(4);
|
|
9256
|
+
for (let i = 0; i < 4; i++) {
|
|
9257
|
+
const part = parts[i];
|
|
9258
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
9259
|
+
const value = Number(part);
|
|
9260
|
+
if (value > 255) return null;
|
|
9261
|
+
out[i] = value;
|
|
9262
|
+
}
|
|
9263
|
+
return out;
|
|
9264
|
+
}
|
|
9265
|
+
function parseV6(text) {
|
|
9266
|
+
let body = text.split("%")[0] ?? "";
|
|
9267
|
+
const lastColon = body.lastIndexOf(":");
|
|
9268
|
+
if (lastColon < 0) return null;
|
|
9269
|
+
const tail = body.slice(lastColon + 1);
|
|
9270
|
+
if (tail.includes(".")) {
|
|
9271
|
+
const v4 = parseV4(tail);
|
|
9272
|
+
if (!v4) return null;
|
|
9273
|
+
const hi = (v4[0] << 8 | v4[1]).toString(16);
|
|
9274
|
+
const lo = (v4[2] << 8 | v4[3]).toString(16);
|
|
9275
|
+
body = `${body.slice(0, lastColon + 1)}${hi}:${lo}`;
|
|
9276
|
+
}
|
|
9277
|
+
const halves = body.split("::");
|
|
9278
|
+
if (halves.length > 2) return null;
|
|
9279
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
9280
|
+
const rest = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
9281
|
+
let groups;
|
|
9282
|
+
if (halves.length === 1) {
|
|
9283
|
+
if (head.length !== 8) return null;
|
|
9284
|
+
groups = head;
|
|
9285
|
+
} else {
|
|
9286
|
+
const missing = 8 - head.length - rest.length;
|
|
9287
|
+
if (missing < 0) return null;
|
|
9288
|
+
groups = [...head, ...Array(missing).fill("0"), ...rest];
|
|
9289
|
+
}
|
|
9290
|
+
const out = new Uint8Array(16);
|
|
9291
|
+
for (let i = 0; i < 8; i++) {
|
|
9292
|
+
const group = groups[i];
|
|
9293
|
+
if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
|
|
9294
|
+
const value = Number.parseInt(group, 16);
|
|
9295
|
+
out[2 * i] = value >> 8;
|
|
9296
|
+
out[2 * i + 1] = value & 255;
|
|
9297
|
+
}
|
|
9298
|
+
return out;
|
|
9299
|
+
}
|
|
9300
|
+
function parseAddress(text) {
|
|
9301
|
+
const family = isIP(text);
|
|
9302
|
+
if (family === 4) return parseV4(text);
|
|
9303
|
+
if (family === 6) return parseV6(text);
|
|
9304
|
+
return null;
|
|
9305
|
+
}
|
|
9306
|
+
function inNet(addr, net, prefix) {
|
|
9307
|
+
if (addr.length !== net.length) return false;
|
|
9308
|
+
const whole = prefix >> 3;
|
|
9309
|
+
for (let i = 0; i < whole; i++) if (addr[i] !== net[i]) return false;
|
|
9310
|
+
const bits = prefix & 7;
|
|
9311
|
+
if (bits === 0) return true;
|
|
9312
|
+
const mask = 255 << 8 - bits;
|
|
9313
|
+
return (addr[whole] & mask) === (net[whole] & mask);
|
|
9314
|
+
}
|
|
9315
|
+
function compile(table) {
|
|
9316
|
+
return table.map(([cidr, prefix]) => {
|
|
9317
|
+
const bytes = parseAddress(cidr);
|
|
9318
|
+
if (!bytes) throw new Error(`egress: unparseable network ${cidr}`);
|
|
9319
|
+
return [bytes, prefix];
|
|
9320
|
+
});
|
|
9321
|
+
}
|
|
9322
|
+
function embeddedV4(bytes) {
|
|
9323
|
+
if (inNet(bytes, MAPPED_V4[0], MAPPED_V4[1]) || inNet(bytes, NAT64[0], NAT64[1])) {
|
|
9324
|
+
return bytes.subarray(12, 16);
|
|
9325
|
+
}
|
|
9326
|
+
if (inNet(bytes, SIXTOFOUR[0], SIXTOFOUR[1])) return bytes.subarray(2, 6);
|
|
9327
|
+
return null;
|
|
9328
|
+
}
|
|
9329
|
+
function addressIsPrivate(address) {
|
|
9330
|
+
const bytes = parseAddress(address);
|
|
9331
|
+
if (!bytes) return true;
|
|
9332
|
+
if (bytes.length === 4) return V4_NETS.some(([net, prefix]) => inNet(bytes, net, prefix));
|
|
9333
|
+
const embedded = embeddedV4(bytes);
|
|
9334
|
+
if (embedded && V4_NETS.some(([net, prefix]) => inNet(embedded, net, prefix))) return true;
|
|
9335
|
+
return V6_NETS.some(([net, prefix]) => inNet(bytes, net, prefix));
|
|
9336
|
+
}
|
|
9337
|
+
async function isPrivateAddress(host) {
|
|
9338
|
+
let name = (host ?? "").trim().toLowerCase().replace(/^\.+|\.+$/g, "");
|
|
9339
|
+
if (!name) return true;
|
|
9340
|
+
if (name === "localhost") return true;
|
|
9341
|
+
if (name.startsWith("[") && name.endsWith("]")) name = name.slice(1, -1);
|
|
9342
|
+
if (isIP(name)) return addressIsPrivate(name);
|
|
9343
|
+
let addresses;
|
|
9344
|
+
try {
|
|
9345
|
+
addresses = await resolver.lookup(name);
|
|
9346
|
+
} catch {
|
|
9347
|
+
return true;
|
|
9348
|
+
}
|
|
9349
|
+
if (!addresses.length) return true;
|
|
9350
|
+
return addresses.some((address) => addressIsPrivate(address.split("%")[0] ?? address));
|
|
9351
|
+
}
|
|
9352
|
+
async function assertEgressAllowed(url, opts) {
|
|
9353
|
+
let parsed;
|
|
9354
|
+
try {
|
|
9355
|
+
parsed = new URL(url ?? "");
|
|
9356
|
+
} catch {
|
|
9357
|
+
throw new EgressDenied(`egress denied: ${JSON.stringify(url)} is not a valid URL`);
|
|
9358
|
+
}
|
|
9359
|
+
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
9360
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
9361
|
+
throw new EgressDenied(
|
|
9362
|
+
`egress denied: unsupported URL scheme ${JSON.stringify(scheme)} \u2014 http tools may only reach http/https`
|
|
9363
|
+
);
|
|
9364
|
+
}
|
|
9365
|
+
if (opts.allowPrivate) return;
|
|
9366
|
+
const host = parsed.hostname;
|
|
9367
|
+
if (await isPrivateAddress(host)) {
|
|
9368
|
+
throw new EgressDenied(
|
|
9369
|
+
`egress denied: ${host || "(no host)"} is a private, loopback, link-local or metadata address; set allowPrivateEgress to permit it`
|
|
9370
|
+
);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
var MAX_RESPONSE_BYTES, MAX_REDIRECTS, EgressDenied, resolver, V4_PRIVATE, V6_PRIVATE, V4_NETS, V6_NETS, MAPPED_V4, NAT64, SIXTOFOUR;
|
|
9374
|
+
var init_egress = __esm({
|
|
9375
|
+
"src/tools/egress.ts"() {
|
|
9376
|
+
MAX_RESPONSE_BYTES = 1e6;
|
|
9377
|
+
MAX_REDIRECTS = 5;
|
|
9378
|
+
EgressDenied = class extends Error {
|
|
9379
|
+
constructor(message) {
|
|
9380
|
+
super(message);
|
|
9381
|
+
this.name = "EgressDenied";
|
|
9382
|
+
}
|
|
9383
|
+
};
|
|
9384
|
+
resolver = {
|
|
9385
|
+
async lookup(host) {
|
|
9386
|
+
const answers = await promises.lookup(host, { all: true, verbatim: true });
|
|
9387
|
+
return answers.map((a) => a.address);
|
|
9388
|
+
}
|
|
9389
|
+
};
|
|
9390
|
+
V4_PRIVATE = [
|
|
9391
|
+
["0.0.0.0", 8],
|
|
9392
|
+
// "this network" / unspecified
|
|
9393
|
+
["10.0.0.0", 8],
|
|
9394
|
+
// RFC 1918
|
|
9395
|
+
["100.64.0.0", 10],
|
|
9396
|
+
// RFC 6598 carrier-grade NAT — overlay VPNs, pod CIDRs
|
|
9397
|
+
["127.0.0.0", 8],
|
|
9398
|
+
// loopback
|
|
9399
|
+
["169.254.0.0", 16],
|
|
9400
|
+
// link-local, incl. the cloud metadata address
|
|
9401
|
+
["172.16.0.0", 12],
|
|
9402
|
+
// RFC 1918
|
|
9403
|
+
["192.168.0.0", 16],
|
|
9404
|
+
// RFC 1918
|
|
9405
|
+
["224.0.0.0", 4],
|
|
9406
|
+
// multicast
|
|
9407
|
+
["240.0.0.0", 4],
|
|
9408
|
+
// reserved
|
|
9409
|
+
["255.255.255.255", 32]
|
|
9410
|
+
// broadcast (inside 240/4; spelled out anyway)
|
|
9411
|
+
];
|
|
9412
|
+
V6_PRIVATE = [
|
|
9413
|
+
["::", 128],
|
|
9414
|
+
// unspecified
|
|
9415
|
+
["::1", 128],
|
|
9416
|
+
// loopback
|
|
9417
|
+
["::ffff:0:0", 96],
|
|
9418
|
+
// IPv4-mapped — the wrapped v4 is checked too
|
|
9419
|
+
["64:ff9b::", 96],
|
|
9420
|
+
// NAT64 — the wrapped v4 is checked too
|
|
9421
|
+
["2002::", 16],
|
|
9422
|
+
// 6to4 — the wrapped v4 is checked too, and this is denied
|
|
9423
|
+
["fc00::", 7],
|
|
9424
|
+
// unique local
|
|
9425
|
+
["fe80::", 10],
|
|
9426
|
+
// link-local
|
|
9427
|
+
["fec0::", 10],
|
|
9428
|
+
// site-local (deprecated, still configured)
|
|
9429
|
+
["ff00::", 8],
|
|
9430
|
+
// multicast
|
|
9431
|
+
["3fff::", 20]
|
|
9432
|
+
// documentation
|
|
9433
|
+
];
|
|
9434
|
+
V4_NETS = compile(V4_PRIVATE);
|
|
9435
|
+
V6_NETS = compile(V6_PRIVATE);
|
|
9436
|
+
MAPPED_V4 = compile([["::ffff:0:0", 96]])[0];
|
|
9437
|
+
NAT64 = compile([["64:ff9b::", 96]])[0];
|
|
9438
|
+
SIXTOFOUR = compile([["2002::", 16]])[0];
|
|
9439
|
+
}
|
|
9440
|
+
});
|
|
8379
9441
|
|
|
8380
9442
|
// src/tools/executors/db.ts
|
|
8381
9443
|
var db_exports = {};
|
|
@@ -8392,6 +9454,9 @@ __export(db_exports, {
|
|
|
8392
9454
|
function stripSqlNoise(sql) {
|
|
8393
9455
|
return sql.replace(SQL_NOISE_RE, (m) => " ".repeat(m.length));
|
|
8394
9456
|
}
|
|
9457
|
+
function stripSqlNoiseBackslash(sql) {
|
|
9458
|
+
return sql.replace(SQL_NOISE_BACKSLASH_RE, (m) => " ".repeat(m.length));
|
|
9459
|
+
}
|
|
8395
9460
|
function hasKeyword(sqlUpper, keyword) {
|
|
8396
9461
|
return new RegExp(`\\b${keyword}\\b`).test(sqlUpper);
|
|
8397
9462
|
}
|
|
@@ -8520,24 +9585,28 @@ async function executeQueryAsync(config, sql, maxRows, accessModeRaw) {
|
|
|
8520
9585
|
`Invalid access_mode ${JSON.stringify(accessModeRaw)}; must be one of ${ALLOWED_ACCESS_MODES}`
|
|
8521
9586
|
);
|
|
8522
9587
|
}
|
|
8523
|
-
const
|
|
9588
|
+
const sqlVariants = [
|
|
9589
|
+
stripSqlNoise(sql).trim().toUpperCase(),
|
|
9590
|
+
stripSqlNoiseBackslash(sql).trim().toUpperCase()
|
|
9591
|
+
];
|
|
9592
|
+
const sqlUpper = sqlVariants[0];
|
|
8524
9593
|
for (const keyword of ALWAYS_BLOCKED) {
|
|
8525
|
-
if (hasKeyword(
|
|
9594
|
+
if ([...sqlVariants, sql.toUpperCase()].some((v) => hasKeyword(v, keyword))) {
|
|
8526
9595
|
return { success: false, error: `${keyword} queries are not allowed` };
|
|
8527
9596
|
}
|
|
8528
9597
|
}
|
|
8529
9598
|
if (accessMode === "readonly") {
|
|
8530
|
-
if (!
|
|
9599
|
+
if (!sqlVariants.every((v) => v.startsWith("SELECT") || v.startsWith("WITH"))) {
|
|
8531
9600
|
return { success: false, error: "Only SELECT queries are allowed in read-only mode" };
|
|
8532
9601
|
}
|
|
8533
9602
|
for (const keyword of READONLY_BLOCKED) {
|
|
8534
|
-
if (hasKeyword(
|
|
9603
|
+
if (sqlVariants.some((v) => hasKeyword(v, keyword))) {
|
|
8535
9604
|
return { success: false, error: `${keyword} queries are not allowed in read-only mode` };
|
|
8536
9605
|
}
|
|
8537
9606
|
}
|
|
8538
9607
|
} else if (accessMode === "readwrite") {
|
|
8539
9608
|
for (const keyword of READWRITE_BLOCKED) {
|
|
8540
|
-
if (hasKeyword(
|
|
9609
|
+
if (sqlVariants.some((v) => hasKeyword(v, keyword))) {
|
|
8541
9610
|
return { success: false, error: `${keyword} queries are not allowed in read-write mode` };
|
|
8542
9611
|
}
|
|
8543
9612
|
}
|
|
@@ -8671,7 +9740,7 @@ async function getSchemaText(config, selectedTables) {
|
|
|
8671
9740
|
}
|
|
8672
9741
|
return rowsToText(schemaRows);
|
|
8673
9742
|
}
|
|
8674
|
-
var require2, MAX_ROWS, CONNECT_TIMEOUT, QUERY_TIMEOUT_MS, ALWAYS_BLOCKED, READONLY_BLOCKED, READWRITE_BLOCKED, ALLOWED_ACCESS_MODES, SQL_NOISE_RE;
|
|
9743
|
+
var require2, MAX_ROWS, CONNECT_TIMEOUT, QUERY_TIMEOUT_MS, ALWAYS_BLOCKED, READONLY_BLOCKED, READWRITE_BLOCKED, ALLOWED_ACCESS_MODES, SQL_NOISE_RE, SQL_NOISE_BACKSLASH_RE;
|
|
8675
9744
|
var init_db2 = __esm({
|
|
8676
9745
|
"src/tools/executors/db.ts"() {
|
|
8677
9746
|
init_errors();
|
|
@@ -8680,10 +9749,22 @@ var init_db2 = __esm({
|
|
|
8680
9749
|
CONNECT_TIMEOUT = 10;
|
|
8681
9750
|
QUERY_TIMEOUT_MS = 3e4;
|
|
8682
9751
|
ALWAYS_BLOCKED = ["GRANT", "REVOKE"];
|
|
8683
|
-
READONLY_BLOCKED = [
|
|
8684
|
-
|
|
9752
|
+
READONLY_BLOCKED = [
|
|
9753
|
+
"INSERT",
|
|
9754
|
+
"UPDATE",
|
|
9755
|
+
"DELETE",
|
|
9756
|
+
"MERGE",
|
|
9757
|
+
"DROP",
|
|
9758
|
+
"ALTER",
|
|
9759
|
+
"CREATE",
|
|
9760
|
+
"TRUNCATE",
|
|
9761
|
+
"DO",
|
|
9762
|
+
"CALL"
|
|
9763
|
+
];
|
|
9764
|
+
READWRITE_BLOCKED = ["DELETE", "DROP", "TRUNCATE", "DO", "CALL"];
|
|
8685
9765
|
ALLOWED_ACCESS_MODES = ["readonly", "readwrite", "full"];
|
|
8686
|
-
SQL_NOISE_RE =
|
|
9766
|
+
SQL_NOISE_RE = /\b[eE]'(?:[^'\\]|\\[\s\S]|'')*'|'(?:[^']|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
9767
|
+
SQL_NOISE_BACKSLASH_RE = /'(?:[^'\\]|\\[\s\S]|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
8687
9768
|
}
|
|
8688
9769
|
});
|
|
8689
9770
|
|
|
@@ -8718,10 +9799,38 @@ function withQuery(url, params) {
|
|
|
8718
9799
|
if (!q) return url;
|
|
8719
9800
|
return url.includes("?") ? `${url}&${q}` : `${url}?${q}`;
|
|
8720
9801
|
}
|
|
9802
|
+
async function readCapped(resp) {
|
|
9803
|
+
if (!resp.body) return { text: await resp.text(), truncated: false };
|
|
9804
|
+
const reader = resp.body.getReader();
|
|
9805
|
+
const chunks = [];
|
|
9806
|
+
let size = 0;
|
|
9807
|
+
let truncated = false;
|
|
9808
|
+
for (; ; ) {
|
|
9809
|
+
const { done, value } = await reader.read();
|
|
9810
|
+
if (done) break;
|
|
9811
|
+
if (value) {
|
|
9812
|
+
chunks.push(Buffer.from(value));
|
|
9813
|
+
size += value.byteLength;
|
|
9814
|
+
}
|
|
9815
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
9816
|
+
truncated = true;
|
|
9817
|
+
await reader.cancel();
|
|
9818
|
+
break;
|
|
9819
|
+
}
|
|
9820
|
+
}
|
|
9821
|
+
const bytes = Buffer.concat(chunks);
|
|
9822
|
+
return {
|
|
9823
|
+
text: (truncated ? bytes.subarray(0, MAX_RESPONSE_BYTES) : bytes).toString("utf8"),
|
|
9824
|
+
truncated
|
|
9825
|
+
};
|
|
9826
|
+
}
|
|
8721
9827
|
async function defaultRequest(init) {
|
|
8722
9828
|
let url = init.url;
|
|
8723
9829
|
if (init.params) url = withQuery(url, init.params);
|
|
8724
9830
|
const headers = { ...init.headers ?? {} };
|
|
9831
|
+
if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
|
|
9832
|
+
headers["Accept-Encoding"] = "identity";
|
|
9833
|
+
}
|
|
8725
9834
|
let body;
|
|
8726
9835
|
if (init.json !== void 0) {
|
|
8727
9836
|
headers["Content-Type"] = headers["Content-Type"] ?? headers["content-type"] ?? "application/json";
|
|
@@ -8733,25 +9842,47 @@ async function defaultRequest(init) {
|
|
|
8733
9842
|
method: init.method,
|
|
8734
9843
|
headers,
|
|
8735
9844
|
body,
|
|
8736
|
-
|
|
9845
|
+
// Redirects are followed by hand in `executeHttp` — an automatic follow
|
|
9846
|
+
// would jump to a destination no egress check ever saw.
|
|
9847
|
+
redirect: "manual",
|
|
8737
9848
|
signal: AbortSignal.timeout(TIMEOUT_MS5)
|
|
8738
9849
|
});
|
|
8739
9850
|
const respHeaders = {};
|
|
8740
9851
|
resp.headers.forEach((v, k) => {
|
|
8741
9852
|
respHeaders[k] = v;
|
|
8742
9853
|
});
|
|
8743
|
-
const text = await resp
|
|
9854
|
+
const { text, truncated } = await readCapped(resp);
|
|
8744
9855
|
return {
|
|
8745
9856
|
status: resp.status,
|
|
8746
9857
|
headers: respHeaders,
|
|
9858
|
+
truncated,
|
|
8747
9859
|
async json() {
|
|
8748
|
-
return JSON.parse(text);
|
|
9860
|
+
return JSON.parse(text.replace(/^/, ""));
|
|
8749
9861
|
},
|
|
8750
9862
|
async text() {
|
|
8751
9863
|
return text;
|
|
8752
9864
|
}
|
|
8753
9865
|
};
|
|
8754
9866
|
}
|
|
9867
|
+
function nextHop(init, status, location) {
|
|
9868
|
+
const current = new URL(init.url);
|
|
9869
|
+
const target = new URL(location, current);
|
|
9870
|
+
let method = init.method;
|
|
9871
|
+
if ((status === 302 || status === 303) && method !== "HEAD") method = "GET";
|
|
9872
|
+
else if (status === 301 && method === "POST") method = "GET";
|
|
9873
|
+
let headers = { ...init.headers ?? {} };
|
|
9874
|
+
if (current.origin !== target.origin) {
|
|
9875
|
+
headers = Object.fromEntries(
|
|
9876
|
+
Object.entries(headers).filter(([k]) => !CROSS_ORIGIN_HEADERS.has(k.toLowerCase()))
|
|
9877
|
+
);
|
|
9878
|
+
}
|
|
9879
|
+
const hop = { method, url: target.toString(), headers };
|
|
9880
|
+
if (method === init.method) {
|
|
9881
|
+
if (init.json !== void 0) hop.json = init.json;
|
|
9882
|
+
if (init.content !== void 0) hop.content = init.content;
|
|
9883
|
+
}
|
|
9884
|
+
return hop;
|
|
9885
|
+
}
|
|
8755
9886
|
async function executeHttp(config, args, opts = {}) {
|
|
8756
9887
|
const method = String(config.method ?? "GET").toUpperCase();
|
|
8757
9888
|
let url = config.url;
|
|
@@ -8772,7 +9903,7 @@ async function executeHttp(config, args, opts = {}) {
|
|
|
8772
9903
|
}
|
|
8773
9904
|
}
|
|
8774
9905
|
}
|
|
8775
|
-
|
|
9906
|
+
let requestInit = {
|
|
8776
9907
|
method,
|
|
8777
9908
|
url: url ?? "",
|
|
8778
9909
|
headers
|
|
@@ -8808,27 +9939,56 @@ async function executeHttp(config, args, opts = {}) {
|
|
|
8808
9939
|
}
|
|
8809
9940
|
}
|
|
8810
9941
|
const client = opts.client;
|
|
8811
|
-
const
|
|
9942
|
+
const allowPrivate = opts.allowPrivate ?? false;
|
|
9943
|
+
let resp;
|
|
9944
|
+
let hops = 0;
|
|
9945
|
+
for (; ; ) {
|
|
9946
|
+
await assertEgressAllowed(requestInit.url, { allowPrivate });
|
|
9947
|
+
resp = client ? await client.request(requestInit) : await defaultRequest(requestInit);
|
|
9948
|
+
const location = resp.headers.location ?? resp.headers.Location;
|
|
9949
|
+
if (!REDIRECT_STATUS.has(resp.status) || !location) break;
|
|
9950
|
+
if (hops >= MAX_REDIRECTS) {
|
|
9951
|
+
throw new EngineActionError(`too many redirects (more than ${MAX_REDIRECTS}) starting at ${url}`);
|
|
9952
|
+
}
|
|
9953
|
+
hops += 1;
|
|
9954
|
+
requestInit = nextHop(requestInit, resp.status, location);
|
|
9955
|
+
}
|
|
8812
9956
|
const contentType = resp.headers["content-type"] ?? resp.headers["Content-Type"] ?? "";
|
|
8813
|
-
let
|
|
9957
|
+
let text = "";
|
|
8814
9958
|
try {
|
|
8815
|
-
|
|
9959
|
+
text = await resp.text();
|
|
8816
9960
|
} catch {
|
|
8817
|
-
|
|
9961
|
+
text = "";
|
|
9962
|
+
}
|
|
9963
|
+
const bytes = Buffer.from(text, "utf8");
|
|
9964
|
+
const truncated = resp.truncated === true || bytes.byteLength > MAX_RESPONSE_BYTES;
|
|
9965
|
+
let data;
|
|
9966
|
+
if (truncated) {
|
|
9967
|
+
data = bytes.subarray(0, MAX_RESPONSE_BYTES).toString("utf8");
|
|
9968
|
+
} else {
|
|
9969
|
+
try {
|
|
9970
|
+
data = contentType.includes("application/json") ? await resp.json() : text;
|
|
9971
|
+
} catch {
|
|
9972
|
+
data = text;
|
|
9973
|
+
}
|
|
8818
9974
|
}
|
|
8819
9975
|
const safeHeaders = {};
|
|
8820
9976
|
for (const [k, v] of Object.entries(resp.headers)) {
|
|
8821
9977
|
if (!SENSITIVE_RESPONSE_HEADERS.has(k.toLowerCase())) safeHeaders[k] = v;
|
|
8822
9978
|
}
|
|
8823
|
-
|
|
9979
|
+
const result = {
|
|
8824
9980
|
status_code: resp.status,
|
|
8825
9981
|
headers: safeHeaders,
|
|
8826
9982
|
data
|
|
8827
9983
|
};
|
|
9984
|
+
if (truncated) result.truncated = true;
|
|
9985
|
+
return result;
|
|
8828
9986
|
}
|
|
8829
|
-
var TIMEOUT_MS5, SENSITIVE_RESPONSE_HEADERS;
|
|
9987
|
+
var TIMEOUT_MS5, SENSITIVE_RESPONSE_HEADERS, REDIRECT_STATUS, CROSS_ORIGIN_HEADERS;
|
|
8830
9988
|
var init_http = __esm({
|
|
8831
9989
|
"src/tools/executors/http.ts"() {
|
|
9990
|
+
init_errors();
|
|
9991
|
+
init_egress();
|
|
8832
9992
|
TIMEOUT_MS5 = 3e4;
|
|
8833
9993
|
SENSITIVE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
8834
9994
|
"set-cookie",
|
|
@@ -8837,10 +9997,19 @@ var init_http = __esm({
|
|
|
8837
9997
|
"proxy-authenticate",
|
|
8838
9998
|
"www-authenticate"
|
|
8839
9999
|
]);
|
|
10000
|
+
REDIRECT_STATUS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
10001
|
+
CROSS_ORIGIN_HEADERS = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie"]);
|
|
8840
10002
|
}
|
|
8841
10003
|
});
|
|
8842
10004
|
|
|
8843
10005
|
// src/tools/executors/mcp-client.ts
|
|
10006
|
+
async function checkEgress(url, allowPrivate) {
|
|
10007
|
+
let checked = url;
|
|
10008
|
+
const scheme = (/^([a-z0-9+.-]+):/i.exec(url ?? "")?.[1] ?? "").toLowerCase();
|
|
10009
|
+
if (scheme === "ws") checked = `http:${url.slice(scheme.length + 1)}`;
|
|
10010
|
+
else if (scheme === "wss") checked = `https:${url.slice(scheme.length + 1)}`;
|
|
10011
|
+
await assertEgressAllowed(checked, { allowPrivate });
|
|
10012
|
+
}
|
|
8844
10013
|
function parseSseBuffer(raw) {
|
|
8845
10014
|
const dataLines = raw.split("\n").filter((ln) => ln.startsWith("data:")).map((ln) => ln.slice(5).trimStart());
|
|
8846
10015
|
if (!dataLines.length) return null;
|
|
@@ -8853,10 +10022,12 @@ function parseSseBuffer(raw) {
|
|
|
8853
10022
|
}
|
|
8854
10023
|
return null;
|
|
8855
10024
|
}
|
|
8856
|
-
var MCPError, HttpTransport, SseTransport, WsTransport, MCPClient, MCPRegistry, PromptevMCP;
|
|
10025
|
+
var REDIRECT_MESSAGE, MCPError, HttpTransport, SseTransport, WsTransport, MCPClient, MCPRegistry, PromptevMCP;
|
|
8857
10026
|
var init_mcp_client = __esm({
|
|
8858
10027
|
"src/tools/executors/mcp-client.ts"() {
|
|
8859
10028
|
init_version();
|
|
10029
|
+
init_egress();
|
|
10030
|
+
REDIRECT_MESSAGE = "MCP server redirected; register the final URL";
|
|
8860
10031
|
MCPError = class _MCPError extends Error {
|
|
8861
10032
|
code;
|
|
8862
10033
|
data;
|
|
@@ -8873,10 +10044,12 @@ var init_mcp_client = __esm({
|
|
|
8873
10044
|
HttpTransport = class {
|
|
8874
10045
|
kind = "http";
|
|
8875
10046
|
url;
|
|
10047
|
+
allowPrivate;
|
|
8876
10048
|
baseHeaders;
|
|
8877
10049
|
sessionId = null;
|
|
8878
|
-
constructor(url, headers) {
|
|
10050
|
+
constructor(url, headers, allowPrivate = false) {
|
|
8879
10051
|
this.url = url;
|
|
10052
|
+
this.allowPrivate = allowPrivate;
|
|
8880
10053
|
this.baseHeaders = {
|
|
8881
10054
|
...headers,
|
|
8882
10055
|
"Content-Type": "application/json",
|
|
@@ -8884,6 +10057,7 @@ var init_mcp_client = __esm({
|
|
|
8884
10057
|
};
|
|
8885
10058
|
}
|
|
8886
10059
|
async connect() {
|
|
10060
|
+
await checkEgress(this.url, this.allowPrivate);
|
|
8887
10061
|
}
|
|
8888
10062
|
async close() {
|
|
8889
10063
|
this.sessionId = null;
|
|
@@ -8897,10 +10071,17 @@ var init_mcp_client = __esm({
|
|
|
8897
10071
|
const resp = await fetch(this.url, {
|
|
8898
10072
|
method: "POST",
|
|
8899
10073
|
headers: this.buildHeaders(),
|
|
8900
|
-
body: JSON.stringify(msg)
|
|
10074
|
+
body: JSON.stringify(msg),
|
|
10075
|
+
// `redirect: "manual"`: a followed redirect would carry this POST —
|
|
10076
|
+
// headers, Authorization and all — to a destination `connect()`'s
|
|
10077
|
+
// check never saw. A 3xx is reported, never chased.
|
|
10078
|
+
redirect: "manual"
|
|
8901
10079
|
});
|
|
8902
10080
|
const sid = resp.headers.get("Mcp-Session-Id");
|
|
8903
10081
|
if (sid) this.sessionId = sid;
|
|
10082
|
+
if (isRedirect(resp)) {
|
|
10083
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
10084
|
+
}
|
|
8904
10085
|
if (resp.status >= 400) {
|
|
8905
10086
|
const body = await resp.text();
|
|
8906
10087
|
console.error(`[HTTP Transport] ${resp.status} from ${this.url}: ${body.slice(0, 300)}`);
|
|
@@ -8933,11 +10114,20 @@ var init_mcp_client = __esm({
|
|
|
8933
10114
|
eventsUrl;
|
|
8934
10115
|
baseUrl;
|
|
8935
10116
|
headers;
|
|
10117
|
+
allowPrivate;
|
|
10118
|
+
/** Where the server told us to POST — it arrives in an `endpoint` event on
|
|
10119
|
+
* the stream, so it is server-chosen and hence checked. Private: the only
|
|
10120
|
+
* way in is the parser, which is the path a real server takes. */
|
|
8936
10121
|
postUrl = null;
|
|
10122
|
+
/** The last POST endpoint cleared by the egress check. The check costs a
|
|
10123
|
+
* DNS resolution and the endpoint changes at most once per connection, so
|
|
10124
|
+
* it is not repeated per message. */
|
|
10125
|
+
checkedPostUrl = null;
|
|
8937
10126
|
queue = [];
|
|
8938
10127
|
waiters = [];
|
|
8939
10128
|
abort = null;
|
|
8940
|
-
constructor(url, headers) {
|
|
10129
|
+
constructor(url, headers, allowPrivate = false) {
|
|
10130
|
+
this.allowPrivate = allowPrivate;
|
|
8941
10131
|
this.eventsUrl = url.replace(/\/$/, "");
|
|
8942
10132
|
if (url.endsWith("/events")) this.baseUrl = url.slice(0, -7);
|
|
8943
10133
|
else if (url.endsWith("/sse")) this.baseUrl = url.slice(0, -4);
|
|
@@ -8950,14 +10140,19 @@ var init_mcp_client = __esm({
|
|
|
8950
10140
|
else this.queue.push(msg);
|
|
8951
10141
|
}
|
|
8952
10142
|
async connect() {
|
|
10143
|
+
await checkEgress(this.eventsUrl, this.allowPrivate);
|
|
8953
10144
|
this.abort = new AbortController();
|
|
8954
10145
|
void this.listen();
|
|
8955
10146
|
}
|
|
8956
10147
|
async listen() {
|
|
8957
10148
|
const resp = await fetch(this.eventsUrl, {
|
|
8958
10149
|
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
8959
|
-
signal: this.abort?.signal
|
|
10150
|
+
signal: this.abort?.signal,
|
|
10151
|
+
redirect: "manual"
|
|
8960
10152
|
});
|
|
10153
|
+
if (isRedirect(resp)) {
|
|
10154
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
10155
|
+
}
|
|
8961
10156
|
if (!resp.ok) throw new Error(`SSE ${resp.status}`);
|
|
8962
10157
|
if (!resp.body) throw new Error("SSE response has no body");
|
|
8963
10158
|
const reader = resp.body.getReader();
|
|
@@ -9009,11 +10204,19 @@ var init_mcp_client = __esm({
|
|
|
9009
10204
|
}
|
|
9010
10205
|
}
|
|
9011
10206
|
const postUrl = this.postUrl || this.baseUrl;
|
|
10207
|
+
if (postUrl !== this.checkedPostUrl) {
|
|
10208
|
+
await checkEgress(postUrl, this.allowPrivate);
|
|
10209
|
+
this.checkedPostUrl = postUrl;
|
|
10210
|
+
}
|
|
9012
10211
|
const resp = await fetch(postUrl, {
|
|
9013
10212
|
method: "POST",
|
|
9014
10213
|
headers: { ...this.headers, "Content-Type": "application/json" },
|
|
9015
|
-
body: JSON.stringify(msg)
|
|
10214
|
+
body: JSON.stringify(msg),
|
|
10215
|
+
redirect: "manual"
|
|
9016
10216
|
});
|
|
10217
|
+
if (isRedirect(resp)) {
|
|
10218
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
10219
|
+
}
|
|
9017
10220
|
if (!resp.ok) throw new Error(`SSE POST ${resp.status}`);
|
|
9018
10221
|
try {
|
|
9019
10222
|
const data = await resp.json();
|
|
@@ -9032,14 +10235,17 @@ var init_mcp_client = __esm({
|
|
|
9032
10235
|
kind = "websocket";
|
|
9033
10236
|
url;
|
|
9034
10237
|
headers;
|
|
10238
|
+
allowPrivate;
|
|
9035
10239
|
ws = null;
|
|
9036
10240
|
queue = [];
|
|
9037
10241
|
waiters = [];
|
|
9038
|
-
constructor(url, headers) {
|
|
10242
|
+
constructor(url, headers, allowPrivate = false) {
|
|
9039
10243
|
this.url = url;
|
|
10244
|
+
this.allowPrivate = allowPrivate;
|
|
9040
10245
|
this.headers = headers;
|
|
9041
10246
|
}
|
|
9042
10247
|
async connect() {
|
|
10248
|
+
await checkEgress(this.url, this.allowPrivate);
|
|
9043
10249
|
const WS = globalThis.WebSocket;
|
|
9044
10250
|
if (!WS) throw new Error("WebSocket is not available in this runtime");
|
|
9045
10251
|
this.ws = new WS(this.url);
|
|
@@ -9080,21 +10286,27 @@ var init_mcp_client = __esm({
|
|
|
9080
10286
|
tools = [];
|
|
9081
10287
|
resources = [];
|
|
9082
10288
|
headers;
|
|
9083
|
-
|
|
10289
|
+
allowPrivate;
|
|
10290
|
+
constructor(url, headers = null, opts = {}) {
|
|
9084
10291
|
this.url = url;
|
|
9085
10292
|
this.headers = headers ?? {};
|
|
9086
|
-
this.
|
|
10293
|
+
this.allowPrivate = opts.allowPrivate ?? false;
|
|
10294
|
+
this.transport = _MCPClient.createTransport(url, this.headers, this.allowPrivate);
|
|
9087
10295
|
}
|
|
9088
|
-
static createTransport(url, headers) {
|
|
10296
|
+
static createTransport(url, headers, allowPrivate = false) {
|
|
9089
10297
|
if (url.startsWith("stdio:") || url === "stdio") {
|
|
9090
10298
|
throw new Error("stdio MCP transport is not supported (cloud deployments use SSE/HTTP/WebSocket)");
|
|
9091
10299
|
}
|
|
9092
|
-
if (url.startsWith("ws://") || url.startsWith("wss://"))
|
|
10300
|
+
if (url.startsWith("ws://") || url.startsWith("wss://")) {
|
|
10301
|
+
return new WsTransport(url, headers, allowPrivate);
|
|
10302
|
+
}
|
|
9093
10303
|
if (url.startsWith("sse+http://") || url.startsWith("sse+https://") || url.endsWith("/events") || url.endsWith("/sse")) {
|
|
9094
10304
|
const clean = url.replace("sse+http://", "http://").replace("sse+https://", "https://");
|
|
9095
|
-
return new SseTransport(clean, headers);
|
|
10305
|
+
return new SseTransport(clean, headers, allowPrivate);
|
|
10306
|
+
}
|
|
10307
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
10308
|
+
return new HttpTransport(url, headers, allowPrivate);
|
|
9096
10309
|
}
|
|
9097
|
-
if (url.startsWith("http://") || url.startsWith("https://")) return new HttpTransport(url, headers);
|
|
9098
10310
|
throw new Error(`Unsupported URL: ${url} (use http(s)://, ws(s)://, or .../sse)`);
|
|
9099
10311
|
}
|
|
9100
10312
|
nextId() {
|
|
@@ -9146,7 +10358,15 @@ var init_mcp_client = __esm({
|
|
|
9146
10358
|
await this.transport.send({ jsonrpc: "2.0", method, params: params ?? {} });
|
|
9147
10359
|
}
|
|
9148
10360
|
async connect() {
|
|
9149
|
-
|
|
10361
|
+
try {
|
|
10362
|
+
await this.transport.connect();
|
|
10363
|
+
} catch (e) {
|
|
10364
|
+
try {
|
|
10365
|
+
await this.transport.close();
|
|
10366
|
+
} catch {
|
|
10367
|
+
}
|
|
10368
|
+
throw e;
|
|
10369
|
+
}
|
|
9150
10370
|
if (!(this.transport instanceof HttpTransport)) {
|
|
9151
10371
|
this.connected = true;
|
|
9152
10372
|
void this.recvLoop();
|
|
@@ -9229,7 +10449,7 @@ var init_mcp_client = __esm({
|
|
|
9229
10449
|
}
|
|
9230
10450
|
this.id = 0;
|
|
9231
10451
|
this.pending.clear();
|
|
9232
|
-
this.transport = _MCPClient.createTransport(url, headers);
|
|
10452
|
+
this.transport = _MCPClient.createTransport(url, headers, this.allowPrivate);
|
|
9233
10453
|
await this.connect();
|
|
9234
10454
|
await this.listTools();
|
|
9235
10455
|
}
|
|
@@ -9237,11 +10457,11 @@ var init_mcp_client = __esm({
|
|
|
9237
10457
|
MCPRegistry = class {
|
|
9238
10458
|
clients = /* @__PURE__ */ new Map();
|
|
9239
10459
|
healthTask = null;
|
|
9240
|
-
async connect(name, url, headers = null, token = null) {
|
|
10460
|
+
async connect(name, url, headers = null, token = null, allowPrivate = false) {
|
|
9241
10461
|
this.clients.delete(name);
|
|
9242
10462
|
const hdrs = { ...headers ?? {} };
|
|
9243
10463
|
if (token) hdrs.Authorization = hdrs.Authorization ?? `Bearer ${token}`;
|
|
9244
|
-
const client = new MCPClient(url, hdrs);
|
|
10464
|
+
const client = new MCPClient(url, hdrs, { allowPrivate });
|
|
9245
10465
|
await client.connect();
|
|
9246
10466
|
await client.listTools();
|
|
9247
10467
|
this.clients.set(name, client);
|
|
@@ -9293,8 +10513,18 @@ var init_mcp_client = __esm({
|
|
|
9293
10513
|
};
|
|
9294
10514
|
PromptevMCP = class {
|
|
9295
10515
|
clients = new MCPRegistry();
|
|
10516
|
+
allowPrivate;
|
|
10517
|
+
/**
|
|
10518
|
+
* `allowPrivate` is the operator knob `allowPrivateEgress`, carried down to
|
|
10519
|
+
* every transport this facade builds. It defaults to `false`, so a
|
|
10520
|
+
* construction site that forgets to pass it denies private destinations
|
|
10521
|
+
* rather than permitting them.
|
|
10522
|
+
*/
|
|
10523
|
+
constructor(opts = {}) {
|
|
10524
|
+
this.allowPrivate = opts.allowPrivate ?? false;
|
|
10525
|
+
}
|
|
9296
10526
|
async addServer(name, url, opts = {}) {
|
|
9297
|
-
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null);
|
|
10527
|
+
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null, this.allowPrivate);
|
|
9298
10528
|
}
|
|
9299
10529
|
async shutdown() {
|
|
9300
10530
|
await this.clients.shutdown();
|
|
@@ -9419,6 +10649,9 @@ var init_registry = __esm({
|
|
|
9419
10649
|
"src/tools/registry.ts"() {
|
|
9420
10650
|
}
|
|
9421
10651
|
});
|
|
10652
|
+
function allowPrivateEgress(engine) {
|
|
10653
|
+
return Boolean(engine?.config?.allowPrivateEgress);
|
|
10654
|
+
}
|
|
9422
10655
|
function toolVisible(ct, principals) {
|
|
9423
10656
|
return aclVisible(ct.acl, principals);
|
|
9424
10657
|
}
|
|
@@ -9500,11 +10733,19 @@ function redactToolResult(result, policy, opts) {
|
|
|
9500
10733
|
if (failed.length) note.rules_failed = failed;
|
|
9501
10734
|
return [redacted, note];
|
|
9502
10735
|
}
|
|
9503
|
-
function
|
|
9504
|
-
|
|
9505
|
-
if (
|
|
9506
|
-
|
|
10736
|
+
function cryptoKey(engine) {
|
|
10737
|
+
const key = getSecretKey(engine.config);
|
|
10738
|
+
if (key.length !== 32) {
|
|
10739
|
+
throw new Error(`CE_SECRET_KEY is malformed: decoded to ${key.length} bytes, need 32`);
|
|
9507
10740
|
}
|
|
10741
|
+
return key;
|
|
10742
|
+
}
|
|
10743
|
+
function decryptRowConfig(row, engine) {
|
|
10744
|
+
if (!row.config_encrypted) return {};
|
|
10745
|
+
return decryptDict(String(row.config_encrypted), cryptoKey(engine));
|
|
10746
|
+
}
|
|
10747
|
+
function rowToToolConfig(row, engine, config) {
|
|
10748
|
+
config ??= decryptRowConfig(row, engine);
|
|
9508
10749
|
return new ToolConfig({
|
|
9509
10750
|
id: String(row.id),
|
|
9510
10751
|
name: String(row.name),
|
|
@@ -9515,9 +10756,24 @@ function rowToToolConfig(row, engine) {
|
|
|
9515
10756
|
acl: row.acl != null ? [...row.acl] : null,
|
|
9516
10757
|
requiresApproval: Boolean(row.requires_approval),
|
|
9517
10758
|
approvalPolicy: row.approval_policy ?? {},
|
|
9518
|
-
enabled: Boolean(row.enabled)
|
|
10759
|
+
enabled: Boolean(row.enabled),
|
|
10760
|
+
metaData: row.meta_data ?? {}
|
|
9519
10761
|
});
|
|
9520
10762
|
}
|
|
10763
|
+
function rowToToolConfigLenient(row, engine) {
|
|
10764
|
+
if (!row.config_encrypted) return rowToToolConfig(row, engine, {});
|
|
10765
|
+
const key = cryptoKey(engine);
|
|
10766
|
+
let config;
|
|
10767
|
+
try {
|
|
10768
|
+
config = decryptDict(String(row.config_encrypted), key);
|
|
10769
|
+
} catch {
|
|
10770
|
+
console.warn(
|
|
10771
|
+
`tool ${String(row.id)} (${String(row.name)}): config_error \u2014 its stored config could not be decrypted (was the secret key rotated?); leaving it out of the tool set`
|
|
10772
|
+
);
|
|
10773
|
+
return null;
|
|
10774
|
+
}
|
|
10775
|
+
return rowToToolConfig(row, engine, config);
|
|
10776
|
+
}
|
|
9521
10777
|
async function loadPersistedCanonicals(engine, sourceId) {
|
|
9522
10778
|
const params = [];
|
|
9523
10779
|
let sql = `SELECT * FROM context_engine_tools WHERE enabled IS TRUE`;
|
|
@@ -9528,7 +10784,9 @@ async function loadPersistedCanonicals(engine, sourceId) {
|
|
|
9528
10784
|
const result = await engine.pool.query(sql, params);
|
|
9529
10785
|
const canonicals = [];
|
|
9530
10786
|
for (const row of result.rows) {
|
|
9531
|
-
|
|
10787
|
+
const tc = rowToToolConfigLenient(row, engine);
|
|
10788
|
+
if (tc === null) continue;
|
|
10789
|
+
canonicals.push(...canonicalFromConfig(tc));
|
|
9532
10790
|
}
|
|
9533
10791
|
return canonicals;
|
|
9534
10792
|
}
|
|
@@ -9548,13 +10806,18 @@ function canonicalToPublic(ct) {
|
|
|
9548
10806
|
async function registerTool(engine, tc) {
|
|
9549
10807
|
canonicalFromConfig(tc);
|
|
9550
10808
|
const config = tc.config ?? {};
|
|
9551
|
-
|
|
10809
|
+
if (containsSentinel(config)) {
|
|
10810
|
+
throw new ConfigTemplateError(
|
|
10811
|
+
`config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a redacted template cannot be registered as a new tool; re-enter the secret values`
|
|
10812
|
+
);
|
|
10813
|
+
}
|
|
10814
|
+
const configEncrypted = Object.keys(config).length ? encryptDict(config, cryptoKey(engine)) : null;
|
|
9552
10815
|
const id = randomUUID();
|
|
9553
10816
|
await engine.pool.query(
|
|
9554
10817
|
`INSERT INTO context_engine_tools
|
|
9555
10818
|
(id, name, kind, description, source_id, acl, config_encrypted,
|
|
9556
|
-
requires_approval, approval_policy, enabled)
|
|
9557
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
|
|
10819
|
+
requires_approval, approval_policy, enabled, meta_data)
|
|
10820
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11::jsonb)`,
|
|
9558
10821
|
[
|
|
9559
10822
|
id,
|
|
9560
10823
|
tc.name,
|
|
@@ -9565,7 +10828,8 @@ async function registerTool(engine, tc) {
|
|
|
9565
10828
|
configEncrypted,
|
|
9566
10829
|
tc.requiresApproval,
|
|
9567
10830
|
JSON.stringify(tc.approvalPolicy ?? {}),
|
|
9568
|
-
tc.enabled
|
|
10831
|
+
tc.enabled,
|
|
10832
|
+
JSON.stringify(tc.metaData ?? {})
|
|
9569
10833
|
]
|
|
9570
10834
|
);
|
|
9571
10835
|
return id;
|
|
@@ -9582,12 +10846,41 @@ async function updateTool(engine, id, opts) {
|
|
|
9582
10846
|
let i = 1;
|
|
9583
10847
|
for (const [key, value] of Object.entries(fields)) {
|
|
9584
10848
|
if (key === "config") {
|
|
10849
|
+
let config = value;
|
|
10850
|
+
if (config && containsSentinel(config)) {
|
|
10851
|
+
if (Object.hasOwn(fields, "kind") && fields.kind !== row.kind) {
|
|
10852
|
+
throw new ConfigTemplateError(
|
|
10853
|
+
"cannot change kind and keep redacted secrets in one PATCH \u2014 re-enter the config in full"
|
|
10854
|
+
);
|
|
10855
|
+
}
|
|
10856
|
+
if (config.body_secret === false && containsSentinel(config.body)) {
|
|
10857
|
+
throw new ConfigTemplateError(
|
|
10858
|
+
`body_secret cannot be turned off while the body still contains ${JSON.stringify(REDACTED_SENTINEL)} \u2014 re-enter the body to declassify it`
|
|
10859
|
+
);
|
|
10860
|
+
}
|
|
10861
|
+
if (!row.config_encrypted) {
|
|
10862
|
+
throw new ConfigTemplateError(
|
|
10863
|
+
`config contains ${JSON.stringify(REDACTED_SENTINEL)} but this tool has no stored config to keep \u2014 re-enter the secret values`
|
|
10864
|
+
);
|
|
10865
|
+
}
|
|
10866
|
+
const key2 = cryptoKey(engine);
|
|
10867
|
+
let stored;
|
|
10868
|
+
try {
|
|
10869
|
+
stored = decryptDict(String(row.config_encrypted), key2);
|
|
10870
|
+
} catch (exc) {
|
|
10871
|
+
throw new ConfigTemplateError(
|
|
10872
|
+
`the stored config cannot be decrypted (was the secret key rotated?) \u2014 re-enter the config in full, without ${JSON.stringify(REDACTED_SENTINEL)} values`,
|
|
10873
|
+
{ cause: exc }
|
|
10874
|
+
);
|
|
10875
|
+
}
|
|
10876
|
+
config = mergeRedacted(config, stored);
|
|
10877
|
+
}
|
|
9585
10878
|
sets.push(`config_encrypted = $${i++}`);
|
|
9586
|
-
params.push(
|
|
10879
|
+
params.push(config && Object.keys(config).length ? encryptDict(config, cryptoKey(engine)) : null);
|
|
9587
10880
|
} else if (UPDATABLE_COLUMNS.has(key)) {
|
|
9588
|
-
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key;
|
|
10881
|
+
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key === "metaData" ? "meta_data" : key;
|
|
9589
10882
|
sets.push(`${col} = $${i++}`);
|
|
9590
|
-
params.push(col === "approval_policy" ? JSON.stringify(value ?? {}) : value);
|
|
10883
|
+
params.push(col === "approval_policy" || col === "meta_data" ? JSON.stringify(value ?? {}) : value);
|
|
9591
10884
|
}
|
|
9592
10885
|
}
|
|
9593
10886
|
sets.push(`updated_at = now()`);
|
|
@@ -9596,7 +10889,8 @@ async function updateTool(engine, id, opts) {
|
|
|
9596
10889
|
`UPDATE context_engine_tools SET ${sets.join(", ")} WHERE id = $${i} RETURNING *`,
|
|
9597
10890
|
params
|
|
9598
10891
|
);
|
|
9599
|
-
|
|
10892
|
+
const row_ = updated.rows[0];
|
|
10893
|
+
return rowToToolConfigLenient(row_, engine) ?? rowToToolConfig(row_, engine, {});
|
|
9600
10894
|
}
|
|
9601
10895
|
async function deleteTool(engine, id, opts = {}) {
|
|
9602
10896
|
const existing = await engine.pool.query(`SELECT * FROM context_engine_tools WHERE id = $1`, [id]);
|
|
@@ -9645,17 +10939,38 @@ function probeFailure(exc, kind) {
|
|
|
9645
10939
|
console.warn(`test_tool(kind=${kind}) failed:`, exc, "->", category);
|
|
9646
10940
|
return { ok: false, error: category };
|
|
9647
10941
|
}
|
|
9648
|
-
async function testTool(
|
|
10942
|
+
async function testTool(engine, tc) {
|
|
9649
10943
|
const kind = tc.kind;
|
|
9650
10944
|
const config = tc.config ?? {};
|
|
10945
|
+
if (containsSentinel(config)) {
|
|
10946
|
+
throw new ConfigTemplateError(
|
|
10947
|
+
`config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a probe would send it verbatim as the credential; re-enter the secret values`
|
|
10948
|
+
);
|
|
10949
|
+
}
|
|
9651
10950
|
if (kind === "http") {
|
|
9652
10951
|
const url = config.url;
|
|
9653
10952
|
if (!url) return { ok: false, error: "http config missing 'url'" };
|
|
9654
10953
|
try {
|
|
9655
|
-
|
|
10954
|
+
await assertEgressAllowed(url, { allowPrivate: allowPrivateEgress(engine) });
|
|
10955
|
+
} catch (exc) {
|
|
10956
|
+
if (exc instanceof EgressDenied) {
|
|
10957
|
+
console.warn(`testTool(kind=http) refused: ${exc.message}`);
|
|
10958
|
+
return { ok: false, error: "egress_denied" };
|
|
10959
|
+
}
|
|
10960
|
+
throw exc;
|
|
10961
|
+
}
|
|
10962
|
+
const client = engine?._toolHttpClient ?? null;
|
|
10963
|
+
try {
|
|
10964
|
+
const resp = client ? await client.request({
|
|
10965
|
+
method: "HEAD",
|
|
10966
|
+
url,
|
|
10967
|
+
headers: config.headers ?? {}
|
|
10968
|
+
}) : await fetch(url, {
|
|
9656
10969
|
method: "HEAD",
|
|
9657
10970
|
headers: config.headers ?? {},
|
|
9658
|
-
|
|
10971
|
+
// No automatic follow: a redirect would reach a destination the
|
|
10972
|
+
// check above never saw.
|
|
10973
|
+
redirect: "manual",
|
|
9659
10974
|
signal: AbortSignal.timeout(1e4)
|
|
9660
10975
|
});
|
|
9661
10976
|
return { ok: resp.status < 500, status_code: resp.status };
|
|
@@ -9676,12 +10991,16 @@ async function testTool(_engine, tc) {
|
|
|
9676
10991
|
const url = config.url;
|
|
9677
10992
|
if (!url) return { ok: false, error: "mcp config missing 'url'" };
|
|
9678
10993
|
const token = config.oauth_token ?? config.bearer ?? config.access_token;
|
|
9679
|
-
const mcp = new PromptevMCP();
|
|
10994
|
+
const mcp = new PromptevMCP({ allowPrivate: allowPrivateEgress(engine) });
|
|
9680
10995
|
try {
|
|
9681
10996
|
await mcp.addServer(tc.name, url, { token: token ?? null });
|
|
9682
10997
|
const client = mcp.clients.get(tc.name);
|
|
9683
10998
|
return { ok: true, tools: client.tools.map((t) => t.name) };
|
|
9684
10999
|
} catch (exc) {
|
|
11000
|
+
if (exc instanceof EgressDenied) {
|
|
11001
|
+
console.warn(`testTool(kind=mcp) refused: ${exc.message}`);
|
|
11002
|
+
return { ok: false, error: "egress_denied" };
|
|
11003
|
+
}
|
|
9685
11004
|
return probeFailure(exc, "mcp");
|
|
9686
11005
|
} finally {
|
|
9687
11006
|
await mcp.shutdown();
|
|
@@ -9689,11 +11008,20 @@ async function testTool(_engine, tc) {
|
|
|
9689
11008
|
}
|
|
9690
11009
|
return { ok: false, error: `test_tool does not support kind=${JSON.stringify(kind)}` };
|
|
9691
11010
|
}
|
|
9692
|
-
async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
11011
|
+
async function findAndClaimApproved(engine, toolName, args, sourceId, principals, approvalScope) {
|
|
9693
11012
|
const claimedAt = /* @__PURE__ */ new Date();
|
|
11013
|
+
const params = [toolName];
|
|
11014
|
+
let scopeSql2 = "approval_scope IS NULL";
|
|
11015
|
+
if (approvalScope !== null) {
|
|
11016
|
+
params.push(approvalScope);
|
|
11017
|
+
scopeSql2 = `approval_scope = $${params.length}`;
|
|
11018
|
+
}
|
|
11019
|
+
const wall = claimVisibilitySql(principals, params.length + 1);
|
|
11020
|
+
params.push(...wall.params);
|
|
9694
11021
|
const candidates = await engine.pool.query(
|
|
9695
|
-
`SELECT * FROM context_engine_tool_approvals
|
|
9696
|
-
|
|
11022
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
11023
|
+
WHERE tool_name = $1 AND status = 'approved' AND ${scopeSql2} AND ${wall.sql}`,
|
|
11024
|
+
params
|
|
9697
11025
|
);
|
|
9698
11026
|
for (const row of candidates.rows) {
|
|
9699
11027
|
if ((row.source_id ?? null) !== (sourceId ?? null)) continue;
|
|
@@ -9710,7 +11038,7 @@ async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
|
9710
11038
|
}
|
|
9711
11039
|
return null;
|
|
9712
11040
|
}
|
|
9713
|
-
async function dispatchMcp(ct, config, args) {
|
|
11041
|
+
async function dispatchMcp(ct, config, args, opts = {}) {
|
|
9714
11042
|
const url = config.url;
|
|
9715
11043
|
if (!url) throw new EngineActionError(`mcp tool ${ct.callName} config missing 'url'`);
|
|
9716
11044
|
const headers = { ...config.headers ?? {} };
|
|
@@ -9719,7 +11047,7 @@ async function dispatchMcp(ct, config, args) {
|
|
|
9719
11047
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
9720
11048
|
else if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
9721
11049
|
const toolName = String(ct.raw?.tool_name ?? ct.displayName);
|
|
9722
|
-
const mcp = new PromptevMCP();
|
|
11050
|
+
const mcp = new PromptevMCP({ allowPrivate: opts.allowPrivate ?? false });
|
|
9723
11051
|
try {
|
|
9724
11052
|
await mcp.addServer(ct.callName, url, { headers: Object.keys(headers).length ? headers : null });
|
|
9725
11053
|
const client = mcp.clients.get(ct.callName);
|
|
@@ -9730,7 +11058,10 @@ async function dispatchMcp(ct, config, args) {
|
|
|
9730
11058
|
}
|
|
9731
11059
|
async function dispatch(engine, ct, config, args) {
|
|
9732
11060
|
if (ct.kind === "http") {
|
|
9733
|
-
return executeHttp(config, args, {
|
|
11061
|
+
return executeHttp(config, args, {
|
|
11062
|
+
client: engine._toolHttpClient ?? null,
|
|
11063
|
+
allowPrivate: allowPrivateEgress(engine)
|
|
11064
|
+
});
|
|
9734
11065
|
}
|
|
9735
11066
|
if (ct.kind === "db") {
|
|
9736
11067
|
const query = String(args.query ?? "");
|
|
@@ -9745,7 +11076,9 @@ async function dispatch(engine, ct, config, args) {
|
|
|
9745
11076
|
}
|
|
9746
11077
|
return result;
|
|
9747
11078
|
}
|
|
9748
|
-
if (ct.kind === "mcp")
|
|
11079
|
+
if (ct.kind === "mcp") {
|
|
11080
|
+
return dispatchMcp(ct, config, args, { allowPrivate: allowPrivateEgress(engine) });
|
|
11081
|
+
}
|
|
9749
11082
|
if (ct.kind === "function") {
|
|
9750
11083
|
const fn = ct.raw?.callable;
|
|
9751
11084
|
if (!fn) throw new EngineActionError(`function tool ${ct.callName} has no callable`);
|
|
@@ -9766,10 +11099,19 @@ async function decryptCtConfig(engine, toolId) {
|
|
|
9766
11099
|
]);
|
|
9767
11100
|
const row = result.rows[0];
|
|
9768
11101
|
if (!row?.config_encrypted) return {};
|
|
9769
|
-
return decryptDict(String(row.config_encrypted),
|
|
11102
|
+
return decryptDict(String(row.config_encrypted), cryptoKey(engine));
|
|
11103
|
+
}
|
|
11104
|
+
function warnUnscopedApproval(callName) {
|
|
11105
|
+
if (warnedUnscoped.has(callName)) return;
|
|
11106
|
+
warnedUnscoped.add(callName);
|
|
11107
|
+
process.emitWarning(
|
|
11108
|
+
`executeTool(${JSON.stringify(callName)}) needs an approval but was called without approvalScope. The record is opened and matched UNSCOPED (tool + args + sourceId, behind the principals wall). Pass approvalScope: <opaque string> (e.g. a run id) so approvals are claimable only within that scope; a gated call without one will be refused in the next release.`,
|
|
11109
|
+
{ type: "DeprecationWarning", code: "CE_APPROVAL_SCOPE_MISSING" }
|
|
11110
|
+
);
|
|
9770
11111
|
}
|
|
9771
11112
|
async function executeTool(engine, callName, args, opts = {}) {
|
|
9772
11113
|
const runtimeArgs = args ?? {};
|
|
11114
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
9773
11115
|
const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
|
|
9774
11116
|
if (!ct) throw new EngineActionError(`tool not found: ${callName}`);
|
|
9775
11117
|
if (!toolVisible(ct, opts.principals ?? null)) {
|
|
@@ -9778,22 +11120,32 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
9778
11120
|
const publicArgs = stripUnderscoreArgs(runtimeArgs);
|
|
9779
11121
|
let approvalId = null;
|
|
9780
11122
|
if (shouldRequireApproval(ct, publicArgs)) {
|
|
9781
|
-
|
|
11123
|
+
if (approvalScope === null) warnUnscopedApproval(callName);
|
|
11124
|
+
approvalId = await findAndClaimApproved(
|
|
11125
|
+
engine,
|
|
11126
|
+
ct.callName,
|
|
11127
|
+
publicArgs,
|
|
11128
|
+
opts.sourceId ?? null,
|
|
11129
|
+
opts.principals ?? null,
|
|
11130
|
+
approvalScope
|
|
11131
|
+
);
|
|
9782
11132
|
if (approvalId == null) {
|
|
9783
|
-
const
|
|
11133
|
+
const pendingOpts = {
|
|
9784
11134
|
toolName: ct.callName,
|
|
9785
11135
|
args: publicArgs,
|
|
9786
11136
|
sourceId: opts.sourceId ?? null,
|
|
9787
11137
|
principals: opts.principals ?? null,
|
|
9788
11138
|
policy: ct.approvalPolicy ?? {}
|
|
9789
|
-
}
|
|
11139
|
+
};
|
|
11140
|
+
const record = approvalScope === null ? await createPending(engine, pendingOpts) : await findOrCreatePending(engine, { ...pendingOpts, approvalScope });
|
|
9790
11141
|
const reason = ct.requiresApproval ? "tool requires approval" : `approval policy condition met: ${ct.approvalPolicy?.condition}`;
|
|
9791
11142
|
return {
|
|
9792
11143
|
approval_required: {
|
|
9793
11144
|
approval_id: String(record.id),
|
|
9794
11145
|
tool_name: ct.callName,
|
|
9795
11146
|
args: publicArgs,
|
|
9796
|
-
reason
|
|
11147
|
+
reason,
|
|
11148
|
+
expires_at: record.expiresAt ? record.expiresAt.toISOString() : null
|
|
9797
11149
|
}
|
|
9798
11150
|
};
|
|
9799
11151
|
}
|
|
@@ -9818,8 +11170,9 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
9818
11170
|
let truncated = false;
|
|
9819
11171
|
if (success) {
|
|
9820
11172
|
try {
|
|
11173
|
+
const redactPrincipals = opts.principals === TRUSTED ? null : opts.principals ?? null;
|
|
9821
11174
|
const [redacted] = redactToolResult(rawResult, engine.config.redaction, {
|
|
9822
|
-
principals:
|
|
11175
|
+
principals: redactPrincipals,
|
|
9823
11176
|
secretKey: engine.config.secretKey,
|
|
9824
11177
|
hooks: engine.hooks
|
|
9825
11178
|
});
|
|
@@ -9859,17 +11212,19 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
9859
11212
|
usage: { units: 1, kind: "tool", tool_name: ct.callName, truncated }
|
|
9860
11213
|
};
|
|
9861
11214
|
}
|
|
9862
|
-
var RESULT_MAX_CHARS, RESULT_MAX_ROWS, UPDATABLE_COLUMNS, PROBE_AUTH, PROBE_UNREACHABLE, PROBE_TIMEOUT, PROBE_MISCONFIGURED;
|
|
11215
|
+
var RESULT_MAX_CHARS, RESULT_MAX_ROWS, UPDATABLE_COLUMNS, PROBE_AUTH, PROBE_UNREACHABLE, PROBE_TIMEOUT, PROBE_MISCONFIGURED, warnedUnscoped;
|
|
9863
11216
|
var init_governance = __esm({
|
|
9864
11217
|
"src/tools/governance.ts"() {
|
|
9865
11218
|
init_errors();
|
|
9866
11219
|
init_hooks();
|
|
9867
11220
|
init_redaction();
|
|
11221
|
+
init_sentinels();
|
|
9868
11222
|
init_acl();
|
|
9869
11223
|
init_approval();
|
|
9870
11224
|
init_audit();
|
|
9871
|
-
|
|
11225
|
+
init_config2();
|
|
9872
11226
|
init_crypto2();
|
|
11227
|
+
init_egress();
|
|
9873
11228
|
init_db2();
|
|
9874
11229
|
init_http();
|
|
9875
11230
|
init_mcp_client();
|
|
@@ -9887,7 +11242,9 @@ var init_governance = __esm({
|
|
|
9887
11242
|
"requires_approval",
|
|
9888
11243
|
"approvalPolicy",
|
|
9889
11244
|
"approval_policy",
|
|
9890
|
-
"enabled"
|
|
11245
|
+
"enabled",
|
|
11246
|
+
"metaData",
|
|
11247
|
+
"meta_data"
|
|
9891
11248
|
]);
|
|
9892
11249
|
PROBE_AUTH = [
|
|
9893
11250
|
"authentication failed",
|
|
@@ -9926,6 +11283,7 @@ var init_governance = __esm({
|
|
|
9926
11283
|
"not supported",
|
|
9927
11284
|
"unsupported"
|
|
9928
11285
|
];
|
|
11286
|
+
warnedUnscoped = /* @__PURE__ */ new Set();
|
|
9929
11287
|
}
|
|
9930
11288
|
});
|
|
9931
11289
|
function mulberry322(seed) {
|
|
@@ -10757,7 +12115,8 @@ var retrieval_exports = {};
|
|
|
10757
12115
|
__export(retrieval_exports, {
|
|
10758
12116
|
buildGraphRanked: () => buildGraphRanked,
|
|
10759
12117
|
corpusIsAclUniform: () => corpusIsAclUniform,
|
|
10760
|
-
shouldUseCommunitySummaries: () => shouldUseCommunitySummaries
|
|
12118
|
+
shouldUseCommunitySummaries: () => shouldUseCommunitySummaries,
|
|
12119
|
+
vectorSeedIds: () => vectorSeedIds
|
|
10761
12120
|
});
|
|
10762
12121
|
function vecLiteral2(vector) {
|
|
10763
12122
|
return `[${vector.map((x) => Number(x)).join(",")}]`;
|
|
@@ -10783,8 +12142,8 @@ async function deriveQueryEntities(pool, chunkIds, opts) {
|
|
|
10783
12142
|
JOIN context_engine_chunks c ON c.id = ce.chunk_id
|
|
10784
12143
|
WHERE ce.chunk_id = ANY($1::uuid[]) ${SCOPE2}
|
|
10785
12144
|
GROUP BY e.normalized_name, e.name, e.type
|
|
10786
|
-
ORDER BY freq DESC LIMIT $
|
|
10787
|
-
[chunkIds, opts.sourceIds, opts.principals, ENTITY_LIMIT]
|
|
12145
|
+
ORDER BY freq DESC LIMIT $5`,
|
|
12146
|
+
[chunkIds, opts.sourceIds, opts.documentIds ?? null, opts.principals, ENTITY_LIMIT]
|
|
10788
12147
|
);
|
|
10789
12148
|
return result.rows.map((r) => ({
|
|
10790
12149
|
normalized_name: r.normalized_name,
|
|
@@ -10849,8 +12208,8 @@ async function computeCommunityScores(pool, chunkIds, vector, opts) {
|
|
|
10849
12208
|
`SELECT ce.chunk_id::text, ce.entity_id::text
|
|
10850
12209
|
FROM context_engine_chunk_entities ce
|
|
10851
12210
|
JOIN context_engine_chunks c ON c.id = ce.chunk_id
|
|
10852
|
-
WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($
|
|
10853
|
-
[chunkIds, opts.sourceIds, opts.principals, Object.keys(entityScore)]
|
|
12211
|
+
WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($5::uuid[]) ${SCOPE2}`,
|
|
12212
|
+
[chunkIds, opts.sourceIds, opts.documentIds ?? null, opts.principals, Object.keys(entityScore)]
|
|
10854
12213
|
);
|
|
10855
12214
|
const chunkScores = {};
|
|
10856
12215
|
for (const row of result.rows) {
|
|
@@ -10859,13 +12218,29 @@ async function computeCommunityScores(pool, chunkIds, vector, opts) {
|
|
|
10859
12218
|
return chunkScores;
|
|
10860
12219
|
}
|
|
10861
12220
|
async function vectorSeedIds(pool, vector, opts) {
|
|
10862
|
-
const
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
12221
|
+
const binds = [opts.sourceIds, opts.documentIds ?? null, opts.principals];
|
|
12222
|
+
const scoped = binds.some((v) => v !== null);
|
|
12223
|
+
const client = await pool.connect();
|
|
12224
|
+
try {
|
|
12225
|
+
await client.query("BEGIN");
|
|
12226
|
+
await opts.backend?.tuneAnnScan?.(client, SEED_LIMIT, scoped);
|
|
12227
|
+
const result = await client.query(
|
|
12228
|
+
`SELECT c.id::text FROM context_engine_chunks c
|
|
12229
|
+
WHERE c.embedding IS NOT NULL ${SCOPE2}
|
|
12230
|
+
ORDER BY c.embedding <=> CAST($1 AS vector) LIMIT $5`,
|
|
12231
|
+
[vecLiteral2(vector), ...binds, SEED_LIMIT]
|
|
12232
|
+
);
|
|
12233
|
+
await client.query("COMMIT");
|
|
12234
|
+
return result.rows.map((r) => String(r.id));
|
|
12235
|
+
} catch (err) {
|
|
12236
|
+
try {
|
|
12237
|
+
await client.query("ROLLBACK");
|
|
12238
|
+
} catch {
|
|
12239
|
+
}
|
|
12240
|
+
throw err;
|
|
12241
|
+
} finally {
|
|
12242
|
+
client.release();
|
|
12243
|
+
}
|
|
10869
12244
|
}
|
|
10870
12245
|
async function buildGraphRanked(query, opts) {
|
|
10871
12246
|
try {
|
|
@@ -10874,11 +12249,18 @@ async function buildGraphRanked(query, opts) {
|
|
|
10874
12249
|
const vector = vectors[0] ? [...vectors[0]] : null;
|
|
10875
12250
|
if (!vector) return [];
|
|
10876
12251
|
const sourceIds = opts.sourceIds ?? null;
|
|
12252
|
+
const documentIds = opts.documentIds ?? null;
|
|
10877
12253
|
const principals = opts.principals ?? null;
|
|
10878
|
-
const seeds = await vectorSeedIds(opts.pool, vector, {
|
|
12254
|
+
const seeds = await vectorSeedIds(opts.pool, vector, {
|
|
12255
|
+
sourceIds,
|
|
12256
|
+
documentIds,
|
|
12257
|
+
principals,
|
|
12258
|
+
backend: opts.backend
|
|
12259
|
+
});
|
|
10879
12260
|
if (!seeds.length) return [];
|
|
10880
12261
|
const queryEntities = await deriveQueryEntities(opts.pool, seeds.slice(0, TOP_SEEDS_FOR_ENTITIES), {
|
|
10881
12262
|
sourceIds,
|
|
12263
|
+
documentIds,
|
|
10882
12264
|
principals
|
|
10883
12265
|
});
|
|
10884
12266
|
const entityNorms = queryEntities.map((e) => String(e.normalized_name));
|
|
@@ -10900,6 +12282,7 @@ async function buildGraphRanked(query, opts) {
|
|
|
10900
12282
|
const relScores = await computeRelationshipScores(opts.pool, allIds, entityNorms);
|
|
10901
12283
|
const commScores = await computeCommunityScores(opts.pool, allIds, vector, {
|
|
10902
12284
|
sourceIds,
|
|
12285
|
+
documentIds,
|
|
10903
12286
|
principals,
|
|
10904
12287
|
useSummaries
|
|
10905
12288
|
});
|
|
@@ -10951,7 +12334,8 @@ var init_retrieval = __esm({
|
|
|
10951
12334
|
TOP_SEEDS_FOR_ENTITIES = 20;
|
|
10952
12335
|
SCOPE2 = `
|
|
10953
12336
|
AND ($2::text[] IS NULL OR c.source_id = ANY($2::text[]))
|
|
10954
|
-
AND ($3::
|
|
12337
|
+
AND ($3::uuid[] IS NULL OR c.document_id = ANY($3::uuid[]))
|
|
12338
|
+
AND ($4::text[] IS NULL OR c.acl IS NULL OR c.acl && $4::text[])
|
|
10955
12339
|
`;
|
|
10956
12340
|
}
|
|
10957
12341
|
});
|
|
@@ -11115,11 +12499,19 @@ var init_engine = __esm({
|
|
|
11115
12499
|
_graphStore = null;
|
|
11116
12500
|
constructor(config, opts = {}) {
|
|
11117
12501
|
this.config = config;
|
|
11118
|
-
this.hooks = {
|
|
12502
|
+
this.hooks = {
|
|
12503
|
+
onUsage: opts.onUsage ?? null,
|
|
12504
|
+
onError: opts.onError ?? null,
|
|
12505
|
+
onProgress: opts.onProgress ?? null
|
|
12506
|
+
};
|
|
11119
12507
|
}
|
|
11120
12508
|
async ensurePool() {
|
|
11121
12509
|
if (!this._pool) {
|
|
11122
|
-
this._pool = await createPool(this.config.databaseUrl
|
|
12510
|
+
this._pool = await createPool(this.config.databaseUrl, {
|
|
12511
|
+
max: this.config.storage.poolMax,
|
|
12512
|
+
idleTimeoutMillis: this.config.storage.poolIdleTimeoutMs,
|
|
12513
|
+
connectionTimeoutMillis: this.config.storage.poolConnectionTimeoutMs
|
|
12514
|
+
});
|
|
11123
12515
|
this.pool = this._pool;
|
|
11124
12516
|
}
|
|
11125
12517
|
if (!this.backend) {
|
|
@@ -11305,7 +12697,9 @@ var init_engine = __esm({
|
|
|
11305
12697
|
graphStore: await this.getGraphStore(),
|
|
11306
12698
|
hooks: this.hooks,
|
|
11307
12699
|
sourceIds: opts.sourceIds ?? null,
|
|
11308
|
-
|
|
12700
|
+
documentIds: opts.documentIds ?? null,
|
|
12701
|
+
principals,
|
|
12702
|
+
backend: this.backend
|
|
11309
12703
|
});
|
|
11310
12704
|
}
|
|
11311
12705
|
return runSearch(query, {
|
|
@@ -11315,6 +12709,7 @@ var init_engine = __esm({
|
|
|
11315
12709
|
embedder: this.embedder,
|
|
11316
12710
|
pool,
|
|
11317
12711
|
sourceIds: opts.sourceIds,
|
|
12712
|
+
documentIds: opts.documentIds,
|
|
11318
12713
|
principals,
|
|
11319
12714
|
topK: opts.topK,
|
|
11320
12715
|
mode: opts.mode,
|
|
@@ -11420,7 +12815,7 @@ var init_engine = __esm({
|
|
|
11420
12815
|
hooks: this.hooks,
|
|
11421
12816
|
sourceIds: opts.sourceIds,
|
|
11422
12817
|
principals: resolvePrincipals(opts.principals, "compute"),
|
|
11423
|
-
|
|
12818
|
+
documentIds: opts.documentIds,
|
|
11424
12819
|
modelCfg: opts.modelCfg,
|
|
11425
12820
|
timeout: opts.timeout
|
|
11426
12821
|
});
|
|
@@ -11473,151 +12868,16 @@ var init_engine = __esm({
|
|
|
11473
12868
|
sourceId: opts.sourceId,
|
|
11474
12869
|
principals: resolvePrincipals(opts.principals, "executeTool"),
|
|
11475
12870
|
actor: opts.actor,
|
|
11476
|
-
source: opts.source ?? "api"
|
|
12871
|
+
source: opts.source ?? "api",
|
|
12872
|
+
approvalScope: opts.approvalScope
|
|
11477
12873
|
});
|
|
11478
12874
|
}
|
|
11479
12875
|
};
|
|
11480
12876
|
}
|
|
11481
12877
|
});
|
|
11482
12878
|
|
|
11483
|
-
// src/config.ts
|
|
11484
|
-
init_redaction();
|
|
11485
|
-
var embeddingSchema = z.object({
|
|
11486
|
-
provider: z.enum(["openai", "azure_openai", "gemini", "voyage", "cohere", "custom"]),
|
|
11487
|
-
model: z.string(),
|
|
11488
|
-
dim: z.number().int().positive().nullable().optional().default(null),
|
|
11489
|
-
apiKey: z.string().nullable().optional().default(null),
|
|
11490
|
-
baseUrl: z.string().nullable().optional().default(null)
|
|
11491
|
-
});
|
|
11492
|
-
var llmSchema = z.object({
|
|
11493
|
-
provider: z.enum(["anthropic", "openai", "azure_openai", "gemini", "bedrock", "custom"]),
|
|
11494
|
-
model: z.string(),
|
|
11495
|
-
apiKey: z.string().nullable().optional().default(null),
|
|
11496
|
-
baseUrl: z.string().nullable().optional().default(null)
|
|
11497
|
-
});
|
|
11498
|
-
var graphSchema = z.object({
|
|
11499
|
-
enabled: z.boolean().default(false),
|
|
11500
|
-
neo4jUri: z.string().nullable().optional().default(null),
|
|
11501
|
-
neo4jUser: z.string().default("neo4j"),
|
|
11502
|
-
neo4jPassword: z.string().nullable().optional().default(null),
|
|
11503
|
-
neo4jDatabase: z.string().default("neo4j"),
|
|
11504
|
-
extractionLlm: llmSchema.nullable().optional().default(null)
|
|
11505
|
-
});
|
|
11506
|
-
var rerankerSchema = z.object({
|
|
11507
|
-
enabled: z.boolean().default(false),
|
|
11508
|
-
provider: z.enum(["cohere", "voyage", "jina", "custom"]).nullable().optional().default(null),
|
|
11509
|
-
model: z.string().nullable().optional().default(null),
|
|
11510
|
-
apiKey: z.string().nullable().optional().default(null),
|
|
11511
|
-
baseUrl: z.string().nullable().optional().default(null),
|
|
11512
|
-
candidates: z.number().int().positive().default(50)
|
|
11513
|
-
});
|
|
11514
|
-
var fusionSchema = z.object({
|
|
11515
|
-
method: z.literal("rrf").default("rrf"),
|
|
11516
|
-
k: z.number().int().positive().default(60),
|
|
11517
|
-
weights: z.record(z.string(), z.number()).default({ fts: 1, trgm: 0.8, ann: 1, graph: 1 })
|
|
11518
|
-
});
|
|
11519
|
-
var storageSchema = z.object({
|
|
11520
|
-
backend: z.literal("postgres").default("postgres"),
|
|
11521
|
-
annExactThreshold: z.number().int().positive().default(5e4)
|
|
11522
|
-
});
|
|
11523
|
-
var ContextEngineConfig = class _ContextEngineConfig {
|
|
11524
|
-
databaseUrl;
|
|
11525
|
-
storage;
|
|
11526
|
-
defaultMode;
|
|
11527
|
-
embedding;
|
|
11528
|
-
llm;
|
|
11529
|
-
visionLlm;
|
|
11530
|
-
graph;
|
|
11531
|
-
reranker;
|
|
11532
|
-
fusion;
|
|
11533
|
-
enableCodeExecution;
|
|
11534
|
-
secretKey;
|
|
11535
|
-
redaction;
|
|
11536
|
-
constructor(init) {
|
|
11537
|
-
this.databaseUrl = init.databaseUrl;
|
|
11538
|
-
this.storage = storageSchema.parse(init.storage ?? {});
|
|
11539
|
-
this.defaultMode = init.defaultMode ?? "hybrid";
|
|
11540
|
-
this.embedding = embeddingSchema.parse(init.embedding);
|
|
11541
|
-
this.llm = init.llm ? llmSchema.parse(init.llm) : null;
|
|
11542
|
-
this.visionLlm = init.visionLlm ? llmSchema.parse(init.visionLlm) : null;
|
|
11543
|
-
this.graph = graphSchema.parse(init.graph ?? {});
|
|
11544
|
-
this.reranker = rerankerSchema.parse(init.reranker ?? {});
|
|
11545
|
-
this.fusion = fusionSchema.parse(init.fusion ?? {});
|
|
11546
|
-
this.enableCodeExecution = init.enableCodeExecution ?? false;
|
|
11547
|
-
this.secretKey = init.secretKey ?? null;
|
|
11548
|
-
this.redaction = init.redaction instanceof RedactionPolicy ? init.redaction : new RedactionPolicy(init.redaction ?? {});
|
|
11549
|
-
this.validate();
|
|
11550
|
-
}
|
|
11551
|
-
validate() {
|
|
11552
|
-
if (this.graph.enabled && !(this.graph.neo4jUri && this.graph.neo4jPassword && this.graph.extractionLlm)) {
|
|
11553
|
-
throw new Error("graph enabled but neo4jUri/neo4jPassword/extractionLlm missing");
|
|
11554
|
-
}
|
|
11555
|
-
if (this.reranker.enabled && !(this.reranker.provider && this.reranker.apiKey)) {
|
|
11556
|
-
throw new Error("reranker enabled but provider/apiKey missing");
|
|
11557
|
-
}
|
|
11558
|
-
if (this.defaultMode === "graph" && !this.graph.enabled) {
|
|
11559
|
-
throw new Error("defaultMode is 'graph' but graph enabled is false");
|
|
11560
|
-
}
|
|
11561
|
-
for (const rule of this.redaction.rules) {
|
|
11562
|
-
if (rule.action === "hash" && !this.secretKey) {
|
|
11563
|
-
throw new Error(
|
|
11564
|
-
`redaction rule '${rule.name}': action='hash' requires ContextEngineConfig.secretKey to be set`
|
|
11565
|
-
);
|
|
11566
|
-
}
|
|
11567
|
-
}
|
|
11568
|
-
}
|
|
11569
|
-
static fromEnv(overrides = {}) {
|
|
11570
|
-
const env = loadCeEnv();
|
|
11571
|
-
const merged = deepMerge(env, overrides);
|
|
11572
|
-
if (!merged.databaseUrl || !merged.embedding) {
|
|
11573
|
-
throw new Error(
|
|
11574
|
-
"ContextEngineConfig.fromEnv requires CE_DATABASE_URL and CE_EMBEDDING__PROVIDER/MODEL (or explicit overrides)"
|
|
11575
|
-
);
|
|
11576
|
-
}
|
|
11577
|
-
return new _ContextEngineConfig(merged);
|
|
11578
|
-
}
|
|
11579
|
-
};
|
|
11580
|
-
function camelize(key) {
|
|
11581
|
-
return key.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
11582
|
-
}
|
|
11583
|
-
function loadCeEnv() {
|
|
11584
|
-
const root = {};
|
|
11585
|
-
for (const [raw, value] of Object.entries(process.env)) {
|
|
11586
|
-
if (!raw.startsWith("CE_") || value === void 0) continue;
|
|
11587
|
-
const path = raw.slice(3).split("__").map(camelize);
|
|
11588
|
-
let cur = root;
|
|
11589
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
11590
|
-
const k = path[i];
|
|
11591
|
-
const next = cur[k];
|
|
11592
|
-
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
11593
|
-
cur = cur[k];
|
|
11594
|
-
}
|
|
11595
|
-
cur[path[path.length - 1]] = coerceEnv(value);
|
|
11596
|
-
}
|
|
11597
|
-
return root;
|
|
11598
|
-
}
|
|
11599
|
-
function coerceEnv(value) {
|
|
11600
|
-
if (value === "true") return true;
|
|
11601
|
-
if (value === "false") return false;
|
|
11602
|
-
if (/^-?\d+$/.test(value)) return Number(value);
|
|
11603
|
-
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
|
|
11604
|
-
return value;
|
|
11605
|
-
}
|
|
11606
|
-
function deepMerge(a, b) {
|
|
11607
|
-
const out = { ...a };
|
|
11608
|
-
for (const [k, v] of Object.entries(b)) {
|
|
11609
|
-
if (v === void 0) continue;
|
|
11610
|
-
const existing = out[k];
|
|
11611
|
-
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
11612
|
-
out[k] = deepMerge(existing, v);
|
|
11613
|
-
} else {
|
|
11614
|
-
out[k] = v;
|
|
11615
|
-
}
|
|
11616
|
-
}
|
|
11617
|
-
return out;
|
|
11618
|
-
}
|
|
11619
|
-
|
|
11620
12879
|
// src/cli.ts
|
|
12880
|
+
init_config();
|
|
11621
12881
|
init_db();
|
|
11622
12882
|
|
|
11623
12883
|
// src/diagnostics.ts
|