@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.cjs
CHANGED
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
var franc = require('franc');
|
|
4
4
|
var OpenAI = require('openai');
|
|
5
|
+
var crypto = require('crypto');
|
|
5
6
|
var jsonrepair = require('jsonrepair');
|
|
6
7
|
var cheerio = require('cheerio');
|
|
7
8
|
var ExcelJS = require('exceljs');
|
|
8
9
|
var JSZip = require('jszip');
|
|
9
10
|
var mammoth = require('mammoth');
|
|
10
11
|
var PostalMime = require('postal-mime');
|
|
12
|
+
var zod = require('zod');
|
|
11
13
|
var unpdf = require('unpdf');
|
|
12
14
|
var module$1 = require('module');
|
|
13
|
-
var crypto = require('crypto');
|
|
14
15
|
var fs = require('fs');
|
|
15
16
|
var path = require('path');
|
|
16
|
-
var zod = require('zod');
|
|
17
17
|
var url = require('url');
|
|
18
18
|
var jsTiktoken = require('js-tiktoken');
|
|
19
|
+
var dns = require('dns');
|
|
20
|
+
var net = require('net');
|
|
19
21
|
|
|
20
22
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
21
23
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -786,6 +788,14 @@ function emitError(hooks, exc, ctx) {
|
|
|
786
788
|
log2.warn("onError callback raised; swallowing");
|
|
787
789
|
}
|
|
788
790
|
}
|
|
791
|
+
function emitProgress(hooks, event) {
|
|
792
|
+
if (!hooks?.onProgress) return;
|
|
793
|
+
try {
|
|
794
|
+
hooks.onProgress(event);
|
|
795
|
+
} catch {
|
|
796
|
+
log2.warn("onProgress callback raised; swallowing");
|
|
797
|
+
}
|
|
798
|
+
}
|
|
789
799
|
function emitToolCall(hooks, event) {
|
|
790
800
|
if (!hooks?.onToolCall) return;
|
|
791
801
|
try {
|
|
@@ -807,6 +817,8 @@ var init_hooks = __esm({
|
|
|
807
817
|
// src/providers/llm.ts
|
|
808
818
|
var llm_exports = {};
|
|
809
819
|
__export(llm_exports, {
|
|
820
|
+
CALL_TIMEOUT_MS: () => CALL_TIMEOUT_MS,
|
|
821
|
+
GEMINI_CALL_TIMEOUT_MS: () => GEMINI_CALL_TIMEOUT_MS,
|
|
810
822
|
LLMClient: () => exports.LLMClient,
|
|
811
823
|
TIMEOUT_MS: () => TIMEOUT_MS,
|
|
812
824
|
buildLlmClient: () => buildLlmClient,
|
|
@@ -849,7 +861,7 @@ async function loadGeminiChatClient(apiKey) {
|
|
|
849
861
|
if (!Ctor) {
|
|
850
862
|
throw new exports.ExtraMissingError("gemini", specifier, "gemini llm");
|
|
851
863
|
}
|
|
852
|
-
return new Ctor({ apiKey: apiKey ?? null });
|
|
864
|
+
return new Ctor({ apiKey: apiKey ?? null, httpOptions: { timeout: GEMINI_CALL_TIMEOUT_MS } });
|
|
853
865
|
}
|
|
854
866
|
async function loadBedrockSdk() {
|
|
855
867
|
const specifier = "@aws-sdk/client-bedrock-runtime";
|
|
@@ -886,11 +898,13 @@ async function callLlm(cfg2, opts) {
|
|
|
886
898
|
await owned.aclose();
|
|
887
899
|
}
|
|
888
900
|
}
|
|
889
|
-
var TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY; exports.LLMClient = void 0;
|
|
901
|
+
var CALL_TIMEOUT_MS, TIMEOUT_MS, GEMINI_CALL_TIMEOUT_MS, ANTHROPIC_VERSION, ANTHROPIC_MAX_TOKENS, OPENAI_FAMILY; exports.LLMClient = void 0;
|
|
890
902
|
var init_llm = __esm({
|
|
891
903
|
"src/providers/llm.ts"() {
|
|
892
904
|
init_errors();
|
|
893
|
-
|
|
905
|
+
CALL_TIMEOUT_MS = 24e4;
|
|
906
|
+
TIMEOUT_MS = CALL_TIMEOUT_MS;
|
|
907
|
+
GEMINI_CALL_TIMEOUT_MS = CALL_TIMEOUT_MS;
|
|
894
908
|
ANTHROPIC_VERSION = "2023-06-01";
|
|
895
909
|
ANTHROPIC_MAX_TOKENS = 4096;
|
|
896
910
|
OPENAI_FAMILY = /* @__PURE__ */ new Set(["openai", "azure_openai", "custom"]);
|
|
@@ -918,22 +932,30 @@ var init_llm = __esm({
|
|
|
918
932
|
await this.aclose();
|
|
919
933
|
}
|
|
920
934
|
async call(opts) {
|
|
921
|
-
const {
|
|
935
|
+
const {
|
|
936
|
+
system,
|
|
937
|
+
user,
|
|
938
|
+
jsonMode = false,
|
|
939
|
+
images = null,
|
|
940
|
+
maxTokens = null,
|
|
941
|
+
thinkingBudget = null,
|
|
942
|
+
temperature = null
|
|
943
|
+
} = opts;
|
|
922
944
|
if (this.provider === "anthropic") {
|
|
923
|
-
return this.callAnthropic(system, user, jsonMode, images);
|
|
945
|
+
return this.callAnthropic(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
924
946
|
}
|
|
925
947
|
if (OPENAI_FAMILY.has(this.provider)) {
|
|
926
|
-
return this.callOpenAI(system, user, jsonMode, images);
|
|
948
|
+
return this.callOpenAI(system, user, jsonMode, images, maxTokens, temperature);
|
|
927
949
|
}
|
|
928
950
|
if (this.provider === "gemini") {
|
|
929
|
-
return this.callGemini(system, user, jsonMode, images);
|
|
951
|
+
return this.callGemini(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
930
952
|
}
|
|
931
953
|
if (this.provider === "bedrock") {
|
|
932
|
-
return this.callBedrock(system, user, jsonMode, images);
|
|
954
|
+
return this.callBedrock(system, user, jsonMode, images, maxTokens, thinkingBudget, temperature);
|
|
933
955
|
}
|
|
934
956
|
throw new Error(`unknown llm provider: ${JSON.stringify(this.provider)}`);
|
|
935
957
|
}
|
|
936
|
-
async callAnthropic(system, user, jsonMode, images) {
|
|
958
|
+
async callAnthropic(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
937
959
|
if (!this.fetchImpl) {
|
|
938
960
|
throw new Error("anthropic llm client has no fetch implementation");
|
|
939
961
|
}
|
|
@@ -954,26 +976,27 @@ Respond with valid JSON only.`;
|
|
|
954
976
|
});
|
|
955
977
|
}
|
|
956
978
|
content.push({ type: "text", text: user });
|
|
957
|
-
const
|
|
958
|
-
this.
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
979
|
+
const body = {
|
|
980
|
+
model: this.model,
|
|
981
|
+
max_tokens: maxTokens ?? ANTHROPIC_MAX_TOKENS,
|
|
982
|
+
system,
|
|
983
|
+
messages: [{ role: "user", content }]
|
|
984
|
+
};
|
|
985
|
+
if (thinkingBudget) {
|
|
986
|
+
body.thinking = { type: "enabled", budget_tokens: thinkingBudget };
|
|
987
|
+
} else if (temperature !== null) {
|
|
988
|
+
body.temperature = temperature;
|
|
989
|
+
}
|
|
990
|
+
const data = await postJson(this.fetchImpl, "https://api.anthropic.com/v1/messages", body, {
|
|
991
|
+
"x-api-key": this.cfg.apiKey ?? "",
|
|
992
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
993
|
+
"content-type": "application/json"
|
|
994
|
+
});
|
|
972
995
|
const text = data.content[0].text;
|
|
973
996
|
const usage = data.usage ?? {};
|
|
974
997
|
return [text, { input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 }];
|
|
975
998
|
}
|
|
976
|
-
async callOpenAI(system, user, jsonMode, images) {
|
|
999
|
+
async callOpenAI(system, user, jsonMode, images, maxTokens, temperature = null) {
|
|
977
1000
|
if (!this.client) {
|
|
978
1001
|
throw new Error("openai-family llm client has no client");
|
|
979
1002
|
}
|
|
@@ -994,12 +1017,18 @@ Respond with valid JSON only.`;
|
|
|
994
1017
|
if (jsonMode) {
|
|
995
1018
|
body.response_format = { type: "json_object" };
|
|
996
1019
|
}
|
|
1020
|
+
if (maxTokens) {
|
|
1021
|
+
body.max_completion_tokens = maxTokens;
|
|
1022
|
+
}
|
|
1023
|
+
if (temperature !== null) {
|
|
1024
|
+
body.temperature = temperature;
|
|
1025
|
+
}
|
|
997
1026
|
const resp = await this.client.chat.completions.create(body);
|
|
998
1027
|
const text = resp.choices[0]?.message?.content ?? "";
|
|
999
1028
|
const usage = resp.usage;
|
|
1000
1029
|
return [text, { input: usage?.prompt_tokens ?? 0, output: usage?.completion_tokens ?? 0 }];
|
|
1001
1030
|
}
|
|
1002
|
-
async callGemini(system, user, jsonMode, images) {
|
|
1031
|
+
async callGemini(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
1003
1032
|
if (!this.genaiClient) {
|
|
1004
1033
|
this.genaiClient = await loadGeminiChatClient(this.cfg.apiKey);
|
|
1005
1034
|
}
|
|
@@ -1008,10 +1037,30 @@ Respond with valid JSON only.`;
|
|
|
1008
1037
|
parts.push({ inlineData: { mimeType: "image/png", data: toBase64(img) } });
|
|
1009
1038
|
}
|
|
1010
1039
|
parts.push({ text: user });
|
|
1011
|
-
const config = {
|
|
1040
|
+
const config = {
|
|
1041
|
+
systemInstruction: system,
|
|
1042
|
+
// ALWAYS off, and not as a preference. Automatic function calling means
|
|
1043
|
+
// the SDK itself EXECUTES a callable it was handed as a tool and loops
|
|
1044
|
+
// on the result — up to ten round trips — before returning anything.
|
|
1045
|
+
// This package passes declarations only, so today nothing is executable;
|
|
1046
|
+
// but that depends on every future caller continuing to do the same, and
|
|
1047
|
+
// an application that gates tool execution behind human approval would
|
|
1048
|
+
// have that gate bypassed silently, by a library, with the loop already
|
|
1049
|
+
// run before it could object.
|
|
1050
|
+
automaticFunctionCalling: { disable: true }
|
|
1051
|
+
};
|
|
1012
1052
|
if (jsonMode) {
|
|
1013
1053
|
config.responseMimeType = "application/json";
|
|
1014
1054
|
}
|
|
1055
|
+
if (maxTokens) {
|
|
1056
|
+
config.maxOutputTokens = maxTokens;
|
|
1057
|
+
}
|
|
1058
|
+
if (temperature !== null) {
|
|
1059
|
+
config.temperature = temperature;
|
|
1060
|
+
}
|
|
1061
|
+
if (thinkingBudget !== null) {
|
|
1062
|
+
config.thinkingConfig = { thinkingBudget };
|
|
1063
|
+
}
|
|
1015
1064
|
const resp = await this.genaiClient.models.generateContent({
|
|
1016
1065
|
model: this.model,
|
|
1017
1066
|
contents: parts,
|
|
@@ -1023,7 +1072,7 @@ Respond with valid JSON only.`;
|
|
|
1023
1072
|
const outputTokens = usageMeta?.candidatesTokenCount ?? usageMeta?.candidates_token_count ?? 0;
|
|
1024
1073
|
return [text, { input: inputTokens || 0, output: outputTokens || 0 }];
|
|
1025
1074
|
}
|
|
1026
|
-
async callBedrock(system, user, jsonMode, images) {
|
|
1075
|
+
async callBedrock(system, user, jsonMode, images, maxTokens, thinkingBudget = null, temperature = null) {
|
|
1027
1076
|
const { BedrockRuntimeClient, ConverseCommand } = await loadBedrockSdk();
|
|
1028
1077
|
if (jsonMode) {
|
|
1029
1078
|
system = `${system}
|
|
@@ -1047,7 +1096,21 @@ Respond with valid JSON only.`;
|
|
|
1047
1096
|
new ConverseCommand({
|
|
1048
1097
|
modelId: this.model,
|
|
1049
1098
|
system: [{ text: system }],
|
|
1050
|
-
messages: [{ role: "user", content }]
|
|
1099
|
+
messages: [{ role: "user", content }],
|
|
1100
|
+
...maxTokens || temperature !== null ? {
|
|
1101
|
+
inferenceConfig: {
|
|
1102
|
+
...maxTokens ? { maxTokens } : {},
|
|
1103
|
+
...temperature !== null ? { temperature } : {}
|
|
1104
|
+
}
|
|
1105
|
+
} : {},
|
|
1106
|
+
// Anthropic-style passthrough — Converse forwards it to the model.
|
|
1107
|
+
// Zero (the vision transcription contract) sends nothing: thinking
|
|
1108
|
+
// is opt-in for the anthropic models bedrock hosts.
|
|
1109
|
+
...thinkingBudget ? {
|
|
1110
|
+
additionalModelRequestFields: {
|
|
1111
|
+
thinking: { type: "enabled", budget_tokens: thinkingBudget }
|
|
1112
|
+
}
|
|
1113
|
+
} : {}
|
|
1051
1114
|
})
|
|
1052
1115
|
);
|
|
1053
1116
|
const text = result.output?.message?.content?.[0]?.text ?? "";
|
|
@@ -1060,6 +1123,302 @@ Respond with valid JSON only.`;
|
|
|
1060
1123
|
};
|
|
1061
1124
|
}
|
|
1062
1125
|
});
|
|
1126
|
+
function encryptDict(data, key) {
|
|
1127
|
+
const nonce = crypto.randomBytes(12);
|
|
1128
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
1129
|
+
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
1130
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1131
|
+
const tag = cipher.getAuthTag();
|
|
1132
|
+
return Buffer.concat([nonce, ciphertext, tag]).toString("base64");
|
|
1133
|
+
}
|
|
1134
|
+
function decryptDict(token, key) {
|
|
1135
|
+
const raw = Buffer.from(token, "base64");
|
|
1136
|
+
const nonce = raw.subarray(0, 12);
|
|
1137
|
+
const tag = raw.subarray(raw.length - 16);
|
|
1138
|
+
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
1139
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
1140
|
+
decipher.setAuthTag(tag);
|
|
1141
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1142
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
1143
|
+
}
|
|
1144
|
+
function getSecretKey(config) {
|
|
1145
|
+
const secretKey = config.secretKey;
|
|
1146
|
+
if (!secretKey) {
|
|
1147
|
+
throw new Error(
|
|
1148
|
+
"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."
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
return Buffer.from(secretKey, "base64url");
|
|
1152
|
+
}
|
|
1153
|
+
function hmacSha256Hex(key, value) {
|
|
1154
|
+
const k = typeof key === "string" ? Buffer.from(key) : key;
|
|
1155
|
+
return crypto.createHmac("sha256", k).update(value, "utf8").digest("hex");
|
|
1156
|
+
}
|
|
1157
|
+
var init_crypto = __esm({
|
|
1158
|
+
"src/crypto.ts"() {
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// src/redaction.ts
|
|
1163
|
+
function spansFrom(re, text) {
|
|
1164
|
+
const out = [];
|
|
1165
|
+
re.lastIndex = 0;
|
|
1166
|
+
let m = re.exec(text);
|
|
1167
|
+
while (m !== null) {
|
|
1168
|
+
out.push([m.index, m.index + m[0].length]);
|
|
1169
|
+
if (m[0].length === 0) re.lastIndex++;
|
|
1170
|
+
m = re.exec(text);
|
|
1171
|
+
}
|
|
1172
|
+
return out;
|
|
1173
|
+
}
|
|
1174
|
+
function luhnOk(digits) {
|
|
1175
|
+
let total = 0;
|
|
1176
|
+
const rev = [...digits].reverse();
|
|
1177
|
+
for (let i = 0; i < rev.length; i++) {
|
|
1178
|
+
let d = Number(rev[i]);
|
|
1179
|
+
if (i % 2 === 1) {
|
|
1180
|
+
d *= 2;
|
|
1181
|
+
if (d > 9) d -= 9;
|
|
1182
|
+
}
|
|
1183
|
+
total += d;
|
|
1184
|
+
}
|
|
1185
|
+
return total % 10 === 0;
|
|
1186
|
+
}
|
|
1187
|
+
function detectCreditCard(text) {
|
|
1188
|
+
const spans = [];
|
|
1189
|
+
for (const [start, end] of spansFrom(CARD_RE, text)) {
|
|
1190
|
+
const digits = text.slice(start, end).replace(/[ -]/g, "");
|
|
1191
|
+
if (digits.length >= 13 && digits.length <= 19 && luhnOk(digits)) {
|
|
1192
|
+
spans.push([start, end]);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return spans;
|
|
1196
|
+
}
|
|
1197
|
+
function looksLikeGenericSecret(token) {
|
|
1198
|
+
if (UUID_RE.test(token)) return false;
|
|
1199
|
+
let hasUpper = false;
|
|
1200
|
+
let hasLower = false;
|
|
1201
|
+
let hasDigit = false;
|
|
1202
|
+
for (const c of token) {
|
|
1203
|
+
if (c >= "A" && c <= "Z") hasUpper = true;
|
|
1204
|
+
else if (c >= "a" && c <= "z") hasLower = true;
|
|
1205
|
+
else if (c >= "0" && c <= "9") hasDigit = true;
|
|
1206
|
+
}
|
|
1207
|
+
return hasUpper && hasLower && hasDigit;
|
|
1208
|
+
}
|
|
1209
|
+
function detectApiKey(text) {
|
|
1210
|
+
const spans = spansFrom(API_KEY_PREFIX_RE, text);
|
|
1211
|
+
for (const [start, end] of spansFrom(API_KEY_GENERIC_RE, text)) {
|
|
1212
|
+
if (spans.some(([s, e]) => start < e && s < end)) continue;
|
|
1213
|
+
if (looksLikeGenericSecret(text.slice(start, end))) spans.push([start, end]);
|
|
1214
|
+
}
|
|
1215
|
+
return spans;
|
|
1216
|
+
}
|
|
1217
|
+
function detectBuiltin(name, text) {
|
|
1218
|
+
const detector = BUILTIN[name];
|
|
1219
|
+
if (!detector) throw new Error(`unknown built-in detector: ${name}`);
|
|
1220
|
+
if (!text) return [];
|
|
1221
|
+
return detector(text).sort((a, b) => a[0] - b[0]);
|
|
1222
|
+
}
|
|
1223
|
+
function ruleApplies(rule, opts) {
|
|
1224
|
+
if (rule.applyAt !== "both" && rule.applyAt !== opts.phase) return false;
|
|
1225
|
+
if (opts.phase === "ingest") return true;
|
|
1226
|
+
if (!rule.unless.length) return true;
|
|
1227
|
+
if (opts.principals === null) return false;
|
|
1228
|
+
const held = new Set(opts.principals);
|
|
1229
|
+
return !rule.unless.some((p) => held.has(p));
|
|
1230
|
+
}
|
|
1231
|
+
function spansForRule(rule, text, policy) {
|
|
1232
|
+
if (rule.pattern !== null) {
|
|
1233
|
+
const re = rule.patternRe() ?? new RegExp(rule.pattern, "g");
|
|
1234
|
+
return spansFrom(re, text);
|
|
1235
|
+
}
|
|
1236
|
+
if (rule.detector !== null) {
|
|
1237
|
+
const custom = policy.customDetectors[rule.detector];
|
|
1238
|
+
if (custom) return [...custom(text)];
|
|
1239
|
+
return detectBuiltin(rule.detector, text);
|
|
1240
|
+
}
|
|
1241
|
+
return [];
|
|
1242
|
+
}
|
|
1243
|
+
function hashToken(value, secretKey) {
|
|
1244
|
+
const key = Buffer.isBuffer(secretKey) ? secretKey : Buffer.from(String(secretKey ?? ""));
|
|
1245
|
+
return hmacSha256Hex(key, value).slice(0, HASH_TOKEN_CHARS);
|
|
1246
|
+
}
|
|
1247
|
+
function applyRedaction(text, policy, opts) {
|
|
1248
|
+
if (!text || policy.isEmpty()) return [text, {}];
|
|
1249
|
+
const principals = opts.principals ?? null;
|
|
1250
|
+
const collected = [];
|
|
1251
|
+
const fired = [];
|
|
1252
|
+
const failed = [];
|
|
1253
|
+
for (const rule of policy.rules) {
|
|
1254
|
+
if (!ruleApplies(rule, { phase: opts.phase, principals })) continue;
|
|
1255
|
+
if (rule.action === "hash" && !opts.secretKey) {
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`rule '${rule.name}': action='hash' requires a non-empty secretKey (an unkeyed HMAC is a reversible pseudonym, not a redaction)`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
try {
|
|
1261
|
+
const raw = spansForRule(rule, text, policy);
|
|
1262
|
+
const ruleSpans = [];
|
|
1263
|
+
let invalid = false;
|
|
1264
|
+
for (const item of raw) {
|
|
1265
|
+
const start = item?.[0];
|
|
1266
|
+
const end = item?.[1];
|
|
1267
|
+
if (typeof start !== "number" || typeof end !== "number") {
|
|
1268
|
+
invalid = true;
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (!(start >= 0 && start < end && end <= text.length)) {
|
|
1272
|
+
invalid = true;
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
ruleSpans.push([start, end]);
|
|
1276
|
+
}
|
|
1277
|
+
if (invalid) failed.push(rule.name);
|
|
1278
|
+
for (const [s, e] of ruleSpans) collected.push([s, e, rule]);
|
|
1279
|
+
} catch (exc) {
|
|
1280
|
+
failed.push(rule.name);
|
|
1281
|
+
if (opts.hooks) emitError(opts.hooks, exc, { stage: "redaction", rule: rule.name });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (!collected.length) {
|
|
1285
|
+
if (failed.length) return [text, { rules_fired: [], spans: 0, rules_failed: failed }];
|
|
1286
|
+
return [text, {}];
|
|
1287
|
+
}
|
|
1288
|
+
collected.sort((a, b) => a[0] - b[0] || b[1] - b[0] - (a[1] - a[0]));
|
|
1289
|
+
const merged = [];
|
|
1290
|
+
for (const [start, end, rule] of collected) {
|
|
1291
|
+
const last = merged[merged.length - 1];
|
|
1292
|
+
if (last && start < last[1]) {
|
|
1293
|
+
if (end > last[1]) last[1] = end;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
merged.push([start, end, rule]);
|
|
1297
|
+
}
|
|
1298
|
+
const out = [];
|
|
1299
|
+
let cursor = 0;
|
|
1300
|
+
for (const [start, end, rule] of merged) {
|
|
1301
|
+
out.push(text.slice(cursor, start));
|
|
1302
|
+
const original = text.slice(start, end);
|
|
1303
|
+
if (rule.action === "mask") out.push(rule.effectivePlaceholder());
|
|
1304
|
+
else if (rule.action === "hash") {
|
|
1305
|
+
out.push(`[${rule.name.toUpperCase()}:${hashToken(original, opts.secretKey)}]`);
|
|
1306
|
+
}
|
|
1307
|
+
if (!fired.includes(rule.name)) fired.push(rule.name);
|
|
1308
|
+
cursor = end;
|
|
1309
|
+
}
|
|
1310
|
+
out.push(text.slice(cursor));
|
|
1311
|
+
const note = { rules_fired: fired, spans: merged.length };
|
|
1312
|
+
if (failed.length) note.rules_failed = failed;
|
|
1313
|
+
return [out.join(""), note];
|
|
1314
|
+
}
|
|
1315
|
+
var BUILTIN_DETECTOR_NAMES; exports.RedactionRule = void 0; exports.RedactionPolicy = void 0; var 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;
|
|
1316
|
+
var init_redaction = __esm({
|
|
1317
|
+
"src/redaction.ts"() {
|
|
1318
|
+
init_crypto();
|
|
1319
|
+
init_hooks();
|
|
1320
|
+
BUILTIN_DETECTOR_NAMES = /* @__PURE__ */ new Set(["email", "phone", "ssn", "credit_card", "iban", "api_key"]);
|
|
1321
|
+
exports.RedactionRule = class {
|
|
1322
|
+
name;
|
|
1323
|
+
detector;
|
|
1324
|
+
pattern;
|
|
1325
|
+
field;
|
|
1326
|
+
action;
|
|
1327
|
+
placeholder;
|
|
1328
|
+
applyAt;
|
|
1329
|
+
unless;
|
|
1330
|
+
compiled = null;
|
|
1331
|
+
constructor(init) {
|
|
1332
|
+
this.name = init.name;
|
|
1333
|
+
this.detector = init.detector ?? null;
|
|
1334
|
+
this.pattern = init.pattern ?? null;
|
|
1335
|
+
this.field = init.field ?? null;
|
|
1336
|
+
this.action = init.action ?? "mask";
|
|
1337
|
+
this.placeholder = init.placeholder ?? null;
|
|
1338
|
+
this.applyAt = init.applyAt ?? "output";
|
|
1339
|
+
this.unless = init.unless ?? [];
|
|
1340
|
+
this.validate();
|
|
1341
|
+
}
|
|
1342
|
+
validate() {
|
|
1343
|
+
if (this.field !== null) {
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
`rule '${this.name}': field-targeted rules are not implemented yet; use detector or pattern instead`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
const targets = [this.detector, this.pattern].filter((t) => t !== null);
|
|
1349
|
+
if (targets.length !== 1) {
|
|
1350
|
+
throw new Error(`rule '${this.name}': exactly one of detector/pattern must be set`);
|
|
1351
|
+
}
|
|
1352
|
+
if (this.pattern !== null) {
|
|
1353
|
+
try {
|
|
1354
|
+
this.compiled = new RegExp(this.pattern, "g");
|
|
1355
|
+
} catch (exc) {
|
|
1356
|
+
throw new Error(`rule '${this.name}': invalid regex: ${exc}`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (this.unless.length && this.applyAt === "ingest") {
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
`rule '${this.name}': unless is output-time only and cannot be set on an applyAt='ingest' rule`
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
patternRe() {
|
|
1366
|
+
return this.compiled;
|
|
1367
|
+
}
|
|
1368
|
+
effectivePlaceholder() {
|
|
1369
|
+
return this.placeholder || `[${this.name.toUpperCase()}]`;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
exports.RedactionPolicy = class {
|
|
1373
|
+
rules;
|
|
1374
|
+
customDetectors;
|
|
1375
|
+
constructor(init = {}) {
|
|
1376
|
+
this.rules = (init.rules ?? []).map((r) => r instanceof exports.RedactionRule ? r : new exports.RedactionRule(r));
|
|
1377
|
+
this.customDetectors = init.customDetectors ?? {};
|
|
1378
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1379
|
+
for (const rule of this.rules) {
|
|
1380
|
+
if (seen.has(rule.name)) throw new Error(`duplicate rule name: '${rule.name}'`);
|
|
1381
|
+
seen.add(rule.name);
|
|
1382
|
+
if (rule.detector !== null) {
|
|
1383
|
+
const known = BUILTIN_DETECTOR_NAMES.has(rule.detector) || rule.detector in this.customDetectors;
|
|
1384
|
+
if (!known) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`rule '${rule.name}': unknown detector '${rule.detector}' (not built-in and not in customDetectors)`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
isEmpty() {
|
|
1393
|
+
return this.rules.length === 0;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
1397
|
+
PHONE_RE = /\+\d[\d\s-]{7,17}\d/g;
|
|
1398
|
+
SSN_RE = /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g;
|
|
1399
|
+
CARD_RE = /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g;
|
|
1400
|
+
IBAN_RE = /(?<![A-Za-z0-9])[A-Z]{2}\d{2}[A-Z0-9]{10,30}(?![A-Za-z0-9])/g;
|
|
1401
|
+
API_KEY_ALNUM = "A-Za-z0-9_\\-+/=";
|
|
1402
|
+
API_KEY_PREFIX_RE = new RegExp(
|
|
1403
|
+
`(?<![${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}])`,
|
|
1404
|
+
"g"
|
|
1405
|
+
);
|
|
1406
|
+
API_KEY_GENERIC_RE = new RegExp(
|
|
1407
|
+
`(?<![${API_KEY_ALNUM}])[${API_KEY_ALNUM}]{24,}(?![${API_KEY_ALNUM}])`,
|
|
1408
|
+
"g"
|
|
1409
|
+
);
|
|
1410
|
+
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}$/;
|
|
1411
|
+
BUILTIN = {
|
|
1412
|
+
email: (t) => spansFrom(EMAIL_RE, t),
|
|
1413
|
+
phone: (t) => spansFrom(PHONE_RE, t),
|
|
1414
|
+
ssn: (t) => spansFrom(SSN_RE, t),
|
|
1415
|
+
credit_card: detectCreditCard,
|
|
1416
|
+
iban: (t) => spansFrom(IBAN_RE, t),
|
|
1417
|
+
api_key: detectApiKey
|
|
1418
|
+
};
|
|
1419
|
+
HASH_TOKEN_CHARS = 16;
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1063
1422
|
function stripFences(text) {
|
|
1064
1423
|
let t = text.trim();
|
|
1065
1424
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(t);
|
|
@@ -1095,17 +1454,38 @@ var init_json = __esm({
|
|
|
1095
1454
|
"src/extraction/json.ts"() {
|
|
1096
1455
|
}
|
|
1097
1456
|
});
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
}
|
|
1107
|
-
function
|
|
1108
|
-
|
|
1457
|
+
|
|
1458
|
+
// src/extras.ts
|
|
1459
|
+
async function tryImport(specifier) {
|
|
1460
|
+
try {
|
|
1461
|
+
return await import(specifier);
|
|
1462
|
+
} catch {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async function requireExtra(specifier, extra, what) {
|
|
1467
|
+
try {
|
|
1468
|
+
return await import(specifier);
|
|
1469
|
+
} catch (_err) {
|
|
1470
|
+
throw new exports.ExtraMissingError(extra, specifier, what);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
var init_extras = __esm({
|
|
1474
|
+
"src/extras.ts"() {
|
|
1475
|
+
init_errors();
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
function getFileExtension(filename) {
|
|
1479
|
+
if (!filename?.includes(".")) return "";
|
|
1480
|
+
return filename.slice(filename.lastIndexOf(".")).toLowerCase();
|
|
1481
|
+
}
|
|
1482
|
+
function isNonIngestibleMedia(filename, mime) {
|
|
1483
|
+
const m = (mime || "").toLowerCase();
|
|
1484
|
+
if (m.startsWith("audio/") || m.startsWith("video/")) return true;
|
|
1485
|
+
return NON_INGESTIBLE_MEDIA_EXTS.has(getFileExtension(filename));
|
|
1486
|
+
}
|
|
1487
|
+
function isZip(content) {
|
|
1488
|
+
return content.length >= 4 && content.subarray(0, 4).equals(ZIP_MAGIC);
|
|
1109
1489
|
}
|
|
1110
1490
|
function decodeXmlEntities(s) {
|
|
1111
1491
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
@@ -1485,25 +1865,182 @@ var init_files = __esm({
|
|
|
1485
1865
|
XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
1486
1866
|
}
|
|
1487
1867
|
});
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1868
|
+
function camelize(key) {
|
|
1869
|
+
return key.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1870
|
+
}
|
|
1871
|
+
function loadCeEnv() {
|
|
1872
|
+
const root = {};
|
|
1873
|
+
for (const [raw, value] of Object.entries(process.env)) {
|
|
1874
|
+
if (!raw.startsWith("CE_") || value === void 0) continue;
|
|
1875
|
+
const path = raw.slice(3).split("__").map(camelize);
|
|
1876
|
+
let cur = root;
|
|
1877
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1878
|
+
const k = path[i];
|
|
1879
|
+
const next = cur[k];
|
|
1880
|
+
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
1881
|
+
cur = cur[k];
|
|
1882
|
+
}
|
|
1883
|
+
cur[path[path.length - 1]] = coerceEnv(value);
|
|
1495
1884
|
}
|
|
1885
|
+
return root;
|
|
1496
1886
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1887
|
+
function coerceEnv(value) {
|
|
1888
|
+
if (value === "true") return true;
|
|
1889
|
+
if (value === "false") return false;
|
|
1890
|
+
if (/^-?\d+$/.test(value)) return Number(value);
|
|
1891
|
+
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
|
|
1892
|
+
return value;
|
|
1893
|
+
}
|
|
1894
|
+
function deepMerge(a, b) {
|
|
1895
|
+
const out = { ...a };
|
|
1896
|
+
for (const [k, v] of Object.entries(b)) {
|
|
1897
|
+
if (v === void 0) continue;
|
|
1898
|
+
const existing = out[k];
|
|
1899
|
+
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
1900
|
+
out[k] = deepMerge(existing, v);
|
|
1901
|
+
} else {
|
|
1902
|
+
out[k] = v;
|
|
1903
|
+
}
|
|
1502
1904
|
}
|
|
1905
|
+
return out;
|
|
1503
1906
|
}
|
|
1504
|
-
var
|
|
1505
|
-
|
|
1506
|
-
|
|
1907
|
+
var embeddingSchema, llmSchema, graphSchema, rerankerSchema, fusionSchema, storageSchema, extractionSchema; exports.ContextEngineConfig = void 0;
|
|
1908
|
+
var init_config = __esm({
|
|
1909
|
+
"src/config.ts"() {
|
|
1910
|
+
init_redaction();
|
|
1911
|
+
embeddingSchema = zod.z.object({
|
|
1912
|
+
provider: zod.z.enum(["openai", "azure_openai", "gemini", "voyage", "cohere", "custom"]),
|
|
1913
|
+
model: zod.z.string(),
|
|
1914
|
+
dim: zod.z.number().int().positive().nullable().optional().default(null),
|
|
1915
|
+
apiKey: zod.z.string().nullable().optional().default(null),
|
|
1916
|
+
baseUrl: zod.z.string().nullable().optional().default(null)
|
|
1917
|
+
});
|
|
1918
|
+
llmSchema = zod.z.object({
|
|
1919
|
+
provider: zod.z.enum(["anthropic", "openai", "azure_openai", "gemini", "bedrock", "custom"]),
|
|
1920
|
+
model: zod.z.string(),
|
|
1921
|
+
apiKey: zod.z.string().nullable().optional().default(null),
|
|
1922
|
+
baseUrl: zod.z.string().nullable().optional().default(null)
|
|
1923
|
+
});
|
|
1924
|
+
graphSchema = zod.z.object({
|
|
1925
|
+
enabled: zod.z.boolean().default(false),
|
|
1926
|
+
neo4jUri: zod.z.string().nullable().optional().default(null),
|
|
1927
|
+
neo4jUser: zod.z.string().default("neo4j"),
|
|
1928
|
+
neo4jPassword: zod.z.string().nullable().optional().default(null),
|
|
1929
|
+
neo4jDatabase: zod.z.string().default("neo4j"),
|
|
1930
|
+
extractionLlm: llmSchema.nullable().optional().default(null)
|
|
1931
|
+
});
|
|
1932
|
+
rerankerSchema = zod.z.object({
|
|
1933
|
+
enabled: zod.z.boolean().default(false),
|
|
1934
|
+
provider: zod.z.enum(["cohere", "voyage", "jina", "custom"]).nullable().optional().default(null),
|
|
1935
|
+
model: zod.z.string().nullable().optional().default(null),
|
|
1936
|
+
apiKey: zod.z.string().nullable().optional().default(null),
|
|
1937
|
+
baseUrl: zod.z.string().nullable().optional().default(null),
|
|
1938
|
+
candidates: zod.z.number().int().positive().default(50)
|
|
1939
|
+
});
|
|
1940
|
+
fusionSchema = zod.z.object({
|
|
1941
|
+
method: zod.z.literal("rrf").default("rrf"),
|
|
1942
|
+
k: zod.z.number().int().positive().default(60),
|
|
1943
|
+
weights: zod.z.record(zod.z.string(), zod.z.number()).default({ fts: 1, trgm: 0.8, ann: 1, graph: 1 })
|
|
1944
|
+
});
|
|
1945
|
+
storageSchema = zod.z.object({
|
|
1946
|
+
backend: zod.z.literal("postgres").default("postgres"),
|
|
1947
|
+
annExactThreshold: zod.z.number().int().positive().default(5e4),
|
|
1948
|
+
// Connection pool, passed straight through to `pg.Pool`. Sizing is a
|
|
1949
|
+
// deployment decision (how many workers times how many concurrent searches
|
|
1950
|
+
// each runs, against what the database allows), so these are pg's own
|
|
1951
|
+
// defaults rather than a guess at your topology. (`pg` has no pre-ping.)
|
|
1952
|
+
//
|
|
1953
|
+
// Connections kept open in the pool.
|
|
1954
|
+
poolMax: zod.z.number().int().positive().default(10),
|
|
1955
|
+
// Milliseconds an idle connection is kept before it is closed.
|
|
1956
|
+
poolIdleTimeoutMs: zod.z.number().int().nonnegative().default(1e4),
|
|
1957
|
+
// Milliseconds a caller waits for a connection before the attempt fails.
|
|
1958
|
+
poolConnectionTimeoutMs: zod.z.number().int().nonnegative().default(3e4)
|
|
1959
|
+
});
|
|
1960
|
+
extractionSchema = zod.z.object({
|
|
1961
|
+
// Ceiling on an image's LONG EDGE in pixels, for rendered PDF pages and
|
|
1962
|
+
// for images sent to the vision LLM.
|
|
1963
|
+
maxRenderPx: zod.z.number().int().positive().default(2e3),
|
|
1964
|
+
// Ceiling on ONE vision batch's transcription, in output tokens — a reply
|
|
1965
|
+
// far past a batch's honest bound is a repetition loop. Raising it is the
|
|
1966
|
+
// fix if very dense pages come back truncated.
|
|
1967
|
+
visionMaxOutputTokens: zod.z.number().int().positive().default(16e3),
|
|
1968
|
+
// Pages per vision-LLM call. Smaller batches mean more calls but more
|
|
1969
|
+
// concurrency and less risk of hitting the output ceiling or a provider
|
|
1970
|
+
// deadline; larger batches the reverse. Measure on your own corpus.
|
|
1971
|
+
pagesPerVisionBatch: zod.z.number().int().positive().default(5),
|
|
1972
|
+
// Language hint for the Tesseract OCR fallback ('en', 'ur', 'mixed', any
|
|
1973
|
+
// Tesseract code, or 'auto' for script detection). null (the default)
|
|
1974
|
+
// detects the document's language from its usable text-layer pages and
|
|
1975
|
+
// falls back to 'auto' when there is nothing to sample.
|
|
1976
|
+
ocrLanguage: zod.z.string().nullable().default(null)
|
|
1977
|
+
});
|
|
1978
|
+
exports.ContextEngineConfig = class _ContextEngineConfig {
|
|
1979
|
+
databaseUrl;
|
|
1980
|
+
storage;
|
|
1981
|
+
defaultMode;
|
|
1982
|
+
embedding;
|
|
1983
|
+
llm;
|
|
1984
|
+
visionLlm;
|
|
1985
|
+
graph;
|
|
1986
|
+
reranker;
|
|
1987
|
+
fusion;
|
|
1988
|
+
extraction;
|
|
1989
|
+
enableCodeExecution;
|
|
1990
|
+
secretKey;
|
|
1991
|
+
/**
|
|
1992
|
+
* Lets http tools and probes reach loopback/private/link-local/metadata
|
|
1993
|
+
* addresses. Off by default — an http tool is registered by a caller, and a
|
|
1994
|
+
* private destination is server-side request forgery. See `tools/egress.ts`.
|
|
1995
|
+
*/
|
|
1996
|
+
allowPrivateEgress;
|
|
1997
|
+
redaction;
|
|
1998
|
+
constructor(init) {
|
|
1999
|
+
this.databaseUrl = init.databaseUrl;
|
|
2000
|
+
this.storage = storageSchema.parse(init.storage ?? {});
|
|
2001
|
+
this.defaultMode = init.defaultMode ?? "hybrid";
|
|
2002
|
+
this.embedding = embeddingSchema.parse(init.embedding);
|
|
2003
|
+
this.llm = init.llm ? llmSchema.parse(init.llm) : null;
|
|
2004
|
+
this.visionLlm = init.visionLlm ? llmSchema.parse(init.visionLlm) : null;
|
|
2005
|
+
this.graph = graphSchema.parse(init.graph ?? {});
|
|
2006
|
+
this.reranker = rerankerSchema.parse(init.reranker ?? {});
|
|
2007
|
+
this.fusion = fusionSchema.parse(init.fusion ?? {});
|
|
2008
|
+
this.extraction = extractionSchema.parse(init.extraction ?? {});
|
|
2009
|
+
this.enableCodeExecution = init.enableCodeExecution ?? false;
|
|
2010
|
+
this.secretKey = init.secretKey ?? null;
|
|
2011
|
+
this.allowPrivateEgress = init.allowPrivateEgress ?? false;
|
|
2012
|
+
this.redaction = init.redaction instanceof exports.RedactionPolicy ? init.redaction : new exports.RedactionPolicy(init.redaction ?? {});
|
|
2013
|
+
this.validate();
|
|
2014
|
+
}
|
|
2015
|
+
validate() {
|
|
2016
|
+
if (this.graph.enabled && !(this.graph.neo4jUri && this.graph.neo4jPassword && this.graph.extractionLlm)) {
|
|
2017
|
+
throw new Error("graph enabled but neo4jUri/neo4jPassword/extractionLlm missing");
|
|
2018
|
+
}
|
|
2019
|
+
if (this.reranker.enabled && !(this.reranker.provider && this.reranker.apiKey)) {
|
|
2020
|
+
throw new Error("reranker enabled but provider/apiKey missing");
|
|
2021
|
+
}
|
|
2022
|
+
if (this.defaultMode === "graph" && !this.graph.enabled) {
|
|
2023
|
+
throw new Error("defaultMode is 'graph' but graph enabled is false");
|
|
2024
|
+
}
|
|
2025
|
+
for (const rule of this.redaction.rules) {
|
|
2026
|
+
if (rule.action === "hash" && !this.secretKey) {
|
|
2027
|
+
throw new Error(
|
|
2028
|
+
`redaction rule '${rule.name}': action='hash' requires ContextEngineConfig.secretKey to be set`
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
static fromEnv(overrides = {}) {
|
|
2034
|
+
const env = loadCeEnv();
|
|
2035
|
+
const merged = deepMerge(env, overrides);
|
|
2036
|
+
if (!merged.databaseUrl || !merged.embedding) {
|
|
2037
|
+
throw new Error(
|
|
2038
|
+
"ContextEngineConfig.fromEnv requires CE_DATABASE_URL and CE_EMBEDDING__PROVIDER/MODEL (or explicit overrides)"
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
return new _ContextEngineConfig(merged);
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
1507
2044
|
}
|
|
1508
2045
|
});
|
|
1509
2046
|
|
|
@@ -1932,8 +2469,26 @@ __export(pdf_exports, {
|
|
|
1932
2469
|
embeddedTextPerPage: () => embeddedTextPerPage,
|
|
1933
2470
|
extract: () => extract,
|
|
1934
2471
|
extractPdf: () => extract,
|
|
2472
|
+
ocrLanguageHint: () => ocrLanguageHint,
|
|
1935
2473
|
pageCount: () => pageCount
|
|
1936
2474
|
});
|
|
2475
|
+
function ocrLanguageHint(texts, override) {
|
|
2476
|
+
if (override) return override;
|
|
2477
|
+
let sample = "";
|
|
2478
|
+
const indices = [...texts.keys()].sort((a, b) => a - b).slice(0, 3);
|
|
2479
|
+
for (const idx of indices) {
|
|
2480
|
+
sample += texts.get(idx) ?? "";
|
|
2481
|
+
if (sample.length >= OCR_LANG_SAMPLE_MIN) break;
|
|
2482
|
+
}
|
|
2483
|
+
if (sample.length >= OCR_LANG_SAMPLE_MIN) {
|
|
2484
|
+
try {
|
|
2485
|
+
const detected = detectLanguage(sample, 30);
|
|
2486
|
+
if (detected) return detected;
|
|
2487
|
+
} catch {
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return "auto";
|
|
2491
|
+
}
|
|
1937
2492
|
function isCcControl(ch) {
|
|
1938
2493
|
if (ch === "\n" || ch === "\r" || ch === " ") return false;
|
|
1939
2494
|
const cp = ch.codePointAt(0);
|
|
@@ -1986,16 +2541,27 @@ async function extract(content, opts) {
|
|
|
1986
2541
|
const { pages, structured: structuredMarkdown } = await classifyPages(content);
|
|
1987
2542
|
const providerTokens = {};
|
|
1988
2543
|
let visionMarkdown = false;
|
|
2544
|
+
let benignBlank = /* @__PURE__ */ new Set();
|
|
1989
2545
|
if (pages.needsOcr.length) {
|
|
1990
2546
|
if (opts.visionLlm) {
|
|
1991
2547
|
try {
|
|
1992
2548
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
1993
|
-
const [visionTexts, tokens] = await vision.extractPdfPages(content, pages.needsOcr, {
|
|
1994
|
-
visionLlm: opts.visionLlm
|
|
2549
|
+
const [visionTexts, tokens, outcomes] = await vision.extractPdfPages(content, pages.needsOcr, {
|
|
2550
|
+
visionLlm: opts.visionLlm,
|
|
2551
|
+
extraction: opts.extraction
|
|
1995
2552
|
});
|
|
1996
2553
|
for (const [idx, text] of Object.entries(visionTexts)) {
|
|
1997
2554
|
pages.texts.set(Number(idx), text);
|
|
1998
2555
|
}
|
|
2556
|
+
benignBlank = new Set(
|
|
2557
|
+
Object.entries(outcomes).filter(([, o]) => o.status === "blank").map(([idx]) => Number(idx))
|
|
2558
|
+
);
|
|
2559
|
+
const failed = Object.entries(outcomes).filter(([, o]) => o.status === "failed");
|
|
2560
|
+
if (failed.length) {
|
|
2561
|
+
console.warn(
|
|
2562
|
+
`[pdf] vision failed on ${failed.length} of ${pages.needsOcr.length} pages: ` + failed.map(([idx, o]) => `page ${Number(idx) + 1}: ${o.error}`).join("; ")
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
1999
2565
|
visionMarkdown = Object.keys(visionTexts).length > 0;
|
|
2000
2566
|
for (const [key, val] of Object.entries(tokens)) {
|
|
2001
2567
|
providerTokens[key] = (providerTokens[key] ?? 0) + val;
|
|
@@ -2007,9 +2573,17 @@ async function extract(content, opts) {
|
|
|
2007
2573
|
try {
|
|
2008
2574
|
const ocr = await Promise.resolve().then(() => (init_ocr(), ocr_exports));
|
|
2009
2575
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2010
|
-
const rendered = await vision.renderPdfPagesAsImages(
|
|
2576
|
+
const rendered = await vision.renderPdfPagesAsImages(
|
|
2577
|
+
content,
|
|
2578
|
+
pages.needsOcr,
|
|
2579
|
+
150,
|
|
2580
|
+
opts.extraction?.maxRenderPx ?? vision.MAX_RENDER_PX
|
|
2581
|
+
);
|
|
2582
|
+
const ocrConfig = {
|
|
2583
|
+
language: ocrLanguageHint(pages.texts, opts.extraction?.ocrLanguage)
|
|
2584
|
+
};
|
|
2011
2585
|
for (const [idx, pngBytes] of rendered) {
|
|
2012
|
-
const result = await ocr.extractImageTextOcr(pngBytes);
|
|
2586
|
+
const result = await ocr.extractImageTextOcr(pngBytes, ocrConfig);
|
|
2013
2587
|
if (result.text.trim()) pages.texts.set(idx, result.text.trim());
|
|
2014
2588
|
}
|
|
2015
2589
|
} catch (exc) {
|
|
@@ -2027,31 +2601,38 @@ async function extract(content, opts) {
|
|
|
2027
2601
|
if (t) textParts.push(`--- Page ${i + 1} ---
|
|
2028
2602
|
${t}`);
|
|
2029
2603
|
}
|
|
2604
|
+
const unread = pages.needsOcr.filter((i) => !pages.texts.get(i) && !benignBlank.has(i));
|
|
2030
2605
|
return new Extracted2({
|
|
2031
2606
|
text: textParts.join("\n\n"),
|
|
2032
2607
|
pages: pages.total,
|
|
2033
2608
|
slides: null,
|
|
2034
2609
|
mediaOnly: false,
|
|
2035
2610
|
providerTokens,
|
|
2036
|
-
isMarkdown: visionMarkdown || structuredMarkdown
|
|
2611
|
+
isMarkdown: visionMarkdown || structuredMarkdown,
|
|
2612
|
+
unreadableReason: unread.length ? opts.visionLlm ? "vision_failed" : "needs_vision" : null,
|
|
2613
|
+
unreadablePages: unread.length
|
|
2037
2614
|
});
|
|
2038
2615
|
}
|
|
2039
|
-
var GARBAGE_SCAN_LIMIT, MIN_USABLE_TEXT_LEN;
|
|
2616
|
+
var GARBAGE_SCAN_LIMIT, MIN_USABLE_TEXT_LEN, OCR_LANG_SAMPLE_MIN;
|
|
2040
2617
|
var init_pdf = __esm({
|
|
2041
2618
|
"src/extraction/pdf.ts"() {
|
|
2042
2619
|
init_errors();
|
|
2043
2620
|
init_hooks();
|
|
2621
|
+
init_text();
|
|
2044
2622
|
init_pdf_structure();
|
|
2045
2623
|
GARBAGE_SCAN_LIMIT = 2e4;
|
|
2046
2624
|
MIN_USABLE_TEXT_LEN = 50;
|
|
2625
|
+
OCR_LANG_SAMPLE_MIN = 100;
|
|
2047
2626
|
}
|
|
2048
2627
|
});
|
|
2049
2628
|
|
|
2050
2629
|
// src/extraction/vision.ts
|
|
2051
2630
|
var vision_exports = {};
|
|
2052
2631
|
__export(vision_exports, {
|
|
2632
|
+
MAX_RENDER_PX: () => MAX_RENDER_PX,
|
|
2053
2633
|
PAGES_PER_VISION_BATCH: () => PAGES_PER_VISION_BATCH,
|
|
2054
2634
|
VISION_BATCH_ATTEMPTS: () => VISION_BATCH_ATTEMPTS,
|
|
2635
|
+
VISION_MAX_OUTPUT_TOKENS: () => VISION_MAX_OUTPUT_TOKENS,
|
|
2055
2636
|
assignBatchPages: () => assignBatchPages,
|
|
2056
2637
|
coercePagesResult: () => coercePagesResult,
|
|
2057
2638
|
detectPdfVisionPages: () => detectPdfVisionPages,
|
|
@@ -2068,6 +2649,17 @@ function pagesPrompt(nImages) {
|
|
|
2068
2649
|
Return JSON: {"pages": [{"page": 1, "text": "<markdown for this page>"}]}
|
|
2069
2650
|
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.`;
|
|
2070
2651
|
}
|
|
2652
|
+
function asText(value) {
|
|
2653
|
+
if (typeof value === "string") return value;
|
|
2654
|
+
if (value == null || typeof value === "boolean") return "";
|
|
2655
|
+
if (Array.isArray(value)) {
|
|
2656
|
+
return value.filter((v) => v != null && v !== "").map(asText).join("\n");
|
|
2657
|
+
}
|
|
2658
|
+
if (typeof value === "object") {
|
|
2659
|
+
return Object.values(value).filter((v) => v != null && v !== "").map(asText).join("\n");
|
|
2660
|
+
}
|
|
2661
|
+
return String(value);
|
|
2662
|
+
}
|
|
2071
2663
|
function coercePagesResult(parsed) {
|
|
2072
2664
|
let base;
|
|
2073
2665
|
let rawPages;
|
|
@@ -2087,7 +2679,7 @@ function coercePagesResult(parsed) {
|
|
|
2087
2679
|
const rec = entry;
|
|
2088
2680
|
pages.push({
|
|
2089
2681
|
page: typeof rec.page === "number" ? rec.page : i + 1,
|
|
2090
|
-
text:
|
|
2682
|
+
text: asText(rec.text)
|
|
2091
2683
|
});
|
|
2092
2684
|
} else if (typeof entry === "string") {
|
|
2093
2685
|
pages.push({ page: i + 1, text: entry });
|
|
@@ -2118,16 +2710,28 @@ function parsePagesResponse(rawText) {
|
|
|
2118
2710
|
function sleep(ms) {
|
|
2119
2711
|
return new Promise((r) => setTimeout(r, ms));
|
|
2120
2712
|
}
|
|
2121
|
-
async function
|
|
2122
|
-
if (!
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2713
|
+
async function capped(image, canvas, maxPx) {
|
|
2714
|
+
if (!canvas) return image;
|
|
2715
|
+
try {
|
|
2716
|
+
const img = await canvas.loadImage(image);
|
|
2717
|
+
const longEdge = Math.max(img.width, img.height);
|
|
2718
|
+
if (longEdge <= maxPx) return image;
|
|
2719
|
+
const ratio = maxPx / longEdge;
|
|
2720
|
+
const w = Math.max(Math.round(img.width * ratio), 1);
|
|
2721
|
+
const h = Math.max(Math.round(img.height * ratio), 1);
|
|
2722
|
+
const out = canvas.createCanvas(w, h);
|
|
2723
|
+
out.getContext("2d").drawImage(img, 0, 0, w, h);
|
|
2724
|
+
return out.toBuffer("image/png");
|
|
2725
|
+
} catch {
|
|
2726
|
+
return image;
|
|
2127
2727
|
}
|
|
2128
|
-
|
|
2728
|
+
}
|
|
2729
|
+
async function runBatches(batchSource, opts) {
|
|
2730
|
+
const started = Date.now();
|
|
2731
|
+
const results = /* @__PURE__ */ new Map();
|
|
2732
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
2129
2733
|
const tokensTotal = { input: 0, output: 0 };
|
|
2130
|
-
const
|
|
2734
|
+
const maxConcurrent = opts.maxConcurrent ?? 8;
|
|
2131
2735
|
let lock = Promise.resolve();
|
|
2132
2736
|
const withLock = async (fn) => {
|
|
2133
2737
|
const prev = lock;
|
|
@@ -2143,66 +2747,183 @@ async function extractTextFromImages(images, opts) {
|
|
|
2143
2747
|
}
|
|
2144
2748
|
};
|
|
2145
2749
|
const client = buildLlmClient(opts.visionLlm);
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2750
|
+
let total = 0;
|
|
2751
|
+
let nBatches = 0;
|
|
2752
|
+
const queue = new BoundedQueue(maxConcurrent);
|
|
2753
|
+
const processBatch = async (batchNum, baseIdx, batchImages) => {
|
|
2754
|
+
const batchStarted = Date.now();
|
|
2755
|
+
let pendingLocal = batchImages.map((_, i) => i);
|
|
2756
|
+
for (let attempt = 1; attempt <= VISION_BATCH_ATTEMPTS; attempt++) {
|
|
2757
|
+
const ask = pendingLocal.map((i) => batchImages[i]);
|
|
2758
|
+
try {
|
|
2759
|
+
const [rawText, usage] = await callLlm(opts.visionLlm, {
|
|
2760
|
+
system: VISION_SYSTEM_PROMPT,
|
|
2761
|
+
user: pagesPrompt(ask.length),
|
|
2762
|
+
jsonMode: true,
|
|
2763
|
+
images: ask,
|
|
2764
|
+
maxTokens: opts.extCfg.visionMaxOutputTokens,
|
|
2765
|
+
// Transcription is not reasoning, and on a thinking model the two
|
|
2766
|
+
// compete for the SAME budget — see `LlmCallOpts.thinkingBudget`.
|
|
2767
|
+
// Zero, not small: there is nothing here to reason ABOUT, the model
|
|
2768
|
+
// is being asked what is on the page. `temperature: 0` for the same
|
|
2769
|
+
// reason — one scan must transcribe to the same text twice.
|
|
2770
|
+
thinkingBudget: 0,
|
|
2771
|
+
temperature: 0,
|
|
2772
|
+
client
|
|
2773
|
+
});
|
|
2774
|
+
if (!rawText?.trim()) throw new Error("empty vision response");
|
|
2775
|
+
const [, pages] = parsePagesResponse(rawText);
|
|
2776
|
+
const assigned = assignBatchPages(pages, ask.length);
|
|
2777
|
+
const batchSeconds = (Date.now() - batchStarted) / 1e3;
|
|
2778
|
+
const stillMissing = [];
|
|
2779
|
+
await withLock(() => {
|
|
2780
|
+
tokensTotal.input += usage.input ?? 0;
|
|
2781
|
+
tokensTotal.output += usage.output ?? 0;
|
|
2782
|
+
pendingLocal.forEach((localIdx, position) => {
|
|
2783
|
+
const pageText = assigned[position];
|
|
2784
|
+
if (pageText === void 0) {
|
|
2785
|
+
stillMissing.push(localIdx);
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
results.set(baseIdx + localIdx, pageText);
|
|
2789
|
+
outcomes.set(baseIdx + localIdx, {
|
|
2790
|
+
status: pageText.trim() ? "read" : "blank",
|
|
2791
|
+
attempts: attempt,
|
|
2792
|
+
error: null,
|
|
2793
|
+
seconds: batchSeconds
|
|
2794
|
+
});
|
|
2159
2795
|
});
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2796
|
+
});
|
|
2797
|
+
pendingLocal = stillMissing;
|
|
2798
|
+
if (pendingLocal.length === 0) return;
|
|
2799
|
+
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
2800
|
+
console.warn(
|
|
2801
|
+
`[vision] batch ${batchNum} attempt ${attempt} answered ${ask.length - pendingLocal.length} of ${ask.length} pages \u2014 asking again for ${pendingLocal.length}`
|
|
2802
|
+
);
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
console.error(
|
|
2806
|
+
`[vision] batch ${batchNum}: ${pendingLocal.length} page(s) never came back after ${attempt} attempts`
|
|
2807
|
+
);
|
|
2808
|
+
await withLock(() => {
|
|
2809
|
+
for (const localIdx of pendingLocal) {
|
|
2810
|
+
outcomes.set(baseIdx + localIdx, {
|
|
2811
|
+
status: "failed",
|
|
2812
|
+
attempts: attempt,
|
|
2813
|
+
error: "page missing from batch response",
|
|
2814
|
+
seconds: batchSeconds
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
return;
|
|
2819
|
+
} catch (exc) {
|
|
2820
|
+
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
2821
|
+
console.warn(`[vision] batch ${batchNum} attempt ${attempt} failed: ${exc} \u2014 retrying`);
|
|
2822
|
+
await sleep(2e3 * attempt);
|
|
2823
|
+
} else {
|
|
2824
|
+
console.error(`[vision] batch ${batchNum} failed after ${VISION_BATCH_ATTEMPTS} attempts: ${exc}`);
|
|
2825
|
+
const batchSeconds = (Date.now() - batchStarted) / 1e3;
|
|
2163
2826
|
await withLock(() => {
|
|
2164
|
-
for (const
|
|
2165
|
-
|
|
2166
|
-
|
|
2827
|
+
for (const localIdx of pendingLocal) {
|
|
2828
|
+
outcomes.set(baseIdx + localIdx, {
|
|
2829
|
+
status: "failed",
|
|
2830
|
+
attempts: attempt,
|
|
2831
|
+
error: String(exc),
|
|
2832
|
+
seconds: batchSeconds
|
|
2833
|
+
});
|
|
2167
2834
|
}
|
|
2168
|
-
tokensTotal.input += usage.input ?? 0;
|
|
2169
|
-
tokensTotal.output += usage.output ?? 0;
|
|
2170
2835
|
});
|
|
2171
|
-
return;
|
|
2172
|
-
} catch (exc) {
|
|
2173
|
-
if (attempt < VISION_BATCH_ATTEMPTS) {
|
|
2174
|
-
console.warn(`[vision] batch ${batchIdx} attempt ${attempt} failed: ${exc} \u2014 retrying`);
|
|
2175
|
-
await sleep(2e3 * attempt);
|
|
2176
|
-
} else {
|
|
2177
|
-
console.error(
|
|
2178
|
-
`[vision] batch ${batchIdx} failed after ${VISION_BATCH_ATTEMPTS} attempts: ${exc}`
|
|
2179
|
-
);
|
|
2180
|
-
}
|
|
2181
2836
|
}
|
|
2182
2837
|
}
|
|
2183
|
-
} finally {
|
|
2184
|
-
semaphore.release();
|
|
2185
2838
|
}
|
|
2186
2839
|
};
|
|
2840
|
+
const worker = async () => {
|
|
2841
|
+
for (; ; ) {
|
|
2842
|
+
const item = await queue.get();
|
|
2843
|
+
if (item === null) return;
|
|
2844
|
+
await processBatch(...item);
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const workers = Array.from({ length: maxConcurrent }, () => worker());
|
|
2187
2848
|
try {
|
|
2188
|
-
await
|
|
2849
|
+
for await (const [baseIdx, batchImages] of batchSource) {
|
|
2850
|
+
if (!batchImages.length) continue;
|
|
2851
|
+
for (let j = 0; j < batchImages.length; j++) {
|
|
2852
|
+
outcomes.set(baseIdx + j, { status: "failed", attempts: 0, error: "never attempted", seconds: 0 });
|
|
2853
|
+
}
|
|
2854
|
+
total = Math.max(total, baseIdx + batchImages.length);
|
|
2855
|
+
await queue.put([nBatches, baseIdx, batchImages]);
|
|
2856
|
+
nBatches += 1;
|
|
2857
|
+
}
|
|
2858
|
+
queue.close();
|
|
2859
|
+
await Promise.all(workers);
|
|
2860
|
+
} catch (exc) {
|
|
2861
|
+
queue.abort();
|
|
2862
|
+
await Promise.allSettled(workers);
|
|
2863
|
+
throw exc;
|
|
2189
2864
|
} finally {
|
|
2190
2865
|
await client.aclose();
|
|
2191
2866
|
}
|
|
2192
|
-
|
|
2867
|
+
const texts = Array.from({ length: total }, (_, i) => results.get(i) ?? "");
|
|
2868
|
+
const outcomeList = Array.from(
|
|
2869
|
+
{ length: total },
|
|
2870
|
+
(_, i) => outcomes.get(i) ?? { status: "failed", attempts: 0, error: "never attempted", seconds: 0 }
|
|
2871
|
+
);
|
|
2872
|
+
const counts = { read: 0, blank: 0, failed: 0 };
|
|
2873
|
+
for (const o of outcomeList) counts[o.status] += 1;
|
|
2874
|
+
console.info(
|
|
2875
|
+
`[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`
|
|
2876
|
+
);
|
|
2877
|
+
return [texts, tokensTotal, outcomeList];
|
|
2878
|
+
}
|
|
2879
|
+
async function extractTextFromImages(images, opts) {
|
|
2880
|
+
if (!images.length) return [[], {}, []];
|
|
2881
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
2882
|
+
const canvasModule = await tryImport("@napi-rs/canvas");
|
|
2883
|
+
const sendable = await Promise.all(images.map((img) => capped(img, canvasModule, extCfg.maxRenderPx)));
|
|
2884
|
+
const batchSize = extCfg.pagesPerVisionBatch ?? PAGES_PER_VISION_BATCH;
|
|
2885
|
+
async function* batches() {
|
|
2886
|
+
for (let i = 0; i < sendable.length; i += batchSize) {
|
|
2887
|
+
yield [i, sendable.slice(i, i + batchSize)];
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return runBatches(batches(), {
|
|
2891
|
+
visionLlm: opts.visionLlm,
|
|
2892
|
+
extCfg,
|
|
2893
|
+
maxConcurrent: opts.maxConcurrent
|
|
2894
|
+
});
|
|
2193
2895
|
}
|
|
2194
2896
|
async function extractPdfPages(content, pageIndices, opts) {
|
|
2195
|
-
const
|
|
2196
|
-
const
|
|
2197
|
-
const
|
|
2897
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
2898
|
+
const batchSize = extCfg.pagesPerVisionBatch ?? PAGES_PER_VISION_BATCH;
|
|
2899
|
+
const pageOrder = [];
|
|
2900
|
+
async function* renderedBatches() {
|
|
2901
|
+
let pos = 0;
|
|
2902
|
+
for (let start = 0; start < pageIndices.length; start += batchSize) {
|
|
2903
|
+
const group = pageIndices.slice(start, start + batchSize);
|
|
2904
|
+
const rendered = await renderPdfPagesAsImages(content, group, 150, extCfg.maxRenderPx);
|
|
2905
|
+
if (!rendered.length) continue;
|
|
2906
|
+
pageOrder.push(...rendered.map(([idx]) => idx));
|
|
2907
|
+
yield [pos, rendered.map(([, png]) => png)];
|
|
2908
|
+
pos += rendered.length;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const [texts, tokens, outcomes] = await runBatches(renderedBatches(), {
|
|
2912
|
+
visionLlm: opts.visionLlm,
|
|
2913
|
+
extCfg
|
|
2914
|
+
});
|
|
2198
2915
|
const pageTexts = {};
|
|
2199
|
-
|
|
2916
|
+
const pageOutcomes = {};
|
|
2917
|
+
for (let i = 0; i < pageOrder.length; i++) {
|
|
2200
2918
|
const text = texts[i];
|
|
2201
|
-
if (text?.trim()) pageTexts[
|
|
2919
|
+
if (text?.trim()) pageTexts[pageOrder[i]] = text;
|
|
2920
|
+
const outcome = outcomes[i];
|
|
2921
|
+
if (outcome) pageOutcomes[pageOrder[i]] = outcome;
|
|
2202
2922
|
}
|
|
2203
|
-
return [pageTexts, tokens];
|
|
2923
|
+
return [pageTexts, tokens, pageOutcomes];
|
|
2204
2924
|
}
|
|
2205
|
-
async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
2925
|
+
async function renderPdfPagesAsImages(content, pageIndices, dpi = 150, maxPx = MAX_RENDER_PX) {
|
|
2926
|
+
const started = Date.now();
|
|
2206
2927
|
const scale = dpi / 72;
|
|
2207
2928
|
const data = new Uint8Array(content);
|
|
2208
2929
|
const pdf = await unpdf.getDocumentProxy(data);
|
|
@@ -2211,9 +2932,12 @@ async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
|
2211
2932
|
for (const idx of pageIndices) {
|
|
2212
2933
|
if (idx < 0 || idx >= pdf.numPages) continue;
|
|
2213
2934
|
try {
|
|
2935
|
+
const page = await pdf.getPage(idx + 1);
|
|
2936
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
2937
|
+
const pageScale = Math.min(scale, maxPx / Math.max(viewport.width, viewport.height, 1));
|
|
2214
2938
|
const buf = await unpdf.renderPageAsImage(pdf, idx + 1, {
|
|
2215
2939
|
canvasImport: canvasImport ? async () => canvasImport : void 0,
|
|
2216
|
-
scale
|
|
2940
|
+
scale: pageScale
|
|
2217
2941
|
});
|
|
2218
2942
|
const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf);
|
|
2219
2943
|
results.push([idx, Buffer.from(bytes)]);
|
|
@@ -2221,6 +2945,7 @@ async function renderPdfPagesAsImages(content, pageIndices, dpi = 150) {
|
|
|
2221
2945
|
console.warn(`[vision] failed to render PDF page ${idx}:`, exc);
|
|
2222
2946
|
}
|
|
2223
2947
|
}
|
|
2948
|
+
console.info(`[pdf] rastered ${results.length} pages in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
|
|
2224
2949
|
return results;
|
|
2225
2950
|
}
|
|
2226
2951
|
async function detectPdfVisionPages(content) {
|
|
@@ -2242,7 +2967,13 @@ async function detectPdfVisionPages(content) {
|
|
|
2242
2967
|
return visionPages;
|
|
2243
2968
|
}
|
|
2244
2969
|
async function extractDocxImages(content) {
|
|
2245
|
-
|
|
2970
|
+
let zip;
|
|
2971
|
+
try {
|
|
2972
|
+
zip = await JSZip__default.default.loadAsync(content);
|
|
2973
|
+
} catch (exc) {
|
|
2974
|
+
console.warn(`[vision] cannot open DOCX for images: ${exc}`);
|
|
2975
|
+
return [];
|
|
2976
|
+
}
|
|
2246
2977
|
const rels = zip.file("word/_rels/document.xml.rels");
|
|
2247
2978
|
if (!rels) return [];
|
|
2248
2979
|
const xml = await rels.async("string");
|
|
@@ -2268,7 +2999,13 @@ async function extractDocxImages(content) {
|
|
|
2268
2999
|
return images;
|
|
2269
3000
|
}
|
|
2270
3001
|
async function extractPptxImages(content) {
|
|
2271
|
-
|
|
3002
|
+
let zip;
|
|
3003
|
+
try {
|
|
3004
|
+
zip = await JSZip__default.default.loadAsync(content);
|
|
3005
|
+
} catch (exc) {
|
|
3006
|
+
console.warn(`[vision] cannot open PPTX for images: ${exc}`);
|
|
3007
|
+
return [];
|
|
3008
|
+
}
|
|
2272
3009
|
const slideNames = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)).sort((a, b) => {
|
|
2273
3010
|
const na = Number(/slide(\d+)/.exec(a)?.[1] ?? 0);
|
|
2274
3011
|
const nb = Number(/slide(\d+)/.exec(b)?.[1] ?? 0);
|
|
@@ -2301,12 +3038,22 @@ async function extractPptxImages(content) {
|
|
|
2301
3038
|
}
|
|
2302
3039
|
async function extractImage(content, opts) {
|
|
2303
3040
|
const { Extracted: Extracted2 } = await Promise.resolve().then(() => (init_extraction(), extraction_exports));
|
|
3041
|
+
const extCfg = opts.extraction ?? EXTRACTION_DEFAULTS;
|
|
3042
|
+
const canvasModule = await tryImport("@napi-rs/canvas");
|
|
3043
|
+
const image = await capped(content, canvasModule, extCfg.maxRenderPx);
|
|
2304
3044
|
const [rawText, usage] = await callLlm(opts.visionLlm, {
|
|
2305
3045
|
system: VISION_SYSTEM_PROMPT,
|
|
2306
3046
|
user: `${MARKDOWN_RULES}
|
|
2307
3047
|
|
|
2308
3048
|
Transcribe this single image. Return the Markdown only.`,
|
|
2309
|
-
images: [
|
|
3049
|
+
images: [image],
|
|
3050
|
+
maxTokens: extCfg.visionMaxOutputTokens,
|
|
3051
|
+
// Same contract as the batched path (see processBatch): with the output
|
|
3052
|
+
// cap in place, default-on thinking bills against it — an all-thinking
|
|
3053
|
+
// truncated-empty reply lands as vision_failed. Transcription is not
|
|
3054
|
+
// reasoning, and it must be deterministic.
|
|
3055
|
+
thinkingBudget: 0,
|
|
3056
|
+
temperature: 0
|
|
2310
3057
|
});
|
|
2311
3058
|
const text = (rawText || "").trim();
|
|
2312
3059
|
return new Extracted2({
|
|
@@ -2315,37 +3062,75 @@ Transcribe this single image. Return the Markdown only.`,
|
|
|
2315
3062
|
slides: null,
|
|
2316
3063
|
mediaOnly: !text,
|
|
2317
3064
|
providerTokens: usage ?? {},
|
|
2318
|
-
isMarkdown: Boolean(text)
|
|
3065
|
+
isMarkdown: Boolean(text),
|
|
3066
|
+
// A configured reader that returns nothing (refusal, filter, truncation)
|
|
3067
|
+
// is the same "reader read nothing" state the PDF path labels
|
|
3068
|
+
// vision_failed — not a blank image with no explanation.
|
|
3069
|
+
unreadableReason: text ? null : "vision_failed",
|
|
3070
|
+
unreadablePages: text ? 0 : 1
|
|
2319
3071
|
});
|
|
2320
3072
|
}
|
|
2321
|
-
var
|
|
3073
|
+
var VISION_BATCH_ATTEMPTS, EXTRACTION_DEFAULTS, MAX_RENDER_PX, VISION_MAX_OUTPUT_TOKENS, PAGES_PER_VISION_BATCH, VISION_SYSTEM_PROMPT, MARKDOWN_RULES, BoundedQueue;
|
|
2322
3074
|
var init_vision = __esm({
|
|
2323
3075
|
"src/extraction/vision.ts"() {
|
|
3076
|
+
init_config();
|
|
2324
3077
|
init_extras();
|
|
2325
3078
|
init_llm();
|
|
2326
3079
|
init_json();
|
|
2327
3080
|
init_pdf();
|
|
2328
|
-
PAGES_PER_VISION_BATCH = 5;
|
|
2329
3081
|
VISION_BATCH_ATTEMPTS = 3;
|
|
3082
|
+
EXTRACTION_DEFAULTS = extractionSchema.parse({});
|
|
3083
|
+
MAX_RENDER_PX = EXTRACTION_DEFAULTS.maxRenderPx;
|
|
3084
|
+
VISION_MAX_OUTPUT_TOKENS = EXTRACTION_DEFAULTS.visionMaxOutputTokens;
|
|
3085
|
+
PAGES_PER_VISION_BATCH = EXTRACTION_DEFAULTS.pagesPerVisionBatch;
|
|
2330
3086
|
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.";
|
|
2331
3087
|
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.";
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
3088
|
+
BoundedQueue = class {
|
|
3089
|
+
constructor(maxsize) {
|
|
3090
|
+
this.maxsize = maxsize;
|
|
3091
|
+
}
|
|
3092
|
+
maxsize;
|
|
3093
|
+
items = [];
|
|
3094
|
+
putters = [];
|
|
3095
|
+
getters = [];
|
|
3096
|
+
closed = false;
|
|
3097
|
+
async put(item) {
|
|
3098
|
+
while (!this.closed && this.items.length >= this.maxsize) {
|
|
3099
|
+
await new Promise((resolve) => this.putters.push(resolve));
|
|
3100
|
+
}
|
|
3101
|
+
if (this.closed) return;
|
|
3102
|
+
this.items.push(item);
|
|
3103
|
+
this.getters.shift()?.();
|
|
3104
|
+
}
|
|
3105
|
+
/** `null` once the queue is closed AND drained — the worker's exit signal. */
|
|
3106
|
+
async get() {
|
|
3107
|
+
for (; ; ) {
|
|
3108
|
+
if (this.items.length) {
|
|
3109
|
+
const item = this.items.shift();
|
|
3110
|
+
this.putters.shift()?.();
|
|
3111
|
+
return item;
|
|
3112
|
+
}
|
|
3113
|
+
if (this.closed) return null;
|
|
3114
|
+
await new Promise((resolve) => this.getters.push(resolve));
|
|
2342
3115
|
}
|
|
2343
|
-
await new Promise((resolve) => this.waiters.push(resolve));
|
|
2344
3116
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
3117
|
+
close() {
|
|
3118
|
+
this.closed = true;
|
|
3119
|
+
for (const wake of this.getters.splice(0)) wake();
|
|
3120
|
+
for (const wake of this.putters.splice(0)) wake();
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Like `close()`, but for the error path: DISCARDS whatever is still
|
|
3124
|
+
* queued rather than letting it drain. A batch already in a worker's hands
|
|
3125
|
+
* keeps running (there is no cancelling that), but one that only ever sat
|
|
3126
|
+
* in the queue must never start — the source has already failed, so
|
|
3127
|
+
* spending a full vision-LLM retry budget on it buys nothing.
|
|
3128
|
+
*/
|
|
3129
|
+
abort() {
|
|
3130
|
+
this.closed = true;
|
|
3131
|
+
this.items.length = 0;
|
|
3132
|
+
for (const wake of this.getters.splice(0)) wake();
|
|
3133
|
+
for (const wake of this.putters.splice(0)) wake();
|
|
2349
3134
|
}
|
|
2350
3135
|
};
|
|
2351
3136
|
}
|
|
@@ -2355,7 +3140,8 @@ var init_vision = __esm({
|
|
|
2355
3140
|
var extraction_exports = {};
|
|
2356
3141
|
__export(extraction_exports, {
|
|
2357
3142
|
Extracted: () => exports.Extracted,
|
|
2358
|
-
extract: () => extract2
|
|
3143
|
+
extract: () => extract2,
|
|
3144
|
+
extractEmbeddedImagesText: () => extractEmbeddedImagesText
|
|
2359
3145
|
});
|
|
2360
3146
|
function isPdf(ext, mime) {
|
|
2361
3147
|
return ext === ".pdf" || mime === "application/pdf";
|
|
@@ -2363,14 +3149,50 @@ function isPdf(ext, mime) {
|
|
|
2363
3149
|
function isImage(ext, mime) {
|
|
2364
3150
|
return mime.startsWith("image/") || IMAGE_EXTS.has(ext);
|
|
2365
3151
|
}
|
|
2366
|
-
async function
|
|
3152
|
+
async function officeUnreadable(text, images, visionLlm) {
|
|
3153
|
+
if (text.trim()) return { reason: null, unread: 0 };
|
|
3154
|
+
let readable = 0;
|
|
3155
|
+
for (const img of images) {
|
|
3156
|
+
if (!await embeddedImageIsNegligible(img.imageBytes)) readable += 1;
|
|
3157
|
+
}
|
|
3158
|
+
if (!readable) return { reason: null, unread: 0 };
|
|
3159
|
+
return { reason: visionLlm ? "vision_failed" : "needs_vision", unread: readable };
|
|
3160
|
+
}
|
|
3161
|
+
async function embeddedImageIsNegligible(data) {
|
|
3162
|
+
if (data.length < MIN_EMBEDDED_IMAGE_BYTES) return true;
|
|
3163
|
+
const canvas = await tryImport("@napi-rs/canvas");
|
|
3164
|
+
if (!canvas) return false;
|
|
3165
|
+
try {
|
|
3166
|
+
const img = await canvas.loadImage(data);
|
|
3167
|
+
return Math.max(img.width, img.height) < MIN_EMBEDDED_IMAGE_PX;
|
|
3168
|
+
} catch {
|
|
3169
|
+
return false;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
async function extractEmbeddedImagesText(images, visionLlm, hooks, extraction) {
|
|
2367
3173
|
if (!images.length) return [[], {}];
|
|
2368
3174
|
try {
|
|
2369
3175
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
3176
|
+
const { createHash: createHash3 } = await import('crypto');
|
|
3177
|
+
const keys = [];
|
|
3178
|
+
const firstAt = /* @__PURE__ */ new Map();
|
|
3179
|
+
const send = [];
|
|
3180
|
+
for (const img of images) {
|
|
3181
|
+
const blob = img.imageBytes;
|
|
3182
|
+
if (await embeddedImageIsNegligible(blob)) {
|
|
3183
|
+
keys.push(null);
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
const digest = createHash3("sha256").update(blob).digest("hex");
|
|
3187
|
+
if (!firstAt.has(digest)) {
|
|
3188
|
+
firstAt.set(digest, send.length);
|
|
3189
|
+
send.push(blob);
|
|
3190
|
+
}
|
|
3191
|
+
keys.push(digest);
|
|
3192
|
+
}
|
|
3193
|
+
if (!send.length) return [images.map(() => ""), {}];
|
|
3194
|
+
const [texts, tokens] = await vision.extractTextFromImages(send, { visionLlm, extraction });
|
|
3195
|
+
return [keys.map((k) => k == null ? "" : texts[firstAt.get(k)] ?? ""), tokens];
|
|
2374
3196
|
} catch (exc) {
|
|
2375
3197
|
emitError(hooks, exc, { stage: "embedded_image_vision" });
|
|
2376
3198
|
return [[], {}];
|
|
@@ -2381,22 +3203,35 @@ async function extract2(content, filename, mime, opts) {
|
|
|
2381
3203
|
const ext = getFileExtension(filename);
|
|
2382
3204
|
const visionLlm = opts.visionLlm ?? null;
|
|
2383
3205
|
const hooks = opts.hooks;
|
|
3206
|
+
const extraction = opts.extraction ?? null;
|
|
2384
3207
|
try {
|
|
2385
3208
|
if (isNonIngestibleMedia(filename, m)) {
|
|
2386
3209
|
return new exports.Extracted({ text: "", mediaOnly: true });
|
|
2387
3210
|
}
|
|
2388
3211
|
if (isPdf(ext, m)) {
|
|
2389
3212
|
const pdf = await Promise.resolve().then(() => (init_pdf(), pdf_exports));
|
|
2390
|
-
return await pdf.extract(content, { visionLlm, hooks });
|
|
3213
|
+
return await pdf.extract(content, { visionLlm, hooks, extraction });
|
|
2391
3214
|
}
|
|
2392
3215
|
if (isImage(ext, m)) {
|
|
2393
|
-
if (!visionLlm)
|
|
3216
|
+
if (!visionLlm) {
|
|
3217
|
+
return new exports.Extracted({
|
|
3218
|
+
text: "",
|
|
3219
|
+
mediaOnly: true,
|
|
3220
|
+
unreadableReason: "needs_vision",
|
|
3221
|
+
unreadablePages: 1
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
2394
3224
|
try {
|
|
2395
3225
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2396
|
-
return await vision.extractImage(content, { visionLlm });
|
|
3226
|
+
return await vision.extractImage(content, { visionLlm, extraction });
|
|
2397
3227
|
} catch (exc) {
|
|
2398
3228
|
emitError(hooks, exc, { stage: "image_vision", filename });
|
|
2399
|
-
return new exports.Extracted({
|
|
3229
|
+
return new exports.Extracted({
|
|
3230
|
+
text: "",
|
|
3231
|
+
mediaOnly: true,
|
|
3232
|
+
unreadableReason: "vision_failed",
|
|
3233
|
+
unreadablePages: 1
|
|
3234
|
+
});
|
|
2400
3235
|
}
|
|
2401
3236
|
}
|
|
2402
3237
|
if (ext === ".docx" || m === DOCX_MIME) {
|
|
@@ -2404,10 +3239,11 @@ async function extract2(content, filename, mime, opts) {
|
|
|
2404
3239
|
let providerTokens = {};
|
|
2405
3240
|
let llmPictures = 0;
|
|
2406
3241
|
let combined = text;
|
|
3242
|
+
let images = [];
|
|
2407
3243
|
if (visionLlm) {
|
|
2408
3244
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2409
|
-
|
|
2410
|
-
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks);
|
|
3245
|
+
images = await vision.extractDocxImages(content);
|
|
3246
|
+
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks, extraction);
|
|
2411
3247
|
for (let i = 0; i < texts.length; i++) {
|
|
2412
3248
|
const imgText = texts[i];
|
|
2413
3249
|
if (imgText) {
|
|
@@ -2418,12 +3254,18 @@ ${imgText}`;
|
|
|
2418
3254
|
}
|
|
2419
3255
|
}
|
|
2420
3256
|
providerTokens = tokens;
|
|
3257
|
+
} else if (!text.trim()) {
|
|
3258
|
+
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
3259
|
+
images = await vision.extractDocxImages(content);
|
|
2421
3260
|
}
|
|
3261
|
+
const { reason, unread } = await officeUnreadable(combined, images, visionLlm);
|
|
2422
3262
|
return new exports.Extracted({
|
|
2423
3263
|
text: combined,
|
|
2424
3264
|
pages: await countDocxPages(content),
|
|
2425
3265
|
providerTokens,
|
|
2426
|
-
llmPictures
|
|
3266
|
+
llmPictures,
|
|
3267
|
+
unreadableReason: reason,
|
|
3268
|
+
unreadablePages: unread
|
|
2427
3269
|
});
|
|
2428
3270
|
}
|
|
2429
3271
|
if (ext === ".pptx" || m === PPTX_MIME) {
|
|
@@ -2431,10 +3273,11 @@ ${imgText}`;
|
|
|
2431
3273
|
let providerTokens = {};
|
|
2432
3274
|
let llmPictures = 0;
|
|
2433
3275
|
let combined = text;
|
|
3276
|
+
let images = [];
|
|
2434
3277
|
if (visionLlm) {
|
|
2435
3278
|
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
2436
|
-
|
|
2437
|
-
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks);
|
|
3279
|
+
images = await vision.extractPptxImages(content);
|
|
3280
|
+
const [texts, tokens] = await extractEmbeddedImagesText(images, visionLlm, hooks, extraction);
|
|
2438
3281
|
for (let i = 0; i < texts.length; i++) {
|
|
2439
3282
|
const imgText = texts[i];
|
|
2440
3283
|
if (imgText) {
|
|
@@ -2446,12 +3289,18 @@ ${imgText}`;
|
|
|
2446
3289
|
}
|
|
2447
3290
|
}
|
|
2448
3291
|
providerTokens = tokens;
|
|
3292
|
+
} else if (!text.trim()) {
|
|
3293
|
+
const vision = await Promise.resolve().then(() => (init_vision(), vision_exports));
|
|
3294
|
+
images = await vision.extractPptxImages(content);
|
|
2449
3295
|
}
|
|
3296
|
+
const { reason, unread } = await officeUnreadable(combined, images, visionLlm);
|
|
2450
3297
|
return new exports.Extracted({
|
|
2451
3298
|
text: combined,
|
|
2452
3299
|
slides: await countPptxSlides(content),
|
|
2453
3300
|
providerTokens,
|
|
2454
|
-
llmPictures
|
|
3301
|
+
llmPictures,
|
|
3302
|
+
unreadableReason: reason,
|
|
3303
|
+
unreadablePages: unread
|
|
2455
3304
|
});
|
|
2456
3305
|
}
|
|
2457
3306
|
if (ext === ".xlsx" || m === XLSX_MIME) {
|
|
@@ -2475,9 +3324,10 @@ ${imgText}`;
|
|
|
2475
3324
|
return new exports.Extracted({ text: content.toString("utf8") });
|
|
2476
3325
|
}
|
|
2477
3326
|
}
|
|
2478
|
-
var IMAGE_EXTS; exports.Extracted = void 0;
|
|
3327
|
+
var IMAGE_EXTS; exports.Extracted = void 0; var MIN_EMBEDDED_IMAGE_BYTES, MIN_EMBEDDED_IMAGE_PX;
|
|
2479
3328
|
var init_extraction = __esm({
|
|
2480
3329
|
"src/extraction/index.ts"() {
|
|
3330
|
+
init_extras();
|
|
2481
3331
|
init_hooks();
|
|
2482
3332
|
init_files();
|
|
2483
3333
|
IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp"]);
|
|
@@ -2489,6 +3339,10 @@ var init_extraction = __esm({
|
|
|
2489
3339
|
providerTokens;
|
|
2490
3340
|
llmPictures;
|
|
2491
3341
|
isMarkdown;
|
|
3342
|
+
/** See `UnreadableReason`. `mediaOnly` is the same idea for whole files and
|
|
3343
|
+
* stays set alongside this, so existing callers keep working. */
|
|
3344
|
+
unreadableReason;
|
|
3345
|
+
unreadablePages;
|
|
2492
3346
|
constructor(init) {
|
|
2493
3347
|
this.text = init.text;
|
|
2494
3348
|
this.pages = init.pages ?? null;
|
|
@@ -2497,8 +3351,12 @@ var init_extraction = __esm({
|
|
|
2497
3351
|
this.providerTokens = init.providerTokens ?? {};
|
|
2498
3352
|
this.llmPictures = init.llmPictures ?? 0;
|
|
2499
3353
|
this.isMarkdown = init.isMarkdown ?? false;
|
|
3354
|
+
this.unreadableReason = init.unreadableReason ?? null;
|
|
3355
|
+
this.unreadablePages = init.unreadablePages ?? 0;
|
|
2500
3356
|
}
|
|
2501
3357
|
};
|
|
3358
|
+
MIN_EMBEDDED_IMAGE_BYTES = 3 * 1024;
|
|
3359
|
+
MIN_EMBEDDED_IMAGE_PX = 100;
|
|
2502
3360
|
}
|
|
2503
3361
|
});
|
|
2504
3362
|
|
|
@@ -2543,6 +3401,9 @@ __export(db_exports, {
|
|
|
2543
3401
|
function stripSqlNoise(sql) {
|
|
2544
3402
|
return sql.replace(SQL_NOISE_RE, (m) => " ".repeat(m.length));
|
|
2545
3403
|
}
|
|
3404
|
+
function stripSqlNoiseBackslash(sql) {
|
|
3405
|
+
return sql.replace(SQL_NOISE_BACKSLASH_RE, (m) => " ".repeat(m.length));
|
|
3406
|
+
}
|
|
2546
3407
|
function hasKeyword(sqlUpper, keyword) {
|
|
2547
3408
|
return new RegExp(`\\b${keyword}\\b`).test(sqlUpper);
|
|
2548
3409
|
}
|
|
@@ -2671,24 +3532,28 @@ async function executeQueryAsync(config, sql, maxRows, accessModeRaw) {
|
|
|
2671
3532
|
`Invalid access_mode ${JSON.stringify(accessModeRaw)}; must be one of ${ALLOWED_ACCESS_MODES}`
|
|
2672
3533
|
);
|
|
2673
3534
|
}
|
|
2674
|
-
const
|
|
3535
|
+
const sqlVariants = [
|
|
3536
|
+
stripSqlNoise(sql).trim().toUpperCase(),
|
|
3537
|
+
stripSqlNoiseBackslash(sql).trim().toUpperCase()
|
|
3538
|
+
];
|
|
3539
|
+
const sqlUpper = sqlVariants[0];
|
|
2675
3540
|
for (const keyword of ALWAYS_BLOCKED) {
|
|
2676
|
-
if (hasKeyword(
|
|
3541
|
+
if ([...sqlVariants, sql.toUpperCase()].some((v) => hasKeyword(v, keyword))) {
|
|
2677
3542
|
return { success: false, error: `${keyword} queries are not allowed` };
|
|
2678
3543
|
}
|
|
2679
3544
|
}
|
|
2680
3545
|
if (accessMode === "readonly") {
|
|
2681
|
-
if (!
|
|
3546
|
+
if (!sqlVariants.every((v) => v.startsWith("SELECT") || v.startsWith("WITH"))) {
|
|
2682
3547
|
return { success: false, error: "Only SELECT queries are allowed in read-only mode" };
|
|
2683
3548
|
}
|
|
2684
3549
|
for (const keyword of READONLY_BLOCKED) {
|
|
2685
|
-
if (hasKeyword(
|
|
3550
|
+
if (sqlVariants.some((v) => hasKeyword(v, keyword))) {
|
|
2686
3551
|
return { success: false, error: `${keyword} queries are not allowed in read-only mode` };
|
|
2687
3552
|
}
|
|
2688
3553
|
}
|
|
2689
3554
|
} else if (accessMode === "readwrite") {
|
|
2690
3555
|
for (const keyword of READWRITE_BLOCKED) {
|
|
2691
|
-
if (hasKeyword(
|
|
3556
|
+
if (sqlVariants.some((v) => hasKeyword(v, keyword))) {
|
|
2692
3557
|
return { success: false, error: `${keyword} queries are not allowed in read-write mode` };
|
|
2693
3558
|
}
|
|
2694
3559
|
}
|
|
@@ -2822,7 +3687,7 @@ async function getSchemaText(config, selectedTables) {
|
|
|
2822
3687
|
}
|
|
2823
3688
|
return rowsToText(schemaRows);
|
|
2824
3689
|
}
|
|
2825
|
-
var require2, MAX_ROWS, CONNECT_TIMEOUT, QUERY_TIMEOUT_MS, ALWAYS_BLOCKED, READONLY_BLOCKED, READWRITE_BLOCKED, ALLOWED_ACCESS_MODES, SQL_NOISE_RE;
|
|
3690
|
+
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;
|
|
2826
3691
|
var init_db = __esm({
|
|
2827
3692
|
"src/tools/executors/db.ts"() {
|
|
2828
3693
|
init_errors();
|
|
@@ -2831,10 +3696,22 @@ var init_db = __esm({
|
|
|
2831
3696
|
CONNECT_TIMEOUT = 10;
|
|
2832
3697
|
QUERY_TIMEOUT_MS = 3e4;
|
|
2833
3698
|
ALWAYS_BLOCKED = ["GRANT", "REVOKE"];
|
|
2834
|
-
READONLY_BLOCKED = [
|
|
2835
|
-
|
|
3699
|
+
READONLY_BLOCKED = [
|
|
3700
|
+
"INSERT",
|
|
3701
|
+
"UPDATE",
|
|
3702
|
+
"DELETE",
|
|
3703
|
+
"MERGE",
|
|
3704
|
+
"DROP",
|
|
3705
|
+
"ALTER",
|
|
3706
|
+
"CREATE",
|
|
3707
|
+
"TRUNCATE",
|
|
3708
|
+
"DO",
|
|
3709
|
+
"CALL"
|
|
3710
|
+
];
|
|
3711
|
+
READWRITE_BLOCKED = ["DELETE", "DROP", "TRUNCATE", "DO", "CALL"];
|
|
2836
3712
|
ALLOWED_ACCESS_MODES = ["readonly", "readwrite", "full"];
|
|
2837
|
-
SQL_NOISE_RE =
|
|
3713
|
+
SQL_NOISE_RE = /\b[eE]'(?:[^'\\]|\\[\s\S]|'')*'|'(?:[^']|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
3714
|
+
SQL_NOISE_BACKSLASH_RE = /'(?:[^'\\]|\\[\s\S]|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
2838
3715
|
}
|
|
2839
3716
|
});
|
|
2840
3717
|
function mulberry322(seed) {
|
|
@@ -3666,7 +4543,8 @@ var retrieval_exports = {};
|
|
|
3666
4543
|
__export(retrieval_exports, {
|
|
3667
4544
|
buildGraphRanked: () => buildGraphRanked,
|
|
3668
4545
|
corpusIsAclUniform: () => corpusIsAclUniform,
|
|
3669
|
-
shouldUseCommunitySummaries: () => shouldUseCommunitySummaries
|
|
4546
|
+
shouldUseCommunitySummaries: () => shouldUseCommunitySummaries,
|
|
4547
|
+
vectorSeedIds: () => vectorSeedIds
|
|
3670
4548
|
});
|
|
3671
4549
|
function vecLiteral(vector) {
|
|
3672
4550
|
return `[${vector.map((x) => Number(x)).join(",")}]`;
|
|
@@ -3692,8 +4570,8 @@ async function deriveQueryEntities(pool, chunkIds, opts) {
|
|
|
3692
4570
|
JOIN context_engine_chunks c ON c.id = ce.chunk_id
|
|
3693
4571
|
WHERE ce.chunk_id = ANY($1::uuid[]) ${SCOPE2}
|
|
3694
4572
|
GROUP BY e.normalized_name, e.name, e.type
|
|
3695
|
-
ORDER BY freq DESC LIMIT $
|
|
3696
|
-
[chunkIds, opts.sourceIds, opts.principals, ENTITY_LIMIT]
|
|
4573
|
+
ORDER BY freq DESC LIMIT $5`,
|
|
4574
|
+
[chunkIds, opts.sourceIds, opts.documentIds ?? null, opts.principals, ENTITY_LIMIT]
|
|
3697
4575
|
);
|
|
3698
4576
|
return result.rows.map((r) => ({
|
|
3699
4577
|
normalized_name: r.normalized_name,
|
|
@@ -3758,8 +4636,8 @@ async function computeCommunityScores(pool, chunkIds, vector, opts) {
|
|
|
3758
4636
|
`SELECT ce.chunk_id::text, ce.entity_id::text
|
|
3759
4637
|
FROM context_engine_chunk_entities ce
|
|
3760
4638
|
JOIN context_engine_chunks c ON c.id = ce.chunk_id
|
|
3761
|
-
WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($
|
|
3762
|
-
[chunkIds, opts.sourceIds, opts.principals, Object.keys(entityScore)]
|
|
4639
|
+
WHERE ce.chunk_id = ANY($1::uuid[]) AND ce.entity_id = ANY($5::uuid[]) ${SCOPE2}`,
|
|
4640
|
+
[chunkIds, opts.sourceIds, opts.documentIds ?? null, opts.principals, Object.keys(entityScore)]
|
|
3763
4641
|
);
|
|
3764
4642
|
const chunkScores = {};
|
|
3765
4643
|
for (const row of result.rows) {
|
|
@@ -3768,13 +4646,29 @@ async function computeCommunityScores(pool, chunkIds, vector, opts) {
|
|
|
3768
4646
|
return chunkScores;
|
|
3769
4647
|
}
|
|
3770
4648
|
async function vectorSeedIds(pool, vector, opts) {
|
|
3771
|
-
const
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
4649
|
+
const binds = [opts.sourceIds, opts.documentIds ?? null, opts.principals];
|
|
4650
|
+
const scoped = binds.some((v) => v !== null);
|
|
4651
|
+
const client = await pool.connect();
|
|
4652
|
+
try {
|
|
4653
|
+
await client.query("BEGIN");
|
|
4654
|
+
await opts.backend?.tuneAnnScan?.(client, SEED_LIMIT, scoped);
|
|
4655
|
+
const result = await client.query(
|
|
4656
|
+
`SELECT c.id::text FROM context_engine_chunks c
|
|
4657
|
+
WHERE c.embedding IS NOT NULL ${SCOPE2}
|
|
4658
|
+
ORDER BY c.embedding <=> CAST($1 AS vector) LIMIT $5`,
|
|
4659
|
+
[vecLiteral(vector), ...binds, SEED_LIMIT]
|
|
4660
|
+
);
|
|
4661
|
+
await client.query("COMMIT");
|
|
4662
|
+
return result.rows.map((r) => String(r.id));
|
|
4663
|
+
} catch (err) {
|
|
4664
|
+
try {
|
|
4665
|
+
await client.query("ROLLBACK");
|
|
4666
|
+
} catch {
|
|
4667
|
+
}
|
|
4668
|
+
throw err;
|
|
4669
|
+
} finally {
|
|
4670
|
+
client.release();
|
|
4671
|
+
}
|
|
3778
4672
|
}
|
|
3779
4673
|
async function buildGraphRanked(query, opts) {
|
|
3780
4674
|
try {
|
|
@@ -3783,11 +4677,18 @@ async function buildGraphRanked(query, opts) {
|
|
|
3783
4677
|
const vector = vectors[0] ? [...vectors[0]] : null;
|
|
3784
4678
|
if (!vector) return [];
|
|
3785
4679
|
const sourceIds = opts.sourceIds ?? null;
|
|
4680
|
+
const documentIds = opts.documentIds ?? null;
|
|
3786
4681
|
const principals = opts.principals ?? null;
|
|
3787
|
-
const seeds = await vectorSeedIds(opts.pool, vector, {
|
|
4682
|
+
const seeds = await vectorSeedIds(opts.pool, vector, {
|
|
4683
|
+
sourceIds,
|
|
4684
|
+
documentIds,
|
|
4685
|
+
principals,
|
|
4686
|
+
backend: opts.backend
|
|
4687
|
+
});
|
|
3788
4688
|
if (!seeds.length) return [];
|
|
3789
4689
|
const queryEntities = await deriveQueryEntities(opts.pool, seeds.slice(0, TOP_SEEDS_FOR_ENTITIES), {
|
|
3790
4690
|
sourceIds,
|
|
4691
|
+
documentIds,
|
|
3791
4692
|
principals
|
|
3792
4693
|
});
|
|
3793
4694
|
const entityNorms = queryEntities.map((e) => String(e.normalized_name));
|
|
@@ -3809,6 +4710,7 @@ async function buildGraphRanked(query, opts) {
|
|
|
3809
4710
|
const relScores = await computeRelationshipScores(opts.pool, allIds, entityNorms);
|
|
3810
4711
|
const commScores = await computeCommunityScores(opts.pool, allIds, vector, {
|
|
3811
4712
|
sourceIds,
|
|
4713
|
+
documentIds,
|
|
3812
4714
|
principals,
|
|
3813
4715
|
useSummaries
|
|
3814
4716
|
});
|
|
@@ -3860,7 +4762,8 @@ var init_retrieval = __esm({
|
|
|
3860
4762
|
TOP_SEEDS_FOR_ENTITIES = 20;
|
|
3861
4763
|
SCOPE2 = `
|
|
3862
4764
|
AND ($2::text[] IS NULL OR c.source_id = ANY($2::text[]))
|
|
3863
|
-
AND ($3::
|
|
4765
|
+
AND ($3::uuid[] IS NULL OR c.document_id = ANY($3::uuid[]))
|
|
4766
|
+
AND ($4::text[] IS NULL OR c.acl IS NULL OR c.acl && $4::text[])
|
|
3864
4767
|
`;
|
|
3865
4768
|
}
|
|
3866
4769
|
});
|
|
@@ -4445,381 +5348,96 @@ function splitIntoChunks(text, maxWords = 180, overlap = 18) {
|
|
|
4445
5348
|
for (const ln of (text || "").split(/\r\n|\n|\r/)) {
|
|
4446
5349
|
const stripped = ln.trim();
|
|
4447
5350
|
if (!stripped) {
|
|
4448
|
-
pendingBlank = units.length > 0;
|
|
4449
|
-
continue;
|
|
4450
|
-
}
|
|
4451
|
-
const lineSep = pendingBlank ? "\n\n" : "\n";
|
|
4452
|
-
pendingBlank = false;
|
|
4453
|
-
if (HEADING_OR_BULLET_RE.test(ln) || stripped.length <= 120) {
|
|
4454
|
-
units.push([lineSep, stripped, tokenize(stripped).length]);
|
|
4455
|
-
} else {
|
|
4456
|
-
let first = true;
|
|
4457
|
-
for (const sent of splitSentences(ln)) {
|
|
4458
|
-
const s = sent.trim();
|
|
4459
|
-
if (s) {
|
|
4460
|
-
units.push([first ? lineSep : " ", s, tokenize(s).length]);
|
|
4461
|
-
first = false;
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
const normalizedUnits = [];
|
|
4467
|
-
for (const [uSep, uText, uTokens] of units) {
|
|
4468
|
-
if (uTokens <= maxWords) {
|
|
4469
|
-
normalizedUnits.push([uSep, uText, uTokens]);
|
|
4470
|
-
continue;
|
|
4471
|
-
}
|
|
4472
|
-
const windowChars = maxWords * 4;
|
|
4473
|
-
const uChars = [...uText];
|
|
4474
|
-
let pos = 0;
|
|
4475
|
-
let pieceSep = uSep;
|
|
4476
|
-
while (pos < uChars.length) {
|
|
4477
|
-
let pieceChars = uChars.slice(pos, pos + windowChars);
|
|
4478
|
-
let piece = pieceChars.join("");
|
|
4479
|
-
if (pos + windowChars < uChars.length) {
|
|
4480
|
-
const ws = piece.lastIndexOf(" ");
|
|
4481
|
-
if (ws > Math.floor(windowChars / 2)) {
|
|
4482
|
-
piece = piece.slice(0, ws);
|
|
4483
|
-
pieceChars = [...piece];
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
let pieceTokens = tokenize(piece);
|
|
4487
|
-
if (pieceTokens.length > maxWords) {
|
|
4488
|
-
piece = pieceChars.slice(0, maxWords).join("");
|
|
4489
|
-
pieceTokens = tokenize(piece);
|
|
4490
|
-
}
|
|
4491
|
-
normalizedUnits.push([pieceSep, piece, pieceTokens.length]);
|
|
4492
|
-
pos += codePointLength(piece);
|
|
4493
|
-
pieceSep = "";
|
|
4494
|
-
}
|
|
4495
|
-
}
|
|
4496
|
-
if (!normalizedUnits.some(([, , t]) => t)) return [];
|
|
4497
|
-
const chunks = [];
|
|
4498
|
-
let lastKept = null;
|
|
4499
|
-
let cur = [];
|
|
4500
|
-
let curTokens = 0;
|
|
4501
|
-
const flush = () => {
|
|
4502
|
-
const chunk = assembleUnits(cur);
|
|
4503
|
-
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
4504
|
-
chunks.push(chunk);
|
|
4505
|
-
lastKept = chunk;
|
|
4506
|
-
}
|
|
4507
|
-
const carried = [];
|
|
4508
|
-
let carriedTokens = 0;
|
|
4509
|
-
for (let i = cur.length - 1; i >= 0; i--) {
|
|
4510
|
-
const prev = cur[i];
|
|
4511
|
-
if (prev[2] === 0 || carriedTokens + prev[2] > overlap) break;
|
|
4512
|
-
carried.unshift(prev);
|
|
4513
|
-
carriedTokens += prev[2];
|
|
4514
|
-
}
|
|
4515
|
-
cur = carried;
|
|
4516
|
-
curTokens = carriedTokens;
|
|
4517
|
-
};
|
|
4518
|
-
for (const unit of normalizedUnits) {
|
|
4519
|
-
if (cur.length && curTokens + unit[2] > maxWords) flush();
|
|
4520
|
-
cur.push(unit);
|
|
4521
|
-
curTokens += unit[2];
|
|
4522
|
-
}
|
|
4523
|
-
if (cur.length) {
|
|
4524
|
-
const chunk = assembleUnits(cur);
|
|
4525
|
-
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
4526
|
-
chunks.push(chunk);
|
|
4527
|
-
}
|
|
4528
|
-
}
|
|
4529
|
-
return chunks;
|
|
4530
|
-
}
|
|
4531
|
-
|
|
4532
|
-
// src/actions.ts
|
|
4533
|
-
init_errors();
|
|
4534
|
-
init_hooks();
|
|
4535
|
-
init_llm();
|
|
4536
|
-
function encryptDict(data, key) {
|
|
4537
|
-
const nonce = crypto.randomBytes(12);
|
|
4538
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
4539
|
-
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
4540
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4541
|
-
const tag = cipher.getAuthTag();
|
|
4542
|
-
return Buffer.concat([nonce, ciphertext, tag]).toString("base64");
|
|
4543
|
-
}
|
|
4544
|
-
function decryptDict(token, key) {
|
|
4545
|
-
const raw = Buffer.from(token, "base64");
|
|
4546
|
-
const nonce = raw.subarray(0, 12);
|
|
4547
|
-
const tag = raw.subarray(raw.length - 16);
|
|
4548
|
-
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
4549
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
4550
|
-
decipher.setAuthTag(tag);
|
|
4551
|
-
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
4552
|
-
return JSON.parse(plaintext.toString("utf8"));
|
|
4553
|
-
}
|
|
4554
|
-
function getSecretKey(config) {
|
|
4555
|
-
const secretKey = config.secretKey;
|
|
4556
|
-
if (!secretKey) {
|
|
4557
|
-
throw new Error(
|
|
4558
|
-
"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."
|
|
4559
|
-
);
|
|
4560
|
-
}
|
|
4561
|
-
return Buffer.from(secretKey, "base64url");
|
|
4562
|
-
}
|
|
4563
|
-
function hmacSha256Hex(key, value) {
|
|
4564
|
-
const k = typeof key === "string" ? Buffer.from(key) : key;
|
|
4565
|
-
return crypto.createHmac("sha256", k).update(value, "utf8").digest("hex");
|
|
4566
|
-
}
|
|
4567
|
-
|
|
4568
|
-
// src/redaction.ts
|
|
4569
|
-
init_hooks();
|
|
4570
|
-
var BUILTIN_DETECTOR_NAMES = /* @__PURE__ */ new Set(["email", "phone", "ssn", "credit_card", "iban", "api_key"]);
|
|
4571
|
-
var RedactionRule = class {
|
|
4572
|
-
name;
|
|
4573
|
-
detector;
|
|
4574
|
-
pattern;
|
|
4575
|
-
field;
|
|
4576
|
-
action;
|
|
4577
|
-
placeholder;
|
|
4578
|
-
applyAt;
|
|
4579
|
-
unless;
|
|
4580
|
-
compiled = null;
|
|
4581
|
-
constructor(init) {
|
|
4582
|
-
this.name = init.name;
|
|
4583
|
-
this.detector = init.detector ?? null;
|
|
4584
|
-
this.pattern = init.pattern ?? null;
|
|
4585
|
-
this.field = init.field ?? null;
|
|
4586
|
-
this.action = init.action ?? "mask";
|
|
4587
|
-
this.placeholder = init.placeholder ?? null;
|
|
4588
|
-
this.applyAt = init.applyAt ?? "output";
|
|
4589
|
-
this.unless = init.unless ?? [];
|
|
4590
|
-
this.validate();
|
|
4591
|
-
}
|
|
4592
|
-
validate() {
|
|
4593
|
-
if (this.field !== null) {
|
|
4594
|
-
throw new Error(
|
|
4595
|
-
`rule '${this.name}': field-targeted rules are not implemented yet; use detector or pattern instead`
|
|
4596
|
-
);
|
|
4597
|
-
}
|
|
4598
|
-
const targets = [this.detector, this.pattern].filter((t) => t !== null);
|
|
4599
|
-
if (targets.length !== 1) {
|
|
4600
|
-
throw new Error(`rule '${this.name}': exactly one of detector/pattern must be set`);
|
|
4601
|
-
}
|
|
4602
|
-
if (this.pattern !== null) {
|
|
4603
|
-
try {
|
|
4604
|
-
this.compiled = new RegExp(this.pattern, "g");
|
|
4605
|
-
} catch (exc) {
|
|
4606
|
-
throw new Error(`rule '${this.name}': invalid regex: ${exc}`);
|
|
4607
|
-
}
|
|
4608
|
-
}
|
|
4609
|
-
if (this.unless.length && this.applyAt === "ingest") {
|
|
4610
|
-
throw new Error(
|
|
4611
|
-
`rule '${this.name}': unless is output-time only and cannot be set on an applyAt='ingest' rule`
|
|
4612
|
-
);
|
|
4613
|
-
}
|
|
4614
|
-
}
|
|
4615
|
-
patternRe() {
|
|
4616
|
-
return this.compiled;
|
|
4617
|
-
}
|
|
4618
|
-
effectivePlaceholder() {
|
|
4619
|
-
return this.placeholder || `[${this.name.toUpperCase()}]`;
|
|
4620
|
-
}
|
|
4621
|
-
};
|
|
4622
|
-
var RedactionPolicy = class {
|
|
4623
|
-
rules;
|
|
4624
|
-
customDetectors;
|
|
4625
|
-
constructor(init = {}) {
|
|
4626
|
-
this.rules = (init.rules ?? []).map((r) => r instanceof RedactionRule ? r : new RedactionRule(r));
|
|
4627
|
-
this.customDetectors = init.customDetectors ?? {};
|
|
4628
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4629
|
-
for (const rule of this.rules) {
|
|
4630
|
-
if (seen.has(rule.name)) throw new Error(`duplicate rule name: '${rule.name}'`);
|
|
4631
|
-
seen.add(rule.name);
|
|
4632
|
-
if (rule.detector !== null) {
|
|
4633
|
-
const known = BUILTIN_DETECTOR_NAMES.has(rule.detector) || rule.detector in this.customDetectors;
|
|
4634
|
-
if (!known) {
|
|
4635
|
-
throw new Error(
|
|
4636
|
-
`rule '${rule.name}': unknown detector '${rule.detector}' (not built-in and not in customDetectors)`
|
|
4637
|
-
);
|
|
4638
|
-
}
|
|
4639
|
-
}
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
isEmpty() {
|
|
4643
|
-
return this.rules.length === 0;
|
|
4644
|
-
}
|
|
4645
|
-
};
|
|
4646
|
-
var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
4647
|
-
var PHONE_RE = /\+\d[\d\s-]{7,17}\d/g;
|
|
4648
|
-
var SSN_RE = /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g;
|
|
4649
|
-
var CARD_RE = /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g;
|
|
4650
|
-
var IBAN_RE = /(?<![A-Za-z0-9])[A-Z]{2}\d{2}[A-Z0-9]{10,30}(?![A-Za-z0-9])/g;
|
|
4651
|
-
var API_KEY_ALNUM = "A-Za-z0-9_\\-+/=";
|
|
4652
|
-
var API_KEY_PREFIX_RE = new RegExp(
|
|
4653
|
-
`(?<![${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}])`,
|
|
4654
|
-
"g"
|
|
4655
|
-
);
|
|
4656
|
-
var API_KEY_GENERIC_RE = new RegExp(
|
|
4657
|
-
`(?<![${API_KEY_ALNUM}])[${API_KEY_ALNUM}]{24,}(?![${API_KEY_ALNUM}])`,
|
|
4658
|
-
"g"
|
|
4659
|
-
);
|
|
4660
|
-
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}$/;
|
|
4661
|
-
function spansFrom(re, text) {
|
|
4662
|
-
const out = [];
|
|
4663
|
-
re.lastIndex = 0;
|
|
4664
|
-
let m = re.exec(text);
|
|
4665
|
-
while (m !== null) {
|
|
4666
|
-
out.push([m.index, m.index + m[0].length]);
|
|
4667
|
-
if (m[0].length === 0) re.lastIndex++;
|
|
4668
|
-
m = re.exec(text);
|
|
4669
|
-
}
|
|
4670
|
-
return out;
|
|
4671
|
-
}
|
|
4672
|
-
function luhnOk(digits) {
|
|
4673
|
-
let total = 0;
|
|
4674
|
-
const rev = [...digits].reverse();
|
|
4675
|
-
for (let i = 0; i < rev.length; i++) {
|
|
4676
|
-
let d = Number(rev[i]);
|
|
4677
|
-
if (i % 2 === 1) {
|
|
4678
|
-
d *= 2;
|
|
4679
|
-
if (d > 9) d -= 9;
|
|
4680
|
-
}
|
|
4681
|
-
total += d;
|
|
4682
|
-
}
|
|
4683
|
-
return total % 10 === 0;
|
|
4684
|
-
}
|
|
4685
|
-
function detectCreditCard(text) {
|
|
4686
|
-
const spans = [];
|
|
4687
|
-
for (const [start, end] of spansFrom(CARD_RE, text)) {
|
|
4688
|
-
const digits = text.slice(start, end).replace(/[ -]/g, "");
|
|
4689
|
-
if (digits.length >= 13 && digits.length <= 19 && luhnOk(digits)) {
|
|
4690
|
-
spans.push([start, end]);
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
return spans;
|
|
4694
|
-
}
|
|
4695
|
-
function looksLikeGenericSecret(token) {
|
|
4696
|
-
if (UUID_RE.test(token)) return false;
|
|
4697
|
-
let hasUpper = false;
|
|
4698
|
-
let hasLower = false;
|
|
4699
|
-
let hasDigit = false;
|
|
4700
|
-
for (const c of token) {
|
|
4701
|
-
if (c >= "A" && c <= "Z") hasUpper = true;
|
|
4702
|
-
else if (c >= "a" && c <= "z") hasLower = true;
|
|
4703
|
-
else if (c >= "0" && c <= "9") hasDigit = true;
|
|
4704
|
-
}
|
|
4705
|
-
return hasUpper && hasLower && hasDigit;
|
|
4706
|
-
}
|
|
4707
|
-
function detectApiKey(text) {
|
|
4708
|
-
const spans = spansFrom(API_KEY_PREFIX_RE, text);
|
|
4709
|
-
for (const [start, end] of spansFrom(API_KEY_GENERIC_RE, text)) {
|
|
4710
|
-
if (spans.some(([s, e]) => start < e && s < end)) continue;
|
|
4711
|
-
if (looksLikeGenericSecret(text.slice(start, end))) spans.push([start, end]);
|
|
4712
|
-
}
|
|
4713
|
-
return spans;
|
|
4714
|
-
}
|
|
4715
|
-
var BUILTIN = {
|
|
4716
|
-
email: (t) => spansFrom(EMAIL_RE, t),
|
|
4717
|
-
phone: (t) => spansFrom(PHONE_RE, t),
|
|
4718
|
-
ssn: (t) => spansFrom(SSN_RE, t),
|
|
4719
|
-
credit_card: detectCreditCard,
|
|
4720
|
-
iban: (t) => spansFrom(IBAN_RE, t),
|
|
4721
|
-
api_key: detectApiKey
|
|
4722
|
-
};
|
|
4723
|
-
function detectBuiltin(name, text) {
|
|
4724
|
-
const detector = BUILTIN[name];
|
|
4725
|
-
if (!detector) throw new Error(`unknown built-in detector: ${name}`);
|
|
4726
|
-
if (!text) return [];
|
|
4727
|
-
return detector(text).sort((a, b) => a[0] - b[0]);
|
|
4728
|
-
}
|
|
4729
|
-
function ruleApplies(rule, opts) {
|
|
4730
|
-
if (rule.applyAt !== "both" && rule.applyAt !== opts.phase) return false;
|
|
4731
|
-
if (opts.phase === "ingest") return true;
|
|
4732
|
-
if (!rule.unless.length) return true;
|
|
4733
|
-
if (opts.principals === null) return false;
|
|
4734
|
-
const held = new Set(opts.principals);
|
|
4735
|
-
return !rule.unless.some((p) => held.has(p));
|
|
4736
|
-
}
|
|
4737
|
-
function spansForRule(rule, text, policy) {
|
|
4738
|
-
if (rule.pattern !== null) {
|
|
4739
|
-
const re = rule.patternRe() ?? new RegExp(rule.pattern, "g");
|
|
4740
|
-
return spansFrom(re, text);
|
|
4741
|
-
}
|
|
4742
|
-
if (rule.detector !== null) {
|
|
4743
|
-
const custom = policy.customDetectors[rule.detector];
|
|
4744
|
-
if (custom) return [...custom(text)];
|
|
4745
|
-
return detectBuiltin(rule.detector, text);
|
|
4746
|
-
}
|
|
4747
|
-
return [];
|
|
4748
|
-
}
|
|
4749
|
-
var HASH_TOKEN_CHARS = 16;
|
|
4750
|
-
function hashToken(value, secretKey) {
|
|
4751
|
-
const key = Buffer.isBuffer(secretKey) ? secretKey : Buffer.from(String(secretKey ?? ""));
|
|
4752
|
-
return hmacSha256Hex(key, value).slice(0, HASH_TOKEN_CHARS);
|
|
4753
|
-
}
|
|
4754
|
-
function applyRedaction(text, policy, opts) {
|
|
4755
|
-
if (!text || policy.isEmpty()) return [text, {}];
|
|
4756
|
-
const principals = opts.principals ?? null;
|
|
4757
|
-
const collected = [];
|
|
4758
|
-
const fired = [];
|
|
4759
|
-
const failed = [];
|
|
4760
|
-
for (const rule of policy.rules) {
|
|
4761
|
-
if (!ruleApplies(rule, { phase: opts.phase, principals })) continue;
|
|
4762
|
-
if (rule.action === "hash" && !opts.secretKey) {
|
|
4763
|
-
throw new Error(
|
|
4764
|
-
`rule '${rule.name}': action='hash' requires a non-empty secretKey (an unkeyed HMAC is a reversible pseudonym, not a redaction)`
|
|
4765
|
-
);
|
|
5351
|
+
pendingBlank = units.length > 0;
|
|
5352
|
+
continue;
|
|
4766
5353
|
}
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
if (!(start >= 0 && start < end && end <= text.length)) {
|
|
4779
|
-
invalid = true;
|
|
4780
|
-
continue;
|
|
5354
|
+
const lineSep = pendingBlank ? "\n\n" : "\n";
|
|
5355
|
+
pendingBlank = false;
|
|
5356
|
+
if (HEADING_OR_BULLET_RE.test(ln) || stripped.length <= 120) {
|
|
5357
|
+
units.push([lineSep, stripped, tokenize(stripped).length]);
|
|
5358
|
+
} else {
|
|
5359
|
+
let first = true;
|
|
5360
|
+
for (const sent of splitSentences(ln)) {
|
|
5361
|
+
const s = sent.trim();
|
|
5362
|
+
if (s) {
|
|
5363
|
+
units.push([first ? lineSep : " ", s, tokenize(s).length]);
|
|
5364
|
+
first = false;
|
|
4781
5365
|
}
|
|
4782
|
-
ruleSpans.push([start, end]);
|
|
4783
5366
|
}
|
|
4784
|
-
if (invalid) failed.push(rule.name);
|
|
4785
|
-
for (const [s, e] of ruleSpans) collected.push([s, e, rule]);
|
|
4786
|
-
} catch (exc) {
|
|
4787
|
-
failed.push(rule.name);
|
|
4788
|
-
if (opts.hooks) emitError(opts.hooks, exc, { stage: "redaction", rule: rule.name });
|
|
4789
5367
|
}
|
|
4790
5368
|
}
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
collected.sort((a, b) => a[0] - b[0] || b[1] - b[0] - (a[1] - a[0]));
|
|
4796
|
-
const merged = [];
|
|
4797
|
-
for (const [start, end, rule] of collected) {
|
|
4798
|
-
const last = merged[merged.length - 1];
|
|
4799
|
-
if (last && start < last[1]) {
|
|
4800
|
-
if (end > last[1]) last[1] = end;
|
|
5369
|
+
const normalizedUnits = [];
|
|
5370
|
+
for (const [uSep, uText, uTokens] of units) {
|
|
5371
|
+
if (uTokens <= maxWords) {
|
|
5372
|
+
normalizedUnits.push([uSep, uText, uTokens]);
|
|
4801
5373
|
continue;
|
|
4802
5374
|
}
|
|
4803
|
-
|
|
5375
|
+
const windowChars = maxWords * 4;
|
|
5376
|
+
const uChars = [...uText];
|
|
5377
|
+
let pos = 0;
|
|
5378
|
+
let pieceSep = uSep;
|
|
5379
|
+
while (pos < uChars.length) {
|
|
5380
|
+
let pieceChars = uChars.slice(pos, pos + windowChars);
|
|
5381
|
+
let piece = pieceChars.join("");
|
|
5382
|
+
if (pos + windowChars < uChars.length) {
|
|
5383
|
+
const ws = piece.lastIndexOf(" ");
|
|
5384
|
+
if (ws > Math.floor(windowChars / 2)) {
|
|
5385
|
+
piece = piece.slice(0, ws);
|
|
5386
|
+
pieceChars = [...piece];
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
let pieceTokens = tokenize(piece);
|
|
5390
|
+
if (pieceTokens.length > maxWords) {
|
|
5391
|
+
piece = pieceChars.slice(0, maxWords).join("");
|
|
5392
|
+
pieceTokens = tokenize(piece);
|
|
5393
|
+
}
|
|
5394
|
+
normalizedUnits.push([pieceSep, piece, pieceTokens.length]);
|
|
5395
|
+
pos += codePointLength(piece);
|
|
5396
|
+
pieceSep = "";
|
|
5397
|
+
}
|
|
4804
5398
|
}
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
5399
|
+
if (!normalizedUnits.some(([, , t]) => t)) return [];
|
|
5400
|
+
const chunks = [];
|
|
5401
|
+
let lastKept = null;
|
|
5402
|
+
let cur = [];
|
|
5403
|
+
let curTokens = 0;
|
|
5404
|
+
const flush = () => {
|
|
5405
|
+
const chunk = assembleUnits(cur);
|
|
5406
|
+
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
5407
|
+
chunks.push(chunk);
|
|
5408
|
+
lastKept = chunk;
|
|
4813
5409
|
}
|
|
4814
|
-
|
|
4815
|
-
|
|
5410
|
+
const carried = [];
|
|
5411
|
+
let carriedTokens = 0;
|
|
5412
|
+
for (let i = cur.length - 1; i >= 0; i--) {
|
|
5413
|
+
const prev = cur[i];
|
|
5414
|
+
if (prev[2] === 0 || carriedTokens + prev[2] > overlap) break;
|
|
5415
|
+
carried.unshift(prev);
|
|
5416
|
+
carriedTokens += prev[2];
|
|
5417
|
+
}
|
|
5418
|
+
cur = carried;
|
|
5419
|
+
curTokens = carriedTokens;
|
|
5420
|
+
};
|
|
5421
|
+
for (const unit of normalizedUnits) {
|
|
5422
|
+
if (cur.length && curTokens + unit[2] > maxWords) flush();
|
|
5423
|
+
cur.push(unit);
|
|
5424
|
+
curTokens += unit[2];
|
|
4816
5425
|
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
5426
|
+
if (cur.length) {
|
|
5427
|
+
const chunk = assembleUnits(cur);
|
|
5428
|
+
if (chunk && (lastKept === null || jaccardSim(chunk, lastKept) < 0.92)) {
|
|
5429
|
+
chunks.push(chunk);
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
return chunks;
|
|
4821
5433
|
}
|
|
4822
5434
|
|
|
5435
|
+
// src/actions.ts
|
|
5436
|
+
init_errors();
|
|
5437
|
+
init_hooks();
|
|
5438
|
+
init_llm();
|
|
5439
|
+
init_redaction();
|
|
5440
|
+
|
|
4823
5441
|
// src/sandbox.ts
|
|
4824
5442
|
init_errors();
|
|
4825
5443
|
var DEFAULT_TIMEOUT = 30;
|
|
@@ -5358,6 +5976,39 @@ async function nearestField(pool, vector, sourceIds, docType) {
|
|
|
5358
5976
|
if (similarity < FIELD_RESOLUTION_SIMILARITY_FLOOR) return null;
|
|
5359
5977
|
return String(row.key_norm);
|
|
5360
5978
|
}
|
|
5979
|
+
|
|
5980
|
+
// src/sentinels.ts
|
|
5981
|
+
var UNSET = /* @__PURE__ */ Symbol.for("context_engine.UNSET");
|
|
5982
|
+
var TRUSTED = /* @__PURE__ */ Symbol.for("context_engine.TRUSTED");
|
|
5983
|
+
var warnedMethods = /* @__PURE__ */ new Set();
|
|
5984
|
+
function resolvePrincipals(value, method) {
|
|
5985
|
+
if (value === TRUSTED) return null;
|
|
5986
|
+
if (value === void 0 || value === null) {
|
|
5987
|
+
if (!warnedMethods.has(method)) {
|
|
5988
|
+
warnedMethods.add(method);
|
|
5989
|
+
process.emitWarning(
|
|
5990
|
+
`${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.`,
|
|
5991
|
+
{ type: "DeprecationWarning", code: "CE_PRINCIPALS_NULL" }
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
return null;
|
|
5995
|
+
}
|
|
5996
|
+
if (!Array.isArray(value)) {
|
|
5997
|
+
throw new TypeError(
|
|
5998
|
+
`${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.`
|
|
5999
|
+
);
|
|
6000
|
+
}
|
|
6001
|
+
return value;
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
// src/tools/acl.ts
|
|
6005
|
+
function aclVisible(acl, principals) {
|
|
6006
|
+
if (principals === null || principals === TRUSTED) return true;
|
|
6007
|
+
if (acl == null) return true;
|
|
6008
|
+
if (!acl.length) return false;
|
|
6009
|
+
const held = new Set(principals ?? []);
|
|
6010
|
+
return acl.some((p) => held.has(p));
|
|
6011
|
+
}
|
|
5361
6012
|
init_extraction();
|
|
5362
6013
|
init_hooks();
|
|
5363
6014
|
var TIMEOUT_MS2 = 3e4;
|
|
@@ -5445,6 +6096,7 @@ async function pollEmbeddingBatch(cfg2, batchId, opts = {}) {
|
|
|
5445
6096
|
}
|
|
5446
6097
|
|
|
5447
6098
|
// src/ingest.ts
|
|
6099
|
+
init_redaction();
|
|
5448
6100
|
init_usage();
|
|
5449
6101
|
var MODE_RANK = { hybrid: 0, graph: 1 };
|
|
5450
6102
|
var EMBED_BATCH = 256;
|
|
@@ -5531,7 +6183,9 @@ async function prepare(opts) {
|
|
|
5531
6183
|
mediaOnly: false,
|
|
5532
6184
|
providerTokens: {},
|
|
5533
6185
|
contentHash: calculateContentHash({ fullText: clean }),
|
|
5534
|
-
isMarkdown: false
|
|
6186
|
+
isMarkdown: false,
|
|
6187
|
+
unreadableReason: null,
|
|
6188
|
+
unreadablePages: 0
|
|
5535
6189
|
};
|
|
5536
6190
|
}
|
|
5537
6191
|
const content = opts.content;
|
|
@@ -5539,11 +6193,12 @@ async function prepare(opts) {
|
|
|
5539
6193
|
const mime = mimeForFilename(fname) || "";
|
|
5540
6194
|
const extracted = await extract2(content, fname, mime || null, {
|
|
5541
6195
|
visionLlm: opts.config.visionLlm,
|
|
6196
|
+
extraction: opts.config.extraction,
|
|
5542
6197
|
hooks: opts.hooks
|
|
5543
6198
|
});
|
|
5544
6199
|
let body = sanitizeText(extracted.text || "") || "";
|
|
5545
6200
|
const effectiveMime = mime || inferMime(body, fname) || DEFAULT_MIME;
|
|
5546
|
-
if (effectiveMime === "application/pdf" && body) body = dedupLines(body);
|
|
6201
|
+
if (effectiveMime === "application/pdf" && body && !extracted.isMarkdown) body = dedupLines(body);
|
|
5547
6202
|
return {
|
|
5548
6203
|
text: body,
|
|
5549
6204
|
mime: effectiveMime,
|
|
@@ -5554,7 +6209,9 @@ async function prepare(opts) {
|
|
|
5554
6209
|
mediaOnly: Boolean(extracted.mediaOnly),
|
|
5555
6210
|
providerTokens: { ...extracted.providerTokens ?? {} },
|
|
5556
6211
|
contentHash: calculateContentHash({ docBytes: content }),
|
|
5557
|
-
isMarkdown: Boolean(extracted.isMarkdown)
|
|
6212
|
+
isMarkdown: Boolean(extracted.isMarkdown),
|
|
6213
|
+
unreadableReason: extracted.unreadableReason ?? null,
|
|
6214
|
+
unreadablePages: extracted.unreadablePages ?? 0
|
|
5558
6215
|
};
|
|
5559
6216
|
}
|
|
5560
6217
|
function computeUnits(prepared) {
|
|
@@ -5732,7 +6389,11 @@ async function decide(pool, opts) {
|
|
|
5732
6389
|
}
|
|
5733
6390
|
const same = stored != null && incoming != null && (Buffer.isBuffer(stored) && Buffer.isBuffer(incoming) ? Buffer.compare(stored, incoming) === 0 : stored === incoming);
|
|
5734
6391
|
if (!same) return { action: "process", documentId: existingId };
|
|
5735
|
-
if (existing.status === "skipped")
|
|
6392
|
+
if (existing.status === "skipped") {
|
|
6393
|
+
const metaData = existing.meta_data;
|
|
6394
|
+
if (metaData?.unreadable_reason) return { action: "process", documentId: existingId };
|
|
6395
|
+
return { action: "skip", documentId: existingId };
|
|
6396
|
+
}
|
|
5736
6397
|
const reqMode = opts.request.mode ?? "hybrid";
|
|
5737
6398
|
if ((MODE_RANK[reqMode] ?? 0) <= (MODE_RANK[existing.mode || "hybrid"] ?? 0)) {
|
|
5738
6399
|
return { action: "skip", documentId: existingId };
|
|
@@ -6023,6 +6684,22 @@ function singleReport(doc) {
|
|
|
6023
6684
|
async function maybeAwait(value) {
|
|
6024
6685
|
return value;
|
|
6025
6686
|
}
|
|
6687
|
+
function progress(hooks, request, displayName, documentId, stage, state, detail = {}) {
|
|
6688
|
+
emitProgress(hooks, {
|
|
6689
|
+
sourceId: request.sourceId ?? null,
|
|
6690
|
+
externalId: request.externalId ?? null,
|
|
6691
|
+
documentId: documentId == null ? null : String(documentId),
|
|
6692
|
+
name: displayName,
|
|
6693
|
+
stage,
|
|
6694
|
+
state,
|
|
6695
|
+
detail: { ...detail }
|
|
6696
|
+
});
|
|
6697
|
+
}
|
|
6698
|
+
function extractMethod(prepared, text) {
|
|
6699
|
+
if (text != null) return "text";
|
|
6700
|
+
if (Object.keys(prepared.providerTokens).length || prepared.isMarkdown) return "vision";
|
|
6701
|
+
return "parser";
|
|
6702
|
+
}
|
|
6026
6703
|
async function runModeUpgrade(request, opts) {
|
|
6027
6704
|
await refreshOnSkip(request, {
|
|
6028
6705
|
pool: opts.pool,
|
|
@@ -6036,6 +6713,7 @@ async function runModeUpgrade(request, opts) {
|
|
|
6036
6713
|
let chunkCount = 0;
|
|
6037
6714
|
try {
|
|
6038
6715
|
if (request.mode === "graph" && opts.graphStage) {
|
|
6716
|
+
progress(opts.hooks, request, opts.displayName, opts.documentId, "graph", "started");
|
|
6039
6717
|
graphUnits2 = Number(
|
|
6040
6718
|
await maybeAwait(
|
|
6041
6719
|
opts.graphStage({
|
|
@@ -6046,6 +6724,9 @@ async function runModeUpgrade(request, opts) {
|
|
|
6046
6724
|
})
|
|
6047
6725
|
) || 0
|
|
6048
6726
|
);
|
|
6727
|
+
progress(opts.hooks, request, opts.displayName, opts.documentId, "graph", "done", {
|
|
6728
|
+
graph_units: graphUnits2
|
|
6729
|
+
});
|
|
6049
6730
|
}
|
|
6050
6731
|
chunkCount = await upgradeMode(opts.pool, opts.documentId, request.mode ?? "hybrid");
|
|
6051
6732
|
} catch (exc) {
|
|
@@ -6135,6 +6816,7 @@ async function runIngest(request, opts) {
|
|
|
6135
6816
|
);
|
|
6136
6817
|
}
|
|
6137
6818
|
}
|
|
6819
|
+
progress(opts.hooks, request, displayName, null, "extract", "started");
|
|
6138
6820
|
const prepared = await prepare({
|
|
6139
6821
|
content,
|
|
6140
6822
|
filename,
|
|
@@ -6143,6 +6825,11 @@ async function runIngest(request, opts) {
|
|
|
6143
6825
|
config: opts.config,
|
|
6144
6826
|
hooks: opts.hooks
|
|
6145
6827
|
});
|
|
6828
|
+
progress(opts.hooks, request, displayName, null, "extract", "done", {
|
|
6829
|
+
pages: prepared.pages,
|
|
6830
|
+
method: extractMethod(prepared, text),
|
|
6831
|
+
mime: prepared.mime
|
|
6832
|
+
});
|
|
6146
6833
|
const ingestHash = sha256Bytes(prepared.text || "");
|
|
6147
6834
|
const decision = await decide(opts.pool, {
|
|
6148
6835
|
request: { ...request, mode },
|
|
@@ -6229,8 +6916,11 @@ async function runIngest(request, opts) {
|
|
|
6229
6916
|
const metaUpdates = { ...request.metaData ?? {} };
|
|
6230
6917
|
metaUpdates.mime_type = prepared.mime;
|
|
6231
6918
|
if (prepared.contentHash) metaUpdates.content_hash = prepared.contentHash;
|
|
6919
|
+
metaUpdates.unreadable_reason = prepared.unreadableReason;
|
|
6920
|
+
metaUpdates.unreadable_pages = prepared.unreadablePages;
|
|
6232
6921
|
const redactionFailed = [];
|
|
6233
6922
|
let documentText;
|
|
6923
|
+
progress(opts.hooks, request, displayName, documentId, "redact", "started");
|
|
6234
6924
|
try {
|
|
6235
6925
|
documentText = redactDocumentText(
|
|
6236
6926
|
prepared.text,
|
|
@@ -6243,6 +6933,9 @@ async function runIngest(request, opts) {
|
|
|
6243
6933
|
await finalizeDocument(opts.pool, documentId, { status: "failed", error: String(exc) });
|
|
6244
6934
|
throw exc;
|
|
6245
6935
|
}
|
|
6936
|
+
progress(opts.hooks, request, displayName, documentId, "redact", "done", {
|
|
6937
|
+
rules_failed: redactionFailed.length
|
|
6938
|
+
});
|
|
6246
6939
|
if (!prepared.text.trim() || prepared.mediaOnly || looksMostlyBoilerplate(prepared.text)) {
|
|
6247
6940
|
const reason = prepared.mediaOnly ? "media-only (no extractable text)" : "no extractable text";
|
|
6248
6941
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, []);
|
|
@@ -6268,7 +6961,13 @@ async function runIngest(request, opts) {
|
|
|
6268
6961
|
graphUnits: 0,
|
|
6269
6962
|
providerTokens: {},
|
|
6270
6963
|
error: reason,
|
|
6271
|
-
redactionFailed
|
|
6964
|
+
redactionFailed,
|
|
6965
|
+
// Extraction RAN here — an unreadable scan is exactly what lands in
|
|
6966
|
+
// this branch, so this is the report that has to say why. The
|
|
6967
|
+
// dedup-skip/upgrade sites stay at the defaults: they never extracted
|
|
6968
|
+
// and would be claiming blind.
|
|
6969
|
+
unreadableReason: prepared.unreadableReason,
|
|
6970
|
+
unreadablePages: prepared.unreadablePages
|
|
6272
6971
|
});
|
|
6273
6972
|
}
|
|
6274
6973
|
const units = computeUnits(prepared);
|
|
@@ -6278,18 +6977,26 @@ async function runIngest(request, opts) {
|
|
|
6278
6977
|
let rows = [];
|
|
6279
6978
|
let effectiveMime = prepared.mime;
|
|
6280
6979
|
try {
|
|
6980
|
+
progress(opts.hooks, request, displayName, documentId, "chunk", "started");
|
|
6281
6981
|
[rows, effectiveMime] = buildChunks(prepared, request.name, {
|
|
6282
6982
|
policy: opts.config.redaction,
|
|
6283
6983
|
secretKey: opts.config.secretKey,
|
|
6284
6984
|
hooks: opts.hooks,
|
|
6285
6985
|
failed: redactionFailed
|
|
6286
6986
|
});
|
|
6987
|
+
progress(opts.hooks, request, displayName, documentId, "chunk", "done", { chunks: rows.length });
|
|
6988
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "started");
|
|
6287
6989
|
if (request.batch) {
|
|
6288
6990
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, rows);
|
|
6289
6991
|
const batchId = await submitEmbeddingBatch(
|
|
6290
6992
|
opts.config.embedding,
|
|
6291
6993
|
rows.map((r) => r.text)
|
|
6292
6994
|
);
|
|
6995
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "done", {
|
|
6996
|
+
batch: true,
|
|
6997
|
+
batch_id: batchId,
|
|
6998
|
+
chunks: rows.length
|
|
6999
|
+
});
|
|
6293
7000
|
metaUpdates.mime_type = effectiveMime;
|
|
6294
7001
|
metaUpdates.batch = {
|
|
6295
7002
|
id: batchId,
|
|
@@ -6319,15 +7026,26 @@ async function runIngest(request, opts) {
|
|
|
6319
7026
|
graphUnits: 0,
|
|
6320
7027
|
providerTokens,
|
|
6321
7028
|
error: null,
|
|
6322
|
-
redactionFailed
|
|
7029
|
+
redactionFailed,
|
|
7030
|
+
// Extraction RAN here just like the skipped/failed/completed sites —
|
|
7031
|
+
// this report has to say why pages were unreadable too, not leave
|
|
7032
|
+
// the caller to re-derive it later from listDocuments.
|
|
7033
|
+
unreadableReason: prepared.unreadableReason,
|
|
7034
|
+
unreadablePages: prepared.unreadablePages
|
|
6323
7035
|
});
|
|
6324
7036
|
}
|
|
6325
7037
|
const embeddingTokens = await embedChunks(opts.embedder, rows);
|
|
6326
7038
|
if (embeddingTokens) providerTokens.embedding_tokens = embeddingTokens;
|
|
6327
7039
|
await recordEmbeddingDim(opts.pool, opts.embedder.dim);
|
|
6328
7040
|
await opts.backend.upsertChunks(documentId, request.sourceId ?? null, request.acl ?? null, rows);
|
|
7041
|
+
progress(opts.hooks, request, displayName, documentId, "embed", "done", {
|
|
7042
|
+
batch: false,
|
|
7043
|
+
chunks: rows.length,
|
|
7044
|
+
tokens: embeddingTokens
|
|
7045
|
+
});
|
|
6329
7046
|
const structuredKw = {};
|
|
6330
7047
|
if (request.extractStructured && opts.config.llm) {
|
|
7048
|
+
progress(opts.hooks, request, displayName, documentId, "structured", "started");
|
|
6331
7049
|
const extraction = await extractStructuredData(documentText, {
|
|
6332
7050
|
llmCfg: opts.config.llm,
|
|
6333
7051
|
fieldHints: request.fieldHints
|
|
@@ -6368,9 +7086,15 @@ async function runIngest(request, opts) {
|
|
|
6368
7086
|
document: displayName
|
|
6369
7087
|
});
|
|
6370
7088
|
}
|
|
7089
|
+
progress(opts.hooks, request, displayName, documentId, "structured", "done", {
|
|
7090
|
+
document_type: extraction.documentType,
|
|
7091
|
+
keys: extraction.keysNormalized.length,
|
|
7092
|
+
quality: extraction.quality
|
|
7093
|
+
});
|
|
6371
7094
|
}
|
|
6372
7095
|
let graphUnits2 = 0;
|
|
6373
7096
|
if (mode === "graph" && opts.graphStage) {
|
|
7097
|
+
progress(opts.hooks, request, displayName, documentId, "graph", "started");
|
|
6374
7098
|
graphUnits2 = Number(
|
|
6375
7099
|
await maybeAwait(
|
|
6376
7100
|
opts.graphStage({
|
|
@@ -6381,6 +7105,7 @@ async function runIngest(request, opts) {
|
|
|
6381
7105
|
})
|
|
6382
7106
|
) || 0
|
|
6383
7107
|
);
|
|
7108
|
+
progress(opts.hooks, request, displayName, documentId, "graph", "done", { graph_units: graphUnits2 });
|
|
6384
7109
|
}
|
|
6385
7110
|
metaUpdates.mime_type = effectiveMime;
|
|
6386
7111
|
await finalizeDocument(opts.pool, documentId, {
|
|
@@ -6420,7 +7145,9 @@ async function runIngest(request, opts) {
|
|
|
6420
7145
|
graphUnits: graphUnits2,
|
|
6421
7146
|
providerTokens,
|
|
6422
7147
|
error: null,
|
|
6423
|
-
redactionFailed
|
|
7148
|
+
redactionFailed,
|
|
7149
|
+
unreadableReason: prepared.unreadableReason,
|
|
7150
|
+
unreadablePages: prepared.unreadablePages
|
|
6424
7151
|
});
|
|
6425
7152
|
} catch (exc) {
|
|
6426
7153
|
emitError(opts.hooks, exc, { stage: "ingest", document: displayName });
|
|
@@ -6435,7 +7162,9 @@ async function runIngest(request, opts) {
|
|
|
6435
7162
|
graphUnits: 0,
|
|
6436
7163
|
providerTokens,
|
|
6437
7164
|
error: String(exc),
|
|
6438
|
-
redactionFailed
|
|
7165
|
+
redactionFailed,
|
|
7166
|
+
unreadableReason: prepared.unreadableReason,
|
|
7167
|
+
unreadablePages: prepared.unreadablePages
|
|
6439
7168
|
});
|
|
6440
7169
|
}
|
|
6441
7170
|
}
|
|
@@ -6532,12 +7261,7 @@ function parseDocumentId(documentId) {
|
|
|
6532
7261
|
}
|
|
6533
7262
|
return raw;
|
|
6534
7263
|
}
|
|
6535
|
-
|
|
6536
|
-
if (principals === null) return true;
|
|
6537
|
-
if (acl == null) return true;
|
|
6538
|
-
const held = new Set(principals);
|
|
6539
|
-
return acl.some((p) => held.has(p));
|
|
6540
|
-
}
|
|
7264
|
+
var visible = aclVisible;
|
|
6541
7265
|
function scopeSql(sourceIds, principals, params) {
|
|
6542
7266
|
const where = [];
|
|
6543
7267
|
if (sourceIds != null) {
|
|
@@ -6626,7 +7350,9 @@ async function getDocumentRow(pool, documentId, principals) {
|
|
|
6626
7350
|
createdAt: doc.created_at,
|
|
6627
7351
|
updatedAt: doc.updated_at,
|
|
6628
7352
|
startedAt: doc.started_at,
|
|
6629
|
-
completedAt: doc.completed_at
|
|
7353
|
+
completedAt: doc.completed_at,
|
|
7354
|
+
unreadableReason: doc.meta_data?.unreadable_reason ?? null,
|
|
7355
|
+
unreadablePages: doc.meta_data?.unreadable_pages ?? 0
|
|
6630
7356
|
};
|
|
6631
7357
|
}
|
|
6632
7358
|
async function stats(pool, sourceId) {
|
|
@@ -6683,7 +7409,7 @@ async function listDocuments(opts) {
|
|
|
6683
7409
|
params.push(limit + 1);
|
|
6684
7410
|
const { rows } = await opts.pool.query(
|
|
6685
7411
|
`SELECT id, source_id, external_id, name, description, mode, mime_type, lang, status,
|
|
6686
|
-
document_type, created_at, updated_at, acl
|
|
7412
|
+
document_type, created_at, updated_at, acl, meta_data
|
|
6687
7413
|
FROM context_engine_documents
|
|
6688
7414
|
${where}
|
|
6689
7415
|
ORDER BY created_at DESC, id DESC
|
|
@@ -6708,7 +7434,15 @@ async function listDocuments(opts) {
|
|
|
6708
7434
|
hooks: opts.hooks
|
|
6709
7435
|
}),
|
|
6710
7436
|
createdAt: doc.created_at instanceof Date ? doc.created_at.toISOString() : doc.created_at,
|
|
6711
|
-
updatedAt: doc.updated_at instanceof Date ? doc.updated_at.toISOString() : doc.updated_at
|
|
7437
|
+
updatedAt: doc.updated_at instanceof Date ? doc.updated_at.toISOString() : doc.updated_at,
|
|
7438
|
+
unreadableReason: doc.meta_data?.unreadable_reason ?? null,
|
|
7439
|
+
unreadablePages: doc.meta_data?.unreadable_pages ?? 0,
|
|
7440
|
+
// WHO can see this. Stored, enforced on every query and editable through
|
|
7441
|
+
// updateDocument — and, until this line, invisible to every caller,
|
|
7442
|
+
// because this serializer builds a FIXED object. `null` is UNRESTRICTED
|
|
7443
|
+
// and must stay null; an empty array would read as "nobody", which is the
|
|
7444
|
+
// opposite claim.
|
|
7445
|
+
acl: doc.acl ?? null
|
|
6712
7446
|
}));
|
|
6713
7447
|
const result = {
|
|
6714
7448
|
documents,
|
|
@@ -6866,9 +7600,9 @@ async function compute(instruction, opts) {
|
|
|
6866
7600
|
return `mime_type ILIKE $${params.length}`;
|
|
6867
7601
|
}).join(" OR ");
|
|
6868
7602
|
where = where ? `${where} AND (${mimeClause})` : `WHERE (${mimeClause})`;
|
|
6869
|
-
if (opts.
|
|
7603
|
+
if (opts.documentIds?.length) {
|
|
6870
7604
|
const parsedIds = [];
|
|
6871
|
-
for (const did of opts.
|
|
7605
|
+
for (const did of opts.documentIds) {
|
|
6872
7606
|
try {
|
|
6873
7607
|
parsedIds.push(parseDocumentId(did));
|
|
6874
7608
|
} catch {
|
|
@@ -6896,13 +7630,13 @@ async function compute(instruction, opts) {
|
|
|
6896
7630
|
}
|
|
6897
7631
|
if (tabular.length > MAX_COMPUTE_DOCUMENTS) {
|
|
6898
7632
|
throw new exports.EngineActionError(
|
|
6899
|
-
`more than ${MAX_COMPUTE_DOCUMENTS} tabular documents are in scope for compute() \u2014 narrow the request with
|
|
7633
|
+
`more than ${MAX_COMPUTE_DOCUMENTS} tabular documents are in scope for compute() \u2014 narrow the request with documentIds or sourceIds`
|
|
6900
7634
|
);
|
|
6901
7635
|
}
|
|
6902
7636
|
const totalChars = tabular.reduce((n, d) => n + String(d.text ?? "").length, 0);
|
|
6903
7637
|
if (totalChars > MAX_COMPUTE_TEXT_CHARS) {
|
|
6904
7638
|
throw new exports.EngineActionError(
|
|
6905
|
-
`in-scope spreadsheet text too large to load (${totalChars} chars > ${MAX_COMPUTE_TEXT_CHARS} cap) \u2014 narrow the request with
|
|
7639
|
+
`in-scope spreadsheet text too large to load (${totalChars} chars > ${MAX_COMPUTE_TEXT_CHARS} cap) \u2014 narrow the request with documentIds or sourceIds`
|
|
6906
7640
|
);
|
|
6907
7641
|
}
|
|
6908
7642
|
const dfs = {};
|
|
@@ -6942,182 +7676,52 @@ ${schemaLines.join("\n")}`;
|
|
|
6942
7676
|
user: userPrompt,
|
|
6943
7677
|
jsonMode: false
|
|
6944
7678
|
});
|
|
6945
|
-
} catch (exc) {
|
|
6946
|
-
emitError(opts.hooks, exc, { stage: "compute_codegen", instruction: instruction.slice(0, 200) });
|
|
6947
|
-
throw exc;
|
|
6948
|
-
}
|
|
6949
|
-
const code = stripCodeFences(rawCode);
|
|
6950
|
-
const execResult = await executeSafeCode(code, { dfs, documents }, { timeout });
|
|
6951
|
-
if (!execResult.success) {
|
|
6952
|
-
let maskedCode;
|
|
6953
|
-
try {
|
|
6954
|
-
maskedCode = redactValueRecursive(code.slice(0, 500), opts.config.redaction, {
|
|
6955
|
-
principals,
|
|
6956
|
-
secretKey: opts.config.secretKey,
|
|
6957
|
-
hooks: opts.hooks
|
|
6958
|
-
});
|
|
6959
|
-
} catch {
|
|
6960
|
-
maskedCode = "<redaction failed: code omitted>";
|
|
6961
|
-
}
|
|
6962
|
-
emitError(opts.hooks, new Error(execResult.error || "compute execution failed"), {
|
|
6963
|
-
stage: "compute_exec",
|
|
6964
|
-
code: maskedCode
|
|
6965
|
-
});
|
|
6966
|
-
}
|
|
6967
|
-
const result = {
|
|
6968
|
-
success: execResult.success,
|
|
6969
|
-
result: execResult.result,
|
|
6970
|
-
code,
|
|
6971
|
-
stdout: execResult.stdout ?? "",
|
|
6972
|
-
error: execResult.error,
|
|
6973
|
-
executionTime: execResult.executionTime,
|
|
6974
|
-
documentsUsed: documents.map((d) => d.id),
|
|
6975
|
-
providerTokens: {
|
|
6976
|
-
llm_input: tokens?.input ?? 0,
|
|
6977
|
-
llm_output: tokens?.output ?? 0
|
|
6978
|
-
}
|
|
6979
|
-
};
|
|
6980
|
-
return redactValueRecursive(result, opts.config.redaction, {
|
|
6981
|
-
principals,
|
|
6982
|
-
secretKey: opts.config.secretKey,
|
|
6983
|
-
hooks: opts.hooks
|
|
6984
|
-
});
|
|
6985
|
-
}
|
|
6986
|
-
var embeddingSchema = zod.z.object({
|
|
6987
|
-
provider: zod.z.enum(["openai", "azure_openai", "gemini", "voyage", "cohere", "custom"]),
|
|
6988
|
-
model: zod.z.string(),
|
|
6989
|
-
dim: zod.z.number().int().positive().nullable().optional().default(null),
|
|
6990
|
-
apiKey: zod.z.string().nullable().optional().default(null),
|
|
6991
|
-
baseUrl: zod.z.string().nullable().optional().default(null)
|
|
6992
|
-
});
|
|
6993
|
-
var llmSchema = zod.z.object({
|
|
6994
|
-
provider: zod.z.enum(["anthropic", "openai", "azure_openai", "gemini", "bedrock", "custom"]),
|
|
6995
|
-
model: zod.z.string(),
|
|
6996
|
-
apiKey: zod.z.string().nullable().optional().default(null),
|
|
6997
|
-
baseUrl: zod.z.string().nullable().optional().default(null)
|
|
6998
|
-
});
|
|
6999
|
-
var graphSchema = zod.z.object({
|
|
7000
|
-
enabled: zod.z.boolean().default(false),
|
|
7001
|
-
neo4jUri: zod.z.string().nullable().optional().default(null),
|
|
7002
|
-
neo4jUser: zod.z.string().default("neo4j"),
|
|
7003
|
-
neo4jPassword: zod.z.string().nullable().optional().default(null),
|
|
7004
|
-
neo4jDatabase: zod.z.string().default("neo4j"),
|
|
7005
|
-
extractionLlm: llmSchema.nullable().optional().default(null)
|
|
7006
|
-
});
|
|
7007
|
-
var rerankerSchema = zod.z.object({
|
|
7008
|
-
enabled: zod.z.boolean().default(false),
|
|
7009
|
-
provider: zod.z.enum(["cohere", "voyage", "jina", "custom"]).nullable().optional().default(null),
|
|
7010
|
-
model: zod.z.string().nullable().optional().default(null),
|
|
7011
|
-
apiKey: zod.z.string().nullable().optional().default(null),
|
|
7012
|
-
baseUrl: zod.z.string().nullable().optional().default(null),
|
|
7013
|
-
candidates: zod.z.number().int().positive().default(50)
|
|
7014
|
-
});
|
|
7015
|
-
var fusionSchema = zod.z.object({
|
|
7016
|
-
method: zod.z.literal("rrf").default("rrf"),
|
|
7017
|
-
k: zod.z.number().int().positive().default(60),
|
|
7018
|
-
weights: zod.z.record(zod.z.string(), zod.z.number()).default({ fts: 1, trgm: 0.8, ann: 1, graph: 1 })
|
|
7019
|
-
});
|
|
7020
|
-
var storageSchema = zod.z.object({
|
|
7021
|
-
backend: zod.z.literal("postgres").default("postgres"),
|
|
7022
|
-
annExactThreshold: zod.z.number().int().positive().default(5e4)
|
|
7023
|
-
});
|
|
7024
|
-
var ContextEngineConfig = class _ContextEngineConfig {
|
|
7025
|
-
databaseUrl;
|
|
7026
|
-
storage;
|
|
7027
|
-
defaultMode;
|
|
7028
|
-
embedding;
|
|
7029
|
-
llm;
|
|
7030
|
-
visionLlm;
|
|
7031
|
-
graph;
|
|
7032
|
-
reranker;
|
|
7033
|
-
fusion;
|
|
7034
|
-
enableCodeExecution;
|
|
7035
|
-
secretKey;
|
|
7036
|
-
redaction;
|
|
7037
|
-
constructor(init) {
|
|
7038
|
-
this.databaseUrl = init.databaseUrl;
|
|
7039
|
-
this.storage = storageSchema.parse(init.storage ?? {});
|
|
7040
|
-
this.defaultMode = init.defaultMode ?? "hybrid";
|
|
7041
|
-
this.embedding = embeddingSchema.parse(init.embedding);
|
|
7042
|
-
this.llm = init.llm ? llmSchema.parse(init.llm) : null;
|
|
7043
|
-
this.visionLlm = init.visionLlm ? llmSchema.parse(init.visionLlm) : null;
|
|
7044
|
-
this.graph = graphSchema.parse(init.graph ?? {});
|
|
7045
|
-
this.reranker = rerankerSchema.parse(init.reranker ?? {});
|
|
7046
|
-
this.fusion = fusionSchema.parse(init.fusion ?? {});
|
|
7047
|
-
this.enableCodeExecution = init.enableCodeExecution ?? false;
|
|
7048
|
-
this.secretKey = init.secretKey ?? null;
|
|
7049
|
-
this.redaction = init.redaction instanceof RedactionPolicy ? init.redaction : new RedactionPolicy(init.redaction ?? {});
|
|
7050
|
-
this.validate();
|
|
7051
|
-
}
|
|
7052
|
-
validate() {
|
|
7053
|
-
if (this.graph.enabled && !(this.graph.neo4jUri && this.graph.neo4jPassword && this.graph.extractionLlm)) {
|
|
7054
|
-
throw new Error("graph enabled but neo4jUri/neo4jPassword/extractionLlm missing");
|
|
7055
|
-
}
|
|
7056
|
-
if (this.reranker.enabled && !(this.reranker.provider && this.reranker.apiKey)) {
|
|
7057
|
-
throw new Error("reranker enabled but provider/apiKey missing");
|
|
7058
|
-
}
|
|
7059
|
-
if (this.defaultMode === "graph" && !this.graph.enabled) {
|
|
7060
|
-
throw new Error("defaultMode is 'graph' but graph enabled is false");
|
|
7061
|
-
}
|
|
7062
|
-
for (const rule of this.redaction.rules) {
|
|
7063
|
-
if (rule.action === "hash" && !this.secretKey) {
|
|
7064
|
-
throw new Error(
|
|
7065
|
-
`redaction rule '${rule.name}': action='hash' requires ContextEngineConfig.secretKey to be set`
|
|
7066
|
-
);
|
|
7067
|
-
}
|
|
7068
|
-
}
|
|
7069
|
-
}
|
|
7070
|
-
static fromEnv(overrides = {}) {
|
|
7071
|
-
const env = loadCeEnv();
|
|
7072
|
-
const merged = deepMerge(env, overrides);
|
|
7073
|
-
if (!merged.databaseUrl || !merged.embedding) {
|
|
7074
|
-
throw new Error(
|
|
7075
|
-
"ContextEngineConfig.fromEnv requires CE_DATABASE_URL and CE_EMBEDDING__PROVIDER/MODEL (or explicit overrides)"
|
|
7076
|
-
);
|
|
7077
|
-
}
|
|
7078
|
-
return new _ContextEngineConfig(merged);
|
|
7079
|
-
}
|
|
7080
|
-
};
|
|
7081
|
-
function camelize(key) {
|
|
7082
|
-
return key.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
7083
|
-
}
|
|
7084
|
-
function loadCeEnv() {
|
|
7085
|
-
const root = {};
|
|
7086
|
-
for (const [raw, value] of Object.entries(process.env)) {
|
|
7087
|
-
if (!raw.startsWith("CE_") || value === void 0) continue;
|
|
7088
|
-
const path = raw.slice(3).split("__").map(camelize);
|
|
7089
|
-
let cur = root;
|
|
7090
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
7091
|
-
const k = path[i];
|
|
7092
|
-
const next = cur[k];
|
|
7093
|
-
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
7094
|
-
cur = cur[k];
|
|
7095
|
-
}
|
|
7096
|
-
cur[path[path.length - 1]] = coerceEnv(value);
|
|
7097
|
-
}
|
|
7098
|
-
return root;
|
|
7099
|
-
}
|
|
7100
|
-
function coerceEnv(value) {
|
|
7101
|
-
if (value === "true") return true;
|
|
7102
|
-
if (value === "false") return false;
|
|
7103
|
-
if (/^-?\d+$/.test(value)) return Number(value);
|
|
7104
|
-
if (/^-?\d+\.\d+$/.test(value)) return Number(value);
|
|
7105
|
-
return value;
|
|
7106
|
-
}
|
|
7107
|
-
function deepMerge(a, b) {
|
|
7108
|
-
const out = { ...a };
|
|
7109
|
-
for (const [k, v] of Object.entries(b)) {
|
|
7110
|
-
if (v === void 0) continue;
|
|
7111
|
-
const existing = out[k];
|
|
7112
|
-
if (v && typeof v === "object" && !Array.isArray(v) && existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
7113
|
-
out[k] = deepMerge(existing, v);
|
|
7114
|
-
} else {
|
|
7115
|
-
out[k] = v;
|
|
7679
|
+
} catch (exc) {
|
|
7680
|
+
emitError(opts.hooks, exc, { stage: "compute_codegen", instruction: instruction.slice(0, 200) });
|
|
7681
|
+
throw exc;
|
|
7682
|
+
}
|
|
7683
|
+
const code = stripCodeFences(rawCode);
|
|
7684
|
+
const execResult = await executeSafeCode(code, { dfs, documents }, { timeout });
|
|
7685
|
+
if (!execResult.success) {
|
|
7686
|
+
let maskedCode;
|
|
7687
|
+
try {
|
|
7688
|
+
maskedCode = redactValueRecursive(code.slice(0, 500), opts.config.redaction, {
|
|
7689
|
+
principals,
|
|
7690
|
+
secretKey: opts.config.secretKey,
|
|
7691
|
+
hooks: opts.hooks
|
|
7692
|
+
});
|
|
7693
|
+
} catch {
|
|
7694
|
+
maskedCode = "<redaction failed: code omitted>";
|
|
7116
7695
|
}
|
|
7696
|
+
emitError(opts.hooks, new Error(execResult.error || "compute execution failed"), {
|
|
7697
|
+
stage: "compute_exec",
|
|
7698
|
+
code: maskedCode
|
|
7699
|
+
});
|
|
7117
7700
|
}
|
|
7118
|
-
|
|
7701
|
+
const result = {
|
|
7702
|
+
success: execResult.success,
|
|
7703
|
+
result: execResult.result,
|
|
7704
|
+
code,
|
|
7705
|
+
stdout: execResult.stdout ?? "",
|
|
7706
|
+
error: execResult.error,
|
|
7707
|
+
executionTime: execResult.executionTime,
|
|
7708
|
+
documentsUsed: documents.map((d) => d.id),
|
|
7709
|
+
providerTokens: {
|
|
7710
|
+
llm_input: tokens?.input ?? 0,
|
|
7711
|
+
llm_output: tokens?.output ?? 0
|
|
7712
|
+
}
|
|
7713
|
+
};
|
|
7714
|
+
return redactValueRecursive(result, opts.config.redaction, {
|
|
7715
|
+
principals,
|
|
7716
|
+
secretKey: opts.config.secretKey,
|
|
7717
|
+
hooks: opts.hooks
|
|
7718
|
+
});
|
|
7119
7719
|
}
|
|
7120
7720
|
|
|
7721
|
+
// src/index.ts
|
|
7722
|
+
init_config();
|
|
7723
|
+
init_crypto();
|
|
7724
|
+
|
|
7121
7725
|
// src/db.ts
|
|
7122
7726
|
init_errors();
|
|
7123
7727
|
var VERSION_TABLE = "context_engine_alembic_version";
|
|
@@ -7200,6 +7804,7 @@ async function runMigrate(databaseUrl, opts = {}) {
|
|
|
7200
7804
|
recorded = rev;
|
|
7201
7805
|
done.add(rev);
|
|
7202
7806
|
}
|
|
7807
|
+
await client.query(loadSql("0004_acl_indexes", dim));
|
|
7203
7808
|
const meta = await client.query("SELECT embedding_dim FROM context_engine_meta WHERE id = 1");
|
|
7204
7809
|
if (!meta.rows.length) {
|
|
7205
7810
|
await client.query("INSERT INTO context_engine_meta (id, embedding_dim) VALUES (1, $1)", [dim]);
|
|
@@ -7376,7 +7981,7 @@ async function loadGeminiEmbedClient(apiKey) {
|
|
|
7376
7981
|
if (!Ctor) {
|
|
7377
7982
|
throw new exports.ExtraMissingError("gemini", specifier, "gemini embeddings");
|
|
7378
7983
|
}
|
|
7379
|
-
return new Ctor({ apiKey: apiKey ?? null });
|
|
7984
|
+
return new Ctor({ apiKey: apiKey ?? null, httpOptions: { timeout: TIMEOUT_MS3 } });
|
|
7380
7985
|
}
|
|
7381
7986
|
function buildEmbedder(cfg2, opts) {
|
|
7382
7987
|
if (opts?.client || opts?.fetch || opts?.fetchImpl) {
|
|
@@ -7394,6 +7999,9 @@ function buildEmbedder(cfg2, opts) {
|
|
|
7394
7999
|
throw new Error(`unknown embedding provider: ${JSON.stringify(cfg2.provider)}`);
|
|
7395
8000
|
}
|
|
7396
8001
|
|
|
8002
|
+
// src/engine.ts
|
|
8003
|
+
init_redaction();
|
|
8004
|
+
|
|
7397
8005
|
// src/compression.ts
|
|
7398
8006
|
init_text();
|
|
7399
8007
|
var TOKENS_PER_CHAR = 0.25;
|
|
@@ -8697,6 +9305,7 @@ async function rerank(cfg2, query, docs, opts = {}) {
|
|
|
8697
9305
|
}
|
|
8698
9306
|
|
|
8699
9307
|
// src/search.ts
|
|
9308
|
+
init_redaction();
|
|
8700
9309
|
var MIN_LEG_CANDIDATES = 50;
|
|
8701
9310
|
var MAX_LEG_CANDIDATES = 500;
|
|
8702
9311
|
var UNITS_PER_SCOPE_HYBRID = 1;
|
|
@@ -8906,6 +9515,7 @@ async function runSearch(query, opts) {
|
|
|
8906
9515
|
const topK = Math.max(1, Math.trunc(opts.topK ?? 10));
|
|
8907
9516
|
const scope = {
|
|
8908
9517
|
sourceIds: opts.sourceIds != null ? [...opts.sourceIds] : null,
|
|
9518
|
+
documentIds: opts.documentIds != null ? [...opts.documentIds] : null,
|
|
8909
9519
|
principals: opts.principals != null ? [...opts.principals] : null,
|
|
8910
9520
|
limit: legLimit(topK, opts.config)
|
|
8911
9521
|
};
|
|
@@ -8926,13 +9536,8 @@ async function runSearch(query, opts) {
|
|
|
8926
9536
|
let degraded = null;
|
|
8927
9537
|
if (mode === "graph" && !graphLeg) {
|
|
8928
9538
|
degraded = "graph_leg_unavailable";
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
new exports.GraphLegUnavailable(
|
|
8932
|
-
"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."
|
|
8933
|
-
),
|
|
8934
|
-
{ stage: "graph_leg", mode, degraded }
|
|
8935
|
-
);
|
|
9539
|
+
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.";
|
|
9540
|
+
emitError(opts.hooks, new exports.GraphLegUnavailable(reason), { stage: "graph_leg", mode, degraded });
|
|
8936
9541
|
}
|
|
8937
9542
|
const fused = rrfFuse(ranked, { k: opts.config.fusion.k, weights: opts.config.fusion.weights });
|
|
8938
9543
|
const window = opts.config.reranker.enabled ? Math.max(topK, opts.config.reranker.candidates) : topK;
|
|
@@ -9007,31 +9612,6 @@ async function runSearch(query, opts) {
|
|
|
9007
9612
|
return { hits, usage };
|
|
9008
9613
|
}
|
|
9009
9614
|
|
|
9010
|
-
// src/sentinels.ts
|
|
9011
|
-
var UNSET = /* @__PURE__ */ Symbol.for("context_engine.UNSET");
|
|
9012
|
-
var TrustedSentinel = class {
|
|
9013
|
-
[Symbol.toStringTag] = "TRUSTED";
|
|
9014
|
-
toString() {
|
|
9015
|
-
return "TRUSTED";
|
|
9016
|
-
}
|
|
9017
|
-
valueOf() {
|
|
9018
|
-
return true;
|
|
9019
|
-
}
|
|
9020
|
-
};
|
|
9021
|
-
var TRUSTED = Object.freeze(new TrustedSentinel());
|
|
9022
|
-
function resolvePrincipals(value, method) {
|
|
9023
|
-
if (value === TRUSTED) return null;
|
|
9024
|
-
if (value === void 0) return null;
|
|
9025
|
-
if (value === null) {
|
|
9026
|
-
process.emitWarning(
|
|
9027
|
-
`${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.`,
|
|
9028
|
-
"DeprecationWarning"
|
|
9029
|
-
);
|
|
9030
|
-
return null;
|
|
9031
|
-
}
|
|
9032
|
-
return Array.isArray(value) ? value : null;
|
|
9033
|
-
}
|
|
9034
|
-
|
|
9035
9615
|
// src/storage.ts
|
|
9036
9616
|
init_text();
|
|
9037
9617
|
var MIN_ITERATIVE_SCAN_VERSION = [0, 8];
|
|
@@ -9070,25 +9650,26 @@ function trigramThreshold(query) {
|
|
|
9070
9650
|
}
|
|
9071
9651
|
var SCOPE = `
|
|
9072
9652
|
AND ($1::text[] IS NULL OR c.source_id = ANY($1))
|
|
9073
|
-
AND ($2::
|
|
9653
|
+
AND ($2::uuid[] IS NULL OR c.document_id = ANY($2::uuid[]))
|
|
9654
|
+
AND ($3::text[] IS NULL OR c.acl IS NULL OR c.acl && $3)
|
|
9074
9655
|
`;
|
|
9075
9656
|
var SQL_FTS = `
|
|
9076
9657
|
SELECT c.id::text
|
|
9077
9658
|
FROM context_engine_chunks c
|
|
9078
9659
|
WHERE c.text IS NOT NULL
|
|
9079
|
-
AND c.text_search @@ websearch_to_tsquery('simple'::regconfig, $
|
|
9660
|
+
AND c.text_search @@ websearch_to_tsquery('simple'::regconfig, $4)
|
|
9080
9661
|
${SCOPE}
|
|
9081
|
-
ORDER BY ts_rank_cd(c.text_search, websearch_to_tsquery('simple'::regconfig, $
|
|
9082
|
-
LIMIT $
|
|
9662
|
+
ORDER BY ts_rank_cd(c.text_search, websearch_to_tsquery('simple'::regconfig, $4)) DESC, c.id
|
|
9663
|
+
LIMIT $5
|
|
9083
9664
|
`;
|
|
9084
9665
|
var SQL_TRGM = `
|
|
9085
9666
|
SELECT c.id::text
|
|
9086
9667
|
FROM context_engine_chunks c
|
|
9087
9668
|
WHERE c.text IS NOT NULL
|
|
9088
|
-
AND c.text_trgm_norm % $
|
|
9669
|
+
AND c.text_trgm_norm % $4
|
|
9089
9670
|
${SCOPE}
|
|
9090
|
-
ORDER BY similarity(c.text_trgm_norm, $
|
|
9091
|
-
LIMIT $
|
|
9671
|
+
ORDER BY similarity(c.text_trgm_norm, $4) DESC, c.id
|
|
9672
|
+
LIMIT $5
|
|
9092
9673
|
`;
|
|
9093
9674
|
var SQL_COUNT_ELIGIBLE = `
|
|
9094
9675
|
SELECT count(*)::int AS count FROM (
|
|
@@ -9096,13 +9677,13 @@ SELECT count(*)::int AS count FROM (
|
|
|
9096
9677
|
FROM context_engine_chunks c
|
|
9097
9678
|
WHERE c.embedding IS NOT NULL
|
|
9098
9679
|
${SCOPE}
|
|
9099
|
-
LIMIT $
|
|
9680
|
+
LIMIT $4
|
|
9100
9681
|
) probe
|
|
9101
9682
|
`;
|
|
9102
9683
|
var SQL_FILTER_IDS = `
|
|
9103
9684
|
SELECT c.id::text
|
|
9104
9685
|
FROM context_engine_chunks c
|
|
9105
|
-
WHERE c.id = ANY($
|
|
9686
|
+
WHERE c.id = ANY($4::uuid[])
|
|
9106
9687
|
${SCOPE}
|
|
9107
9688
|
`;
|
|
9108
9689
|
var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -9121,7 +9702,7 @@ FROM context_engine_chunks c
|
|
|
9121
9702
|
WHERE c.embedding IS NOT NULL
|
|
9122
9703
|
${SCOPE}
|
|
9123
9704
|
ORDER BY c.embedding <=> ${vec}
|
|
9124
|
-
LIMIT $
|
|
9705
|
+
LIMIT $4
|
|
9125
9706
|
`;
|
|
9126
9707
|
}
|
|
9127
9708
|
function sqlAnnExact(vec) {
|
|
@@ -9134,7 +9715,7 @@ WITH eligible AS MATERIALIZED (
|
|
|
9134
9715
|
)
|
|
9135
9716
|
SELECT id::text FROM eligible
|
|
9136
9717
|
ORDER BY embedding <=> ${vec}
|
|
9137
|
-
LIMIT $
|
|
9718
|
+
LIMIT $4
|
|
9138
9719
|
`;
|
|
9139
9720
|
}
|
|
9140
9721
|
function idsOf(rows) {
|
|
@@ -9161,24 +9742,26 @@ var PostgresBackend = class {
|
|
|
9161
9742
|
* mean different things.
|
|
9162
9743
|
*/
|
|
9163
9744
|
scopeParams(scope) {
|
|
9164
|
-
return
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9745
|
+
return {
|
|
9746
|
+
binds: [
|
|
9747
|
+
scope.sourceIds != null ? [...scope.sourceIds] : null,
|
|
9748
|
+
scope.documentIds != null ? [...scope.documentIds] : null,
|
|
9749
|
+
scope.principals != null ? [...scope.principals] : null
|
|
9750
|
+
],
|
|
9751
|
+
lim: Math.max(1, Math.trunc(scope.limit ?? 50))
|
|
9752
|
+
};
|
|
9169
9753
|
}
|
|
9170
9754
|
async upsertChunks(documentId, sourceId, acl, chunks) {
|
|
9171
9755
|
const client = await this.pool.connect();
|
|
9172
9756
|
try {
|
|
9173
9757
|
await client.query("BEGIN");
|
|
9174
9758
|
await client.query("DELETE FROM context_engine_chunks WHERE document_id = $1", [documentId]);
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
[
|
|
9759
|
+
const BATCH = 500;
|
|
9760
|
+
for (let start = 0; start < chunks.length; start += BATCH) {
|
|
9761
|
+
const batch = chunks.slice(start, start + BATCH);
|
|
9762
|
+
const params = [];
|
|
9763
|
+
const rows = batch.map((c) => {
|
|
9764
|
+
params.push(
|
|
9182
9765
|
crypto.randomUUID(),
|
|
9183
9766
|
documentId,
|
|
9184
9767
|
sourceId,
|
|
@@ -9187,7 +9770,16 @@ var PostgresBackend = class {
|
|
|
9187
9770
|
c.text,
|
|
9188
9771
|
c.lang ?? null,
|
|
9189
9772
|
JSON.stringify(c.meta ?? {})
|
|
9190
|
-
|
|
9773
|
+
);
|
|
9774
|
+
const p = params.length;
|
|
9775
|
+
const emb = c.embedding != null ? vectorLiteral2(c.embedding) : "NULL";
|
|
9776
|
+
return `($${p - 7}, $${p - 6}, $${p - 5}, $${p - 4}, $${p - 3}, $${p - 2}, $${p - 1}, ${emb}, $${p}::jsonb)`;
|
|
9777
|
+
});
|
|
9778
|
+
await client.query(
|
|
9779
|
+
`INSERT INTO context_engine_chunks
|
|
9780
|
+
(id, document_id, source_id, acl, idx, text, lang, embedding, meta_data)
|
|
9781
|
+
VALUES ${rows.join(", ")}`,
|
|
9782
|
+
params
|
|
9191
9783
|
);
|
|
9192
9784
|
}
|
|
9193
9785
|
await client.query("COMMIT");
|
|
@@ -9252,19 +9844,19 @@ var PostgresBackend = class {
|
|
|
9252
9844
|
}
|
|
9253
9845
|
}
|
|
9254
9846
|
if (!parsed.length) return [];
|
|
9255
|
-
const
|
|
9256
|
-
const { rows } = await this.pool.query(SQL_FILTER_IDS, [
|
|
9847
|
+
const { binds } = this.scopeParams(scope);
|
|
9848
|
+
const { rows } = await this.pool.query(SQL_FILTER_IDS, [...binds, parsed]);
|
|
9257
9849
|
const visible2 = new Set(idsOf(rows));
|
|
9258
9850
|
return chunkIds.filter((cid) => visible2.has(String(cid))).map(String);
|
|
9259
9851
|
}
|
|
9260
9852
|
async ftsSearch(query, scope) {
|
|
9261
|
-
const
|
|
9262
|
-
const { rows } = await this.pool.query(SQL_FTS, [
|
|
9853
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
9854
|
+
const { rows } = await this.pool.query(SQL_FTS, [...binds, query, lim]);
|
|
9263
9855
|
return idsOf(rows);
|
|
9264
9856
|
}
|
|
9265
9857
|
async trgmSearch(query, scope) {
|
|
9266
9858
|
const threshold = trigramThreshold(query);
|
|
9267
|
-
const
|
|
9859
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
9268
9860
|
const client = await this.pool.connect();
|
|
9269
9861
|
let previous = null;
|
|
9270
9862
|
let usedSetLimit = false;
|
|
@@ -9273,7 +9865,7 @@ var PostgresBackend = class {
|
|
|
9273
9865
|
const applied = await this.applyTrgmThreshold(client, threshold);
|
|
9274
9866
|
previous = applied.previous;
|
|
9275
9867
|
usedSetLimit = applied.usedSetLimit;
|
|
9276
|
-
const { rows } = await client.query(SQL_TRGM, [
|
|
9868
|
+
const { rows } = await client.query(SQL_TRGM, [...binds, query, lim]);
|
|
9277
9869
|
if (usedSetLimit) await this.restoreTrgmLimit(client, previous);
|
|
9278
9870
|
await client.query("COMMIT");
|
|
9279
9871
|
return idsOf(rows);
|
|
@@ -9319,18 +9911,18 @@ var PostgresBackend = class {
|
|
|
9319
9911
|
}
|
|
9320
9912
|
async annSearch(vector, scope) {
|
|
9321
9913
|
const literal = vectorLiteral2(vector);
|
|
9322
|
-
const
|
|
9323
|
-
const scoped =
|
|
9914
|
+
const { binds, lim } = this.scopeParams(scope);
|
|
9915
|
+
const scoped = binds.some((v) => v !== null);
|
|
9324
9916
|
const client = await this.pool.connect();
|
|
9325
9917
|
try {
|
|
9326
9918
|
await client.query("BEGIN");
|
|
9327
|
-
if (scoped && await this.eligibleIsSmall(client,
|
|
9328
|
-
const { rows: rows2 } = await client.query(sqlAnnExact(literal), [
|
|
9919
|
+
if (scoped && await this.eligibleIsSmall(client, binds)) {
|
|
9920
|
+
const { rows: rows2 } = await client.query(sqlAnnExact(literal), [...binds, lim]);
|
|
9329
9921
|
await client.query("COMMIT");
|
|
9330
9922
|
return idsOf(rows2);
|
|
9331
9923
|
}
|
|
9332
9924
|
await this.tuneAnnScan(client, lim, scoped);
|
|
9333
|
-
const { rows } = await client.query(sqlAnn(literal), [
|
|
9925
|
+
const { rows } = await client.query(sqlAnn(literal), [...binds, lim]);
|
|
9334
9926
|
await client.query("COMMIT");
|
|
9335
9927
|
return idsOf(rows);
|
|
9336
9928
|
} catch (err) {
|
|
@@ -9343,11 +9935,17 @@ var PostgresBackend = class {
|
|
|
9343
9935
|
client.release();
|
|
9344
9936
|
}
|
|
9345
9937
|
}
|
|
9346
|
-
async eligibleIsSmall(client,
|
|
9938
|
+
async eligibleIsSmall(client, binds) {
|
|
9347
9939
|
const cap = this.exactThreshold + 1;
|
|
9348
|
-
const { rows } = await client.query(SQL_COUNT_ELIGIBLE, [
|
|
9940
|
+
const { rows } = await client.query(SQL_COUNT_ELIGIBLE, [...binds, cap]);
|
|
9349
9941
|
return Number(rows[0]?.count ?? 0) <= this.exactThreshold;
|
|
9350
9942
|
}
|
|
9943
|
+
/** Size the HNSW candidate budget, and make it iterative when filtered.
|
|
9944
|
+
*
|
|
9945
|
+
* Public because the graph leg's seed query (`graph/retrieval.ts`) is the
|
|
9946
|
+
* same shape — an ANN walk with the scope predicate as a POST-filter — and
|
|
9947
|
+
* must not carry a second copy of this. Call it inside a transaction on the
|
|
9948
|
+
* client the query itself will run on: every setting here is `SET LOCAL`. */
|
|
9351
9949
|
async tuneAnnScan(client, limit, scoped) {
|
|
9352
9950
|
const efSearch = Math.min(Math.max(Math.trunc(limit) * 4, ANN_MIN_EF_SEARCH), ANN_MAX_EF_SEARCH);
|
|
9353
9951
|
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
@@ -9440,15 +10038,7 @@ function functionTool(fn) {
|
|
|
9440
10038
|
// src/tools/governance.ts
|
|
9441
10039
|
init_errors();
|
|
9442
10040
|
init_hooks();
|
|
9443
|
-
|
|
9444
|
-
// src/tools/acl.ts
|
|
9445
|
-
function aclVisible(acl, principals) {
|
|
9446
|
-
if (principals === null) return true;
|
|
9447
|
-
if (acl == null) return true;
|
|
9448
|
-
if (!acl.length) return false;
|
|
9449
|
-
const held = new Set(principals ?? []);
|
|
9450
|
-
return acl.some((p) => held.has(p));
|
|
9451
|
-
}
|
|
10041
|
+
init_redaction();
|
|
9452
10042
|
|
|
9453
10043
|
// src/tools/approval.ts
|
|
9454
10044
|
init_errors();
|
|
@@ -9506,17 +10096,24 @@ function rowToRecord(row) {
|
|
|
9506
10096
|
toolName: String(row.tool_name),
|
|
9507
10097
|
toolArgsFrozen: row.tool_args_frozen ?? {},
|
|
9508
10098
|
sourceId: row.source_id ?? null,
|
|
10099
|
+
approvalScope: row.approval_scope ?? null,
|
|
9509
10100
|
principals: row.principals,
|
|
9510
10101
|
status: String(row.status),
|
|
9511
10102
|
approver: row.approver ?? null,
|
|
9512
10103
|
approverMeta: row.approver_meta ?? null,
|
|
9513
|
-
expiresAt:
|
|
9514
|
-
resolvedAt:
|
|
9515
|
-
createdAt:
|
|
10104
|
+
expiresAt: toDate(row.expires_at),
|
|
10105
|
+
resolvedAt: toDate(row.resolved_at),
|
|
10106
|
+
createdAt: toDate(row.created_at)
|
|
9516
10107
|
};
|
|
9517
10108
|
}
|
|
10109
|
+
function toDate(value) {
|
|
10110
|
+
if (value == null) return null;
|
|
10111
|
+
if (value instanceof Date) return value;
|
|
10112
|
+
return new Date(String(value));
|
|
10113
|
+
}
|
|
9518
10114
|
function visibilitySql(principals, paramIndex) {
|
|
9519
|
-
if (principals === null) return { sql: "TRUE", params: [] };
|
|
10115
|
+
if (principals === null || principals === TRUSTED) return { sql: "TRUE", params: [] };
|
|
10116
|
+
principals = principals;
|
|
9520
10117
|
if (!principals.length) {
|
|
9521
10118
|
return { sql: `(principals IS NULL OR principals = 'null'::jsonb)`, params: [] };
|
|
9522
10119
|
}
|
|
@@ -9525,41 +10122,95 @@ function visibilitySql(principals, paramIndex) {
|
|
|
9525
10122
|
params: [principals]
|
|
9526
10123
|
};
|
|
9527
10124
|
}
|
|
9528
|
-
|
|
10125
|
+
function claimVisibilitySql(principals, paramIndex) {
|
|
10126
|
+
if (principals === null || principals === TRUSTED) return { sql: "TRUE", params: [] };
|
|
10127
|
+
const noWall = `principals IS NULL OR principals = 'null'::jsonb OR principals = '[]'::jsonb`;
|
|
10128
|
+
principals = principals;
|
|
10129
|
+
if (!principals.length) return { sql: `(${noWall})`, params: [] };
|
|
10130
|
+
return { sql: `(${noWall} OR principals ?| $${paramIndex}::text[])`, params: [principals] };
|
|
10131
|
+
}
|
|
10132
|
+
function validateApprovalScope(approvalScope) {
|
|
10133
|
+
if (approvalScope === null || approvalScope === void 0) return null;
|
|
10134
|
+
if (typeof approvalScope !== "string") {
|
|
10135
|
+
throw new TypeError(`approvalScope must be a non-empty string or null, got ${typeof approvalScope}`);
|
|
10136
|
+
}
|
|
10137
|
+
if (!approvalScope) throw new Error("approvalScope must be a non-empty string or null, got ''");
|
|
10138
|
+
return approvalScope;
|
|
10139
|
+
}
|
|
10140
|
+
function pendingRow(opts, approvalScope) {
|
|
9529
10141
|
const policy = opts.policy ?? {};
|
|
9530
10142
|
const frozen = structuredClone(opts.args);
|
|
9531
10143
|
const createdAt = opts.now ?? /* @__PURE__ */ new Date();
|
|
9532
10144
|
const timeout = Number(policy.timeout_minutes ?? DEFAULT_TIMEOUT_MINUTES);
|
|
9533
10145
|
const expiresAt = new Date(createdAt.getTime() + timeout * 6e4);
|
|
10146
|
+
const principals = opts.principals === void 0 || opts.principals === TRUSTED ? null : opts.principals;
|
|
10147
|
+
return { frozen, createdAt, expiresAt, principals, approvalScope };
|
|
10148
|
+
}
|
|
10149
|
+
async function insertPending(engine, opts, row) {
|
|
9534
10150
|
const id = crypto.randomUUID();
|
|
9535
10151
|
await engine.pool.query(
|
|
9536
10152
|
`INSERT INTO context_engine_tool_approvals
|
|
9537
|
-
(id, tool_name, tool_args_frozen, source_id, principals, status, expires_at, created_at)
|
|
9538
|
-
VALUES ($1,$2,$3::jsonb,$4,$5::jsonb,'pending',$
|
|
10153
|
+
(id, tool_name, tool_args_frozen, source_id, approval_scope, principals, status, expires_at, created_at)
|
|
10154
|
+
VALUES ($1,$2,$3::jsonb,$4,$5,$6::jsonb,'pending',$7,$8)`,
|
|
9539
10155
|
[
|
|
9540
10156
|
id,
|
|
9541
10157
|
opts.toolName,
|
|
9542
|
-
JSON.stringify(frozen),
|
|
10158
|
+
JSON.stringify(row.frozen),
|
|
9543
10159
|
opts.sourceId ?? null,
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
10160
|
+
row.approvalScope,
|
|
10161
|
+
row.principals === null ? null : JSON.stringify(row.principals),
|
|
10162
|
+
row.expiresAt,
|
|
10163
|
+
row.createdAt
|
|
9547
10164
|
]
|
|
9548
10165
|
);
|
|
9549
10166
|
return {
|
|
9550
10167
|
id,
|
|
9551
10168
|
toolName: opts.toolName,
|
|
9552
|
-
toolArgsFrozen: frozen,
|
|
10169
|
+
toolArgsFrozen: row.frozen,
|
|
9553
10170
|
sourceId: opts.sourceId ?? null,
|
|
9554
|
-
|
|
10171
|
+
approvalScope: row.approvalScope,
|
|
10172
|
+
principals: row.principals,
|
|
9555
10173
|
status: "pending",
|
|
9556
10174
|
approver: null,
|
|
9557
10175
|
approverMeta: null,
|
|
9558
|
-
expiresAt,
|
|
10176
|
+
expiresAt: row.expiresAt,
|
|
9559
10177
|
resolvedAt: null,
|
|
9560
|
-
createdAt
|
|
10178
|
+
createdAt: row.createdAt
|
|
9561
10179
|
};
|
|
9562
10180
|
}
|
|
10181
|
+
async function createPending(engine, opts) {
|
|
10182
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
10183
|
+
return insertPending(engine, opts, pendingRow(opts, approvalScope));
|
|
10184
|
+
}
|
|
10185
|
+
async function findOrCreatePending(engine, opts) {
|
|
10186
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
10187
|
+
if (approvalScope === null)
|
|
10188
|
+
throw new Error("findOrCreatePending requires an approvalScope; use createPending");
|
|
10189
|
+
const row = pendingRow(opts, approvalScope);
|
|
10190
|
+
const frozenJson = JSON.stringify(row.frozen);
|
|
10191
|
+
const sameCall = `tool_name = $1 AND approval_scope = $2 AND tool_args_frozen = $3::jsonb`;
|
|
10192
|
+
const params = [opts.toolName, approvalScope, frozenJson, row.createdAt];
|
|
10193
|
+
await engine.pool.query(
|
|
10194
|
+
`UPDATE context_engine_tool_approvals SET status = 'expired'
|
|
10195
|
+
WHERE ${sameCall} AND status = 'pending' AND expires_at <= $4`,
|
|
10196
|
+
params
|
|
10197
|
+
);
|
|
10198
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
10199
|
+
const found = await engine.pool.query(
|
|
10200
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
10201
|
+
WHERE ${sameCall} AND status = 'pending' AND (expires_at IS NULL OR expires_at > $4)
|
|
10202
|
+
ORDER BY created_at ASC LIMIT 1`,
|
|
10203
|
+
params
|
|
10204
|
+
);
|
|
10205
|
+
if (found.rows[0]) return rowToRecord(found.rows[0]);
|
|
10206
|
+
try {
|
|
10207
|
+
return await insertPending(engine, opts, row);
|
|
10208
|
+
} catch (exc) {
|
|
10209
|
+
if (exc?.code !== "23505") throw exc;
|
|
10210
|
+
}
|
|
10211
|
+
}
|
|
10212
|
+
throw new Error("findOrCreatePending: lost the insert race twice and found no live pending row");
|
|
10213
|
+
}
|
|
9563
10214
|
async function resolveApproval(engine, approvalId, decision, approver, meta = null, opts = {}) {
|
|
9564
10215
|
if (decision !== "approved" && decision !== "rejected") {
|
|
9565
10216
|
throw new Error(`invalid decision: ${JSON.stringify(decision)} (expected 'approved' or 'rejected')`);
|
|
@@ -9707,6 +10358,10 @@ var ToolConfig = class _ToolConfig {
|
|
|
9707
10358
|
requiresApproval;
|
|
9708
10359
|
approvalPolicy;
|
|
9709
10360
|
enabled;
|
|
10361
|
+
/** Engine-opaque user metadata (documents have the same column). Stored
|
|
10362
|
+
* and returned in CLEAR on the admin surface only — secrets go in
|
|
10363
|
+
* `config`, which is encrypted. */
|
|
10364
|
+
metaData;
|
|
9710
10365
|
constructor(init) {
|
|
9711
10366
|
this.id = init.id ?? null;
|
|
9712
10367
|
this.name = init.name;
|
|
@@ -9718,6 +10373,7 @@ var ToolConfig = class _ToolConfig {
|
|
|
9718
10373
|
this.requiresApproval = init.requiresApproval ?? init.requires_approval ?? false;
|
|
9719
10374
|
this.approvalPolicy = init.approvalPolicy ?? init.approval_policy ?? {};
|
|
9720
10375
|
this.enabled = init.enabled ?? true;
|
|
10376
|
+
this.metaData = init.metaData ?? init.meta_data ?? {};
|
|
9721
10377
|
}
|
|
9722
10378
|
static fromUnknown(body) {
|
|
9723
10379
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -9740,7 +10396,8 @@ var ToolConfig = class _ToolConfig {
|
|
|
9740
10396
|
acl: b.acl ?? null,
|
|
9741
10397
|
requiresApproval: Boolean(b.requiresApproval ?? b.requires_approval ?? false),
|
|
9742
10398
|
approvalPolicy: b.approvalPolicy ?? b.approval_policy ?? {},
|
|
9743
|
-
enabled: b.enabled === void 0 ? true : Boolean(b.enabled)
|
|
10399
|
+
enabled: b.enabled === void 0 ? true : Boolean(b.enabled),
|
|
10400
|
+
metaData: b.metaData ?? b.meta_data ?? {}
|
|
9744
10401
|
});
|
|
9745
10402
|
}
|
|
9746
10403
|
};
|
|
@@ -9759,9 +10416,26 @@ var SCHEMAS = {
|
|
|
9759
10416
|
additionalProperties: { type: "string" },
|
|
9760
10417
|
writeOnly: true
|
|
9761
10418
|
},
|
|
10419
|
+
// The request body TEMPLATE (the http executor reads it). Whether it
|
|
10420
|
+
// round-trips is the USER's call — see `body_secret` and the
|
|
10421
|
+
// conditional in `redactConfig`; listing it documents the shape, the
|
|
10422
|
+
// redactor decides.
|
|
10423
|
+
body: {
|
|
10424
|
+
description: "Request body template; top-level keys merge with LLM parameters"
|
|
10425
|
+
},
|
|
10426
|
+
// Salman (2026-08-18): "give checkbox to user" — the author declares
|
|
10427
|
+
// whether their body carries secrets. True (or ABSENT — every pre-flag
|
|
10428
|
+
// row, fail closed) masks body values like headers; an explicit false
|
|
10429
|
+
// round-trips the body in clear like `url`. The flag round-trips.
|
|
10430
|
+
body_secret: {
|
|
10431
|
+
type: "boolean",
|
|
10432
|
+
description: "Body values are write-only (mask like headers)"
|
|
10433
|
+
},
|
|
9762
10434
|
parameters: {
|
|
9763
10435
|
type: "object",
|
|
9764
|
-
description: "Static/user-fixed parameters sent on every call"
|
|
10436
|
+
description: "Static/user-fixed parameters sent on every call",
|
|
10437
|
+
// The canonical home of a static api_key — secret by position.
|
|
10438
|
+
writeOnly: true
|
|
9765
10439
|
},
|
|
9766
10440
|
llmParameters: {
|
|
9767
10441
|
type: "object",
|
|
@@ -9792,7 +10466,13 @@ var SCHEMAS = {
|
|
|
9792
10466
|
default: "readonly"
|
|
9793
10467
|
},
|
|
9794
10468
|
max_rows: { type: "integer", default: 1e3 },
|
|
9795
|
-
selected_tables: { type: "array", items: { type: "string" } }
|
|
10469
|
+
selected_tables: { type: "array", items: { type: "string" } },
|
|
10470
|
+
// Dialect connection identifiers the db executor reads — same
|
|
10471
|
+
// sensitivity class as `database`, listed so they round-trip instead
|
|
10472
|
+
// of dying to redactConfig's default-deny.
|
|
10473
|
+
service_name: { type: "string" },
|
|
10474
|
+
schema_name: { type: "string" },
|
|
10475
|
+
warehouse: { type: "string" }
|
|
9796
10476
|
},
|
|
9797
10477
|
required: ["engine", "database"]
|
|
9798
10478
|
},
|
|
@@ -9824,11 +10504,245 @@ function configSchema(kind) {
|
|
|
9824
10504
|
if (!schema) throw new Error(`unknown tool kind: ${JSON.stringify(kind)}`);
|
|
9825
10505
|
return schema;
|
|
9826
10506
|
}
|
|
10507
|
+
var REDACTED_SENTINEL = "__redacted__";
|
|
10508
|
+
var ConfigTemplateError = class extends Error {
|
|
10509
|
+
};
|
|
10510
|
+
function isPlainObject(value) {
|
|
10511
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10512
|
+
}
|
|
10513
|
+
function containsSentinel(value) {
|
|
10514
|
+
if (typeof value === "string") return value === REDACTED_SENTINEL;
|
|
10515
|
+
if (Array.isArray(value)) return value.some(containsSentinel);
|
|
10516
|
+
if (isPlainObject(value)) return Object.values(value).some(containsSentinel);
|
|
10517
|
+
return false;
|
|
10518
|
+
}
|
|
10519
|
+
function mergeRedacted(incoming, stored) {
|
|
10520
|
+
const resolve = (value, kept, keptPresent, path) => {
|
|
10521
|
+
if (isPlainObject(value)) {
|
|
10522
|
+
const keptDict = isPlainObject(kept) ? kept : {};
|
|
10523
|
+
const out2 = {};
|
|
10524
|
+
for (const [k, v] of Object.entries(value)) {
|
|
10525
|
+
out2[k] = resolve(v, keptDict[k], k in keptDict, path ? `${path}.${k}` : k);
|
|
10526
|
+
}
|
|
10527
|
+
return out2;
|
|
10528
|
+
}
|
|
10529
|
+
if (Array.isArray(value)) {
|
|
10530
|
+
const keptList = Array.isArray(kept) ? kept : [];
|
|
10531
|
+
return value.map((v, i) => resolve(v, keptList[i], i < keptList.length, `${path}[${i}]`));
|
|
10532
|
+
}
|
|
10533
|
+
if (value === REDACTED_SENTINEL) {
|
|
10534
|
+
if (!keptPresent) {
|
|
10535
|
+
throw new ConfigTemplateError(
|
|
10536
|
+
`config.${path} is ${JSON.stringify(REDACTED_SENTINEL)} but there is no stored value to keep \u2014 re-enter the secret or omit the field`
|
|
10537
|
+
);
|
|
10538
|
+
}
|
|
10539
|
+
return kept;
|
|
10540
|
+
}
|
|
10541
|
+
return value;
|
|
10542
|
+
};
|
|
10543
|
+
const keptRoot = stored ?? {};
|
|
10544
|
+
const out = {};
|
|
10545
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
10546
|
+
out[k] = resolve(v, keptRoot[k], k in keptRoot, k);
|
|
10547
|
+
}
|
|
10548
|
+
return out;
|
|
10549
|
+
}
|
|
10550
|
+
|
|
10551
|
+
// src/tools/crypto.ts
|
|
10552
|
+
init_crypto();
|
|
10553
|
+
var MAX_RESPONSE_BYTES = 1e6;
|
|
10554
|
+
var MAX_REDIRECTS = 5;
|
|
10555
|
+
function isRedirect(resp) {
|
|
10556
|
+
return resp.status >= 300 && resp.status < 400 || resp.type === "opaqueredirect";
|
|
10557
|
+
}
|
|
10558
|
+
var EgressDenied = class extends Error {
|
|
10559
|
+
constructor(message) {
|
|
10560
|
+
super(message);
|
|
10561
|
+
this.name = "EgressDenied";
|
|
10562
|
+
}
|
|
10563
|
+
};
|
|
10564
|
+
var resolver = {
|
|
10565
|
+
async lookup(host) {
|
|
10566
|
+
const answers = await dns.promises.lookup(host, { all: true, verbatim: true });
|
|
10567
|
+
return answers.map((a) => a.address);
|
|
10568
|
+
}
|
|
10569
|
+
};
|
|
10570
|
+
var V4_PRIVATE = [
|
|
10571
|
+
["0.0.0.0", 8],
|
|
10572
|
+
// "this network" / unspecified
|
|
10573
|
+
["10.0.0.0", 8],
|
|
10574
|
+
// RFC 1918
|
|
10575
|
+
["100.64.0.0", 10],
|
|
10576
|
+
// RFC 6598 carrier-grade NAT — overlay VPNs, pod CIDRs
|
|
10577
|
+
["127.0.0.0", 8],
|
|
10578
|
+
// loopback
|
|
10579
|
+
["169.254.0.0", 16],
|
|
10580
|
+
// link-local, incl. the cloud metadata address
|
|
10581
|
+
["172.16.0.0", 12],
|
|
10582
|
+
// RFC 1918
|
|
10583
|
+
["192.168.0.0", 16],
|
|
10584
|
+
// RFC 1918
|
|
10585
|
+
["224.0.0.0", 4],
|
|
10586
|
+
// multicast
|
|
10587
|
+
["240.0.0.0", 4],
|
|
10588
|
+
// reserved
|
|
10589
|
+
["255.255.255.255", 32]
|
|
10590
|
+
// broadcast (inside 240/4; spelled out anyway)
|
|
10591
|
+
];
|
|
10592
|
+
var V6_PRIVATE = [
|
|
10593
|
+
["::", 128],
|
|
10594
|
+
// unspecified
|
|
10595
|
+
["::1", 128],
|
|
10596
|
+
// loopback
|
|
10597
|
+
["::ffff:0:0", 96],
|
|
10598
|
+
// IPv4-mapped — the wrapped v4 is checked too
|
|
10599
|
+
["64:ff9b::", 96],
|
|
10600
|
+
// NAT64 — the wrapped v4 is checked too
|
|
10601
|
+
["2002::", 16],
|
|
10602
|
+
// 6to4 — the wrapped v4 is checked too, and this is denied
|
|
10603
|
+
["fc00::", 7],
|
|
10604
|
+
// unique local
|
|
10605
|
+
["fe80::", 10],
|
|
10606
|
+
// link-local
|
|
10607
|
+
["fec0::", 10],
|
|
10608
|
+
// site-local (deprecated, still configured)
|
|
10609
|
+
["ff00::", 8],
|
|
10610
|
+
// multicast
|
|
10611
|
+
["3fff::", 20]
|
|
10612
|
+
// documentation
|
|
10613
|
+
];
|
|
10614
|
+
function parseV4(text) {
|
|
10615
|
+
const parts = text.split(".");
|
|
10616
|
+
if (parts.length !== 4) return null;
|
|
10617
|
+
const out = new Uint8Array(4);
|
|
10618
|
+
for (let i = 0; i < 4; i++) {
|
|
10619
|
+
const part = parts[i];
|
|
10620
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
10621
|
+
const value = Number(part);
|
|
10622
|
+
if (value > 255) return null;
|
|
10623
|
+
out[i] = value;
|
|
10624
|
+
}
|
|
10625
|
+
return out;
|
|
10626
|
+
}
|
|
10627
|
+
function parseV6(text) {
|
|
10628
|
+
let body = text.split("%")[0] ?? "";
|
|
10629
|
+
const lastColon = body.lastIndexOf(":");
|
|
10630
|
+
if (lastColon < 0) return null;
|
|
10631
|
+
const tail = body.slice(lastColon + 1);
|
|
10632
|
+
if (tail.includes(".")) {
|
|
10633
|
+
const v4 = parseV4(tail);
|
|
10634
|
+
if (!v4) return null;
|
|
10635
|
+
const hi = (v4[0] << 8 | v4[1]).toString(16);
|
|
10636
|
+
const lo = (v4[2] << 8 | v4[3]).toString(16);
|
|
10637
|
+
body = `${body.slice(0, lastColon + 1)}${hi}:${lo}`;
|
|
10638
|
+
}
|
|
10639
|
+
const halves = body.split("::");
|
|
10640
|
+
if (halves.length > 2) return null;
|
|
10641
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
10642
|
+
const rest = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
10643
|
+
let groups;
|
|
10644
|
+
if (halves.length === 1) {
|
|
10645
|
+
if (head.length !== 8) return null;
|
|
10646
|
+
groups = head;
|
|
10647
|
+
} else {
|
|
10648
|
+
const missing = 8 - head.length - rest.length;
|
|
10649
|
+
if (missing < 0) return null;
|
|
10650
|
+
groups = [...head, ...Array(missing).fill("0"), ...rest];
|
|
10651
|
+
}
|
|
10652
|
+
const out = new Uint8Array(16);
|
|
10653
|
+
for (let i = 0; i < 8; i++) {
|
|
10654
|
+
const group = groups[i];
|
|
10655
|
+
if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
|
|
10656
|
+
const value = Number.parseInt(group, 16);
|
|
10657
|
+
out[2 * i] = value >> 8;
|
|
10658
|
+
out[2 * i + 1] = value & 255;
|
|
10659
|
+
}
|
|
10660
|
+
return out;
|
|
10661
|
+
}
|
|
10662
|
+
function parseAddress(text) {
|
|
10663
|
+
const family = net.isIP(text);
|
|
10664
|
+
if (family === 4) return parseV4(text);
|
|
10665
|
+
if (family === 6) return parseV6(text);
|
|
10666
|
+
return null;
|
|
10667
|
+
}
|
|
10668
|
+
function inNet(addr, net, prefix) {
|
|
10669
|
+
if (addr.length !== net.length) return false;
|
|
10670
|
+
const whole = prefix >> 3;
|
|
10671
|
+
for (let i = 0; i < whole; i++) if (addr[i] !== net[i]) return false;
|
|
10672
|
+
const bits = prefix & 7;
|
|
10673
|
+
if (bits === 0) return true;
|
|
10674
|
+
const mask = 255 << 8 - bits;
|
|
10675
|
+
return (addr[whole] & mask) === (net[whole] & mask);
|
|
10676
|
+
}
|
|
10677
|
+
function compile(table) {
|
|
10678
|
+
return table.map(([cidr, prefix]) => {
|
|
10679
|
+
const bytes = parseAddress(cidr);
|
|
10680
|
+
if (!bytes) throw new Error(`egress: unparseable network ${cidr}`);
|
|
10681
|
+
return [bytes, prefix];
|
|
10682
|
+
});
|
|
10683
|
+
}
|
|
10684
|
+
var V4_NETS = compile(V4_PRIVATE);
|
|
10685
|
+
var V6_NETS = compile(V6_PRIVATE);
|
|
10686
|
+
var MAPPED_V4 = compile([["::ffff:0:0", 96]])[0];
|
|
10687
|
+
var NAT64 = compile([["64:ff9b::", 96]])[0];
|
|
10688
|
+
var SIXTOFOUR = compile([["2002::", 16]])[0];
|
|
10689
|
+
function embeddedV4(bytes) {
|
|
10690
|
+
if (inNet(bytes, MAPPED_V4[0], MAPPED_V4[1]) || inNet(bytes, NAT64[0], NAT64[1])) {
|
|
10691
|
+
return bytes.subarray(12, 16);
|
|
10692
|
+
}
|
|
10693
|
+
if (inNet(bytes, SIXTOFOUR[0], SIXTOFOUR[1])) return bytes.subarray(2, 6);
|
|
10694
|
+
return null;
|
|
10695
|
+
}
|
|
10696
|
+
function addressIsPrivate(address) {
|
|
10697
|
+
const bytes = parseAddress(address);
|
|
10698
|
+
if (!bytes) return true;
|
|
10699
|
+
if (bytes.length === 4) return V4_NETS.some(([net, prefix]) => inNet(bytes, net, prefix));
|
|
10700
|
+
const embedded = embeddedV4(bytes);
|
|
10701
|
+
if (embedded && V4_NETS.some(([net, prefix]) => inNet(embedded, net, prefix))) return true;
|
|
10702
|
+
return V6_NETS.some(([net, prefix]) => inNet(bytes, net, prefix));
|
|
10703
|
+
}
|
|
10704
|
+
async function isPrivateAddress(host) {
|
|
10705
|
+
let name = (host ?? "").trim().toLowerCase().replace(/^\.+|\.+$/g, "");
|
|
10706
|
+
if (!name) return true;
|
|
10707
|
+
if (name === "localhost") return true;
|
|
10708
|
+
if (name.startsWith("[") && name.endsWith("]")) name = name.slice(1, -1);
|
|
10709
|
+
if (net.isIP(name)) return addressIsPrivate(name);
|
|
10710
|
+
let addresses;
|
|
10711
|
+
try {
|
|
10712
|
+
addresses = await resolver.lookup(name);
|
|
10713
|
+
} catch {
|
|
10714
|
+
return true;
|
|
10715
|
+
}
|
|
10716
|
+
if (!addresses.length) return true;
|
|
10717
|
+
return addresses.some((address) => addressIsPrivate(address.split("%")[0] ?? address));
|
|
10718
|
+
}
|
|
10719
|
+
async function assertEgressAllowed(url, opts) {
|
|
10720
|
+
let parsed;
|
|
10721
|
+
try {
|
|
10722
|
+
parsed = new URL(url ?? "");
|
|
10723
|
+
} catch {
|
|
10724
|
+
throw new EgressDenied(`egress denied: ${JSON.stringify(url)} is not a valid URL`);
|
|
10725
|
+
}
|
|
10726
|
+
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
10727
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
10728
|
+
throw new EgressDenied(
|
|
10729
|
+
`egress denied: unsupported URL scheme ${JSON.stringify(scheme)} \u2014 http tools may only reach http/https`
|
|
10730
|
+
);
|
|
10731
|
+
}
|
|
10732
|
+
if (opts.allowPrivate) return;
|
|
10733
|
+
const host = parsed.hostname;
|
|
10734
|
+
if (await isPrivateAddress(host)) {
|
|
10735
|
+
throw new EgressDenied(
|
|
10736
|
+
`egress denied: ${host || "(no host)"} is a private, loopback, link-local or metadata address; set allowPrivateEgress to permit it`
|
|
10737
|
+
);
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
9827
10740
|
|
|
9828
10741
|
// src/tools/governance.ts
|
|
9829
10742
|
init_db();
|
|
9830
10743
|
|
|
9831
10744
|
// src/tools/executors/http.ts
|
|
10745
|
+
init_errors();
|
|
9832
10746
|
var TIMEOUT_MS5 = 3e4;
|
|
9833
10747
|
var SENSITIVE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
9834
10748
|
"set-cookie",
|
|
@@ -9837,6 +10751,8 @@ var SENSITIVE_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
|
9837
10751
|
"proxy-authenticate",
|
|
9838
10752
|
"www-authenticate"
|
|
9839
10753
|
]);
|
|
10754
|
+
var REDIRECT_STATUS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
10755
|
+
var CROSS_ORIGIN_HEADERS = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie"]);
|
|
9840
10756
|
function percentEncode(value) {
|
|
9841
10757
|
return encodeURIComponent(value).replace(
|
|
9842
10758
|
/[!'()*]/g,
|
|
@@ -9867,10 +10783,38 @@ function withQuery(url, params) {
|
|
|
9867
10783
|
if (!q) return url;
|
|
9868
10784
|
return url.includes("?") ? `${url}&${q}` : `${url}?${q}`;
|
|
9869
10785
|
}
|
|
10786
|
+
async function readCapped(resp) {
|
|
10787
|
+
if (!resp.body) return { text: await resp.text(), truncated: false };
|
|
10788
|
+
const reader = resp.body.getReader();
|
|
10789
|
+
const chunks = [];
|
|
10790
|
+
let size = 0;
|
|
10791
|
+
let truncated = false;
|
|
10792
|
+
for (; ; ) {
|
|
10793
|
+
const { done, value } = await reader.read();
|
|
10794
|
+
if (done) break;
|
|
10795
|
+
if (value) {
|
|
10796
|
+
chunks.push(Buffer.from(value));
|
|
10797
|
+
size += value.byteLength;
|
|
10798
|
+
}
|
|
10799
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
10800
|
+
truncated = true;
|
|
10801
|
+
await reader.cancel();
|
|
10802
|
+
break;
|
|
10803
|
+
}
|
|
10804
|
+
}
|
|
10805
|
+
const bytes = Buffer.concat(chunks);
|
|
10806
|
+
return {
|
|
10807
|
+
text: (truncated ? bytes.subarray(0, MAX_RESPONSE_BYTES) : bytes).toString("utf8"),
|
|
10808
|
+
truncated
|
|
10809
|
+
};
|
|
10810
|
+
}
|
|
9870
10811
|
async function defaultRequest(init) {
|
|
9871
10812
|
let url = init.url;
|
|
9872
10813
|
if (init.params) url = withQuery(url, init.params);
|
|
9873
10814
|
const headers = { ...init.headers ?? {} };
|
|
10815
|
+
if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
|
|
10816
|
+
headers["Accept-Encoding"] = "identity";
|
|
10817
|
+
}
|
|
9874
10818
|
let body;
|
|
9875
10819
|
if (init.json !== void 0) {
|
|
9876
10820
|
headers["Content-Type"] = headers["Content-Type"] ?? headers["content-type"] ?? "application/json";
|
|
@@ -9882,25 +10826,47 @@ async function defaultRequest(init) {
|
|
|
9882
10826
|
method: init.method,
|
|
9883
10827
|
headers,
|
|
9884
10828
|
body,
|
|
9885
|
-
|
|
10829
|
+
// Redirects are followed by hand in `executeHttp` — an automatic follow
|
|
10830
|
+
// would jump to a destination no egress check ever saw.
|
|
10831
|
+
redirect: "manual",
|
|
9886
10832
|
signal: AbortSignal.timeout(TIMEOUT_MS5)
|
|
9887
10833
|
});
|
|
9888
10834
|
const respHeaders = {};
|
|
9889
10835
|
resp.headers.forEach((v, k) => {
|
|
9890
10836
|
respHeaders[k] = v;
|
|
9891
10837
|
});
|
|
9892
|
-
const text = await resp
|
|
10838
|
+
const { text, truncated } = await readCapped(resp);
|
|
9893
10839
|
return {
|
|
9894
10840
|
status: resp.status,
|
|
9895
10841
|
headers: respHeaders,
|
|
10842
|
+
truncated,
|
|
9896
10843
|
async json() {
|
|
9897
|
-
return JSON.parse(text);
|
|
10844
|
+
return JSON.parse(text.replace(/^/, ""));
|
|
9898
10845
|
},
|
|
9899
10846
|
async text() {
|
|
9900
10847
|
return text;
|
|
9901
10848
|
}
|
|
9902
10849
|
};
|
|
9903
10850
|
}
|
|
10851
|
+
function nextHop(init, status, location) {
|
|
10852
|
+
const current = new URL(init.url);
|
|
10853
|
+
const target = new URL(location, current);
|
|
10854
|
+
let method = init.method;
|
|
10855
|
+
if ((status === 302 || status === 303) && method !== "HEAD") method = "GET";
|
|
10856
|
+
else if (status === 301 && method === "POST") method = "GET";
|
|
10857
|
+
let headers = { ...init.headers ?? {} };
|
|
10858
|
+
if (current.origin !== target.origin) {
|
|
10859
|
+
headers = Object.fromEntries(
|
|
10860
|
+
Object.entries(headers).filter(([k]) => !CROSS_ORIGIN_HEADERS.has(k.toLowerCase()))
|
|
10861
|
+
);
|
|
10862
|
+
}
|
|
10863
|
+
const hop = { method, url: target.toString(), headers };
|
|
10864
|
+
if (method === init.method) {
|
|
10865
|
+
if (init.json !== void 0) hop.json = init.json;
|
|
10866
|
+
if (init.content !== void 0) hop.content = init.content;
|
|
10867
|
+
}
|
|
10868
|
+
return hop;
|
|
10869
|
+
}
|
|
9904
10870
|
async function executeHttp(config, args, opts = {}) {
|
|
9905
10871
|
const method = String(config.method ?? "GET").toUpperCase();
|
|
9906
10872
|
let url = config.url;
|
|
@@ -9921,7 +10887,7 @@ async function executeHttp(config, args, opts = {}) {
|
|
|
9921
10887
|
}
|
|
9922
10888
|
}
|
|
9923
10889
|
}
|
|
9924
|
-
|
|
10890
|
+
let requestInit = {
|
|
9925
10891
|
method,
|
|
9926
10892
|
url: url ?? "",
|
|
9927
10893
|
headers
|
|
@@ -9957,29 +10923,64 @@ async function executeHttp(config, args, opts = {}) {
|
|
|
9957
10923
|
}
|
|
9958
10924
|
}
|
|
9959
10925
|
const client = opts.client;
|
|
9960
|
-
const
|
|
10926
|
+
const allowPrivate = opts.allowPrivate ?? false;
|
|
10927
|
+
let resp;
|
|
10928
|
+
let hops = 0;
|
|
10929
|
+
for (; ; ) {
|
|
10930
|
+
await assertEgressAllowed(requestInit.url, { allowPrivate });
|
|
10931
|
+
resp = client ? await client.request(requestInit) : await defaultRequest(requestInit);
|
|
10932
|
+
const location = resp.headers.location ?? resp.headers.Location;
|
|
10933
|
+
if (!REDIRECT_STATUS.has(resp.status) || !location) break;
|
|
10934
|
+
if (hops >= MAX_REDIRECTS) {
|
|
10935
|
+
throw new exports.EngineActionError(`too many redirects (more than ${MAX_REDIRECTS}) starting at ${url}`);
|
|
10936
|
+
}
|
|
10937
|
+
hops += 1;
|
|
10938
|
+
requestInit = nextHop(requestInit, resp.status, location);
|
|
10939
|
+
}
|
|
9961
10940
|
const contentType = resp.headers["content-type"] ?? resp.headers["Content-Type"] ?? "";
|
|
9962
|
-
let
|
|
10941
|
+
let text = "";
|
|
9963
10942
|
try {
|
|
9964
|
-
|
|
10943
|
+
text = await resp.text();
|
|
9965
10944
|
} catch {
|
|
9966
|
-
|
|
10945
|
+
text = "";
|
|
10946
|
+
}
|
|
10947
|
+
const bytes = Buffer.from(text, "utf8");
|
|
10948
|
+
const truncated = resp.truncated === true || bytes.byteLength > MAX_RESPONSE_BYTES;
|
|
10949
|
+
let data;
|
|
10950
|
+
if (truncated) {
|
|
10951
|
+
data = bytes.subarray(0, MAX_RESPONSE_BYTES).toString("utf8");
|
|
10952
|
+
} else {
|
|
10953
|
+
try {
|
|
10954
|
+
data = contentType.includes("application/json") ? await resp.json() : text;
|
|
10955
|
+
} catch {
|
|
10956
|
+
data = text;
|
|
10957
|
+
}
|
|
9967
10958
|
}
|
|
9968
10959
|
const safeHeaders = {};
|
|
9969
10960
|
for (const [k, v] of Object.entries(resp.headers)) {
|
|
9970
10961
|
if (!SENSITIVE_RESPONSE_HEADERS.has(k.toLowerCase())) safeHeaders[k] = v;
|
|
9971
10962
|
}
|
|
9972
|
-
|
|
10963
|
+
const result = {
|
|
9973
10964
|
status_code: resp.status,
|
|
9974
10965
|
headers: safeHeaders,
|
|
9975
10966
|
data
|
|
9976
10967
|
};
|
|
10968
|
+
if (truncated) result.truncated = true;
|
|
10969
|
+
return result;
|
|
9977
10970
|
}
|
|
9978
10971
|
|
|
9979
10972
|
// src/version.ts
|
|
9980
10973
|
var __version__ = "0.0.0";
|
|
9981
10974
|
|
|
9982
10975
|
// src/tools/executors/mcp-client.ts
|
|
10976
|
+
async function checkEgress(url, allowPrivate) {
|
|
10977
|
+
let checked = url;
|
|
10978
|
+
const scheme = (/^([a-z0-9+.-]+):/i.exec(url ?? "")?.[1] ?? "").toLowerCase();
|
|
10979
|
+
if (scheme === "ws") checked = `http:${url.slice(scheme.length + 1)}`;
|
|
10980
|
+
else if (scheme === "wss") checked = `https:${url.slice(scheme.length + 1)}`;
|
|
10981
|
+
await assertEgressAllowed(checked, { allowPrivate });
|
|
10982
|
+
}
|
|
10983
|
+
var REDIRECT_MESSAGE = "MCP server redirected; register the final URL";
|
|
9983
10984
|
var MCPError = class _MCPError extends Error {
|
|
9984
10985
|
code;
|
|
9985
10986
|
data;
|
|
@@ -10008,10 +11009,12 @@ function parseSseBuffer(raw) {
|
|
|
10008
11009
|
var HttpTransport = class {
|
|
10009
11010
|
kind = "http";
|
|
10010
11011
|
url;
|
|
11012
|
+
allowPrivate;
|
|
10011
11013
|
baseHeaders;
|
|
10012
11014
|
sessionId = null;
|
|
10013
|
-
constructor(url, headers) {
|
|
11015
|
+
constructor(url, headers, allowPrivate = false) {
|
|
10014
11016
|
this.url = url;
|
|
11017
|
+
this.allowPrivate = allowPrivate;
|
|
10015
11018
|
this.baseHeaders = {
|
|
10016
11019
|
...headers,
|
|
10017
11020
|
"Content-Type": "application/json",
|
|
@@ -10019,6 +11022,7 @@ var HttpTransport = class {
|
|
|
10019
11022
|
};
|
|
10020
11023
|
}
|
|
10021
11024
|
async connect() {
|
|
11025
|
+
await checkEgress(this.url, this.allowPrivate);
|
|
10022
11026
|
}
|
|
10023
11027
|
async close() {
|
|
10024
11028
|
this.sessionId = null;
|
|
@@ -10032,10 +11036,17 @@ var HttpTransport = class {
|
|
|
10032
11036
|
const resp = await fetch(this.url, {
|
|
10033
11037
|
method: "POST",
|
|
10034
11038
|
headers: this.buildHeaders(),
|
|
10035
|
-
body: JSON.stringify(msg)
|
|
11039
|
+
body: JSON.stringify(msg),
|
|
11040
|
+
// `redirect: "manual"`: a followed redirect would carry this POST —
|
|
11041
|
+
// headers, Authorization and all — to a destination `connect()`'s
|
|
11042
|
+
// check never saw. A 3xx is reported, never chased.
|
|
11043
|
+
redirect: "manual"
|
|
10036
11044
|
});
|
|
10037
11045
|
const sid = resp.headers.get("Mcp-Session-Id");
|
|
10038
11046
|
if (sid) this.sessionId = sid;
|
|
11047
|
+
if (isRedirect(resp)) {
|
|
11048
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
11049
|
+
}
|
|
10039
11050
|
if (resp.status >= 400) {
|
|
10040
11051
|
const body = await resp.text();
|
|
10041
11052
|
console.error(`[HTTP Transport] ${resp.status} from ${this.url}: ${body.slice(0, 300)}`);
|
|
@@ -10068,11 +11079,20 @@ var SseTransport = class {
|
|
|
10068
11079
|
eventsUrl;
|
|
10069
11080
|
baseUrl;
|
|
10070
11081
|
headers;
|
|
11082
|
+
allowPrivate;
|
|
11083
|
+
/** Where the server told us to POST — it arrives in an `endpoint` event on
|
|
11084
|
+
* the stream, so it is server-chosen and hence checked. Private: the only
|
|
11085
|
+
* way in is the parser, which is the path a real server takes. */
|
|
10071
11086
|
postUrl = null;
|
|
11087
|
+
/** The last POST endpoint cleared by the egress check. The check costs a
|
|
11088
|
+
* DNS resolution and the endpoint changes at most once per connection, so
|
|
11089
|
+
* it is not repeated per message. */
|
|
11090
|
+
checkedPostUrl = null;
|
|
10072
11091
|
queue = [];
|
|
10073
11092
|
waiters = [];
|
|
10074
11093
|
abort = null;
|
|
10075
|
-
constructor(url, headers) {
|
|
11094
|
+
constructor(url, headers, allowPrivate = false) {
|
|
11095
|
+
this.allowPrivate = allowPrivate;
|
|
10076
11096
|
this.eventsUrl = url.replace(/\/$/, "");
|
|
10077
11097
|
if (url.endsWith("/events")) this.baseUrl = url.slice(0, -7);
|
|
10078
11098
|
else if (url.endsWith("/sse")) this.baseUrl = url.slice(0, -4);
|
|
@@ -10085,14 +11105,19 @@ var SseTransport = class {
|
|
|
10085
11105
|
else this.queue.push(msg);
|
|
10086
11106
|
}
|
|
10087
11107
|
async connect() {
|
|
11108
|
+
await checkEgress(this.eventsUrl, this.allowPrivate);
|
|
10088
11109
|
this.abort = new AbortController();
|
|
10089
11110
|
void this.listen();
|
|
10090
11111
|
}
|
|
10091
11112
|
async listen() {
|
|
10092
11113
|
const resp = await fetch(this.eventsUrl, {
|
|
10093
11114
|
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
10094
|
-
signal: this.abort?.signal
|
|
11115
|
+
signal: this.abort?.signal,
|
|
11116
|
+
redirect: "manual"
|
|
10095
11117
|
});
|
|
11118
|
+
if (isRedirect(resp)) {
|
|
11119
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
11120
|
+
}
|
|
10096
11121
|
if (!resp.ok) throw new Error(`SSE ${resp.status}`);
|
|
10097
11122
|
if (!resp.body) throw new Error("SSE response has no body");
|
|
10098
11123
|
const reader = resp.body.getReader();
|
|
@@ -10144,11 +11169,19 @@ var SseTransport = class {
|
|
|
10144
11169
|
}
|
|
10145
11170
|
}
|
|
10146
11171
|
const postUrl = this.postUrl || this.baseUrl;
|
|
11172
|
+
if (postUrl !== this.checkedPostUrl) {
|
|
11173
|
+
await checkEgress(postUrl, this.allowPrivate);
|
|
11174
|
+
this.checkedPostUrl = postUrl;
|
|
11175
|
+
}
|
|
10147
11176
|
const resp = await fetch(postUrl, {
|
|
10148
11177
|
method: "POST",
|
|
10149
11178
|
headers: { ...this.headers, "Content-Type": "application/json" },
|
|
10150
|
-
body: JSON.stringify(msg)
|
|
11179
|
+
body: JSON.stringify(msg),
|
|
11180
|
+
redirect: "manual"
|
|
10151
11181
|
});
|
|
11182
|
+
if (isRedirect(resp)) {
|
|
11183
|
+
throw new MCPError(resp.status, REDIRECT_MESSAGE);
|
|
11184
|
+
}
|
|
10152
11185
|
if (!resp.ok) throw new Error(`SSE POST ${resp.status}`);
|
|
10153
11186
|
try {
|
|
10154
11187
|
const data = await resp.json();
|
|
@@ -10167,14 +11200,17 @@ var WsTransport = class {
|
|
|
10167
11200
|
kind = "websocket";
|
|
10168
11201
|
url;
|
|
10169
11202
|
headers;
|
|
11203
|
+
allowPrivate;
|
|
10170
11204
|
ws = null;
|
|
10171
11205
|
queue = [];
|
|
10172
11206
|
waiters = [];
|
|
10173
|
-
constructor(url, headers) {
|
|
11207
|
+
constructor(url, headers, allowPrivate = false) {
|
|
10174
11208
|
this.url = url;
|
|
11209
|
+
this.allowPrivate = allowPrivate;
|
|
10175
11210
|
this.headers = headers;
|
|
10176
11211
|
}
|
|
10177
11212
|
async connect() {
|
|
11213
|
+
await checkEgress(this.url, this.allowPrivate);
|
|
10178
11214
|
const WS = globalThis.WebSocket;
|
|
10179
11215
|
if (!WS) throw new Error("WebSocket is not available in this runtime");
|
|
10180
11216
|
this.ws = new WS(this.url);
|
|
@@ -10215,21 +11251,27 @@ var MCPClient = class _MCPClient {
|
|
|
10215
11251
|
tools = [];
|
|
10216
11252
|
resources = [];
|
|
10217
11253
|
headers;
|
|
10218
|
-
|
|
11254
|
+
allowPrivate;
|
|
11255
|
+
constructor(url, headers = null, opts = {}) {
|
|
10219
11256
|
this.url = url;
|
|
10220
11257
|
this.headers = headers ?? {};
|
|
10221
|
-
this.
|
|
11258
|
+
this.allowPrivate = opts.allowPrivate ?? false;
|
|
11259
|
+
this.transport = _MCPClient.createTransport(url, this.headers, this.allowPrivate);
|
|
10222
11260
|
}
|
|
10223
|
-
static createTransport(url, headers) {
|
|
11261
|
+
static createTransport(url, headers, allowPrivate = false) {
|
|
10224
11262
|
if (url.startsWith("stdio:") || url === "stdio") {
|
|
10225
11263
|
throw new Error("stdio MCP transport is not supported (cloud deployments use SSE/HTTP/WebSocket)");
|
|
10226
11264
|
}
|
|
10227
|
-
if (url.startsWith("ws://") || url.startsWith("wss://"))
|
|
11265
|
+
if (url.startsWith("ws://") || url.startsWith("wss://")) {
|
|
11266
|
+
return new WsTransport(url, headers, allowPrivate);
|
|
11267
|
+
}
|
|
10228
11268
|
if (url.startsWith("sse+http://") || url.startsWith("sse+https://") || url.endsWith("/events") || url.endsWith("/sse")) {
|
|
10229
11269
|
const clean = url.replace("sse+http://", "http://").replace("sse+https://", "https://");
|
|
10230
|
-
return new SseTransport(clean, headers);
|
|
11270
|
+
return new SseTransport(clean, headers, allowPrivate);
|
|
11271
|
+
}
|
|
11272
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
11273
|
+
return new HttpTransport(url, headers, allowPrivate);
|
|
10231
11274
|
}
|
|
10232
|
-
if (url.startsWith("http://") || url.startsWith("https://")) return new HttpTransport(url, headers);
|
|
10233
11275
|
throw new Error(`Unsupported URL: ${url} (use http(s)://, ws(s)://, or .../sse)`);
|
|
10234
11276
|
}
|
|
10235
11277
|
nextId() {
|
|
@@ -10281,7 +11323,15 @@ var MCPClient = class _MCPClient {
|
|
|
10281
11323
|
await this.transport.send({ jsonrpc: "2.0", method, params: params ?? {} });
|
|
10282
11324
|
}
|
|
10283
11325
|
async connect() {
|
|
10284
|
-
|
|
11326
|
+
try {
|
|
11327
|
+
await this.transport.connect();
|
|
11328
|
+
} catch (e) {
|
|
11329
|
+
try {
|
|
11330
|
+
await this.transport.close();
|
|
11331
|
+
} catch {
|
|
11332
|
+
}
|
|
11333
|
+
throw e;
|
|
11334
|
+
}
|
|
10285
11335
|
if (!(this.transport instanceof HttpTransport)) {
|
|
10286
11336
|
this.connected = true;
|
|
10287
11337
|
void this.recvLoop();
|
|
@@ -10364,7 +11414,7 @@ var MCPClient = class _MCPClient {
|
|
|
10364
11414
|
}
|
|
10365
11415
|
this.id = 0;
|
|
10366
11416
|
this.pending.clear();
|
|
10367
|
-
this.transport = _MCPClient.createTransport(url, headers);
|
|
11417
|
+
this.transport = _MCPClient.createTransport(url, headers, this.allowPrivate);
|
|
10368
11418
|
await this.connect();
|
|
10369
11419
|
await this.listTools();
|
|
10370
11420
|
}
|
|
@@ -10372,11 +11422,11 @@ var MCPClient = class _MCPClient {
|
|
|
10372
11422
|
var MCPRegistry = class {
|
|
10373
11423
|
clients = /* @__PURE__ */ new Map();
|
|
10374
11424
|
healthTask = null;
|
|
10375
|
-
async connect(name, url, headers = null, token = null) {
|
|
11425
|
+
async connect(name, url, headers = null, token = null, allowPrivate = false) {
|
|
10376
11426
|
this.clients.delete(name);
|
|
10377
11427
|
const hdrs = { ...headers ?? {} };
|
|
10378
11428
|
if (token) hdrs.Authorization = hdrs.Authorization ?? `Bearer ${token}`;
|
|
10379
|
-
const client = new MCPClient(url, hdrs);
|
|
11429
|
+
const client = new MCPClient(url, hdrs, { allowPrivate });
|
|
10380
11430
|
await client.connect();
|
|
10381
11431
|
await client.listTools();
|
|
10382
11432
|
this.clients.set(name, client);
|
|
@@ -10428,8 +11478,18 @@ var MCPRegistry = class {
|
|
|
10428
11478
|
};
|
|
10429
11479
|
var PromptevMCP = class {
|
|
10430
11480
|
clients = new MCPRegistry();
|
|
11481
|
+
allowPrivate;
|
|
11482
|
+
/**
|
|
11483
|
+
* `allowPrivate` is the operator knob `allowPrivateEgress`, carried down to
|
|
11484
|
+
* every transport this facade builds. It defaults to `false`, so a
|
|
11485
|
+
* construction site that forgets to pass it denies private destinations
|
|
11486
|
+
* rather than permitting them.
|
|
11487
|
+
*/
|
|
11488
|
+
constructor(opts = {}) {
|
|
11489
|
+
this.allowPrivate = opts.allowPrivate ?? false;
|
|
11490
|
+
}
|
|
10431
11491
|
async addServer(name, url, opts = {}) {
|
|
10432
|
-
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null);
|
|
11492
|
+
await this.clients.connect(name, url, opts.headers ?? null, opts.token ?? null, this.allowPrivate);
|
|
10433
11493
|
}
|
|
10434
11494
|
async shutdown() {
|
|
10435
11495
|
await this.clients.shutdown();
|
|
@@ -10563,8 +11623,13 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
10563
11623
|
"requires_approval",
|
|
10564
11624
|
"approvalPolicy",
|
|
10565
11625
|
"approval_policy",
|
|
10566
|
-
"enabled"
|
|
11626
|
+
"enabled",
|
|
11627
|
+
"metaData",
|
|
11628
|
+
"meta_data"
|
|
10567
11629
|
]);
|
|
11630
|
+
function allowPrivateEgress(engine) {
|
|
11631
|
+
return Boolean(engine?.config?.allowPrivateEgress);
|
|
11632
|
+
}
|
|
10568
11633
|
function toolVisible(ct, principals) {
|
|
10569
11634
|
return aclVisible(ct.acl, principals);
|
|
10570
11635
|
}
|
|
@@ -10646,11 +11711,19 @@ function redactToolResult(result, policy, opts) {
|
|
|
10646
11711
|
if (failed.length) note.rules_failed = failed;
|
|
10647
11712
|
return [redacted, note];
|
|
10648
11713
|
}
|
|
10649
|
-
function
|
|
10650
|
-
|
|
10651
|
-
if (
|
|
10652
|
-
|
|
11714
|
+
function cryptoKey(engine) {
|
|
11715
|
+
const key = getSecretKey(engine.config);
|
|
11716
|
+
if (key.length !== 32) {
|
|
11717
|
+
throw new Error(`CE_SECRET_KEY is malformed: decoded to ${key.length} bytes, need 32`);
|
|
10653
11718
|
}
|
|
11719
|
+
return key;
|
|
11720
|
+
}
|
|
11721
|
+
function decryptRowConfig(row, engine) {
|
|
11722
|
+
if (!row.config_encrypted) return {};
|
|
11723
|
+
return decryptDict(String(row.config_encrypted), cryptoKey(engine));
|
|
11724
|
+
}
|
|
11725
|
+
function rowToToolConfig(row, engine, config) {
|
|
11726
|
+
config ??= decryptRowConfig(row, engine);
|
|
10654
11727
|
return new ToolConfig({
|
|
10655
11728
|
id: String(row.id),
|
|
10656
11729
|
name: String(row.name),
|
|
@@ -10661,9 +11734,24 @@ function rowToToolConfig(row, engine) {
|
|
|
10661
11734
|
acl: row.acl != null ? [...row.acl] : null,
|
|
10662
11735
|
requiresApproval: Boolean(row.requires_approval),
|
|
10663
11736
|
approvalPolicy: row.approval_policy ?? {},
|
|
10664
|
-
enabled: Boolean(row.enabled)
|
|
11737
|
+
enabled: Boolean(row.enabled),
|
|
11738
|
+
metaData: row.meta_data ?? {}
|
|
10665
11739
|
});
|
|
10666
11740
|
}
|
|
11741
|
+
function rowToToolConfigLenient(row, engine) {
|
|
11742
|
+
if (!row.config_encrypted) return rowToToolConfig(row, engine, {});
|
|
11743
|
+
const key = cryptoKey(engine);
|
|
11744
|
+
let config;
|
|
11745
|
+
try {
|
|
11746
|
+
config = decryptDict(String(row.config_encrypted), key);
|
|
11747
|
+
} catch {
|
|
11748
|
+
console.warn(
|
|
11749
|
+
`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`
|
|
11750
|
+
);
|
|
11751
|
+
return null;
|
|
11752
|
+
}
|
|
11753
|
+
return rowToToolConfig(row, engine, config);
|
|
11754
|
+
}
|
|
10667
11755
|
async function loadPersistedCanonicals(engine, sourceId) {
|
|
10668
11756
|
const params = [];
|
|
10669
11757
|
let sql = `SELECT * FROM context_engine_tools WHERE enabled IS TRUE`;
|
|
@@ -10674,7 +11762,9 @@ async function loadPersistedCanonicals(engine, sourceId) {
|
|
|
10674
11762
|
const result = await engine.pool.query(sql, params);
|
|
10675
11763
|
const canonicals = [];
|
|
10676
11764
|
for (const row of result.rows) {
|
|
10677
|
-
|
|
11765
|
+
const tc = rowToToolConfigLenient(row, engine);
|
|
11766
|
+
if (tc === null) continue;
|
|
11767
|
+
canonicals.push(...canonicalFromConfig(tc));
|
|
10678
11768
|
}
|
|
10679
11769
|
return canonicals;
|
|
10680
11770
|
}
|
|
@@ -10694,13 +11784,18 @@ function canonicalToPublic(ct) {
|
|
|
10694
11784
|
async function registerTool(engine, tc) {
|
|
10695
11785
|
canonicalFromConfig(tc);
|
|
10696
11786
|
const config = tc.config ?? {};
|
|
10697
|
-
|
|
11787
|
+
if (containsSentinel(config)) {
|
|
11788
|
+
throw new ConfigTemplateError(
|
|
11789
|
+
`config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a redacted template cannot be registered as a new tool; re-enter the secret values`
|
|
11790
|
+
);
|
|
11791
|
+
}
|
|
11792
|
+
const configEncrypted = Object.keys(config).length ? encryptDict(config, cryptoKey(engine)) : null;
|
|
10698
11793
|
const id = crypto.randomUUID();
|
|
10699
11794
|
await engine.pool.query(
|
|
10700
11795
|
`INSERT INTO context_engine_tools
|
|
10701
11796
|
(id, name, kind, description, source_id, acl, config_encrypted,
|
|
10702
|
-
requires_approval, approval_policy, enabled)
|
|
10703
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
|
|
11797
|
+
requires_approval, approval_policy, enabled, meta_data)
|
|
11798
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11::jsonb)`,
|
|
10704
11799
|
[
|
|
10705
11800
|
id,
|
|
10706
11801
|
tc.name,
|
|
@@ -10711,7 +11806,8 @@ async function registerTool(engine, tc) {
|
|
|
10711
11806
|
configEncrypted,
|
|
10712
11807
|
tc.requiresApproval,
|
|
10713
11808
|
JSON.stringify(tc.approvalPolicy ?? {}),
|
|
10714
|
-
tc.enabled
|
|
11809
|
+
tc.enabled,
|
|
11810
|
+
JSON.stringify(tc.metaData ?? {})
|
|
10715
11811
|
]
|
|
10716
11812
|
);
|
|
10717
11813
|
return id;
|
|
@@ -10728,12 +11824,41 @@ async function updateTool(engine, id, opts) {
|
|
|
10728
11824
|
let i = 1;
|
|
10729
11825
|
for (const [key, value] of Object.entries(fields)) {
|
|
10730
11826
|
if (key === "config") {
|
|
11827
|
+
let config = value;
|
|
11828
|
+
if (config && containsSentinel(config)) {
|
|
11829
|
+
if (Object.hasOwn(fields, "kind") && fields.kind !== row.kind) {
|
|
11830
|
+
throw new ConfigTemplateError(
|
|
11831
|
+
"cannot change kind and keep redacted secrets in one PATCH \u2014 re-enter the config in full"
|
|
11832
|
+
);
|
|
11833
|
+
}
|
|
11834
|
+
if (config.body_secret === false && containsSentinel(config.body)) {
|
|
11835
|
+
throw new ConfigTemplateError(
|
|
11836
|
+
`body_secret cannot be turned off while the body still contains ${JSON.stringify(REDACTED_SENTINEL)} \u2014 re-enter the body to declassify it`
|
|
11837
|
+
);
|
|
11838
|
+
}
|
|
11839
|
+
if (!row.config_encrypted) {
|
|
11840
|
+
throw new ConfigTemplateError(
|
|
11841
|
+
`config contains ${JSON.stringify(REDACTED_SENTINEL)} but this tool has no stored config to keep \u2014 re-enter the secret values`
|
|
11842
|
+
);
|
|
11843
|
+
}
|
|
11844
|
+
const key2 = cryptoKey(engine);
|
|
11845
|
+
let stored;
|
|
11846
|
+
try {
|
|
11847
|
+
stored = decryptDict(String(row.config_encrypted), key2);
|
|
11848
|
+
} catch (exc) {
|
|
11849
|
+
throw new ConfigTemplateError(
|
|
11850
|
+
`the stored config cannot be decrypted (was the secret key rotated?) \u2014 re-enter the config in full, without ${JSON.stringify(REDACTED_SENTINEL)} values`,
|
|
11851
|
+
{ cause: exc }
|
|
11852
|
+
);
|
|
11853
|
+
}
|
|
11854
|
+
config = mergeRedacted(config, stored);
|
|
11855
|
+
}
|
|
10731
11856
|
sets.push(`config_encrypted = $${i++}`);
|
|
10732
|
-
params.push(
|
|
11857
|
+
params.push(config && Object.keys(config).length ? encryptDict(config, cryptoKey(engine)) : null);
|
|
10733
11858
|
} else if (UPDATABLE_COLUMNS.has(key)) {
|
|
10734
|
-
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key;
|
|
11859
|
+
const col = key === "sourceId" ? "source_id" : key === "requiresApproval" ? "requires_approval" : key === "approvalPolicy" ? "approval_policy" : key === "metaData" ? "meta_data" : key;
|
|
10735
11860
|
sets.push(`${col} = $${i++}`);
|
|
10736
|
-
params.push(col === "approval_policy" ? JSON.stringify(value ?? {}) : value);
|
|
11861
|
+
params.push(col === "approval_policy" || col === "meta_data" ? JSON.stringify(value ?? {}) : value);
|
|
10737
11862
|
}
|
|
10738
11863
|
}
|
|
10739
11864
|
sets.push(`updated_at = now()`);
|
|
@@ -10742,7 +11867,8 @@ async function updateTool(engine, id, opts) {
|
|
|
10742
11867
|
`UPDATE context_engine_tools SET ${sets.join(", ")} WHERE id = $${i} RETURNING *`,
|
|
10743
11868
|
params
|
|
10744
11869
|
);
|
|
10745
|
-
|
|
11870
|
+
const row_ = updated.rows[0];
|
|
11871
|
+
return rowToToolConfigLenient(row_, engine) ?? rowToToolConfig(row_, engine, {});
|
|
10746
11872
|
}
|
|
10747
11873
|
async function deleteTool(engine, id, opts = {}) {
|
|
10748
11874
|
const existing = await engine.pool.query(`SELECT * FROM context_engine_tools WHERE id = $1`, [id]);
|
|
@@ -10828,17 +11954,38 @@ function probeFailure(exc, kind) {
|
|
|
10828
11954
|
console.warn(`test_tool(kind=${kind}) failed:`, exc, "->", category);
|
|
10829
11955
|
return { ok: false, error: category };
|
|
10830
11956
|
}
|
|
10831
|
-
async function testTool(
|
|
11957
|
+
async function testTool(engine, tc) {
|
|
10832
11958
|
const kind = tc.kind;
|
|
10833
11959
|
const config = tc.config ?? {};
|
|
11960
|
+
if (containsSentinel(config)) {
|
|
11961
|
+
throw new ConfigTemplateError(
|
|
11962
|
+
`config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a probe would send it verbatim as the credential; re-enter the secret values`
|
|
11963
|
+
);
|
|
11964
|
+
}
|
|
10834
11965
|
if (kind === "http") {
|
|
10835
11966
|
const url = config.url;
|
|
10836
11967
|
if (!url) return { ok: false, error: "http config missing 'url'" };
|
|
10837
11968
|
try {
|
|
10838
|
-
|
|
11969
|
+
await assertEgressAllowed(url, { allowPrivate: allowPrivateEgress(engine) });
|
|
11970
|
+
} catch (exc) {
|
|
11971
|
+
if (exc instanceof EgressDenied) {
|
|
11972
|
+
console.warn(`testTool(kind=http) refused: ${exc.message}`);
|
|
11973
|
+
return { ok: false, error: "egress_denied" };
|
|
11974
|
+
}
|
|
11975
|
+
throw exc;
|
|
11976
|
+
}
|
|
11977
|
+
const client = engine?._toolHttpClient ?? null;
|
|
11978
|
+
try {
|
|
11979
|
+
const resp = client ? await client.request({
|
|
11980
|
+
method: "HEAD",
|
|
11981
|
+
url,
|
|
11982
|
+
headers: config.headers ?? {}
|
|
11983
|
+
}) : await fetch(url, {
|
|
10839
11984
|
method: "HEAD",
|
|
10840
11985
|
headers: config.headers ?? {},
|
|
10841
|
-
|
|
11986
|
+
// No automatic follow: a redirect would reach a destination the
|
|
11987
|
+
// check above never saw.
|
|
11988
|
+
redirect: "manual",
|
|
10842
11989
|
signal: AbortSignal.timeout(1e4)
|
|
10843
11990
|
});
|
|
10844
11991
|
return { ok: resp.status < 500, status_code: resp.status };
|
|
@@ -10859,12 +12006,16 @@ async function testTool(_engine, tc) {
|
|
|
10859
12006
|
const url = config.url;
|
|
10860
12007
|
if (!url) return { ok: false, error: "mcp config missing 'url'" };
|
|
10861
12008
|
const token = config.oauth_token ?? config.bearer ?? config.access_token;
|
|
10862
|
-
const mcp = new PromptevMCP();
|
|
12009
|
+
const mcp = new PromptevMCP({ allowPrivate: allowPrivateEgress(engine) });
|
|
10863
12010
|
try {
|
|
10864
12011
|
await mcp.addServer(tc.name, url, { token: token ?? null });
|
|
10865
12012
|
const client = mcp.clients.get(tc.name);
|
|
10866
12013
|
return { ok: true, tools: client.tools.map((t) => t.name) };
|
|
10867
12014
|
} catch (exc) {
|
|
12015
|
+
if (exc instanceof EgressDenied) {
|
|
12016
|
+
console.warn(`testTool(kind=mcp) refused: ${exc.message}`);
|
|
12017
|
+
return { ok: false, error: "egress_denied" };
|
|
12018
|
+
}
|
|
10868
12019
|
return probeFailure(exc, "mcp");
|
|
10869
12020
|
} finally {
|
|
10870
12021
|
await mcp.shutdown();
|
|
@@ -10872,11 +12023,20 @@ async function testTool(_engine, tc) {
|
|
|
10872
12023
|
}
|
|
10873
12024
|
return { ok: false, error: `test_tool does not support kind=${JSON.stringify(kind)}` };
|
|
10874
12025
|
}
|
|
10875
|
-
async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
12026
|
+
async function findAndClaimApproved(engine, toolName, args, sourceId, principals, approvalScope) {
|
|
10876
12027
|
const claimedAt = /* @__PURE__ */ new Date();
|
|
12028
|
+
const params = [toolName];
|
|
12029
|
+
let scopeSql2 = "approval_scope IS NULL";
|
|
12030
|
+
if (approvalScope !== null) {
|
|
12031
|
+
params.push(approvalScope);
|
|
12032
|
+
scopeSql2 = `approval_scope = $${params.length}`;
|
|
12033
|
+
}
|
|
12034
|
+
const wall = claimVisibilitySql(principals, params.length + 1);
|
|
12035
|
+
params.push(...wall.params);
|
|
10877
12036
|
const candidates = await engine.pool.query(
|
|
10878
|
-
`SELECT * FROM context_engine_tool_approvals
|
|
10879
|
-
|
|
12037
|
+
`SELECT * FROM context_engine_tool_approvals
|
|
12038
|
+
WHERE tool_name = $1 AND status = 'approved' AND ${scopeSql2} AND ${wall.sql}`,
|
|
12039
|
+
params
|
|
10880
12040
|
);
|
|
10881
12041
|
for (const row of candidates.rows) {
|
|
10882
12042
|
if ((row.source_id ?? null) !== (sourceId ?? null)) continue;
|
|
@@ -10893,7 +12053,7 @@ async function findAndClaimApproved(engine, toolName, args, sourceId) {
|
|
|
10893
12053
|
}
|
|
10894
12054
|
return null;
|
|
10895
12055
|
}
|
|
10896
|
-
async function dispatchMcp(ct, config, args) {
|
|
12056
|
+
async function dispatchMcp(ct, config, args, opts = {}) {
|
|
10897
12057
|
const url = config.url;
|
|
10898
12058
|
if (!url) throw new exports.EngineActionError(`mcp tool ${ct.callName} config missing 'url'`);
|
|
10899
12059
|
const headers = { ...config.headers ?? {} };
|
|
@@ -10902,7 +12062,7 @@ async function dispatchMcp(ct, config, args) {
|
|
|
10902
12062
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
10903
12063
|
else if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
10904
12064
|
const toolName = String(ct.raw?.tool_name ?? ct.displayName);
|
|
10905
|
-
const mcp = new PromptevMCP();
|
|
12065
|
+
const mcp = new PromptevMCP({ allowPrivate: opts.allowPrivate ?? false });
|
|
10906
12066
|
try {
|
|
10907
12067
|
await mcp.addServer(ct.callName, url, { headers: Object.keys(headers).length ? headers : null });
|
|
10908
12068
|
const client = mcp.clients.get(ct.callName);
|
|
@@ -10913,7 +12073,10 @@ async function dispatchMcp(ct, config, args) {
|
|
|
10913
12073
|
}
|
|
10914
12074
|
async function dispatch(engine, ct, config, args) {
|
|
10915
12075
|
if (ct.kind === "http") {
|
|
10916
|
-
return executeHttp(config, args, {
|
|
12076
|
+
return executeHttp(config, args, {
|
|
12077
|
+
client: engine._toolHttpClient ?? null,
|
|
12078
|
+
allowPrivate: allowPrivateEgress(engine)
|
|
12079
|
+
});
|
|
10917
12080
|
}
|
|
10918
12081
|
if (ct.kind === "db") {
|
|
10919
12082
|
const query = String(args.query ?? "");
|
|
@@ -10928,7 +12091,9 @@ async function dispatch(engine, ct, config, args) {
|
|
|
10928
12091
|
}
|
|
10929
12092
|
return result;
|
|
10930
12093
|
}
|
|
10931
|
-
if (ct.kind === "mcp")
|
|
12094
|
+
if (ct.kind === "mcp") {
|
|
12095
|
+
return dispatchMcp(ct, config, args, { allowPrivate: allowPrivateEgress(engine) });
|
|
12096
|
+
}
|
|
10932
12097
|
if (ct.kind === "function") {
|
|
10933
12098
|
const fn = ct.raw?.callable;
|
|
10934
12099
|
if (!fn) throw new exports.EngineActionError(`function tool ${ct.callName} has no callable`);
|
|
@@ -10949,10 +12114,20 @@ async function decryptCtConfig(engine, toolId) {
|
|
|
10949
12114
|
]);
|
|
10950
12115
|
const row = result.rows[0];
|
|
10951
12116
|
if (!row?.config_encrypted) return {};
|
|
10952
|
-
return decryptDict(String(row.config_encrypted),
|
|
12117
|
+
return decryptDict(String(row.config_encrypted), cryptoKey(engine));
|
|
12118
|
+
}
|
|
12119
|
+
var warnedUnscoped = /* @__PURE__ */ new Set();
|
|
12120
|
+
function warnUnscopedApproval(callName) {
|
|
12121
|
+
if (warnedUnscoped.has(callName)) return;
|
|
12122
|
+
warnedUnscoped.add(callName);
|
|
12123
|
+
process.emitWarning(
|
|
12124
|
+
`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.`,
|
|
12125
|
+
{ type: "DeprecationWarning", code: "CE_APPROVAL_SCOPE_MISSING" }
|
|
12126
|
+
);
|
|
10953
12127
|
}
|
|
10954
12128
|
async function executeTool(engine, callName, args, opts = {}) {
|
|
10955
12129
|
const runtimeArgs = args ?? {};
|
|
12130
|
+
const approvalScope = validateApprovalScope(opts.approvalScope);
|
|
10956
12131
|
const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
|
|
10957
12132
|
if (!ct) throw new exports.EngineActionError(`tool not found: ${callName}`);
|
|
10958
12133
|
if (!toolVisible(ct, opts.principals ?? null)) {
|
|
@@ -10961,22 +12136,32 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
10961
12136
|
const publicArgs = stripUnderscoreArgs(runtimeArgs);
|
|
10962
12137
|
let approvalId = null;
|
|
10963
12138
|
if (shouldRequireApproval(ct, publicArgs)) {
|
|
10964
|
-
|
|
12139
|
+
if (approvalScope === null) warnUnscopedApproval(callName);
|
|
12140
|
+
approvalId = await findAndClaimApproved(
|
|
12141
|
+
engine,
|
|
12142
|
+
ct.callName,
|
|
12143
|
+
publicArgs,
|
|
12144
|
+
opts.sourceId ?? null,
|
|
12145
|
+
opts.principals ?? null,
|
|
12146
|
+
approvalScope
|
|
12147
|
+
);
|
|
10965
12148
|
if (approvalId == null) {
|
|
10966
|
-
const
|
|
12149
|
+
const pendingOpts = {
|
|
10967
12150
|
toolName: ct.callName,
|
|
10968
12151
|
args: publicArgs,
|
|
10969
12152
|
sourceId: opts.sourceId ?? null,
|
|
10970
12153
|
principals: opts.principals ?? null,
|
|
10971
12154
|
policy: ct.approvalPolicy ?? {}
|
|
10972
|
-
}
|
|
12155
|
+
};
|
|
12156
|
+
const record = approvalScope === null ? await createPending(engine, pendingOpts) : await findOrCreatePending(engine, { ...pendingOpts, approvalScope });
|
|
10973
12157
|
const reason = ct.requiresApproval ? "tool requires approval" : `approval policy condition met: ${ct.approvalPolicy?.condition}`;
|
|
10974
12158
|
return {
|
|
10975
12159
|
approval_required: {
|
|
10976
12160
|
approval_id: String(record.id),
|
|
10977
12161
|
tool_name: ct.callName,
|
|
10978
12162
|
args: publicArgs,
|
|
10979
|
-
reason
|
|
12163
|
+
reason,
|
|
12164
|
+
expires_at: record.expiresAt ? record.expiresAt.toISOString() : null
|
|
10980
12165
|
}
|
|
10981
12166
|
};
|
|
10982
12167
|
}
|
|
@@ -11001,8 +12186,9 @@ async function executeTool(engine, callName, args, opts = {}) {
|
|
|
11001
12186
|
let truncated = false;
|
|
11002
12187
|
if (success) {
|
|
11003
12188
|
try {
|
|
12189
|
+
const redactPrincipals = opts.principals === TRUSTED ? null : opts.principals ?? null;
|
|
11004
12190
|
const [redacted] = redactToolResult(rawResult, engine.config.redaction, {
|
|
11005
|
-
principals:
|
|
12191
|
+
principals: redactPrincipals,
|
|
11006
12192
|
secretKey: engine.config.secretKey,
|
|
11007
12193
|
hooks: engine.hooks
|
|
11008
12194
|
});
|
|
@@ -11101,11 +12287,19 @@ var ContextEngine = class {
|
|
|
11101
12287
|
_graphStore = null;
|
|
11102
12288
|
constructor(config, opts = {}) {
|
|
11103
12289
|
this.config = config;
|
|
11104
|
-
this.hooks = {
|
|
12290
|
+
this.hooks = {
|
|
12291
|
+
onUsage: opts.onUsage ?? null,
|
|
12292
|
+
onError: opts.onError ?? null,
|
|
12293
|
+
onProgress: opts.onProgress ?? null
|
|
12294
|
+
};
|
|
11105
12295
|
}
|
|
11106
12296
|
async ensurePool() {
|
|
11107
12297
|
if (!this._pool) {
|
|
11108
|
-
this._pool = await createPool(this.config.databaseUrl
|
|
12298
|
+
this._pool = await createPool(this.config.databaseUrl, {
|
|
12299
|
+
max: this.config.storage.poolMax,
|
|
12300
|
+
idleTimeoutMillis: this.config.storage.poolIdleTimeoutMs,
|
|
12301
|
+
connectionTimeoutMillis: this.config.storage.poolConnectionTimeoutMs
|
|
12302
|
+
});
|
|
11109
12303
|
this.pool = this._pool;
|
|
11110
12304
|
}
|
|
11111
12305
|
if (!this.backend) {
|
|
@@ -11291,7 +12485,9 @@ var ContextEngine = class {
|
|
|
11291
12485
|
graphStore: await this.getGraphStore(),
|
|
11292
12486
|
hooks: this.hooks,
|
|
11293
12487
|
sourceIds: opts.sourceIds ?? null,
|
|
11294
|
-
|
|
12488
|
+
documentIds: opts.documentIds ?? null,
|
|
12489
|
+
principals,
|
|
12490
|
+
backend: this.backend
|
|
11295
12491
|
});
|
|
11296
12492
|
}
|
|
11297
12493
|
return runSearch(query, {
|
|
@@ -11301,6 +12497,7 @@ var ContextEngine = class {
|
|
|
11301
12497
|
embedder: this.embedder,
|
|
11302
12498
|
pool,
|
|
11303
12499
|
sourceIds: opts.sourceIds,
|
|
12500
|
+
documentIds: opts.documentIds,
|
|
11304
12501
|
principals,
|
|
11305
12502
|
topK: opts.topK,
|
|
11306
12503
|
mode: opts.mode,
|
|
@@ -11406,7 +12603,7 @@ var ContextEngine = class {
|
|
|
11406
12603
|
hooks: this.hooks,
|
|
11407
12604
|
sourceIds: opts.sourceIds,
|
|
11408
12605
|
principals: resolvePrincipals(opts.principals, "compute"),
|
|
11409
|
-
|
|
12606
|
+
documentIds: opts.documentIds,
|
|
11410
12607
|
modelCfg: opts.modelCfg,
|
|
11411
12608
|
timeout: opts.timeout
|
|
11412
12609
|
});
|
|
@@ -11459,7 +12656,8 @@ var ContextEngine = class {
|
|
|
11459
12656
|
sourceId: opts.sourceId,
|
|
11460
12657
|
principals: resolvePrincipals(opts.principals, "executeTool"),
|
|
11461
12658
|
actor: opts.actor,
|
|
11462
|
-
source: opts.source ?? "api"
|
|
12659
|
+
source: opts.source ?? "api",
|
|
12660
|
+
approvalScope: opts.approvalScope
|
|
11463
12661
|
});
|
|
11464
12662
|
}
|
|
11465
12663
|
};
|
|
@@ -11472,18 +12670,83 @@ init_hooks();
|
|
|
11472
12670
|
// src/mcp.ts
|
|
11473
12671
|
init_errors();
|
|
11474
12672
|
init_extras();
|
|
11475
|
-
|
|
11476
|
-
|
|
12673
|
+
var HandlerError = class extends Error {
|
|
12674
|
+
status;
|
|
12675
|
+
detail;
|
|
12676
|
+
constructor(status, detail) {
|
|
12677
|
+
super(typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
12678
|
+
this.name = "HandlerError";
|
|
12679
|
+
this.status = status;
|
|
12680
|
+
this.detail = detail;
|
|
12681
|
+
}
|
|
12682
|
+
};
|
|
12683
|
+
zod.z.object({
|
|
12684
|
+
text: zod.z.string(),
|
|
12685
|
+
name: zod.z.string(),
|
|
12686
|
+
source_id: zod.z.string().nullable().optional(),
|
|
12687
|
+
external_id: zod.z.string().nullable().optional(),
|
|
12688
|
+
description: zod.z.string().nullable().optional(),
|
|
12689
|
+
meta_data: zod.z.record(zod.z.unknown()).nullable().optional(),
|
|
12690
|
+
acl: zod.z.array(zod.z.string()).nullable().optional(),
|
|
12691
|
+
mode: zod.z.enum(["hybrid", "graph"]).nullable().optional(),
|
|
12692
|
+
extract_structured: zod.z.boolean().optional().default(false),
|
|
12693
|
+
batch: zod.z.boolean().optional().default(false)
|
|
12694
|
+
}).strip();
|
|
12695
|
+
zod.z.object({
|
|
12696
|
+
acl: zod.z.array(zod.z.string()).nullable().optional(),
|
|
12697
|
+
name: zod.z.string().nullable().optional(),
|
|
12698
|
+
description: zod.z.string().nullable().optional(),
|
|
12699
|
+
meta_data: zod.z.record(zod.z.unknown()).nullable().optional()
|
|
12700
|
+
}).strict();
|
|
12701
|
+
zod.z.object({
|
|
12702
|
+
query: zod.z.string(),
|
|
12703
|
+
source_ids: zod.z.array(zod.z.string()).nullable().optional(),
|
|
12704
|
+
// Narrows WITHIN a source and INTERSECTS with source_ids — it can only
|
|
12705
|
+
// shrink the result set (the ACL predicate still applies in the same SQL
|
|
12706
|
+
// conjunction), so exposing it needs no authorizeAcl-style grant check.
|
|
12707
|
+
document_ids: zod.z.array(zod.z.string()).nullable().optional(),
|
|
12708
|
+
top_k: zod.z.number().int().optional().default(10),
|
|
12709
|
+
mode: zod.z.enum(["hybrid", "graph"]).optional().default("hybrid"),
|
|
12710
|
+
compress_to_tokens: zod.z.number().int().nullable().optional()
|
|
12711
|
+
}).strip();
|
|
12712
|
+
var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12713
|
+
function requireValidUuid(documentId) {
|
|
12714
|
+
if (!UUID_RE5.test(String(documentId))) {
|
|
12715
|
+
throw new HandlerError(400, `invalid document id: ${JSON.stringify(documentId)}`);
|
|
12716
|
+
}
|
|
12717
|
+
}
|
|
12718
|
+
function resolveRequestPrincipals(value, opts) {
|
|
12719
|
+
if (value === TRUSTED) return TRUSTED;
|
|
12720
|
+
if (value == null) {
|
|
12721
|
+
throw new HandlerError(
|
|
12722
|
+
500,
|
|
12723
|
+
`${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.`
|
|
12724
|
+
);
|
|
12725
|
+
}
|
|
12726
|
+
if (!Array.isArray(value) || value.some((p) => typeof p !== "string")) {
|
|
12727
|
+
throw new HandlerError(
|
|
12728
|
+
500,
|
|
12729
|
+
`${opts.surface}: the \`principals\` dependency must return a list of principal strings, [] for an anonymous caller, or TRUSTED \u2014 got ${typeof value}.`
|
|
12730
|
+
);
|
|
12731
|
+
}
|
|
12732
|
+
return value;
|
|
12733
|
+
}
|
|
11477
12734
|
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.";
|
|
11478
12735
|
async function resolvePrincipalsFn(principals) {
|
|
11479
12736
|
const result = await Promise.resolve(principals());
|
|
11480
|
-
|
|
12737
|
+
try {
|
|
12738
|
+
return resolveRequestPrincipals(result, { surface: "the MCP `principals` dependency" });
|
|
12739
|
+
} catch (exc) {
|
|
12740
|
+
if (exc instanceof HandlerError) throw new Error(exc.message, { cause: exc });
|
|
12741
|
+
throw exc;
|
|
12742
|
+
}
|
|
11481
12743
|
}
|
|
11482
|
-
function registerToolGateway(mcp, engine, principals) {
|
|
12744
|
+
function registerToolGateway(mcp, engine, principals, approvalScope = null) {
|
|
11483
12745
|
mcp.tool(
|
|
11484
12746
|
"search_tools",
|
|
11485
12747
|
"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,
|
|
11486
|
-
|
|
12748
|
+
// Zod raw shape — see the note on the search tool in mcp.ts.
|
|
12749
|
+
{ query: zod.z.string(), limit: zod.z.number().int().optional() },
|
|
11487
12750
|
async (...raw) => {
|
|
11488
12751
|
const args = raw[0] ?? {};
|
|
11489
12752
|
const callerPrincipals = await resolvePrincipalsFn(principals);
|
|
@@ -11497,13 +12760,15 @@ function registerToolGateway(mcp, engine, principals) {
|
|
|
11497
12760
|
mcp.tool(
|
|
11498
12761
|
"execute_tool",
|
|
11499
12762
|
"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,
|
|
11500
|
-
{ name:
|
|
12763
|
+
{ name: zod.z.string(), args: zod.z.record(zod.z.unknown()).optional() },
|
|
11501
12764
|
async (...raw) => {
|
|
11502
12765
|
const body = raw[0] ?? {};
|
|
11503
12766
|
const callerPrincipals = await resolvePrincipalsFn(principals);
|
|
12767
|
+
const scope = approvalScope ? await Promise.resolve(approvalScope()) : null;
|
|
11504
12768
|
return engine.executeTool(String(body.name), body.args ?? null, {
|
|
11505
12769
|
principals: callerPrincipals,
|
|
11506
|
-
source: "mcp"
|
|
12770
|
+
source: "mcp",
|
|
12771
|
+
approvalScope: scope
|
|
11507
12772
|
});
|
|
11508
12773
|
}
|
|
11509
12774
|
);
|
|
@@ -11545,15 +12810,24 @@ async function createMcpApp(engine, opts) {
|
|
|
11545
12810
|
"search_knowledge_base",
|
|
11546
12811
|
`Hybrid search (full-text + trigram + vector, RRF-fused) over the ingested corpus. Returns the top-matching chunks with their source document.${SCOPE_NOTE}`,
|
|
11547
12812
|
{
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
12813
|
+
// Zod RAW SHAPES, not JSON schema: the SDK's isZodRawShape test
|
|
12814
|
+
// rejects a plain schema object — on current SDK versions that made
|
|
12815
|
+
// registration THROW at startup, and on 1.12.0 the object was consumed
|
|
12816
|
+
// as annotations and every handler ran with NO arguments.
|
|
12817
|
+
query: zod.z.string(),
|
|
12818
|
+
source_ids: zod.z.array(zod.z.string()).optional(),
|
|
12819
|
+
document_ids: zod.z.array(zod.z.string()).optional(),
|
|
12820
|
+
top_k: zod.z.number().int().optional(),
|
|
12821
|
+
mode: zod.z.enum(["hybrid", "graph"]).optional()
|
|
11552
12822
|
},
|
|
11553
12823
|
async (args) => {
|
|
12824
|
+
for (const did of args.document_ids ?? []) {
|
|
12825
|
+
requireValidUuid(did);
|
|
12826
|
+
}
|
|
11554
12827
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11555
12828
|
const result = await engine.search(String(args.query ?? ""), {
|
|
11556
12829
|
sourceIds: args.source_ids,
|
|
12830
|
+
documentIds: args.document_ids,
|
|
11557
12831
|
principals: callerPrincipals,
|
|
11558
12832
|
topK: args.top_k ?? 10,
|
|
11559
12833
|
mode: args.mode ?? "hybrid"
|
|
@@ -11575,7 +12849,7 @@ async function createMcpApp(engine, opts) {
|
|
|
11575
12849
|
tool(
|
|
11576
12850
|
"get_document",
|
|
11577
12851
|
`Fetch one document's full text and metadata by id.${SCOPE_NOTE}`,
|
|
11578
|
-
{ document_id:
|
|
12852
|
+
{ document_id: zod.z.string() },
|
|
11579
12853
|
async (args) => {
|
|
11580
12854
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11581
12855
|
return engine.getDocument(String(args.document_id), { principals: callerPrincipals });
|
|
@@ -11585,7 +12859,7 @@ async function createMcpApp(engine, opts) {
|
|
|
11585
12859
|
tool(
|
|
11586
12860
|
"query_structured",
|
|
11587
12861
|
`Answer a question against documents' extracted structured data (only documents with structured data are candidates).${SCOPE_NOTE}`,
|
|
11588
|
-
{ question:
|
|
12862
|
+
{ question: zod.z.string(), source_ids: zod.z.array(zod.z.string()).optional() },
|
|
11589
12863
|
async (args) => {
|
|
11590
12864
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11591
12865
|
if (!engine.queryStructured) throw new Error("queryStructured is not available on this engine");
|
|
@@ -11599,7 +12873,7 @@ async function createMcpApp(engine, opts) {
|
|
|
11599
12873
|
tool(
|
|
11600
12874
|
"compute",
|
|
11601
12875
|
`Run LLM-authored JavaScript over in-scope spreadsheet documents and return the computed result.${SCOPE_NOTE}`,
|
|
11602
|
-
{ instruction:
|
|
12876
|
+
{ instruction: zod.z.string(), source_ids: zod.z.array(zod.z.string()).optional() },
|
|
11603
12877
|
async (args) => {
|
|
11604
12878
|
const callerPrincipals = await resolvePrincipalsFn(opts.principals);
|
|
11605
12879
|
if (!engine.compute) throw new Error("compute is not available on this engine");
|
|
@@ -11618,7 +12892,8 @@ async function createMcpApp(engine, opts) {
|
|
|
11618
12892
|
}
|
|
11619
12893
|
},
|
|
11620
12894
|
engine,
|
|
11621
|
-
opts.principals
|
|
12895
|
+
opts.principals,
|
|
12896
|
+
opts.approvalScope ?? null
|
|
11622
12897
|
);
|
|
11623
12898
|
const handler = (async (req, res) => {
|
|
11624
12899
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
@@ -11631,6 +12906,9 @@ async function createMcpApp(engine, opts) {
|
|
|
11631
12906
|
|
|
11632
12907
|
// src/providers/index.ts
|
|
11633
12908
|
init_llm();
|
|
12909
|
+
|
|
12910
|
+
// src/index.ts
|
|
12911
|
+
init_redaction();
|
|
11634
12912
|
var InProcessRunner = class {
|
|
11635
12913
|
registry = /* @__PURE__ */ new Map();
|
|
11636
12914
|
/** Kept so the Promise isn't garbage-collected mid-flight. */
|
|
@@ -11684,14 +12962,11 @@ init_usage();
|
|
|
11684
12962
|
|
|
11685
12963
|
exports.CeleryRunner = CeleryRunner;
|
|
11686
12964
|
exports.ContextEngine = ContextEngine;
|
|
11687
|
-
exports.ContextEngineConfig = ContextEngineConfig;
|
|
11688
12965
|
exports.DEFAULT_LEG_WEIGHT = DEFAULT_LEG_WEIGHT;
|
|
11689
12966
|
exports.EXTRACTION_VERSION = EXTRACTION_VERSION;
|
|
11690
12967
|
exports.Embedder = Embedder;
|
|
11691
12968
|
exports.InProcessRunner = InProcessRunner;
|
|
11692
12969
|
exports.PostgresBackend = PostgresBackend;
|
|
11693
|
-
exports.RedactionPolicy = RedactionPolicy;
|
|
11694
|
-
exports.RedactionRule = RedactionRule;
|
|
11695
12970
|
exports.TRUSTED = TRUSTED;
|
|
11696
12971
|
exports.ToolConfig = ToolConfig;
|
|
11697
12972
|
exports.UNSET = UNSET;
|
|
@@ -11705,6 +12980,7 @@ exports.configSchema = configSchema;
|
|
|
11705
12980
|
exports.createMcpApp = createMcpApp;
|
|
11706
12981
|
exports.decryptDict = decryptDict;
|
|
11707
12982
|
exports.emitError = emitError;
|
|
12983
|
+
exports.emitProgress = emitProgress;
|
|
11708
12984
|
exports.emitToolCall = emitToolCall;
|
|
11709
12985
|
exports.emitUsage = emitUsage;
|
|
11710
12986
|
exports.encryptDict = encryptDict;
|