@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/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { francAll } from 'franc';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
|
+
import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash, createHmac } from 'crypto';
|
|
3
4
|
import { jsonrepair } from 'jsonrepair';
|
|
4
5
|
import { load } from 'cheerio';
|
|
5
6
|
import ExcelJS from 'exceljs';
|
|
6
7
|
import JSZip from 'jszip';
|
|
7
8
|
import mammoth from 'mammoth';
|
|
8
9
|
import PostalMime from 'postal-mime';
|
|
10
|
+
import { z } from 'zod';
|
|
9
11
|
import { getDocumentProxy, extractText, renderPageAsImage, extractImages } from 'unpdf';
|
|
10
12
|
import { createRequire } from 'module';
|
|
11
|
-
import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash, createHmac } from 'crypto';
|
|
12
13
|
import { readFileSync, openSync, readSync, closeSync } from 'fs';
|
|
13
14
|
import { join, basename, dirname } from 'path';
|
|
14
|
-
import { z } from 'zod';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { getEncoding } from 'js-tiktoken';
|
|
17
|
+
import { promises } from 'dns';
|
|
18
|
+
import { isIP } from 'net';
|
|
17
19
|
|
|
18
20
|
var __defProp = Object.defineProperty;
|
|
19
21
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -775,6 +777,14 @@ function emitError(hooks, exc, ctx) {
|
|
|
775
777
|
log2.warn("onError callback raised; swallowing");
|
|
776
778
|
}
|
|
777
779
|
}
|
|
780
|
+
function emitProgress(hooks, event) {
|
|
781
|
+
if (!hooks?.onProgress) return;
|
|
782
|
+
try {
|
|
783
|
+
hooks.onProgress(event);
|
|
784
|
+
} catch {
|
|
785
|
+
log2.warn("onProgress callback raised; swallowing");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
778
788
|
function emitToolCall(hooks, event) {
|
|
779
789
|
if (!hooks?.onToolCall) return;
|
|
780
790
|
try {
|
|
@@ -796,6 +806,8 @@ var init_hooks = __esm({
|
|
|
796
806
|
// src/providers/llm.ts
|
|
797
807
|
var llm_exports = {};
|
|
798
808
|
__export(llm_exports, {
|
|
809
|
+
CALL_TIMEOUT_MS: () => CALL_TIMEOUT_MS,
|
|
810
|
+
GEMINI_CALL_TIMEOUT_MS: () => GEMINI_CALL_TIMEOUT_MS,
|
|
799
811
|
LLMClient: () => LLMClient,
|
|
800
812
|
TIMEOUT_MS: () => TIMEOUT_MS,
|
|
801
813
|
buildLlmClient: () => buildLlmClient,
|
|
@@ -838,7 +850,7 @@ async function loadGeminiChatClient(apiKey) {
|
|
|
838
850
|
if (!Ctor) {
|
|
839
851
|
throw new ExtraMissingError("gemini", specifier, "gemini llm");
|
|
840
852
|
}
|
|
841
|
-
return new Ctor({ apiKey: apiKey ?? null });
|
|
853
|
+
return new Ctor({ apiKey: apiKey ?? null, httpOptions: { timeout: GEMINI_CALL_TIMEOUT_MS } });
|
|
842
854
|
}
|
|
843
855
|
async function loadBedrockSdk() {
|
|
844
856
|
const specifier = "@aws-sdk/client-bedrock-runtime";
|
|
@@ -875,11 +887,13 @@ async function callLlm(cfg2, opts) {
|
|
|
875
887
|
await owned.aclose();
|
|
876
888
|
}
|
|
877
889
|
}
|
|
878
|
-
var TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY, LLMClient;
|
|
890
|
+
var CALL_TIMEOUT_MS, TIMEOUT_MS, GEMINI_CALL_TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY, LLMClient;
|
|
879
891
|
var init_llm = __esm({
|
|
880
892
|
"src/providers/llm.ts"() {
|
|
881
893
|
init_errors();
|
|
882
|
-
|
|
894
|
+
CALL_TIMEOUT_MS = 24e4;
|
|
895
|
+
TIMEOUT_MS = CALL_TIMEOUT_MS;
|
|
896
|
+
GEMINI_CALL_TIMEOUT_MS = CALL_TIMEOUT_MS;
|
|
883
897
|
ANTHROPIC_VERSION = "2023-06-01";
|
|
884
898
|
ANTHROPIC_MAX_TOKENS = 4096;
|
|
885
899
|
OPENAI_FAMILY = /* @__PURE__ */ new Set(["openai", "azure_openai", "custom"]);
|
|
@@ -907,22 +921,30 @@ var init_llm = __esm({
|
|
|
907
921
|
await this.aclose();
|
|
908
922
|
}
|
|
909
923
|
async call(opts) {
|
|
910
|
-
const {
|
|
924
|
+
const {
|
|
925
|
+
system,
|
|
926
|
+
user,
|
|
927
|
+
jsonMode = false,
|
|
928
|
+
images = null,
|
|
929
|
+
maxTokens = null,
|
|
930
|
+
thinkingBudget = null,
|
|
931
|
+
temperature = null
|
|
932
|
+
} = opts;
|
|
911
933
|
if (this.provider === "anthropic") {
|
|
912
|
-
return this.callAnthropic(system, user, jsonMode, images);
|
|
934
|
+
return this.callAnthropic(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
913
935
|
}
|
|
914
936
|
if (OPENAI_FAMILY.has(this.provider)) {
|
|
915
|
-
return this.callOpenAI(system, user, jsonMode, images);
|
|
937
|
+
return this.callOpenAI(system, user, jsonMode, images, maxTokens, temperature);
|
|
916
938
|
}
|
|
917
939
|
if (this.provider === "gemini") {
|
|
918
|
-
return this.callGemini(system, user, jsonMode, images);
|
|
940
|
+
return this.callGemini(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
919
941
|
}
|
|
920
942
|
if (this.provider === "bedrock") {
|
|
921
|
-
return this.callBedrock(system, user, jsonMode, images);
|
|
943
|
+
return this.callBedrock(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
922
944
|
}
|
|
923
945
|
throw new Error(`unknown llm provider: ${JSON.stringify(this.provider)}`);
|
|
924
946
|
}
|
|
925
|
-
async callAnthropic(system, user, jsonMode, images) {
|
|
947
|
+
async callAnthropic(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
926
948
|
if (!this.fetchImpl) {
|
|
927
949
|
throw new Error("anthropic llm client has no fetch implementation");
|
|
928
950
|
}
|
|
@@ -943,26 +965,27 @@ Respond with valid JSON only.`;
|
|
|
943
965
|
});
|
|
944
966
|
}
|
|
945
967
|
content.push({ type: "text", text: user });
|
|
946
|
-
const
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
968
|
+
const body = {
|
|
969
|
+
model: this.model,
|
|
970
|
+
max_tokens: maxTokens ?? ANTHROPIC_MAX_TOKENS,
|
|
971
|
+
system,
|
|
972
|
+
messages: [{ role: "user", content }]
|
|
973
|
+
};
|
|
974
|
+
if (thinkingBudget) {
|
|
975
|
+
body.thinking = { type: "enabled", budget_tokens: thinkingBudget };
|
|
976
|
+
} else if (temperature !== null) {
|
|
977
|
+
body.temperature = temperature;
|
|
978
|
+
}
|
|
979
|
+
const data = await postJson(this.fetchImpl, "https://api.anthropic.com/v1/messages", body, {
|
|
980
|
+
"x-api-key": this.cfg.apiKey ?? "",
|
|
981
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
982
|
+
"content-type": "application/json"
|
|
983
|
+
});
|
|
961
984
|
const text = data.content[0].text;
|
|
962
985
|
const usage = data.usage ?? {};
|
|
963
986
|
return [text, { input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 }];
|
|
964
987
|
}
|
|
965
|
-
async callOpenAI(system, user, jsonMode, images) {
|
|
988
|
+
async callOpenAI(system, user, jsonMode, images, maxTokens, temperature = null) {
|
|
966
989
|
if (!this.client) {
|
|
967
990
|
throw new Error("openai-family llm client has no client");
|
|
968
991
|
}
|
|
@@ -983,12 +1006,18 @@ Respond with valid JSON only.`;
|
|
|
983
1006
|
if (jsonMode) {
|
|
984
1007
|
body.response_format = { type: "json_object" };
|
|
985
1008
|
}
|
|
1009
|
+
if (maxTokens) {
|
|
1010
|
+
body.max_completion_tokens = maxTokens;
|
|
1011
|
+
}
|
|
1012
|
+
if (temperature !== null) {
|
|
1013
|
+
body.temperature = temperature;
|
|
1014
|
+
}
|
|
986
1015
|
const resp = await this.client.chat.completions.create(body);
|
|
987
1016
|
const text = resp.choices[0]?.message?.content ?? "";
|
|
988
1017
|
const usage = resp.usage;
|
|
989
1018
|
return [text, { input: usage?.prompt_tokens ?? 0, output: usage?.completion_tokens ?? 0 }];
|
|
990
1019
|
}
|
|
991
|
-
async callGemini(system, user, jsonMode, images) {
|
|
1020
|
+
async callGemini(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
992
1021
|
if (!this.genaiClient) {
|
|
993
1022
|
this.genaiClient = await loadGeminiChatClient(this.cfg.apiKey);
|
|
994
1023
|
}
|
|
@@ -997,10 +1026,30 @@ Respond with valid JSON only.`;
|
|
|
997
1026
|
parts.push({ inlineData: { mimeType: "image/png", data: toBase64(img) } });
|
|
998
1027
|
}
|
|
999
1028
|
parts.push({ text: user });
|
|
1000
|
-
const config = {
|
|
1029
|
+
const config = {
|
|
1030
|
+
systemInstruction: system,
|
|
1031
|
+
// ALWAYS off, and not as a preference. Automatic function calling means
|
|
1032
|
+
// the SDK itself EXECUTES a callable it was handed as a tool and loops
|
|
1033
|
+
// on the result — up to ten round trips — before returning anything.
|
|
1034
|
+
// This package passes declarations only, so today nothing is executable;
|
|
1035
|
+
// but that depends on every future caller continuing to do the same, and
|
|
1036
|
+
// an application that gates tool execution behind human approval would
|
|
1037
|
+
// have that gate bypassed silently, by a library, with the loop already
|
|
1038
|
+
// run before it could object.
|
|
1039
|
+
automaticFunctionCalling: { disable: true }
|
|
1040
|
+
};
|
|
1001
1041
|
if (jsonMode) {
|
|
1002
1042
|
config.responseMimeType = "application/json";
|
|
1003
1043
|
}
|
|
1044
|
+
if (maxTokens) {
|
|
1045
|
+
config.maxOutputTokens = maxTokens;
|
|
1046
|
+
}
|
|
1047
|
+
if (temperature !== null) {
|
|
1048
|
+
config.temperature = temperature;
|
|
1049
|
+
}
|
|
1050
|
+
if (thinkingBudget !== null) {
|
|
1051
|
+
config.thinkingConfig = { thinkingBudget };
|
|
1052
|
+
}
|
|
1004
1053
|
const resp = await this.genaiClient.models.generateContent({
|
|
1005
1054
|
model: this.model,
|
|
1006
1055
|
contents: parts,
|
|
@@ -1012,7 +1061,7 @@ Respond with valid JSON only.`;
|
|
|
1012
1061
|
const outputTokens = usageMeta?.candidatesTokenCount ?? usageMeta?.candidates_token_count ?? 0;
|
|
1013
1062
|
return [text, { input: inputTokens || 0, output: outputTokens || 0 }];
|
|
1014
1063
|
}
|
|
1015
|
-
async callBedrock(system, user, jsonMode, images) {
|
|
1064
|
+
async callBedrock(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
1016
1065
|
const { BedrockRuntimeClient, ConverseCommand } = await loadBedrockSdk();
|
|
1017
1066
|
if (jsonMode) {
|
|
1018
1067
|
system = `${system}
|
|
@@ -1036,7 +1085,21 @@ Respond with valid JSON only.`;
|
|
|
1036
1085
|
new ConverseCommand({
|
|
1037
1086
|
modelId: this.model,
|
|
1038
1087
|
system: [{ text: system }],
|
|
1039
|
-
messages: [{ role: "user", content }]
|
|
1088
|
+
messages: [{ role: "user", content }],
|
|
1089
|
+
...maxTokens || temperature !== null ? {
|
|
1090
|
+
inferenceConfig: {
|
|
1091
|
+
...maxTokens ? { maxTokens } : {},
|
|
1092
|
+
...temperature !== null ? { temperature } : {}
|
|
1093
|
+
}
|
|
1094
|
+
} : {},
|
|
1095
|
+
// Anthropic-style passthrough — Converse forwards it to the model.
|
|
1096
|
+
// Zero (the vision transcription contract) sends nothing: thinking
|
|
1097
|
+
// is opt-in for the anthropic models bedrock hosts.
|
|
1098
|
+
...thinkingBudget ? {
|
|
1099
|
+
additionalModelRequestFields: {
|
|
1100
|
+
thinking: { type: "enabled", budget_tokens: thinkingBudget }
|
|
1101
|
+
}
|
|
1102
|
+
} : {}
|
|
1040
1103
|
})
|
|
1041
1104
|
);
|
|
1042
1105
|
const text = result.output?.message?.content?.[0]?.text ?? "";
|
|
@@ -1049,6 +1112,302 @@ Respond with valid JSON only.`;
|
|
|
1049
1112
|
};
|
|
1050
1113
|
}
|
|
1051
1114
|
});
|
|
1115
|
+
function encryptDict(data, key) {
|
|
1116
|
+
const nonce = randomBytes(12);
|
|
1117
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
1118
|
+
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
1119
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1120
|
+
const tag = cipher.getAuthTag();
|
|
1121
|
+
return Buffer.concat([nonce, ciphertext, tag]).toString("base64");
|
|
1122
|
+
}
|
|
1123
|
+
function decryptDict(token, key) {
|
|
1124
|
+
const raw = Buffer.from(token, "base64");
|
|
1125
|
+
const nonce = raw.subarray(0, 12);
|
|
1126
|
+
const tag = raw.subarray(raw.length - 16);
|
|
1127
|
+
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
1128
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
1129
|
+
decipher.setAuthTag(tag);
|
|
1130
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1131
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
1132
|
+
}
|
|
1133
|
+
function getSecretKey(config) {
|
|
1134
|
+
const secretKey = config.secretKey;
|
|
1135
|
+
if (!secretKey) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
"ContextEngineConfig.secretKey (env CE_SECRET_KEY) is not configured \u2014 a base64url-encoded 32-byte AES key is required to encrypt/decrypt tool configs that hold secrets."
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
return Buffer.from(secretKey, "base64url");
|
|
1141
|
+
}
|
|
1142
|
+
function hmacSha256Hex(key, value) {
|
|
1143
|
+
const k = typeof key === "string" ? Buffer.from(key) : key;
|
|
1144
|
+
return createHmac("sha256", k).update(value, "utf8").digest("hex");
|
|
1145
|
+
}
|
|
1146
|
+
var init_crypto = __esm({
|
|
1147
|
+
"src/crypto.ts"() {
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// src/redaction.ts
|
|
1152
|
+
function spansFrom(re, text) {
|
|
1153
|
+
const out = [];
|
|
1154
|
+
re.lastIndex = 0;
|
|
1155
|
+
let m = re.exec(text);
|
|
1156
|
+
while (m !== null) {
|
|
1157
|
+
out.push([m.index, m.index + m[0].length]);
|
|
1158
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
1159
|
+
m = re.exec(text);
|
|
1160
|
+
}
|
|
1161
|
+
return out;
|
|
1162
|
+
}
|
|
1163
|
+
function luhnOk(digits) {
|
|
1164
|
+
let total = 0;
|
|
1165
|
+
const rev = [...digits].reverse();
|
|
1166
|
+
for (let i = 0; i < rev.length; i++) {
|
|
1167
|
+
let d = Number(rev[i]);
|
|
1168
|
+
if (i % 2 === 1) {
|
|
1169
|
+
d *= 2;
|
|
1170
|
+
if (d > 9) d -= 9;
|
|
1171
|
+
}
|
|
1172
|
+
total += d;
|
|
1173
|
+
}
|
|
1174
|
+
return total % 10 === 0;
|
|
1175
|
+
}
|
|
1176
|
+
function detectCreditCard(text) {
|
|
1177
|
+
const spans = [];
|
|
1178
|
+
for (const [start, end] of spansFrom(CARD_RE, text)) {
|
|
1179
|
+
const digits = text.slice(start, end).replace(/[ -]/g, "");
|
|
1180
|
+
if (digits.length >= 13 && digits.length <= 19 && luhnOk(digits)) {
|
|
1181
|
+
spans.push([start, end]);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return spans;
|
|
1185
|
+
}
|
|
1186
|
+
function looksLikeGenericSecret(token) {
|
|
1187
|
+
if (UUID_RE.test(token)) return false;
|
|
1188
|
+
let hasUpper = false;
|
|
1189
|
+
let hasLower = false;
|
|
1190
|
+
let hasDigit = false;
|
|
1191
|
+
for (const c of token) {
|
|
1192
|
+
if (c >= "A" && c <= "Z") hasUpper = true;
|
|
1193
|
+
else if (c >= "a" && c <= "z") hasLower = true;
|
|
1194
|
+
else if (c >= "0" && c <= "9") hasDigit = true;
|
|
1195
|
+
}
|
|
1196
|
+
return hasUpper && hasLower && hasDigit;
|
|
1197
|
+
}
|
|
1198
|
+
function detectApiKey(text) {
|
|
1199
|
+
const spans = spansFrom(API_KEY_PREFIX_RE, text);
|
|
1200
|
+
for (const [start, end] of spansFrom(API_KEY_GENERIC_RE, text)) {
|
|
1201
|
+
if (spans.some(([s, e]) => start < e && s < end)) continue;
|
|
1202
|
+
if (looksLikeGenericSecret(text.slice(start, end))) spans.push([start, end]);
|
|
1203
|
+
}
|
|
1204
|
+
return spans;
|
|
1205
|
+
}
|
|
1206
|
+
function detectBuiltin(name, text) {
|
|
1207
|
+
const detector = BUILTIN[name];
|
|
1208
|
+
if (!detector) throw new Error(`unknown built-in detector: ${name}`);
|
|
1209
|
+
if (!text) return [];
|
|
1210
|
+
return detector(text).sort((a, b) => a[0] - b[0]);
|
|
1211
|
+
}
|
|
1212
|
+
function ruleApplies(rule, opts) {
|
|
1213
|
+
if (rule.applyAt !== "both" && rule.applyAt !== opts.phase) return false;
|
|
1214
|
+
if (opts.phase === "ingest") return true;
|
|
1215
|
+
if (!rule.unless.length) return true;
|
|
1216
|
+
if (opts.principals === null) return false;
|
|
1217
|
+
const held = new Set(opts.principals);
|
|
1218
|
+
return !rule.unless.some((p) => held.has(p));
|
|
1219
|
+
}
|
|
1220
|
+
function spansForRule(rule, text, policy) {
|
|
1221
|
+
if (rule.pattern !== null) {
|
|
1222
|
+
const re = rule.patternRe() ?? new RegExp(rule.pattern, "g");
|
|
1223
|
+
return spansFrom(re, text);
|
|
1224
|
+
}
|
|
1225
|
+
if (rule.detector !== null) {
|
|
1226
|
+
const custom = policy.customDetectors[rule.detector];
|
|
1227
|
+
if (custom) return [...custom(text)];
|
|
1228
|
+
return detectBuiltin(rule.detector, text);
|
|
1229
|
+
}
|
|
1230
|
+
return [];
|
|
1231
|
+
}
|
|
1232
|
+
function hashToken(value, secretKey) {
|
|
1233
|
+
const key = Buffer.isBuffer(secretKey) ? secretKey : Buffer.from(String(secretKey ?? ""));
|
|
1234
|
+
return hmacSha256Hex(key, value).slice(0, HASH_TOKEN_CHARS);
|
|
1235
|
+
}
|
|
1236
|
+
function applyRedaction(text, policy, opts) {
|
|
1237
|
+
if (!text || policy.isEmpty()) return [text, {}];
|
|
1238
|
+
const principals = opts.principals ?? null;
|
|
1239
|
+
const collected = [];
|
|
1240
|
+
const fired = [];
|
|
1241
|
+
const failed = [];
|
|
1242
|
+
for (const rule of policy.rules) {
|
|
1243
|
+
if (!ruleApplies(rule, { phase: opts.phase, principals })) continue;
|
|
1244
|
+
if (rule.action === "hash" && !opts.secretKey) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
`rule '${rule.name}': action='hash' requires a non-empty secretKey (an unkeyed HMAC is a reversible pseudonym, not a redaction)`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
const raw = spansForRule(rule, text, policy);
|
|
1251
|
+
const ruleSpans = [];
|
|
1252
|
+
let invalid = false;
|
|
1253
|
+
for (const item of raw) {
|
|
1254
|
+
const start = item?.[0];
|
|
1255
|
+
const end = item?.[1];
|
|
1256
|
+
if (typeof start !== "number" || typeof end !== "number") {
|
|
1257
|
+
invalid = true;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (!(start >= 0 && start < end && end <= text.length)) {
|
|
1261
|
+
invalid = true;
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
ruleSpans.push([start, end]);
|
|
1265
|
+
}
|
|
1266
|
+
if (invalid) failed.push(rule.name);
|
|
1267
|
+
for (const [s, e] of ruleSpans) collected.push([s, e, rule]);
|
|
1268
|
+
} catch (exc) {
|
|
1269
|
+
failed.push(rule.name);
|
|
1270
|
+
if (opts.hooks) emitError(opts.hooks, exc, { stage: "redaction", rule: rule.name });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (!collected.length) {
|
|
1274
|
+
if (failed.length) return [text, { rules_fired: [], spans: 0, rules_failed: failed }];
|
|
1275
|
+
return [text, {}];
|
|
1276
|
+
}
|
|
1277
|
+
collected.sort((a, b) => a[0] - b[0] || b[1] - b[0] - (a[1] - a[0]));
|
|
1278
|
+
const merged = [];
|
|
1279
|
+
for (const [start, end, rule] of collected) {
|
|
1280
|
+
const last = merged[merged.length - 1];
|
|
1281
|
+
if (last && start < last[1]) {
|
|
1282
|
+
if (end > last[1]) last[1] = end;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
merged.push([start, end, rule]);
|
|
1286
|
+
}
|
|
1287
|
+
const out = [];
|
|
1288
|
+
let cursor = 0;
|
|
1289
|
+
for (const [start, end, rule] of merged) {
|
|
1290
|
+
out.push(text.slice(cursor, start));
|
|
1291
|
+
const original = text.slice(start, end);
|
|
1292
|
+
if (rule.action === "mask") out.push(rule.effectivePlaceholder());
|
|
1293
|
+
else if (rule.action === "hash") {
|
|
1294
|
+
out.push(`[${rule.name.toUpperCase()}:${hashToken(original, opts.secretKey)}]`);
|
|
1295
|
+
}
|
|
1296
|
+
if (!fired.includes(rule.name)) fired.push(rule.name);
|
|
1297
|
+
cursor = end;
|
|
1298
|
+
}
|
|
1299
|
+
out.push(text.slice(cursor));
|
|
1300
|
+
const note = { rules_fired: fired, spans: merged.length };
|
|
1301
|
+
if (failed.length) note.rules_failed = failed;
|
|
1302
|
+
return [out.join(""), note];
|
|
1303
|
+
}
|
|
1304
|
+
var BUILTIN_DETECTOR_NAMES, RedactionRule, RedactionPolicy, EMAIL_RE, PHONE_RE, SSN_RE, CARD_RE, IBAN_RE, API_KEY_ALNUM, API_KEY_PREFIX_RE, API_KEY_GENERIC_RE, UUID_RE, BUILTIN, HASH_TOKEN_CHARS;
|
|
1305
|
+
var init_redaction = __esm({
|
|
1306
|
+
"src/redaction.ts"() {
|
|
1307
|
+
init_crypto();
|
|
1308
|
+
init_hooks();
|
|
1309
|
+
BUILTIN_DETECTOR_NAMES = /* @__PURE__ */ new Set(["email", "phone", "ssn", "credit_card", "iban", "api_key"]);
|
|
1310
|
+
RedactionRule = class {
|
|
1311
|
+
name;
|
|
1312
|
+
detector;
|
|
1313
|
+
pattern;
|
|
1314
|
+
field;
|
|
1315
|
+
action;
|
|
1316
|
+
placeholder;
|
|
1317
|
+
applyAt;
|
|
1318
|
+
unless;
|
|
1319
|
+
compiled = null;
|
|
1320
|
+
constructor(init) {
|
|
1321
|
+
this.name = init.name;
|
|
1322
|
+
this.detector = init.detector ?? null;
|
|
1323
|
+
this.pattern = init.pattern ?? null;
|
|
1324
|
+
this.field = init.field ?? null;
|
|
1325
|
+
this.action = init.action ?? "mask";
|
|
1326
|
+
this.placeholder = init.placeholder ?? null;
|
|
1327
|
+
this.applyAt = init.applyAt ?? "output";
|
|
1328
|
+
this.unless = init.unless ?? [];
|
|
1329
|
+
this.validate();
|
|
1330
|
+
}
|
|
1331
|
+
validate() {
|
|
1332
|
+
if (this.field !== null) {
|
|
1333
|
+
throw new Error(
|
|
1334
|
+
`rule '${this.name}': field-targeted rules are not implemented yet; use detector or pattern instead`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
const targets = [this.detector, this.pattern].filter((t) => t !== null);
|
|
1338
|
+
if (targets.length !== 1) {
|
|
1339
|
+
throw new Error(`rule '${this.name}': exactly one of detector/pattern must be set`);
|
|
1340
|
+
}
|
|
1341
|
+
if (this.pattern !== null) {
|
|
1342
|
+
try {
|
|
1343
|
+
this.compiled = new RegExp(this.pattern, "g");
|
|
1344
|
+
} catch (exc) {
|
|
1345
|
+
throw new Error(`rule '${this.name}': invalid regex: ${exc}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (this.unless.length && this.applyAt === "ingest") {
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
`rule '${this.name}': unless is output-time only and cannot be set on an applyAt='ingest' rule`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
patternRe() {
|
|
1355
|
+
return this.compiled;
|
|
1356
|
+
}
|
|
1357
|
+
effectivePlaceholder() {
|
|
1358
|
+
return this.placeholder || `[${this.name.toUpperCase()}]`;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
RedactionPolicy = class {
|
|
1362
|
+
rules;
|
|
1363
|
+
customDetectors;
|
|
1364
|
+
constructor(init = {}) {
|
|
1365
|
+
this.rules = (init.rules ?? []).map((r) => r instanceof RedactionRule ? r : new RedactionRule(r));
|
|
1366
|
+
this.customDetectors = init.customDetectors ?? {};
|
|
1367
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1368
|
+
for (const rule of this.rules) {
|
|
1369
|
+
if (seen.has(rule.name)) throw new Error(`duplicate rule name: '${rule.name}'`);
|
|
1370
|
+
seen.add(rule.name);
|
|
1371
|
+
if (rule.detector !== null) {
|
|
1372
|
+
const known = BUILTIN_DETECTOR_NAMES.has(rule.detector) || rule.detector in this.customDetectors;
|
|
1373
|
+
if (!known) {
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
`rule '${rule.name}': unknown detector '${rule.detector}' (not built-in and not in customDetectors)`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
isEmpty() {
|
|
1382
|
+
return this.rules.length === 0;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
1386
|
+
PHONE_RE = /\+\d[\d\s-]{7,17}\d/g;
|
|
1387
|
+
SSN_RE = /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g;
|
|
1388
|
+
CARD_RE = /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g;
|
|
1389
|
+
IBAN_RE = /(?<![A-Za-z0-9])[A-Z]{2}\d{2}[A-Z0-9]{10,30}(?![A-Za-z0-9])/g;
|
|
1390
|
+
API_KEY_ALNUM = "A-Za-z0-9_\\-+/=";
|
|
1391
|
+
API_KEY_PREFIX_RE = new RegExp(
|
|
1392
|
+
`(?<![${API_KEY_ALNUM}])(?:(?:AKIA|ASIA)[0-9A-Z]{16}|sk-[A-Za-z0-9]{20,}|gh[opsu]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9\\-]{10,}|AIza[0-9A-Za-z_\\-]{35})(?![${API_KEY_ALNUM}])`,
|
|
1393
|
+
"g"
|
|
1394
|
+
);
|
|
1395
|
+
API_KEY_GENERIC_RE = new RegExp(
|
|
1396
|
+
`(?<![${API_KEY_ALNUM}])[${API_KEY_ALNUM}]{24,}(?![${API_KEY_ALNUM}])`,
|
|
1397
|
+
"g"
|
|
1398
|
+
);
|
|
1399
|
+
UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1400
|
+
BUILTIN = {
|
|
1401
|
+
email: (t) => spansFrom(EMAIL_RE, t),
|
|
1402
|
+
phone: (t) => spansFrom(PHONE_RE, t),
|
|
1403
|
+
ssn: (t) => spansFrom(SSN_RE, t),
|
|
1404
|
+
credit_card: detectCreditCard,
|
|
1405
|
+
iban: (t) => spansFrom(IBAN_RE, t),
|
|
1406
|
+
api_key: detectApiKey
|
|
1407
|
+
};
|
|
1408
|
+
HASH_TOKEN_CHARS = 16;
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1052
1411
|
function stripFences(text) {
|
|
1053
1412
|
let t = text.trim();
|
|
1054
1413
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(t);
|
|
@@ -1084,17 +1443,38 @@ var init_json = __esm({
|
|
|
1084
1443
|
"src/extraction/json.ts"() {
|
|
1085
1444
|
}
|
|
1086
1445
|
});
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
function
|
|
1097
|
-
|
|
1446
|
+
|
|
1447
|
+
// src/extras.ts
|
|
1448
|
+
async function tryImport(specifier) {
|
|
1449
|
+
try {
|
|
1450
|
+
return await import(specifier);
|
|
1451
|
+
} catch {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function requireExtra(specifier, extra, what) {
|
|
1456
|
+
try {
|
|
1457
|
+
return await import(specifier);
|
|
1458
|
+
} catch (_err) {
|
|
1459
|
+
throw new ExtraMissingError(extra, specifier, what);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
var init_extras = __esm({
|
|
1463
|
+
"src/extras.ts"() {
|
|
1464
|
+
init_errors();
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
function getFileExtension(filename) {
|
|
1468
|
+
if (!filename?.includes(".")) return "";
|
|
1469
|
+
return filename.slice(filename.lastIndexOf(".")).toLowerCase();
|
|
1470
|
+
}
|
|
1471
|
+
function isNonIngestibleMedia(filename, mime) {
|
|
1472
|
+
const m = (mime || "").toLowerCase();
|
|
1473
|
+
if (m.startsWith("audio/") || m.startsWith("video/")) return true;
|
|
1474
|
+
return NON_INGESTIBLE_MEDIA_EXTS.has(getFileExtension(filename));
|
|
1475
|
+
}
|
|
1476
|
+
function isZip(content) {
|
|
1477
|
+
return content.length >= 4 && content.subarray(0, 4).equals(ZIP_MAGIC);
|
|
1098
1478
|
}
|
|
1099
1479
|
function decodeXmlEntities(s) {
|
|
1100
1480
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
@@ -1474,25 +1854,182 @@ var init_files = __esm({
|
|
|
1474
1854
|
XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
1475
1855
|
}
|
|
1476
1856
|
});
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1857
|
+
function camelize(key) {
|
|
1858
|
+
return key.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1859
|
+
}
|
|
1860
|
+
function loadCeEnv() {
|
|
1861
|
+
const root = {};
|
|
1862
|
+
for (const [raw, value] of Object.entries(process.env)) {
|
|
1863
|
+
if (!raw.startsWith("CE_") || value === void 0) continue;
|
|
1864
|
+
const path = raw.slice(3).split("__").map(camelize);
|
|
1865
|
+
let cur = root;
|
|
1866
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1867
|
+
const k = path[i];
|
|
1868
|
+
const next = cur[k];
|
|
1869
|
+
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
1870
|
+
cur = cur[k];
|
|
1871
|
+
}
|
|
1872
|
+
cur[path[path.length - 1]] = coerceEnv(value);
|
|
1484
1873
|
}
|
|
1874
|
+
return root;
|
|
1485
1875
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1876
|
+
function coerceEnv(value) {
|
|
1877
|
+
if (value === "true") return true;
|
|
1878
|
+
if (value === "false") return false;
|
|
1879
|
+
if (/^-?\d+$/.test(value)) return Number(value);
|
|
1880
|
+
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
|
|
1881
|
+
return value;
|
|
1882
|
+
}
|
|
1883
|
+
function deepMerge(a, b) {
|
|
1884
|
+
const out = { ...a };
|
|
1885
|
+
for (const [k, v] of Object.entries(b)) {
|
|
1886
|
+
if (v === void 0) continue;
|
|
1887
|
+
const existing = out[k];
|
|
1888
|
+
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
1889
|
+
out[k] = deepMerge(existing, v);
|
|
1890
|
+
} else {
|
|
1891
|
+
out[k] = v;
|
|
1892
|
+
}
|
|
1491
1893
|
}
|
|
1894
|
+
return out;
|
|
1492
1895
|
}
|
|
1493
|
-
var
|
|
1494
|
-
|
|
1495
|
-
|
|
1896
|
+
var embeddingSchema, llmSchema, graphSchema, rerankerSchema, fusionSchema, storageSchema, extractionSchema, ContextEngineConfig;
|
|
1897
|
+
var init_config = __esm({
|
|
1898
|
+
"src/config.ts"() {
|
|
1899
|
+
init_redaction();
|
|
1900
|
+
embeddingSchema = z.object({
|
|
1901
|
+
provider: z.enum(["openai", "azure_openai", "gemini", "voyage", "cohere", "custom"]),
|
|
1902
|
+
model: z.string(),
|
|
1903
|
+
dim: z.number().int().positive().nullable().optional().default(null),
|
|
1904
|
+
apiKey: z.string().nullable().optional().default(null),
|
|
1905
|
+
baseUrl: z.string().nullable().optional().default(null)
|
|
1906
|
+
});
|
|
1907
|
+
llmSchema = z.object({
|
|
1908
|
+
provider: z.enum(["anthropic", "openai", "azure_openai", "gemini", "bedrock", "custom"]),
|
|
1909
|
+
model: z.string(),
|
|
1910
|
+
apiKey: z.string().nullable().optional().default(null),
|
|
1911
|
+
baseUrl: z.string().nullable().optional().default(null)
|
|
1912
|
+
});
|
|
1913
|
+
graphSchema = z.object({
|
|
1914
|
+
enabled: z.boolean().default(false),
|
|
1915
|
+
neo4jUri: z.string().nullable().optional().default(null),
|
|
1916
|
+
neo4jUser: z.string().default("neo4j"),
|
|
1917
|
+
neo4jPassword: z.string().nullable().optional().default(null),
|
|
1918
|
+
neo4jDatabase: z.string().default("neo4j"),
|
|
1919
|
+
extractionLlm: llmSchema.nullable().optional().default(null)
|
|
1920
|
+
});
|
|
1921
|
+
rerankerSchema = z.object({
|
|
1922
|
+
enabled: z.boolean().default(false),
|
|
1923
|
+
provider: z.enum(["cohere", "voyage", "jina", "custom"]).nullable().optional().default(null),
|
|
1924
|
+
model: z.string().nullable().optional().default(null),
|
|
1925
|
+
apiKey: z.string().nullable().optional().default(null),
|
|
1926
|
+
baseUrl: z.string().nullable().optional().default(null),
|
|
1927
|
+
candidates: z.number().int().positive().default(50)
|
|
1928
|
+
});
|
|
1929
|
+
fusionSchema = z.object({
|
|
1930
|
+
method: z.literal("rrf").default("rrf"),
|
|
1931
|
+
k: z.number().int().positive().default(60),
|
|
1932
|
+
weights: z.record(z.string(), z.number()).default({ fts: 1, trgm: 0.8, ann: 1, graph: 1 })
|
|
1933
|
+
});
|
|
1934
|
+
storageSchema = z.object({
|
|
1935
|
+
backend: z.literal("postgres").default("postgres"),
|
|
1936
|
+
annExactThreshold: z.number().int().positive().default(5e4),
|
|
1937
|
+
// Connection pool, passed straight through to `pg.Pool`. Sizing is a
|
|
1938
|
+
// deployment decision (how many workers times how many concurrent searches
|
|
1939
|
+
// each runs, against what the database allows), so these are pg's own
|
|
1940
|
+
// defaults rather than a guess at your topology. (`pg` has no pre-ping.)
|
|
1941
|
+
//
|
|
1942
|
+
// Connections kept open in the pool.
|
|
1943
|
+
poolMax: z.number().int().positive().default(10),
|
|
1944
|
+
// Milliseconds an idle connection is kept before it is closed.
|
|
1945
|
+
poolIdleTimeoutMs: z.number().int().nonnegative().default(1e4),
|
|
1946
|
+
// Milliseconds a caller waits for a connection before the attempt fails.
|
|
1947
|
+
poolConnectionTimeoutMs: z.number().int().nonnegative().default(3e4)
|
|
1948
|
+
});
|
|
1949
|
+
extractionSchema = z.object({
|
|
1950
|
+
// Ceiling on an image's LONG EDGE in pixels, for rendered PDF pages and
|
|
1951
|
+
// for images sent to the vision LLM.
|
|
1952
|
+
maxRenderPx: z.number().int().positive().default(2e3),
|
|
1953
|
+
// Ceiling on ONE vision batch's transcription, in output tokens — a reply
|
|
1954
|
+
// far past a batch's honest bound is a repetition loop. Raising it is the
|
|
1955
|
+
// fix if very dense pages come back truncated.
|
|
1956
|
+
visionMaxOutputTokens: z.number().int().positive().default(16e3),
|
|
1957
|
+
// Pages per vision-LLM call. Smaller batches mean more calls but more
|
|
1958
|
+
// concurrency and less risk of hitting the output ceiling or a provider
|
|
1959
|
+
// deadline; larger batches the reverse. Measure on your own corpus.
|
|
1960
|
+
pagesPerVisionBatch: z.number().int().positive().default(5),
|
|
1961
|
+
// Language hint for the Tesseract OCR fallback ('en', 'ur', 'mixed', any
|
|
1962
|
+
// Tesseract code, or 'auto' for script detection). null (the default)
|
|
1963
|
+
// detects the document's language from its usable text-layer pages and
|
|
1964
|
+
// falls back to 'auto' when there is nothing to sample.
|
|
1965
|
+
ocrLanguage: z.string().nullable().default(null)
|
|
1966
|
+
});
|
|
1967
|
+
ContextEngineConfig = class _ContextEngineConfig {
|
|
1968
|
+
databaseUrl;
|
|
1969
|
+
storage;
|
|
1970
|
+
defaultMode;
|
|
1971
|
+
embedding;
|
|
1972
|
+
llm;
|
|
1973
|
+
visionLlm;
|
|
1974
|
+
graph;
|
|
1975
|
+
reranker;
|
|
1976
|
+
fusion;
|
|
1977
|
+
extraction;
|
|
1978
|
+
enableCodeExecution;
|
|
1979
|
+
secretKey;
|
|
1980
|
+
/**
|
|
1981
|
+
* Lets http tools and probes reach loopback/private/link-local/metadata
|
|
1982
|
+
* addresses. Off by default — an http tool is registered by a caller, and a
|
|
1983
|
+
* private destination is server-side request forgery. See `tools/egress.ts`.
|
|
1984
|
+
*/
|
|
1985
|
+
allowPrivateEgress;
|
|
1986
|
+
redaction;
|
|
1987
|
+
constructor(init) {
|
|
1988
|
+
this.databaseUrl = init.databaseUrl;
|
|
1989
|
+
this.storage = storageSchema.parse(init.storage ?? {});
|
|
1990
|
+
this.defaultMode = init.defaultMode ?? "hybrid";
|
|
1991
|
+
this.embedding = embeddingSchema.parse(init.embedding);
|
|
1992
|
+
this.llm = init.llm ? llmSchema.parse(init.llm) : null;
|
|
1993
|
+
this.visionLlm = init.visionLlm ? llmSchema.parse(init.visionLlm) : null;
|
|
1994
|
+
this.graph = graphSchema.parse(init.graph ?? {});
|
|
1995
|
+
this.reranker = rerankerSchema.parse(init.reranker ?? {});
|
|
1996
|
+
this.fusion = fusionSchema.parse(init.fusion ?? {});
|
|
1997
|
+
this.extraction = extractionSchema.parse(init.extraction ?? {});
|
|
1998
|
+
this.enableCodeExecution = init.enableCodeExecution ?? false;
|
|
1999
|
+
this.secretKey = init.secretKey ?? null;
|
|
2000
|
+
this.allowPrivateEgress = init.allowPrivateEgress ?? false;
|
|
2001
|
+
this.redaction = init.redaction instanceof RedactionPolicy ? init.redaction : new RedactionPolicy(init.redaction ?? {});
|
|
2002
|
+
this.validate();
|
|
2003
|
+
}
|
|
2004
|
+
validate() {
|
|
2005
|
+
if (this.graph.enabled && !(this.graph.neo4jUri && this.graph.neo4jPassword && this.graph.extractionLlm)) {
|
|
2006
|
+
throw new Error("graph enabled but neo4jUri/neo4jPassword/extractionLlm missing");
|
|
2007
|
+
}
|
|
2008
|
+
if (this.reranker.enabled && !(this.reranker.provider && this.reranker.apiKey)) {
|
|
2009
|
+
throw new Error("reranker enabled but provider/apiKey missing");
|
|
2010
|
+
}
|
|
2011
|
+
if (this.defaultMode === "graph" && !this.graph.enabled) {
|
|
2012
|
+
throw new Error("defaultMode is 'graph' but graph enabled is false");
|
|
2013
|
+
}
|
|
2014
|
+
for (const rule of this.redaction.rules) {
|
|
2015
|
+
if (rule.action === "hash" && !this.secretKey) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
`redaction rule '${rule.name}': action='hash' requires ContextEngineConfig.secretKey to be set`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
static fromEnv(overrides = {}) {
|
|
2023
|
+
const env = loadCeEnv();
|
|
2024
|
+
const merged = deepMerge(env, overrides);
|
|
2025
|
+
if (!merged.databaseUrl || !merged.embedding) {
|
|
2026
|
+
throw new Error(
|
|
2027
|
+
"ContextEngineConfig.fromEnv requires CE_DATABASE_URL and CE_EMBEDDING__PROVIDER/MODEL (or explicit overrides)"
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
return new _ContextEngineConfig(merged);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
1496
2033
|
}
|
|
1497
2034
|
});
|
|
1498
2035
|
|
|
@@ -1921,8 +2458,26 @@ __export(pdf_exports, {
|
|
|
1921
2458
|
embeddedTextPerPage: () => embeddedTextPerPage,
|
|
1922
2459
|
extract: () => extract,
|
|
1923
2460
|
extractPdf: () => extract,
|
|
2461
|
+
ocrLanguageHint: () => ocrLanguageHint,
|
|
1924
2462
|
pageCount: () => pageCount
|
|
1925
2463
|
});
|
|
2464
|
+
function ocrLanguageHint(texts, override) {
|
|
2465
|
+
if (override) return override;
|
|
2466
|
+
let sample = "";
|
|
2467
|
+
const indices = [...texts.keys()].sort((a, b) => a - b).slice(0, 3);
|
|
2468
|
+
for (const idx of indices) {
|
|
2469
|
+
sample += texts.get(idx) ?? "";
|
|
2470
|
+
if (sample.length >= OCR_LANG_SAMPLE_MIN) break;
|
|
2471
|
+
}
|
|
2472
|
+
if (sample.length >= OCR_LANG_SAMPLE_MIN) {
|
|
2473
|
+
try {
|
|
2474
|
+
const detected = detectLanguage(sample, 30);
|
|
2475
|
+
if (detected) return detected;
|
|
2476
|
+
} catch {
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return "auto";
|
|
2480
|
+
}
|
|
1926
2481
|
function isCcControl(ch) {
|
|
1927
2482
|
if (ch === "\n" || ch === "\r" || ch === " ") return false;
|
|
1928
2483
|
const cp = ch.codePointAt(0);
|
|
@@ -1975,16 +2530,27 @@ async function extract(content, opts) {
|
|
|
1975
2530
|
const { pages, structured: structuredMarkdown } = await classifyPages(content);
|
|
1976
2531
|
const providerTokens = {};
|
|
1977
2532
|
let visionMarkdown = false;
|
|
2533
|
+
let benignBlank = /* @__PURE__ */ new Set();
|
|
1978
2534
|
if (pages.needsOcr.length) {
|
|
1979
2535
|
if (opts.visionLlm) {
|
|
1980
2536
|
try {
|
|
1981
2537
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
1982
|
-
const [visionTexts, tokens] = await vision.extractPdfPages(content, pages.needsOcr, {
|
|
1983
|
-
visionLlm: opts.visionLlm
|
|
2538
|
+
const [visionTexts, tokens, outcomes] = await vision.extractPdfPages(content, pages.needsOcr, {
|
|
2539
|
+
visionLlm: opts.visionLlm,
|
|
2540
|
+
extraction: opts.extraction
|
|
1984
2541
|
});
|
|
1985
2542
|
for (const [idx, text] of Object.entries(visionTexts)) {
|
|
1986
2543
|
pages.texts.set(Number(idx), text);
|
|
1987
2544
|
}
|
|
2545
|
+
benignBlank = new Set(
|
|
2546
|
+
Object.entries(outcomes).filter(([, o]) => o.status === "blank").map(([idx]) => Number(idx))
|
|
2547
|
+
);
|
|
2548
|
+
const failed = Object.entries(outcomes).filter(([, o]) => o.status === "failed");
|
|
2549
|
+
if (failed.length) {
|
|
2550
|
+
console.warn(
|
|
2551
|
+
`[pdf] vision failed on ${failed.length} of ${pages.needsOcr.length} pages: ` + failed.map(([idx, o]) => `page ${Number(idx) + 1}: ${o.error}`).join("; ")
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
1988
2554
|
visionMarkdown = Object.keys(visionTexts).length > 0;
|
|
1989
2555
|
for (const [key, val] of Object.entries(tokens)) {
|
|
1990
2556
|
providerTokens[key] = (providerTokens[key] ?? 0) + val;
|
|
@@ -1996,9 +2562,17 @@ async function extract(content, opts) {
|
|
|
1996
2562
|
try {
|
|
1997
2563
|
const ocr = await Promise.resolve().then(() => (init_ocr(), ocr_exports));
|
|
1998
2564
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
1999
|
-
const rendered = await vision.renderPdfPagesAsImages(
|
|
2565
|
+
const rendered = await vision.renderPdfPagesAsImages(
|
|
2566
|
+
content,
|
|
2567
|
+
pages.needsOcr,
|
|
2568
|
+
150,
|
|
2569
|
+
opts.extraction?.maxRenderPx ?? vision.MAX_RENDER_PX
|
|
2570
|
+
);
|
|
2571
|
+
const ocrConfig = {
|
|
2572
|
+
language: ocrLanguageHint(pages.texts, opts.extraction?.ocrLanguage)
|
|
2573
|
+
};
|
|
2000
2574
|
for (const [idx, pngBytes] of rendered) {
|
|
2001
|
-
const result = await ocr.extractImageTextOcr(pngBytes);
|
|
2575
|
+
const result = await ocr.extractImageTextOcr(pngBytes, ocrConfig);
|
|
2002
2576
|
if (result.text.trim()) pages.texts.set(idx, result.text.trim());
|
|
2003
2577
|
}
|
|
2004
2578
|
} catch (exc) {
|
|
@@ -2016,31 +2590,38 @@ async function extract(content, opts) {
|
|
|
2016
2590
|
if (t) textParts.push(`--- Page ${i + 1} ---
|
|
2017
2591
|
${t}`);
|
|
2018
2592
|
}
|
|
2593
|
+
const unread = pages.needsOcr.filter((i) => !pages.texts.get(i) && !benignBlank.has(i));
|
|
2019
2594
|
return new Extracted2({
|
|
2020
2595
|
text: textParts.join("\n\n"),
|
|
2021
2596
|
pages: pages.total,
|
|
2022
2597
|
slides: null,
|
|
2023
2598
|
mediaOnly: false,
|
|
2024
2599
|
providerTokens,
|
|
2025
|
-
isMarkdown: visionMarkdown || structuredMarkdown
|
|
2600
|
+
isMarkdown: visionMarkdown || structuredMarkdown,
|
|
2601
|
+
unreadableReason: unread.length ? opts.visionLlm ? "vision_failed" : "needs_vision" : null,
|
|
2602
|
+
unreadablePages: unread.length
|
|
2026
2603
|
});
|
|
2027
2604
|
}
|
|
2028
|
-
var GARBAGE_SCAN_LIMIT, MIN_USABLE_TEXT_LEN;
|
|
2605
|
+
var GARBAGE_SCAN_LIMIT, MIN_USABLE_TEXT_LEN, OCR_LANG_SAMPLE_MIN;
|
|
2029
2606
|
var init_pdf = __esm({
|
|
2030
2607
|
"src/extraction/pdf.ts"() {
|
|
2031
2608
|
init_errors();
|
|
2032
2609
|
init_hooks();
|
|
2610
|
+
init_text();
|
|
2033
2611
|
init_pdf_structure();
|
|
2034
2612
|
GARBAGE_SCAN_LIMIT = 2e4;
|
|
2035
2613
|
MIN_USABLE_TEXT_LEN = 50;
|
|
2614
|
+
OCR_LANG_SAMPLE_MIN = 100;
|
|
2036
2615
|
}
|
|
2037
2616
|
});
|
|
2038
2617
|
|
|
2039
2618
|
// src/extraction/vision.ts
|
|
2040
2619
|
var vision_exports = {};
|
|
2041
2620
|
__export(vision_exports, {
|
|
2621
|
+
MAX_RENDER_PX: () => MAX_RENDER_PX,
|
|
2042
2622
|
PAGES_PER_VISION_BATCH: () => PAGES_PER_VISION_BATCH,
|
|
2043
2623
|
VISION_BATCH_ATTEMPTS: () => VISION_BATCH_ATTEMPTS,
|
|
2624
|
+
VISION_MAX_OUTPUT_TOKENS: () => VISION_MAX_OUTPUT_TOKENS,
|
|
2044
2625
|
assignBatchPages: () => assignBatchPages,
|
|
2045
2626
|
coercePagesResult: () => coercePagesResult,
|
|
2046
2627
|
detectPdfVisionPages: () => detectPdfVisionPages,
|
|
@@ -2057,6 +2638,17 @@ function pagesPrompt(nImages) {
|
|
|
2057
2638
|
Return JSON: {"pages": [{"page": 1, "text": "<markdown for this page>"}]}
|
|
2058
2639
|
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.`;
|
|
2059
2640
|
}
|
|
2641
|
+
function asText(value) {
|
|
2642
|
+
if (typeof value === "string") return value;
|
|
2643
|
+
if (value == null || typeof value === "boolean") return "";
|
|
2644
|
+
if (Array.isArray(value)) {
|
|
2645
|
+
return value.filter((v) => v != null && v !== "").map(asText).join("\n");
|
|
2646
|
+
}
|
|
2647
|
+
if (typeof value === "object") {
|
|
2648
|
+
return Object.values(value).filter((v) => v != null && v !== "").map(asText).join("\n");
|
|
2649
|
+
}
|
|
2650
|
+
return String(value);
|
|
2651
|
+
}
|
|
2060
2652
|
function coercePagesResult(parsed) {
|
|
2061
2653
|
let base;
|
|
2062
2654
|
let rawPages;
|
|
@@ -2076,7 +2668,7 @@ function coercePagesResult(parsed) {
|
|
|
2076
2668
|
const rec = entry;
|
|
2077
2669
|
pages.push({
|
|
2078
2670
|
page: typeof rec.page === "number" ? rec.page : i + 1,
|
|
2079
|
-
text:
|
|
2671
|
+
text: asText(rec.text)
|
|
2080
2672
|
});
|
|
2081
2673
|
} else if (typeof entry === "string") {
|
|
2082
2674
|
pages.push({ page: i + 1, text: entry });
|
|
@@ -2107,16 +2699,28 @@ function parsePagesResponse(rawText) {
|
|
|
2107
2699
|
function sleep(ms) {
|
|
2108
2700
|
return new Promise((r) => setTimeout(r, ms));
|
|
2109
2701
|
}
|
|
2110
|
-
async function
|
|
2111
|
-
if (!
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2702
|
+
async function capped(image, canvas, maxPx) {
|
|
2703
|
+
if (!canvas) return image;
|
|
2704
|
+
try {
|
|
2705
|
+
const img = await canvas.loadImage(image);
|
|
2706
|
+
const longEdge = Math.max(img.width, img.height);
|
|
2707
|
+
if (longEdge <= maxPx) return image;
|
|
2708
|
+
const ratio = maxPx / longEdge;
|
|
2709
|
+
const w = Math.max(Math.round(img.width * ratio), 1);
|
|
2710
|
+
const h = Math.max(Math.round(img.height * ratio), 1);
|
|
2711
|
+
const out = canvas.createCanvas(w, h);
|
|
2712
|
+
out.getContext("2d").drawImage(img, 0, 0, w, h);
|
|
2713
|
+
return out.toBuffer("image/png");
|
|
2714
|
+
} catch {
|
|
2715
|
+
return image;
|
|
2116
2716
|
}
|
|
2117
|
-
|
|
2717
|
+
}
|
|
2718
|
+
async function runBatches(batchSource, opts) {
|
|
2719
|
+
const started = Date.now();
|
|
2720
|
+
const results = /* @__PURE__ */ new Map();
|
|
2721
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
2118
2722
|
const tokensTotal = { input: 0, output: 0 };
|
|
2119
|
-
const
|
|
2723
|
+
const maxConcurrent = opts.maxConcurrent ?? 8;
|
|
2120
2724
|
let lock = Promise.resolve();
|
|
2121
2725
|
const withLock = async (fn) => {
|
|
2122
2726
|
const prev = lock;
|
|
@@ -2132,66 +2736,183 @@ async function extractTextFromImages(images, opts) {
|
|
|
2132
2736
|
}
|
|
2133
2737
|
};
|
|
2134
2738
|
const client = buildLlmClient(opts.visionLlm);
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2739
|
+
let total = 0;
|
|
2740
|
+
let nBatches = 0;
|
|
2741
|
+
const queue = new BoundedQueue(maxConcurrent);
|
|
2742
|
+
const processBatch = async (batchNum, baseIdx, batchImages) => {
|
|
2743
|
+
const batchStarted = Date.now();
|
|
2744
|
+
let pendingLocal = batchImages.map((_, i) => i);
|
|
2745
|
+
for (let attempt = 1; attempt <= VISION_BATCH_ATTEMPTS; attempt++) {
|
|
2746
|
+
const ask = pendingLocal.map((i) => batchImages[i]);
|
|
2747
|
+
try {
|
|
2748
|
+
const [rawText, usage] = await callLlm(opts.visionLlm, {
|
|
2749
|
+
system: VISION_SYSTEM_PROMPT,
|
|
2750
|
+
user: pagesPrompt(ask.length),
|
|
2751
|
+
jsonMode: true,
|
|
2752
|
+
images: ask,
|
|
2753
|
+
maxTokens: opts.extCfg.visionMaxOutputTokens,
|
|
2754
|
+
// Transcription is not reasoning, and on a thinking model the two
|
|
2755
|
+
// compete for the SAME budget — see `LlmCallOpts.thinkingBudget`.
|
|
2756
|
+
// Zero, not small: there is nothing here to reason ABOUT, the model
|
|
2757
|
+
// is being asked what is on the page. `temperature: 0` for the same
|
|
2758
|
+
// reason — one scan must transcribe to the same text twice.
|
|
2759
|
+
thinkingBudget: 0,
|
|
2760
|
+
temperature: 0,
|
|
2761
|
+
client
|
|
2762
|
+
});
|
|
2763
|
+
if (!rawText?.trim()) throw new Error("empty vision response");
|
|
2764
|
+
const [, pages] = parsePagesResponse(rawText);
|
|
2765
|
+
const assigned = assignBatchPages(pages, ask.length);
|
|
2766
|
+
const batchSeconds = (Date.now() - batchStarted) / 1e3;
|
|
2767
|
+
const stillMissing = [];
|
|
2768
|
+
await withLock(() => {
|
|
2769
|
+
tokensTotal.input += usage.input ?? 0;
|
|
2770
|
+
tokensTotal.output += usage.output ?? 0;
|
|
2771
|
+
pendingLocal.forEach((localIdx, position) => {
|
|
2772
|
+
const pageText = assigned[position];
|
|
2773
|
+
if (pageText === void 0) {
|
|
2774
|
+
stillMissing.push(localIdx);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
results.set(baseIdx + localIdx, pageText);
|
|
2778
|
+
outcomes.set(baseIdx + localIdx, {
|
|
2779
|
+
status: pageText.trim() ? "read" : "blank",
|
|
2780
|
+
attempts: attempt,
|
|
2781
|
+
error: null,
|
|
2782
|
+
seconds: batchSeconds
|
|
2783
|
+
});
|
|
2148
2784
|
});
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2785
|
+
});
|
|
2786
|
+
pendingLocal = stillMissing;
|
|
2787
|
+
if (pendingLocal.length === 0) return;
|
|
2788
|
+
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
2789
|
+
console.warn(
|
|
2790
|
+
`[vision] batch ${batchNum} attempt ${attempt} answered ${ask.length - pendingLocal.length} of ${ask.length} pages \u2014 asking again for ${pendingLocal.length}`
|
|
2791
|
+
);
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
console.error(
|
|
2795
|
+
`[vision] batch ${batchNum}: ${pendingLocal.length} page(s) never came back after ${attempt} attempts`
|
|
2796
|
+
);
|
|
2797
|
+
await withLock(() => {
|
|
2798
|
+
for (const localIdx of pendingLocal) {
|
|
2799
|
+
outcomes.set(baseIdx + localIdx, {
|
|
2800
|
+
status: "failed",
|
|
2801
|
+
attempts: attempt,
|
|
2802
|
+
error: "page missing from batch response",
|
|
2803
|
+
seconds: batchSeconds
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
return;
|
|
2808
|
+
} catch (exc) {
|
|
2809
|
+
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
2810
|
+
console.warn(`[vision] batch ${batchNum} attempt ${attempt} failed: ${exc} \u2014 retrying`);
|
|
2811
|
+
await sleep(2e3 * attempt);
|
|
2812
|
+
} else {
|
|
2813
|
+
console.error(`[vision] batch ${batchNum} failed after ${VISION_BATCH_ATTEMPTS} attempts: ${exc}`);
|
|
2814
|
+
const batchSeconds = (Date.now() - batchStarted) / 1e3;
|
|
2152
2815
|
await withLock(() => {
|
|
2153
|
-
for (const
|
|
2154
|
-
|
|
2155
|
-
|
|
2816
|
+
for (const localIdx of pendingLocal) {
|
|
2817
|
+
outcomes.set(baseIdx + localIdx, {
|
|
2818
|
+
status: "failed",
|
|
2819
|
+
attempts: attempt,
|
|
2820
|
+
error: String(exc),
|
|
2821
|
+
seconds: batchSeconds
|
|
2822
|
+
});
|
|
2156
2823
|
}
|
|
2157
|
-
tokensTotal.input += usage.input ?? 0;
|
|
2158
|
-
tokensTotal.output += usage.output ?? 0;
|
|
2159
2824
|
});
|
|
2160
|
-
return;
|
|
2161
|
-
} catch (exc) {
|
|
2162
|
-
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
2163
|
-
console.warn(`[vision] batch ${batchIdx} attempt ${attempt} failed: ${exc} \u2014 retrying`);
|
|
2164
|
-
await sleep(2e3 * attempt);
|
|
2165
|
-
} else {
|
|
2166
|
-
console.error(
|
|
2167
|
-
`[vision] batch ${batchIdx} failed after ${VISION_BATCH_ATTEMPTS} attempts: ${exc}`
|
|
2168
|
-
);
|
|
2169
|
-
}
|
|
2170
2825
|
}
|
|
2171
2826
|
}
|
|
2172
|
-
} finally {
|
|
2173
|
-
semaphore.release();
|
|
2174
2827
|
}
|
|
2175
2828
|
};
|
|
2829
|
+
const worker = async () => {
|
|
2830
|
+
for (; ; ) {
|
|
2831
|
+
const item = await queue.get();
|
|
2832
|
+
if (item === null) return;
|
|
2833
|
+
await processBatch(...item);
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
const workers = Array.from({ length: maxConcurrent }, () => worker());
|
|
2176
2837
|
try {
|
|
2177
|
-
await
|
|
2838
|
+
for await (const [baseIdx, batchImages] of batchSource) {
|
|
2839
|
+
if (!batchImages.length) continue;
|
|
2840
|
+
for (let j = 0; j < batchImages.length; j++) {
|
|
2841
|
+
outcomes.set(baseIdx + j, { status: "failed", attempts: 0, error: "never attempted", seconds: 0 });
|
|
2842
|
+
}
|
|
2843
|
+
total = Math.max(total, baseIdx + batchImages.length);
|
|
2844
|
+
await queue.put([nBatches, baseIdx, batchImages]);
|
|
2845
|
+
nBatches += 1;
|
|
2846
|
+
}
|
|
2847
|
+
queue.close();
|
|
2848
|
+
await Promise.all(workers);
|
|
2849
|
+
} catch (exc) {
|
|
2850
|
+
queue.abort();
|
|
2851
|
+
await Promise.allSettled(workers);
|
|
2852
|
+
throw exc;
|
|
2178
2853
|
} finally {
|
|
2179
2854
|
await client.aclose();
|
|
2180
2855
|
}
|
|
2181
|
-
|
|
2856
|
+
const texts = Array.from({ length: total }, (_, i) => results.get(i) ?? "");
|
|
2857
|
+
const outcomeList = Array.from(
|
|
2858
|
+
{ length: total },
|
|
2859
|
+
(_, i) => outcomes.get(i) ?? { status: "failed", attempts: 0, error: "never attempted", seconds: 0 }
|
|
2860
|
+
);
|
|
2861
|
+
const counts = { read: 0, blank: 0, failed: 0 };
|
|
2862
|
+
for (const o of outcomeList) counts[o.status] += 1;
|
|
2863
|
+
console.info(
|
|
2864
|
+
`[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`
|
|
2865
|
+
);
|
|
2866
|
+
return [texts, tokensTotal, outcomeList];
|
|
2867
|
+
}
|
|
2868
|
+
async function extractTextFromImages(images, opts) {
|
|
2869
|
+
if (!images.length) return [[], {}, []];
|
|
2870
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
2871
|
+
const canvasModule = await tryImport("@napi-rs/canvas");
|
|
2872
|
+
const sendable = await Promise.all(images.map((img) => capped(img, canvasModule, extCfg.maxRenderPx)));
|
|
2873
|
+
const batchSize = extCfg.pagesPerVisionBatch ?? PAGES_PER_VISION_BATCH;
|
|
2874
|
+
async function* batches() {
|
|
2875
|
+
for (let i = 0; i < sendable.length; i += batchSize) {
|
|
2876
|
+
yield [i, sendable.slice(i, i + batchSize)];
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return runBatches(batches(), {
|
|
2880
|
+
visionLlm: opts.visionLlm,
|
|
2881
|
+
extCfg,
|
|
2882
|
+
maxConcurrent: opts.maxConcurrent
|
|
2883
|
+
});
|
|
2182
2884
|
}
|
|
2183
2885
|
async function extractPdfPages(content, pageIndices, opts) {
|
|
2184
|
-
const
|
|
2185
|
-
const
|
|
2186
|
-
const
|
|
2886
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
2887
|
+
const batchSize = extCfg.pagesPerVisionBatch ?? PAGES_PER_VISION_BATCH;
|
|
2888
|
+
const pageOrder = [];
|
|
2889
|
+
async function* renderedBatches() {
|
|
2890
|
+
let pos = 0;
|
|
2891
|
+
for (let start = 0; start < pageIndices.length; start += batchSize) {
|
|
2892
|
+
const group = pageIndices.slice(start, start + batchSize);
|
|
2893
|
+
const rendered = await renderPdfPagesAsImages(content, group, 150, extCfg.maxRenderPx);
|
|
2894
|
+
if (!rendered.length) continue;
|
|
2895
|
+
pageOrder.push(...rendered.map(([idx]) => idx));
|
|
2896
|
+
yield [pos, rendered.map(([, png]) => png)];
|
|
2897
|
+
pos += rendered.length;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
const [texts, tokens, outcomes] = await runBatches(renderedBatches(), {
|
|
2901
|
+
visionLlm: opts.visionLlm,
|
|
2902
|
+
extCfg
|
|
2903
|
+
});
|
|
2187
2904
|
const pageTexts = {};
|
|
2188
|
-
|
|
2905
|
+
const pageOutcomes = {};
|
|
2906
|
+
for (let i = 0; i < pageOrder.length; i++) {
|
|
2189
2907
|
const text = texts[i];
|
|
2190
|
-
if (text?.trim()) pageTexts[
|
|
2908
|
+
if (text?.trim()) pageTexts[pageOrder[i]] = text;
|
|
2909
|
+
const outcome = outcomes[i];
|
|
2910
|
+
if (outcome) pageOutcomes[pageOrder[i]] = outcome;
|
|
2191
2911
|
}
|
|
2192
|
-
return [pageTexts, tokens];
|
|
2912
|
+
return [pageTexts, tokens, pageOutcomes];
|
|
2193
2913
|
}
|
|
2194
|
-
async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
2914
|
+
async function renderPdfPagesAsImages(content, pageIndices, dpi = 150, maxPx = MAX_RENDER_PX) {
|
|
2915
|
+
const started = Date.now();
|
|
2195
2916
|
const scale = dpi / 72;
|
|
2196
2917
|
const data = new Uint8Array(content);
|
|
2197
2918
|
const pdf = await getDocumentProxy(data);
|
|
@@ -2200,9 +2921,12 @@ async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
|
2200
2921
|
for (const idx of pageIndices) {
|
|
2201
2922
|
if (idx < 0 || idx >= pdf.numPages) continue;
|
|
2202
2923
|
try {
|
|
2924
|
+
const page = await pdf.getPage(idx + 1);
|
|
2925
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
2926
|
+
const pageScale = Math.min(scale, maxPx / Math.max(viewport.width, viewport.height, 1));
|
|
2203
2927
|
const buf = await renderPageAsImage(pdf, idx + 1, {
|
|
2204
2928
|
canvasImport: canvasImport ? async () => canvasImport : void 0,
|
|
2205
|
-
scale
|
|
2929
|
+
scale: pageScale
|
|
2206
2930
|
});
|
|
2207
2931
|
const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf);
|
|
2208
2932
|
results.push([idx, Buffer.from(bytes)]);
|
|
@@ -2210,6 +2934,7 @@ async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
|
2210
2934
|
console.warn(`[vision] failed to render PDF page ${idx}:`, exc);
|
|
2211
2935
|
}
|
|
2212
2936
|
}
|
|
2937
|
+
console.info(`[pdf] rastered ${results.length} pages in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
|
|
2213
2938
|
return results;
|
|
2214
2939
|
}
|
|
2215
2940
|
async function detectPdfVisionPages(content) {
|
|
@@ -2231,7 +2956,13 @@ async function detectPdfVisionPages(content) {
|
|
|
2231
2956
|
return visionPages;
|
|
2232
2957
|
}
|
|
2233
2958
|
async function extractDocxImages(content) {
|
|
2234
|
-
|
|
2959
|
+
let zip;
|
|
2960
|
+
try {
|
|
2961
|
+
zip = await JSZip.loadAsync(content);
|
|
2962
|
+
} catch (exc) {
|
|
2963
|
+
console.warn(`[vision] cannot open DOCX for images: ${exc}`);
|
|
2964
|
+
return [];
|
|
2965
|
+
}
|
|
2235
2966
|
const rels = zip.file("word/_rels/document.xml.rels");
|
|
2236
2967
|
if (!rels) return [];
|
|
2237
2968
|
const xml = await rels.async("string");
|
|
@@ -2257,7 +2988,13 @@ async function extractDocxImages(content) {
|
|
|
2257
2988
|
return images;
|
|
2258
2989
|
}
|
|
2259
2990
|
async function extractPptxImages(content) {
|
|
2260
|
-
|
|
2991
|
+
let zip;
|
|
2992
|
+
try {
|
|
2993
|
+
zip = await JSZip.loadAsync(content);
|
|
2994
|
+
} catch (exc) {
|
|
2995
|
+
console.warn(`[vision] cannot open PPTX for images: ${exc}`);
|
|
2996
|
+
return [];
|
|
2997
|
+
}
|
|
2261
2998
|
const slideNames = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)).sort((a, b) => {
|
|
2262
2999
|
const na = Number(/slide(\d+)/.exec(a)?.[1] ?? 0);
|
|
2263
3000
|
const nb = Number(/slide(\d+)/.exec(b)?.[1] ?? 0);
|
|
@@ -2290,12 +3027,22 @@ async function extractPptxImages(content) {
|
|
|
2290
3027
|
}
|
|
2291
3028
|
async function extractImage(content, opts) {
|
|
2292
3029
|
const { Extracted: Extracted2 } = await Promise.resolve().then(() => (init_extraction(), extraction_exports));
|
|
3030
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
3031
|
+
const canvasModule = await tryImport("@napi-rs/canvas");
|
|
3032
|
+
const image = await capped(content, canvasModule, extCfg.maxRenderPx);
|
|
2293
3033
|
const [rawText, usage] = await callLlm(opts.visionLlm, {
|
|
2294
3034
|
system: VISION_SYSTEM_PROMPT,
|
|
2295
3035
|
user: `${MARKDOWN_RULES}
|
|
2296
3036
|
|
|
2297
3037
|
Transcribe this single image. Return the Markdown only.`,
|
|
2298
|
-
images: [
|
|
3038
|
+
images: [image],
|
|
3039
|
+
maxTokens: extCfg.visionMaxOutputTokens,
|
|
3040
|
+
// Same contract as the batched path (see processBatch): with the output
|
|
3041
|
+
// cap in place, default-on thinking bills against it — an all-thinking
|
|
3042
|
+
// truncated-empty reply lands as vision_failed. Transcription is not
|
|
3043
|
+
// reasoning, and it must be deterministic.
|
|
3044
|
+
thinkingBudget: 0,
|
|
3045
|
+
temperature: 0
|
|
2299
3046
|
});
|
|
2300
3047
|
const text = (rawText || "").trim();
|
|
2301
3048
|
return new Extracted2({
|
|
@@ -2304,37 +3051,75 @@ Transcribe this single image. Return the Markdown only.`,
|
|
|
2304
3051
|
slides: null,
|
|
2305
3052
|
mediaOnly: !text,
|
|
2306
3053
|
providerTokens: usage ?? {},
|
|
2307
|
-
isMarkdown: Boolean(text)
|
|
3054
|
+
isMarkdown: Boolean(text),
|
|
3055
|
+
// A configured reader that returns nothing (refusal, filter, truncation)
|
|
3056
|
+
// is the same "reader read nothing" state the PDF path labels
|
|
3057
|
+
// vision_failed — not a blank image with no explanation.
|
|
3058
|
+
unreadableReason: text ? null : "vision_failed",
|
|
3059
|
+
unreadablePages: text ? 0 : 1
|
|
2308
3060
|
});
|
|
2309
3061
|
}
|
|
2310
|
-
var
|
|
3062
|
+
var VISION_BATCH_ATTEMPTS, EXTRACTION_DEFAULTS, MAX_RENDER_PX, VISION_MAX_OUTPUT_TOKENS, PAGES_PER_VISION_BATCH, VISION_SYSTEM_PROMPT, MARKDOWN_RULES, BoundedQueue;
|
|
2311
3063
|
var init_vision = __esm({
|
|
2312
3064
|
"src/extraction/vision.ts"() {
|
|
3065
|
+
init_config();
|
|
2313
3066
|
init_extras();
|
|
2314
3067
|
init_llm();
|
|
2315
3068
|
init_json();
|
|
2316
3069
|
init_pdf();
|
|
2317
|
-
PAGES_PER_VISION_BATCH = 5;
|
|
2318
3070
|
VISION_BATCH_ATTEMPTS = 3;
|
|
3071
|
+
EXTRACTION_DEFAULTS = extractionSchema.parse({});
|
|
3072
|
+
MAX_RENDER_PX = EXTRACTION_DEFAULTS.maxRenderPx;
|
|
3073
|
+
VISION_MAX_OUTPUT_TOKENS = EXTRACTION_DEFAULTS.visionMaxOutputTokens;
|
|
3074
|
+
PAGES_PER_VISION_BATCH = EXTRACTION_DEFAULTS.pagesPerVisionBatch;
|
|
2319
3075
|
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.";
|
|
2320
3076
|
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.";
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
3077
|
+
BoundedQueue = class {
|
|
3078
|
+
constructor(maxsize) {
|
|
3079
|
+
this.maxsize = maxsize;
|
|
3080
|
+
}
|
|
3081
|
+
maxsize;
|
|
3082
|
+
items = [];
|
|
3083
|
+
putters = [];
|
|
3084
|
+
getters = [];
|
|
3085
|
+
closed = false;
|
|
3086
|
+
async put(item) {
|
|
3087
|
+
while (!this.closed && this.items.length >= this.maxsize) {
|
|
3088
|
+
await new Promise((resolve) => this.putters.push(resolve));
|
|
3089
|
+
}
|
|
3090
|
+
if (this.closed) return;
|
|
3091
|
+
this.items.push(item);
|
|
3092
|
+
this.getters.shift()?.();
|
|
3093
|
+
}
|
|
3094
|
+
/** `null` once the queue is closed AND drained — the worker's exit signal. */
|
|
3095
|
+
async get() {
|
|
3096
|
+
for (; ; ) {
|
|
3097
|
+
if (this.items.length) {
|
|
3098
|
+
const item = this.items.shift();
|
|
3099
|
+
this.putters.shift()?.();
|
|
3100
|
+
return item;
|
|
3101
|
+
}
|
|
3102
|
+
if (this.closed) return null;
|
|
3103
|
+
await new Promise((resolve) => this.getters.push(resolve));
|
|
2331
3104
|
}
|
|
2332
|
-
await new Promise((resolve) => this.waiters.push(resolve));
|
|
2333
3105
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
3106
|
+
close() {
|
|
3107
|
+
this.closed = true;
|
|
3108
|
+
for (const wake of this.getters.splice(0)) wake();
|
|
3109
|
+
for (const wake of this.putters.splice(0)) wake();
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Like `close()`, but for the error path: DISCARDS whatever is still
|
|
3113
|
+
* queued rather than letting it drain. A batch already in a worker's hands
|
|
3114
|
+
* keeps running (there is no cancelling that), but one that only ever sat
|
|
3115
|
+
* in the queue must never start — the source has already failed, so
|
|
3116
|
+
* spending a full vision-LLM retry budget on it buys nothing.
|
|
3117
|
+
*/
|
|
3118
|
+
abort() {
|
|
3119
|
+
this.closed = true;
|
|
3120
|
+
this.items.length = 0;
|
|
3121
|
+
for (const wake of this.getters.splice(0)) wake();
|
|
3122
|
+
for (const wake of this.putters.splice(0)) wake();
|
|
2338
3123
|
}
|
|
2339
3124
|
};
|
|
2340
3125
|
}
|
|
@@ -2344,7 +3129,8 @@ var init_vision = __esm({
|
|
|
2344
3129
|
var extraction_exports = {};
|
|
2345
3130
|
__export(extraction_exports, {
|
|
2346
3131
|
Extracted: () => Extracted,
|
|
2347
|
-
extract: () => extract2
|
|
3132
|
+
extract: () => extract2,
|
|
3133
|
+
extractEmbeddedImagesText: () => extractEmbeddedImagesText
|
|
2348
3134
|
});
|
|
2349
3135
|
function isPdf(ext, mime) {
|
|
2350
3136
|
return ext === ".pdf" || mime === "application/pdf";
|
|
@@ -2352,14 +3138,50 @@ function isPdf(ext, mime) {
|
|
|
2352
3138
|
function isImage(ext, mime) {
|
|
2353
3139
|
return mime.startsWith("image/") || IMAGE_EXTS.has(ext);
|
|
2354
3140
|
}
|
|
2355
|
-
async function
|
|
3141
|
+
async function officeUnreadable(text, images, visionLlm) {
|
|
3142
|
+
if (text.trim()) return { reason: null, unread: 0 };
|
|
3143
|
+
let readable = 0;
|
|
3144
|
+
for (const img of images) {
|
|
3145
|
+
if (!await embeddedImageIsNegligible(img.imageBytes)) readable += 1;
|
|
3146
|
+
}
|
|
3147
|
+
if (!readable) return { reason: null, unread: 0 };
|
|
3148
|
+
return { reason: visionLlm ? "vision_failed" : "needs_vision", unread: readable };
|
|
3149
|
+
}
|
|
3150
|
+
async function embeddedImageIsNegligible(data) {
|
|
3151
|
+
if (data.length < MIN_EMBEDDED_IMAGE_BYTES) return true;
|
|
3152
|
+
const canvas = await tryImport("@napi-rs/canvas");
|
|
3153
|
+
if (!canvas) return false;
|
|
3154
|
+
try {
|
|
3155
|
+
const img = await canvas.loadImage(data);
|
|
3156
|
+
return Math.max(img.width, img.height) < MIN_EMBEDDED_IMAGE_PX;
|
|
3157
|
+
} catch {
|
|
3158
|
+
return false;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
async function extractEmbeddedImagesText(images, visionLlm, hooks, extraction) {
|
|
2356
3162
|
if (!images.length) return [[], {}];
|
|
2357
3163
|
try {
|
|
2358
3164
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
3165
|
+
const { createHash: createHash3 } = await import('crypto');
|
|
3166
|
+
const keys = [];
|
|
3167
|
+
const firstAt = /* @__PURE__ */ new Map();
|
|
3168
|
+
const send = [];
|
|
3169
|
+
for (const img of images) {
|
|
3170
|
+
const blob = img.imageBytes;
|
|
3171
|
+
if (await embeddedImageIsNegligible(blob)) {
|
|
3172
|
+
keys.push(null);
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
const digest = createHash3("sha256").update(blob).digest("hex");
|
|
3176
|
+
if (!firstAt.has(digest)) {
|
|
3177
|
+
firstAt.set(digest, send.length);
|
|
3178
|
+
send.push(blob);
|
|
3179
|
+
}
|
|
3180
|
+
keys.push(digest);
|
|
3181
|
+
}
|
|
3182
|
+
if (!send.length) return [images.map(() => ""), {}];
|
|
3183
|
+
const [texts, tokens] = await vision.extractTextFromImages(send, { visionLlm, extraction });
|
|
3184
|
+
return [keys.map((k) => k == null ? "" : texts[firstAt.get(k)] ?? ""), tokens];
|
|
2363
3185
|
} catch (exc) {
|
|
2364
3186
|
emitError(hooks, exc, { stage: "embedded_image_vision" });
|
|
2365
3187
|
return [[], {}];
|
|
@@ -2370,22 +3192,35 @@ async function extract2(content, filename, mime, opts) {
|
|
|
2370
3192
|
const ext = getFileExtension(filename);
|
|
2371
3193
|
const visionLlm = opts.visionLlm ?? null;
|
|
2372
3194
|
const hooks = opts.hooks;
|
|
3195
|
+
const extraction = opts.extraction ?? null;
|
|
2373
3196
|
try {
|
|
2374
3197
|
if (isNonIngestibleMedia(filename, m)) {
|
|
2375
3198
|
return new Extracted({ text: "", mediaOnly: true });
|
|
2376
3199
|
}
|
|
2377
3200
|
if (isPdf(ext, m)) {
|
|
2378
3201
|
const pdf = await Promise.resolve().then(() => (init_pdf(), pdf_exports));
|
|
2379
|
-
return await pdf.extract(content, { visionLlm, hooks });
|
|
3202
|
+
return await pdf.extract(content, { visionLlm, hooks, extraction });
|
|
2380
3203
|
}
|
|
2381
3204
|
if (isImage(ext, m)) {
|
|
2382
|
-
if (!visionLlm)
|
|
3205
|
+
if (!visionLlm) {
|
|
3206
|
+
return new Extracted({
|
|
3207
|
+
text: "",
|
|
3208
|
+
mediaOnly: true,
|
|
3209
|
+
unreadableReason: "needs_vision",
|
|
3210
|
+
unreadablePages: 1
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
2383
3213
|
try {
|
|
2384
3214
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2385
|
-
return await vision.extractImage(content, { visionLlm });
|
|
3215
|
+
return await vision.extractImage(content, { visionLlm, extraction });
|
|
2386
3216
|
} catch (exc) {
|
|
2387
3217
|
emitError(hooks, exc, { stage: "image_vision", filename });
|
|
2388
|
-
return new Extracted({
|
|
3218
|
+
return new Extracted({
|
|
3219
|
+
text: "",
|
|
3220
|
+
mediaOnly: true,
|
|
3221
|
+
unreadableReason: "vision_failed",
|
|
3222
|
+
unreadablePages: 1
|
|
3223
|
+
});
|
|
2389
3224
|
}
|
|
2390
3225
|
}
|
|
2391
3226
|
if (ext === ".docx" || m === DOCX_MIME) {
|
|
@@ -2393,10 +3228,11 @@ async function extract2(content, filename, mime, opts) {
|
|
|
2393
3228
|
let providerTokens = {};
|
|
2394
3229
|
let llmPictures = 0;
|
|
2395
3230
|
let combined = text;
|
|
3231
|
+
let images = [];
|
|
2396
3232
|
if (visionLlm) {
|
|
2397
3233
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2398
|
-
|
|
2399
|
-
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks);
|
|
3234
|
+
images = await vision.extractDocxImages(content);
|
|
3235
|
+
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks, extraction);
|
|
2400
3236
|
for (let i = 0; i < texts.length; i++) {
|
|
2401
3237
|
const imgText = texts[i];
|
|
2402
3238
|
if (imgText) {
|
|
@@ -2407,12 +3243,18 @@ ${imgText}`;
|
|
|
2407
3243
|
}
|
|
2408
3244
|
}
|
|
2409
3245
|
providerTokens = tokens;
|
|
3246
|
+
} else if (!text.trim()) {
|
|
3247
|
+
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
3248
|
+
images = await vision.extractDocxImages(content);
|
|
2410
3249
|
}
|
|
3250
|
+
const { reason, unread } = await officeUnreadable(combined, images, visionLlm);
|
|
2411
3251
|
return new Extracted({
|
|
2412
3252
|
text: combined,
|
|
2413
3253
|
pages: await countDocxPages(content),
|
|
2414
3254
|
providerTokens,
|
|
2415
|
-
llmPictures
|
|
3255
|
+
llmPictures,
|
|
3256
|
+
unreadableReason: reason,
|
|
3257
|
+
unreadablePages: unread
|
|
2416
3258
|
});
|
|
2417
3259
|
}
|
|
2418
3260
|
if (ext === ".pptx" || m === PPTX_MIME) {
|
|
@@ -2420,10 +3262,11 @@ ${imgText}`;
|
|
|
2420
3262
|
let providerTokens = {};
|
|
2421
3263
|
let llmPictures = 0;
|
|
2422
3264
|
let combined = text;
|
|
3265
|
+
let images = [];
|
|
2423
3266
|
if (visionLlm) {
|
|
2424
3267
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2425
|
-
|
|
2426
|
-
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks);
|
|
3268
|
+
images = await vision.extractPptxImages(content);
|
|
3269
|
+
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks, extraction);
|
|
2427
3270
|
for (let i = 0; i < texts.length; i++) {
|
|
2428
3271
|
const imgText = texts[i];
|
|
2429
3272
|
if (imgText) {
|
|
@@ -2435,12 +3278,18 @@ ${imgText}`;
|
|
|
2435
3278
|
}
|
|
2436
3279
|
}
|
|
2437
3280
|
providerTokens = tokens;
|
|
3281
|
+
} else if (!text.trim()) {
|
|
3282
|
+
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
3283
|
+
images = await vision.extractPptxImages(content);
|
|
2438
3284
|
}
|
|
3285
|
+
const { reason, unread } = await officeUnreadable(combined, images, visionLlm);
|
|
2439
3286
|
return new Extracted({
|
|
2440
3287
|
text: combined,
|
|
2441
3288
|
slides: await countPptxSlides(content),
|
|
2442
3289
|
providerTokens,
|
|
2443
|
-
llmPictures
|
|
3290
|
+
llmPictures,
|
|
3291
|
+
unreadableReason: reason,
|
|
3292
|
+
unreadablePages: unread
|
|
2444
3293
|
});
|
|
2445
3294
|
}
|
|
2446
3295
|
if (ext === ".xlsx" || m === XLSX_MIME) {
|
|
@@ -2464,9 +3313,10 @@ ${imgText}`;
|
|
|
2464
3313
|
return new Extracted({ text: content.toString("utf8") });
|
|
2465
3314
|
}
|
|
2466
3315
|
}
|
|
2467
|
-
var IMAGE_EXTS, Extracted;
|
|
3316
|
+
var IMAGE_EXTS, Extracted, MIN_EMBEDDED_IMAGE_BYTES, MIN_EMBEDDED_IMAGE_PX;
|
|
2468
3317
|
var init_extraction = __esm({
|
|
2469
3318
|
"src/extraction/index.ts"() {
|
|
3319
|
+
init_extras();
|
|
2470
3320
|
init_hooks();
|
|
2471
3321
|
init_files();
|
|
2472
3322
|
IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp"]);
|
|
@@ -2478,6 +3328,10 @@ var init_extraction = __esm({
|
|
|
2478
3328
|
providerTokens;
|
|
2479
3329
|
llmPictures;
|
|
2480
3330
|
isMarkdown;
|
|
3331
|
+
/** See `UnreadableReason`. `mediaOnly` is the same idea for whole files and
|
|
3332
|
+
* stays set alongside this, so existing callers keep working. */
|
|
3333
|
+
unreadableReason;
|
|
3334
|
+
unreadablePages;
|
|
2481
3335
|
constructor(init) {
|
|
2482
3336
|
this.text = init.text;
|
|
2483
3337
|
this.pages = init.pages ?? null;
|
|
@@ -2486,8 +3340,12 @@ var init_extraction = __esm({
|
|
|
2486
3340
|
this.providerTokens = init.providerTokens ?? {};
|
|
2487
3341
|
this.llmPictures = init.llmPictures ?? 0;
|
|
2488
3342
|
this.isMarkdown = init.isMarkdown ?? false;
|
|
3343
|
+
this.unreadableReason = init.unreadableReason ?? null;
|
|
3344
|
+
this.unreadablePages = init.unreadablePages ?? 0;
|
|
2489
3345
|
}
|
|
2490
3346
|
};
|
|
3347
|
+
MIN_EMBEDDED_IMAGE_BYTES = 3 * 1024;
|
|
3348
|
+
MIN_EMBEDDED_IMAGE_PX = 100;
|
|
2491
3349
|
}
|
|
2492
3350
|
});
|
|
2493
3351
|
|
|
@@ -2532,6 +3390,9 @@ __export(db_exports, {
|
|
|
2532
3390
|
function stripSqlNoise(sql) {
|
|
2533
3391
|
return sql.replace(SQL_NOISE_RE, (m) => " ".repeat(m.length));
|
|
2534
3392
|
}
|
|
3393
|
+
function stripSqlNoiseBackslash(sql) {
|
|
3394
|
+
return sql.replace(SQL_NOISE_BACKSLASH_RE, (m) => " ".repeat(m.length));
|
|
3395
|
+
}
|
|
2535
3396
|
function hasKeyword(sqlUpper, keyword) {
|
|
2536
3397
|
return new RegExp(`\\b${keyword}\\b`).test(sqlUpper);
|
|
2537
3398
|
}
|
|
@@ -2660,24 +3521,28 @@ async function executeQueryAsync(config, sql, maxRows, accessModeRaw) {
|
|
|
2660
3521
|
`Invalid access_mode ${JSON.stringify(accessModeRaw)}; must be one of ${ALLOWED_ACCESS_MODES}`
|
|
2661
3522
|
);
|
|
2662
3523
|
}
|
|
2663
|
-
const
|
|
3524
|
+
const sqlVariants = [
|
|
3525
|
+
stripSqlNoise(sql).trim().toUpperCase(),
|
|
3526
|
+
stripSqlNoiseBackslash(sql).trim().toUpperCase()
|
|
3527
|
+
];
|
|
3528
|
+
const sqlUpper = sqlVariants[0];
|
|
2664
3529
|
for (const keyword of ALWAYS_BLOCKED) {
|
|
2665
|
-
if (hasKeyword(
|
|
3530
|
+
if ([...sqlVariants, sql.toUpperCase()].some((v) => hasKeyword(v, keyword))) {
|
|
2666
3531
|
return { success: false, error: `${keyword} queries are not allowed` };
|
|
2667
3532
|
}
|
|
2668
3533
|
}
|
|
2669
3534
|
if (accessMode === "readonly") {
|
|
2670
|
-
if (!
|
|
3535
|
+
if (!sqlVariants.every((v) => v.startsWith("SELECT") || v.startsWith("WITH"))) {
|
|
2671
3536
|
return { success: false, error: "Only SELECT queries are allowed in read-only mode" };
|
|
2672
3537
|
}
|
|
2673
3538
|
for (const keyword of READONLY_BLOCKED) {
|
|
2674
|
-
if (hasKeyword(
|
|
3539
|
+
if (sqlVariants.some((v) => hasKeyword(v, keyword))) {
|
|
2675
3540
|
return { success: false, error: `${keyword} queries are not allowed in read-only mode` };
|
|
2676
3541
|
}
|
|
2677
3542
|
}
|
|
2678
3543
|
} else if (accessMode === "readwrite") {
|
|
2679
3544
|
for (const keyword of READWRITE_BLOCKED) {
|
|
2680
|
-
if (hasKeyword(
|
|
3545
|
+
if (sqlVariants.some((v) => hasKeyword(v, keyword))) {
|
|
2681
3546
|
return { success: false, error: `${keyword} queries are not allowed in read-write mode` };
|
|
2682
3547
|
}
|
|
2683
3548
|
}
|
|
@@ -2811,7 +3676,7 @@ async function getSchemaText(config, selectedTables) {
|
|
|
2811
3676
|
}
|
|
2812
3677
|
return rowsToText(schemaRows);
|
|
2813
3678
|
}
|
|
2814
|
-
var require2, MAX_ROWS, CONNECT_TIMEOUT, QUERY_TIMEOUT_MS, ALWAYS_BLOCKED, READONLY_BLOCKED, READWRITE_BLOCKED, ALLOWED_ACCESS_MODES, SQL_NOISE_RE;
|
|
3679
|
+
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;
|
|
2815
3680
|
var init_db = __esm({
|
|
2816
3681
|
"src/tools/executors/db.ts"() {
|
|
2817
3682
|
init_errors();
|
|
@@ -2820,10 +3685,22 @@ var init_db = __esm({
|
|
|
2820
3685
|
CONNECT_TIMEOUT = 10;
|
|
2821
3686
|
QUERY_TIMEOUT_MS = 3e4;
|
|
2822
3687
|
ALWAYS_BLOCKED = ["GRANT", "REVOKE"];
|
|
2823
|
-
READONLY_BLOCKED = [
|
|
2824
|
-
|
|
3688
|
+
READONLY_BLOCKED = [
|
|
3689
|
+
"INSERT",
|
|
3690
|
+
"UPDATE",
|
|
3691
|
+
"DELETE",
|
|
3692
|
+
"MERGE",
|
|
3693
|
+
"DROP",
|
|
3694
|
+
"ALTER",
|
|
3695
|
+
"CREATE",
|
|
3696
|
+
"TRUNCATE",
|
|
3697
|
+
"DO",
|
|
3698
|
+
"CALL"
|
|
3699
|
+
];
|
|
3700
|
+
READWRITE_BLOCKED = ["DELETE", "DROP", "TRUNCATE", "DO", "CALL"];
|
|
2825
3701
|
ALLOWED_ACCESS_MODES = ["readonly", "readwrite", "full"];
|
|
2826
|
-
SQL_NOISE_RE =
|
|
3702
|
+
SQL_NOISE_RE = /\b[eE]'(?:[^'\\]|\\[\s\S]|'')*'|'(?:[^']|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
3703
|
+
SQL_NOISE_BACKSLASH_RE = /'(?:[^'\\]|\\[\s\S]|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
2827
3704
|
}
|
|
2828
3705
|
});
|
|
2829
3706
|
function mulberry322(seed) {
|
|
@@ -3655,7 +4532,8 @@ var retrieval_exports = {};
|
|
|
3655
4532
|
__export(retrieval_exports, {
|
|
3656
4533
|
buildGraphRanked: () => buildGraphRanked,
|
|
3657
4534
|
corpusIsAclUniform: () => corpusIsAclUniform,
|
|
3658
|
-
shouldUseCommunitySummaries: () => shouldUseCommunitySummaries
|
|
4535
|
+
shouldUseCommunitySummaries: () => shouldUseCommunitySummaries,
|
|
4536
|
+
vectorSeedIds: () => vectorSeedIds
|
|
3659
4537
|
});
|
|
3660
4538
|
function vecLiteral(vector) {
|
|
3661
4539
|
return `[${vector.map((x) => Number(x)).join(",")}]`;
|
|
@@ -3681,8 +4559,8 @@ async function deriveQueryEntities(pool, chunkIds, opts) {
|
|
|
3681
4559
|
JOIN context_engine_chunks c ON c.id = ce.chunk_id
|
|
3682
4560
|
WHERE ce.chunk_id = ANY($1::uuid[]) ${SCOPE2}
|
|
3683
4561
|
GROUP BY e.normalized_name, e.name, e.type
|
|
3684
|
-
ORDER BY freq DESC LIMIT $
|
|
3685
|
-
[chunkIds, opts.sourceIds, opts.principals, ENTITY_LIMIT]
|
|
4562
|
+
ORDER BY freq DESC LIMIT $5`,
|
|
4563
|
+
[chunkIds, opts.sourceIds, opts.documentIds ?? null, opts.principals, ENTITY_LIMIT]
|
|
3686
4564
|
);
|
|
3687
4565
|
return result.rows.map((r) => ({
|
|
3688
4566
|
normalized_name: r.normalized_name,
|
|
@@ -3747,8 +4625,8 @@ async function computeCommunityScores(pool, chunkIds, vector, opts) {
|
|
|
3747
4625
|
`SELECT ce.chunk_id::text, ce.entity_id::text
|
|
3748
4626
|
FROM context_engine_chunk_entities ce
|
|
3749
4627
|
JOIN context_engine_chunks c ON c.id = ce.chunk_id
|
|
3750
|
-
WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($
|
|
3751
|
-
[chunkIds, opts.sourceIds, opts.principals, Object.keys(entityScore)]
|
|
4628
|
+
WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($5::uuid[]) ${SCOPE2}`,
|
|
4629
|
+
[chunkIds, opts.sourceIds, opts.documentIds ?? null, opts.principals, Object.keys(entityScore)]
|
|
3752
4630
|
);
|
|
3753
4631
|
const chunkScores = {};
|
|
3754
4632
|
for (const row of result.rows) {
|
|
@@ -3757,13 +4635,29 @@ async function computeCommunityScores(pool, chunkIds, vector, opts) {
|
|
|
3757
4635
|
return chunkScores;
|
|
3758
4636
|
}
|
|
3759
4637
|
async function vectorSeedIds(pool, vector, opts) {
|
|
3760
|
-
const
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
4638
|
+
const binds = [opts.sourceIds, opts.documentIds ?? null, opts.principals];
|
|
4639
|
+
const scoped = binds.some((v) => v !== null);
|
|
4640
|
+
const client = await pool.connect();
|
|
4641
|
+
try {
|
|
4642
|
+
await client.query("BEGIN");
|
|
4643
|
+
await opts.backend?.tuneAnnScan?.(client, SEED_LIMIT, scoped);
|
|
4644
|
+
const result = await client.query(
|
|
4645
|
+
`SELECT c.id::text FROM context_engine_chunks c
|
|
4646
|
+
WHERE c.embedding IS NOT NULL ${SCOPE2}
|
|
4647
|
+
ORDER BY c.embedding <=> CAST($1 AS vector) LIMIT $5`,
|
|
4648
|
+
[vecLiteral(vector), ...binds, SEED_LIMIT]
|
|
4649
|
+
);
|
|
4650
|
+
await client.query("COMMIT");
|
|
4651
|
+
return result.rows.map((r) => String(r.id));
|
|
4652
|
+
} catch (err) {
|
|
4653
|
+
try {
|
|
4654
|
+
await client.query("ROLLBACK");
|
|
4655
|
+
} catch {
|
|
4656
|
+
}
|
|
4657
|
+
throw err;
|
|
4658
|
+
} finally {
|
|
4659
|
+
client.release();
|
|
4660
|
+
}
|
|
3767
4661
|
}
|
|
3768
4662
|
async function buildGraphRanked(query, opts) {
|
|
3769
4663
|
try {
|
|
@@ -3772,11 +4666,18 @@ async function buildGraphRanked(query, opts) {
|
|
|
3772
4666
|
const vector = vectors[0] ? [...vectors[0]] : null;
|
|
3773
4667
|
if (!vector) return [];
|
|
3774
4668
|
const sourceIds = opts.sourceIds ?? null;
|
|
4669
|
+
const documentIds = opts.documentIds ?? null;
|
|
3775
4670
|
const principals = opts.principals ?? null;
|
|
3776
|
-
const seeds = await vectorSeedIds(opts.pool, vector, {
|
|
4671
|
+
const seeds = await vectorSeedIds(opts.pool, vector, {
|
|
4672
|
+
sourceIds,
|
|
4673
|
+
documentIds,
|
|
4674
|
+
principals,
|
|
4675
|
+
backend: opts.backend
|
|
4676
|
+
});
|
|
3777
4677
|
if (!seeds.length) return [];
|
|
3778
4678
|
const queryEntities = await deriveQueryEntities(opts.pool, seeds.slice(0, TOP_SEEDS_FOR_ENTITIES), {
|
|
3779
4679
|
sourceIds,
|
|
4680
|
+
documentIds,
|
|
3780
4681
|
principals
|
|
3781
4682
|
});
|
|
3782
4683
|
const entityNorms = queryEntities.map((e) => String(e.normalized_name));
|
|
@@ -3798,6 +4699,7 @@ async function buildGraphRanked(query, opts) {
|
|
|
3798
4699
|
const relScores = await computeRelationshipScores(opts.pool, allIds, entityNorms);
|
|
3799
4700
|
const commScores = await computeCommunityScores(opts.pool, allIds, vector, {
|
|
3800
4701
|
sourceIds,
|
|
4702
|
+
documentIds,
|
|
3801
4703
|
principals,
|
|
3802
4704
|
useSummaries
|
|
3803
4705
|
});
|
|
@@ -3849,7 +4751,8 @@ var init_retrieval = __esm({
|
|
|
3849
4751
|
TOP_SEEDS_FOR_ENTITIES = 20;
|
|
3850
4752
|
SCOPE2 = `
|
|
3851
4753
|
AND ($2::text[] IS NULL OR c.source_id = ANY($2::text[]))
|
|
3852
|
-
AND ($3::
|
|
4754
|
+
AND ($3::uuid[] IS NULL OR c.document_id = ANY($3::uuid[]))
|
|
4755
|
+
AND ($4::text[] IS NULL OR c.acl IS NULL OR c.acl && $4::text[])
|
|
3853
4756
|
`;
|
|
3854
4757
|
}
|
|
3855
4758
|
});
|
|
@@ -4434,381 +5337,96 @@ function splitIntoChunks(text, maxWords = 180, overlap = 18) {
|
|
|
4434
5337
|
for (const ln of (text || "").split(/\r\n|\n|\r/)) {
|
|
4435
5338
|
const stripped = ln.trim();
|
|
4436
5339
|
if (!stripped) {
|
|
4437
|
-
pendingBlank = units.length > 0;
|
|
4438
|
-
continue;
|
|
4439
|
-
}
|
|
4440
|
-
const lineSep = pendingBlank ? "\n\n" : "\n";
|
|
4441
|
-
pendingBlank = false;
|
|
4442
|
-
if (HEADING_OR_BULLET_RE.test(ln) || stripped.length <= 120) {
|
|
4443
|
-
units.push([lineSep, stripped, tokenize(stripped).length]);
|
|
4444
|
-
} else {
|
|
4445
|
-
let first = true;
|
|
4446
|
-
for (const sent of splitSentences(ln)) {
|
|
4447
|
-
const s = sent.trim();
|
|
4448
|
-
if (s) {
|
|
4449
|
-
units.push([first ? lineSep : " ", s, tokenize(s).length]);
|
|
4450
|
-
first = false;
|
|
4451
|
-
}
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
}
|
|
4455
|
-
const normalizedUnits = [];
|
|
4456
|
-
for (const [uSep, uText, uTokens] of units) {
|
|
4457
|
-
if (uTokens <= maxWords) {
|
|
4458
|
-
normalizedUnits.push([uSep, uText, uTokens]);
|
|
4459
|
-
continue;
|
|
4460
|
-
}
|
|
4461
|
-
const windowChars = maxWords * 4;
|
|
4462
|
-
const uChars = [...uText];
|
|
4463
|
-
let pos = 0;
|
|
4464
|
-
let pieceSep = uSep;
|
|
4465
|
-
while (pos < uChars.length) {
|
|
4466
|
-
let pieceChars = uChars.slice(pos, pos + windowChars);
|
|
4467
|
-
let piece = pieceChars.join("");
|
|
4468
|
-
if (pos + windowChars < uChars.length) {
|
|
4469
|
-
const ws = piece.lastIndexOf(" ");
|
|
4470
|
-
if (ws > Math.floor(windowChars / 2)) {
|
|
4471
|
-
piece = piece.slice(0, ws);
|
|
4472
|
-
pieceChars = [...piece];
|
|
4473
|
-
}
|
|
4474
|
-
}
|
|
4475
|
-
let pieceTokens = tokenize(piece);
|
|
4476
|
-
if (pieceTokens.length > maxWords) {
|
|
4477
|
-
piece = pieceChars.slice(0, maxWords).join("");
|
|
4478
|
-
pieceTokens = tokenize(piece);
|
|
4479
|
-
}
|
|
4480
|
-
normalizedUnits.push([pieceSep, piece, pieceTokens.length]);
|
|
4481
|
-
pos += codePointLength(piece);
|
|
4482
|
-
pieceSep = "";
|
|
4483
|
-
}
|
|
4484
|
-
}
|
|
4485
|
-
if (!normalizedUnits.some(([, , t]) => t)) return [];
|
|
4486
|
-
const chunks = [];
|
|
4487
|
-
let lastKept = null;
|
|
4488
|
-
let cur = [];
|
|
4489
|
-
let curTokens = 0;
|
|
4490
|
-
const flush = () => {
|
|
4491
|
-
const chunk = assembleUnits(cur);
|
|
4492
|
-
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
4493
|
-
chunks.push(chunk);
|
|
4494
|
-
lastKept = chunk;
|
|
4495
|
-
}
|
|
4496
|
-
const carried = [];
|
|
4497
|
-
let carriedTokens = 0;
|
|
4498
|
-
for (let i = cur.length - 1; i >= 0; i--) {
|
|
4499
|
-
const prev = cur[i];
|
|
4500
|
-
if (prev[2] === 0 || carriedTokens + prev[2] > overlap) break;
|
|
4501
|
-
carried.unshift(prev);
|
|
4502
|
-
carriedTokens += prev[2];
|
|
4503
|
-
}
|
|
4504
|
-
cur = carried;
|
|
4505
|
-
curTokens = carriedTokens;
|
|
4506
|
-
};
|
|
4507
|
-
for (const unit of normalizedUnits) {
|
|
4508
|
-
if (cur.length && curTokens + unit[2] > maxWords) flush();
|
|
4509
|
-
cur.push(unit);
|
|
4510
|
-
curTokens += unit[2];
|
|
4511
|
-
}
|
|
4512
|
-
if (cur.length) {
|
|
4513
|
-
const chunk = assembleUnits(cur);
|
|
4514
|
-
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
4515
|
-
chunks.push(chunk);
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4518
|
-
return chunks;
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
// src/actions.ts
|
|
4522
|
-
init_errors();
|
|
4523
|
-
init_hooks();
|
|
4524
|
-
init_llm();
|
|
4525
|
-
function encryptDict(data, key) {
|
|
4526
|
-
const nonce = randomBytes(12);
|
|
4527
|
-
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
4528
|
-
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
4529
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4530
|
-
const tag = cipher.getAuthTag();
|
|
4531
|
-
return Buffer.concat([nonce, ciphertext, tag]).toString("base64");
|
|
4532
|
-
}
|
|
4533
|
-
function decryptDict(token, key) {
|
|
4534
|
-
const raw = Buffer.from(token, "base64");
|
|
4535
|
-
const nonce = raw.subarray(0, 12);
|
|
4536
|
-
const tag = raw.subarray(raw.length - 16);
|
|
4537
|
-
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
4538
|
-
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
4539
|
-
decipher.setAuthTag(tag);
|
|
4540
|
-
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
4541
|
-
return JSON.parse(plaintext.toString("utf8"));
|
|
4542
|
-
}
|
|
4543
|
-
function getSecretKey(config) {
|
|
4544
|
-
const secretKey = config.secretKey;
|
|
4545
|
-
if (!secretKey) {
|
|
4546
|
-
throw new Error(
|
|
4547
|
-
"ContextEngineConfig.secretKey (env CE_SECRET_KEY) is not configured \u2014 a base64url-encoded 32-byte AES key is required to encrypt/decrypt tool configs that hold secrets."
|
|
4548
|
-
);
|
|
4549
|
-
}
|
|
4550
|
-
return Buffer.from(secretKey, "base64url");
|
|
4551
|
-
}
|
|
4552
|
-
function hmacSha256Hex(key, value) {
|
|
4553
|
-
const k = typeof key === "string" ? Buffer.from(key) : key;
|
|
4554
|
-
return createHmac("sha256", k).update(value, "utf8").digest("hex");
|
|
4555
|
-
}
|
|
4556
|
-
|
|
4557
|
-
// src/redaction.ts
|
|
4558
|
-
init_hooks();
|
|
4559
|
-
var BUILTIN_DETECTOR_NAMES = /* @__PURE__ */ new Set(["email", "phone", "ssn", "credit_card", "iban", "api_key"]);
|
|
4560
|
-
var RedactionRule = class {
|
|
4561
|
-
name;
|
|
4562
|
-
detector;
|
|
4563
|
-
pattern;
|
|
4564
|
-
field;
|
|
4565
|
-
action;
|
|
4566
|
-
placeholder;
|
|
4567
|
-
applyAt;
|
|
4568
|
-
unless;
|
|
4569
|
-
compiled = null;
|
|
4570
|
-
constructor(init) {
|
|
4571
|
-
this.name = init.name;
|
|
4572
|
-
this.detector = init.detector ?? null;
|
|
4573
|
-
this.pattern = init.pattern ?? null;
|
|
4574
|
-
this.field = init.field ?? null;
|
|
4575
|
-
this.action = init.action ?? "mask";
|
|
4576
|
-
this.placeholder = init.placeholder ?? null;
|
|
4577
|
-
this.applyAt = init.applyAt ?? "output";
|
|
4578
|
-
this.unless = init.unless ?? [];
|
|
4579
|
-
this.validate();
|
|
4580
|
-
}
|
|
4581
|
-
validate() {
|
|
4582
|
-
if (this.field !== null) {
|
|
4583
|
-
throw new Error(
|
|
4584
|
-
`rule '${this.name}': field-targeted rules are not implemented yet; use detector or pattern instead`
|
|
4585
|
-
);
|
|
4586
|
-
}
|
|
4587
|
-
const targets = [this.detector, this.pattern].filter((t) => t !== null);
|
|
4588
|
-
if (targets.length !== 1) {
|
|
4589
|
-
throw new Error(`rule '${this.name}': exactly one of detector/pattern must be set`);
|
|
4590
|
-
}
|
|
4591
|
-
if (this.pattern !== null) {
|
|
4592
|
-
try {
|
|
4593
|
-
this.compiled = new RegExp(this.pattern, "g");
|
|
4594
|
-
} catch (exc) {
|
|
4595
|
-
throw new Error(`rule '${this.name}': invalid regex: ${exc}`);
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
if (this.unless.length && this.applyAt === "ingest") {
|
|
4599
|
-
throw new Error(
|
|
4600
|
-
`rule '${this.name}': unless is output-time only and cannot be set on an applyAt='ingest' rule`
|
|
4601
|
-
);
|
|
4602
|
-
}
|
|
4603
|
-
}
|
|
4604
|
-
patternRe() {
|
|
4605
|
-
return this.compiled;
|
|
4606
|
-
}
|
|
4607
|
-
effectivePlaceholder() {
|
|
4608
|
-
return this.placeholder || `[${this.name.toUpperCase()}]`;
|
|
4609
|
-
}
|
|
4610
|
-
};
|
|
4611
|
-
var RedactionPolicy = class {
|
|
4612
|
-
rules;
|
|
4613
|
-
customDetectors;
|
|
4614
|
-
constructor(init = {}) {
|
|
4615
|
-
this.rules = (init.rules ?? []).map((r) => r instanceof RedactionRule ? r : new RedactionRule(r));
|
|
4616
|
-
this.customDetectors = init.customDetectors ?? {};
|
|
4617
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4618
|
-
for (const rule of this.rules) {
|
|
4619
|
-
if (seen.has(rule.name)) throw new Error(`duplicate rule name: '${rule.name}'`);
|
|
4620
|
-
seen.add(rule.name);
|
|
4621
|
-
if (rule.detector !== null) {
|
|
4622
|
-
const known = BUILTIN_DETECTOR_NAMES.has(rule.detector) || rule.detector in this.customDetectors;
|
|
4623
|
-
if (!known) {
|
|
4624
|
-
throw new Error(
|
|
4625
|
-
`rule '${rule.name}': unknown detector '${rule.detector}' (not built-in and not in customDetectors)`
|
|
4626
|
-
);
|
|
4627
|
-
}
|
|
4628
|
-
}
|
|
4629
|
-
}
|
|
4630
|
-
}
|
|
4631
|
-
isEmpty() {
|
|
4632
|
-
return this.rules.length === 0;
|
|
4633
|
-
}
|
|
4634
|
-
};
|
|
4635
|
-
var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
4636
|
-
var PHONE_RE = /\+\d[\d\s-]{7,17}\d/g;
|
|
4637
|
-
var SSN_RE = /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g;
|
|
4638
|
-
var CARD_RE = /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g;
|
|
4639
|
-
var IBAN_RE = /(?<![A-Za-z0-9])[A-Z]{2}\d{2}[A-Z0-9]{10,30}(?![A-Za-z0-9])/g;
|
|
4640
|
-
var API_KEY_ALNUM = "A-Za-z0-9_\\-+/=";
|
|
4641
|
-
var API_KEY_PREFIX_RE = new RegExp(
|
|
4642
|
-
`(?<![${API_KEY_ALNUM}])(?:(?:AKIA|ASIA)[0-9A-Z]{16}|sk-[A-Za-z0-9]{20,}|gh[opsu]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9\\-]{10,}|AIza[0-9A-Za-z_\\-]{35})(?![${API_KEY_ALNUM}])`,
|
|
4643
|
-
"g"
|
|
4644
|
-
);
|
|
4645
|
-
var API_KEY_GENERIC_RE = new RegExp(
|
|
4646
|
-
`(?<![${API_KEY_ALNUM}])[${API_KEY_ALNUM}]{24,}(?![${API_KEY_ALNUM}])`,
|
|
4647
|
-
"g"
|
|
4648
|
-
);
|
|
4649
|
-
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
4650
|
-
function spansFrom(re, text) {
|
|
4651
|
-
const out = [];
|
|
4652
|
-
re.lastIndex = 0;
|
|
4653
|
-
let m = re.exec(text);
|
|
4654
|
-
while (m !== null) {
|
|
4655
|
-
out.push([m.index, m.index + m[0].length]);
|
|
4656
|
-
if (m[0].length === 0) re.lastIndex++;
|
|
4657
|
-
m = re.exec(text);
|
|
4658
|
-
}
|
|
4659
|
-
return out;
|
|
4660
|
-
}
|
|
4661
|
-
function luhnOk(digits) {
|
|
4662
|
-
let total = 0;
|
|
4663
|
-
const rev = [...digits].reverse();
|
|
4664
|
-
for (let i = 0; i < rev.length; i++) {
|
|
4665
|
-
let d = Number(rev[i]);
|
|
4666
|
-
if (i % 2 === 1) {
|
|
4667
|
-
d *= 2;
|
|
4668
|
-
if (d > 9) d -= 9;
|
|
4669
|
-
}
|
|
4670
|
-
total += d;
|
|
4671
|
-
}
|
|
4672
|
-
return total % 10 === 0;
|
|
4673
|
-
}
|
|
4674
|
-
function detectCreditCard(text) {
|
|
4675
|
-
const spans = [];
|
|
4676
|
-
for (const [start, end] of spansFrom(CARD_RE, text)) {
|
|
4677
|
-
const digits = text.slice(start, end).replace(/[ -]/g, "");
|
|
4678
|
-
if (digits.length >= 13 && digits.length <= 19 && luhnOk(digits)) {
|
|
4679
|
-
spans.push([start, end]);
|
|
4680
|
-
}
|
|
4681
|
-
}
|
|
4682
|
-
return spans;
|
|
4683
|
-
}
|
|
4684
|
-
function looksLikeGenericSecret(token) {
|
|
4685
|
-
if (UUID_RE.test(token)) return false;
|
|
4686
|
-
let hasUpper = false;
|
|
4687
|
-
let hasLower = false;
|
|
4688
|
-
let hasDigit = false;
|
|
4689
|
-
for (const c of token) {
|
|
4690
|
-
if (c >= "A" && c <= "Z") hasUpper = true;
|
|
4691
|
-
else if (c >= "a" && c <= "z") hasLower = true;
|
|
4692
|
-
else if (c >= "0" && c <= "9") hasDigit = true;
|
|
4693
|
-
}
|
|
4694
|
-
return hasUpper && hasLower && hasDigit;
|
|
4695
|
-
}
|
|
4696
|
-
function detectApiKey(text) {
|
|
4697
|
-
const spans = spansFrom(API_KEY_PREFIX_RE, text);
|
|
4698
|
-
for (const [start, end] of spansFrom(API_KEY_GENERIC_RE, text)) {
|
|
4699
|
-
if (spans.some(([s, e]) => start < e && s < end)) continue;
|
|
4700
|
-
if (looksLikeGenericSecret(text.slice(start, end))) spans.push([start, end]);
|
|
4701
|
-
}
|
|
4702
|
-
return spans;
|
|
4703
|
-
}
|
|
4704
|
-
var BUILTIN = {
|
|
4705
|
-
email: (t) => spansFrom(EMAIL_RE, t),
|
|
4706
|
-
phone: (t) => spansFrom(PHONE_RE, t),
|
|
4707
|
-
ssn: (t) => spansFrom(SSN_RE, t),
|
|
4708
|
-
credit_card: detectCreditCard,
|
|
4709
|
-
iban: (t) => spansFrom(IBAN_RE, t),
|
|
4710
|
-
api_key: detectApiKey
|
|
4711
|
-
};
|
|
4712
|
-
function detectBuiltin(name, text) {
|
|
4713
|
-
const detector = BUILTIN[name];
|
|
4714
|
-
if (!detector) throw new Error(`unknown built-in detector: ${name}`);
|
|
4715
|
-
if (!text) return [];
|
|
4716
|
-
return detector(text).sort((a, b) => a[0] - b[0]);
|
|
4717
|
-
}
|
|
4718
|
-
function ruleApplies(rule, opts) {
|
|
4719
|
-
if (rule.applyAt !== "both" && rule.applyAt !== opts.phase) return false;
|
|
4720
|
-
if (opts.phase === "ingest") return true;
|
|
4721
|
-
if (!rule.unless.length) return true;
|
|
4722
|
-
if (opts.principals === null) return false;
|
|
4723
|
-
const held = new Set(opts.principals);
|
|
4724
|
-
return !rule.unless.some((p) => held.has(p));
|
|
4725
|
-
}
|
|
4726
|
-
function spansForRule(rule, text, policy) {
|
|
4727
|
-
if (rule.pattern !== null) {
|
|
4728
|
-
const re = rule.patternRe() ?? new RegExp(rule.pattern, "g");
|
|
4729
|
-
return spansFrom(re, text);
|
|
4730
|
-
}
|
|
4731
|
-
if (rule.detector !== null) {
|
|
4732
|
-
const custom = policy.customDetectors[rule.detector];
|
|
4733
|
-
if (custom) return [...custom(text)];
|
|
4734
|
-
return detectBuiltin(rule.detector, text);
|
|
4735
|
-
}
|
|
4736
|
-
return [];
|
|
4737
|
-
}
|
|
4738
|
-
var HASH_TOKEN_CHARS = 16;
|
|
4739
|
-
function hashToken(value, secretKey) {
|
|
4740
|
-
const key = Buffer.isBuffer(secretKey) ? secretKey : Buffer.from(String(secretKey ?? ""));
|
|
4741
|
-
return hmacSha256Hex(key, value).slice(0, HASH_TOKEN_CHARS);
|
|
4742
|
-
}
|
|
4743
|
-
function applyRedaction(text, policy, opts) {
|
|
4744
|
-
if (!text || policy.isEmpty()) return [text, {}];
|
|
4745
|
-
const principals = opts.principals ?? null;
|
|
4746
|
-
const collected = [];
|
|
4747
|
-
const fired = [];
|
|
4748
|
-
const failed = [];
|
|
4749
|
-
for (const rule of policy.rules) {
|
|
4750
|
-
if (!ruleApplies(rule, { phase: opts.phase, principals })) continue;
|
|
4751
|
-
if (rule.action === "hash" && !opts.secretKey) {
|
|
4752
|
-
throw new Error(
|
|
4753
|
-
`rule '${rule.name}': action='hash' requires a non-empty secretKey (an unkeyed HMAC is a reversible pseudonym, not a redaction)`
|
|
4754
|
-
);
|
|
5340
|
+
pendingBlank = units.length > 0;
|
|
5341
|
+
continue;
|
|
4755
5342
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
if (!(start >= 0 && start < end && end <= text.length)) {
|
|
4768
|
-
invalid = true;
|
|
4769
|
-
continue;
|
|
5343
|
+
const lineSep = pendingBlank ? "\n\n" : "\n";
|
|
5344
|
+
pendingBlank = false;
|
|
5345
|
+
if (HEADING_OR_BULLET_RE.test(ln) || stripped.length <= 120) {
|
|
5346
|
+
units.push([lineSep, stripped, tokenize(stripped).length]);
|
|
5347
|
+
} else {
|
|
5348
|
+
let first = true;
|
|
5349
|
+
for (const sent of splitSentences(ln)) {
|
|
5350
|
+
const s = sent.trim();
|
|
5351
|
+
if (s) {
|
|
5352
|
+
units.push([first ? lineSep : " ", s, tokenize(s).length]);
|
|
5353
|
+
first = false;
|
|
4770
5354
|
}
|
|
4771
|
-
ruleSpans.push([start, end]);
|
|
4772
5355
|
}
|
|
4773
|
-
if (invalid) failed.push(rule.name);
|
|
4774
|
-
for (const [s, e] of ruleSpans) collected.push([s, e, rule]);
|
|
4775
|
-
} catch (exc) {
|
|
4776
|
-
failed.push(rule.name);
|
|
4777
|
-
if (opts.hooks) emitError(opts.hooks, exc, { stage: "redaction", rule: rule.name });
|
|
4778
5356
|
}
|
|
4779
5357
|
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
collected.sort((a, b) => a[0] - b[0] || b[1] - b[0] - (a[1] - a[0]));
|
|
4785
|
-
const merged = [];
|
|
4786
|
-
for (const [start, end, rule] of collected) {
|
|
4787
|
-
const last = merged[merged.length - 1];
|
|
4788
|
-
if (last && start < last[1]) {
|
|
4789
|
-
if (end > last[1]) last[1] = end;
|
|
5358
|
+
const normalizedUnits = [];
|
|
5359
|
+
for (const [uSep, uText, uTokens] of units) {
|
|
5360
|
+
if (uTokens <= maxWords) {
|
|
5361
|
+
normalizedUnits.push([uSep, uText, uTokens]);
|
|
4790
5362
|
continue;
|
|
4791
5363
|
}
|
|
4792
|
-
|
|
5364
|
+
const windowChars = maxWords * 4;
|
|
5365
|
+
const uChars = [...uText];
|
|
5366
|
+
let pos = 0;
|
|
5367
|
+
let pieceSep = uSep;
|
|
5368
|
+
while (pos < uChars.length) {
|
|
5369
|
+
let pieceChars = uChars.slice(pos, pos + windowChars);
|
|
5370
|
+
let piece = pieceChars.join("");
|
|
5371
|
+
if (pos + windowChars < uChars.length) {
|
|
5372
|
+
const ws = piece.lastIndexOf(" ");
|
|
5373
|
+
if (ws > Math.floor(windowChars / 2)) {
|
|
5374
|
+
piece = piece.slice(0, ws);
|
|
5375
|
+
pieceChars = [...piece];
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
let pieceTokens = tokenize(piece);
|
|
5379
|
+
if (pieceTokens.length > maxWords) {
|
|
5380
|
+
piece = pieceChars.slice(0, maxWords).join("");
|
|
5381
|
+
pieceTokens = tokenize(piece);
|
|
5382
|
+
}
|
|
5383
|
+
normalizedUnits.push([pieceSep, piece, pieceTokens.length]);
|
|
5384
|
+
pos += codePointLength(piece);
|
|
5385
|
+
pieceSep = "";
|
|
5386
|
+
}
|
|
4793
5387
|
}
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
5388
|
+
if (!normalizedUnits.some(([, , t]) => t)) return [];
|
|
5389
|
+
const chunks = [];
|
|
5390
|
+
let lastKept = null;
|
|
5391
|
+
let cur = [];
|
|
5392
|
+
let curTokens = 0;
|
|
5393
|
+
const flush = () => {
|
|
5394
|
+
const chunk = assembleUnits(cur);
|
|
5395
|
+
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
5396
|
+
chunks.push(chunk);
|
|
5397
|
+
lastKept = chunk;
|
|
4802
5398
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
5399
|
+
const carried = [];
|
|
5400
|
+
let carriedTokens = 0;
|
|
5401
|
+
for (let i = cur.length - 1; i >= 0; i--) {
|
|
5402
|
+
const prev = cur[i];
|
|
5403
|
+
if (prev[2] === 0 || carriedTokens + prev[2] > overlap) break;
|
|
5404
|
+
carried.unshift(prev);
|
|
5405
|
+
carriedTokens += prev[2];
|
|
5406
|
+
}
|
|
5407
|
+
cur = carried;
|
|
5408
|
+
curTokens = carriedTokens;
|
|
5409
|
+
};
|
|
5410
|
+
for (const unit of normalizedUnits) {
|
|
5411
|
+
if (cur.length && curTokens + unit[2] > maxWords) flush();
|
|
5412
|
+
cur.push(unit);
|
|
5413
|
+
curTokens += unit[2];
|
|
4805
5414
|
}
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
5415
|
+
if (cur.length) {
|
|
5416
|
+
const chunk = assembleUnits(cur);
|
|
5417
|
+
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
5418
|
+
chunks.push(chunk);
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
return chunks;
|
|
4810
5422
|
}
|
|
4811
5423
|
|
|
5424
|
+
// src/actions.ts
|
|
5425
|
+
init_errors();
|
|
5426
|
+
init_hooks();
|
|
5427
|
+
init_llm();
|
|
5428
|
+
init_redaction();
|
|
5429
|
+
|
|
4812
5430
|
// src/sandbox.ts
|
|
4813
5431
|
init_errors();
|
|
4814
5432
|
var DEFAULT_TIMEOUT = 30;
|
|
@@ -5347,6 +5965,39 @@ async function nearestField(pool, vector, sourceIds, docType) {
|
|
|
5347
5965
|
if (similarity < FIELD_RESOLUTION_SIMILARITY_FLOOR) return null;
|
|
5348
5966
|
return String(row.key_norm);
|
|
5349
5967
|
}
|
|
5968
|
+
|
|
5969
|
+
// src/sentinels.ts
|
|
5970
|
+
var UNSET = /* @__PURE__ */ Symbol.for("context_engine.UNSET");
|
|
5971
|
+
var TRUSTED = /* @__PURE__ */ Symbol.for("context_engine.TRUSTED");
|
|
5972
|
+
var warnedMethods = /* @__PURE__ */ new Set();
|
|
5973
|
+
function resolvePrincipals(value, method) {
|
|
5974
|
+
if (value === TRUSTED) return null;
|
|
5975
|
+
if (value === void 0 || value === null) {
|
|
5976
|
+
if (!warnedMethods.has(method)) {
|
|
5977
|
+
warnedMethods.add(method);
|
|
5978
|
+
process.emitWarning(
|
|
5979
|
+
`${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.`,
|
|
5980
|
+
{ type: "DeprecationWarning", code: "CE_PRINCIPALS_NULL" }
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5983
|
+
return null;
|
|
5984
|
+
}
|
|
5985
|
+
if (!Array.isArray(value)) {
|
|
5986
|
+
throw new TypeError(
|
|
5987
|
+
`${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.`
|
|
5988
|
+
);
|
|
5989
|
+
}
|
|
5990
|
+
return value;
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
// src/tools/acl.ts
|
|
5994
|
+
function aclVisible(acl, principals) {
|
|
5995
|
+
if (principals === null || principals === TRUSTED) return true;
|
|
5996
|
+
if (acl == null) return true;
|
|
5997
|
+
if (!acl.length) return false;
|
|
5998
|
+
const held = new Set(principals ?? []);
|
|
5999
|
+
return acl.some((p) => held.has(p));
|
|
6000
|
+
}
|
|
5350
6001
|
init_extraction();
|
|
5351
6002
|
init_hooks();
|
|
5352
6003
|
var TIMEOUT_MS2 = 3e4;
|
|
@@ -5434,6 +6085,7 @@ async function pollEmbeddingBatch(cfg2, batchId, opts = {}) {
|
|
|
5434
6085
|
}
|
|
5435
6086
|
|
|
5436
6087
|
// src/ingest.ts
|
|
6088
|
+
init_redaction();
|
|
5437
6089
|
init_usage();
|
|
5438
6090
|
var MODE_RANK = { hybrid: 0, graph: 1 };
|
|
5439
6091
|
var EMBED_BATCH = 256;
|
|
@@ -5520,7 +6172,9 @@ async function prepare(opts) {
|
|
|
5520
6172
|
mediaOnly: false,
|
|
5521
6173
|
providerTokens: {},
|
|
5522
6174
|
contentHash: calculateContentHash({ fullText: clean }),
|
|
5523
|
-
isMarkdown: false
|
|
6175
|
+
isMarkdown: false,
|
|
6176
|
+
unreadableReason: null,
|
|
6177
|
+
unreadablePages: 0
|
|
5524
6178
|
};
|
|
5525
6179
|
}
|
|
5526
6180
|
const content = opts.content;
|
|
@@ -5528,11 +6182,12 @@ async function prepare(opts) {
|
|
|
5528
6182
|
const mime = mimeForFilename(fname) || "";
|
|
5529
6183
|
const extracted = await extract2(content, fname, mime || null, {
|
|
5530
6184
|
visionLlm: opts.config.visionLlm,
|
|
6185
|
+
extraction: opts.config.extraction,
|
|
5531
6186
|
hooks: opts.hooks
|
|
5532
6187
|
});
|
|
5533
6188
|
let body = sanitizeText(extracted.text || "") || "";
|
|
5534
6189
|
const effectiveMime = mime || inferMime(body, fname) || DEFAULT_MIME;
|
|
5535
|
-
if (effectiveMime === "application/pdf" && body) body = dedupLines(body);
|
|
6190
|
+
if (effectiveMime === "application/pdf" && body && !extracted.isMarkdown) body = dedupLines(body);
|
|
5536
6191
|
return {
|
|
5537
6192
|
text: body,
|
|
5538
6193
|
mime: effectiveMime,
|
|
@@ -5543,7 +6198,9 @@ async function prepare(opts) {
|
|
|
5543
6198
|
mediaOnly: Boolean(extracted.mediaOnly),
|
|
5544
6199
|
providerTokens: { ...extracted.providerTokens ?? {} },
|
|
5545
6200
|
contentHash: calculateContentHash({ docBytes: content }),
|
|
5546
|
-
isMarkdown: Boolean(extracted.isMarkdown)
|
|
6201
|
+
isMarkdown: Boolean(extracted.isMarkdown),
|
|
6202
|
+
unreadableReason: extracted.unreadableReason ?? null,
|
|
6203
|
+
unreadablePages: extracted.unreadablePages ?? 0
|
|
5547
6204
|
};
|
|
5548
6205
|
}
|
|
5549
6206
|
function computeUnits(prepared) {
|
|
@@ -5721,7 +6378,11 @@ async function decide(pool, opts) {
|
|
|
5721
6378
|
}
|
|
5722
6379
|
const same = stored != null && incoming != null && (Buffer.isBuffer(stored) && Buffer.isBuffer(incoming) ? Buffer.compare(stored, incoming) === 0 : stored === incoming);
|
|
5723
6380
|
if (!same) return { action: "process", documentId: existingId };
|
|
5724
|
-
if (existing.status === "skipped")
|
|
6381
|
+
if (existing.status === "skipped") {
|
|
6382
|
+
const metaData = existing.meta_data;
|
|
6383
|
+
if (metaData?.unreadable_reason) return { action: "process", documentId: existingId };
|
|
6384
|
+
return { action: "skip", documentId: existingId };
|
|
6385
|
+
}
|
|
5725
6386
|
const reqMode = opts.request.mode ?? "hybrid";
|
|
5726
6387
|
if ((MODE_RANK[reqMode] ?? 0) <= (MODE_RANK[existing.mode || "hybrid"] ?? 0)) {
|
|
5727
6388
|
return { action: "skip", documentId: existingId };
|
|
@@ -6012,6 +6673,22 @@ function singleReport(doc) {
|
|
|
6012
6673
|
async function maybeAwait(value) {
|
|
6013
6674
|
return value;
|
|
6014
6675
|
}
|
|
6676
|
+
function progress(hooks, request, displayName, documentId, stage, state, detail = {}) {
|
|
6677
|
+
emitProgress(hooks, {
|
|
6678
|
+
sourceId: request.sourceId ?? null,
|
|
6679
|
+
externalId: request.externalId ?? null,
|
|
6680
|
+
documentId: documentId == null ? null : String(documentId),
|
|
6681
|
+
name: displayName,
|
|
6682
|
+
stage,
|
|
6683
|
+
state,
|
|
6684
|
+
detail: { ...detail }
|
|
6685
|
+
});
|
|
6686
|
+
}
|
|
6687
|
+
function extractMethod(prepared, text) {
|
|
6688
|
+
if (text != null) return "text";
|
|
6689
|
+
if (Object.keys(prepared.providerTokens).length || prepared.isMarkdown) return "vision";
|
|
6690
|
+
return "parser";
|
|
6691
|
+
}
|
|
6015
6692
|
async function runModeUpgrade(request, opts) {
|
|
6016
6693
|
await refreshOnSkip(request, {
|
|
6017
6694
|
pool: opts.pool,
|
|
@@ -6025,6 +6702,7 @@ async function runModeUpgrade(request, opts) {
|
|
|
6025
6702
|
let chunkCount = 0;
|
|
6026
6703
|
try {
|
|
6027
6704
|
if (request.mode === "graph" && opts.graphStage) {
|
|
6705
|
+
progress(opts.hooks, request, opts.displayName, opts.documentId, "graph", "started");
|
|
6028
6706
|
graphUnits2 = Number(
|
|
6029
6707
|
await maybeAwait(
|
|
6030
6708
|
opts.graphStage({
|
|
@@ -6035,6 +6713,9 @@ async function runModeUpgrade(request, opts) {
|
|
|
6035
6713
|
})
|
|
6036
6714
|
) || 0
|
|
6037
6715
|
);
|
|
6716
|
+
progress(opts.hooks, request, opts.displayName, opts.documentId, "graph", "done", {
|
|
6717
|
+
graph_units: graphUnits2
|
|
6718
|
+
});
|
|
6038
6719
|
}
|
|
6039
6720
|
chunkCount = await upgradeMode(opts.pool, opts.documentId, request.mode ?? "hybrid");
|
|
6040
6721
|
} catch (exc) {
|
|
@@ -6124,6 +6805,7 @@ async function runIngest(request, opts) {
|
|
|
6124
6805
|
);
|
|
6125
6806
|
}
|
|
6126
6807
|
}
|
|
6808
|
+
progress(opts.hooks, request, displayName, null, "extract", "started");
|
|
6127
6809
|
const prepared = await prepare({
|
|
6128
6810
|
content,
|
|
6129
6811
|
filename,
|
|
@@ -6132,6 +6814,11 @@ async function runIngest(request, opts) {
|
|
|
6132
6814
|
config: opts.config,
|
|
6133
6815
|
hooks: opts.hooks
|
|
6134
6816
|
});
|
|
6817
|
+
progress(opts.hooks, request, displayName, null, "extract", "done", {
|
|
6818
|
+
pages: prepared.pages,
|
|
6819
|
+
method: extractMethod(prepared, text),
|
|
6820
|
+
mime: prepared.mime
|
|
6821
|
+
});
|
|
6135
6822
|
const ingestHash = sha256Bytes(prepared.text || "");
|
|
6136
6823
|
const decision = await decide(opts.pool, {
|
|
6137
6824
|
request: { ...request, mode },
|
|
@@ -6218,8 +6905,11 @@ async function runIngest(request, opts) {
|
|
|
6218
6905
|
const metaUpdates = { ...request.metaData ?? {} };
|
|
6219
6906
|
metaUpdates.mime_type = prepared.mime;
|
|
6220
6907
|
if (prepared.contentHash) metaUpdates.content_hash = prepared.contentHash;
|
|
6908
|
+
metaUpdates.unreadable_reason = prepared.unreadableReason;
|
|
6909
|
+
metaUpdates.unreadable_pages = prepared.unreadablePages;
|
|
6221
6910
|
const redactionFailed = [];
|
|
6222
6911
|
let documentText;
|
|
6912
|
+
progress(opts.hooks, request, displayName, documentId, "redact", "started");
|
|
6223
6913
|
try {
|
|
6224
6914
|
documentText = redactDocumentText(
|
|
6225
6915
|
prepared.text,
|
|
@@ -6232,6 +6922,9 @@ async function runIngest(request, opts) {
|
|
|
6232
6922
|
await finalizeDocument(opts.pool, documentId, { status: "failed", error: String(exc) });
|
|
6233
6923
|
throw exc;
|
|
6234
6924
|
}
|
|
6925
|
+
progress(opts.hooks, request, displayName, documentId, "redact", "done", {
|
|
6926
|
+
rules_failed: redactionFailed.length
|
|
6927
|
+
});
|
|
6235
6928
|
if (!prepared.text.trim() || prepared.mediaOnly || looksMostlyBoilerplate(prepared.text)) {
|
|
6236
6929
|
const reason = prepared.mediaOnly ? "media-only (no extractable text)" : "no extractable text";
|
|
6237
6930
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, []);
|
|
@@ -6257,7 +6950,13 @@ async function runIngest(request, opts) {
|
|
|
6257
6950
|
graphUnits: 0,
|
|
6258
6951
|
providerTokens: {},
|
|
6259
6952
|
error: reason,
|
|
6260
|
-
redactionFailed
|
|
6953
|
+
redactionFailed,
|
|
6954
|
+
// Extraction RAN here — an unreadable scan is exactly what lands in
|
|
6955
|
+
// this branch, so this is the report that has to say why. The
|
|
6956
|
+
// dedup-skip/upgrade sites stay at the defaults: they never extracted
|
|
6957
|
+
// and would be claiming blind.
|
|
6958
|
+
unreadableReason: prepared.unreadableReason,
|
|
6959
|
+
unreadablePages: prepared.unreadablePages
|
|
6261
6960
|
});
|
|
6262
6961
|
}
|
|
6263
6962
|
const units = computeUnits(prepared);
|
|
@@ -6267,18 +6966,26 @@ async function runIngest(request, opts) {
|
|
|
6267
6966
|
let rows = [];
|
|
6268
6967
|
let effectiveMime = prepared.mime;
|
|
6269
6968
|
try {
|
|
6969
|
+
progress(opts.hooks, request, displayName, documentId, "chunk", "started");
|
|
6270
6970
|
[rows, effectiveMime] = buildChunks(prepared, request.name, {
|
|
6271
6971
|
policy: opts.config.redaction,
|
|
6272
6972
|
secretKey: opts.config.secretKey,
|
|
6273
6973
|
hooks: opts.hooks,
|
|
6274
6974
|
failed: redactionFailed
|
|
6275
6975
|
});
|
|
6976
|
+
progress(opts.hooks, request, displayName, documentId, "chunk", "done", { chunks: rows.length });
|
|
6977
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "started");
|
|
6276
6978
|
if (request.batch) {
|
|
6277
6979
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, rows);
|
|
6278
6980
|
const batchId = await submitEmbeddingBatch(
|
|
6279
6981
|
opts.config.embedding,
|
|
6280
6982
|
rows.map((r) => r.text)
|
|
6281
6983
|
);
|
|
6984
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "done", {
|
|
6985
|
+
batch: true,
|
|
6986
|
+
batch_id: batchId,
|
|
6987
|
+
chunks: rows.length
|
|
6988
|
+
});
|
|
6282
6989
|
metaUpdates.mime_type = effectiveMime;
|
|
6283
6990
|
metaUpdates.batch = {
|
|
6284
6991
|
id: batchId,
|
|
@@ -6308,15 +7015,26 @@ async function runIngest(request, opts) {
|
|
|
6308
7015
|
graphUnits: 0,
|
|
6309
7016
|
providerTokens,
|
|
6310
7017
|
error: null,
|
|
6311
|
-
redactionFailed
|
|
7018
|
+
redactionFailed,
|
|
7019
|
+
// Extraction RAN here just like the skipped/failed/completed sites —
|
|
7020
|
+
// this report has to say why pages were unreadable too, not leave
|
|
7021
|
+
// the caller to re-derive it later from listDocuments.
|
|
7022
|
+
unreadableReason: prepared.unreadableReason,
|
|
7023
|
+
unreadablePages: prepared.unreadablePages
|
|
6312
7024
|
});
|
|
6313
7025
|
}
|
|
6314
7026
|
const embeddingTokens = await embedChunks(opts.embedder, rows);
|
|
6315
7027
|
if (embeddingTokens) providerTokens.embedding_tokens = embeddingTokens;
|
|
6316
7028
|
await recordEmbeddingDim(opts.pool, opts.embedder.dim);
|
|
6317
7029
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, rows);
|
|
7030
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "done", {
|
|
7031
|
+
batch: false,
|
|
7032
|
+
chunks: rows.length,
|
|
7033
|
+
tokens: embeddingTokens
|
|
7034
|
+
});
|
|
6318
7035
|
const structuredKw = {};
|
|
6319
7036
|
if (request.extractStructured && opts.config.llm) {
|
|
7037
|
+
progress(opts.hooks, request, displayName, documentId, "structured", "started");
|
|
6320
7038
|
const extraction = await extractStructuredData(documentText, {
|
|
6321
7039
|
llmCfg: opts.config.llm,
|
|
6322
7040
|
fieldHints: request.fieldHints
|
|
@@ -6357,9 +7075,15 @@ async function runIngest(request, opts) {
|
|
|
6357
7075
|
document: displayName
|
|
6358
7076
|
});
|
|
6359
7077
|
}
|
|
7078
|
+
progress(opts.hooks, request, displayName, documentId, "structured", "done", {
|
|
7079
|
+
document_type: extraction.documentType,
|
|
7080
|
+
keys: extraction.keysNormalized.length,
|
|
7081
|
+
quality: extraction.quality
|
|
7082
|
+
});
|
|
6360
7083
|
}
|
|
6361
7084
|
let graphUnits2 = 0;
|
|
6362
7085
|
if (mode === "graph" && opts.graphStage) {
|
|
7086
|
+
progress(opts.hooks, request, displayName, documentId, "graph", "started");
|
|
6363
7087
|
graphUnits2 = Number(
|
|
6364
7088
|
await maybeAwait(
|
|
6365
7089
|
opts.graphStage({
|
|
@@ -6370,6 +7094,7 @@ async function runIngest(request, opts) {
|
|
|
6370
7094
|
})
|
|
6371
7095
|
) || 0
|
|
6372
7096
|
);
|
|
7097
|
+
progress(opts.hooks, request, displayName, documentId, "graph", "done", { graph_units: graphUnits2 });
|
|
6373
7098
|
}
|
|
6374
7099
|
metaUpdates.mime_type = effectiveMime;
|
|
6375
7100
|
await finalizeDocument(opts.pool, documentId, {
|
|
@@ -6409,7 +7134,9 @@ async function runIngest(request, opts) {
|
|
|
6409
7134
|
graphUnits: graphUnits2,
|
|
6410
7135
|
providerTokens,
|
|
6411
7136
|
error: null,
|
|
6412
|
-
redactionFailed
|
|
7137
|
+
redactionFailed,
|
|
7138
|
+
unreadableReason: prepared.unreadableReason,
|
|
7139
|
+
unreadablePages: prepared.unreadablePages
|
|
6413
7140
|
});
|
|
6414
7141
|
} catch (exc) {
|
|
6415
7142
|
emitError(opts.hooks, exc, { stage: "ingest", document: displayName });
|
|
@@ -6424,7 +7151,9 @@ async function runIngest(request, opts) {
|
|
|
6424
7151
|
graphUnits: 0,
|
|
6425
7152
|
providerTokens,
|
|
6426
7153
|
error: String(exc),
|
|
6427
|
-
redactionFailed
|
|
7154
|
+
redactionFailed,
|
|
7155
|
+
unreadableReason: prepared.unreadableReason,
|
|
7156
|
+
unreadablePages: prepared.unreadablePages
|
|
6428
7157
|
});
|
|
6429
7158
|
}
|
|
6430
7159
|
}
|
|
@@ -6521,12 +7250,7 @@ function parseDocumentId(documentId) {
|
|
|
6521
7250
|
}
|
|
6522
7251
|
return raw;
|
|
6523
7252
|
}
|
|
6524
|
-
|
|
6525
|
-
if (principals === null) return true;
|
|
6526
|
-
if (acl == null) return true;
|
|
6527
|
-
const held = new Set(principals);
|
|
6528
|
-
return acl.some((p) => held.has(p));
|
|
6529
|
-
}
|
|
7253
|
+
var visible = aclVisible;
|
|
6530
7254
|
function scopeSql(sourceIds, principals, params) {
|
|
6531
7255
|
const where = [];
|
|
6532
7256
|
if (sourceIds != null) {
|
|
@@ -6615,7 +7339,9 @@ async function getDocumentRow(pool, documentId, principals) {
|
|
|
6615
7339
|
createdAt: doc.created_at,
|
|
6616
7340
|
updatedAt: doc.updated_at,
|
|
6617
7341
|
startedAt: doc.started_at,
|
|
6618
|
-
completedAt: doc.completed_at
|
|
7342
|
+
completedAt: doc.completed_at,
|
|
7343
|
+
unreadableReason: doc.meta_data?.unreadable_reason ?? null,
|
|
7344
|
+
unreadablePages: doc.meta_data?.unreadable_pages ?? 0
|
|
6619
7345
|
};
|
|
6620
7346
|
}
|
|
6621
7347
|
async function stats(pool, sourceId) {
|
|
@@ -6672,7 +7398,7 @@ async function listDocuments(opts) {
|
|
|
6672
7398
|
params.push(limit + 1);
|
|
6673
7399
|
const { rows } = await opts.pool.query(
|
|
6674
7400
|
`SELECT id, source_id, external_id, name, description, mode, mime_type, lang, status,
|
|
6675
|
-
document_type, created_at, updated_at, acl
|
|
7401
|
+
document_type, created_at, updated_at, acl, meta_data
|
|
6676
7402
|
FROM context_engine_documents
|
|
6677
7403
|
${where}
|
|
6678
7404
|
ORDER BY created_at DESC, id DESC
|
|
@@ -6697,7 +7423,15 @@ async function listDocuments(opts) {
|
|
|
6697
7423
|
hooks: opts.hooks
|
|
6698
7424
|
}),
|
|
6699
7425
|
createdAt: doc.created_at instanceof Date ? doc.created_at.toISOString() : doc.created_at,
|
|
6700
|
-
updatedAt: doc.updated_at instanceof Date ? doc.updated_at.toISOString() : doc.updated_at
|
|
7426
|
+
updatedAt: doc.updated_at instanceof Date ? doc.updated_at.toISOString() : doc.updated_at,
|
|
7427
|
+
unreadableReason: doc.meta_data?.unreadable_reason ?? null,
|
|
7428
|
+
unreadablePages: doc.meta_data?.unreadable_pages ?? 0,
|
|
7429
|
+
// WHO can see this. Stored, enforced on every query and editable through
|
|
7430
|
+
// updateDocument — and, until this line, invisible to every caller,
|
|
7431
|
+
// because this serializer builds a FIXED object. `null` is UNRESTRICTED
|
|
7432
|
+
// and must stay null; an empty array would read as "nobody", which is the
|
|
7433
|
+
// opposite claim.
|
|
7434
|
+
acl: doc.acl ?? null
|
|
6701
7435
|
}));
|
|
6702
7436
|
const result = {
|
|
6703
7437
|
documents,
|
|
@@ -6855,9 +7589,9 @@ async function compute(instruction, opts) {
|
|
|
6855
7589
|
return `mime_type ILIKE $${params.length}`;
|
|
6856
7590
|
}).join(" OR ");
|
|
6857
7591
|
where = where ? `${where} AND (${mimeClause})` : `WHERE (${mimeClause})`;
|
|
6858
|
-
if (opts.
|
|
7592
|
+
if (opts.documentIds?.length) {
|
|
6859
7593
|
const parsedIds = [];
|
|
6860
|
-
for (const did of opts.
|
|
7594
|
+
for (const did of opts.documentIds) {
|
|
6861
7595
|
try {
|
|
6862
7596
|
parsedIds.push(parseDocumentId(did));
|
|
6863
7597
|
} catch {
|
|
@@ -6885,13 +7619,13 @@ async function compute(instruction, opts) {
|
|
|
6885
7619
|
}
|
|
6886
7620
|
if (tabular.length > MAX_COMPUTE_DOCUMENTS) {
|
|
6887
7621
|
throw new EngineActionError(
|
|
6888
|
-
`more than ${MAX_COMPUTE_DOCUMENTS} tabular documents are in scope for compute() \u2014 narrow the request with
|
|
7622
|
+
`more than ${MAX_COMPUTE_DOCUMENTS} tabular documents are in scope for compute() \u2014 narrow the request with documentIds or sourceIds`
|
|
6889
7623
|
);
|
|
6890
7624
|
}
|
|
6891
7625
|
const totalChars = tabular.reduce((n, d) => n + String(d.text ?? "").length, 0);
|
|
6892
7626
|
if (totalChars > MAX_COMPUTE_TEXT_CHARS) {
|
|
6893
7627
|
throw new EngineActionError(
|
|
6894
|
-
`in-scope spreadsheet text too large to load (${totalChars} chars > ${MAX_COMPUTE_TEXT_CHARS} cap) \u2014 narrow the request with
|
|
7628
|
+
`in-scope spreadsheet text too large to load (${totalChars} chars > ${MAX_COMPUTE_TEXT_CHARS} cap) \u2014 narrow the request with documentIds or sourceIds`
|
|
6895
7629
|
);
|
|
6896
7630
|
}
|
|
6897
7631
|
const dfs = {};
|
|
@@ -6931,182 +7665,52 @@ ${schemaLines.join("\n")}`;
|
|
|
6931
7665
|
user: userPrompt,
|
|
6932
7666
|
jsonMode: false
|
|
6933
7667
|
});
|
|
6934
|
-
} catch (exc) {
|
|
6935
|
-
emitError(opts.hooks, exc, { stage: "compute_codegen", instruction: instruction.slice(0, 200) });
|
|
6936
|
-
throw exc;
|
|
6937
|
-
}
|
|
6938
|
-
const code = stripCodeFences(rawCode);
|
|
6939
|
-
const execResult = await executeSafeCode(code, { dfs, documents }, { timeout });
|
|
6940
|
-
if (!execResult.success) {
|
|
6941
|
-
let maskedCode;
|
|
6942
|
-
try {
|
|
6943
|
-
maskedCode = redactValueRecursive(code.slice(0, 500), opts.config.redaction, {
|
|
6944
|
-
principals,
|
|
6945
|
-
secretKey: opts.config.secretKey,
|
|
6946
|
-
hooks: opts.hooks
|
|
6947
|
-
});
|
|
6948
|
-
} catch {
|
|
6949
|
-
maskedCode = "<redaction failed: code omitted>";
|
|
6950
|
-
}
|
|
6951
|
-
emitError(opts.hooks, new Error(execResult.error || "compute execution failed"), {
|
|
6952
|
-
stage: "compute_exec",
|
|
6953
|
-
code: maskedCode
|
|
6954
|
-
});
|
|
6955
|
-
}
|
|
6956
|
-
const result = {
|
|
6957
|
-
success: execResult.success,
|
|
6958
|
-
result: execResult.result,
|
|
6959
|
-
code,
|
|
6960
|
-
stdout: execResult.stdout ?? "",
|
|
6961
|
-
error: execResult.error,
|
|
6962
|
-
executionTime: execResult.executionTime,
|
|
6963
|
-
documentsUsed: documents.map((d) => d.id),
|
|
6964
|
-
providerTokens: {
|
|
6965
|
-
llm_input: tokens?.input ?? 0,
|
|
6966
|
-
llm_output: tokens?.output ?? 0
|
|
6967
|
-
}
|
|
6968
|
-
};
|
|
6969
|
-
return redactValueRecursive(result, opts.config.redaction, {
|
|
6970
|
-
principals,
|
|
6971
|
-
secretKey: opts.config.secretKey,
|
|
6972
|
-
hooks: opts.hooks
|
|
6973
|
-
});
|
|
6974
|
-
}
|
|
6975
|
-
var embeddingSchema = z.object({
|
|
6976
|
-
provider: z.enum(["openai", "azure_openai", "gemini", "voyage", "cohere", "custom"]),
|
|
6977
|
-
model: z.string(),
|
|
6978
|
-
dim: z.number().int().positive().nullable().optional().default(null),
|
|
6979
|
-
apiKey: z.string().nullable().optional().default(null),
|
|
6980
|
-
baseUrl: z.string().nullable().optional().default(null)
|
|
6981
|
-
});
|
|
6982
|
-
var llmSchema = z.object({
|
|
6983
|
-
provider: z.enum(["anthropic", "openai", "azure_openai", "gemini", "bedrock", "custom"]),
|
|
6984
|
-
model: z.string(),
|
|
6985
|
-
apiKey: z.string().nullable().optional().default(null),
|
|
6986
|
-
baseUrl: z.string().nullable().optional().default(null)
|
|
6987
|
-
});
|
|
6988
|
-
var graphSchema = z.object({
|
|
6989
|
-
enabled: z.boolean().default(false),
|
|
6990
|
-
neo4jUri: z.string().nullable().optional().default(null),
|
|
6991
|
-
neo4jUser: z.string().default("neo4j"),
|
|
6992
|
-
neo4jPassword: z.string().nullable().optional().default(null),
|
|
6993
|
-
neo4jDatabase: z.string().default("neo4j"),
|
|
6994
|
-
extractionLlm: llmSchema.nullable().optional().default(null)
|
|
6995
|
-
});
|
|
6996
|
-
var rerankerSchema = z.object({
|
|
6997
|
-
enabled: z.boolean().default(false),
|
|
6998
|
-
provider: z.enum(["cohere", "voyage", "jina", "custom"]).nullable().optional().default(null),
|
|
6999
|
-
model: z.string().nullable().optional().default(null),
|
|
7000
|
-
apiKey: z.string().nullable().optional().default(null),
|
|
7001
|
-
baseUrl: z.string().nullable().optional().default(null),
|
|
7002
|
-
candidates: z.number().int().positive().default(50)
|
|
7003
|
-
});
|
|
7004
|
-
var fusionSchema = z.object({
|
|
7005
|
-
method: z.literal("rrf").default("rrf"),
|
|
7006
|
-
k: z.number().int().positive().default(60),
|
|
7007
|
-
weights: z.record(z.string(), z.number()).default({ fts: 1, trgm: 0.8, ann: 1, graph: 1 })
|
|
7008
|
-
});
|
|
7009
|
-
var storageSchema = z.object({
|
|
7010
|
-
backend: z.literal("postgres").default("postgres"),
|
|
7011
|
-
annExactThreshold: z.number().int().positive().default(5e4)
|
|
7012
|
-
});
|
|
7013
|
-
var ContextEngineConfig = class _ContextEngineConfig {
|
|
7014
|
-
databaseUrl;
|
|
7015
|
-
storage;
|
|
7016
|
-
defaultMode;
|
|
7017
|
-
embedding;
|
|
7018
|
-
llm;
|
|
7019
|
-
visionLlm;
|
|
7020
|
-
graph;
|
|
7021
|
-
reranker;
|
|
7022
|
-
fusion;
|
|
7023
|
-
enableCodeExecution;
|
|
7024
|
-
secretKey;
|
|
7025
|
-
redaction;
|
|
7026
|
-
constructor(init) {
|
|
7027
|
-
this.databaseUrl = init.databaseUrl;
|
|
7028
|
-
this.storage = storageSchema.parse(init.storage ?? {});
|
|
7029
|
-
this.defaultMode = init.defaultMode ?? "hybrid";
|
|
7030
|
-
this.embedding = embeddingSchema.parse(init.embedding);
|
|
7031
|
-
this.llm = init.llm ? llmSchema.parse(init.llm) : null;
|
|
7032
|
-
this.visionLlm = init.visionLlm ? llmSchema.parse(init.visionLlm) : null;
|
|
7033
|
-
this.graph = graphSchema.parse(init.graph ?? {});
|
|
7034
|
-
this.reranker = rerankerSchema.parse(init.reranker ?? {});
|
|
7035
|
-
this.fusion = fusionSchema.parse(init.fusion ?? {});
|
|
7036
|
-
this.enableCodeExecution = init.enableCodeExecution ?? false;
|
|
7037
|
-
this.secretKey = init.secretKey ?? null;
|
|
7038
|
-
this.redaction = init.redaction instanceof RedactionPolicy ? init.redaction : new RedactionPolicy(init.redaction ?? {});
|
|
7039
|
-
this.validate();
|
|
7040
|
-
}
|
|
7041
|
-
validate() {
|
|
7042
|
-
if (this.graph.enabled && !(this.graph.neo4jUri && this.graph.neo4jPassword && this.graph.extractionLlm)) {
|
|
7043
|
-
throw new Error("graph enabled but neo4jUri/neo4jPassword/extractionLlm missing");
|
|
7044
|
-
}
|
|
7045
|
-
if (this.reranker.enabled && !(this.reranker.provider && this.reranker.apiKey)) {
|
|
7046
|
-
throw new Error("reranker enabled but provider/apiKey missing");
|
|
7047
|
-
}
|
|
7048
|
-
if (this.defaultMode === "graph" && !this.graph.enabled) {
|
|
7049
|
-
throw new Error("defaultMode is 'graph' but graph enabled is false");
|
|
7050
|
-
}
|
|
7051
|
-
for (const rule of this.redaction.rules) {
|
|
7052
|
-
if (rule.action === "hash" && !this.secretKey) {
|
|
7053
|
-
throw new Error(
|
|
7054
|
-
`redaction rule '${rule.name}': action='hash' requires ContextEngineConfig.secretKey to be set`
|
|
7055
|
-
);
|
|
7056
|
-
}
|
|
7057
|
-
}
|
|
7058
|
-
}
|
|
7059
|
-
static fromEnv(overrides = {}) {
|
|
7060
|
-
const env = loadCeEnv();
|
|
7061
|
-
const merged = deepMerge(env, overrides);
|
|
7062
|
-
if (!merged.databaseUrl || !merged.embedding) {
|
|
7063
|
-
throw new Error(
|
|
7064
|
-
"ContextEngineConfig.fromEnv requires CE_DATABASE_URL and CE_EMBEDDING__PROVIDER/MODEL (or explicit overrides)"
|
|
7065
|
-
);
|
|
7066
|
-
}
|
|
7067
|
-
return new _ContextEngineConfig(merged);
|
|
7068
|
-
}
|
|
7069
|
-
};
|
|
7070
|
-
function camelize(key) {
|
|
7071
|
-
return key.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
7072
|
-
}
|
|
7073
|
-
function loadCeEnv() {
|
|
7074
|
-
const root = {};
|
|
7075
|
-
for (const [raw, value] of Object.entries(process.env)) {
|
|
7076
|
-
if (!raw.startsWith("CE_") || value === void 0) continue;
|
|
7077
|
-
const path = raw.slice(3).split("__").map(camelize);
|
|
7078
|
-
let cur = root;
|
|
7079
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
7080
|
-
const k = path[i];
|
|
7081
|
-
const next = cur[k];
|
|
7082
|
-
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
7083
|
-
cur = cur[k];
|
|
7084
|
-
}
|
|
7085
|
-
cur[path[path.length - 1]] = coerceEnv(value);
|
|
7086
|
-
}
|
|
7087
|
-
return root;
|
|
7088
|
-
}
|
|
7089
|
-
function coerceEnv(value) {
|
|
7090
|
-
if (value === "true") return true;
|
|
7091
|
-
if (value === "false") return false;
|
|
7092
|
-
if (/^-?\d+$/.test(value)) return Number(value);
|
|
7093
|
-
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
|
|
7094
|
-
return value;
|
|
7095
|
-
}
|
|
7096
|
-
function deepMerge(a, b) {
|
|
7097
|
-
const out = { ...a };
|
|
7098
|
-
for (const [k, v] of Object.entries(b)) {
|
|
7099
|
-
if (v === void 0) continue;
|
|
7100
|
-
const existing = out[k];
|
|
7101
|
-
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
7102
|
-
out[k] = deepMerge(existing, v);
|
|
7103
|
-
} else {
|
|
7104
|
-
out[k] = v;
|
|
7668
|
+
} catch (exc) {
|
|
7669
|
+
emitError(opts.hooks, exc, { stage: "compute_codegen", instruction: instruction.slice(0, 200) });
|
|
7670
|
+
throw exc;
|
|
7671
|
+
}
|
|
7672
|
+
const code = stripCodeFences(rawCode);
|
|
7673
|
+
const execResult = await executeSafeCode(code, { dfs, documents }, { timeout });
|
|
7674
|
+
if (!execResult.success) {
|
|
7675
|
+
let maskedCode;
|
|
7676
|
+
try {
|
|
7677
|
+
maskedCode = redactValueRecursive(code.slice(0, 500), opts.config.redaction, {
|
|
7678
|
+
principals,
|
|
7679
|
+
secretKey: opts.config.secretKey,
|
|
7680
|
+
hooks: opts.hooks
|
|
7681
|
+
});
|
|
7682
|
+
} catch {
|
|
7683
|
+
maskedCode = "<redaction failed: code omitted>";
|
|
7105
7684
|
}
|
|
7685
|
+
emitError(opts.hooks, new Error(execResult.error || "compute execution failed"), {
|
|
7686
|
+
stage: "compute_exec",
|
|
7687
|
+
code: maskedCode
|
|
7688
|
+
});
|
|
7106
7689
|
}
|
|
7107
|
-
|
|
7690
|
+
const result = {
|
|
7691
|
+
success: execResult.success,
|
|
7692
|
+
result: execResult.result,
|
|
7693
|
+
code,
|
|
7694
|
+
stdout: execResult.stdout ?? "",
|
|
7695
|
+
error: execResult.error,
|
|
7696
|
+
executionTime: execResult.executionTime,
|
|
7697
|
+
documentsUsed: documents.map((d) => d.id),
|
|
7698
|
+
providerTokens: {
|
|
7699
|
+
llm_input: tokens?.input ?? 0,
|
|
7700
|
+
llm_output: tokens?.output ?? 0
|
|
7701
|
+
}
|
|
7702
|
+
};
|
|
7703
|
+
return redactValueRecursive(result, opts.config.redaction, {
|
|
7704
|
+
principals,
|
|
7705
|
+
secretKey: opts.config.secretKey,
|
|
7706
|
+
hooks: opts.hooks
|
|
7707
|
+
});
|
|
7108
7708
|
}
|
|
7109
7709
|
|
|
7710
|
+
// src/index.ts
|
|
7711
|
+
init_config();
|
|
7712
|
+
init_crypto();
|
|
7713
|
+
|
|
7110
7714
|
// src/db.ts
|
|
7111
7715
|
init_errors();
|
|
7112
7716
|
var VERSION_TABLE = "context_engine_alembic_version";
|
|
@@ -7189,6 +7793,7 @@ async function runMigrate(databaseUrl, opts = {}) {
|
|
|
7189
7793
|
recorded = rev;
|
|
7190
7794
|
done.add(rev);
|
|
7191
7795
|
}
|
|
7796
|
+
await client.query(loadSql("0004_acl_indexes", dim));
|
|
7192
7797
|
const meta = await client.query("SELECT embedding_dim FROM context_engine_meta WHERE id = 1");
|
|
7193
7798
|
if (!meta.rows.length) {
|
|
7194
7799
|
await client.query("INSERT INTO context_engine_meta (id, embedding_dim) VALUES (1, $1)", [dim]);
|
|
@@ -7365,7 +7970,7 @@ async function loadGeminiEmbedClient(apiKey) {
|
|
|
7365
7970
|
if (!Ctor) {
|
|
7366
7971
|
throw new ExtraMissingError("gemini", specifier, "gemini embeddings");
|
|
7367
7972
|
}
|
|
7368
|
-
return new Ctor({ apiKey: apiKey ?? null });
|
|
7973
|
+
return new Ctor({ apiKey: apiKey ?? null, httpOptions: { timeout: TIMEOUT_MS3 } });
|
|
7369
7974
|
}
|
|
7370
7975
|
function buildEmbedder(cfg2, opts) {
|
|
7371
7976
|
if (opts?.client || opts?.fetch || opts?.fetchImpl) {
|
|
@@ -7383,6 +7988,9 @@ function buildEmbedder(cfg2, opts) {
|
|
|
7383
7988
|
throw new Error(`unknown embedding provider: ${JSON.stringify(cfg2.provider)}`);
|
|
7384
7989
|
}
|
|
7385
7990
|
|
|
7991
|
+
// src/engine.ts
|
|
7992
|
+
init_redaction();
|
|
7993
|
+
|
|
7386
7994
|
// src/compression.ts
|
|
7387
7995
|
init_text();
|
|
7388
7996
|
var TOKENS_PER_CHAR = 0.25;
|
|
@@ -8686,6 +9294,7 @@ async function rerank(cfg2, query, docs, opts = {}) {
|
|
|
8686
9294
|
}
|
|
8687
9295
|
|
|
8688
9296
|
// src/search.ts
|
|
9297
|
+
init_redaction();
|
|
8689
9298
|
var MIN_LEG_CANDIDATES = 50;
|
|
8690
9299
|
var MAX_LEG_CANDIDATES = 500;
|
|
8691
9300
|
var UNITS_PER_SCOPE_HYBRID = 1;
|
|
@@ -8895,6 +9504,7 @@ async function runSearch(query, opts) {
|
|
|
8895
9504
|
const topK = Math.max(1, Math.trunc(opts.topK ?? 10));
|
|
8896
9505
|
const scope = {
|
|
8897
9506
|
sourceIds: opts.sourceIds != null ? [...opts.sourceIds] : null,
|
|
9507
|
+
documentIds: opts.documentIds != null ? [...opts.documentIds] : null,
|
|
8898
9508
|
principals: opts.principals != null ? [...opts.principals] : null,
|
|
8899
9509
|
limit: legLimit(topK, opts.config)
|
|
8900
9510
|
};
|
|
@@ -8915,13 +9525,8 @@ async function runSearch(query, opts) {
|
|
|
8915
9525
|
let degraded = null;
|
|
8916
9526
|
if (mode === "graph" && !graphLeg) {
|
|
8917
9527
|
degraded = "graph_leg_unavailable";
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
new GraphLegUnavailable(
|
|
8921
|
-
"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."
|
|
8922
|
-
),
|
|
8923
|
-
{ stage: "graph_leg", mode, degraded }
|
|
8924
|
-
);
|
|
9528
|
+
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.";
|
|
9529
|
+
emitError(opts.hooks, new GraphLegUnavailable(reason), { stage: "graph_leg", mode, degraded });
|
|
8925
9530
|
}
|
|
8926
9531
|
const fused = rrfFuse(ranked, { k: opts.config.fusion.k, weights: opts.config.fusion.weights });
|
|
8927
9532
|
const window = opts.config.reranker.enabled ? Math.max(topK, opts.config.reranker.candidates) : topK;
|
|
@@ -8996,31 +9601,6 @@ async function runSearch(query, opts) {
|
|
|
8996
9601
|
return { hits, usage };
|
|
8997
9602
|
}
|
|
8998
9603
|
|
|
8999
|
-
// src/sentinels.ts
|
|
9000
|
-
var UNSET = /* @__PURE__ */ Symbol.for("context_engine.UNSET");
|
|
9001
|
-
var TrustedSentinel = class {
|
|
9002
|
-
[Symbol.toStringTag] = "TRUSTED";
|
|
9003
|
-
toString() {
|
|
9004
|
-
return "TRUSTED";
|
|
9005
|
-
}
|
|
9006
|
-
valueOf() {
|
|
9007
|
-
return true;
|
|
9008
|
-
}
|
|
9009
|
-
};
|
|
9010
|
-
var TRUSTED = Object.freeze(new TrustedSentinel());
|
|
9011
|
-
function resolvePrincipals(value, method) {
|
|
9012
|
-
if (value === TRUSTED) return null;
|
|
9013
|
-
if (value === void 0) return null;
|
|
9014
|
-
if (value === null) {
|
|
9015
|
-
process.emitWarning(
|
|
9016
|
-
`${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.`,
|
|
9017
|
-
"DeprecationWarning"
|
|
9018
|
-
);
|
|
9019
|
-
return null;
|
|
9020
|
-
}
|
|
9021
|
-
return Array.isArray(value) ? value : null;
|
|
9022
|
-
}
|
|
9023
|
-
|
|
9024
9604
|
// src/storage.ts
|
|
9025
9605
|
init_text();
|
|
9026
9606
|
var MIN_ITERATIVE_SCAN_VERSION = [0, 8];
|
|
@@ -9059,25 +9639,26 @@ function trigramThreshold(query) {
|
|
|
9059
9639
|
}
|
|
9060
9640
|
var SCOPE = `
|
|
9061
9641
|
AND ($1::text[] IS NULL OR c.source_id = ANY($1))
|
|
9062
|
-
AND ($2::
|
|
9642
|
+
AND ($2::uuid[] IS NULL OR c.document_id = ANY($2::uuid[]))
|
|
9643
|
+
AND ($3::text[] IS NULL OR c.acl IS NULL OR c.acl && $3)
|
|
9063
9644
|
`;
|
|
9064
9645
|
var SQL_FTS = `
|
|
9065
9646
|
SELECT c.id::text
|
|
9066
9647
|
FROM context_engine_chunks c
|
|
9067
9648
|
WHERE c.text IS NOT NULL
|
|
9068
|
-
AND c.text_search @@ websearch_to_tsquery('simple'::regconfig, $
|
|
9649
|
+
AND c.text_search @@ websearch_to_tsquery('simple'::regconfig, $4)
|
|
9069
9650
|
${SCOPE}
|
|
9070
|
-
ORDER BY ts_rank_cd(c.text_search, websearch_to_tsquery('simple'::regconfig, $
|
|
9071
|
-
LIMIT $
|
|
9651
|
+
ORDER BY ts_rank_cd(c.text_search, websearch_to_tsquery('simple'::regconfig, $4)) DESC, c.id
|
|
9652
|
+
LIMIT $5
|
|
9072
9653
|
`;
|
|
9073
9654
|
var SQL_TRGM = `
|
|
9074
9655
|
SELECT c.id::text
|
|
9075
9656
|
FROM context_engine_chunks c
|
|
9076
9657
|
WHERE c.text IS NOT NULL
|
|
9077
|
-
AND c.text_trgm_norm % $
|
|
9658
|
+
AND c.text_trgm_norm % $4
|
|
9078
9659
|
${SCOPE}
|
|
9079
|
-
ORDER BY similarity(c.text_trgm_norm, $
|
|
9080
|
-
LIMIT $
|
|
9660
|
+
ORDER BY similarity(c.text_trgm_norm, $4) DESC, c.id
|
|
9661
|
+
LIMIT $5
|
|
9081
9662
|
`;
|
|
9082
9663
|
var SQL_COUNT_ELIGIBLE = `
|
|
9083
9664
|
SELECT count(*)::int AS count FROM (
|
|
@@ -9085,13 +9666,13 @@ SELECT count(*)::int AS count FROM (
|
|
|
9085
9666
|
FROM context_engine_chunks c
|
|
9086
9667
|
WHERE c.embedding IS NOT NULL
|
|
9087
9668
|
${SCOPE}
|
|
9088
|
-
LIMIT $
|
|
9669
|
+
LIMIT $4
|
|
9089
9670
|
) probe
|
|
9090
9671
|
`;
|
|
9091
9672
|
var SQL_FILTER_IDS = `
|
|
9092
9673
|
SELECT c.id::text
|
|
9093
9674
|
FROM context_engine_chunks c
|
|
9094
|
-
WHERE c.id = ANY($
|
|
9675
|
+
WHERE c.id = ANY($4::uuid[])
|
|
9095
9676
|
${SCOPE}
|
|
9096
9677
|
`;
|
|
9097
9678
|
var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -9110,7 +9691,7 @@ FROM context_engine_chunks c
|
|
|
9110
9691
|
WHERE c.embedding IS NOT NULL
|
|
9111
9692
|
${SCOPE}
|
|
9112
9693
|
ORDER BY c.embedding <=> ${vec}
|
|
9113
|
-
LIMIT $
|
|
9694
|
+
LIMIT $4
|
|
9114
9695
|
`;
|
|
9115
9696
|
}
|
|
9116
9697
|
function sqlAnnExact(vec) {
|
|
@@ -9123,7 +9704,7 @@ WITH eligible AS MATERIALIZED (
|
|
|
9123
9704
|
)
|
|
9124
9705
|
SELECT id::text FROM eligible
|
|
9125
9706
|
ORDER BY embedding <=> ${vec}
|
|
9126
|
-
LIMIT $
|
|
9707
|
+
LIMIT $4
|
|
9127
9708
|
`;
|
|
9128
9709
|
}
|
|
9129
9710
|
function idsOf(rows) {
|
|
@@ -9150,24 +9731,26 @@ var PostgresBackend = class {
|
|
|
9150
9731
|
* mean different things.
|
|
9151
9732
|
*/
|
|
9152
9733
|
scopeParams(scope) {
|
|
9153
|
-
return
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9734
|
+
return {
|
|
9735
|
+
binds: [
|
|
9736
|
+
scope.sourceIds != null ? [...scope.sourceIds] : null,
|
|
9737
|
+
scope.documentIds != null ? [...scope.documentIds] : null,
|
|
9738
|
+
scope.principals != null ? [...scope.principals] : null
|
|
9739
|
+
],
|
|
9740
|
+
lim: Math.max(1, Math.trunc(scope.limit ?? 50))
|
|
9741
|
+
};
|
|
9158
9742
|
}
|
|
9159
9743
|
async upsertChunks(documentId, sourceId, acl, chunks) {
|
|
9160
9744
|
const client = await this.pool.connect();
|
|
9161
9745
|
try {
|
|
9162
9746
|
await client.query("BEGIN");
|
|
9163
9747
|
await client.query("DELETE FROM context_engine_chunks WHERE document_id = $1", [documentId]);
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
[
|
|
9748
|
+
const BATCH = 500;
|
|
9749
|
+
for (let start = 0; start < chunks.length; start += BATCH) {
|
|
9750
|
+
const batch = chunks.slice(start, start + BATCH);
|
|
9751
|
+
const params = [];
|
|
9752
|
+
const rows = batch.map((c) => {
|
|
9753
|
+
params.push(
|
|
9171
9754
|
randomUUID(),
|
|
9172
9755
|
documentId,
|
|
9173
9756
|
sourceId,
|
|
@@ -9176,7 +9759,16 @@ var PostgresBackend = class {
|
|
|
9176
9759
|
c.text,
|
|
9177
9760
|
c.lang ?? null,
|
|
9178
9761
|
JSON.stringify(c.meta ?? {})
|
|
9179
|
-
|
|
9762
|
+
);
|
|
9763
|
+
const p = params.length;
|
|
9764
|
+
const emb = c.embedding != null ? vectorLiteral2(c.embedding) : "NULL";
|
|
9765
|
+
return `($${p - 7}, $${p - 6}, $${p - 5}, $${p - 4}, $${p - 3}, $${p - 2}, $${p - 1}, ${emb}, $${p}::jsonb)`;
|
|
9766
|
+
});
|
|
9767
|
+
await client.query(
|
|
9768
|
+
`INSERT INTO context_engine_chunks
|
|
9769
|
+
(id, document_id, source_id, acl, idx, text, lang, embedding, meta_data)
|
|
9770
|
+
VALUES ${rows.join(", ")}`,
|
|
9771
|
+
params
|
|
9180
9772
|
);
|
|
9181
9773
|
}
|
|
9182
9774
|
await client.query("COMMIT");
|
|
@@ -9241,19 +9833,19 @@ var PostgresBackend = class {
|
|
|
9241
9833
|
}
|
|
9242
9834
|
}
|
|
9243
9835
|
if (!parsed.length) return [];
|
|
9244
|
-
const
|
|
9245
|
-
const { rows } = await this.pool.query(SQL_FILTER_IDS, [
|
|
9836
|
+
const { binds } = this.scopeParams(scope);
|
|
9837
|
+
const { rows } = await this.pool.query(SQL_FILTER_IDS, [...binds, parsed]);
|
|
9246
9838
|
const visible2 = new Set(idsOf(rows));
|
|
9247
9839
|
return chunkIds.filter((cid) => visible2.has(String(cid))).map(String);
|
|
9248
9840
|
}
|
|
9249
9841
|
async ftsSearch(query, scope) {
|
|
9250
|
-
const
|
|
9251
|
-
const { rows } = await this.pool.query(SQL_FTS, [
|
|
9842
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
9843
|
+
const { rows } = await this.pool.query(SQL_FTS, [...binds, query, lim]);
|
|
9252
9844
|
return idsOf(rows);
|
|
9253
9845
|
}
|
|
9254
9846
|
async trgmSearch(query, scope) {
|
|
9255
9847
|
const threshold = trigramThreshold(query);
|
|
9256
|
-
const
|
|
9848
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
9257
9849
|
const client = await this.pool.connect();
|
|
9258
9850
|
let previous = null;
|
|
9259
9851
|
let usedSetLimit = false;
|
|
@@ -9262,7 +9854,7 @@ var PostgresBackend = class {
|
|
|
9262
9854
|
const applied = await this.applyTrgmThreshold(client, threshold);
|
|
9263
9855
|
previous = applied.previous;
|
|
9264
9856
|
usedSetLimit = applied.usedSetLimit;
|
|
9265
|
-
const { rows } = await client.query(SQL_TRGM, [
|
|
9857
|
+
const { rows } = await client.query(SQL_TRGM, [...binds, query, lim]);
|
|
9266
9858
|
if (usedSetLimit) await this.restoreTrgmLimit(client, previous);
|
|
9267
9859
|
await client.query("COMMIT");
|
|
9268
9860
|
return idsOf(rows);
|
|
@@ -9308,18 +9900,18 @@ var PostgresBackend = class {
|
|
|
9308
9900
|
}
|
|
9309
9901
|
async annSearch(vector, scope) {
|
|
9310
9902
|
const literal = vectorLiteral2(vector);
|
|
9311
|
-
const
|
|
9312
|
-
const scoped =
|
|
9903
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
9904
|
+
const scoped = binds.some((v) => v !== null);
|
|
9313
9905
|
const client = await this.pool.connect();
|
|
9314
9906
|
try {
|
|
9315
9907
|
await client.query("BEGIN");
|
|
9316
|
-
if (scoped && await this.eligibleIsSmall(client,
|
|
9317
|
-
const { rows: rows2 } = await client.query(sqlAnnExact(literal), [
|
|
9908
|
+
if (scoped && await this.eligibleIsSmall(client, binds)) {
|
|
9909
|
+
const { rows: rows2 } = await client.query(sqlAnnExact(literal), [...binds, lim]);
|
|
9318
9910
|
await client.query("COMMIT");
|
|
9319
9911
|
return idsOf(rows2);
|
|
9320
9912
|
}
|
|
9321
9913
|
await this.tuneAnnScan(client, lim, scoped);
|
|
9322
|
-
const { rows } = await client.query(sqlAnn(literal), [
|
|
9914
|
+
const { rows } = await client.query(sqlAnn(literal), [...binds, lim]);
|
|
9323
9915
|
await client.query("COMMIT");
|
|
9324
9916
|
return idsOf(rows);
|
|
9325
9917
|
} catch (err) {
|
|
@@ -9332,11 +9924,17 @@ var PostgresBackend = class {
|
|
|
9332
9924
|
client.release();
|
|
9333
9925
|
}
|
|
9334
9926
|
}
|
|
9335
|
-
async eligibleIsSmall(client,
|
|
9927
|
+
async eligibleIsSmall(client, binds) {
|
|
9336
9928
|
const cap = this.exactThreshold + 1;
|
|
9337
|
-
const { rows } = await client.query(SQL_COUNT_ELIGIBLE, [
|
|
9929
|
+
const { rows } = await client.query(SQL_COUNT_ELIGIBLE, [...binds, cap]);
|
|
9338
9930
|
return Number(rows[0]?.count ?? 0) <= this.exactThreshold;
|
|
9339
9931
|
}
|
|
9932
|
+
/** Size the HNSW candidate budget, and make it iterative when filtered.
|
|
9933
|
+
*
|
|
9934
|
+
* Public because the graph leg's seed query (`graph/retrieval.ts`) is the
|
|
9935
|
+
* same shape — an ANN walk with the scope predicate as a POST-filter — and
|
|
9936
|
+
* must not carry a second copy of this. Call it inside a transaction on the
|
|
9937
|
+
* client the query itself will run on: every setting here is `SET LOCAL`. */
|
|
9340
9938
|
async tuneAnnScan(client, limit, scoped) {
|
|
9341
9939
|
const efSearch = Math.min(Math.max(Math.trunc(limit) * 4, ANN_MIN_EF_SEARCH), ANN_MAX_EF_SEARCH);
|
|
9342
9940
|
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
@@ -9429,15 +10027,7 @@ function functionTool(fn) {
|
|
|
9429
10027
|
// src/tools/governance.ts
|
|
9430
10028
|
init_errors();
|
|
9431
10029
|
init_hooks();
|
|
9432
|
-
|
|
9433
|
-
// src/tools/acl.ts
|
|
9434
|
-
function aclVisible(acl, principals) {
|
|
9435
|
-
if (principals === null) return true;
|
|
9436
|
-
if (acl == null) return true;
|
|
9437
|
-
if (!acl.length) return false;
|
|
9438
|
-
const held = new Set(principals ?? []);
|
|
9439
|
-
return acl.some((p) => held.has(p));
|
|
9440
|
-
}
|
|
10030
|
+
init_redaction();
|
|
9441
10031
|
|
|
9442
10032
|
// src/tools/approval.ts
|
|
9443
10033
|
init_errors();
|
|
@@ -9495,17 +10085,24 @@ function rowToRecord(row) {
|
|
|
9495
10085
|
toolName: String(row.tool_name),
|
|
9496
10086
|
toolArgsFrozen: row.tool_args_frozen ?? {},
|
|
9497
10087
|
sourceId: row.source_id ?? null,
|
|
10088
|
+
approvalScope: row.approval_scope ?? null,
|
|
9498
10089
|
principals: row.principals,
|
|
9499
10090
|
status: String(row.status),
|
|
9500
10091
|
approver: row.approver ?? null,
|
|
9501
10092
|
approverMeta: row.approver_meta ?? null,
|
|
9502
|
-
expiresAt:
|
|
9503
|
-
resolvedAt:
|
|
9504
|
-
createdAt:
|
|
10093
|
+
expiresAt: toDate(row.expires_at),
|
|
10094
|
+
resolvedAt: toDate(row.resolved_at),
|
|
10095
|
+
createdAt: toDate(row.created_at)
|
|
9505
10096
|
};
|
|
9506
10097
|
}
|
|
10098
|
+
function toDate(value) {
|
|
10099
|
+
if (value == null) return null;
|
|
10100
|
+
if (value instanceof Date) return value;
|
|
10101
|
+
return new Date(String(value));
|
|
10102
|
+
}
|
|
9507
10103
|
function visibilitySql(principals, paramIndex) {
|
|
9508
|
-
if (principals === null) return { sql: "TRUE", params: [] };
|
|
10104
|
+
if (principals === null || principals === TRUSTED) return { sql: "TRUE", params: [] };
|
|
10105
|
+
principals = principals;
|
|
9509
10106
|
if (!principals.length) {
|
|
9510
10107
|
return { sql: `(principals IS NULL OR principals = 'null'::jsonb)`, params: [] };
|
|
9511
10108
|
}
|
|
@@ -9514,41 +10111,95 @@ function visibilitySql(principals, paramIndex) {
|
|
|
9514
10111
|
params: [principals]
|
|
9515
10112
|
};
|
|
9516
10113
|
}
|
|
9517
|
-
|
|
10114
|
+
function claimVisibilitySql(principals, paramIndex) {
|
|
10115
|
+
if (principals === null || principals === TRUSTED) return { sql: "TRUE", params: [] };
|
|
10116
|
+
const noWall = `principals IS NULL OR principals = 'null'::jsonb OR principals = '[]'::jsonb`;
|
|
10117
|
+
principals = principals;
|
|
10118
|
+
if (!principals.length) return { sql: `(${noWall})`, params: [] };
|
|
10119
|
+
return { sql: `(${noWall} OR principals ?| $${paramIndex}::text[])`, params: [principals] };
|
|
10120
|
+
}
|
|
10121
|
+
function validateApprovalScope(approvalScope) {
|
|
10122
|
+
if (approvalScope === null || approvalScope === void 0) return null;
|
|
10123
|
+
if (typeof approvalScope !== "string") {
|
|
10124
|
+
throw new TypeError(`approvalScope must be a non-empty string or null, got ${typeof approvalScope}`);
|
|
10125
|
+
}
|
|
10126
|
+
if (!approvalScope) throw new Error("approvalScope must be a non-empty string or null, got ''");
|
|
10127
|
+
return approvalScope;
|
|
10128
|
+
}
|
|
10129
|
+
function pendingRow(opts, approvalScope) {
|
|
9518
10130
|
const policy = opts.policy ?? {};
|
|
9519
10131
|
const frozen = structuredClone(opts.args);
|
|
9520
10132
|
const createdAt = opts.now ?? /* @__PURE__ */ new Date();
|
|
9521
10133
|
const timeout = Number(policy.timeout_minutes ?? DEFAULT_TIMEOUT_MINUTES);
|
|
9522
10134
|
const expiresAt = new Date(createdAt.getTime() + timeout * 6e4);
|
|
10135
|
+
const principals = opts.principals === void 0 || opts.principals === TRUSTED ? null : opts.principals;
|
|
10136
|
+
return { frozen, createdAt, expiresAt, principals, approvalScope };
|
|
10137
|
+
}
|
|
10138
|
+
async function insertPending(engine, opts, row) {
|
|
9523
10139
|
const id = randomUUID();
|
|
9524
10140
|
await engine.pool.query(
|
|
9525
10141
|
`INSERT INTO context_engine_tool_approvals
|
|
9526
|
-
(id, tool_name, tool_args_frozen, source_id, principals, status, expires_at, created_at)
|
|
9527
|
-
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,'pending',$
|
|
10142
|
+
(id, tool_name, tool_args_frozen, source_id, approval_scope, principals, status, expires_at, created_at)
|
|
10143
|
+
VALUES ($1,$2,$3::jsonb,$4,$5,$6::jsonb,'pending',$7,$8)`,
|
|
9528
10144
|
[
|
|
9529
10145
|
id,
|
|
9530
10146
|
opts.toolName,
|
|
9531
|
-
JSON.stringify(frozen),
|
|
10147
|
+
JSON.stringify(row.frozen),
|
|
9532
10148
|
opts.sourceId ?? null,
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
10149
|
+
row.approvalScope,
|
|
10150
|
+
row.principals === null ? null : JSON.stringify(row.principals),
|
|
10151
|
+
row.expiresAt,
|
|
10152
|
+
row.createdAt
|
|
9536
10153
|
]
|
|
9537
10154
|
);
|
|
9538
10155
|
return {
|
|
9539
10156
|
id,
|
|
9540
10157
|
toolName: opts.toolName,
|
|
9541
|
-
toolArgsFrozen: frozen,
|
|
10158
|
+
toolArgsFrozen: row.frozen,
|
|
9542
10159
|
sourceId: opts.sourceId ?? null,
|
|
9543
|
-
|
|
10160
|
+
approvalScope: row.approvalScope,
|
|
10161
|
+
principals: row.principals,
|
|
9544
10162
|
status: "pending",
|
|
9545
10163
|
approver: null,
|
|
9546
10164
|
approverMeta: null,
|
|
9547
|
-
expiresAt,
|
|
10165
|
+
expiresAt: row.expiresAt,
|
|
9548
10166
|
resolvedAt: null,
|
|
9549
|
-
createdAt
|
|
10167
|
+
createdAt: row.createdAt
|
|
9550
10168
|
};
|
|
9551
10169
|
}
|
|
10170
|
+
async function createPending(engine, opts) {
|
|
10171
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
10172
|
+
return insertPending(engine, opts, pendingRow(opts, approvalScope));
|
|
10173
|
+
}
|
|
10174
|
+
async function findOrCreatePending(engine, opts) {
|
|
10175
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
10176
|
+
if (approvalScope === null)
|
|
10177
|
+
throw new Error("findOrCreatePending requires an approvalScope; use createPending");
|
|
10178
|
+
const row = pendingRow(opts, approvalScope);
|
|
10179
|
+
const frozenJson = JSON.stringify(row.frozen);
|
|
10180
|
+
const sameCall = `tool_name = $1 AND approval_scope = $2 AND tool_args_frozen = $3::jsonb`;
|
|
10181
|
+
const params = [opts.toolName, approvalScope, frozenJson, row.createdAt];
|
|
10182
|
+
await engine.pool.query(
|
|
10183
|
+
`UPDATE context_engine_tool_approvals SET status = 'expired'
|
|
10184
|
+
WHERE ${sameCall} AND status = 'pending' AND expires_at <= $4`,
|
|
10185
|
+
params
|
|
10186
|
+
);
|
|
10187
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
10188
|
+
const found = await engine.pool.query(
|
|
10189
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
10190
|
+
WHERE ${sameCall} AND status = 'pending' AND (expires_at IS NULL OR expires_at > $4)
|
|
10191
|
+
ORDER BY created_at ASC LIMIT 1`,
|
|
10192
|
+
params
|
|
10193
|
+
);
|
|
10194
|
+
if (found.rows[0]) return rowToRecord(found.rows[0]);
|
|
10195
|
+
try {
|
|
10196
|
+
return await insertPending(engine, opts, row);
|
|
10197
|
+
} catch (exc) {
|
|
10198
|
+
if (exc?.code !== "23505") throw exc;
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
throw new Error("findOrCreatePending: lost the insert race twice and found no live pending row");
|
|
10202
|
+
}
|
|
9552
10203
|
async function resolveApproval(engine, approvalId, decision, approver, meta = null, opts = {}) {
|
|
9553
10204
|
if (decision !== "approved" && decision !== "rejected") {
|
|
9554
10205
|
throw new Error(`invalid decision: ${JSON.stringify(decision)} (expected 'approved' or 'rejected')`);
|
|
@@ -9696,6 +10347,10 @@ var ToolConfig = class _ToolConfig {
|
|
|
9696
10347
|
requiresApproval;
|
|
9697
10348
|
approvalPolicy;
|
|
9698
10349
|
enabled;
|
|
10350
|
+
/** Engine-opaque user metadata (documents have the same column). Stored
|
|
10351
|
+
* and returned in CLEAR on the admin surface only — secrets go in
|
|
10352
|
+
* `config`, which is encrypted. */
|
|
10353
|
+
metaData;
|
|
9699
10354
|
constructor(init) {
|
|
9700
10355
|
this.id = init.id ?? null;
|
|
9701
10356
|
this.name = init.name;
|
|
@@ -9707,6 +10362,7 @@ var ToolConfig = class _ToolConfig {
|
|
|
9707
10362
|
this.requiresApproval = init.requiresApproval ?? init.requires_approval ?? false;
|
|
9708
10363
|
this.approvalPolicy = init.approvalPolicy ?? init.approval_policy ?? {};
|
|
9709
10364
|
this.enabled = init.enabled ?? true;
|
|
10365
|
+
this.metaData = init.metaData ?? init.meta_data ?? {};
|
|
9710
10366
|
}
|
|
9711
10367
|
static fromUnknown(body) {
|
|
9712
10368
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -9729,7 +10385,8 @@ var ToolConfig = class _ToolConfig {
|
|
|
9729
10385
|
acl: b.acl ?? null,
|
|
9730
10386
|
requiresApproval: Boolean(b.requiresApproval ?? b.requires_approval ?? false),
|
|
9731
10387
|
approvalPolicy: b.approvalPolicy ?? b.approval_policy ?? {},
|
|
9732
|
-
enabled: b.enabled === void 0 ? true : Boolean(b.enabled)
|
|
10388
|
+
enabled: b.enabled === void 0 ? true : Boolean(b.enabled),
|
|
10389
|
+
metaData: b.metaData ?? b.meta_data ?? {}
|
|
9733
10390
|
});
|
|
9734
10391
|
}
|
|
9735
10392
|
};
|
|
@@ -9748,9 +10405,26 @@ var SCHEMAS = {
|
|
|
9748
10405
|
additionalProperties: { type: "string" },
|
|
9749
10406
|
writeOnly: true
|
|
9750
10407
|
},
|
|
10408
|
+
// The request body TEMPLATE (the http executor reads it). Whether it
|
|
10409
|
+
// round-trips is the USER's call — see `body_secret` and the
|
|
10410
|
+
// conditional in `redactConfig`; listing it documents the shape, the
|
|
10411
|
+
// redactor decides.
|
|
10412
|
+
body: {
|
|
10413
|
+
description: "Request body template; top-level keys merge with LLM parameters"
|
|
10414
|
+
},
|
|
10415
|
+
// Salman (2026-08-18): "give checkbox to user" — the author declares
|
|
10416
|
+
// whether their body carries secrets. True (or ABSENT — every pre-flag
|
|
10417
|
+
// row, fail closed) masks body values like headers; an explicit false
|
|
10418
|
+
// round-trips the body in clear like `url`. The flag round-trips.
|
|
10419
|
+
body_secret: {
|
|
10420
|
+
type: "boolean",
|
|
10421
|
+
description: "Body values are write-only (mask like headers)"
|
|
10422
|
+
},
|
|
9751
10423
|
parameters: {
|
|
9752
10424
|
type: "object",
|
|
9753
|
-
description: "Static/user-fixed parameters sent on every call"
|
|
10425
|
+
description: "Static/user-fixed parameters sent on every call",
|
|
10426
|
+
// The canonical home of a static api_key — secret by position.
|
|
10427
|
+
writeOnly: true
|
|
9754
10428
|
},
|
|
9755
10429
|
llmParameters: {
|
|
9756
10430
|
type: "object",
|
|
@@ -9781,7 +10455,13 @@ var SCHEMAS = {
|
|
|
9781
10455
|
default: "readonly"
|
|
9782
10456
|
},
|
|
9783
10457
|
max_rows: { type: "integer", default: 1e3 },
|
|
9784
|
-
selected_tables: { type: "array", items: { type: "string" } }
|
|
10458
|
+
selected_tables: { type: "array", items: { type: "string" } },
|
|
10459
|
+
// Dialect connection identifiers the db executor reads — same
|
|
10460
|
+
// sensitivity class as `database`, listed so they round-trip instead
|
|
10461
|
+
// of dying to redactConfig's default-deny.
|
|
10462
|
+
service_name: { type: "string" },
|
|
10463
|
+
schema_name: { type: "string" },
|
|
10464
|
+
warehouse: { type: "string" }
|
|
9785
10465
|
},
|
|
9786
10466
|
required: ["engine", "database"]
|
|
9787
10467
|
},
|
|
@@ -9813,11 +10493,245 @@ function configSchema(kind) {
|
|
|
9813
10493
|
if (!schema) throw new Error(`unknown tool kind: ${JSON.stringify(kind)}`);
|
|
9814
10494
|
return schema;
|
|
9815
10495
|
}
|
|
10496
|
+
var REDACTED_SENTINEL = "__redacted__";
|
|
10497
|
+
var ConfigTemplateError = class extends Error {
|
|
10498
|
+
};
|
|
10499
|
+
function isPlainObject(value) {
|
|
10500
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10501
|
+
}
|
|
10502
|
+
function containsSentinel(value) {
|
|
10503
|
+
if (typeof value === "string") return value === REDACTED_SENTINEL;
|
|
10504
|
+
if (Array.isArray(value)) return value.some(containsSentinel);
|
|
10505
|
+
if (isPlainObject(value)) return Object.values(value).some(containsSentinel);
|
|
10506
|
+
return false;
|
|
10507
|
+
}
|
|
10508
|
+
function mergeRedacted(incoming, stored) {
|
|
10509
|
+
const resolve = (value, kept, keptPresent, path) => {
|
|
10510
|
+
if (isPlainObject(value)) {
|
|
10511
|
+
const keptDict = isPlainObject(kept) ? kept : {};
|
|
10512
|
+
const out2 = {};
|
|
10513
|
+
for (const [k, v] of Object.entries(value)) {
|
|
10514
|
+
out2[k] = resolve(v, keptDict[k], k in keptDict, path ? `${path}.${k}` : k);
|
|
10515
|
+
}
|
|
10516
|
+
return out2;
|
|
10517
|
+
}
|
|
10518
|
+
if (Array.isArray(value)) {
|
|
10519
|
+
const keptList = Array.isArray(kept) ? kept : [];
|
|
10520
|
+
return value.map((v, i) => resolve(v, keptList[i], i < keptList.length, `${path}[${i}]`));
|
|
10521
|
+
}
|
|
10522
|
+
if (value === REDACTED_SENTINEL) {
|
|
10523
|
+
if (!keptPresent) {
|
|
10524
|
+
throw new ConfigTemplateError(
|
|
10525
|
+
`config.${path} is ${JSON.stringify(REDACTED_SENTINEL)} but there is no stored value to keep \u2014 re-enter the secret or omit the field`
|
|
10526
|
+
);
|
|
10527
|
+
}
|
|
10528
|
+
return kept;
|
|
10529
|
+
}
|
|
10530
|
+
return value;
|
|
10531
|
+
};
|
|
10532
|
+
const keptRoot = stored ?? {};
|
|
10533
|
+
const out = {};
|
|
10534
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
10535
|
+
out[k] = resolve(v, keptRoot[k], k in keptRoot, k);
|
|
10536
|
+
}
|
|
10537
|
+
return out;
|
|
10538
|
+
}
|
|
10539
|
+
|
|
10540
|
+
// src/tools/crypto.ts
|
|
10541
|
+
init_crypto();
|
|
10542
|
+
var MAX_RESPONSE_BYTES = 1e6;
|
|
10543
|
+
var MAX_REDIRECTS = 5;
|
|
10544
|
+
function isRedirect(resp) {
|
|
10545
|
+
return resp.status >= 300 && resp.status < 400 || resp.type === "opaqueredirect";
|
|
10546
|
+
}
|
|
10547
|
+
var EgressDenied = class extends Error {
|
|
10548
|
+
constructor(message) {
|
|
10549
|
+
super(message);
|
|
10550
|
+
this.name = "EgressDenied";
|
|
10551
|
+
}
|
|
10552
|
+
};
|
|
10553
|
+
var resolver = {
|
|
10554
|
+
async lookup(host) {
|
|
10555
|
+
const answers = await promises.lookup(host, { all: true, verbatim: true });
|
|
10556
|
+
return answers.map((a) => a.address);
|
|
10557
|
+
}
|
|
10558
|
+
};
|
|
10559
|
+
var V4_PRIVATE = [
|
|
10560
|
+
["0.0.0.0", 8],
|
|
10561
|
+
// "this network" / unspecified
|
|
10562
|
+
["10.0.0.0", 8],
|
|
10563
|
+
// RFC 1918
|
|
10564
|
+
["100.64.0.0", 10],
|
|
10565
|
+
// RFC 6598 carrier-grade NAT — overlay VPNs, pod CIDRs
|
|
10566
|
+
["127.0.0.0", 8],
|
|
10567
|
+
// loopback
|
|
10568
|
+
["169.254.0.0", 16],
|
|
10569
|
+
// link-local, incl. the cloud metadata address
|
|
10570
|
+
["172.16.0.0", 12],
|
|
10571
|
+
// RFC 1918
|
|
10572
|
+
["192.168.0.0", 16],
|
|
10573
|
+
// RFC 1918
|
|
10574
|
+
["224.0.0.0", 4],
|
|
10575
|
+
// multicast
|
|
10576
|
+
["240.0.0.0", 4],
|
|
10577
|
+
// reserved
|
|
10578
|
+
["255.255.255.255", 32]
|
|
10579
|
+
// broadcast (inside 240/4; spelled out anyway)
|
|
10580
|
+
];
|
|
10581
|
+
var V6_PRIVATE = [
|
|
10582
|
+
["::", 128],
|
|
10583
|
+
// unspecified
|
|
10584
|
+
["::1", 128],
|
|
10585
|
+
// loopback
|
|
10586
|
+
["::ffff:0:0", 96],
|
|
10587
|
+
// IPv4-mapped — the wrapped v4 is checked too
|
|
10588
|
+
["64:ff9b::", 96],
|
|
10589
|
+
// NAT64 — the wrapped v4 is checked too
|
|
10590
|
+
["2002::", 16],
|
|
10591
|
+
// 6to4 — the wrapped v4 is checked too, and this is denied
|
|
10592
|
+
["fc00::", 7],
|
|
10593
|
+
// unique local
|
|
10594
|
+
["fe80::", 10],
|
|
10595
|
+
// link-local
|
|
10596
|
+
["fec0::", 10],
|
|
10597
|
+
// site-local (deprecated, still configured)
|
|
10598
|
+
["ff00::", 8],
|
|
10599
|
+
// multicast
|
|
10600
|
+
["3fff::", 20]
|
|
10601
|
+
// documentation
|
|
10602
|
+
];
|
|
10603
|
+
function parseV4(text) {
|
|
10604
|
+
const parts = text.split(".");
|
|
10605
|
+
if (parts.length !== 4) return null;
|
|
10606
|
+
const out = new Uint8Array(4);
|
|
10607
|
+
for (let i = 0; i < 4; i++) {
|
|
10608
|
+
const part = parts[i];
|
|
10609
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
10610
|
+
const value = Number(part);
|
|
10611
|
+
if (value > 255) return null;
|
|
10612
|
+
out[i] = value;
|
|
10613
|
+
}
|
|
10614
|
+
return out;
|
|
10615
|
+
}
|
|
10616
|
+
function parseV6(text) {
|
|
10617
|
+
let body = text.split("%")[0] ?? "";
|
|
10618
|
+
const lastColon = body.lastIndexOf(":");
|
|
10619
|
+
if (lastColon < 0) return null;
|
|
10620
|
+
const tail = body.slice(lastColon + 1);
|
|
10621
|
+
if (tail.includes(".")) {
|
|
10622
|
+
const v4 = parseV4(tail);
|
|
10623
|
+
if (!v4) return null;
|
|
10624
|
+
const hi = (v4[0] << 8 | v4[1]).toString(16);
|
|
10625
|
+
const lo = (v4[2] << 8 | v4[3]).toString(16);
|
|
10626
|
+
body = `${body.slice(0, lastColon + 1)}${hi}:${lo}`;
|
|
10627
|
+
}
|
|
10628
|
+
const halves = body.split("::");
|
|
10629
|
+
if (halves.length > 2) return null;
|
|
10630
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
10631
|
+
const rest = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
10632
|
+
let groups;
|
|
10633
|
+
if (halves.length === 1) {
|
|
10634
|
+
if (head.length !== 8) return null;
|
|
10635
|
+
groups = head;
|
|
10636
|
+
} else {
|
|
10637
|
+
const missing = 8 - head.length - rest.length;
|
|
10638
|
+
if (missing < 0) return null;
|
|
10639
|
+
groups = [...head, ...Array(missing).fill("0"), ...rest];
|
|
10640
|
+
}
|
|
10641
|
+
const out = new Uint8Array(16);
|
|
10642
|
+
for (let i = 0; i < 8; i++) {
|
|
10643
|
+
const group = groups[i];
|
|
10644
|
+
if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
|
|
10645
|
+
const value = Number.parseInt(group, 16);
|
|
10646
|
+
out[2 * i] = value >> 8;
|
|
10647
|
+
out[2 * i + 1] = value & 255;
|
|
10648
|
+
}
|
|
10649
|
+
return out;
|
|
10650
|
+
}
|
|
10651
|
+
function parseAddress(text) {
|
|
10652
|
+
const family = isIP(text);
|
|
10653
|
+
if (family === 4) return parseV4(text);
|
|
10654
|
+
if (family === 6) return parseV6(text);
|
|
10655
|
+
return null;
|
|
10656
|
+
}
|
|
10657
|
+
function inNet(addr, net, prefix) {
|
|
10658
|
+
if (addr.length !== net.length) return false;
|
|
10659
|
+
const whole = prefix >> 3;
|
|
10660
|
+
for (let i = 0; i < whole; i++) if (addr[i] !== net[i]) return false;
|
|
10661
|
+
const bits = prefix & 7;
|
|
10662
|
+
if (bits === 0) return true;
|
|
10663
|
+
const mask = 255 << 8 - bits;
|
|
10664
|
+
return (addr[whole] & mask) === (net[whole] & mask);
|
|
10665
|
+
}
|
|
10666
|
+
function compile(table) {
|
|
10667
|
+
return table.map(([cidr, prefix]) => {
|
|
10668
|
+
const bytes = parseAddress(cidr);
|
|
10669
|
+
if (!bytes) throw new Error(`egress: unparseable network ${cidr}`);
|
|
10670
|
+
return [bytes, prefix];
|
|
10671
|
+
});
|
|
10672
|
+
}
|
|
10673
|
+
var V4_NETS = compile(V4_PRIVATE);
|
|
10674
|
+
var V6_NETS = compile(V6_PRIVATE);
|
|
10675
|
+
var MAPPED_V4 = compile([["::ffff:0:0", 96]])[0];
|
|
10676
|
+
var NAT64 = compile([["64:ff9b::", 96]])[0];
|
|
10677
|
+
var SIXTOFOUR = compile([["2002::", 16]])[0];
|
|
10678
|
+
function embeddedV4(bytes) {
|
|
10679
|
+
if (inNet(bytes, MAPPED_V4[0], MAPPED_V4[1]) || inNet(bytes, NAT64[0], NAT64[1])) {
|
|
10680
|
+
return bytes.subarray(12, 16);
|
|
10681
|
+
}
|
|
10682
|
+
if (inNet(bytes, SIXTOFOUR[0], SIXTOFOUR[1])) return bytes.subarray(2, 6);
|
|
10683
|
+
return null;
|
|
10684
|
+
}
|
|
10685
|
+
function addressIsPrivate(address) {
|
|
10686
|
+
const bytes = parseAddress(address);
|
|
10687
|
+
if (!bytes) return true;
|
|
10688
|
+
if (bytes.length === 4) return V4_NETS.some(([net, prefix]) => inNet(bytes, net, prefix));
|
|
10689
|
+
const embedded = embeddedV4(bytes);
|
|
10690
|
+
if (embedded && V4_NETS.some(([net, prefix]) => inNet(embedded, net, prefix))) return true;
|
|
10691
|
+
return V6_NETS.some(([net, prefix]) => inNet(bytes, net, prefix));
|
|
10692
|
+
}
|
|
10693
|
+
async function isPrivateAddress(host) {
|
|
10694
|
+
let name = (host ?? "").trim().toLowerCase().replace(/^\.+|\.+$/g, "");
|
|
10695
|
+
if (!name) return true;
|
|
10696
|
+
if (name === "localhost") return true;
|
|
10697
|
+
if (name.startsWith("[") && name.endsWith("]")) name = name.slice(1, -1);
|
|
10698
|
+
if (isIP(name)) return addressIsPrivate(name);
|
|
10699
|
+
let addresses;
|
|
10700
|
+
try {
|
|
10701
|
+
addresses = await resolver.lookup(name);
|
|
10702
|
+
} catch {
|
|
10703
|
+
return true;
|
|
10704
|
+
}
|
|
10705
|
+
if (!addresses.length) return true;
|
|
10706
|
+
return addresses.some((address) => addressIsPrivate(address.split("%")[0] ?? address));
|
|
10707
|
+
}
|
|
10708
|
+
async function assertEgressAllowed(url, opts) {
|
|
10709
|
+
let parsed;
|
|
10710
|
+
try {
|
|
10711
|
+
parsed = new URL(url ?? "");
|
|
10712
|
+
} catch {
|
|
10713
|
+
throw new EgressDenied(`egress denied: ${JSON.stringify(url)} is not a valid URL`);
|
|
10714
|
+
}
|
|
10715
|
+
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
10716
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
10717
|
+
throw new EgressDenied(
|
|
10718
|
+
`egress denied: unsupported URL scheme ${JSON.stringify(scheme)} \u2014 http tools may only reach http/https`
|
|
10719
|
+
);
|
|
10720
|
+
}
|
|
10721
|
+
if (opts.allowPrivate) return;
|
|
10722
|
+
const host = parsed.hostname;
|
|
10723
|
+
if (await isPrivateAddress(host)) {
|
|
10724
|
+
throw new EgressDenied(
|
|
10725
|
+
`egress denied: ${host || "(no host)"} is a private, loopback, link-local or metadata address; set allowPrivateEgress to permit it`
|
|
10726
|
+
);
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
9816
10729
|
|
|
9817
10730
|
// src/tools/governance.ts
|
|
9818
10731
|
init_db();
|
|
9819
10732
|
|
|
9820
10733
|
// src/tools/executors/http.ts
|
|
10734
|
+
init_errors();
|
|
9821
10735
|
var TIMEOUT_MS5 = 3e4;
|
|
9822
10736
|
var SENSITIVE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
9823
10737
|
"set-cookie",
|
|
@@ -9826,6 +10740,8 @@ var SENSITIVE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
|
9826
10740
|
"proxy-authenticate",
|
|
9827
10741
|
"www-authenticate"
|
|
9828
10742
|
]);
|
|
10743
|
+
var REDIRECT_STATUS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
10744
|
+
var CROSS_ORIGIN_HEADERS = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie"]);
|
|
9829
10745
|
function percentEncode(value) {
|
|
9830
10746
|
return encodeURIComponent(value).replace(
|
|
9831
10747
|
/[!'()*]/g,
|
|
@@ -9856,10 +10772,38 @@ function withQuery(url, params) {
|
|
|
9856
10772
|
if (!q) return url;
|
|
9857
10773
|
return url.includes("?") ? `${url}&${q}` : `${url}?${q}`;
|
|
9858
10774
|
}
|
|
10775
|
+
async function readCapped(resp) {
|
|
10776
|
+
if (!resp.body) return { text: await resp.text(), truncated: false };
|
|
10777
|
+
const reader = resp.body.getReader();
|
|
10778
|
+
const chunks = [];
|
|
10779
|
+
let size = 0;
|
|
10780
|
+
let truncated = false;
|
|
10781
|
+
for (; ; ) {
|
|
10782
|
+
const { done, value } = await reader.read();
|
|
10783
|
+
if (done) break;
|
|
10784
|
+
if (value) {
|
|
10785
|
+
chunks.push(Buffer.from(value));
|
|
10786
|
+
size += value.byteLength;
|
|
10787
|
+
}
|
|
10788
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
10789
|
+
truncated = true;
|
|
10790
|
+
await reader.cancel();
|
|
10791
|
+
break;
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
const bytes = Buffer.concat(chunks);
|
|
10795
|
+
return {
|
|
10796
|
+
text: (truncated ? bytes.subarray(0, MAX_RESPONSE_BYTES) : bytes).toString("utf8"),
|
|
10797
|
+
truncated
|
|
10798
|
+
};
|
|
10799
|
+
}
|
|
9859
10800
|
async function defaultRequest(init) {
|
|
9860
10801
|
let url = init.url;
|
|
9861
10802
|
if (init.params) url = withQuery(url, init.params);
|
|
9862
10803
|
const headers = { ...init.headers ?? {} };
|
|
10804
|
+
if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
|
|
10805
|
+
headers["Accept-Encoding"] = "identity";
|
|
10806
|
+
}
|
|
9863
10807
|
let body;
|
|
9864
10808
|
if (init.json !== void 0) {
|
|
9865
10809
|
headers["Content-Type"] = headers["Content-Type"] ?? headers["content-type"] ?? "application/json";
|
|
@@ -9871,25 +10815,47 @@ async function defaultRequest(init) {
|
|
|
9871
10815
|
method: init.method,
|
|
9872
10816
|
headers,
|
|
9873
10817
|
body,
|
|
9874
|
-
|
|
10818
|
+
// Redirects are followed by hand in `executeHttp` — an automatic follow
|
|
10819
|
+
// would jump to a destination no egress check ever saw.
|
|
10820
|
+
redirect: "manual",
|
|
9875
10821
|
signal: AbortSignal.timeout(TIMEOUT_MS5)
|
|
9876
10822
|
});
|
|
9877
10823
|
const respHeaders = {};
|
|
9878
10824
|
resp.headers.forEach((v, k) => {
|
|
9879
10825
|
respHeaders[k] = v;
|
|
9880
10826
|
});
|
|
9881
|
-
const text = await resp
|
|
10827
|
+
const { text, truncated } = await readCapped(resp);
|
|
9882
10828
|
return {
|
|
9883
10829
|
status: resp.status,
|
|
9884
10830
|
headers: respHeaders,
|
|
10831
|
+
truncated,
|
|
9885
10832
|
async json() {
|
|
9886
|
-
return JSON.parse(text);
|
|
10833
|
+
return JSON.parse(text.replace(/^/, ""));
|
|
9887
10834
|
},
|
|
9888
10835
|
async text() {
|
|
9889
10836
|
return text;
|
|
9890
10837
|
}
|
|
9891
10838
|
};
|
|
9892
10839
|
}
|
|
10840
|
+
function nextHop(init, status, location) {
|
|
10841
|
+
const current = new URL(init.url);
|
|
10842
|
+
const target = new URL(location, current);
|
|
10843
|
+
let method = init.method;
|
|
10844
|
+
if ((status === 302 || status === 303) && method !== "HEAD") method = "GET";
|
|
10845
|
+
else if (status === 301 && method === "POST") method = "GET";
|
|
10846
|
+
let headers = { ...init.headers ?? {} };
|
|
10847
|
+
if (current.origin !== target.origin) {
|
|
10848
|
+
headers = Object.fromEntries(
|
|
10849
|
+
Object.entries(headers).filter(([k]) => !CROSS_ORIGIN_HEADERS.has(k.toLowerCase()))
|
|
10850
|
+
);
|
|
10851
|
+
}
|
|
10852
|
+
const hop = { method, url: target.toString(), headers };
|
|
10853
|
+
if (method === init.method) {
|
|
10854
|
+
if (init.json !== void 0) hop.json = init.json;
|
|
10855
|
+
if (init.content !== void 0) hop.content = init.content;
|
|
10856
|
+
}
|
|
10857
|
+
return hop;
|
|
10858
|
+
}
|
|
9893
10859
|
async function executeHttp(config, args, opts = {}) {
|
|
9894
10860
|
const method = String(config.method ?? "GET").toUpperCase();
|
|
9895
10861
|
let url = config.url;
|
|
@@ -9910,7 +10876,7 @@ async function executeHttp(config, args, opts = {}) {
|
|
|
9910
10876
|
}
|
|
9911
10877
|
}
|
|
9912
10878
|
}
|
|
9913
|
-
|
|
10879
|
+
let requestInit = {
|
|
9914
10880
|
method,
|
|
9915
10881
|
url: url ?? "",
|
|
9916
10882
|
headers
|
|
@@ -9946,29 +10912,64 @@ async function executeHttp(config, args, opts = {}) {
|
|
|
9946
10912
|
}
|
|
9947
10913
|
}
|
|
9948
10914
|
const client = opts.client;
|
|
9949
|
-
const
|
|
10915
|
+
const allowPrivate = opts.allowPrivate ?? false;
|
|
10916
|
+
let resp;
|
|
10917
|
+
let hops = 0;
|
|
10918
|
+
for (; ; ) {
|
|
10919
|
+
await assertEgressAllowed(requestInit.url, { allowPrivate });
|
|
10920
|
+
resp = client ? await client.request(requestInit) : await defaultRequest(requestInit);
|
|
10921
|
+
const location = resp.headers.location ?? resp.headers.Location;
|
|
10922
|
+
if (!REDIRECT_STATUS.has(resp.status) || !location) break;
|
|
10923
|
+
if (hops >= MAX_REDIRECTS) {
|
|
10924
|
+
throw new EngineActionError(`too many redirects (more than ${MAX_REDIRECTS}) starting at ${url}`);
|
|
10925
|
+
}
|
|
10926
|
+
hops += 1;
|
|
10927
|
+
requestInit = nextHop(requestInit, resp.status, location);
|
|
10928
|
+
}
|
|
9950
10929
|
const contentType = resp.headers["content-type"] ?? resp.headers["Content-Type"] ?? "";
|
|
9951
|
-
let
|
|
10930
|
+
let text = "";
|
|
9952
10931
|
try {
|
|
9953
|
-
|
|
10932
|
+
text = await resp.text();
|
|
9954
10933
|
} catch {
|
|
9955
|
-
|
|
10934
|
+
text = "";
|
|
10935
|
+
}
|
|
10936
|
+
const bytes = Buffer.from(text, "utf8");
|
|
10937
|
+
const truncated = resp.truncated === true || bytes.byteLength > MAX_RESPONSE_BYTES;
|
|
10938
|
+
let data;
|
|
10939
|
+
if (truncated) {
|
|
10940
|
+
data = bytes.subarray(0, MAX_RESPONSE_BYTES).toString("utf8");
|
|
10941
|
+
} else {
|
|
10942
|
+
try {
|
|
10943
|
+
data = contentType.includes("application/json") ? await resp.json() : text;
|
|
10944
|
+
} catch {
|
|
10945
|
+
data = text;
|
|
10946
|
+
}
|
|
9956
10947
|
}
|
|
9957
10948
|
const safeHeaders = {};
|
|
9958
10949
|
for (const [k, v] of Object.entries(resp.headers)) {
|
|
9959
10950
|
if (!SENSITIVE_RESPONSE_HEADERS.has(k.toLowerCase())) safeHeaders[k] = v;
|
|
9960
10951
|
}
|
|
9961
|
-
|
|
10952
|
+
const result = {
|
|
9962
10953
|
status_code: resp.status,
|
|
9963
10954
|
headers: safeHeaders,
|
|
9964
10955
|
data
|
|
9965
10956
|
};
|
|
10957
|
+
if (truncated) result.truncated = true;
|
|
10958
|
+
return result;
|
|
9966
10959
|
}
|
|
9967
10960
|
|
|
9968
10961
|
// src/version.ts
|
|
9969
10962
|
var __version__ = "0.0.0";
|
|
9970
10963
|
|
|
9971
10964
|
// src/tools/executors/mcp-client.ts
|
|
10965
|
+
async function checkEgress(url, allowPrivate) {
|
|
10966
|
+
let checked = url;
|
|
10967
|
+
const scheme = (/^([a-z0-9+.-]+):/i.exec(url ?? "")?.[1] ?? "").toLowerCase();
|
|
10968
|
+
if (scheme === "ws") checked = `http:${url.slice(scheme.length + 1)}`;
|
|
10969
|
+
else if (scheme === "wss") checked = `https:${url.slice(scheme.length + 1)}`;
|
|
10970
|
+
await assertEgressAllowed(checked, { allowPrivate });
|
|
10971
|
+
}
|
|
10972
|
+
var REDIRECT_MESSAGE = "MCP server redirected; register the final URL";
|
|
9972
10973
|
var MCPError = class _MCPError extends Error {
|
|
9973
10974
|
code;
|
|
9974
10975
|
data;
|
|
@@ -9997,10 +10998,12 @@ function parseSseBuffer(raw) {
|
|
|
9997
10998
|
var HttpTransport = class {
|
|
9998
10999
|
kind = "http";
|
|
9999
11000
|
url;
|
|
11001
|
+
allowPrivate;
|
|
10000
11002
|
baseHeaders;
|
|
10001
11003
|
sessionId = null;
|
|
10002
|
-
constructor(url, headers) {
|
|
11004
|
+
constructor(url, headers, allowPrivate = false) {
|
|
10003
11005
|
this.url = url;
|
|
11006
|
+
this.allowPrivate = allowPrivate;
|
|
10004
11007
|
this.baseHeaders = {
|
|
10005
11008
|
...headers,
|
|
10006
11009
|
"Content-Type": "application/json",
|
|
@@ -10008,6 +11011,7 @@ var HttpTransport = class {
|
|
|
10008
11011
|
};
|
|
10009
11012
|
}
|
|
10010
11013
|
async connect() {
|
|
11014
|
+
await checkEgress(this.url, this.allowPrivate);
|
|
10011
11015
|
}
|
|
10012
11016
|
async close() {
|
|
10013
11017
|
this.sessionId = null;
|
|
@@ -10021,10 +11025,17 @@ var HttpTransport = class {
|
|
|
10021
11025
|
const resp = await fetch(this.url, {
|
|
10022
11026
|
method: "POST",
|
|
10023
11027
|
headers: this.buildHeaders(),
|
|
10024
|
-
body: JSON.stringify(msg)
|
|
11028
|
+
body: JSON.stringify(msg),
|
|
11029
|
+
// `redirect: "manual"`: a followed redirect would carry this POST —
|
|
11030
|
+
// headers, Authorization and all — to a destination `connect()`'s
|
|
11031
|
+
// check never saw. A 3xx is reported, never chased.
|
|
11032
|
+
redirect: "manual"
|
|
10025
11033
|
});
|
|
10026
11034
|
const sid = resp.headers.get("Mcp-Session-Id");
|
|
10027
11035
|
if (sid) this.sessionId = sid;
|
|
11036
|
+
if (isRedirect(resp)) {
|
|
11037
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
11038
|
+
}
|
|
10028
11039
|
if (resp.status >= 400) {
|
|
10029
11040
|
const body = await resp.text();
|
|
10030
11041
|
console.error(`[HTTP Transport] ${resp.status} from ${this.url}: ${body.slice(0, 300)}`);
|
|
@@ -10057,11 +11068,20 @@ var SseTransport = class {
|
|
|
10057
11068
|
eventsUrl;
|
|
10058
11069
|
baseUrl;
|
|
10059
11070
|
headers;
|
|
11071
|
+
allowPrivate;
|
|
11072
|
+
/** Where the server told us to POST — it arrives in an `endpoint` event on
|
|
11073
|
+
* the stream, so it is server-chosen and hence checked. Private: the only
|
|
11074
|
+
* way in is the parser, which is the path a real server takes. */
|
|
10060
11075
|
postUrl = null;
|
|
11076
|
+
/** The last POST endpoint cleared by the egress check. The check costs a
|
|
11077
|
+
* DNS resolution and the endpoint changes at most once per connection, so
|
|
11078
|
+
* it is not repeated per message. */
|
|
11079
|
+
checkedPostUrl = null;
|
|
10061
11080
|
queue = [];
|
|
10062
11081
|
waiters = [];
|
|
10063
11082
|
abort = null;
|
|
10064
|
-
constructor(url, headers) {
|
|
11083
|
+
constructor(url, headers, allowPrivate = false) {
|
|
11084
|
+
this.allowPrivate = allowPrivate;
|
|
10065
11085
|
this.eventsUrl = url.replace(/\/$/, "");
|
|
10066
11086
|
if (url.endsWith("/events")) this.baseUrl = url.slice(0, -7);
|
|
10067
11087
|
else if (url.endsWith("/sse")) this.baseUrl = url.slice(0, -4);
|
|
@@ -10074,14 +11094,19 @@ var SseTransport = class {
|
|
|
10074
11094
|
else this.queue.push(msg);
|
|
10075
11095
|
}
|
|
10076
11096
|
async connect() {
|
|
11097
|
+
await checkEgress(this.eventsUrl, this.allowPrivate);
|
|
10077
11098
|
this.abort = new AbortController();
|
|
10078
11099
|
void this.listen();
|
|
10079
11100
|
}
|
|
10080
11101
|
async listen() {
|
|
10081
11102
|
const resp = await fetch(this.eventsUrl, {
|
|
10082
11103
|
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
10083
|
-
signal: this.abort?.signal
|
|
11104
|
+
signal: this.abort?.signal,
|
|
11105
|
+
redirect: "manual"
|
|
10084
11106
|
});
|
|
11107
|
+
if (isRedirect(resp)) {
|
|
11108
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
11109
|
+
}
|
|
10085
11110
|
if (!resp.ok) throw new Error(`SSE ${resp.status}`);
|
|
10086
11111
|
if (!resp.body) throw new Error("SSE response has no body");
|
|
10087
11112
|
const reader = resp.body.getReader();
|
|
@@ -10133,11 +11158,19 @@ var SseTransport = class {
|
|
|
10133
11158
|
}
|
|
10134
11159
|
}
|
|
10135
11160
|
const postUrl = this.postUrl || this.baseUrl;
|
|
11161
|
+
if (postUrl !== this.checkedPostUrl) {
|
|
11162
|
+
await checkEgress(postUrl, this.allowPrivate);
|
|
11163
|
+
this.checkedPostUrl = postUrl;
|
|
11164
|
+
}
|
|
10136
11165
|
const resp = await fetch(postUrl, {
|
|
10137
11166
|
method: "POST",
|
|
10138
11167
|
headers: { ...this.headers, "Content-Type": "application/json" },
|
|
10139
|
-
body: JSON.stringify(msg)
|
|
11168
|
+
body: JSON.stringify(msg),
|
|
11169
|
+
redirect: "manual"
|
|
10140
11170
|
});
|
|
11171
|
+
if (isRedirect(resp)) {
|
|
11172
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
11173
|
+
}
|
|
10141
11174
|
if (!resp.ok) throw new Error(`SSE POST ${resp.status}`);
|
|
10142
11175
|
try {
|
|
10143
11176
|
const data = await resp.json();
|
|
@@ -10156,14 +11189,17 @@ var WsTransport = class {
|
|
|
10156
11189
|
kind = "websocket";
|
|
10157
11190
|
url;
|
|
10158
11191
|
headers;
|
|
11192
|
+
allowPrivate;
|
|
10159
11193
|
ws = null;
|
|
10160
11194
|
queue = [];
|
|
10161
11195
|
waiters = [];
|
|
10162
|
-
constructor(url, headers) {
|
|
11196
|
+
constructor(url, headers, allowPrivate = false) {
|
|
10163
11197
|
this.url = url;
|
|
11198
|
+
this.allowPrivate = allowPrivate;
|
|
10164
11199
|
this.headers = headers;
|
|
10165
11200
|
}
|
|
10166
11201
|
async connect() {
|
|
11202
|
+
await checkEgress(this.url, this.allowPrivate);
|
|
10167
11203
|
const WS = globalThis.WebSocket;
|
|
10168
11204
|
if (!WS) throw new Error("WebSocket is not available in this runtime");
|
|
10169
11205
|
this.ws = new WS(this.url);
|
|
@@ -10204,21 +11240,27 @@ var MCPClient = class _MCPClient {
|
|
|
10204
11240
|
tools = [];
|
|
10205
11241
|
resources = [];
|
|
10206
11242
|
headers;
|
|
10207
|
-
|
|
11243
|
+
allowPrivate;
|
|
11244
|
+
constructor(url, headers = null, opts = {}) {
|
|
10208
11245
|
this.url = url;
|
|
10209
11246
|
this.headers = headers ?? {};
|
|
10210
|
-
this.
|
|
11247
|
+
this.allowPrivate = opts.allowPrivate ?? false;
|
|
11248
|
+
this.transport = _MCPClient.createTransport(url, this.headers, this.allowPrivate);
|
|
10211
11249
|
}
|
|
10212
|
-
static createTransport(url, headers) {
|
|
11250
|
+
static createTransport(url, headers, allowPrivate = false) {
|
|
10213
11251
|
if (url.startsWith("stdio:") || url === "stdio") {
|
|
10214
11252
|
throw new Error("stdio MCP transport is not supported (cloud deployments use SSE/HTTP/WebSocket)");
|
|
10215
11253
|
}
|
|
10216
|
-
if (url.startsWith("ws://") || url.startsWith("wss://"))
|
|
11254
|
+
if (url.startsWith("ws://") || url.startsWith("wss://")) {
|
|
11255
|
+
return new WsTransport(url, headers, allowPrivate);
|
|
11256
|
+
}
|
|
10217
11257
|
if (url.startsWith("sse+http://") || url.startsWith("sse+https://") || url.endsWith("/events") || url.endsWith("/sse")) {
|
|
10218
11258
|
const clean = url.replace("sse+http://", "http://").replace("sse+https://", "https://");
|
|
10219
|
-
return new SseTransport(clean, headers);
|
|
11259
|
+
return new SseTransport(clean, headers, allowPrivate);
|
|
11260
|
+
}
|
|
11261
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
11262
|
+
return new HttpTransport(url, headers, allowPrivate);
|
|
10220
11263
|
}
|
|
10221
|
-
if (url.startsWith("http://") || url.startsWith("https://")) return new HttpTransport(url, headers);
|
|
10222
11264
|
throw new Error(`Unsupported URL: ${url} (use http(s)://, ws(s)://, or .../sse)`);
|
|
10223
11265
|
}
|
|
10224
11266
|
nextId() {
|
|
@@ -10270,7 +11312,15 @@ var MCPClient = class _MCPClient {
|
|
|
10270
11312
|
await this.transport.send({ jsonrpc: "2.0", method, params: params ?? {} });
|
|
10271
11313
|
}
|
|
10272
11314
|
async connect() {
|
|
10273
|
-
|
|
11315
|
+
try {
|
|
11316
|
+
await this.transport.connect();
|
|
11317
|
+
} catch (e) {
|
|
11318
|
+
try {
|
|
11319
|
+
await this.transport.close();
|
|
11320
|
+
} catch {
|
|
11321
|
+
}
|
|
11322
|
+
throw e;
|
|
11323
|
+
}
|
|
10274
11324
|
if (!(this.transport instanceof HttpTransport)) {
|
|
10275
11325
|
this.connected = true;
|
|
10276
11326
|
void this.recvLoop();
|
|
@@ -10353,7 +11403,7 @@ var MCPClient = class _MCPClient {
|
|
|
10353
11403
|
}
|
|
10354
11404
|
this.id = 0;
|
|
10355
11405
|
this.pending.clear();
|
|
10356
|
-
this.transport = _MCPClient.createTransport(url, headers);
|
|
11406
|
+
this.transport = _MCPClient.createTransport(url, headers, this.allowPrivate);
|
|
10357
11407
|
await this.connect();
|
|
10358
11408
|
await this.listTools();
|
|
10359
11409
|
}
|
|
@@ -10361,11 +11411,11 @@ var MCPClient = class _MCPClient {
|
|
|
10361
11411
|
var MCPRegistry = class {
|
|
10362
11412
|
clients = /* @__PURE__ */ new Map();
|
|
10363
11413
|
healthTask = null;
|
|
10364
|
-
async connect(name, url, headers = null, token = null) {
|
|
11414
|
+
async connect(name, url, headers = null, token = null, allowPrivate = false) {
|
|
10365
11415
|
this.clients.delete(name);
|
|
10366
11416
|
const hdrs = { ...headers ?? {} };
|
|
10367
11417
|
if (token) hdrs.Authorization = hdrs.Authorization ?? `Bearer ${token}`;
|
|
10368
|
-
const client = new MCPClient(url, hdrs);
|
|
11418
|
+
const client = new MCPClient(url, hdrs, { allowPrivate });
|
|
10369
11419
|
await client.connect();
|
|
10370
11420
|
await client.listTools();
|
|
10371
11421
|
this.clients.set(name, client);
|
|
@@ -10417,8 +11467,18 @@ var MCPRegistry = class {
|
|
|
10417
11467
|
};
|
|
10418
11468
|
var PromptevMCP = class {
|
|
10419
11469
|
clients = new MCPRegistry();
|
|
11470
|
+
allowPrivate;
|
|
11471
|
+
/**
|
|
11472
|
+
* `allowPrivate` is the operator knob `allowPrivateEgress`, carried down to
|
|
11473
|
+
* every transport this facade builds. It defaults to `false`, so a
|
|
11474
|
+
* construction site that forgets to pass it denies private destinations
|
|
11475
|
+
* rather than permitting them.
|
|
11476
|
+
*/
|
|
11477
|
+
constructor(opts = {}) {
|
|
11478
|
+
this.allowPrivate = opts.allowPrivate ?? false;
|
|
11479
|
+
}
|
|
10420
11480
|
async addServer(name, url, opts = {}) {
|
|
10421
|
-
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null);
|
|
11481
|
+
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null, this.allowPrivate);
|
|
10422
11482
|
}
|
|
10423
11483
|
async shutdown() {
|
|
10424
11484
|
await this.clients.shutdown();
|
|
@@ -10552,8 +11612,13 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
10552
11612
|
"requires_approval",
|
|
10553
11613
|
"approvalPolicy",
|
|
10554
11614
|
"approval_policy",
|
|
10555
|
-
"enabled"
|
|
11615
|
+
"enabled",
|
|
11616
|
+
"metaData",
|
|
11617
|
+
"meta_data"
|
|
10556
11618
|
]);
|
|
11619
|
+
function allowPrivateEgress(engine) {
|
|
11620
|
+
return Boolean(engine?.config?.allowPrivateEgress);
|
|
11621
|
+
}
|
|
10557
11622
|
function toolVisible(ct, principals) {
|
|
10558
11623
|
return aclVisible(ct.acl, principals);
|
|
10559
11624
|
}
|
|
@@ -10635,11 +11700,19 @@ function redactToolResult(result, policy, opts) {
|
|
|
10635
11700
|
if (failed.length) note.rules_failed = failed;
|
|
10636
11701
|
return [redacted, note];
|
|
10637
11702
|
}
|
|
10638
|
-
function
|
|
10639
|
-
|
|
10640
|
-
if (
|
|
10641
|
-
|
|
11703
|
+
function cryptoKey(engine) {
|
|
11704
|
+
const key = getSecretKey(engine.config);
|
|
11705
|
+
if (key.length !== 32) {
|
|
11706
|
+
throw new Error(`CE_SECRET_KEY is malformed: decoded to ${key.length} bytes, need 32`);
|
|
10642
11707
|
}
|
|
11708
|
+
return key;
|
|
11709
|
+
}
|
|
11710
|
+
function decryptRowConfig(row, engine) {
|
|
11711
|
+
if (!row.config_encrypted) return {};
|
|
11712
|
+
return decryptDict(String(row.config_encrypted), cryptoKey(engine));
|
|
11713
|
+
}
|
|
11714
|
+
function rowToToolConfig(row, engine, config) {
|
|
11715
|
+
config ??= decryptRowConfig(row, engine);
|
|
10643
11716
|
return new ToolConfig({
|
|
10644
11717
|
id: String(row.id),
|
|
10645
11718
|
name: String(row.name),
|
|
@@ -10650,9 +11723,24 @@ function rowToToolConfig(row, engine) {
|
|
|
10650
11723
|
acl: row.acl != null ? [...row.acl] : null,
|
|
10651
11724
|
requiresApproval: Boolean(row.requires_approval),
|
|
10652
11725
|
approvalPolicy: row.approval_policy ?? {},
|
|
10653
|
-
enabled: Boolean(row.enabled)
|
|
11726
|
+
enabled: Boolean(row.enabled),
|
|
11727
|
+
metaData: row.meta_data ?? {}
|
|
10654
11728
|
});
|
|
10655
11729
|
}
|
|
11730
|
+
function rowToToolConfigLenient(row, engine) {
|
|
11731
|
+
if (!row.config_encrypted) return rowToToolConfig(row, engine, {});
|
|
11732
|
+
const key = cryptoKey(engine);
|
|
11733
|
+
let config;
|
|
11734
|
+
try {
|
|
11735
|
+
config = decryptDict(String(row.config_encrypted), key);
|
|
11736
|
+
} catch {
|
|
11737
|
+
console.warn(
|
|
11738
|
+
`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`
|
|
11739
|
+
);
|
|
11740
|
+
return null;
|
|
11741
|
+
}
|
|
11742
|
+
return rowToToolConfig(row, engine, config);
|
|
11743
|
+
}
|
|
10656
11744
|
async function loadPersistedCanonicals(engine, sourceId) {
|
|
10657
11745
|
const params = [];
|
|
10658
11746
|
let sql = `SELECT * FROM context_engine_tools WHERE enabled IS TRUE`;
|
|
@@ -10663,7 +11751,9 @@ async function loadPersistedCanonicals(engine, sourceId) {
|
|
|
10663
11751
|
const result = await engine.pool.query(sql, params);
|
|
10664
11752
|
const canonicals = [];
|
|
10665
11753
|
for (const row of result.rows) {
|
|
10666
|
-
|
|
11754
|
+
const tc = rowToToolConfigLenient(row, engine);
|
|
11755
|
+
if (tc === null) continue;
|
|
11756
|
+
canonicals.push(...canonicalFromConfig(tc));
|
|
10667
11757
|
}
|
|
10668
11758
|
return canonicals;
|
|
10669
11759
|
}
|
|
@@ -10683,13 +11773,18 @@ function canonicalToPublic(ct) {
|
|
|
10683
11773
|
async function registerTool(engine, tc) {
|
|
10684
11774
|
canonicalFromConfig(tc);
|
|
10685
11775
|
const config = tc.config ?? {};
|
|
10686
|
-
|
|
11776
|
+
if (containsSentinel(config)) {
|
|
11777
|
+
throw new ConfigTemplateError(
|
|
11778
|
+
`config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a redacted template cannot be registered as a new tool; re-enter the secret values`
|
|
11779
|
+
);
|
|
11780
|
+
}
|
|
11781
|
+
const configEncrypted = Object.keys(config).length ? encryptDict(config, cryptoKey(engine)) : null;
|
|
10687
11782
|
const id = randomUUID();
|
|
10688
11783
|
await engine.pool.query(
|
|
10689
11784
|
`INSERT INTO context_engine_tools
|
|
10690
11785
|
(id, name, kind, description, source_id, acl, config_encrypted,
|
|
10691
|
-
requires_approval, approval_policy, enabled)
|
|
10692
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
|
|
11786
|
+
requires_approval, approval_policy, enabled, meta_data)
|
|
11787
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11::jsonb)`,
|
|
10693
11788
|
[
|
|
10694
11789
|
id,
|
|
10695
11790
|
tc.name,
|
|
@@ -10700,7 +11795,8 @@ async function registerTool(engine, tc) {
|
|
|
10700
11795
|
configEncrypted,
|
|
10701
11796
|
tc.requiresApproval,
|
|
10702
11797
|
JSON.stringify(tc.approvalPolicy ?? {}),
|
|
10703
|
-
tc.enabled
|
|
11798
|
+
tc.enabled,
|
|
11799
|
+
JSON.stringify(tc.metaData ?? {})
|
|
10704
11800
|
]
|
|
10705
11801
|
);
|
|
10706
11802
|
return id;
|
|
@@ -10717,12 +11813,41 @@ async function updateTool(engine, id, opts) {
|
|
|
10717
11813
|
let i = 1;
|
|
10718
11814
|
for (const [key, value] of Object.entries(fields)) {
|
|
10719
11815
|
if (key === "config") {
|
|
11816
|
+
let config = value;
|
|
11817
|
+
if (config && containsSentinel(config)) {
|
|
11818
|
+
if (Object.hasOwn(fields, "kind") && fields.kind !== row.kind) {
|
|
11819
|
+
throw new ConfigTemplateError(
|
|
11820
|
+
"cannot change kind and keep redacted secrets in one PATCH \u2014 re-enter the config in full"
|
|
11821
|
+
);
|
|
11822
|
+
}
|
|
11823
|
+
if (config.body_secret === false && containsSentinel(config.body)) {
|
|
11824
|
+
throw new ConfigTemplateError(
|
|
11825
|
+
`body_secret cannot be turned off while the body still contains ${JSON.stringify(REDACTED_SENTINEL)} \u2014 re-enter the body to declassify it`
|
|
11826
|
+
);
|
|
11827
|
+
}
|
|
11828
|
+
if (!row.config_encrypted) {
|
|
11829
|
+
throw new ConfigTemplateError(
|
|
11830
|
+
`config contains ${JSON.stringify(REDACTED_SENTINEL)} but this tool has no stored config to keep \u2014 re-enter the secret values`
|
|
11831
|
+
);
|
|
11832
|
+
}
|
|
11833
|
+
const key2 = cryptoKey(engine);
|
|
11834
|
+
let stored;
|
|
11835
|
+
try {
|
|
11836
|
+
stored = decryptDict(String(row.config_encrypted), key2);
|
|
11837
|
+
} catch (exc) {
|
|
11838
|
+
throw new ConfigTemplateError(
|
|
11839
|
+
`the stored config cannot be decrypted (was the secret key rotated?) \u2014 re-enter the config in full, without ${JSON.stringify(REDACTED_SENTINEL)} values`,
|
|
11840
|
+
{ cause: exc }
|
|
11841
|
+
);
|
|
11842
|
+
}
|
|
11843
|
+
config = mergeRedacted(config, stored);
|
|
11844
|
+
}
|
|
10720
11845
|
sets.push(`config_encrypted = $${i++}`);
|
|
10721
|
-
params.push(
|
|
11846
|
+
params.push(config && Object.keys(config).length ? encryptDict(config, cryptoKey(engine)) : null);
|
|
10722
11847
|
} else if (UPDATABLE_COLUMNS.has(key)) {
|
|
10723
|
-
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key;
|
|
11848
|
+
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key === "metaData" ? "meta_data" : key;
|
|
10724
11849
|
sets.push(`${col} = $${i++}`);
|
|
10725
|
-
params.push(col === "approval_policy" ? JSON.stringify(value ?? {}) : value);
|
|
11850
|
+
params.push(col === "approval_policy" || col === "meta_data" ? JSON.stringify(value ?? {}) : value);
|
|
10726
11851
|
}
|
|
10727
11852
|
}
|
|
10728
11853
|
sets.push(`updated_at = now()`);
|
|
@@ -10731,7 +11856,8 @@ async function updateTool(engine, id, opts) {
|
|
|
10731
11856
|
`UPDATE context_engine_tools SET ${sets.join(", ")} WHERE id = $${i} RETURNING *`,
|
|
10732
11857
|
params
|
|
10733
11858
|
);
|
|
10734
|
-
|
|
11859
|
+
const row_ = updated.rows[0];
|
|
11860
|
+
return rowToToolConfigLenient(row_, engine) ?? rowToToolConfig(row_, engine, {});
|
|
10735
11861
|
}
|
|
10736
11862
|
async function deleteTool(engine, id, opts = {}) {
|
|
10737
11863
|
const existing = await engine.pool.query(`SELECT * FROM context_engine_tools WHERE id = $1`, [id]);
|
|
@@ -10817,17 +11943,38 @@ function probeFailure(exc, kind) {
|
|
|
10817
11943
|
console.warn(`test_tool(kind=${kind}) failed:`, exc, "->", category);
|
|
10818
11944
|
return { ok: false, error: category };
|
|
10819
11945
|
}
|
|
10820
|
-
async function testTool(
|
|
11946
|
+
async function testTool(engine, tc) {
|
|
10821
11947
|
const kind = tc.kind;
|
|
10822
11948
|
const config = tc.config ?? {};
|
|
11949
|
+
if (containsSentinel(config)) {
|
|
11950
|
+
throw new ConfigTemplateError(
|
|
11951
|
+
`config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a probe would send it verbatim as the credential; re-enter the secret values`
|
|
11952
|
+
);
|
|
11953
|
+
}
|
|
10823
11954
|
if (kind === "http") {
|
|
10824
11955
|
const url = config.url;
|
|
10825
11956
|
if (!url) return { ok: false, error: "http config missing 'url'" };
|
|
10826
11957
|
try {
|
|
10827
|
-
|
|
11958
|
+
await assertEgressAllowed(url, { allowPrivate: allowPrivateEgress(engine) });
|
|
11959
|
+
} catch (exc) {
|
|
11960
|
+
if (exc instanceof EgressDenied) {
|
|
11961
|
+
console.warn(`testTool(kind=http) refused: ${exc.message}`);
|
|
11962
|
+
return { ok: false, error: "egress_denied" };
|
|
11963
|
+
}
|
|
11964
|
+
throw exc;
|
|
11965
|
+
}
|
|
11966
|
+
const client = engine?._toolHttpClient ?? null;
|
|
11967
|
+
try {
|
|
11968
|
+
const resp = client ? await client.request({
|
|
11969
|
+
method: "HEAD",
|
|
11970
|
+
url,
|
|
11971
|
+
headers: config.headers ?? {}
|
|
11972
|
+
}) : await fetch(url, {
|
|
10828
11973
|
method: "HEAD",
|
|
10829
11974
|
headers: config.headers ?? {},
|
|
10830
|
-
|
|
11975
|
+
// No automatic follow: a redirect would reach a destination the
|
|
11976
|
+
// check above never saw.
|
|
11977
|
+
redirect: "manual",
|
|
10831
11978
|
signal: AbortSignal.timeout(1e4)
|
|
10832
11979
|
});
|
|
10833
11980
|
return { ok: resp.status < 500, status_code: resp.status };
|
|
@@ -10848,12 +11995,16 @@ async function testTool(_engine, tc) {
|
|
|
10848
11995
|
const url = config.url;
|
|
10849
11996
|
if (!url) return { ok: false, error: "mcp config missing 'url'" };
|
|
10850
11997
|
const token = config.oauth_token ?? config.bearer ?? config.access_token;
|
|
10851
|
-
const mcp = new PromptevMCP();
|
|
11998
|
+
const mcp = new PromptevMCP({ allowPrivate: allowPrivateEgress(engine) });
|
|
10852
11999
|
try {
|
|
10853
12000
|
await mcp.addServer(tc.name, url, { token: token ?? null });
|
|
10854
12001
|
const client = mcp.clients.get(tc.name);
|
|
10855
12002
|
return { ok: true, tools: client.tools.map((t) => t.name) };
|
|
10856
12003
|
} catch (exc) {
|
|
12004
|
+
if (exc instanceof EgressDenied) {
|
|
12005
|
+
console.warn(`testTool(kind=mcp) refused: ${exc.message}`);
|
|
12006
|
+
return { ok: false, error: "egress_denied" };
|
|
12007
|
+
}
|
|
10857
12008
|
return probeFailure(exc, "mcp");
|
|
10858
12009
|
} finally {
|
|
10859
12010
|
await mcp.shutdown();
|
|
@@ -10861,11 +12012,20 @@ async function testTool(_engine, tc) {
|
|
|
10861
12012
|
}
|
|
10862
12013
|
return { ok: false, error: `test_tool does not support kind=${JSON.stringify(kind)}` };
|
|
10863
12014
|
}
|
|
10864
|
-
async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
12015
|
+
async function findAndClaimApproved(engine, toolName, args, sourceId, principals, approvalScope) {
|
|
10865
12016
|
const claimedAt = /* @__PURE__ */ new Date();
|
|
12017
|
+
const params = [toolName];
|
|
12018
|
+
let scopeSql2 = "approval_scope IS NULL";
|
|
12019
|
+
if (approvalScope !== null) {
|
|
12020
|
+
params.push(approvalScope);
|
|
12021
|
+
scopeSql2 = `approval_scope = $${params.length}`;
|
|
12022
|
+
}
|
|
12023
|
+
const wall = claimVisibilitySql(principals, params.length + 1);
|
|
12024
|
+
params.push(...wall.params);
|
|
10866
12025
|
const candidates = await engine.pool.query(
|
|
10867
|
-
`SELECT * FROM context_engine_tool_approvals
|
|
10868
|
-
|
|
12026
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
12027
|
+
WHERE tool_name = $1 AND status = 'approved' AND ${scopeSql2} AND ${wall.sql}`,
|
|
12028
|
+
params
|
|
10869
12029
|
);
|
|
10870
12030
|
for (const row of candidates.rows) {
|
|
10871
12031
|
if ((row.source_id ?? null) !== (sourceId ?? null)) continue;
|
|
@@ -10882,7 +12042,7 @@ async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
|
10882
12042
|
}
|
|
10883
12043
|
return null;
|
|
10884
12044
|
}
|
|
10885
|
-
async function dispatchMcp(ct, config, args) {
|
|
12045
|
+
async function dispatchMcp(ct, config, args, opts = {}) {
|
|
10886
12046
|
const url = config.url;
|
|
10887
12047
|
if (!url) throw new EngineActionError(`mcp tool ${ct.callName} config missing 'url'`);
|
|
10888
12048
|
const headers = { ...config.headers ?? {} };
|
|
@@ -10891,7 +12051,7 @@ async function dispatchMcp(ct, config, args) {
|
|
|
10891
12051
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
10892
12052
|
else if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
10893
12053
|
const toolName = String(ct.raw?.tool_name ?? ct.displayName);
|
|
10894
|
-
const mcp = new PromptevMCP();
|
|
12054
|
+
const mcp = new PromptevMCP({ allowPrivate: opts.allowPrivate ?? false });
|
|
10895
12055
|
try {
|
|
10896
12056
|
await mcp.addServer(ct.callName, url, { headers: Object.keys(headers).length ? headers : null });
|
|
10897
12057
|
const client = mcp.clients.get(ct.callName);
|
|
@@ -10902,7 +12062,10 @@ async function dispatchMcp(ct, config, args) {
|
|
|
10902
12062
|
}
|
|
10903
12063
|
async function dispatch(engine, ct, config, args) {
|
|
10904
12064
|
if (ct.kind === "http") {
|
|
10905
|
-
return executeHttp(config, args, {
|
|
12065
|
+
return executeHttp(config, args, {
|
|
12066
|
+
client: engine._toolHttpClient ?? null,
|
|
12067
|
+
allowPrivate: allowPrivateEgress(engine)
|
|
12068
|
+
});
|
|
10906
12069
|
}
|
|
10907
12070
|
if (ct.kind === "db") {
|
|
10908
12071
|
const query = String(args.query ?? "");
|
|
@@ -10917,7 +12080,9 @@ async function dispatch(engine, ct, config, args) {
|
|
|
10917
12080
|
}
|
|
10918
12081
|
return result;
|
|
10919
12082
|
}
|
|
10920
|
-
if (ct.kind === "mcp")
|
|
12083
|
+
if (ct.kind === "mcp") {
|
|
12084
|
+
return dispatchMcp(ct, config, args, { allowPrivate: allowPrivateEgress(engine) });
|
|
12085
|
+
}
|
|
10921
12086
|
if (ct.kind === "function") {
|
|
10922
12087
|
const fn = ct.raw?.callable;
|
|
10923
12088
|
if (!fn) throw new EngineActionError(`function tool ${ct.callName} has no callable`);
|
|
@@ -10938,10 +12103,20 @@ async function decryptCtConfig(engine, toolId) {
|
|
|
10938
12103
|
]);
|
|
10939
12104
|
const row = result.rows[0];
|
|
10940
12105
|
if (!row?.config_encrypted) return {};
|
|
10941
|
-
return decryptDict(String(row.config_encrypted),
|
|
12106
|
+
return decryptDict(String(row.config_encrypted), cryptoKey(engine));
|
|
12107
|
+
}
|
|
12108
|
+
var warnedUnscoped = /* @__PURE__ */ new Set();
|
|
12109
|
+
function warnUnscopedApproval(callName) {
|
|
12110
|
+
if (warnedUnscoped.has(callName)) return;
|
|
12111
|
+
warnedUnscoped.add(callName);
|
|
12112
|
+
process.emitWarning(
|
|
12113
|
+
`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.`,
|
|
12114
|
+
{ type: "DeprecationWarning", code: "CE_APPROVAL_SCOPE_MISSING" }
|
|
12115
|
+
);
|
|
10942
12116
|
}
|
|
10943
12117
|
async function executeTool(engine, callName, args, opts = {}) {
|
|
10944
12118
|
const runtimeArgs = args ?? {};
|
|
12119
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
10945
12120
|
const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
|
|
10946
12121
|
if (!ct) throw new EngineActionError(`tool not found: ${callName}`);
|
|
10947
12122
|
if (!toolVisible(ct, opts.principals ?? null)) {
|
|
@@ -10950,22 +12125,32 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
10950
12125
|
const publicArgs = stripUnderscoreArgs(runtimeArgs);
|
|
10951
12126
|
let approvalId = null;
|
|
10952
12127
|
if (shouldRequireApproval(ct, publicArgs)) {
|
|
10953
|
-
|
|
12128
|
+
if (approvalScope === null) warnUnscopedApproval(callName);
|
|
12129
|
+
approvalId = await findAndClaimApproved(
|
|
12130
|
+
engine,
|
|
12131
|
+
ct.callName,
|
|
12132
|
+
publicArgs,
|
|
12133
|
+
opts.sourceId ?? null,
|
|
12134
|
+
opts.principals ?? null,
|
|
12135
|
+
approvalScope
|
|
12136
|
+
);
|
|
10954
12137
|
if (approvalId == null) {
|
|
10955
|
-
const
|
|
12138
|
+
const pendingOpts = {
|
|
10956
12139
|
toolName: ct.callName,
|
|
10957
12140
|
args: publicArgs,
|
|
10958
12141
|
sourceId: opts.sourceId ?? null,
|
|
10959
12142
|
principals: opts.principals ?? null,
|
|
10960
12143
|
policy: ct.approvalPolicy ?? {}
|
|
10961
|
-
}
|
|
12144
|
+
};
|
|
12145
|
+
const record = approvalScope === null ? await createPending(engine, pendingOpts) : await findOrCreatePending(engine, { ...pendingOpts, approvalScope });
|
|
10962
12146
|
const reason = ct.requiresApproval ? "tool requires approval" : `approval policy condition met: ${ct.approvalPolicy?.condition}`;
|
|
10963
12147
|
return {
|
|
10964
12148
|
approval_required: {
|
|
10965
12149
|
approval_id: String(record.id),
|
|
10966
12150
|
tool_name: ct.callName,
|
|
10967
12151
|
args: publicArgs,
|
|
10968
|
-
reason
|
|
12152
|
+
reason,
|
|
12153
|
+
expires_at: record.expiresAt ? record.expiresAt.toISOString() : null
|
|
10969
12154
|
}
|
|
10970
12155
|
};
|
|
10971
12156
|
}
|
|
@@ -10990,8 +12175,9 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
10990
12175
|
let truncated = false;
|
|
10991
12176
|
if (success) {
|
|
10992
12177
|
try {
|
|
12178
|
+
const redactPrincipals = opts.principals === TRUSTED ? null : opts.principals ?? null;
|
|
10993
12179
|
const [redacted] = redactToolResult(rawResult, engine.config.redaction, {
|
|
10994
|
-
principals:
|
|
12180
|
+
principals: redactPrincipals,
|
|
10995
12181
|
secretKey: engine.config.secretKey,
|
|
10996
12182
|
hooks: engine.hooks
|
|
10997
12183
|
});
|
|
@@ -11090,11 +12276,19 @@ var ContextEngine = class {
|
|
|
11090
12276
|
_graphStore = null;
|
|
11091
12277
|
constructor(config, opts = {}) {
|
|
11092
12278
|
this.config = config;
|
|
11093
|
-
this.hooks = {
|
|
12279
|
+
this.hooks = {
|
|
12280
|
+
onUsage: opts.onUsage ?? null,
|
|
12281
|
+
onError: opts.onError ?? null,
|
|
12282
|
+
onProgress: opts.onProgress ?? null
|
|
12283
|
+
};
|
|
11094
12284
|
}
|
|
11095
12285
|
async ensurePool() {
|
|
11096
12286
|
if (!this._pool) {
|
|
11097
|
-
this._pool = await createPool(this.config.databaseUrl
|
|
12287
|
+
this._pool = await createPool(this.config.databaseUrl, {
|
|
12288
|
+
max: this.config.storage.poolMax,
|
|
12289
|
+
idleTimeoutMillis: this.config.storage.poolIdleTimeoutMs,
|
|
12290
|
+
connectionTimeoutMillis: this.config.storage.poolConnectionTimeoutMs
|
|
12291
|
+
});
|
|
11098
12292
|
this.pool = this._pool;
|
|
11099
12293
|
}
|
|
11100
12294
|
if (!this.backend) {
|
|
@@ -11280,7 +12474,9 @@ var ContextEngine = class {
|
|
|
11280
12474
|
graphStore: await this.getGraphStore(),
|
|
11281
12475
|
hooks: this.hooks,
|
|
11282
12476
|
sourceIds: opts.sourceIds ?? null,
|
|
11283
|
-
|
|
12477
|
+
documentIds: opts.documentIds ?? null,
|
|
12478
|
+
principals,
|
|
12479
|
+
backend: this.backend
|
|
11284
12480
|
});
|
|
11285
12481
|
}
|
|
11286
12482
|
return runSearch(query, {
|
|
@@ -11290,6 +12486,7 @@ var ContextEngine = class {
|
|
|
11290
12486
|
embedder: this.embedder,
|
|
11291
12487
|
pool,
|
|
11292
12488
|
sourceIds: opts.sourceIds,
|
|
12489
|
+
documentIds: opts.documentIds,
|
|
11293
12490
|
principals,
|
|
11294
12491
|
topK: opts.topK,
|
|
11295
12492
|
mode: opts.mode,
|
|
@@ -11395,7 +12592,7 @@ var ContextEngine = class {
|
|
|
11395
12592
|
hooks: this.hooks,
|
|
11396
12593
|
sourceIds: opts.sourceIds,
|
|
11397
12594
|
principals: resolvePrincipals(opts.principals, "compute"),
|
|
11398
|
-
|
|
12595
|
+
documentIds: opts.documentIds,
|
|
11399
12596
|
modelCfg: opts.modelCfg,
|
|
11400
12597
|
timeout: opts.timeout
|
|
11401
12598
|
});
|
|
@@ -11448,7 +12645,8 @@ var ContextEngine = class {
|
|
|
11448
12645
|
sourceId: opts.sourceId,
|
|
11449
12646
|
principals: resolvePrincipals(opts.principals, "executeTool"),
|
|
11450
12647
|
actor: opts.actor,
|
|
11451
|
-
source: opts.source ?? "api"
|
|
12648
|
+
source: opts.source ?? "api",
|
|
12649
|
+
approvalScope: opts.approvalScope
|
|
11452
12650
|
});
|
|
11453
12651
|
}
|
|
11454
12652
|
};
|
|
@@ -11461,18 +12659,83 @@ init_hooks();
|
|
|
11461
12659
|
// src/mcp.ts
|
|
11462
12660
|
init_errors();
|
|
11463
12661
|
init_extras();
|
|
11464
|
-
|
|
11465
|
-
|
|
12662
|
+
var HandlerError = class extends Error {
|
|
12663
|
+
status;
|
|
12664
|
+
detail;
|
|
12665
|
+
constructor(status, detail) {
|
|
12666
|
+
super(typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
12667
|
+
this.name = "HandlerError";
|
|
12668
|
+
this.status = status;
|
|
12669
|
+
this.detail = detail;
|
|
12670
|
+
}
|
|
12671
|
+
};
|
|
12672
|
+
z.object({
|
|
12673
|
+
text: z.string(),
|
|
12674
|
+
name: z.string(),
|
|
12675
|
+
source_id: z.string().nullable().optional(),
|
|
12676
|
+
external_id: z.string().nullable().optional(),
|
|
12677
|
+
description: z.string().nullable().optional(),
|
|
12678
|
+
meta_data: z.record(z.unknown()).nullable().optional(),
|
|
12679
|
+
acl: z.array(z.string()).nullable().optional(),
|
|
12680
|
+
mode: z.enum(["hybrid", "graph"]).nullable().optional(),
|
|
12681
|
+
extract_structured: z.boolean().optional().default(false),
|
|
12682
|
+
batch: z.boolean().optional().default(false)
|
|
12683
|
+
}).strip();
|
|
12684
|
+
z.object({
|
|
12685
|
+
acl: z.array(z.string()).nullable().optional(),
|
|
12686
|
+
name: z.string().nullable().optional(),
|
|
12687
|
+
description: z.string().nullable().optional(),
|
|
12688
|
+
meta_data: z.record(z.unknown()).nullable().optional()
|
|
12689
|
+
}).strict();
|
|
12690
|
+
z.object({
|
|
12691
|
+
query: z.string(),
|
|
12692
|
+
source_ids: z.array(z.string()).nullable().optional(),
|
|
12693
|
+
// Narrows WITHIN a source and INTERSECTS with source_ids — it can only
|
|
12694
|
+
// shrink the result set (the ACL predicate still applies in the same SQL
|
|
12695
|
+
// conjunction), so exposing it needs no authorizeAcl-style grant check.
|
|
12696
|
+
document_ids: z.array(z.string()).nullable().optional(),
|
|
12697
|
+
top_k: z.number().int().optional().default(10),
|
|
12698
|
+
mode: z.enum(["hybrid", "graph"]).optional().default("hybrid"),
|
|
12699
|
+
compress_to_tokens: z.number().int().nullable().optional()
|
|
12700
|
+
}).strip();
|
|
12701
|
+
var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12702
|
+
function requireValidUuid(documentId) {
|
|
12703
|
+
if (!UUID_RE5.test(String(documentId))) {
|
|
12704
|
+
throw new HandlerError(400, `invalid document id: ${JSON.stringify(documentId)}`);
|
|
12705
|
+
}
|
|
12706
|
+
}
|
|
12707
|
+
function resolveRequestPrincipals(value, opts) {
|
|
12708
|
+
if (value === TRUSTED) return TRUSTED;
|
|
12709
|
+
if (value == null) {
|
|
12710
|
+
throw new HandlerError(
|
|
12711
|
+
500,
|
|
12712
|
+
`${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.`
|
|
12713
|
+
);
|
|
12714
|
+
}
|
|
12715
|
+
if (!Array.isArray(value) || value.some((p) => typeof p !== "string")) {
|
|
12716
|
+
throw new HandlerError(
|
|
12717
|
+
500,
|
|
12718
|
+
`${opts.surface}: the \`principals\` dependency must return a list of principal strings, [] for an anonymous caller, or TRUSTED \u2014 got ${typeof value}.`
|
|
12719
|
+
);
|
|
12720
|
+
}
|
|
12721
|
+
return value;
|
|
12722
|
+
}
|
|
11466
12723
|
var 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.";
|
|
11467
12724
|
async function resolvePrincipalsFn(principals) {
|
|
11468
12725
|
const result = await Promise.resolve(principals());
|
|
11469
|
-
|
|
12726
|
+
try {
|
|
12727
|
+
return resolveRequestPrincipals(result, { surface: "the MCP `principals` dependency" });
|
|
12728
|
+
} catch (exc) {
|
|
12729
|
+
if (exc instanceof HandlerError) throw new Error(exc.message, { cause: exc });
|
|
12730
|
+
throw exc;
|
|
12731
|
+
}
|
|
11470
12732
|
}
|
|
11471
|
-
function registerToolGateway(mcp, engine, principals) {
|
|
12733
|
+
function registerToolGateway(mcp, engine, principals, approvalScope = null) {
|
|
11472
12734
|
mcp.tool(
|
|
11473
12735
|
"search_tools",
|
|
11474
12736
|
"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,
|
|
11475
|
-
|
|
12737
|
+
// Zod raw shape — see the note on the search tool in mcp.ts.
|
|
12738
|
+
{ query: z.string(), limit: z.number().int().optional() },
|
|
11476
12739
|
async (...raw) => {
|
|
11477
12740
|
const args = raw[0] ?? {};
|
|
11478
12741
|
const callerPrincipals = await resolvePrincipalsFn(principals);
|
|
@@ -11486,13 +12749,15 @@ function registerToolGateway(mcp, engine, principals) {
|
|
|
11486
12749
|
mcp.tool(
|
|
11487
12750
|
"execute_tool",
|
|
11488
12751
|
"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,
|
|
11489
|
-
{ name:
|
|
12752
|
+
{ name: z.string(), args: z.record(z.unknown()).optional() },
|
|
11490
12753
|
async (...raw) => {
|
|
11491
12754
|
const body = raw[0] ?? {};
|
|
11492
12755
|
const callerPrincipals = await resolvePrincipalsFn(principals);
|
|
12756
|
+
const scope = approvalScope ? await Promise.resolve(approvalScope()) : null;
|
|
11493
12757
|
return engine.executeTool(String(body.name), body.args ?? null, {
|
|
11494
12758
|
principals: callerPrincipals,
|
|
11495
|
-
source: "mcp"
|
|
12759
|
+
source: "mcp",
|
|
12760
|
+
approvalScope: scope
|
|
11496
12761
|
});
|
|
11497
12762
|
}
|
|
11498
12763
|
);
|
|
@@ -11534,15 +12799,24 @@ async function createMcpApp(engine, opts) {
|
|
|
11534
12799
|
"search_knowledge_base",
|
|
11535
12800
|
`Hybrid search (full-text + trigram + vector, RRF-fused) over the ingested corpus. Returns the top-matching chunks with their source document.${SCOPE_NOTE}`,
|
|
11536
12801
|
{
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
12802
|
+
// Zod RAW SHAPES, not JSON schema: the SDK's isZodRawShape test
|
|
12803
|
+
// rejects a plain schema object — on current SDK versions that made
|
|
12804
|
+
// registration THROW at startup, and on 1.12.0 the object was consumed
|
|
12805
|
+
// as annotations and every handler ran with NO arguments.
|
|
12806
|
+
query: z.string(),
|
|
12807
|
+
source_ids: z.array(z.string()).optional(),
|
|
12808
|
+
document_ids: z.array(z.string()).optional(),
|
|
12809
|
+
top_k: z.number().int().optional(),
|
|
12810
|
+
mode: z.enum(["hybrid", "graph"]).optional()
|
|
11541
12811
|
},
|
|
11542
12812
|
async (args) => {
|
|
12813
|
+
for (const did of args.document_ids ?? []) {
|
|
12814
|
+
requireValidUuid(did);
|
|
12815
|
+
}
|
|
11543
12816
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11544
12817
|
const result = await engine.search(String(args.query ?? ""), {
|
|
11545
12818
|
sourceIds: args.source_ids,
|
|
12819
|
+
documentIds: args.document_ids,
|
|
11546
12820
|
principals: callerPrincipals,
|
|
11547
12821
|
topK: args.top_k ?? 10,
|
|
11548
12822
|
mode: args.mode ?? "hybrid"
|
|
@@ -11564,7 +12838,7 @@ async function createMcpApp(engine, opts) {
|
|
|
11564
12838
|
tool(
|
|
11565
12839
|
"get_document",
|
|
11566
12840
|
`Fetch one document's full text and metadata by id.${SCOPE_NOTE}`,
|
|
11567
|
-
{ document_id:
|
|
12841
|
+
{ document_id: z.string() },
|
|
11568
12842
|
async (args) => {
|
|
11569
12843
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11570
12844
|
return engine.getDocument(String(args.document_id), { principals: callerPrincipals });
|
|
@@ -11574,7 +12848,7 @@ async function createMcpApp(engine, opts) {
|
|
|
11574
12848
|
tool(
|
|
11575
12849
|
"query_structured",
|
|
11576
12850
|
`Answer a question against documents' extracted structured data (only documents with structured data are candidates).${SCOPE_NOTE}`,
|
|
11577
|
-
{ question:
|
|
12851
|
+
{ question: z.string(), source_ids: z.array(z.string()).optional() },
|
|
11578
12852
|
async (args) => {
|
|
11579
12853
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11580
12854
|
if (!engine.queryStructured) throw new Error("queryStructured is not available on this engine");
|
|
@@ -11588,7 +12862,7 @@ async function createMcpApp(engine, opts) {
|
|
|
11588
12862
|
tool(
|
|
11589
12863
|
"compute",
|
|
11590
12864
|
`Run LLM-authored JavaScript over in-scope spreadsheet documents and return the computed result.${SCOPE_NOTE}`,
|
|
11591
|
-
{ instruction:
|
|
12865
|
+
{ instruction: z.string(), source_ids: z.array(z.string()).optional() },
|
|
11592
12866
|
async (args) => {
|
|
11593
12867
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11594
12868
|
if (!engine.compute) throw new Error("compute is not available on this engine");
|
|
@@ -11607,7 +12881,8 @@ async function createMcpApp(engine, opts) {
|
|
|
11607
12881
|
}
|
|
11608
12882
|
},
|
|
11609
12883
|
engine,
|
|
11610
|
-
opts.principals
|
|
12884
|
+
opts.principals,
|
|
12885
|
+
opts.approvalScope ?? null
|
|
11611
12886
|
);
|
|
11612
12887
|
const handler = (async (req, res) => {
|
|
11613
12888
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
@@ -11620,6 +12895,9 @@ async function createMcpApp(engine, opts) {
|
|
|
11620
12895
|
|
|
11621
12896
|
// src/providers/index.ts
|
|
11622
12897
|
init_llm();
|
|
12898
|
+
|
|
12899
|
+
// src/index.ts
|
|
12900
|
+
init_redaction();
|
|
11623
12901
|
var InProcessRunner = class {
|
|
11624
12902
|
registry = /* @__PURE__ */ new Map();
|
|
11625
12903
|
/** Kept so the Promise isn't garbage-collected mid-flight. */
|
|
@@ -11671,6 +12949,6 @@ var CeleryRunner = class {
|
|
|
11671
12949
|
// src/index.ts
|
|
11672
12950
|
init_usage();
|
|
11673
12951
|
|
|
11674
|
-
export { ApprovalExpired, ApprovalNotPending, CeleryRunner, CodeExecutionError, CodeExecutionTimeout, ContextEngine, ContextEngineConfig, DEFAULT_LEG_WEIGHT, DocumentNotFoundError, EXTRACTION_VERSION, Embedder, EngineActionError, ExtraMissingError, Extracted, GraphLegUnavailable, InProcessRunner, LLMClient, PostgresBackend, RedactionPolicy, RedactionRule, TRUSTED, ToolConfig, UNSET, __version__, applyRedaction, buildEmbedder, buildLlmClient, callLlm, compute, configSchema, createMcpApp, decryptDict, emitError, emitToolCall, emitUsage, encryptDict, extract2 as extract, extractStructuredData, functionTool, getDocumentText, getSecretKey, graphUnits, listDocuments, queryStructured, redactHits, rerank, resolveApproval, resolveFields, resolvePrincipals, rrfFuse, runMigrate, runSearch, shouldRequireApproval, unitsForFile, upsertRegistry };
|
|
12952
|
+
export { ApprovalExpired, ApprovalNotPending, CeleryRunner, CodeExecutionError, CodeExecutionTimeout, ContextEngine, ContextEngineConfig, DEFAULT_LEG_WEIGHT, DocumentNotFoundError, EXTRACTION_VERSION, Embedder, EngineActionError, ExtraMissingError, Extracted, GraphLegUnavailable, InProcessRunner, LLMClient, PostgresBackend, RedactionPolicy, RedactionRule, TRUSTED, ToolConfig, UNSET, __version__, applyRedaction, buildEmbedder, buildLlmClient, callLlm, compute, configSchema, createMcpApp, decryptDict, emitError, emitProgress, emitToolCall, emitUsage, encryptDict, extract2 as extract, extractStructuredData, functionTool, getDocumentText, getSecretKey, graphUnits, listDocuments, queryStructured, redactHits, rerank, resolveApproval, resolveFields, resolvePrincipals, rrfFuse, runMigrate, runSearch, shouldRequireApproval, unitsForFile, upsertRegistry };
|
|
11675
12953
|
//# sourceMappingURL=index.js.map
|
|
11676
12954
|
//# sourceMappingURL=index.js.map
|